diff --git a/src/Mod/CAM/CAMTests/TestPathDressupArray.py b/src/Mod/CAM/CAMTests/TestPathDressupArray.py new file mode 100644 index 0000000000..6f8081db00 --- /dev/null +++ b/src/Mod/CAM/CAMTests/TestPathDressupArray.py @@ -0,0 +1,126 @@ +# -*- 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/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index 5acfaab8be..0a09f4ae58 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 @@ -316,6 +319,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 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", diff --git a/src/Mod/CAM/Path/Dressup/Array.py b/src/Mod/CAM/Path/Dressup/Array.py new file mode 100644 index 0000000000..398ada044e --- /dev/null +++ b/src/Mod/CAM/Path/Dressup/Array.py @@ -0,0 +1,377 @@ +# -*- 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..a53f14554d --- /dev/null +++ b/src/Mod/CAM/Path/Dressup/Base.py @@ -0,0 +1,91 @@ +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..98fb4f3b87 --- /dev/null +++ b/src/Mod/CAM/Path/Dressup/Gui/Array.py @@ -0,0 +1,102 @@ +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..0e670d76d7 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,22 @@ 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