| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | 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." |
| |
|
| | |
| | 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 = [] |
| | topZ = 10 |
| |
|
| | |
| | _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) |
| |
|
| |
|
| | 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 |
| |
|
| | |
| | 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) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | finishStep = min(obj.FinishDepth.Value, stepDown) if hasattr(obj, "FinishDepth") else 0.0 |
| |
|
| | |
| | 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]) |
| |
|
| | |
| | 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 |
| | ) |
| | 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), |
| | ] |
| |
|
| | |
| | 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, |
| | }, |
| | ) |
| | ) |
| |
|
| | |
| | op.commandlist.append( |
| | Path.Command( |
| | "G0", |
| | { |
| | "X": helixStart[0], |
| | "Y": helixStart[1], |
| | "Z": obj.SafeHeight.Value, |
| | }, |
| | ) |
| | ) |
| |
|
| | |
| | op.commandlist.append( |
| | Path.Command( |
| | "G1", |
| | { |
| | "X": helixStart[0], |
| | "Y": helixStart[1], |
| | "Z": passStartDepth, |
| | "F": op.vertFeed, |
| | }, |
| | ) |
| | ) |
| |
|
| | |
| | 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 |
| | ) |
| |
|
| | |
| | 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: |
| | |
| | helixStart = [ |
| | region["HelixCenterPoint"][0] + r, |
| | region["HelixCenterPoint"][1], |
| | ] |
| |
|
| | |
| | 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, |
| | }, |
| | ) |
| | ) |
| |
|
| | |
| | op.commandlist.append( |
| | Path.Command( |
| | "G0", |
| | { |
| | "X": helixStart[0], |
| | "Y": helixStart[1], |
| | "Z": obj.SafeHeight.Value, |
| | }, |
| | ) |
| | ) |
| |
|
| | |
| | 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}, |
| | ) |
| | ) |
| |
|
| | |
| | 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: |
| | |
| | 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, |
| | }, |
| | ) |
| | ) |
| | |
| | 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)) |
| |
|
| | |
| | for pth in region["AdaptivePaths"]: |
| | motionType = pth[0] |
| |
|
| | for pt in pth[1]: |
| | 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 |
| |
|
| | |
| | z = obj.ClearanceHeight.Value |
| | if z != lz: |
| | op.commandlist.append(Path.Command("G0", {"Z": z})) |
| |
|
| | lz = z |
| |
|
| | passStartDepth = passEndDepth |
| |
|
| | |
| | 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") |
| |
|
| | |
| | obj.Path = Path.Path("(Calculating...)") |
| |
|
| | oldObjVisibility = oldJobVisibility = False |
| | if FreeCAD.GuiUp: |
| | |
| | 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 |
| |
|
| | |
| | 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) |
| |
|
| | |
| | |
| | outer_wire = TechDraw.findShapeOutline(op.stock.Shape, 1, FreeCAD.Vector(0, 0, 1)) |
| | stockPaths = [[discretize(outer_wire)]] |
| |
|
| | stockPath2d = convertTo2d(stockPaths) |
| |
|
| | |
| | if obj.OperationType == "Clearing": |
| | if obj.Side == "Outside": |
| | opType = area.AdaptiveOperationType.ClearingOutside |
| |
|
| | else: |
| | opType = area.AdaptiveOperationType.ClearingInside |
| |
|
| | else: |
| | if obj.Side == "Outside": |
| | opType = area.AdaptiveOperationType.ProfilingOutside |
| |
|
| | else: |
| | opType = area.AdaptiveOperationType.ProfilingInside |
| |
|
| | keepToolDownRatio = 3.0 |
| | if hasattr(obj, "KeepToolDownRatio"): |
| | keepToolDownRatio = float(obj.KeepToolDownRatio) |
| |
|
| | |
| |
|
| | 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 |
| |
|
| | |
| | def progressFn(tpaths): |
| | if FreeCAD.GuiUp: |
| | for ( |
| | path |
| | ) in tpaths: |
| | 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 |
| |
|
| | |
| | results = a2d.Execute(stockPath2d, path2d, progressFn) |
| |
|
| | |
| | adaptiveResults = [] |
| | for result in results: |
| | adaptiveResults.append( |
| | { |
| | "HelixCenterPoint": result.HelixCenterPoint, |
| | "StartPoint": result.StartPoint, |
| | "AdaptivePaths": result.AdaptivePaths, |
| | "ReturnMotionType": result.ReturnMotionType, |
| | } |
| | ) |
| |
|
| | |
| | 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") |
| |
|
| | |
| | obj.Path = Path.Path("(Calculating...)") |
| |
|
| | oldObjVisibility = oldJobVisibility = False |
| | if FreeCAD.GuiUp: |
| | |
| | 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) |
| |
|
| | |
| | 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 |
| |
|
| | |
| | |
| | |
| | |
| | |
| | regionOps = list() |
| | outsidePathArray2dDepthTuples = list() |
| | insidePathArray2dDepthTuples = list() |
| | |
| | |
| | |
| | |
| | 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"], |
| | |
| | |
| | "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"], |
| | |
| | |
| | "startdepth": rdict["depths"][0], |
| | } |
| | ) |
| | insidePathArray2dDepthTuples.append( |
| | (sorted(rdict["depths"], reverse=True), regionOps[-1]) |
| | ) |
| |
|
| | keepToolDownRatio = 3.0 |
| | if hasattr(obj, "KeepToolDownRatio"): |
| | keepToolDownRatio = obj.KeepToolDownRatio.Value |
| |
|
| | |
| | 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 obj.AdaptiveOutputState: |
| | adaptiveResults = obj.AdaptiveOutputState |
| |
|
| | |
| | |
| | if json.dumps(obj.AdaptiveInputState) != json.dumps(inputStateObject): |
| | inputStateChanged = True |
| | adaptiveResults = None |
| |
|
| | |
| | def progressFn(tpaths): |
| | if FreeCAD.GuiUp: |
| | for ( |
| | path |
| | ) in tpaths: |
| | 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: |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | 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 |
| | |
| | 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 |
| | ) |
| |
|
| | |
| | |
| | cutlist = list() |
| | |
| | cutids = list() |
| | |
| | |
| | depths = list() |
| | |
| | alltuples = outsidePathArray2dDepthTuples + insidePathArray2dDepthTuples |
| | for t in alltuples: |
| | depths += [d for d in t[0]] |
| | depths = sorted(list(set(depths)), reverse=True) |
| | if obj.OrderCutsByRegion: |
| | |
| | |
| | for rdict in regionOps: |
| | rdict["childTuples"] = [t for t in alltuples if t[1]["id"] in rdict["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]] |
| |
|
| | |
| | stepdown = max(obj.StepDown.Value, _ADAPTIVE_MIN_STEPDOWN) |
| | adaptiveResults = list() |
| | for depths, region in cutlist: |
| | for result in region["toolpaths"]: |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | 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], |
| | } |
| | ) |
| |
|
| | |
| | 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: |
| | |
| | |
| | if Path.Geom.isVertical(f) and type(f.Surface) not in [Part.Cone, Part.Sphere]: |
| | continue |
| |
|
| | |
| | projface = Path.Geom.makeBoundBoxFace(f.BoundBox, offset=1, zHeight=0) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if type(f.Surface) in [Part.Cone, Part.Cylinder, Part.Sphere]: |
| | |
| | |
| | |
| | oface = Part.makeFace(TechDraw.findShapeOutline(f, 1, projdir)) |
| |
|
| | |
| | |
| | |
| | endfacewires = DraftGeomUtils.findWires( |
| | [e for e in f.Edges if not e.isSeam(f) and e.Length > minEdgeLength] |
| | ) |
| |
|
| | |
| | |
| | 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) |
| | |
| | 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:]) |
| | |
| | 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 |
| |
|
| | |
| | |
| | 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): |
| | |
| | |
| | |
| | insideRegions = list() |
| | outsideRegions = list() |
| |
|
| | |
| | |
| | shps = op.model[0].Shape.fuse([k.Shape for k in op.model[1:]]) |
| |
|
| | projdir = FreeCAD.Vector(0, 0, 1) |
| |
|
| | |
| | |
| | modelOutlineFaces = [ |
| | Part.makeFace(TechDraw.findShapeOutline(s, 1, projdir)) for s in shps.Solids |
| | ] |
| |
|
| | lastdepth = obj.StartDepth.Value |
| |
|
| | for depth in depths: |
| | |
| | if depth >= op.stock.Shape.BoundBox.ZMax: |
| | lastdepth = depth |
| | continue |
| |
|
| | |
| | |
| | |
| | |
| | stockface = _getSolidProjection(op.stock.Shape, depth - obj.ZStockToLeave.Value) |
| | aboveRefined = _getSolidProjection(shps, depth - obj.ZStockToLeave.Value) |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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 ( |
| | 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]}) |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | if aboveModelFaces.Wires: |
| | |
| | outsideface = stockface.cut(outsideRegions[-1]["region"].Faces) |
| | |
| | if outsideface.Wires: |
| | belowFaces = [f.cut(outsideface) for f in modelOutlineFaces] |
| | else: |
| | |
| | |
| | belowFaces = [f for f in modelOutlineFaces] |
| |
|
| | |
| | |
| | |
| | if belowFaces: |
| | |
| | belowCut = belowFaces[0].fuse(belowFaces[1:]).cut(aboveRefined) |
| | |
| | if belowCut.Wires: |
| | |
| | |
| | finalCut = DraftGeomUtils.concatenate(belowCut.removeSplitter()) |
| | else: |
| | finalCut = Part.Shape() |
| | else: |
| | |
| | finalCut = Part.Shape() |
| |
|
| | |
| | |
| | |
| | for f in finalCut.Faces: |
| | addNew = True |
| | |
| | newtop = lastdepth |
| | for rdict in insideRegions: |
| | |
| | if not rdict["region"].cut(f).Wires: |
| | rdict["depths"].append(depth) |
| | addNew = False |
| | break |
| | if addNew: |
| | insideRegions.append({"region": f, "depths": [depth]}) |
| |
|
| | |
| | lastdepth = depth |
| | |
| |
|
| | return insideRegions, outsideRegions |
| |
|
| |
|
| | def _workingEdgeHelperManual(op, obj, depths): |
| | |
| | |
| | |
| | insideRegions = list() |
| | outsideRegions = list() |
| |
|
| | |
| | selectedRegions = list() |
| | selectedEdges = list() |
| |
|
| | |
| | extensions = FeatureExtensions.getExtensions(obj) |
| | avoidFeatures = [e for e in extensions if e.avoid] |
| |
|
| | |
| | for ext in extensions: |
| | if not ext.avoid: |
| | if wire := ext.getWire(): |
| | |
| | |
| | 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) |
| | |
| | |
| | elif sub.startswith("Edge") and not Path.Geom.isVertical(element): |
| | selectedEdges.append(element) |
| |
|
| | |
| | |
| | shps = op.model[0].Shape.fuse([k.Shape for k in op.model[1:]]) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | projface = Path.Geom.makeBoundBoxFace(shps.BoundBox, offset=1, zHeight=0) |
| | projdir = FreeCAD.Vector(0, 0, 1) |
| |
|
| | |
| | |
| | |
| | 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 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 depth >= op.stock.Shape.BoundBox.ZMax: |
| | lastdepth = depth |
| | continue |
| |
|
| | |
| | |
| | aboveRefined = _getSolidProjection(shps, depth - obj.ZStockToLeave.Value) |
| |
|
| | |
| | |
| | if obj.Side == "Outside": |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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 |
| | |
| | |
| | |
| | regions = DraftGeomUtils.concatenate(finalMerge.removeSplitter()) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | 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]}) |
| |
|
| | |
| | |
| | |
| | else: |
| | if aboveRefined.Wires: |
| | finalCut = selectedRefined.cut(aboveRefined) |
| | else: |
| | finalCut = selectedRefined |
| |
|
| | |
| | |
| | |
| | for f in finalCut.Faces: |
| | addNew = True |
| | |
| | newtop = lastdepth |
| | for rdict in insideRegions: |
| | |
| | if not rdict["region"].cut(f).Wires: |
| | rdict["depths"].append(depth) |
| | addNew = False |
| | break |
| | if addNew: |
| | insideRegions.append({"region": f, "depths": [depth]}) |
| |
|
| | |
| | lastdepth = 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() |
| |
|
| | |
| | extensions = FeatureExtensions.getExtensions(obj) |
| | for e in extensions: |
| | if e.avoid: |
| | avoidFeatures.append(e.feature) |
| |
|
| | |
| | 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) |
| | |
| | |
| | wire_B = face.OuterWire |
| | shape = Part.Face(wire_B) |
| | else: |
| | shape = base.Shape.getElement(sub) |
| | all_regions.append(shape) |
| | elif sub.startswith("Edge"): |
| | |
| | rawEdges.append(base.Shape.getElement(sub)) |
| | |
| |
|
| | |
| | if rawEdges: |
| | edgeWires = DraftGeomUtils.findWires(rawEdges) |
| | if edgeWires: |
| | for w in edgeWires: |
| | for e in w.Edges: |
| | edge_list.append([discretize(e)]) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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 |
| | """ |
| |
|
| | |
| | |
| | |
| | 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, |
| | ) |
| |
|
| | |
| | |
| | |
| | depths = [d for d in depthParams.data if d - op.stock.Shape.BoundBox.ZMax < -obj.Tolerance] |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | stockProjectionDict = { |
| | d: _getSolidProjection(op.stock.Shape, d - obj.ZStockToLeave.Value) for d in depths |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | if obj.Base: |
| | insideRegions, outsideRegions = _workingEdgeHelperManual(op, obj, depths) |
| | else: |
| | insideRegions, outsideRegions = _workingEdgeHelperRoughing(op, obj, depths) |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | idnumber = 0 |
| | for r in insideRegions + outsideRegions: |
| | r["id"] = idnumber |
| | r["children"] = list() |
| | idnumber += 1 |
| |
|
| | |
| | |
| | for rx in insideRegions: |
| | for ry in [k for k in insideRegions if k != rx]: |
| | dist = min(rx["depths"]) - max(ry["depths"]) |
| | |
| | if dist <= 0 or dist > depthParams.step_down: |
| | continue |
| | if not ry["region"].cut(rx["region"]).Wires: |
| | rx["children"].append(ry["id"]) |
| | |
| | for ry in [k for k in outsideRegions]: |
| | dist = min(ry["depths"]) - max(rx["depths"]) |
| | |
| | if dist <= 0 or dist > depthParams.step_down: |
| | continue |
| | |
| | |
| | |
| | |
| | if not ry["region"].Wires or not rx["region"].common(ry["region"]).Wires: |
| | ry["children"].append(rx["id"]) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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: |
| | |
| | |
| | |
| | |
| | if not r["region"].Wires: |
| | rcut = stockProjectionDict[depths[0]] |
| | else: |
| | rcut = stockProjectionDict[depths[0]].cut(r["region"]) |
| |
|
| | |
| | |
| | |
| | if not rcut.Wires: |
| | continue |
| |
|
| | |
| | |
| | |
| | |
| | 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"], |
| | } |
| | |
| | |
| | r["children"] = [idnumber] |
| | r["depths"] = parentdepths |
| |
|
| | |
| | |
| | |
| | |
| | regions.append(newregion) |
| | idnumber += 1 |
| | continue |
| | |
| | |
| | parentdepths.append(d) |
| |
|
| | _regionChildSplitterHelper(insideRegions, True) |
| | _regionChildSplitterHelper(outsideRegions, False) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | |
| | 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 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 |
| | ) |
| |
|
| | @classmethod |
| | 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 |
| | """ |
| |
|
| | |
| | enums = { |
| | "Side": [ |
| | (translate("CAM_Adaptive", "Outside"), "Outside"), |
| | (translate("CAM_Adaptive", "Inside"), "Inside"), |
| | ], |
| | "OperationType": [ |
| | (translate("CAM_Adaptive", "Clearing"), "Clearing"), |
| | (translate("CAM_Adaptive", "Profiling"), "Profiling"), |
| | ], |
| | } |
| |
|
| | 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) |
| | obj.addProperty( |
| | "App::PropertyBool", |
| | "StopProcessing", |
| | "Adaptive", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Stop processing", |
| | ), |
| | ) |
| | obj.setEditorMode("StopProcessing", 2) |
| | 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) |
| | obj.setEditorMode("AdaptiveOutputState", 2) |
| | 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) |
| |
|
| | 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.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: |
| | |
| | |
| | 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) |
| |
|
| | 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 |
| |
|