[CAM] make the adaptive model aware feature optional (#24553)
* [CAM] make the adaptive model aware feature optional * fix CAM tests * placate github codql * remove model-aware-only properties from the adaptive task ui panel
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -279,32 +279,6 @@ Larger values (further to the right) will calculate faster; smaller values (furt
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="26" column="0">
|
||||
<widget class="QCheckBox" name="orderCutsByRegion">
|
||||
<property name="toolTip">
|
||||
<string>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.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Order cuts by region</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="21" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Z stock to leave</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="21" column="1">
|
||||
<widget class="Gui::QuantitySpinBox" name="ZStockToLeave" native="true">
|
||||
<property name="toolTip">
|
||||
<string>How much material to leave along the Z axis (i.e. for finishing operation)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user