diff --git a/src/Mod/CAM/CAMTests/TestPathAdaptive.py b/src/Mod/CAM/CAMTests/TestPathAdaptive.py
index 1dfd64741a..7c4193f84b 100644
--- a/src/Mod/CAM/CAMTests/TestPathAdaptive.py
+++ b/src/Mod/CAM/CAMTests/TestPathAdaptive.py
@@ -411,6 +411,7 @@ class TestPathAdaptive(PathTestBase):
adaptive.StepOver = 75
adaptive.UseOutline = False
adaptive.setExpression("StepDown", None)
+ adaptive.ModelAwareExperiment = True
adaptive.StepDown.Value = (
5.0 # Have to set expression to None before numerical value assignment
)
@@ -542,6 +543,7 @@ class TestPathAdaptive(PathTestBase):
adaptive.StepOver = 75
adaptive.UseOutline = False
adaptive.setExpression("StepDown", None)
+ adaptive.ModelAwareExperiment = True
adaptive.StepDown.Value = (
5.0 # Have to set expression to None before numerical value assignment
)
@@ -623,6 +625,7 @@ class TestPathAdaptive(PathTestBase):
adaptive.StepOver = 75
adaptive.UseOutline = False
adaptive.setExpression("StepDown", None)
+ adaptive.ModelAwareExperiment = True
adaptive.StepDown.Value = (
5.0 # Have to set expression to None before numerical value assignment
)
diff --git a/src/Mod/CAM/Gui/Resources/panels/PageOpAdaptiveEdit.ui b/src/Mod/CAM/Gui/Resources/panels/PageOpAdaptiveEdit.ui
index b1f31d1dd5..d2b1d43549 100644
--- a/src/Mod/CAM/Gui/Resources/panels/PageOpAdaptiveEdit.ui
+++ b/src/Mod/CAM/Gui/Resources/panels/PageOpAdaptiveEdit.ui
@@ -279,32 +279,6 @@ Larger values (further to the right) will calculate faster; smaller values (furt
- -
-
-
- After calculating toolpaths, the default cut order is by depth- all regions at a given stepdown are cleared before moving to the next stepdown.
-
-This option changes that behavior to cut each discrete area to its full depth before moving on to the next.
-
-
- Order cuts by region
-
-
-
- -
-
-
- Z stock to leave
-
-
-
- -
-
-
- How much material to leave along the Z axis (i.e. for finishing operation)
-
-
-
diff --git a/src/Mod/CAM/Path/Op/Adaptive.py b/src/Mod/CAM/Path/Op/Adaptive.py
index 3cc5fc6db2..7f0e449f7c 100644
--- a/src/Mod/CAM/Path/Op/Adaptive.py
+++ b/src/Mod/CAM/Path/Op/Adaptive.py
@@ -151,15 +151,15 @@ def GenerateGCode(op, obj, adaptiveResults, helixDiameter):
lz = obj.StartDepth.Value
for region in adaptiveResults:
- passStartDepth = region["TopDepth"]
+ passStartDepth = region.get("TopDepth", obj.StartDepth.Value)
depthParams = PathUtils.depth_params(
clearance_height=obj.ClearanceHeight.Value,
safe_height=obj.SafeHeight.Value,
- start_depth=region["TopDepth"],
+ start_depth=passStartDepth,
step_down=stepDown,
z_finish_step=finishStep,
- final_depth=region["BottomDepth"],
+ final_depth=region.get("BottomDepth", obj.FinalDepth.Value),
user_depths=None,
)
@@ -523,6 +523,168 @@ def Execute(op, obj):
# 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:
+ helixDiameter = obj.HelixDiameterLimit.Value
+ topZ = op.stock.Shape.BoundBox.ZMax
+ obj.Stopped = False
+ obj.StopProcessing = False
+ if obj.Tolerance < 0.001:
+ obj.Tolerance = 0.001
+
+ # Get list of working edges for adaptive algorithm
+ pathArray = op.pathArray
+ if not pathArray:
+ 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),
+ "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.helixRampDiameter = helixDiameter
+ a2d.keepToolDownDistRatio = keepToolDownRatio
+ a2d.stockToLeave = float(obj.StockToLeave)
+ a2d.tolerance = float(obj.Tolerance)
+ a2d.forceInsideOut = obj.ForceInsideOut
+ a2d.finishingProfile = obj.FinishingProfile
+ a2d.opType = opType
+
+ # EXECUTE
+ results = a2d.Execute(stockPath2d, path2d, progressFn)
+
+ # need to convert results to python object to be JSON serializable
+ adaptiveResults = []
+ for result in results:
+ adaptiveResults.append(
+ {
+ "HelixCenterPoint": result.HelixCenterPoint,
+ "StartPoint": result.StartPoint,
+ "AdaptivePaths": result.AdaptivePaths,
+ "ReturnMotionType": result.ReturnMotionType,
+ }
+ )
+
+ # GENERATE
+ GenerateGCode(op, obj, adaptiveResults, helixDiameter)
+
+ if not obj.StopProcessing:
+ 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)
@@ -637,6 +799,7 @@ def Execute(op, obj):
"stockToLeave": obj.StockToLeave.Value,
"zStockToLeave": obj.ZStockToLeave.Value,
"orderCutsByRegion": obj.OrderCutsByRegion,
+ "modelAwareExperiment": obj.ModelAwareExperiment,
}
inputStateObject = [outsideInputStateObject, insideInputStateObject]
@@ -1178,8 +1341,75 @@ def _workingEdgeHelperManual(op, obj, depths):
return insideRegions, outsideRegions
-def _getWorkingEdges(op, obj):
- """_getWorkingEdges(op, obj)...
+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,
@@ -1492,7 +1722,7 @@ class PathAdaptive(PathOp.ObjectOp):
"Adaptive",
QT_TRANSLATE_NOOP(
"App::Property",
- "How much stock to leave along the Z axis (eg for finishing operation)",
+ "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(
@@ -1601,7 +1831,7 @@ class PathAdaptive(PathOp.ObjectOp):
"Adaptive",
QT_TRANSLATE_NOOP(
"App::Property",
- "Orders cuts by region instead of depth.",
+ "Orders cuts by region instead of depth. This property is only used if the ModelAwareExperiment is enabled.",
),
)
obj.addProperty(
@@ -1610,6 +1840,17 @@ class PathAdaptive(PathOp.ObjectOp):
"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", 0 if obj.ModelAwareExperiment else 2)
+ obj.setEditorMode("ZStockToLeave", 0 if obj.ModelAwareExperiment else 2)
for n in self.propertyEnumerations():
setattr(obj, n[0], n[1])
@@ -1640,6 +1881,7 @@ class PathAdaptive(PathOp.ObjectOp):
obj.UseHelixArcs = False
obj.UseOutline = False
obj.OrderCutsByRegion = False
+ obj.ModelAwareExperiment = False
FeatureExtensions.set_default_property_values(obj, job)
def opExecute(self, obj):
@@ -1647,15 +1889,22 @@ class PathAdaptive(PathOp.ObjectOp):
See documentation of execute() for a list of base functionality provided.
Should be overwritten by subclasses."""
- # Contains both geometry to machine and the applicable depths
- # NOTE: Reminder that stock is formatted differently than inside/outside!
- inside, outside, stock = _getWorkingEdges(self, obj)
+ obj.setEditorMode("OrderCutsByRegion", 0 if obj.ModelAwareExperiment else 2)
+ obj.setEditorMode("ZStockToLeave", 0 if obj.ModelAwareExperiment else 2)
- self.insidePathArray = inside
- self.outsidePathArray = outside
- self.stockPathArray = stock
+ 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)
- Execute(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"):
@@ -1696,6 +1945,19 @@ class PathAdaptive(PathOp.ObjectOp):
),
)
+ 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
diff --git a/src/Mod/CAM/Path/Op/Gui/Adaptive.py b/src/Mod/CAM/Path/Op/Gui/Adaptive.py
index 4eb2bb2c71..eeed4516f1 100644
--- a/src/Mod/CAM/Path/Op/Gui/Adaptive.py
+++ b/src/Mod/CAM/Path/Op/Gui/Adaptive.py
@@ -49,7 +49,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
)
self.form.KeepToolDownRatio.setProperty("unit", obj.KeepToolDownRatio.getUserPreferred()[2])
self.form.StockToLeave.setProperty("unit", obj.StockToLeave.getUserPreferred()[2])
- self.form.ZStockToLeave.setProperty("unit", obj.ZStockToLeave.getUserPreferred()[2])
def getSignalsForUpdate(self, obj):
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
@@ -65,18 +64,15 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
signals.append(self.form.LiftDistance.valueChanged)
signals.append(self.form.KeepToolDownRatio.valueChanged)
signals.append(self.form.StockToLeave.valueChanged)
- signals.append(self.form.ZStockToLeave.valueChanged)
signals.append(self.form.coolantController.currentIndexChanged)
if hasattr(self.form.ForceInsideOut, "checkStateChanged"): # Qt version >= 6.7.0
signals.append(self.form.ForceInsideOut.checkStateChanged)
signals.append(self.form.FinishingProfile.checkStateChanged)
signals.append(self.form.useOutline.checkStateChanged)
- signals.append(self.form.orderCutsByRegion.checkStateChanged)
else: # Qt version < 6.7.0
signals.append(self.form.ForceInsideOut.stateChanged)
signals.append(self.form.FinishingProfile.stateChanged)
signals.append(self.form.useOutline.stateChanged)
- signals.append(self.form.orderCutsByRegion.stateChanged)
signals.append(self.form.StopButton.toggled)
return signals
@@ -105,13 +101,9 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
if hasattr(obj, "StockToLeave"):
self.form.StockToLeave.setProperty("rawValue", obj.StockToLeave.Value)
- if hasattr(obj, "ZStockToLeave"):
- self.form.ZStockToLeave.setProperty("rawValue", obj.ZStockToLeave.Value)
-
self.form.ForceInsideOut.setChecked(obj.ForceInsideOut)
self.form.FinishingProfile.setChecked(obj.FinishingProfile)
self.form.useOutline.setChecked(obj.UseOutline)
- self.form.orderCutsByRegion.setChecked(obj.OrderCutsByRegion)
self.setupToolController(obj, self.form.toolController)
self.setupCoolant(obj, self.form.coolantController)
self.form.StopButton.setChecked(obj.Stopped)
@@ -142,13 +134,9 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
if hasattr(obj, "StockToLeave"):
PathGuiUtil.updateInputField(obj, "StockToLeave", self.form.StockToLeave)
- if hasattr(obj, "ZStockToLeave"):
- PathGuiUtil.updateInputField(obj, "ZStockToLeave", self.form.ZStockToLeave)
-
obj.ForceInsideOut = self.form.ForceInsideOut.isChecked()
obj.FinishingProfile = self.form.FinishingProfile.isChecked()
obj.UseOutline = self.form.useOutline.isChecked()
- obj.OrderCutsByRegion = self.form.orderCutsByRegion.isChecked()
obj.Stopped = self.form.StopButton.isChecked()
if obj.Stopped:
self.form.StopButton.setChecked(False) # reset the button