# -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2018 Kresimir Tusek * # * Copyright (c) 2019-2021 Schildkroet * # * * # * This file is part of the FreeCAD CAx development system. * # * * # * This library is free software; you can redistribute it and/or * # * modify it under the terms of the GNU Library General Public * # * License as published by the Free Software Foundation; either * # * version 2 of the License, or (at your option) any later version. * # * * # * This library is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # * GNU Library General Public License for more details. * # * * # * You should have received a copy of the GNU Library General Public * # * License along with this library; see the file COPYING.LIB. If not, * # * write to the Free Software Foundation, Inc., 59 Temple Place, * # * Suite 330, Boston, MA 02111-1307, USA * # * * # *************************************************************************** import PathScripts.PathOp as PathOp import PathScripts.PathUtils as PathUtils import PathScripts.PathLog as PathLog import PathScripts.PathGeom as PathGeom import Path import FreeCAD import time import json import math import area from PySide import QtCore # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader Part = LazyLoader('Part', globals(), 'Part') # TechDraw = LazyLoader('TechDraw', globals(), 'TechDraw') FeatureExtensions = LazyLoader('PathScripts.PathFeatureExtensions', globals(), 'PathScripts.PathFeatureExtensions') DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') if FreeCAD.GuiUp: from pivy import coin import FreeCADGui __doc__ = "Class and implementation of the Adaptive path operation." PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) # PathLog.trackModule(PathLog.thisModule()) # Qt translation handling def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) def convertTo2d(pathArray): output = [] for path in pathArray: pth2 = [] for edge in path: for pt in edge: pth2.append([pt[0], pt[1]]) output.append(pth2) return output sceneGraph = None scenePathNodes = [] # for scene cleanup aftewards topZ = 10 def sceneDrawPath(path, color=(0, 0, 1)): coPoint = coin.SoCoordinate3() pts = [] for pt in path: pts.append([pt[0], pt[1], topZ]) coPoint.point.setValues(0, len(pts), pts) ma = coin.SoBaseColor() ma.rgb = color li = coin.SoLineSet() li.numVertices.setValue(len(pts)) pathNode = coin.SoSeparator() pathNode.addChild(coPoint) pathNode.addChild(ma) pathNode.addChild(li) sceneGraph.addChild(pathNode) scenePathNodes.append(pathNode) # for scene cleanup afterwards def sceneClean(): for n in scenePathNodes: sceneGraph.removeChild(n) del scenePathNodes[:] def discretize(edge, flipDirection=False): pts = edge.discretize(Deflection=0.002) if flipDirection: pts.reverse() return pts def CalcHelixConePoint(height, cur_z, radius, angle): x = ((height - cur_z) / height) * radius * math.cos(math.radians(angle)*cur_z) y = ((height - cur_z) / height) * radius * math.sin(math.radians(angle)*cur_z) z = cur_z return {'X': x, 'Y': y, 'Z': z} def GenerateGCode(op, obj, adaptiveResults, helixDiameter): # pylint: disable=unused-argument if len(adaptiveResults) == 0 or len(adaptiveResults[0]["AdaptivePaths"]) == 0: return # minLiftDistance = op.tool.Diameter helixRadius = 0 for region in adaptiveResults: p1 = region["HelixCenterPoint"] p2 = region["StartPoint"] r = math.sqrt((p1[0]-p2[0]) * (p1[0]-p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1])) if r > helixRadius: helixRadius = r stepDown = obj.StepDown.Value passStartDepth = obj.StartDepth.Value if stepDown < 0.1: stepDown = 0.1 length = 2*math.pi * helixRadius if float(obj.HelixAngle) < 1: obj.HelixAngle = 1 if float(obj.HelixAngle) > 89: obj.HelixAngle = 89 if float(obj.HelixConeAngle) < 0: obj.HelixConeAngle = 0 helixAngleRad = math.pi * float(obj.HelixAngle) / 180.0 depthPerOneCircle = length * math.tan(helixAngleRad) # print("Helix circle depth: {}".format(depthPerOneCircle)) stepUp = obj.LiftDistance.Value if stepUp < 0: stepUp = 0 finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 if finish_step > stepDown: finish_step = stepDown depth_params = PathUtils.depth_params( clearance_height=obj.ClearanceHeight.Value, safe_height=obj.SafeHeight.Value, start_depth=obj.StartDepth.Value, step_down=stepDown, z_finish_step=finish_step, final_depth=obj.FinalDepth.Value, user_depths=None) # ml: this is dangerous because it'll hide all unused variables hence forward # however, I don't know what lx and ly signify so I'll leave them for now # pylint: disable=unused-variable # lx = adaptiveResults[0]["HelixCenterPoint"][0] # ly = adaptiveResults[0]["HelixCenterPoint"][1] lz = passStartDepth step = 0 for passEndDepth in depth_params.data: step = step + 1 for region in adaptiveResults: startAngle = math.atan2(region["StartPoint"][1] - region["HelixCenterPoint"][1], region["StartPoint"][0] - region["HelixCenterPoint"][0]) # lx = region["HelixCenterPoint"][0] # ly = region["HelixCenterPoint"][1] passDepth = (passStartDepth - passEndDepth) p1 = region["HelixCenterPoint"] p2 = region["StartPoint"] helixRadius = math.sqrt((p1[0]-p2[0]) * (p1[0]-p2[0]) + (p1[1]-p2[1]) * (p1[1]-p2[1])) # Helix ramp if helixRadius > 0.01: r = helixRadius - 0.01 maxfi = passDepth / depthPerOneCircle * 2 * math.pi fi = 0 offsetFi = -maxfi + startAngle-math.pi/16 helixStart = [region["HelixCenterPoint"][0] + r * math.cos(offsetFi), region["HelixCenterPoint"][1] + r * math.sin(offsetFi)] op.commandlist.append(Path.Command("(Helix to depth: %f)" % passEndDepth)) if obj.UseHelixArcs is False: # rapid move to start point op.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value})) op.commandlist.append(Path.Command("G0", {"X": helixStart[0], "Y": helixStart[1], "Z": obj.ClearanceHeight.Value})) # rapid move to safe height op.commandlist.append(Path.Command("G0", {"X": helixStart[0], "Y": helixStart[1], "Z": obj.SafeHeight.Value})) # move to start depth op.commandlist.append(Path.Command("G1", {"X": helixStart[0], "Y": helixStart[1], "Z": passStartDepth, "F": op.vertFeed})) if obj.HelixConeAngle == 0: while fi < maxfi: x = region["HelixCenterPoint"][0] + r * math.cos(fi+offsetFi) y = region["HelixCenterPoint"][1] + r * math.sin(fi+offsetFi) z = passStartDepth - fi / maxfi * (passStartDepth - passEndDepth) op.commandlist.append(Path.Command("G1", {"X": x, "Y": y, "Z": z, "F": op.vertFeed})) # lx = x # ly = y fi = fi + math.pi / 16 # one more circle at target depth to make sure center is cleared maxfi = maxfi + 2*math.pi while fi < maxfi: x = region["HelixCenterPoint"][0] + r * math.cos(fi+offsetFi) y = region["HelixCenterPoint"][1] + r * math.sin(fi+offsetFi) z = passEndDepth op.commandlist.append(Path.Command("G1", {"X": x, "Y": y, "Z": z, "F": op.horizFeed})) # lx = x # ly = y fi = fi + math.pi/16 else: # Cone _HelixAngle = 360 - (float(obj.HelixAngle) * 4) if obj.HelixConeAngle > 6: obj.HelixConeAngle = 6 helixRadius *= 0.9 # Calculate everything helix_height = passStartDepth - passEndDepth r_extra = helix_height * math.tan(math.radians(obj.HelixConeAngle)) HelixTopRadius = helixRadius + r_extra helix_full_height = HelixTopRadius * (math.cos(math.radians(obj.HelixConeAngle)) / math.sin(math.radians(obj.HelixConeAngle))) # Start height z = passStartDepth i = 0 # Default step down z_step = 0.05 # Bigger angle, smaller step down if _HelixAngle > 120: z_step = 0.025 if _HelixAngle > 240: z_step = 0.015 p = None # Calculate conical helix while(z >= passEndDepth): if z < passEndDepth: z = passEndDepth p = CalcHelixConePoint(helix_full_height, i, HelixTopRadius, _HelixAngle) op.commandlist.append(Path.Command("G1", {"X": p['X'] + region["HelixCenterPoint"][0], "Y": p['Y'] + region["HelixCenterPoint"][1], "Z": z, "F": op.vertFeed})) z = z - z_step i = i + z_step # Calculate some stuff for arcs at bottom p['X'] = p['X'] + region["HelixCenterPoint"][0] p['Y'] = p['Y'] + region["HelixCenterPoint"][1] x_m = region["HelixCenterPoint"][0] - p['X'] + region["HelixCenterPoint"][0] y_m = region["HelixCenterPoint"][1] - p['Y'] + region["HelixCenterPoint"][1] i_off = (x_m - p['X']) / 2 j_off = (y_m - p['Y']) / 2 # One more circle at target depth to make sure center is cleared op.commandlist.append(Path.Command("G3", {"X": x_m, "Y": y_m, "Z": passEndDepth, "I": i_off, "J": j_off, "F": op.horizFeed})) op.commandlist.append(Path.Command("G3", {"X": p['X'], "Y": p['Y'], "Z": passEndDepth, "I": -i_off, "J": -j_off, "F": op.horizFeed})) else: # Use arcs for helix - no conical shape support helixStart = [region["HelixCenterPoint"][0] + r, region["HelixCenterPoint"][1]] # rapid move to start point op.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value})) op.commandlist.append(Path.Command("G0", {"X": helixStart[0], "Y": helixStart[1], "Z": obj.ClearanceHeight.Value})) # rapid move to safe height op.commandlist.append(Path.Command("G0", {"X": helixStart[0], "Y": helixStart[1], "Z": obj.SafeHeight.Value})) # move to start depth op.commandlist.append(Path.Command("G1", {"X": helixStart[0], "Y": helixStart[1], "Z": passStartDepth, "F": op.vertFeed})) x = region["HelixCenterPoint"][0] + r y = region["HelixCenterPoint"][1] curDep = passStartDepth while curDep > (passEndDepth + depthPerOneCircle): op.commandlist.append(Path.Command("G2", {"X": x - (2*r), "Y": y, "Z": curDep - (depthPerOneCircle/2), "I": -r, "F": op.vertFeed})) op.commandlist.append(Path.Command("G2", {"X": x, "Y": y, "Z": curDep - depthPerOneCircle, "I": r, "F": op.vertFeed})) curDep = curDep - depthPerOneCircle lastStep = curDep - passEndDepth if lastStep > (depthPerOneCircle/2): op.commandlist.append(Path.Command("G2", {"X": x - (2*r), "Y": y, "Z": curDep - (lastStep/2), "I": -r, "F": op.vertFeed})) op.commandlist.append(Path.Command("G2", {"X": x, "Y": y, "Z": passEndDepth, "I": r, "F": op.vertFeed})) else: op.commandlist.append(Path.Command("G2", {"X": x - (2*r), "Y": y, "Z": passEndDepth, "I": -r, "F": op.vertFeed})) op.commandlist.append(Path.Command("G1", {"X": x, "Y": y, "Z": passEndDepth, "F": op.vertFeed})) # one more circle at target depth to make sure center is cleared op.commandlist.append(Path.Command("G2", {"X": x - (2*r), "Y": y, "Z": passEndDepth, "I": -r, "F": op.horizFeed})) op.commandlist.append(Path.Command("G2", {"X": x, "Y": y, "Z": passEndDepth, "I": r, "F": op.horizFeed})) # lx = x # ly = y else: # no helix entry # rapid move to clearance height op.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value})) op.commandlist.append(Path.Command("G0", {"X": region["StartPoint"][0], "Y": region["StartPoint"][1], "Z": obj.ClearanceHeight.Value})) # straight plunge to target depth op.commandlist.append(Path.Command("G1", {"X": region["StartPoint"][0], "Y": region["StartPoint"][1], "Z": passEndDepth, "F": op.vertFeed})) lz = passEndDepth z = obj.ClearanceHeight.Value op.commandlist.append(Path.Command("(Adaptive - depth: %f)" % passEndDepth)) # add adaptive paths for pth in region["AdaptivePaths"]: motionType = pth[0] # [0] contains motion type for pt in pth[1]: # [1] contains list of points x = pt[0] y = pt[1] # dist = math.sqrt((x-lx)*(x-lx) + (y-ly)*(y-ly)) if motionType == area.AdaptiveMotionType.Cutting: z = passEndDepth if z != lz: op.commandlist.append(Path.Command("G1", {"Z": z, "F": op.vertFeed})) op.commandlist.append(Path.Command("G1", {"X": x, "Y": y, "F": op.horizFeed})) elif motionType == area.AdaptiveMotionType.LinkClear: z = passEndDepth + stepUp if z != lz: op.commandlist.append(Path.Command("G0", {"Z": z})) op.commandlist.append(Path.Command("G0", {"X": x, "Y": y})) elif motionType == area.AdaptiveMotionType.LinkNotClear: z = obj.ClearanceHeight.Value if z != lz: op.commandlist.append(Path.Command("G0", {"Z": z})) op.commandlist.append(Path.Command("G0", {"X": x, "Y": y})) # elif motionType == area.AdaptiveMotionType.LinkClearAtPrevPass: # if lx!=x or ly!=y: # op.commandlist.append(Path.Command("G0", { "X": lx, "Y":ly, "Z":passStartDepth+stepUp})) # op.commandlist.append(Path.Command("G0", { "X": x, "Y":y, "Z":passStartDepth+stepUp})) # lx = x # ly = y lz = z # return to safe height in this Z pass z = obj.ClearanceHeight.Value if z != lz: op.commandlist.append(Path.Command("G0", {"Z": z})) lz = z passStartDepth = passEndDepth # return to safe height in this Z pass z = obj.ClearanceHeight.Value if z != lz: op.commandlist.append(Path.Command("G0", {"Z": z})) lz = z z = obj.ClearanceHeight.Value if z != lz: op.commandlist.append(Path.Command("G0", {"Z": z})) def Execute(op, obj): # pylint: disable=global-statement global sceneGraph global topZ if FreeCAD.GuiUp: sceneGraph = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph() PathLog.info("*** Adaptive toolpath processing started...\n") # hide old toolpaths during recalculation obj.Path = Path.Path("(Calculating...)") if FreeCAD.GuiUp: #store old visibility state job = op.getJob(obj) oldObjVisibility = obj.ViewObject.Visibility oldJobVisibility = job.ViewObject.Visibility obj.ViewObject.Visibility = False job.ViewObject.Visibility = False FreeCADGui.updateGui() try: helixDiameter = obj.HelixDiameterLimit.Value topZ = op.stock.Shape.BoundBox.ZMax obj.Stopped = False obj.StopProcessing = False if obj.Tolerance < 0.001: obj.Tolerance = 0.001 # Get list of working edges for adaptive algorithm pathArray = op.pathArray if not pathArray: PathLog.error("No wire data returned.") return path2d = convertTo2d(pathArray) stockPaths = [] if hasattr(op.stock, "StockType") and op.stock.StockType == "CreateCylinder": stockPaths.append([discretize(op.stock.Shape.Edges[0])]) else: stockBB = op.stock.Shape.BoundBox v = [] v.append(FreeCAD.Vector(stockBB.XMin, stockBB.YMin, 0)) v.append(FreeCAD.Vector(stockBB.XMax, stockBB.YMin, 0)) v.append(FreeCAD.Vector(stockBB.XMax, stockBB.YMax, 0)) v.append(FreeCAD.Vector(stockBB.XMin, stockBB.YMax, 0)) v.append(FreeCAD.Vector(stockBB.XMin, stockBB.YMin, 0)) stockPaths.append([v]) stockPath2d = convertTo2d(stockPaths) # opType = area.AdaptiveOperationType.ClearingInside # Commented out per LGTM suggestion if obj.OperationType == "Clearing": if obj.Side == "Outside": opType = area.AdaptiveOperationType.ClearingOutside else: opType = area.AdaptiveOperationType.ClearingInside else: # profiling if obj.Side == "Outside": opType = area.AdaptiveOperationType.ProfilingOutside else: opType = area.AdaptiveOperationType.ProfilingInside keepToolDownRatio = 3.0 if hasattr(obj, 'KeepToolDownRatio'): keepToolDownRatio = float(obj.KeepToolDownRatio) # put here all properties that influence calculation of adaptive base paths, inputStateObject = { "tool": float(op.tool.Diameter), "tolerance": float(obj.Tolerance), "geometry": path2d, "stockGeometry": stockPath2d, "stepover": float(obj.StepOver), "effectiveHelixDiameter": float(helixDiameter), "operationType": obj.OperationType, "side": obj.Side, "forceInsideOut": obj.ForceInsideOut, "finishingProfile": obj.FinishingProfile, "keepToolDownRatio": keepToolDownRatio, "stockToLeave": float(obj.StockToLeave) } inputStateChanged = False adaptiveResults = None if obj.AdaptiveOutputState is not None and obj.AdaptiveOutputState != "": adaptiveResults = obj.AdaptiveOutputState if json.dumps(obj.AdaptiveInputState) != json.dumps(inputStateObject): inputStateChanged = True adaptiveResults = None # progress callback fn, if return true it will stop processing def progressFn(tpaths): if FreeCAD.GuiUp: for path in tpaths: #path[0] contains the MotionType, #path[1] contains list of points if path[0] == area.AdaptiveMotionType.Cutting: sceneDrawPath(path[1],(0,0,1)) else: sceneDrawPath(path[1],(1,0,1)) FreeCADGui.updateGui() return obj.StopProcessing start = time.time() if inputStateChanged or adaptiveResults is None: a2d = area.Adaptive2d() a2d.stepOverFactor = 0.01 * obj.StepOver a2d.toolDiameter = float(op.tool.Diameter) a2d.helixRampDiameter = helixDiameter a2d.keepToolDownDistRatio = keepToolDownRatio a2d.stockToLeave = float(obj.StockToLeave) a2d.tolerance = float(obj.Tolerance) a2d.forceInsideOut = obj.ForceInsideOut a2d.finishingProfile = obj.FinishingProfile a2d.opType = opType # EXECUTE results = a2d.Execute(stockPath2d, path2d, progressFn) # need to convert results to python object to be JSON serializable adaptiveResults = [] for result in results: adaptiveResults.append({ "HelixCenterPoint": result.HelixCenterPoint, "StartPoint": result.StartPoint, "AdaptivePaths": result.AdaptivePaths, "ReturnMotionType": result.ReturnMotionType}) # GENERATE GenerateGCode(op, obj, adaptiveResults, helixDiameter) if not obj.StopProcessing: PathLog.info("*** Done. Elapsed time: %f sec\n\n" % (time.time()-start)) obj.AdaptiveOutputState = adaptiveResults obj.AdaptiveInputState = inputStateObject else: PathLog.info("*** Processing cancelled (after: %f sec).\n\n" % (time.time()-start)) finally: if FreeCAD.GuiUp: obj.ViewObject.Visibility = oldObjVisibility job.ViewObject.Visibility = oldJobVisibility sceneClean() def _get_working_edges(op, obj): '''_get_working_edges(op, obj)... Compile all working edges from the Base Geometry selection (obj.Base) for the current operation. Additional modifications to selected region(face), such as extensions, should be placed within this function. ''' all_regions = list() edge_list = list() avoidFeatures = list() rawEdges = list() # Get extensions and identify faces to avoid extensions = FeatureExtensions.getExtensions(obj) for e in extensions: if e.avoid: avoidFeatures.append(e.feature) # Get faces selected by user for base, subs in obj.Base: for sub in subs: if sub.startswith("Face"): if sub not in avoidFeatures: if obj.UseOutline: face = base.Shape.getElement(sub) # get outline with wire_A method used in PocketShape, but it does not play nicely later # wire_A = TechDraw.findShapeOutline(face, 1, FreeCAD.Vector(0.0, 0.0, 1.0)) wire_B = face.Wires[0] shape = Part.Face(wire_B) else: shape = base.Shape.getElement(sub) all_regions.append(shape) elif sub.startswith("Edge"): # Save edges for later processing rawEdges.append(base.Shape.getElement(sub)) # Efor # Process selected edges if rawEdges: edgeWires = DraftGeomUtils.findWires(rawEdges) if edgeWires: for w in edgeWires: for e in w.Edges: edge_list.append([discretize(e)]) # Apply regular Extensions op.exts = [] # pylint: disable=attribute-defined-outside-init for ext in extensions: if not ext.avoid: wire = ext.getWire() if wire: for f in ext.getExtensionFaces(wire): op.exts.append(f) all_regions.append(f) # Second face-combining method attempted horizontal = PathGeom.combineHorizontalFaces(all_regions) if horizontal: obj.removalshape = Part.makeCompound(horizontal) for f in horizontal: for w in f.Wires: for e in w.Edges: edge_list.append([discretize(e)]) return edge_list class PathAdaptive(PathOp.ObjectOp): def opFeatures(self, obj): '''opFeatures(obj) ... returns the OR'ed list of features used and supported by the operation. The default implementation returns "FeatureTool | FeatureDepths | FeatureHeights | FeatureStartPoint" Should be overwritten by subclasses.''' return PathOp.FeatureTool | PathOp.FeatureBaseEdges | PathOp.FeatureDepths \ | PathOp.FeatureFinishDepth | PathOp.FeatureStepDown | PathOp.FeatureHeights \ | PathOp.FeatureBaseGeometry | PathOp.FeatureCoolant | PathOp.FeatureLocations def initOperation(self, obj): '''initOperation(obj) ... implement to create additional properties. Should be overwritten by subclasses.''' obj.addProperty("App::PropertyEnumeration", "Side", "Adaptive", "Side of selected faces that tool should cut") obj.Side = ['Outside', 'Inside'] # side of profile that cutter is on in relation to direction of profile obj.addProperty("App::PropertyEnumeration", "OperationType", "Adaptive", "Type of adaptive operation") obj.OperationType = ['Clearing', 'Profiling'] # side of profile that cutter is on in relation to direction of profile obj.addProperty("App::PropertyFloat", "Tolerance", "Adaptive", "Influences accuracy and performance") obj.addProperty("App::PropertyPercent", "StepOver", "Adaptive", "Percent of cutter diameter to step over on each pass") obj.addProperty("App::PropertyDistance", "LiftDistance", "Adaptive", "Lift distance for rapid moves") obj.addProperty("App::PropertyDistance", "KeepToolDownRatio", "Adaptive", "Max length of keep tool down path compared to direct distance between points") obj.addProperty("App::PropertyDistance", "StockToLeave", "Adaptive", "How much stock to leave (i.e. for finishing operation)") # obj.addProperty("App::PropertyBool", "ProcessHoles", "Adaptive","Process holes as well as the face outline") obj.addProperty("App::PropertyBool", "ForceInsideOut", "Adaptive", "Force plunging into material inside and clearing towards the edges") obj.addProperty("App::PropertyBool", "FinishingProfile", "Adaptive", "To take a finishing profile path at the end") obj.addProperty("App::PropertyBool", "Stopped", "Adaptive", "Stop processing") obj.setEditorMode('Stopped', 2) # hide this property obj.addProperty("App::PropertyBool", "StopProcessing", "Adaptive", "Stop processing") obj.setEditorMode('StopProcessing', 2) # hide this property obj.addProperty("App::PropertyBool", "UseHelixArcs", "Adaptive", "Use Arcs (G2) for helix ramp") obj.addProperty("App::PropertyPythonObject", "AdaptiveInputState", "Adaptive", "Internal input state") obj.addProperty("App::PropertyPythonObject", "AdaptiveOutputState", "Adaptive", "Internal output state") obj.setEditorMode('AdaptiveInputState', 2) # hide this property obj.setEditorMode('AdaptiveOutputState', 2) # hide this property obj.addProperty("App::PropertyAngle", "HelixAngle", "Adaptive", "Helix ramp entry angle (degrees)") obj.addProperty("App::PropertyAngle", "HelixConeAngle", "Adaptive", "Helix cone angle (degrees)") obj.addProperty("App::PropertyLength", "HelixDiameterLimit", "Adaptive", "Limit helix entry diameter, if limit larger than tool diameter or 0, tool diameter is used") obj.addProperty("App::PropertyBool", "UseOutline", "Adaptive", "Uses the outline of the base geometry.") obj.addProperty("Part::PropertyPartShape", "removalshape", "Path", "") obj.setEditorMode('removalshape', 2) # hide FeatureExtensions.initialize_properties(obj) def opSetDefaultValues(self, obj, job): obj.Side = "Inside" obj.OperationType = "Clearing" obj.Tolerance = 0.1 obj.StepOver = 20 obj.LiftDistance = 0 # obj.ProcessHoles = True obj.ForceInsideOut = False obj.FinishingProfile = True obj.Stopped = False obj.StopProcessing = False obj.HelixAngle = 5 obj.HelixConeAngle = 0 obj.HelixDiameterLimit = 0.0 obj.AdaptiveInputState = "" obj.AdaptiveOutputState = "" obj.StockToLeave = 0 obj.KeepToolDownRatio = 3.0 obj.UseHelixArcs = False obj.UseOutline = False FeatureExtensions.set_default_property_values(obj, job) def opExecute(self, obj): '''opExecute(obj) ... called whenever the receiver needs to be recalculated. See documentation of execute() for a list of base functionality provided. Should be overwritten by subclasses.''' self.pathArray = _get_working_edges(self, obj) Execute(self, obj) def opOnDocumentRestored(self, obj): if not hasattr(obj, 'HelixConeAngle'): obj.addProperty("App::PropertyAngle", "HelixConeAngle", "Adaptive", "Helix cone angle (degrees)") if not hasattr(obj, "UseOutline"): obj.addProperty("App::PropertyBool", "UseOutline", "Adaptive", "Uses the outline of the base geometry.") if not hasattr(obj, "removalshape"): obj.addProperty("Part::PropertyPartShape", "removalshape", "Path", "") obj.setEditorMode('removalshape', 2) # hide FeatureExtensions.initialize_properties(obj) # Eclass def SetupProperties(): setup = ["Side", "OperationType", "Tolerance", "StepOver", "LiftDistance", "KeepToolDownRatio", "StockToLeave", "ForceInsideOut", "FinishingProfile", "Stopped", "StopProcessing", "UseHelixArcs", "AdaptiveInputState", "AdaptiveOutputState", "HelixAngle", "HelixConeAngle", "HelixDiameterLimit", "UseOutline"] return setup def Create(name, obj=None, parentJob=None): '''Create(name) ... Creates and returns a Adaptive operation.''' if obj is None: obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) obj.Proxy = PathAdaptive(obj, name, parentJob) return obj