From 41d20a62cd8227e94b843a3f3dca4a682bd60766 Mon Sep 17 00:00:00 2001 From: PhaseLoop Date: Thu, 20 Mar 2025 12:48:52 +0100 Subject: [PATCH 1/9] Replace Array operation with array dressup --- src/Mod/CAM/Path/Dressup/Array.py | 379 ++++++++++++++++++++++++++ src/Mod/CAM/Path/Dressup/Base.py | 93 +++++++ src/Mod/CAM/Path/Dressup/Gui/Array.py | 103 +++++++ src/Mod/CAM/Path/GuiInit.py | 1 + src/Mod/CAM/Path/Op/Gui/Array.py | 15 + 5 files changed, 591 insertions(+) create mode 100644 src/Mod/CAM/Path/Dressup/Array.py create mode 100644 src/Mod/CAM/Path/Dressup/Base.py create mode 100644 src/Mod/CAM/Path/Dressup/Gui/Array.py diff --git a/src/Mod/CAM/Path/Dressup/Array.py b/src/Mod/CAM/Path/Dressup/Array.py new file mode 100644 index 0000000000..7a73811232 --- /dev/null +++ b/src/Mod/CAM/Path/Dressup/Array.py @@ -0,0 +1,379 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2015 Yorik van Havre * +# * Reimplemented as dressup in 2025 phaseloop 0: + ang = self.angle / self.copies * (1 + i) + + pl = FreeCAD.Placement() + pl.rotate(self.centre, FreeCAD.Vector(0, 0, 1), ang) + np = PathUtils.applyPlacementToPath(pl, PathUtils.getPathWithPlacement(base)) + output += np.toGCode() + + # return output + return Path.Path(output) + + +def Create(base, name="DressupArray"): + """Create(base, name='DressupPathBoundary') ... creates a dressup array.""" + + if not base.isDerivedFrom("Path::Feature"): + Path.Log.error( + translate("CAM_DressupArray", "The selected object is not a path") + "\n" + ) + return None + + obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) + job = PathUtils.findParentJob(base) + obj.Proxy = DressupArray(obj, base, job) + job.Proxy.addOperation(obj, base, True) + return obj diff --git a/src/Mod/CAM/Path/Dressup/Base.py b/src/Mod/CAM/Path/Dressup/Base.py new file mode 100644 index 0000000000..abf86550ec --- /dev/null +++ b/src/Mod/CAM/Path/Dressup/Base.py @@ -0,0 +1,93 @@ +from PySide.QtCore import QT_TRANSLATE_NOOP + +from Path.Op.Base import ObjectOp + +class DressupBase: + """ + Base class for all dressups to provide common interface with the rest of CAM + One major example is making sure all dressups export base operation settings + like coolant, tool controller, etc. + """ + + def setup_coolant_property(self, obj): + if not hasattr(obj, "CoolantMode"): + obj.addProperty( + "App::PropertyEnumeration", + "CoolantMode", + "CoolantMode", + QT_TRANSLATE_NOOP("App::Property", "Default coolant mode."), + ) + + for n in ObjectOp.opPropertyEnumerations(): + if n[0] == "CoolantMode": + setattr(obj, n[0], n[1]) + + def setup_tool_controller_property(self, obj): + if not hasattr(obj, "ToolController"): + obj.addProperty( + "App::PropertyLink", + "ToolController", + "Path", + QT_TRANSLATE_NOOP( + "App::Property", + "The tool controller that will be used to calculate the path", + ), + ) + + def __init__(self, obj, base): + + obj.addProperty( + "App::PropertyLink", + "Base", + "Base", + QT_TRANSLATE_NOOP("App::Property", "The base path to modify"), + ) + + obj.addProperty( + "App::PropertyBool", + "Active", + "Path", + QT_TRANSLATE_NOOP( + "App::Property", "Make False, to prevent operation from generating code" + ), + ) + + self.setup_coolant_property(obj) + self.setup_tool_controller_property(obj) + + def onDocumentRestored(self, obj): + """ + Called then document is being restored. Often used for object migrations, + adding missing properties, etc. + Do not overwrite - child classes should use dressupOnDocumentRestored(). + """ + self.setup_coolant_property(obj) + self.setup_tool_controller_property(obj) + + def dressupOnDocumentRestored(self, obj): + """Overwrite this method for custom handling.""" + pass + + def execute(self, obj): + """ + Export common properties from base object and + run dressupExecute() + """ + + if hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): + obj.CoolantMode = obj.Base.CoolantMode + + if hasattr(obj, "Base") and hasattr(obj.Base, "ToolController"): + obj.ToolController = obj.Base.ToolController + + return self.dressupExecute(obj) + + def dressupExecute(self, obj): + """ + Called whenever receiver should be recalculated. + Should be overwritten by subclasses. + """ + pass + + + diff --git a/src/Mod/CAM/Path/Dressup/Gui/Array.py b/src/Mod/CAM/Path/Dressup/Gui/Array.py new file mode 100644 index 0000000000..e72a994646 --- /dev/null +++ b/src/Mod/CAM/Path/Dressup/Gui/Array.py @@ -0,0 +1,103 @@ + + +from PySide.QtCore import QT_TRANSLATE_NOOP +import FreeCAD +import Path +import Path.Base.Util as PathUtil +import Path.Dressup.Array as DressupArray +import Path.Main.Stock as PathStock +import PathScripts.PathUtils as PathUtils + +from PySide import QtGui +from PySide.QtCore import QT_TRANSLATE_NOOP +import FreeCAD +import FreeCADGui +import Path +import PathGui + + +class DressupArrayViewProvider(object): + def __init__(self, vobj): + self.attach(vobj) + + def dumps(self): + return None + + def loads(self, state): + return None + + def attach(self, vobj): + self.vobj = vobj + self.obj = vobj.Object + self.panel = None + + def claimChildren(self): + return [self.obj.Base] + + def onDelete(self, vobj, args=None): + if vobj.Object and vobj.Object.Proxy: + vobj.Object.Proxy.onDelete(vobj.Object, args) + return True + + def setEdit(self, vobj, mode=0): + return True + + def unsetEdit(self, vobj, mode=0): + pass + + def setupTaskPanel(self, panel): + pass + + def clearTaskPanel(self): + pass + + +class CommandPathDressupArray: + def GetResources(self): + return { + "Pixmap": "CAM_Dressup", + "MenuText": QT_TRANSLATE_NOOP("CAM_DressupArray", "Array"), + "ToolTip": QT_TRANSLATE_NOOP( + "CAM_DressupArray", + "Creates an array from a selected toolpath", + ), + } + + def IsActive(self): + if FreeCAD.ActiveDocument is not None: + for o in FreeCAD.ActiveDocument.Objects: + if o.Name[:3] == "Job": + return True + return False + + def Activated(self): + # check that the selection contains exactly what we want + selection = FreeCADGui.Selection.getSelection() + if len(selection) != 1: + Path.Log.error( + translate("CAM_DressupArray", "Please select one toolpath object") + "\n" + ) + return + baseObject = selection[0] + + # everything ok! + FreeCAD.ActiveDocument.openTransaction("Create Path Array Dress-up") + FreeCADGui.addModule("Path.Dressup.Gui.Array") + FreeCADGui.doCommand( + "Path.Dressup.Gui.Array.Create(App.ActiveDocument.%s)" % baseObject.Name + ) + # FreeCAD.ActiveDocument.commitTransaction() # Final `commitTransaction()` called via TaskPanel.accept() + FreeCAD.ActiveDocument.recompute() + + +def Create(base, name="DressupPathArray"): + FreeCAD.ActiveDocument.openTransaction("Create an Array dressup") + obj = DressupArray.Create(base, name) + obj.ViewObject.Proxy = DressupArrayViewProvider(obj.ViewObject) + obj.Base.ViewObject.Visibility = False + FreeCAD.ActiveDocument.commitTransaction() + return obj + +if FreeCAD.GuiUp: + # register the FreeCAD command + FreeCADGui.addCommand("CAM_DressupArray", CommandPathDressupArray()) diff --git a/src/Mod/CAM/Path/GuiInit.py b/src/Mod/CAM/Path/GuiInit.py index 65a7b798d1..94a29a5638 100644 --- a/src/Mod/CAM/Path/GuiInit.py +++ b/src/Mod/CAM/Path/GuiInit.py @@ -40,6 +40,7 @@ def Startup(): Path.Log.debug("Initializing PathGui") from Path.Base.Gui import PropertyBag from Path.Base.Gui import SetupSheet + from Path.Dressup.Gui import Array from Path.Dressup.Gui import AxisMap from Path.Dressup.Gui import Dogbone from Path.Dressup.Gui import DogboneII diff --git a/src/Mod/CAM/Path/Op/Gui/Array.py b/src/Mod/CAM/Path/Op/Gui/Array.py index 8bbd026730..93ec682081 100644 --- a/src/Mod/CAM/Path/Op/Gui/Array.py +++ b/src/Mod/CAM/Path/Op/Gui/Array.py @@ -27,6 +27,8 @@ import PathScripts import PathScripts.PathUtils as PathUtils from Path.Dressup.Utils import toolController from PySide import QtCore +from PySide import QtGui + import math import random from PySide.QtCore import QT_TRANSLATE_NOOP @@ -203,6 +205,19 @@ class ObjectArray: self.setEditorModes(obj) def execute(self, obj): + if FreeCAD.GuiUp: + + QtGui.QMessageBox.warning( + None, + QT_TRANSLATE_NOOP("CAM_ArrayOp", "Operation is depreciated"), + QT_TRANSLATE_NOOP("CAM_ArrayOp", + ("CAM -> Path Modification -> Array operation is depreciated " + "and will be removed in future FreeCAD versions.\n\n" + "Please use CAM -> Path Dressup -> Array instead.\n\n" + "DO NOT USE CURRENT ARRAY OPERATION WHEN MACHINING WITH COOLANT!\n" + "Due to a bug - collant will not be enabled for array paths." + )), + ) # backwards compatibility for PathArrays created before support for multiple bases if isinstance(obj.Base, list): base = obj.Base From 585ac82c34180aba484d2509979a7a864dfd87f4 Mon Sep 17 00:00:00 2001 From: PhaseLoop Date: Thu, 20 Mar 2025 14:26:36 +0100 Subject: [PATCH 2/9] log error on invalid base object --- src/Mod/CAM/Path/Dressup/Array.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/CAM/Path/Dressup/Array.py b/src/Mod/CAM/Path/Dressup/Array.py index 7a73811232..4e860989cc 100644 --- a/src/Mod/CAM/Path/Dressup/Array.py +++ b/src/Mod/CAM/Path/Dressup/Array.py @@ -183,6 +183,7 @@ class DressupArray(DressupBase): or not obj.Base.isDerivedFrom("Path::Feature") or not obj.Base.Path ): + Path.Log.error(translate("PathArray", "Base is empty or an invalid object.")) return None # Do not generate paths and clear current Path data if operation not From e8cd9a61d6e6279fccde86003d2261f49c212b64 Mon Sep 17 00:00:00 2001 From: PhaseLoop Date: Thu, 20 Mar 2025 14:28:09 +0100 Subject: [PATCH 3/9] add missing dressup import --- src/Mod/CAM/InitGui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/CAM/InitGui.py b/src/Mod/CAM/InitGui.py index aef883fef9..3975cb7f2f 100644 --- a/src/Mod/CAM/InitGui.py +++ b/src/Mod/CAM/InitGui.py @@ -122,6 +122,7 @@ class CAMWorkbench(Workbench): drillingcmdlist = ["CAM_Drilling", "CAM_Tapping"] modcmdlist = ["CAM_OperationCopy", "CAM_Array", "CAM_SimpleCopy"] dressupcmdlist = [ + "CAM_DressupArray", "CAM_DressupAxisMap", "CAM_DressupPathBoundary", "CAM_DressupDogbone", From c249b62b6cd23e084dbc5a3875a5cc48477adde6 Mon Sep 17 00:00:00 2001 From: PhaseLoop Date: Thu, 20 Mar 2025 15:02:56 +0100 Subject: [PATCH 4/9] update CMake imports --- src/Mod/CAM/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index de03de7190..88976ed088 100644 --- a/src/Mod/CAM/CMakeLists.txt +++ b/src/Mod/CAM/CMakeLists.txt @@ -59,6 +59,8 @@ SET(PathPythonBaseGui_SRCS SET(PathPythonDressup_SRCS Path/Dressup/__init__.py Path/Dressup/Utils.py + Path/Dressup/Array.py + Path/Dressup/Base.py Path/Dressup/Boundary.py Path/Dressup/DogboneII.py Path/Dressup/Tags.py @@ -66,6 +68,7 @@ SET(PathPythonDressup_SRCS SET(PathPythonDressupGui_SRCS Path/Dressup/Gui/__init__.py + Path/Dressup/Gui/Array.py Path/Dressup/Gui/AxisMap.py Path/Dressup/Gui/Dogbone.py Path/Dressup/Gui/DogboneII.py From 2bca01f0cf819b7f97004ea48c64dca94cccb8ab Mon Sep 17 00:00:00 2001 From: PhaseLoop Date: Sun, 23 Mar 2025 23:38:14 +0100 Subject: [PATCH 5/9] unit tests for array dressup --- src/Mod/CAM/CAMTests/TestPathDressupArray.py | 150 +++++++++++++++++++ src/Mod/CAM/Path/Dressup/Array.py | 16 +- 2 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 src/Mod/CAM/CAMTests/TestPathDressupArray.py diff --git a/src/Mod/CAM/CAMTests/TestPathDressupArray.py b/src/Mod/CAM/CAMTests/TestPathDressupArray.py new file mode 100644 index 0000000000..aa470391f6 --- /dev/null +++ b/src/Mod/CAM/CAMTests/TestPathDressupArray.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2025 phaseloop * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + + +import FreeCAD +import Path +from Path.Dressup.Array import DressupArray +import Path.Main.Job as PathJob +import Path.Op.Profile as PathProfile + +from CAMTests.PathTestUtils import PathTestBase + + +class TestEngrave: + def __init__(self, path): + self.Path = Path.Path(path) + self.ToolController = None # default tool 5mm + self.CoolantMode = "None" + self.Name = "Engrave" + + def isDerivedFrom(self, type): + if type == "Path::Feature": + return True + return False + +class TestFeature: + def __init__(self): + self.Path = Path.Path() + self.Name = "" + + def addProperty(self, typ, name, category, tip): + setattr(self, name, None) + + def setEditorMode(self, prop, mode): + pass + + +class TestDressupArray(PathTestBase): + """Unit tests for the Array dressup.""" + + def test00(self): + """Verify array with zero copies provides original path.""" + + source_gcode = ( + "G0 X0 Y0 Z0\n" + "G1 X10 Y10 Z0\n" + ) + + expected_gcode = ( + "G0 X0.000000 Y0.000000 Z0.000000\n" + "G1 X10.000000 Y10.000000 Z0.000000\n" + ) + + base = TestEngrave(source_gcode) + obj = TestFeature() + da = DressupArray(obj, base, None) + da.execute(obj) + self.assertTrue(obj.Path.toGCode() == expected_gcode, "Incorrect g-code generated") + + def test01(self): + """Verify linear x/y/z 1D array with 1 copy.""" + + source_gcode = ( + "G0 X0 Y0 Z0\n" + "G1 X10 Y10 Z0\n" + ) + + expected_gcode = ( + "G0 X0.000000 Y0.000000 Z0.000000\n" + "G1 X10.000000 Y10.000000 Z0.000000\n" + + "G0 X12.000000 Y12.000000 Z5.000000\n" + "G1 X22.000000 Y22.000000 Z5.000000\n" + ) + + base = TestEngrave(source_gcode) + obj = TestFeature() + da = DressupArray(obj, base, None) + obj.Copies = 1 + obj.Offset = FreeCAD.Vector(12, 12, 5) + + da.execute(obj) + self.assertTrue(obj.Path.toGCode() == expected_gcode, "Incorrect g-code generated") + + def test01(self): + """Verify linear x/y/z 2D array.""" + + source_gcode = ( + "G0 X0 Y0 Z0\n" + "G1 X10 Y10 Z0\n" + ) + + expected_gcode = ( + "G0 X0.000000 Y0.000000 Z0.000000\n" + "G1 X10.000000 Y10.000000 Z0.000000\n" + + "G0 X0.000000 Y6.000000 Z0.000000\n" + "G1 X10.000000 Y16.000000 Z0.000000\n" + + "G0 X12.000000 Y6.000000 Z0.000000\n" + "G1 X22.000000 Y16.000000 Z0.000000\n" + + "G0 X12.000000 Y0.000000 Z0.000000\n" + "G1 X22.000000 Y10.000000 Z0.000000\n" + + "G0 X24.000000 Y0.000000 Z0.000000\n" + "G1 X34.000000 Y10.000000 Z0.000000\n" + + "G0 X24.000000 Y6.000000 Z0.000000\n" + "G1 X34.000000 Y16.000000 Z0.000000\n" + ) + + base = TestEngrave(source_gcode) + obj = TestFeature() + da = DressupArray(obj, base, None) + obj.Type = "Linear2D" + obj.Copies = 0 + obj.CopiesX = 2 + obj.CopiesY = 1 + + obj.Offset = FreeCAD.Vector(12, 6, 0) + + da.execute(obj) + self.assertTrue(obj.Path.toGCode() == expected_gcode, "Incorrect g-code generated") + + + + + + + diff --git a/src/Mod/CAM/Path/Dressup/Array.py b/src/Mod/CAM/Path/Dressup/Array.py index 4e860989cc..4a421ae8d3 100644 --- a/src/Mod/CAM/Path/Dressup/Array.py +++ b/src/Mod/CAM/Path/Dressup/Array.py @@ -22,13 +22,9 @@ import FreeCAD -import FreeCADGui import Path -import PathScripts import PathScripts.PathUtils as PathUtils from Path.Dressup.Base import DressupBase -from PySide import QtCore -import math import random from PySide.QtCore import QT_TRANSLATE_NOOP @@ -124,7 +120,13 @@ class DressupArray(DressupBase): obj.Base = base obj.Active = True + # assigning array tells the type of possible enum choices obj.Type = ["Linear1D", "Linear2D", "Polar"] + # assign value + obj.Type = "Linear1D" + + obj.Copies = 0 + obj.JitterPercent = 0 self.setEditorModes(obj) obj.Proxy = self @@ -186,7 +188,7 @@ class DressupArray(DressupBase): Path.Log.error(translate("PathArray", "Base is empty or an invalid object.")) return None - # Do not generate paths and clear current Path data if operation not + # Do not generate paths and clear current Path data if operation not active if not obj.Active: if obj.Path: obj.Path = Path.Path() @@ -270,7 +272,9 @@ class PathArray: base = self.base # build copies - output = "" + # initially output contains original base path, copies are added on top of that + output = PathUtils.getPathWithPlacement(base).toGCode() + random.seed(self.seed) if self.arrayType == "Linear1D": From f072bde2d0f37e92770d39280477b11bdc3c81df Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 22:43:52 +0000 Subject: [PATCH 6/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/Mod/CAM/CAMTests/TestPathDressupArray.py | 72 +++++++------------- src/Mod/CAM/Path/Dressup/Array.py | 17 ++--- src/Mod/CAM/Path/Dressup/Base.py | 24 +++---- src/Mod/CAM/Path/Dressup/Gui/Array.py | 3 +- src/Mod/CAM/Path/Op/Gui/Array.py | 17 +++-- 5 files changed, 51 insertions(+), 82 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestPathDressupArray.py b/src/Mod/CAM/CAMTests/TestPathDressupArray.py index aa470391f6..6f8081db00 100644 --- a/src/Mod/CAM/CAMTests/TestPathDressupArray.py +++ b/src/Mod/CAM/CAMTests/TestPathDressupArray.py @@ -42,6 +42,7 @@ class TestEngrave: return True return False + class TestFeature: def __init__(self): self.Path = Path.Path() @@ -60,15 +61,9 @@ class TestDressupArray(PathTestBase): def test00(self): """Verify array with zero copies provides original path.""" - source_gcode = ( - "G0 X0 Y0 Z0\n" - "G1 X10 Y10 Z0\n" - ) - - expected_gcode = ( - "G0 X0.000000 Y0.000000 Z0.000000\n" - "G1 X10.000000 Y10.000000 Z0.000000\n" - ) + source_gcode = "G0 X0 Y0 Z0\n" "G1 X10 Y10 Z0\n" + + expected_gcode = "G0 X0.000000 Y0.000000 Z0.000000\n" "G1 X10.000000 Y10.000000 Z0.000000\n" base = TestEngrave(source_gcode) obj = TestFeature() @@ -79,17 +74,13 @@ class TestDressupArray(PathTestBase): def test01(self): """Verify linear x/y/z 1D array with 1 copy.""" - source_gcode = ( - "G0 X0 Y0 Z0\n" - "G1 X10 Y10 Z0\n" - ) - - expected_gcode = ( - "G0 X0.000000 Y0.000000 Z0.000000\n" - "G1 X10.000000 Y10.000000 Z0.000000\n" + source_gcode = "G0 X0 Y0 Z0\n" "G1 X10 Y10 Z0\n" - "G0 X12.000000 Y12.000000 Z5.000000\n" - "G1 X22.000000 Y22.000000 Z5.000000\n" + expected_gcode = ( + "G0 X0.000000 Y0.000000 Z0.000000\n" + "G1 X10.000000 Y10.000000 Z0.000000\n" + "G0 X12.000000 Y12.000000 Z5.000000\n" + "G1 X22.000000 Y22.000000 Z5.000000\n" ) base = TestEngrave(source_gcode) @@ -104,29 +95,21 @@ class TestDressupArray(PathTestBase): def test01(self): """Verify linear x/y/z 2D array.""" - source_gcode = ( - "G0 X0 Y0 Z0\n" - "G1 X10 Y10 Z0\n" - ) - + source_gcode = "G0 X0 Y0 Z0\n" "G1 X10 Y10 Z0\n" + expected_gcode = ( - "G0 X0.000000 Y0.000000 Z0.000000\n" - "G1 X10.000000 Y10.000000 Z0.000000\n" - - "G0 X0.000000 Y6.000000 Z0.000000\n" - "G1 X10.000000 Y16.000000 Z0.000000\n" - - "G0 X12.000000 Y6.000000 Z0.000000\n" - "G1 X22.000000 Y16.000000 Z0.000000\n" - - "G0 X12.000000 Y0.000000 Z0.000000\n" - "G1 X22.000000 Y10.000000 Z0.000000\n" - - "G0 X24.000000 Y0.000000 Z0.000000\n" - "G1 X34.000000 Y10.000000 Z0.000000\n" - - "G0 X24.000000 Y6.000000 Z0.000000\n" - "G1 X34.000000 Y16.000000 Z0.000000\n" + "G0 X0.000000 Y0.000000 Z0.000000\n" + "G1 X10.000000 Y10.000000 Z0.000000\n" + "G0 X0.000000 Y6.000000 Z0.000000\n" + "G1 X10.000000 Y16.000000 Z0.000000\n" + "G0 X12.000000 Y6.000000 Z0.000000\n" + "G1 X22.000000 Y16.000000 Z0.000000\n" + "G0 X12.000000 Y0.000000 Z0.000000\n" + "G1 X22.000000 Y10.000000 Z0.000000\n" + "G0 X24.000000 Y0.000000 Z0.000000\n" + "G1 X34.000000 Y10.000000 Z0.000000\n" + "G0 X24.000000 Y6.000000 Z0.000000\n" + "G1 X34.000000 Y16.000000 Z0.000000\n" ) base = TestEngrave(source_gcode) @@ -141,10 +124,3 @@ class TestDressupArray(PathTestBase): da.execute(obj) self.assertTrue(obj.Path.toGCode() == expected_gcode, "Incorrect g-code generated") - - - - - - - diff --git a/src/Mod/CAM/Path/Dressup/Array.py b/src/Mod/CAM/Path/Dressup/Array.py index 4a421ae8d3..398ada044e 100644 --- a/src/Mod/CAM/Path/Dressup/Array.py +++ b/src/Mod/CAM/Path/Dressup/Array.py @@ -115,7 +115,7 @@ class DressupArray(DressupBase): "Path", QT_TRANSLATE_NOOP("App::Property", "Seed value for jitter randomness"), ) - + self.obj = obj obj.Base = base @@ -179,12 +179,8 @@ class DressupArray(DressupBase): return True def dressupExecute(self, obj): - - if ( - not obj.Base - or not obj.Base.isDerivedFrom("Path::Feature") - or not obj.Base.Path - ): + + if not obj.Base or not obj.Base.isDerivedFrom("Path::Feature") or not obj.Base.Path: Path.Log.error(translate("PathArray", "Base is empty or an invalid object.")) return None @@ -312,7 +308,6 @@ class PathArray: ) pos = self._calculateJitter(pos) - pl = FreeCAD.Placement() # do not process the index 0,0. It will be processed by the base Paths themselves if not (i == 0 and j == 0): @@ -323,7 +318,7 @@ class PathArray: for cm in PathUtils.getPathWithPlacement(base).Commands ] ) - output+= np.toGCode() + output += np.toGCode() else: for i in range(self.copiesX + 1): for j in range(self.copiesY + 1): @@ -372,9 +367,7 @@ def Create(base, name="DressupArray"): """Create(base, name='DressupPathBoundary') ... creates a dressup array.""" if not base.isDerivedFrom("Path::Feature"): - Path.Log.error( - translate("CAM_DressupArray", "The selected object is not a path") + "\n" - ) + Path.Log.error(translate("CAM_DressupArray", "The selected object is not a path") + "\n") return None obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) diff --git a/src/Mod/CAM/Path/Dressup/Base.py b/src/Mod/CAM/Path/Dressup/Base.py index abf86550ec..a53f14554d 100644 --- a/src/Mod/CAM/Path/Dressup/Base.py +++ b/src/Mod/CAM/Path/Dressup/Base.py @@ -2,12 +2,13 @@ from PySide.QtCore import QT_TRANSLATE_NOOP from Path.Op.Base import ObjectOp + class DressupBase: """ Base class for all dressups to provide common interface with the rest of CAM One major example is making sure all dressups export base operation settings like coolant, tool controller, etc. - """ + """ def setup_coolant_property(self, obj): if not hasattr(obj, "CoolantMode"): @@ -25,14 +26,14 @@ class DressupBase: def setup_tool_controller_property(self, obj): if not hasattr(obj, "ToolController"): obj.addProperty( - "App::PropertyLink", - "ToolController", - "Path", - QT_TRANSLATE_NOOP( - "App::Property", - "The tool controller that will be used to calculate the path", - ), - ) + "App::PropertyLink", + "ToolController", + "Path", + QT_TRANSLATE_NOOP( + "App::Property", + "The tool controller that will be used to calculate the path", + ), + ) def __init__(self, obj, base): @@ -42,7 +43,7 @@ class DressupBase: "Base", QT_TRANSLATE_NOOP("App::Property", "The base path to modify"), ) - + obj.addProperty( "App::PropertyBool", "Active", @@ -88,6 +89,3 @@ class DressupBase: Should be overwritten by subclasses. """ pass - - - diff --git a/src/Mod/CAM/Path/Dressup/Gui/Array.py b/src/Mod/CAM/Path/Dressup/Gui/Array.py index e72a994646..98fb4f3b87 100644 --- a/src/Mod/CAM/Path/Dressup/Gui/Array.py +++ b/src/Mod/CAM/Path/Dressup/Gui/Array.py @@ -1,5 +1,3 @@ - - from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD import Path @@ -98,6 +96,7 @@ def Create(base, name="DressupPathArray"): FreeCAD.ActiveDocument.commitTransaction() return obj + if FreeCAD.GuiUp: # register the FreeCAD command FreeCADGui.addCommand("CAM_DressupArray", CommandPathDressupArray()) diff --git a/src/Mod/CAM/Path/Op/Gui/Array.py b/src/Mod/CAM/Path/Op/Gui/Array.py index 93ec682081..0e670d76d7 100644 --- a/src/Mod/CAM/Path/Op/Gui/Array.py +++ b/src/Mod/CAM/Path/Op/Gui/Array.py @@ -210,13 +210,16 @@ class ObjectArray: QtGui.QMessageBox.warning( None, QT_TRANSLATE_NOOP("CAM_ArrayOp", "Operation is depreciated"), - QT_TRANSLATE_NOOP("CAM_ArrayOp", - ("CAM -> Path Modification -> Array operation is depreciated " - "and will be removed in future FreeCAD versions.\n\n" - "Please use CAM -> Path Dressup -> Array instead.\n\n" - "DO NOT USE CURRENT ARRAY OPERATION WHEN MACHINING WITH COOLANT!\n" - "Due to a bug - collant will not be enabled for array paths." - )), + QT_TRANSLATE_NOOP( + "CAM_ArrayOp", + ( + "CAM -> Path Modification -> Array operation is depreciated " + "and will be removed in future FreeCAD versions.\n\n" + "Please use CAM -> Path Dressup -> Array instead.\n\n" + "DO NOT USE CURRENT ARRAY OPERATION WHEN MACHINING WITH COOLANT!\n" + "Due to a bug - collant will not be enabled for array paths." + ), + ), ) # backwards compatibility for PathArrays created before support for multiple bases if isinstance(obj.Base, list): From 81ccf9cc5ae3d5085c7aa37dd43c41e7d851defd Mon Sep 17 00:00:00 2001 From: PhaseLoop Date: Mon, 24 Mar 2025 00:05:29 +0100 Subject: [PATCH 7/9] update cmake list --- src/Mod/CAM/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index 88976ed088..51c7797e03 100644 --- a/src/Mod/CAM/CMakeLists.txt +++ b/src/Mod/CAM/CMakeLists.txt @@ -318,6 +318,7 @@ SET(Tests_SRCS CAMTests/TestPathAdaptive.py CAMTests/TestPathCore.py CAMTests/TestPathDepthParams.py + CAMTests/TestPathDressupArray.py CAMTests/TestPathDressupDogbone.py CAMTests/TestPathDressupDogboneII.py CAMTests/TestPathDressupHoldingTags.py From 5a6fd53b3a071a1932392c17c72af7769af42e73 Mon Sep 17 00:00:00 2001 From: PhaseLoop Date: Fri, 28 Mar 2025 19:46:28 +0100 Subject: [PATCH 8/9] fix issues --- src/Mod/CAM/Path/Tool/Bit.py | 46 ++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/Mod/CAM/Path/Tool/Bit.py b/src/Mod/CAM/Path/Tool/Bit.py index b2b2905128..20a21382e9 100644 --- a/src/Mod/CAM/Path/Tool/Bit.py +++ b/src/Mod/CAM/Path/Tool/Bit.py @@ -118,22 +118,14 @@ def _findRelativePath(path, typ): return relative -# Unused due to bug fix related to relative paths -""" -def findRelativePathShape(path): - return _findRelativePath(path, 'Shape') - - -def findRelativePathTool(path): - return _findRelativePath(path, 'Bit') -""" - - def findRelativePathLibrary(path): return _findRelativePath(path, "Library") class ToolBit(object): + + TOOL_TYPES = [ "EndMill", "BallEndMill", "BullNoseMill", "Drill", "VBit", "Probe", "Laser", "Other"] + def __init__(self, obj, shapeFile, path=None): Path.Log.track(obj.Label, shapeFile, path) self.obj = obj @@ -155,6 +147,14 @@ class ToolBit(object): "Base", QT_TRANSLATE_NOOP("App::Property", "The file of the tool"), ) + + obj.addProperty( + "App::PropertyEnumeration", + "ToolType", + "Base", + QT_TRANSLATE_NOOP("App::Property", "The type of the tool"), + ) + obj.addProperty( "App::PropertyString", "ShapeName", @@ -168,10 +168,13 @@ class ToolBit(object): QT_TRANSLATE_NOOP("App::Property", "List of all properties inherited from the bit"), ) + # set enum types by assigning array + obj.ToolType = self.TOOL_TYPES if path: obj.File = path if shapeFile is None: obj.BitShape = "endmill.fcstd" + obj.ToolType = "EndMill" self._setupBitShape(obj) self.unloadBitBody(obj) else: @@ -196,6 +199,16 @@ class ToolBit(object): obj.setEditorMode("BitBody", 2) obj.setEditorMode("File", 1) obj.setEditorMode("Shape", 2) + + if not hasattr(obj, "ToolType"): + obj.addProperty( + "App::PropertyString", + "ToolType", + "Base", + QT_TRANSLATE_NOOP("App::Property", "The type of the tool"), + ) + obj.ToolType = self.TOOL_TYPES + if not hasattr(obj, "BitPropertyNames"): obj.addProperty( "App::PropertyStringList", @@ -438,6 +451,10 @@ class ToolBit(object): attrs = {} attrs["version"] = 2 attrs["name"] = obj.Label + + if obj.ToolType: + attrs["type"] = obj.ToolType + if Path.Preferences.toolsStoreAbsolutePaths(): attrs["shape"] = obj.BitShape else: @@ -451,8 +468,7 @@ class ToolBit(object): for name in obj.BitPropertyNames: params[name] = PathUtil.getPropertyValueString(obj, name) attrs["parameter"] = params - params = {} - attrs["attribute"] = params + attrs["attribute"] = {} return attrs @@ -467,6 +483,10 @@ class ToolBitFactory(object): Path.Log.track(attrs, path) obj = Factory.Create(name, attrs["shape"], path) obj.Label = attrs["name"] + + if "type" in attrs: + obj.ToolType = attrs["type"] + params = attrs["parameter"] for prop in params: PathUtil.setProperty(obj, prop, params[prop]) From 63dcbd548ef98961b1c4b331b387478919dbae58 Mon Sep 17 00:00:00 2001 From: PhaseLoop Date: Thu, 3 Apr 2025 10:32:25 +0200 Subject: [PATCH 9/9] Revert "fix issues" This reverts commit 5a6fd53b3a071a1932392c17c72af7769af42e73. --- src/Mod/CAM/Path/Tool/Bit.py | 46 ++++++++++-------------------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/src/Mod/CAM/Path/Tool/Bit.py b/src/Mod/CAM/Path/Tool/Bit.py index 20a21382e9..b2b2905128 100644 --- a/src/Mod/CAM/Path/Tool/Bit.py +++ b/src/Mod/CAM/Path/Tool/Bit.py @@ -118,14 +118,22 @@ def _findRelativePath(path, typ): return relative +# Unused due to bug fix related to relative paths +""" +def findRelativePathShape(path): + return _findRelativePath(path, 'Shape') + + +def findRelativePathTool(path): + return _findRelativePath(path, 'Bit') +""" + + def findRelativePathLibrary(path): return _findRelativePath(path, "Library") class ToolBit(object): - - TOOL_TYPES = [ "EndMill", "BallEndMill", "BullNoseMill", "Drill", "VBit", "Probe", "Laser", "Other"] - def __init__(self, obj, shapeFile, path=None): Path.Log.track(obj.Label, shapeFile, path) self.obj = obj @@ -147,14 +155,6 @@ class ToolBit(object): "Base", QT_TRANSLATE_NOOP("App::Property", "The file of the tool"), ) - - obj.addProperty( - "App::PropertyEnumeration", - "ToolType", - "Base", - QT_TRANSLATE_NOOP("App::Property", "The type of the tool"), - ) - obj.addProperty( "App::PropertyString", "ShapeName", @@ -168,13 +168,10 @@ class ToolBit(object): QT_TRANSLATE_NOOP("App::Property", "List of all properties inherited from the bit"), ) - # set enum types by assigning array - obj.ToolType = self.TOOL_TYPES if path: obj.File = path if shapeFile is None: obj.BitShape = "endmill.fcstd" - obj.ToolType = "EndMill" self._setupBitShape(obj) self.unloadBitBody(obj) else: @@ -199,16 +196,6 @@ class ToolBit(object): obj.setEditorMode("BitBody", 2) obj.setEditorMode("File", 1) obj.setEditorMode("Shape", 2) - - if not hasattr(obj, "ToolType"): - obj.addProperty( - "App::PropertyString", - "ToolType", - "Base", - QT_TRANSLATE_NOOP("App::Property", "The type of the tool"), - ) - obj.ToolType = self.TOOL_TYPES - if not hasattr(obj, "BitPropertyNames"): obj.addProperty( "App::PropertyStringList", @@ -451,10 +438,6 @@ class ToolBit(object): attrs = {} attrs["version"] = 2 attrs["name"] = obj.Label - - if obj.ToolType: - attrs["type"] = obj.ToolType - if Path.Preferences.toolsStoreAbsolutePaths(): attrs["shape"] = obj.BitShape else: @@ -468,7 +451,8 @@ class ToolBit(object): for name in obj.BitPropertyNames: params[name] = PathUtil.getPropertyValueString(obj, name) attrs["parameter"] = params - attrs["attribute"] = {} + params = {} + attrs["attribute"] = params return attrs @@ -483,10 +467,6 @@ class ToolBitFactory(object): Path.Log.track(attrs, path) obj = Factory.Create(name, attrs["shape"], path) obj.Label = attrs["name"] - - if "type" in attrs: - obj.ToolType = attrs["type"] - params = attrs["parameter"] for prop in params: PathUtil.setProperty(obj, prop, params[prop])