Download src/Mod/CAM/Path/Op/Adaptive.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 84.4 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/Adaptive.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/CAM/Path/Op/Adaptive.py
-
curl -L -o Adaptive.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/Adaptive.py
84.4 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| # *************************************************************************** | |
| # * Copyright (c) 2018 Kresimir Tusek <kresimir.tusek@gmail.com> * | |
| # * 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 * | |
| # * * | |
| # *************************************************************************** | |
| # NOTE: "isNull() note" | |
| # After performing cut operations, checking the resulting shape.isNull() will | |
| # sometimes return False even when the resulting shape is infinitesimal and | |
| # further operations with it will raise exceptions. Instead checking if the | |
| # shape.Wires list is non-empty bypasses this issue. | |
| import Path | |
| import Path.Op.Base as PathOp | |
| import PathScripts.PathUtils as PathUtils | |
| import FreeCAD | |
| import time | |
| import json | |
| import math | |
| import area | |
| from PySide.QtCore import QT_TRANSLATE_NOOP | |
| if FreeCAD.GuiUp: | |
| from pivy import coin | |
| import FreeCADGui | |
| __doc__ = "Class and implementation of the Adaptive CAM operation." | |
| # lazily loaded modules | |
| from lazy_loader.lazy_loader import LazyLoader | |
| Part = LazyLoader("Part", globals(), "Part") | |
| TechDraw = LazyLoader("TechDraw", globals(), "TechDraw") | |
| FeatureExtensions = LazyLoader("Path.Op.FeatureExtension", globals(), "Path.Op.FeatureExtension") | |
| DraftGeomUtils = LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils") | |
| if False: | |
| Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) | |
| Path.Log.trackModule(Path.Log.thisModule()) | |
| else: | |
| Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) | |
| translate = FreeCAD.Qt.translate | |
| 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 afterwards | |
| topZ = 10 | |
| # Constants to avoid magic numbers in the code | |
| _ADAPTIVE_MIN_STEPDOWN = 0.1 | |
| 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 GenerateGCode(op, obj, adaptiveResults): | |
| if not adaptiveResults or not adaptiveResults[0]["AdaptivePaths"]: | |
| return | |
| # minLiftDistance = op.tool.Diameter | |
| helixRadius = 0 | |
| for region in adaptiveResults: | |
| p1 = region["HelixCenterPoint"] | |
| p2 = region["StartPoint"] | |
| helixRadius = max(math.dist(p1[:2], p2[:2]), helixRadius) | |
| stepDown = max(obj.StepDown.Value, _ADAPTIVE_MIN_STEPDOWN) | |
| length = 2 * math.pi * helixRadius | |
| obj.HelixAngle = min(89.99, max(obj.HelixAngle.Value, 0.01)) | |
| obj.HelixConeAngle = max(obj.HelixConeAngle, 0) | |
| helixAngleRad = math.radians(obj.HelixAngle) | |
| depthPerOneCircle = length * math.tan(helixAngleRad) | |
| if obj.HelixMaxStepdown.Value != 0 and obj.HelixMaxStepdown.Value < depthPerOneCircle: | |
| depthPerOneCircle = obj.HelixMaxStepdown.Value | |
| stepUp = max(obj.LiftDistance.Value, 0) | |
| # TODO: finishStep is of limited utility with how regions are now broken | |
| # up based on the model geometry- the "finish" step gets applied to each | |
| # region separately, which results in excessive "finish" steps being taken | |
| # where they really need not be. Leaving stock in Z generally makes more | |
| # sense, but both technically have their uses, so leaving this here as | |
| # option. Implementing flat area detection would make better use of both. | |
| finishStep = min(obj.FinishDepth.Value, stepDown) if hasattr(obj, "FinishDepth") else 0.0 | |
| # Track Z position to determine when changing height is necessary prior to a move | |
| lz = obj.StartDepth.Value | |
| for region in adaptiveResults: | |
| passStartDepth = region.get("TopDepth", obj.StartDepth.Value) | |
| depthParams = PathUtils.depth_params( | |
| clearance_height=obj.ClearanceHeight.Value, | |
| safe_height=obj.SafeHeight.Value, | |
| start_depth=passStartDepth, | |
| step_down=stepDown, | |
| z_finish_step=finishStep, | |
| final_depth=region.get("BottomDepth", obj.FinalDepth.Value), | |
| user_depths=None, | |
| ) | |
| for passEndDepth in depthParams.data: | |
| pass_start_angle = math.atan2( | |
| region["StartPoint"][1] - region["HelixCenterPoint"][1], | |
| region["StartPoint"][0] - region["HelixCenterPoint"][0], | |
| ) | |
| passDepth = passStartDepth - passEndDepth | |
| p1 = region["HelixCenterPoint"] | |
| p2 = region["StartPoint"] | |
| helixRadius = math.dist(p1[:2], p2[:2]) | |
| # Helix ramp | |
| if helixRadius > 0.01: | |
| r = helixRadius - 0.01 | |
| op.commandlist.append(Path.Command("(Helix to depth: %f)" % passEndDepth)) | |
| if obj.UseHelixArcs is False: | |
| helix_down_angle = passDepth / depthPerOneCircle * 2 * math.pi | |
| r_bottom = r - (passStartDepth - passEndDepth) * math.tan( | |
| math.radians(obj.HelixConeAngle.Value) | |
| ) | |
| r_bottom = max( | |
| r_bottom, r * 0.5 | |
| ) # put a limit on how small the cone tip can be | |
| step_over = obj.StepOver * 0.01 * op.tool.Diameter.Value | |
| spiral_out_angle = (r - r_bottom) / step_over * 2 * math.pi | |
| helix_base_angle = pass_start_angle - helix_down_angle - spiral_out_angle | |
| helix_angular_progress = 0 | |
| helixStart = [ | |
| region["HelixCenterPoint"][0] + r * math.cos(helix_base_angle), | |
| region["HelixCenterPoint"][1] + r * math.sin(helix_base_angle), | |
| ] | |
| # 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, | |
| }, | |
| ) | |
| ) | |
| # helix down | |
| while helix_angular_progress < helix_down_angle: | |
| progress = helix_angular_progress / helix_down_angle | |
| r_current = r * (1 - progress) + r_bottom * progress | |
| x = region["HelixCenterPoint"][0] + r_current * math.cos( | |
| helix_angular_progress + helix_base_angle | |
| ) | |
| y = region["HelixCenterPoint"][1] + r_current * math.sin( | |
| helix_angular_progress + helix_base_angle | |
| ) | |
| z = passStartDepth - progress * (passStartDepth - passEndDepth) | |
| op.commandlist.append( | |
| Path.Command("G1", {"X": x, "Y": y, "Z": z, "F": op.vertFeed}) | |
| ) | |
| helix_angular_progress = min( | |
| helix_angular_progress + math.pi / 16, helix_down_angle | |
| ) | |
| # spiral out, plus a full extra circle | |
| max_angle = helix_down_angle + spiral_out_angle + 2 * math.pi | |
| while helix_angular_progress < max_angle: | |
| if spiral_out_angle == 0: | |
| progress = 1 | |
| else: | |
| progress = min( | |
| 1, (helix_angular_progress - helix_down_angle) / spiral_out_angle | |
| ) | |
| r_current = r_bottom * (1 - progress) + r * progress | |
| x = region["HelixCenterPoint"][0] + r_current * math.cos( | |
| helix_angular_progress + helix_base_angle | |
| ) | |
| y = region["HelixCenterPoint"][1] + r_current * math.sin( | |
| helix_angular_progress + helix_base_angle | |
| ) | |
| z = passEndDepth | |
| op.commandlist.append( | |
| Path.Command("G1", {"X": x, "Y": y, "Z": z, "F": op.horizFeed}) | |
| ) | |
| helix_angular_progress = min( | |
| helix_angular_progress + math.pi / 16, max_angle | |
| ) | |
| 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, | |
| }, | |
| ) | |
| ) | |
| 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] | |
| 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})) | |
| 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): | |
| global sceneGraph | |
| global topZ | |
| if FreeCAD.GuiUp: | |
| sceneGraph = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph() | |
| Path.Log.info("*** Adaptive toolpath processing started...\n") | |
| # hide old toolpaths during recalculation | |
| obj.Path = Path.Path("(Calculating...)") | |
| oldObjVisibility = oldJobVisibility = False | |
| 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: | |
| obj.HelixMinDiameterPercent = max(obj.HelixMinDiameterPercent, 10) | |
| obj.HelixMaxDiameterPercent = max(obj.HelixMaxDiameterPercent, obj.HelixMinDiameterPercent) | |
| helixDiameter = obj.HelixMaxDiameterPercent / 100 * op.tool.Diameter.Value | |
| helixMinDiameter = obj.HelixMinDiameterPercent / 100 * op.tool.Diameter.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: | |
| msg = translate( | |
| "CAM", | |
| "Adaptive operation couldn't determine the boundary wire. Did you select base geometry?", | |
| ) | |
| FreeCAD.Console.PrintUserWarning(msg) | |
| return | |
| path2d = convertTo2d(pathArray) | |
| # Use the 2D outline of the stock as the stock | |
| # FIXME: This does not account for holes in the middle of stock! | |
| outer_wire = TechDraw.findShapeOutline(op.stock.Shape, 1, FreeCAD.Vector(0, 0, 1)) | |
| stockPaths = [[discretize(outer_wire)]] | |
| 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), | |
| "helixMinDiameter": float(helixMinDiameter), | |
| "operationType": obj.OperationType, | |
| "side": obj.Side, | |
| "forceInsideOut": obj.ForceInsideOut, | |
| "finishingProfile": obj.FinishingProfile, | |
| "keepToolDownRatio": keepToolDownRatio, | |
| "stockToLeave": float(obj.StockToLeave), | |
| "modelAwareExperiment": obj.ModelAwareExperiment, | |
| } | |
| 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.helixRampTargetDiameter = helixDiameter | |
| a2d.helixRampMinDiameter = helixMinDiameter | |
| 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) | |
| if not obj.StopProcessing: | |
| Path.Log.info("*** Done. Elapsed time: %f sec\n\n" % (time.time() - start)) | |
| obj.AdaptiveOutputState = adaptiveResults | |
| obj.AdaptiveInputState = inputStateObject | |
| else: | |
| Path.Log.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 ExecuteModelAware(op, obj): | |
| global sceneGraph | |
| global topZ | |
| if FreeCAD.GuiUp: | |
| sceneGraph = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph() | |
| Path.Log.info("*** Adaptive toolpath processing started...\n") | |
| # hide old toolpaths during recalculation | |
| obj.Path = Path.Path("(Calculating...)") | |
| oldObjVisibility = oldJobVisibility = False | |
| 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: | |
| obj.HelixMinDiameterPercent = max(obj.HelixMinDiameterPercent, 10) | |
| obj.HelixMaxDiameterPercent = max(obj.HelixMaxDiameterPercent, obj.HelixMinDiameterPercent) | |
| obj.StepOver = max(obj.StepOver, 1) | |
| helixDiameter = obj.HelixMaxDiameterPercent / 100 * op.tool.Diameter.Value | |
| helixMinDiameter = obj.HelixMinDiameterPercent / 100 * op.tool.Diameter.Value | |
| topZ = op.stock.Shape.BoundBox.ZMax | |
| obj.Stopped = False | |
| obj.StopProcessing = False | |
| obj.Tolerance = max(0.001, obj.Tolerance) | |
| # NOTE: Reminder that stock is formatted differently than inside/outside! | |
| stockPaths = {d: convertTo2d(op.stockPathArray[d]) for d in op.stockPathArray} | |
| outsideClearing = area.AdaptiveOperationType.ClearingOutside | |
| insideClearing = area.AdaptiveOperationType.ClearingInside | |
| outsideProfiling = area.AdaptiveOperationType.ProfilingOutside | |
| insideProfiling = area.AdaptiveOperationType.ProfilingInside | |
| # List every REGION separately- we can then calculate a toolpath based | |
| # on the region. One or more stepdowns may use that same toolpath by | |
| # keeping a reference to the region without requiring we calculate the | |
| # toolpath once per step down OR forcing all stepdowns of a region into | |
| # a single list. | |
| regionOps = list() | |
| outsidePathArray2dDepthTuples = list() | |
| insidePathArray2dDepthTuples = list() | |
| # NOTE: Make sure the depth lists are sorted for use in order-by-depth | |
| # and order-by-region algorithms below | |
| # NOTE: Pretty sure sorting is already guaranteed by how these are | |
| # created, but best to not assume that | |
| for rdict in op.outsidePathArray: | |
| regionOps.append( | |
| { | |
| "opType": ( | |
| outsideClearing if obj.OperationType == "Clearing" else outsideProfiling | |
| ), | |
| "path2d": convertTo2d(rdict["edges"]), | |
| "id": rdict["id"], | |
| "children": rdict["children"], | |
| # FIXME: Kinda gross- just use this to match up with the | |
| # appropriate stockpaths entry... | |
| "startdepth": rdict["depths"][0], | |
| } | |
| ) | |
| outsidePathArray2dDepthTuples.append( | |
| (sorted(rdict["depths"], reverse=True), regionOps[-1]) | |
| ) | |
| for rdict in op.insidePathArray: | |
| regionOps.append( | |
| { | |
| "opType": ( | |
| insideClearing if obj.OperationType == "Clearing" else insideProfiling | |
| ), | |
| "path2d": convertTo2d(rdict["edges"]), | |
| "id": rdict["id"], | |
| "children": rdict["children"], | |
| # FIXME: Kinda gross- just use this to match up with the | |
| # appropriate stockpaths entry... | |
| "startdepth": rdict["depths"][0], | |
| } | |
| ) | |
| insidePathArray2dDepthTuples.append( | |
| (sorted(rdict["depths"], reverse=True), regionOps[-1]) | |
| ) | |
| keepToolDownRatio = 3.0 | |
| if hasattr(obj, "KeepToolDownRatio"): | |
| keepToolDownRatio = obj.KeepToolDownRatio.Value | |
| # These fields are used to determine if toolpaths should be recalculated | |
| outsideInputStateObject = { | |
| "tool": op.tool.Diameter.Value, | |
| "tolerance": obj.Tolerance, | |
| "geometry": [ | |
| k["path2d"] for k in regionOps if k["opType"] in [outsideClearing, outsideProfiling] | |
| ], | |
| "stockGeometry": stockPaths, | |
| "stepover": obj.StepOver, | |
| "effectiveHelixDiameter": helixDiameter, | |
| "helixMinDiameter": helixMinDiameter, | |
| "operationType": "Clearing", | |
| "side": "Outside", | |
| "forceInsideOut": obj.ForceInsideOut, | |
| "finishingProfile": obj.FinishingProfile, | |
| "keepToolDownRatio": keepToolDownRatio, | |
| "stockToLeave": obj.StockToLeave.Value, | |
| "zStockToLeave": obj.ZStockToLeave.Value, | |
| "orderCutsByRegion": obj.OrderCutsByRegion, | |
| } | |
| insideInputStateObject = { | |
| "tool": op.tool.Diameter.Value, | |
| "tolerance": obj.Tolerance, | |
| "geometry": [ | |
| k["path2d"] for k in regionOps if k["opType"] in [insideClearing, insideProfiling] | |
| ], | |
| "stockGeometry": stockPaths, | |
| "stepover": obj.StepOver, | |
| "effectiveHelixDiameter": helixDiameter, | |
| "helixMinDiameter": helixMinDiameter, | |
| "operationType": "Clearing", | |
| "side": "Inside", | |
| "forceInsideOut": obj.ForceInsideOut, | |
| "finishingProfile": obj.FinishingProfile, | |
| "keepToolDownRatio": keepToolDownRatio, | |
| "stockToLeave": obj.StockToLeave.Value, | |
| "zStockToLeave": obj.ZStockToLeave.Value, | |
| "orderCutsByRegion": obj.OrderCutsByRegion, | |
| "modelAwareExperiment": obj.ModelAwareExperiment, | |
| } | |
| inputStateObject = [outsideInputStateObject, insideInputStateObject] | |
| inputStateChanged = False | |
| adaptiveResults = None | |
| # If we have a valid... path? Something. Generated, make that | |
| # tentatively the output | |
| if obj.AdaptiveOutputState: | |
| adaptiveResults = obj.AdaptiveOutputState | |
| # If ANYTHING in our input-cutting parameters, cutting regions, | |
| # etc.- changes, force recalculating | |
| 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: | |
| # NOTE: Seem to need to create a new a2d for each area when we're | |
| # stepping down depths like this. If we don't, it will keep history | |
| # from the last region we did. | |
| # TODO: QThread/QRunnable trigger Python's global interpreter lock | |
| # (GIL). To calculate toolpaths in parallel, making a C++ shim that | |
| # takes in the array of regions/stock paths and parallelizes in | |
| # C++-land is probably the way to do it. | |
| # Create a toolpath for each region to avoid re-calculating for | |
| # identical stepdowns | |
| for rdict in regionOps: | |
| path2d = rdict["path2d"] | |
| opType = rdict["opType"] | |
| a2d = area.Adaptive2d() | |
| a2d.stepOverFactor = 0.01 * obj.StepOver | |
| a2d.toolDiameter = op.tool.Diameter.Value | |
| a2d.helixRampTargetDiameter = helixDiameter | |
| a2d.helixRampMinDiameter = helixMinDiameter | |
| a2d.keepToolDownDistRatio = keepToolDownRatio | |
| # NOTE: Z stock is handled in our stepdowns | |
| a2d.stockToLeave = obj.StockToLeave.Value | |
| a2d.tolerance = obj.Tolerance | |
| a2d.forceInsideOut = obj.ForceInsideOut | |
| a2d.finishingProfile = obj.FinishingProfile | |
| a2d.opType = opType | |
| rdict["toolpaths"] = a2d.Execute( | |
| stockPaths[rdict["startdepth"]], path2d, progressFn | |
| ) | |
| # Sort regions to cut by either depth or area. | |
| # TODO: Bonus points for ordering to minimize rapids | |
| cutlist = list() | |
| # Region IDs that have been cut already | |
| cutids = list() | |
| # Create sorted list of unique depths | |
| # NOTE: reverse because we cut top-down! | |
| depths = list() | |
| # NOTE: alltuples is sorted by depth already | |
| alltuples = outsidePathArray2dDepthTuples + insidePathArray2dDepthTuples | |
| for t in alltuples: | |
| depths += [d for d in t[0]] | |
| depths = sorted(list(set(depths)), reverse=True) | |
| if obj.OrderCutsByRegion: | |
| # Translate child ID numbers to an actual reference to the | |
| # associated tuple | |
| for rdict in regionOps: | |
| rdict["childTuples"] = [t for t in alltuples if t[1]["id"] in rdict["children"]] | |
| # Helper function to recurse down children | |
| def addToCutList(tuples): | |
| for k in tuples: | |
| if k in cutlist: | |
| continue | |
| cutlist.append(k) | |
| addToCutList(k[1]["childTuples"]) | |
| addToCutList(alltuples) | |
| else: | |
| for d in depths: | |
| cutlist += [([d], o[1]) for o in outsidePathArray2dDepthTuples if d in o[0]] | |
| cutlist += [([d], i[1]) for i in insidePathArray2dDepthTuples if d in i[0]] | |
| # need to convert results to python object to be JSON serializable | |
| stepdown = max(obj.StepDown.Value, _ADAPTIVE_MIN_STEPDOWN) | |
| adaptiveResults = list() | |
| for depths, region in cutlist: | |
| for result in region["toolpaths"]: | |
| # Top depth is the height where the helix starts for a | |
| # region. | |
| # We want the lowest of 3 possibilities: | |
| # - the top of the stock OR | |
| # - the region's first cut depth + stepdown OR | |
| # - the operation's starting depth | |
| # The starting depth option covers the case where the user | |
| # has a previous operations that cleared some stock and | |
| # wants the adaptive toolpath to pick up where the previous | |
| # operation left off. | |
| # Regions are only generated where stock needs to be | |
| # removed, so we can't start at the cut level- we know | |
| # there's material there. | |
| # TODO: Due to the adaptive algorithm currently not | |
| # processing holes in the stock when finding entry points, | |
| # this may result in a helix up to stepdown in height where | |
| # one isn't required. This should be fixed in FindEntryPoint | |
| # or FindEntryPointOutside in Adaptive.cpp, not bandaged | |
| # here. | |
| TopDepth = min( | |
| topZ, | |
| depths[0] + stepdown, | |
| obj.StartDepth.Value, | |
| ) | |
| adaptiveResults.append( | |
| { | |
| "HelixCenterPoint": result.HelixCenterPoint, | |
| "StartPoint": result.StartPoint, | |
| "AdaptivePaths": result.AdaptivePaths, | |
| "ReturnMotionType": result.ReturnMotionType, | |
| "TopDepth": TopDepth, | |
| "BottomDepth": depths[-1], | |
| } | |
| ) | |
| # GENERATE | |
| GenerateGCode(op, obj, adaptiveResults) | |
| if not obj.StopProcessing: | |
| Path.Log.info("*** Done. Elapsed time: %f sec\n\n" % (time.time() - start)) | |
| obj.AdaptiveOutputState = adaptiveResults | |
| obj.AdaptiveInputState = inputStateObject | |
| else: | |
| Path.Log.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 projectFacesToXY(faces, minEdgeLength=1e-10): | |
| """projectFacesToXY(faces, minEdgeLength) | |
| Calculates the projection of the provided list of faces onto the XY plane. | |
| The returned value is a single shape that may contain multiple faces if | |
| there were disjoint projections. Each individual face will be clean, without | |
| triangulated geometry, etc., and will be at Z=0 on the XY plane | |
| minEdgeLength is provided to (eg) filter out the tips of cones that are | |
| internally represented as arbitrarily-small circular faces- using those for | |
| additional operations causes problems. | |
| """ | |
| projdir = FreeCAD.Vector(0, 0, 1) | |
| outfaces = [] | |
| for f in faces: | |
| # Vertical cones and spheres will still have a projection on the XY | |
| # plane. Cylinders and flat faces will not. | |
| if Path.Geom.isVertical(f) and type(f.Surface) not in [Part.Cone, Part.Sphere]: | |
| continue | |
| # NOTE: Wires/edges get clipped if we have an "exact fit" bounding box | |
| projface = Path.Geom.makeBoundBoxFace(f.BoundBox, offset=1, zHeight=0) | |
| # NOTE: Cylinders, cones, and spheres are messy: | |
| # - Internal representation of non-truncted cones and spheres includes | |
| # the "tip" with a ~0-area closed edge. This is different than the | |
| # "isNull() note" at the top in magnitude | |
| # - Projecting edges doesn't naively work due to the way seams are handled | |
| # - There may be holes at either end that may or may not line up- any | |
| # overlap is a hole in the projection | |
| if type(f.Surface) in [Part.Cone, Part.Cylinder, Part.Sphere]: | |
| # This gets most of the face outline, but since cylinder/cone faces | |
| # are hollow, if the ends overlap in the projection there may be a | |
| # hole we need to remove from the solid projection | |
| oface = Part.makeFace(TechDraw.findShapeOutline(f, 1, projdir)) | |
| # "endfacewires" is JUST the end faces of a cylinder/cone, used to | |
| # determine if there's a hole we can see through the shape that | |
| # should NOT be solid in the projection | |
| endfacewires = DraftGeomUtils.findWires( | |
| [e for e in f.Edges if not e.isSeam(f) and e.Length > minEdgeLength] | |
| ) | |
| # Need to verify that there actually is a projection before taking | |
| # a wire from the list, else this could nicely be one line. | |
| projwires = [] | |
| for w in endfacewires: | |
| pp = projface.makeParallelProjection(w, projdir).Wires | |
| if pp: | |
| projwires.append(pp[0]) | |
| if len(projwires) > 1: | |
| faces = [Part.makeFace(x) for x in projwires] | |
| overlap = faces[0].common(faces[1:]) | |
| outfaces.append(oface.cut(overlap)) | |
| else: | |
| outfaces.append(oface) | |
| # For other cases, projecting the wires to a plane should suffice | |
| else: | |
| facewires = list() | |
| for w in f.Wires: | |
| if w.isClosed(): | |
| projwire = projface.makeParallelProjection(w, projdir).Wires[0] | |
| if projwire.isClosed(): | |
| facewires.append(projwire) | |
| if facewires: | |
| outfaces.append(Part.makeFace(facewires)) | |
| if outfaces: | |
| fusion = outfaces[0].fuse(outfaces[1:]) | |
| # removeSplitter fixes occasional concatenate issues for some face orders | |
| return DraftGeomUtils.concatenate(fusion.removeSplitter()) | |
| else: | |
| return Part.Shape() | |
| def _getSolidProjection(shp, z): | |
| """_getSolidProjection(shp, z) | |
| Calculates a shape obtained by slicing shp at the height z, then projecting | |
| the solids above that height onto a region of proj_face, and creating a | |
| simplified face | |
| """ | |
| bb = shp.BoundBox | |
| # Find all faces above the machining depth. This is used to mask future | |
| # interior cuts, and the outer wire is used as the external wire | |
| bbCutTop = Part.makeBox( | |
| bb.XLength, | |
| bb.YLength, | |
| max(bb.ZLength, bb.ZLength - z), | |
| FreeCAD.Vector(bb.XMin, bb.YMin, z), | |
| ) | |
| aboveSolids = shp.common(bbCutTop).Solids | |
| faces = list() | |
| for s in aboveSolids: | |
| faces += s.Faces | |
| return projectFacesToXY(faces) | |
| def _workingEdgeHelperRoughing(op, obj, depths): | |
| # Final calculated regions- list of dicts with entries: | |
| # "region" - actual shape | |
| # "depths" - list of depths this region applies to | |
| insideRegions = list() | |
| outsideRegions = list() | |
| # Multiple input solids can be selected- make a single part out of them, | |
| # will process each solid separately as appropriate | |
| shps = op.model[0].Shape.fuse([k.Shape for k in op.model[1:]]) | |
| projdir = FreeCAD.Vector(0, 0, 1) | |
| # Take outline of entire model as our baseline machining region. No need to | |
| # do this repeatedly inside the loop. | |
| modelOutlineFaces = [ | |
| Part.makeFace(TechDraw.findShapeOutline(s, 1, projdir)) for s in shps.Solids | |
| ] | |
| lastdepth = obj.StartDepth.Value | |
| for depth in depths: | |
| # If we have no stock to machine, just skip all the rest of the math | |
| if depth >= op.stock.Shape.BoundBox.ZMax: | |
| lastdepth = depth | |
| continue | |
| # NOTE: To "leave" stock along Z without actually checking any face | |
| # depths, we simply slice the model "lower" than our actual cut depth by | |
| # the Z stock to leave, which ensures we stay at least that far from the | |
| # actual faces | |
| stockface = _getSolidProjection(op.stock.Shape, depth - obj.ZStockToLeave.Value) | |
| aboveRefined = _getSolidProjection(shps, depth - obj.ZStockToLeave.Value) | |
| # Outside is based on the outer wire of the above_faces | |
| # Insides are based on the remaining "below" regions, masked by the | |
| # "above"- if something is above an area, we can't machine it in 2.5D | |
| # OUTSIDE REGIONS | |
| # Outside: Take the outer wire of the above faces | |
| # NOTE: Exactly one entry per depth (not necessarily one depth entry per | |
| # stepdown, however), which is a LIST of the wires we're staying outside | |
| # NOTE: Do this FIRST- if any inside regions share enough of an edge | |
| # with an outside region for a tool to get through, we want to skip them | |
| # for the current stepdown | |
| if aboveModelFaces := [ | |
| Part.makeFace(TechDraw.findShapeOutline(f, 1, projdir)) for f in aboveRefined.Faces | |
| ]: | |
| aboveModelFaces = aboveModelFaces[0].fuse(aboveModelFaces[1:]) | |
| else: | |
| aboveModelFaces = Part.Shape() | |
| # If this region exists in our list, it has to be the last entry, due to | |
| # proceeding in order and having only one per depth. If it's already | |
| # there, replace with the new, deeper depth, else add new | |
| # NOTE: Check for NULL regions to not barf on regions between the top of | |
| # the model and the top of the stock, which are "outside" of nothing | |
| # NOTE: See "isNull() note" at top of file | |
| if ( | |
| outsideRegions | |
| and outsideRegions[-1]["region"].Wires | |
| and aboveModelFaces.Wires | |
| and not aboveModelFaces.cut(outsideRegions[-1]["region"]).Wires | |
| ): | |
| outsideRegions[-1]["depths"].append(depth) | |
| else: | |
| outsideRegions.append({"region": aboveModelFaces, "depths": [depth]}) | |
| # NOTE: If you don't care about controlling depth vs region ordering, | |
| # you can actually just do everything with "outside" processing, if you | |
| # don't remove internal holes from the regions above | |
| # INSIDE REGIONS | |
| # NOTE: Nothing inside if there's no model above us | |
| # NOTE: See "isNull() note" at top of file | |
| if aboveModelFaces.Wires: | |
| # Remove any overlapping areas already machined from the outside. | |
| outsideface = stockface.cut(outsideRegions[-1]["region"].Faces) | |
| # NOTE: See "isNull() note" at top of file | |
| if outsideface.Wires: | |
| belowFaces = [f.cut(outsideface) for f in modelOutlineFaces] | |
| else: | |
| # NOTE: Doesn't matter here, but ensure we're making a new list so | |
| # we don't clobber modelOutlineFaces | |
| belowFaces = [f for f in modelOutlineFaces] | |
| # This isn't really necessary unless the user inputs bad data- eg, a | |
| # min depth above the top of the model. In which case we still want to | |
| # clear the stock | |
| if belowFaces: | |
| # Remove the overhangs from the desired region to cut | |
| belowCut = belowFaces[0].fuse(belowFaces[1:]).cut(aboveRefined) | |
| # NOTE: See "isNull() note" at top of file | |
| if belowCut.Wires: | |
| # removeSplitter fixes occasional concatenate issues for | |
| # some face orders | |
| finalCut = DraftGeomUtils.concatenate(belowCut.removeSplitter()) | |
| else: | |
| finalCut = Part.Shape() | |
| else: | |
| # Make a dummy shape if we don't have anything actually below | |
| finalCut = Part.Shape() | |
| # Split up into individual faces if any are disjoint, then update | |
| # insideRegions- either by adding a new entry OR by updating the depth | |
| # of an existing entry | |
| for f in finalCut.Faces: | |
| addNew = True | |
| # Brute-force search all existing regions to see if any are the same | |
| newtop = lastdepth | |
| for rdict in insideRegions: | |
| # FIXME: Smarter way to do this than a full cut operation? | |
| if not rdict["region"].cut(f).Wires: | |
| rdict["depths"].append(depth) | |
| addNew = False | |
| break | |
| if addNew: | |
| insideRegions.append({"region": f, "depths": [depth]}) | |
| # Update the last depth step | |
| lastdepth = depth | |
| # end for depth | |
| return insideRegions, outsideRegions | |
| def _workingEdgeHelperManual(op, obj, depths): | |
| # Final calculated regions- list of dicts with entries: | |
| # "region" - actual shape | |
| # "depths" - list of depths this region applies to | |
| insideRegions = list() | |
| outsideRegions = list() | |
| # User selections, with extensions | |
| selectedRegions = list() | |
| selectedEdges = list() | |
| # Get extensions and identify faces to avoid | |
| extensions = FeatureExtensions.getExtensions(obj) | |
| avoidFeatures = [e for e in extensions if e.avoid] | |
| # Similarly, expand selected regions with extensions | |
| for ext in extensions: | |
| if not ext.avoid: | |
| if wire := ext.getWire(): | |
| # NOTE: Can NOT just make a face directly, since that just gives | |
| # the outside profile and removes internal holes | |
| for f in ext.getExtensionFaces(wire): | |
| selectedRegions.extend(f.Faces) | |
| for base, subs in obj.Base: | |
| for sub in subs: | |
| element = base.Shape.getElement(sub) | |
| if sub.startswith("Face") and sub not in avoidFeatures: | |
| shape = Part.Face(element.OuterWire) if obj.UseOutline else element | |
| selectedRegions.append(shape) | |
| # Omit vertical edges, since they project to nothing in the XY plane | |
| # and cause processing failures later if included | |
| elif sub.startswith("Edge") and not Path.Geom.isVertical(element): | |
| selectedEdges.append(element) | |
| # Multiple input solids can be selected- make a single part out of them, | |
| # will process each solid separately as appropriate | |
| shps = op.model[0].Shape.fuse([k.Shape for k in op.model[1:]]) | |
| # Make a face to project onto | |
| # NOTE: Use 0 as the height, since that's what TechDraw.findShapeOutline | |
| # uses, which we use to find the machining boundary, and the actual depth | |
| # is tracked separately. | |
| # NOTE: Project to the PART bounding box- with some padding- not the stock, | |
| # since the stock may be smaller than the part | |
| projface = Path.Geom.makeBoundBoxFace(shps.BoundBox, offset=1, zHeight=0) | |
| projdir = FreeCAD.Vector(0, 0, 1) | |
| # When looking for selected edges, project to a single plane first, THEN try | |
| # to find wires. Take all the resulting wires and make faces in one shot to | |
| # make bullseye-style cutouts where selected wires nest. | |
| edgefaces = list() | |
| if selectedEdges: | |
| pp = [projface.makeParallelProjection(e, projdir).Wires[0] for e in selectedEdges] | |
| ppe = list() | |
| for w in pp: | |
| ppe += w.Edges | |
| edgeWires = DraftGeomUtils.findWires(ppe) | |
| edgefaces = Part.makeFace(edgeWires).Faces | |
| selectedRefined = projectFacesToXY(selectedRegions + edgefaces) | |
| # If the user selected only faces that don't have an XY projection AND no | |
| # edges, give a useful message | |
| if not selectedRefined.Wires: | |
| Path.Log.warning("Selected faces/wires have no projection on the XY plane") | |
| return insideRegions, outsideRegions | |
| lastdepth = obj.StartDepth.Value | |
| for depth in depths: | |
| # If our depth is above the top of the stock, there's nothing to machine | |
| if depth >= op.stock.Shape.BoundBox.ZMax: | |
| lastdepth = depth | |
| continue | |
| # NOTE: See note in _workingEdgeHelperRoughing- tl;dr slice stock | |
| # lower than cut depth to effectively leave (at least) obj.ZStockToLeave | |
| aboveRefined = _getSolidProjection(shps, depth - obj.ZStockToLeave.Value) | |
| # Create appropriate tuples and add to list, processing inside/outside | |
| # as requested by operation | |
| if obj.Side == "Outside": | |
| # Outside is based on the outer wire of the faces of aboveRefined | |
| # Insides are based on the remaining "below" regions, masked by the | |
| # "above"- if something is above an area, we can't machine it in 2.5D | |
| # Outside: Take the outer wire of the above faces, added to selected | |
| # edges and regions | |
| # NOTE: Exactly one entry per depth (not necessarily one depth entry per | |
| # stepdown, however), which is a LIST of the wires we're staying outside | |
| # NOTE: Do this FIRST- if any inside regions share enough of an edge | |
| # with an outside region for a tool to get through, we want to skip them | |
| # for the current stepdown | |
| # NOTE: This check naively seems unnecessary, but it's possible the | |
| # user selected a vertical face as the only face to stay outside of, | |
| # and we're above the model, causing keepOutFaces to be empty | |
| if keepOutFaces := [ | |
| Part.makeFace(TechDraw.findShapeOutline(f, 1, projdir)) | |
| for f in aboveRefined.Faces + selectedRefined.Faces | |
| ]: | |
| finalMerge = keepOutFaces[0].fuse(keepOutFaces[1:]) | |
| else: | |
| finalMerge = selectedRefined | |
| # Without removeSplitter(), concatenate will sometimes fail when | |
| # trying to merge faces that are (eg) connected A-B and B-C, | |
| # seemingly when trying to merge A-C | |
| regions = DraftGeomUtils.concatenate(finalMerge.removeSplitter()) | |
| # If this region exists in our list, it has to be the last entry, due to | |
| # proceeding in order and having only one per depth. If it's already | |
| # there, replace with the new, deeper depth, else add new | |
| # NOTE: Do NOT need a check for whether outsideRegions[-1]["region"] | |
| # is valid since we have a user-specified region regardless of depth | |
| # NOTE: See "isNull() note" at top of file | |
| if ( | |
| outsideRegions | |
| and regions.Wires | |
| and not regions.cut(outsideRegions[-1]["region"]).Wires | |
| ): | |
| outsideRegions[-1]["depths"].append(depth) | |
| else: | |
| outsideRegions.append({"region": regions, "depths": [depth]}) | |
| # Inside | |
| # For every area selected by the user, project to a plane | |
| # NOTE: See "isNull() note" at top of file | |
| else: | |
| if aboveRefined.Wires: | |
| finalCut = selectedRefined.cut(aboveRefined) | |
| else: | |
| finalCut = selectedRefined | |
| # Split up into individual faces if any are disjoint, then update | |
| # insideRegions- either by adding a new entry OR by updating the depth | |
| # of an existing entry | |
| for f in finalCut.Faces: | |
| addNew = True | |
| # Brute-force search all existing regions to see if any are the same | |
| newtop = lastdepth | |
| for rdict in insideRegions: | |
| # FIXME: Smarter way to do this than a full cut operation? | |
| if not rdict["region"].cut(f).Wires: | |
| rdict["depths"].append(depth) | |
| addNew = False | |
| break | |
| if addNew: | |
| insideRegions.append({"region": f, "depths": [depth]}) | |
| # Update the last depth step | |
| lastdepth = depth | |
| # end for depth | |
| return insideRegions, outsideRegions | |
| 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.OuterWire | |
| 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 = [] | |
| 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 = Path.Geom.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 | |
| def _getWorkingEdgesModelAware(op, obj): | |
| """_getWorkingEdgesModelAware(op, obj)... | |
| Compile all working edges from the Base Geometry selection (obj.Base) | |
| for the current operation (or the entire model if no selections). | |
| Additional modifications to selected region(face), such as extensions, | |
| should be placed within this function. | |
| This version will return two lists- one for outside (keepout) edges and one | |
| for inside ("machine inside") edges. Each list will be a dict with "region" | |
| and "depths" entries- the former being discretized geometry of the region, | |
| the latter being a list of every depth the geometry is machined on | |
| """ | |
| # Find depth steps, throwing out all depths above anywhere we might cut | |
| # NOTE: Finish stepdown = 0 here- it's actually applied when gcode is | |
| # generated; doing so here would cause it to be applied twice. | |
| depthParams = PathUtils.depth_params( | |
| clearance_height=obj.ClearanceHeight.Value, | |
| safe_height=obj.SafeHeight.Value, | |
| start_depth=obj.StartDepth.Value, | |
| step_down=max(obj.StepDown.Value, _ADAPTIVE_MIN_STEPDOWN), | |
| z_finish_step=0.0, | |
| final_depth=obj.FinalDepth.Value, | |
| user_depths=None, | |
| ) | |
| # d < op.stock.Shape.BoundBox.ZMax may be true even if slicing at that | |
| # height causes no projection, which results in a NULL shape. Use the | |
| # operation tolerance to prevent that. | |
| depths = [d for d in depthParams.data if d - op.stock.Shape.BoundBox.ZMax < -obj.Tolerance] | |
| # Get the stock outline at each stepdown. Used to calculate toolpaths and | |
| # for calculating cut regions in some instances | |
| # NOTE: See note in _workingEdgeHelperRoughing- tl;dr slice stock lower | |
| # than cut depth to effectively leave (at least) obj.ZStockToLeave | |
| # NOTE: Stock is handled DIFFERENTLY than inside and outside regions! | |
| # Combining different depths just adds code to look up the correct outline | |
| # when computing inside/outside regions, for no real benefit. | |
| stockProjectionDict = { | |
| d: _getSolidProjection(op.stock.Shape, d - obj.ZStockToLeave.Value) for d in depths | |
| } | |
| # If user specified edges, calculate the machining regions based on that | |
| # input. Otherwise, process entire model | |
| # Output are lists of dicts with "region" and "depths" entries. Depths are | |
| # a list of Z depths that the region applies to | |
| # Inside regions are a single face; outside regions consist of ALL geometry | |
| # to be avoided at those depths. | |
| if obj.Base: | |
| insideRegions, outsideRegions = _workingEdgeHelperManual(op, obj, depths) | |
| else: | |
| insideRegions, outsideRegions = _workingEdgeHelperRoughing(op, obj, depths) | |
| # Find all children of each region. A child of region X is any region Y such | |
| # that Y is a subset of X AND Y starts within one stepdown of X (ie, direct | |
| # children only). | |
| # NOTE: Inside and outside regions are inverses of each other, so above | |
| # refers to the area to be machined! | |
| # Assign an ID number to track each region | |
| idnumber = 0 | |
| for r in insideRegions + outsideRegions: | |
| r["id"] = idnumber | |
| r["children"] = list() | |
| idnumber += 1 | |
| # NOTE: Inside and outside regions are inverses of each other | |
| # NOTE: Outside regions can't have parents | |
| for rx in insideRegions: | |
| for ry in [k for k in insideRegions if k != rx]: | |
| dist = min(rx["depths"]) - max(ry["depths"]) | |
| # Ignore regions at our level or above, or more than one step down | |
| if dist <= 0 or dist > depthParams.step_down: | |
| continue | |
| if not ry["region"].cut(rx["region"]).Wires: | |
| rx["children"].append(ry["id"]) | |
| # See which outside region this is a child of- basically inverse of above | |
| for ry in [k for k in outsideRegions]: | |
| dist = min(ry["depths"]) - max(rx["depths"]) | |
| # Ignore regions at our level or above, or more than one step down | |
| if dist <= 0 or dist > depthParams.step_down: | |
| continue | |
| # child if there is NO overlap between the stay-outside and stay- | |
| # inside regions | |
| # Also a child if the outer region is NULL (includes everything) | |
| # NOTE: See "isNull() note" at top of file | |
| if not ry["region"].Wires or not rx["region"].common(ry["region"]).Wires: | |
| ry["children"].append(rx["id"]) | |
| # Further split regions as necessary for when the stock changes- a region as | |
| # reported here is where a toolpath will be generated, and can be projected | |
| # along all of the depths associated with it. By doing this, we can minimize | |
| # the number of toolpaths that need to be generated AND avoid more complex | |
| # logic in depth-first vs region-first sorting of regions. | |
| # NOTE: For internal regions, stock is "the same" if the region cut with | |
| # the stock results in the same region. | |
| # NOTE: For external regions, stock is "the same" if the stock cut by the | |
| # region results in the same region | |
| def _regionChildSplitterHelper(regions, areInsideRegions): | |
| nonlocal stockProjectionDict | |
| nonlocal idnumber | |
| for r in regions: | |
| depths = sorted(r["depths"], reverse=True) | |
| if areInsideRegions: | |
| rcut = r["region"].cut(stockProjectionDict[depths[0]]) | |
| else: | |
| # NOTE: We may end up with empty "outside" regions in the space | |
| # between the top of the stock and the top of the model- want | |
| # to machine the entire stock in that case | |
| # NOTE: See "isNull() note" at top of file | |
| if not r["region"].Wires: | |
| rcut = stockProjectionDict[depths[0]] | |
| else: | |
| rcut = stockProjectionDict[depths[0]].cut(r["region"]) | |
| # If the region is already entirely within in the stock, there's no | |
| # way the region can change at a lower depth. That rcut is not | |
| # empty is an assumption for the check in the depth loop below | |
| if not rcut.Wires: | |
| continue | |
| # If the region cut with the stock at a new depth is different than | |
| # the original cut, we need to split this region | |
| # The new region gets all of the children, and becomes a child of | |
| # the existing region. | |
| parentdepths = depths[0:1] | |
| for d in depths[1:]: | |
| if ( | |
| areInsideRegions and r["region"].cut(stockProjectionDict[d]).cut(rcut).Wires | |
| ) or stockProjectionDict[d].cut(r["region"]).cut(rcut).Wires: | |
| newregion = { | |
| "id": idnumber, | |
| "depths": [k for k in depths if k not in parentdepths], | |
| "region": r["region"], | |
| "children": r["children"], | |
| } | |
| # Update parent with the new region as a child, along with all | |
| # the depths it was unchanged on | |
| r["children"] = [idnumber] | |
| r["depths"] = parentdepths | |
| # Add the new region to the end of the list and stop processing | |
| # this region | |
| # When the new region is processed at the end, we'll effectively | |
| # recurse and handle splitting that new region if required | |
| regions.append(newregion) | |
| idnumber += 1 | |
| continue | |
| # If we didn't split at this depth, the parent will keep "control" | |
| # of this depth | |
| parentdepths.append(d) | |
| _regionChildSplitterHelper(insideRegions, True) | |
| _regionChildSplitterHelper(outsideRegions, False) | |
| # Create discretized regions | |
| def _createDiscretizedRegions(regionDicts): | |
| discretizedRegions = list() | |
| for rdict in regionDicts: | |
| discretizedRegions.append( | |
| { | |
| "edges": [[discretize(w)] for w in rdict["region"].Wires], | |
| "depths": rdict["depths"], | |
| "id": rdict["id"], | |
| "children": rdict["children"], | |
| } | |
| ) | |
| return discretizedRegions | |
| insideDiscretized = _createDiscretizedRegions(insideRegions) | |
| outsideDiscretized = _createDiscretizedRegions(outsideRegions) | |
| # NOTE: REMINDER: This is notably different from machining regions- just | |
| # a dict with depth: region entries, single depth for easy lookup | |
| stockDiscretized = {} | |
| for d in stockProjectionDict: | |
| discretizedEdges = list() | |
| for a in stockProjectionDict[d].Faces: | |
| for w in a.Wires: | |
| discretizedEdges.append([discretize(w)]) | |
| stockDiscretized[d] = discretizedEdges | |
| # Return found inside and outside regions/depths. Up to the caller to decide | |
| # which ones it cares about. | |
| # NOTE: REMINDER: Stock is notably different from machining regions- just | |
| # a dict with depth: region entries, single depth for easy lookup | |
| return insideDiscretized, outsideDiscretized, stockDiscretized | |
| 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 propertyEnumerations(self, dataType="data"): | |
| """helixOpPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType. | |
| Args: | |
| dataType = 'data', 'raw', 'translated' | |
| Notes: | |
| 'data' is list of internal string literals used in code | |
| 'raw' is list of (translated_text, data_string) tuples | |
| 'translated' is list of translated string literals | |
| """ | |
| # Enumeration lists for App::PropertyEnumeration properties | |
| enums = { | |
| "Side": [ | |
| (translate("CAM_Adaptive", "Outside"), "Outside"), | |
| (translate("CAM_Adaptive", "Inside"), "Inside"), | |
| ], # this is the direction that the profile runs | |
| "OperationType": [ | |
| (translate("CAM_Adaptive", "Clearing"), "Clearing"), | |
| (translate("CAM_Adaptive", "Profiling"), "Profiling"), | |
| ], # side of profile that cutter is on in relation to direction of profile | |
| } | |
| if dataType == "raw": | |
| return enums | |
| data = list() | |
| idx = 0 if dataType == "translated" else 1 | |
| Path.Log.debug(enums) | |
| for k, v in enumerate(enums): | |
| data.append((v, [tup[idx] for tup in enums[v]])) | |
| Path.Log.debug(data) | |
| return data | |
| def initOperation(self, obj): | |
| """initOperation(obj) ... implement to create additional properties. | |
| Should be overwritten by subclasses.""" | |
| obj.addProperty( | |
| "App::PropertyEnumeration", | |
| "Side", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Side of selected faces that tool should cut", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyEnumeration", | |
| "OperationType", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Type of adaptive operation", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyFloat", | |
| "Tolerance", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Influences calculation performance vs stability and accuracy.\n\nLarger values (further to the right) will calculate faster; smaller values (further to the left) will result in more accurate toolpaths.", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyPercent", | |
| "StepOver", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Percent of cutter diameter to step over on each pass", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyDistance", | |
| "LiftDistance", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Lift distance for rapid moves", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyDistance", | |
| "KeepToolDownRatio", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Max length of keep tool down path compared to direct distance between points", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyDistance", | |
| "StockToLeave", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "How much stock to leave in the XY plane (eg for finishing operation)", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyDistance", | |
| "ZStockToLeave", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "How much stock to leave along the Z axis (eg for finishing operation). This property is only used if the ModelAwareExperiment is enabled.", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyBool", | |
| "ForceInsideOut", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Force plunging into material inside and clearing towards the edges", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyBool", | |
| "FinishingProfile", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "To take a finishing profile path at the end", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyBool", | |
| "Stopped", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP("App::Property", "Stop processing"), | |
| ) | |
| obj.setEditorMode("Stopped", 2) # hide this property | |
| obj.addProperty( | |
| "App::PropertyBool", | |
| "StopProcessing", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Stop processing", | |
| ), | |
| ) | |
| obj.setEditorMode("StopProcessing", 2) # hide this property | |
| obj.addProperty( | |
| "App::PropertyBool", | |
| "UseHelixArcs", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Use Arcs (G2) for helix ramp", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyPythonObject", | |
| "AdaptiveInputState", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Internal input state", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyPythonObject", | |
| "AdaptiveOutputState", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Internal output state", | |
| ), | |
| ) | |
| obj.setEditorMode("AdaptiveInputState", 2) # hide this property | |
| obj.setEditorMode("AdaptiveOutputState", 2) # hide this property | |
| obj.addProperty( | |
| "App::PropertyAngle", | |
| "HelixAngle", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Helix ramp entry angle (degrees)", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyLength", | |
| "HelixMaxStepdown", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "The maximum allowable descent in a single revolution of the helix.", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyAngle", | |
| "HelixConeAngle", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Helix cone angle (degrees)", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyPercent", | |
| "HelixMaxDiameterPercent", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Maximum (and nominal) helix entry diameter, as a percentage of the tool diameter", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyPercent", | |
| "HelixMinDiameterPercent", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Minimum acceptable helix entry diameter, as a percentage of the tool diameter", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyBool", | |
| "UseOutline", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Uses the outline of the base geometry.", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyBool", | |
| "OrderCutsByRegion", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Orders cuts by region instead of depth. This property is only used if the ModelAwareExperiment is enabled.", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "Part::PropertyPartShape", | |
| "removalshape", | |
| "Path", | |
| QT_TRANSLATE_NOOP("App::Property", ""), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyBool", | |
| "ModelAwareExperiment", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Enable the experimental model awareness feature to respect 3D geometry and prevent cutting under overhangs", | |
| ), | |
| ) | |
| obj.setEditorMode("OrderCutsByRegion", 2) | |
| obj.setEditorMode("ZStockToLeave", 2) | |
| for n in self.propertyEnumerations(): | |
| setattr(obj, n[0], n[1]) | |
| 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.HelixMaxDiameterPercent = 100 | |
| obj.HelixMinDiameterPercent = 10 | |
| obj.AdaptiveInputState = "" | |
| obj.AdaptiveOutputState = "" | |
| obj.StockToLeave = 0 | |
| obj.ZStockToLeave = 0 | |
| obj.KeepToolDownRatio = 3.0 | |
| obj.UseHelixArcs = False | |
| obj.UseOutline = False | |
| obj.OrderCutsByRegion = False | |
| obj.ModelAwareExperiment = 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.""" | |
| obj.setEditorMode("OrderCutsByRegion", 0 if obj.ModelAwareExperiment else 2) | |
| obj.setEditorMode("ZStockToLeave", 0 if obj.ModelAwareExperiment else 2) | |
| if obj.ModelAwareExperiment: | |
| # Contains both geometry to machine and the applicable depths | |
| # NOTE: Reminder that stock is formatted differently than inside/outside! | |
| inside, outside, stock = _getWorkingEdgesModelAware(self, obj) | |
| self.insidePathArray = inside | |
| self.outsidePathArray = outside | |
| self.stockPathArray = stock | |
| ExecuteModelAware(self, obj) | |
| else: | |
| 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, "OrderCutsByRegion"): | |
| obj.addProperty( | |
| "App::PropertyBool", | |
| "OrderCutsByRegion", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Orders cuts by region instead of depth.", | |
| ), | |
| ) | |
| if not hasattr(obj, "ZStockToLeave"): | |
| obj.addProperty( | |
| "App::PropertyDistance", | |
| "ZStockToLeave", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "How much stock to leave along the Z axis (eg for finishing operation)", | |
| ), | |
| ) | |
| if not hasattr(obj, "ModelAwareExperiment"): | |
| obj.addProperty( | |
| "App::PropertyBool", | |
| "ModelAwareExperiment", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Enable the experimental model awareness feature to respect 3D geometry and prevent cutting under overhangs", | |
| ), | |
| ) | |
| obj.setEditorMode("OrderCutsByRegion", 0 if obj.ModelAwareExperiment else 2) | |
| obj.setEditorMode("ZStockToLeave", 0 if obj.ModelAwareExperiment else 2) | |
| if not hasattr(obj, "removalshape"): | |
| obj.addProperty("Part::PropertyPartShape", "removalshape", "Path", "") | |
| obj.setEditorMode("removalshape", 2) # hide | |
| if hasattr(obj, "HelixDiameterLimit"): | |
| oldD = obj.HelixDiameterLimit.Value | |
| obj.removeProperty("HelixDiameterLimit") | |
| obj.addProperty( | |
| "App::PropertyPercent", | |
| "HelixMaxDiameterPercent", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Maximum (and nominal) helix entry diameter, as a percentage of the tool diameter", | |
| ), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyPercent", | |
| "HelixMinDiameterPercent", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Minimum acceptable helix entry diameter, as a percentage of the tool diameter", | |
| ), | |
| ) | |
| obj.HelixMinDiameterPercent = 10 | |
| if hasattr(obj, "ToolController"): | |
| obj.HelixMaxDiameterPercent = int( | |
| 75 if oldD == 0 else 100 * oldD / obj.ToolController.Tool.Diameter.Value | |
| ) | |
| if not hasattr(obj, "HelixMaxStepdown"): | |
| obj.addProperty( | |
| "App::PropertyLength", | |
| "HelixMaxStepdown", | |
| "Adaptive", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "The maximum allowable descent in a single revolution of the helix.", | |
| ), | |
| ) | |
| FeatureExtensions.initialize_properties(obj) | |
| def SetupProperties(): | |
| setup = [ | |
| "Side", | |
| "OperationType", | |
| "Tolerance", | |
| "StepOver", | |
| "LiftDistance", | |
| "KeepToolDownRatio", | |
| "StockToLeave", | |
| "ZStockToLeave", | |
| "ForceInsideOut", | |
| "FinishingProfile", | |
| "Stopped", | |
| "StopProcessing", | |
| "UseHelixArcs", | |
| "AdaptiveInputState", | |
| "AdaptiveOutputState", | |
| "HelixAngle", | |
| "HelixConeAngle", | |
| "HelixMaxDiameterPercent", | |
| "HelixMinDiameterPercent", | |
| "UseOutline", | |
| "OrderCutsByRegion", | |
| ] | |
| 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 | |