CAM: Adaptive: Add Z stock to leave (separate from XY stock to leave) and order-by-region/order-by-depth cut ordering options

This commit is contained in:
Dan Taylor
2025-04-02 20:47:44 -05:00
parent eece614172
commit 31ca3e742f
4 changed files with 411 additions and 154 deletions

View File

@@ -628,6 +628,8 @@ def Execute(op, obj):
{
"opType": outsideOpType,
"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],
@@ -641,6 +643,8 @@ def Execute(op, obj):
{
"opType": insideOpType,
"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],
@@ -668,6 +672,8 @@ def Execute(op, obj):
"finishingProfile": obj.FinishingProfile,
"keepToolDownRatio": keepToolDownRatio,
"stockToLeave": obj.StockToLeave.Value,
"zStockToLeave": obj.ZStockToLeave.Value,
"orderCutsByRegion": obj.OrderCutsByRegion,
}
insideInputStateObject = {
@@ -683,6 +689,8 @@ def Execute(op, obj):
"finishingProfile": obj.FinishingProfile,
"keepToolDownRatio": keepToolDownRatio,
"stockToLeave": obj.StockToLeave.Value,
"zStockToLeave": obj.ZStockToLeave.Value,
"orderCutsByRegion": obj.OrderCutsByRegion,
}
inputStateObject = [outsideInputStateObject, insideInputStateObject]
@@ -740,6 +748,7 @@ def Execute(op, obj):
a2d.toolDiameter = op.tool.Diameter.Value
a2d.helixRampDiameter = helixDiameter
a2d.keepToolDownDistRatio = keepToolDownRatio
# NOTE: Z stock is handled in our stepdowns
a2d.stockToLeave = obj.StockToLeave.Value
a2d.tolerance = obj.Tolerance
a2d.forceInsideOut = obj.ForceInsideOut
@@ -762,9 +771,25 @@ def Execute(op, obj):
for t in alltuples:
depths += [d for d in t[0]]
depths = sorted(list(set(depths)), reverse=True)
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]]
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)
@@ -972,7 +997,9 @@ def _workingEdgeHelperManual(op, obj, depths):
lastdepth = depth
continue
aboveRefined = _getSolidProjection(shps, depth)
# NOTE: 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
@@ -1079,10 +1106,14 @@ def _getWorkingEdges(op, obj):
# Get the stock outline at each stepdown. Used to calculate toolpaths and
# for calcuating cut regions in some instances
# NOTE: 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) for d in depths}
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
@@ -1092,6 +1123,101 @@ def _getWorkingEdges(op, obj):
# to be avoided at those depths.
insideRegions, outsideRegions = _workingEdgeHelperManual(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"])
parentdepths = depths[0:1]
# 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.
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()
@@ -1100,6 +1226,8 @@ def _getWorkingEdges(op, obj):
{
"edges": [[discretize(w)] for w in rdict["region"].Wires],
"depths": rdict["depths"],
"id": rdict["id"],
"children": rdict["children"],
}
)
return discretizedRegions
@@ -1190,11 +1318,6 @@ class PathAdaptive(PathOp.ObjectOp):
"Side of selected faces that tool should cut",
),
)
# obj.Side = [
# "Outside",
# "Inside",
# ] # side of profile that cutter is on in relation to direction of profile
obj.addProperty(
"App::PropertyEnumeration",
"OperationType",
@@ -1204,11 +1327,6 @@ class PathAdaptive(PathOp.ObjectOp):
"Type of adaptive operation",
),
)
# obj.OperationType = [
# "Clearing",
# "Profiling",
# ] # side of profile that cutter is on in relation to direction of profile
obj.addProperty(
"App::PropertyFloat",
"Tolerance",
@@ -1251,7 +1369,16 @@ class PathAdaptive(PathOp.ObjectOp):
"Adaptive",
QT_TRANSLATE_NOOP(
"App::Property",
"How much stock to leave (i.e. for finishing operation)",
"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)",
),
)
obj.addProperty(
@@ -1279,7 +1406,6 @@ class PathAdaptive(PathOp.ObjectOp):
QT_TRANSLATE_NOOP("App::Property", "Stop processing"),
)
obj.setEditorMode("Stopped", 2) # hide this property
obj.addProperty(
"App::PropertyBool",
"StopProcessing",
@@ -1290,7 +1416,6 @@ class PathAdaptive(PathOp.ObjectOp):
),
)
obj.setEditorMode("StopProcessing", 2) # hide this property
obj.addProperty(
"App::PropertyBool",
"UseHelixArcs",
@@ -1300,7 +1425,6 @@ class PathAdaptive(PathOp.ObjectOp):
"Use Arcs (G2) for helix ramp",
),
)
obj.addProperty(
"App::PropertyPythonObject",
"AdaptiveInputState",
@@ -1348,7 +1472,6 @@ class PathAdaptive(PathOp.ObjectOp):
"Limit helix entry diameter, if limit larger than tool diameter or 0, tool diameter is used",
),
)
obj.addProperty(
"App::PropertyBool",
"UseOutline",
@@ -1358,7 +1481,15 @@ class PathAdaptive(PathOp.ObjectOp):
"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.",
),
)
obj.addProperty(
"Part::PropertyPartShape",
"removalshape",
@@ -1390,9 +1521,11 @@ class PathAdaptive(PathOp.ObjectOp):
obj.AdaptiveInputState = ""
obj.AdaptiveOutputState = ""
obj.StockToLeave = 0
obj.ZStockToLeave = 0
obj.KeepToolDownRatio = 3.0
obj.UseHelixArcs = False
obj.UseOutline = False
obj.OrderCutsByRegion = False
FeatureExtensions.set_default_property_values(obj, job)
def opExecute(self, obj):
@@ -1427,6 +1560,28 @@ class PathAdaptive(PathOp.ObjectOp):
"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, "removalshape"):
obj.addProperty("Part::PropertyPartShape", "removalshape", "Path", "")
obj.setEditorMode("removalshape", 2) # hide
@@ -1446,6 +1601,7 @@ def SetupProperties():
"LiftDistance",
"KeepToolDownRatio",
"StockToLeave",
"ZStockToLeave",
"ForceInsideOut",
"FinishingProfile",
"Stopped",
@@ -1457,6 +1613,7 @@ def SetupProperties():
"HelixConeAngle",
"HelixDiameterLimit",
"UseOutline",
"OrderCutsByRegion",
]
return setup

View File

@@ -50,6 +50,7 @@ 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,10 +66,12 @@ 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)
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
@@ -97,9 +100,13 @@ 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)
@@ -130,9 +137,13 @@ 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