diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt
index 0221cca79a..daab112b11 100644
--- a/src/Mod/CAM/CMakeLists.txt
+++ b/src/Mod/CAM/CMakeLists.txt
@@ -222,6 +222,7 @@ SET(PathPythonOpGui_SRCS
Path/Op/Gui/FeatureExtension.py
Path/Op/Gui/Helix.py
Path/Op/Gui/MillFace.py
+ Path/Op/Gui/PathShapeTC.py
Path/Op/Gui/Pocket.py
Path/Op/Gui/PocketBase.py
Path/Op/Gui/PocketShape.py
diff --git a/src/Mod/CAM/Gui/Resources/Path.qrc b/src/Mod/CAM/Gui/Resources/Path.qrc
index 964f76bf09..5271f1c37f 100644
--- a/src/Mod/CAM/Gui/Resources/Path.qrc
+++ b/src/Mod/CAM/Gui/Resources/Path.qrc
@@ -47,6 +47,7 @@
icons/CAM_SelectLoop.svg
icons/CAM_SetupSheet.svg
icons/CAM_Shape.svg
+ icons/CAM_ShapeTC.svg
icons/CAM_SimpleCopy.svg
icons/CAM_Simulator.svg
icons/CAM_SimulatorGL.svg
diff --git a/src/Mod/CAM/Gui/Resources/icons/CAM_ShapeTC.svg b/src/Mod/CAM/Gui/Resources/icons/CAM_ShapeTC.svg
new file mode 100644
index 0000000000..1074f7d62c
--- /dev/null
+++ b/src/Mod/CAM/Gui/Resources/icons/CAM_ShapeTC.svg
@@ -0,0 +1,721 @@
+
+
+
+
diff --git a/src/Mod/CAM/InitGui.py b/src/Mod/CAM/InitGui.py
index 7b93f8767e..29aa791901 100644
--- a/src/Mod/CAM/InitGui.py
+++ b/src/Mod/CAM/InitGui.py
@@ -164,7 +164,7 @@ class CAMWorkbench(Workbench):
)
threedcmdgroup = threedopcmdlist
if Path.Preferences.experimentalFeaturesEnabled():
- prepcmdlist.append("CAM_Shape")
+ prepcmdlist.append("CAM_PathShapeTC")
extracmdlist.extend(["CAM_Area", "CAM_Area_Workplane"])
specialcmdlist.append("CAM_ThreadMilling")
twodopcmdlist.append("CAM_Slot")
diff --git a/src/Mod/CAM/Path/GuiInit.py b/src/Mod/CAM/Path/GuiInit.py
index 94a29a5638..ccf815abb4 100644
--- a/src/Mod/CAM/Path/GuiInit.py
+++ b/src/Mod/CAM/Path/GuiInit.py
@@ -67,6 +67,7 @@ def Startup():
from Path.Op.Gui import Engrave
from Path.Op.Gui import Helix
from Path.Op.Gui import MillFace
+ from Path.Op.Gui import PathShapeTC
from Path.Op.Gui import Pocket
from Path.Op.Gui import PocketShape
from Path.Op.Gui import Probe
diff --git a/src/Mod/CAM/Path/Op/Gui/PathShapeTC.py b/src/Mod/CAM/Path/Op/Gui/PathShapeTC.py
new file mode 100644
index 0000000000..b7c5eeeec7
--- /dev/null
+++ b/src/Mod/CAM/Path/Op/Gui/PathShapeTC.py
@@ -0,0 +1,239 @@
+# -*- coding: utf-8 -*-
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# ***************************************************************************
+# * *
+# * Copyright (c) 2022 FreeCAD Project Association *
+# * *
+# * This file is part of FreeCAD. *
+# * *
+# * FreeCAD is free software: you can redistribute it and/or modify it *
+# * under the terms of the GNU Lesser General Public License as *
+# * published by the Free Software Foundation, either version 2.1 of the *
+# * License, or (at your option) any later version. *
+# * *
+# * FreeCAD 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 *
+# * Lesser General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Lesser General Public *
+# * License along with FreeCAD. If not, see *
+# * . *
+# * *
+# ***************************************************************************
+
+import Draft
+import FreeCAD
+import FreeCADGui
+import Part
+import Path
+import Path.Op.Base as OpBase
+import PathScripts.PathUtils as PathUtils
+
+from PySide.QtCore import QT_TRANSLATE_NOOP
+
+__title__ = "CAM Path from Shape with Tool Controller"
+__author__ = ""
+__inspirer__ = "Russ4262"
+__url__ = "https://forum.freecad.org/viewtopic.php?t=93896"
+__doc__ = ""
+
+
+if False:
+ Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
+ Path.Log.trackModule(Path.Log.thisModule())
+else:
+ Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
+
+
+translate = FreeCAD.Qt.translate
+
+
+# Add base set of operation properties
+def _addBaseProperties(obj):
+ obj.addProperty(
+ "App::PropertyBool",
+ "Active",
+ "Path",
+ QT_TRANSLATE_NOOP("App::Property", "Make False, to prevent operation from generating code"),
+ locked=True,
+ )
+ obj.addProperty(
+ "App::PropertyString",
+ "Comment",
+ "Path",
+ QT_TRANSLATE_NOOP("App::Property", "An optional comment for this Operation"),
+ locked=True,
+ )
+ obj.addProperty(
+ "App::PropertyString",
+ "UserLabel",
+ "Path",
+ QT_TRANSLATE_NOOP("App::Property", "User Assigned Label"),
+ locked=True,
+ )
+ obj.addProperty(
+ "App::PropertyString",
+ "CycleTime",
+ "Path",
+ QT_TRANSLATE_NOOP("App::Property", "Operations Cycle Time Estimation"),
+ locked=True,
+ )
+ obj.setEditorMode("CycleTime", 1) # Set property read-only
+ obj.Active = True
+
+
+# Add ToolController properties
+def _addToolController(obj):
+ obj.addProperty(
+ "App::PropertyLink",
+ "ToolController",
+ "Path",
+ QT_TRANSLATE_NOOP(
+ "App::Property",
+ "The tool controller that will be used to calculate the path",
+ ),
+ )
+ obj.addProperty(
+ "App::PropertyDistance",
+ "OpToolDiameter",
+ "Op Values",
+ QT_TRANSLATE_NOOP("App::Property", "Holds the diameter of the tool"),
+ )
+ obj.setEditorMode("OpToolDiameter", 1) # Set property read-only
+ obj.ToolController = PathUtils.findToolController(obj, None)
+ if not obj.ToolController:
+ raise OpBase.PathNoTCException()
+ obj.OpToolDiameter = obj.ToolController.Tool.Diameter
+
+ obj.FeedRate = obj.ToolController.HorizFeed.Value
+ obj.FeedRateVertical = obj.ToolController.VertFeed.Value
+
+
+# Get list of tool controllers
+def _getToolControllers(obj, proxy=None):
+ # Modified getToolControllers() from PathScripts.PathUtils
+ # for Path object without Proxy
+ job = PathUtils.findParentJob(obj)
+ if job:
+ return [tc for tc in job.Tools.Group]
+ else:
+ return []
+
+
+# Set safety height parameters for Path operation
+def _setSafetyZ(obj):
+ job = PathUtils.findParentJob(obj)
+ if job:
+ safetyZ = job.Stock.Shape.BoundBox.ZMax + 10
+ obj.RetractThreshold = safetyZ
+ obj.Retraction = safetyZ
+ obj.ResumeHeight = safetyZ
+
+
+# Geometry for selected shapes
+class ObjectPartShape:
+ def __init__(self, obj, base):
+ # Path.Log.info("ObjectPartShape.__init__()")
+ self.obj = obj
+ obj.addProperty(
+ "App::PropertyLinkSubListGlobal",
+ "Base",
+ "Path",
+ QT_TRANSLATE_NOOP("App::Property", "The base geometry for this operation"),
+ )
+ obj.Base = base
+
+ def __getstate__(self):
+ return None
+
+ def __setstate__(self, state):
+ return None
+
+ def onDelete(self, obj, args):
+ return True
+
+ def onDocumentRestored(self, obj):
+ self.obj = obj
+
+ def onChanged(self, obj, prop):
+ """onChanged(obj, prop) ... method called when objECT is changed,
+ with source propERTY of the change."""
+ if "Restore" in obj.State:
+ pass
+
+ def execute(self, obj):
+ edges = []
+ if obj.Base:
+ (base, subNames) = obj.Base[0]
+ edges = [
+ base.Shape.getElement(sub).copy() for sub in subNames if sub.startswith("Edge")
+ ]
+
+ if edges:
+ obj.Shape = Part.Wire(Part.__sortEdges__(edges))
+ else:
+ obj.Shape = Part.Shape()
+
+
+class CommandPathShapeTC:
+ def GetResources(self):
+ return {
+ "Pixmap": "CAM_ShapeTC",
+ "MenuText": QT_TRANSLATE_NOOP("CAM_PathShapeTC", "Path from Shape TC"),
+ "ToolTip": QT_TRANSLATE_NOOP(
+ "CAM_PathShapeTC", "Creates path from selected shapes with tool controller"
+ ),
+ }
+
+ def IsActive(self):
+ isJob = False
+ if FreeCAD.ActiveDocument is not None:
+ for o in FreeCAD.ActiveDocument.Objects:
+ if o.Name.startswith("Job"):
+ isJob = True
+ break
+ if isJob:
+ selection = FreeCADGui.Selection.getSelectionEx()
+ if selection:
+ base = selection[0].Object
+ subBase = selection[0].SubElementNames
+ if subBase and [edge for edge in subBase if "Edge" in edge]:
+ return True
+ elif base.Shape.ShapeType in ["Wire", "Edge"]:
+ return True
+ return False
+
+ def Activated(self):
+ print("Create PathShape object with Tool Controller")
+ doc = FreeCAD.ActiveDocument
+ selection = FreeCADGui.Selection.getSelectionEx()
+ shapeObj = None
+ if selection:
+ base = selection[0].Object
+ subBase = selection[0].SubElementNames
+ if subBase:
+ subEdges = [edge for edge in subBase if "Edge" in edge]
+ shapeObj = doc.addObject("Part::FeaturePython", "PartShape")
+ shapeObj.ViewObject.Proxy = 0
+ shapeObj.Visibility = False
+ shapeObj.Proxy = ObjectPartShape(shapeObj, [(base, subEdges)])
+ elif base.Shape.ShapeType in ["Wire", "Edge"]:
+ shapeObj = Draft.make_clone(base)
+
+ pathObj = doc.addObject("Path::FeatureShape", "PathShape")
+ pathObj.Sources = [shapeObj]
+
+ # Overwrite getToolControllers() function with modified version
+ PathUtils.getToolControllers = _getToolControllers
+
+ PathUtils.addToJob(pathObj)
+ _addBaseProperties(pathObj)
+ _addToolController(pathObj)
+ _setSafetyZ(pathObj)
+ doc.recompute()
+
+
+if FreeCAD.GuiUp:
+ # Register the FreeCAD command
+ FreeCADGui.addCommand("CAM_PathShapeTC", CommandPathShapeTC())