From 1955f280536488ae08ccb25080086d490f5ad5c3 Mon Sep 17 00:00:00 2001 From: David Kaufman Date: Fri, 13 Dec 2024 12:20:32 -0500 Subject: [PATCH] [CAM] implement multipass profile operations (#17326) * implement multipass profile operations * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/Mod/CAM/App/Area.cpp | 4 -- .../Resources/panels/PageOpProfileFullEdit.ui | 37 ++++++++++ src/Mod/CAM/Path/Op/Gui/Profile.py | 11 +++ src/Mod/CAM/Path/Op/Profile.py | 68 ++++++++++++++++++- 4 files changed, 115 insertions(+), 5 deletions(-) diff --git a/src/Mod/CAM/App/Area.cpp b/src/Mod/CAM/App/Area.cpp index 6d94ea27c4..bb1969a63f 100644 --- a/src/Mod/CAM/App/Area.cpp +++ b/src/Mod/CAM/App/Area.cpp @@ -2420,14 +2420,10 @@ void Area::makeOffset(list>& areas, #endif if (offset < 0) { - stepover = -fabs(stepover); if (count < 0) { if (!last_stepover) { last_stepover = offset * 0.5; } - else { - last_stepover = -fabs(last_stepover); - } } else { last_stepover = 0; diff --git a/src/Mod/CAM/Gui/Resources/panels/PageOpProfileFullEdit.ui b/src/Mod/CAM/Gui/Resources/panels/PageOpProfileFullEdit.ui index 4b6ced799d..8c539a4a51 100644 --- a/src/Mod/CAM/Gui/Resources/panels/PageOpProfileFullEdit.ui +++ b/src/Mod/CAM/Gui/Resources/panels/PageOpProfileFullEdit.ui @@ -115,6 +115,43 @@ + + + + Number of Passes + + + + + + + 1 + + + The number of passes to do. If more than one, requires a non-zero value for Pass Stepover. + + + + + + + Pass Stepover + + + + + + + + 0 + 0 + + + + If doing multiple passes, the extra offset of each additional pass. + + + diff --git a/src/Mod/CAM/Path/Op/Gui/Profile.py b/src/Mod/CAM/Path/Op/Gui/Profile.py index a9ce036787..f7aa0aff22 100644 --- a/src/Mod/CAM/Path/Op/Gui/Profile.py +++ b/src/Mod/CAM/Path/Op/Gui/Profile.py @@ -77,6 +77,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): if obj.Direction != str(self.form.direction.currentData()): obj.Direction = str(self.form.direction.currentData()) PathGuiUtil.updateInputField(obj, "OffsetExtra", self.form.extraOffset) + obj.NumPasses = self.form.numPasses.value() + PathGuiUtil.updateInputField(obj, "Stepover", self.form.stepover) if obj.UseComp != self.form.useCompensation.isChecked(): obj.UseComp = self.form.useCompensation.isChecked() @@ -100,6 +102,10 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.form.extraOffset.setText( FreeCAD.Units.Quantity(obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString ) + self.form.numPasses.setValue(obj.NumPasses) + self.form.stepover.setText( + FreeCAD.Units.Quantity(obj.Stepover.Value, FreeCAD.Units.Length).UserString + ) self.form.useCompensation.setChecked(obj.UseComp) self.form.useStartPoint.setChecked(obj.UseStartPoint) @@ -117,6 +123,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.cutSide.currentIndexChanged) signals.append(self.form.direction.currentIndexChanged) signals.append(self.form.extraOffset.editingFinished) + signals.append(self.form.numPasses.editingFinished) + signals.append(self.form.stepover.editingFinished) signals.append(self.form.useCompensation.stateChanged) signals.append(self.form.useStartPoint.stateChanged) signals.append(self.form.processHoles.stateChanged) @@ -148,8 +156,11 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.form.processHoles.hide() self.form.processPerimeter.hide() + self.form.stepover.setEnabled(self.obj.NumPasses > 1) + def registerSignalHandlers(self, obj): self.form.useCompensation.stateChanged.connect(self.updateVisibility) + self.form.numPasses.editingFinished.connect(self.updateVisibility) # Eclass diff --git a/src/Mod/CAM/Path/Op/Profile.py b/src/Mod/CAM/Path/Op/Profile.py index b2037e5494..e19bdac9c4 100644 --- a/src/Mod/CAM/Path/Op/Profile.py +++ b/src/Mod/CAM/Path/Op/Profile.py @@ -172,6 +172,24 @@ class ObjectProfile(PathAreaOp.ObjectOp): "App::Property", "Make True, if using Cutter Radius Compensation" ), ), + ( + "App::PropertyInteger", + "NumPasses", + "Profile", + QT_TRANSLATE_NOOP( + "App::Property", + "The number of passes to do. If more than one, requires a non-zero value for Stepover", + ), + ), + ( + "App::PropertyDistance", + "Stepover", + "Profile", + QT_TRANSLATE_NOOP( + "App::Property", + "If doing multiple passes, the extra offset of each additional pass", + ), + ), ] @classmethod @@ -235,6 +253,8 @@ class ObjectProfile(PathAreaOp.ObjectOp): "processCircles": False, "processHoles": False, "processPerimeter": True, + "Stepover": 0, + "NumPasses": 1, } def areaOpApplyPropertyDefaults(self, obj, job, propList): @@ -295,6 +315,26 @@ class ObjectProfile(PathAreaOp.ObjectOp): self.initAreaOpProperties(obj, warn=True) self.areaOpSetDefaultValues(obj, PathUtils.findParentJob(obj)) self.setOpEditorProperties(obj) + if not hasattr(obj, "NumPasses"): + obj.addProperty( + "App::PropertyInteger", + "NumPasses", + "Profile", + QT_TRANSLATE_NOOP( + "App::Property", + "The number of passes to do. Requires a non-zero value for Stepover", + ), + ) + if not hasattr(obj, "Stepover"): + obj.addProperty( + "App::PropertyDistance", + "Stepover", + "Profile", + QT_TRANSLATE_NOOP( + "App::Property", + "If doing multiple passes, the extra offset of each additional pass", + ), + ) def areaOpOnChanged(self, obj, prop): """areaOpOnChanged(obj, prop) ... updates certain property visibilities depending on changed properties.""" @@ -311,13 +351,31 @@ class ObjectProfile(PathAreaOp.ObjectOp): params["SectionCount"] = -1 offset = obj.OffsetExtra.Value # 0.0 + num_passes = max(1, obj.NumPasses) + stepover = obj.Stepover.Value + if num_passes > 1 and stepover == 0: + # This check is important because C++ code has a default value for stepover if it's 0 and extra passes are requested + num_passes = 1 + Path.Log.warning( + "Multipass profile requires a non-zero stepover. Reducing to a single pass." + ) + if obj.UseComp: offset = self.radius + obj.OffsetExtra.Value if obj.Side == "Inside": offset = 0 - offset + stepover = -stepover if isHole: offset = 0 - offset + stepover = -stepover + + # Modify offset and stepover to do passes from most-offset to least + offset += stepover * (num_passes - 1) + stepover = -stepover + params["Offset"] = offset + params["ExtraPass"] = num_passes - 1 + params["Stepover"] = stepover jointype = ["Round", "Square", "Miter"] params["JoinType"] = jointype.index(obj.JoinType) @@ -356,6 +414,10 @@ class ObjectProfile(PathAreaOp.ObjectOp): else: params["orientation"] = 0 + if obj.NumPasses > 1: + # Disable path sorting to ensure that offsets appear in order, from farthest offset to closest, on all layers + params["sort_mode"] = 0 + return params def areaOpUseProjection(self, obj): @@ -590,7 +652,11 @@ class ObjectProfile(PathAreaOp.ObjectOp): if flattened and zDiff >= self.JOB.GeometryTolerance.Value: cutWireObjs = False openEdges = [] - passOffsets = [self.ofstRadius] + params = self.areaOpAreaParams(obj, False) + passOffsets = [ + self.ofstRadius + i * abs(params["Stepover"]) + for i in range(params["ExtraPass"] + 1) + ][::-1] (origWire, flatWire) = flattened self._addDebugObject("FlatWire", flatWire)