diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt
index 83937b084f..3b515b4f2a 100644
--- a/src/Mod/Path/CMakeLists.txt
+++ b/src/Mod/Path/CMakeLists.txt
@@ -101,6 +101,12 @@ SET(PathScripts_SRCS
PathScripts/PathStop.py
PathScripts/PathSurface.py
PathScripts/PathSurfaceGui.py
+ PathScripts/PathToolBit.py
+ PathScripts/PathToolBitCmd.py
+ PathScripts/PathToolBitEdit.py
+ PathScripts/PathToolBitGui.py
+ PathScripts/PathToolBitLibraryCmd.py
+ PathScripts/PathToolBitLibraryGui.py
PathScripts/PathToolController.py
PathScripts/PathToolControllerGui.py
PathScripts/PathToolEdit.py
@@ -133,6 +139,29 @@ SET(PathScripts_post_SRCS
PathScripts/post/smoothie_post.py
)
+SET(Tools_Bit_SRCS
+ Tools/Bit/t1.fctb
+ Tools/Bit/t2.fctb
+ Tools/Bit/t3.fctb
+ Tools/Bit/t4.fctb
+ Tools/Bit/t5.fctb
+ Tools/Bit/t6.fctb
+ Tools/Bit/t7.fctb
+ Tools/Bit/t8.fctb
+ Tools/Bit/t9.fctb
+)
+
+SET(Tools_Library_SRCS
+ Tools/Library/endmills.fctl
+)
+
+SET(Tools_Shape_SRCS
+ Tools/Shape/ballend.fcstd
+ Tools/Shape/bullnose.fcstd
+ Tools/Shape/drill.fcstd
+ Tools/Shape/endmill.fcstd
+ Tools/Shape/v-bit.fcstd
+)
SET(PathTests_SRCS
PathTests/__init__.py
@@ -147,9 +176,11 @@ SET(PathTests_SRCS
PathTests/TestPathLog.py
PathTests/TestPathOpTools.py
PathTests/TestPathPost.py
+ PathTests/TestPathPreferences.py
PathTests/TestPathSetupSheet.py
PathTests/TestPathStock.py
PathTests/TestPathTool.py
+ PathTests/TestPathToolBit.py
PathTests/TestPathToolController.py
PathTests/TestPathTooltable.py
PathTests/TestPathUtil.py
@@ -178,6 +209,9 @@ SET(Path_Images
SET(all_files
${PathScripts_SRCS}
${PathScripts_post_SRCS}
+ ${Tools_Bit_SRCS}
+ ${Tools_Library_SRCS}
+ ${Tools_Shape_SRCS}
${Path_Images}
)
@@ -218,6 +252,27 @@ INSTALL(
Mod/Path/PathScripts/post
)
+INSTALL(
+ FILES
+ ${Tools_Bit_SRCS}
+ DESTINATION
+ Mod/Path/Tools/Bit
+)
+
+INSTALL(
+ FILES
+ ${Tools_Library_SRCS}
+ DESTINATION
+ Mod/Path/Tools/Library
+)
+
+INSTALL(
+ FILES
+ ${Tools_Shape_SRCS}
+ DESTINATION
+ Mod/Path/Tools/Shape
+)
+
INSTALL(
FILES
${PathImages_Ops}
diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc
index 19246750df..b461c8cdf6 100644
--- a/src/Mod/Path/Gui/Resources/Path.qrc
+++ b/src/Mod/Path/Gui/Resources/Path.qrc
@@ -48,9 +48,10 @@
icons/Path-Speed.svg
icons/Path-Stock.svg
icons/Path-Stop.svg
+ icons/Path-ToolBit.svg
icons/Path-ToolChange.svg
icons/Path-ToolController.svg
- icons/Path-ToolDuplicate.svg
+ icons/Path-ToolDuplicate.svg
icons/Path-Toolpath.svg
icons/Path-ToolTable.svg
icons/Path-Area.svg
@@ -107,6 +108,9 @@
panels/PointEdit.ui
panels/SetupGlobal.ui
panels/SetupOp.ui
+ panels/ToolBitEditor.ui
+ panels/ToolBitLibraryEdit.ui
+ panels/ToolBitSelector.ui
panels/ToolEditor.ui
panels/ToolLibraryEditor.ui
panels/TaskPathSimulator.ui
diff --git a/src/Mod/Path/Gui/Resources/icons/Path-ToolBit.svg b/src/Mod/Path/Gui/Resources/icons/Path-ToolBit.svg
new file mode 100644
index 0000000000..025637f1bc
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/icons/Path-ToolBit.svg
@@ -0,0 +1,933 @@
+
+
+
+
diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui
new file mode 100644
index 0000000000..1ffb408a8a
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui
@@ -0,0 +1,233 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 587
+ 744
+
+
+
+ Form
+
+
+ -
+
+
+ 0
+
+
+
+
+ 0
+ 0
+ 559
+ 626
+
+
+
+ Shape
+
+
+
-
+
+
+ Tool Bit
+
+
+
-
+
+
+ Name
+
+
+
+ -
+
+
+ <html><head/><body><p>Display name of the Tool Bit (initial value taken from the shape file).</p></body></html>
+
+
+ 50
+
+
+ Display Name
+
+
+
+ -
+
+
+ Type
+
+
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ <html><head/><body><p>The file which defines the type and shape of the Tool Bit.</p></body></html>
+
+
+
+ -
+
+
+ <html><head/><body><p>Change file defining type and shape of Tool Bit.</p></body></html>
+
+
+ ...
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Bit Parameter
+
+
+
+ QFormLayout::AllNonFixedFieldsGrow
+
+
-
+
+
+ Point/Tip Angle
+
+
+
+ -
+
+
+ 180°
+
+
+ °
+
+
+
+ -
+
+
+ Cutting Edge Height
+
+
+
+ -
+
+
+ 0.00
+
+
+ mm
+
+
+
+
+
+
+ -
+
+
+
+ 210
+ 297
+
+
+
+ Image
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 277
+
+
+
+
+
+
+
+
+
+ 0
+ 0
+ 559
+ 626
+
+
+
+ Attributes
+
+
+ -
+
+
+
+ 0
+ 2
+
+
+
+
+ 0
+ 300
+
+
+
+ QAbstractItemView::AllEditTriggers
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+ Gui::InputField
+ QLineEdit
+
+
+
+
+
+
diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui
new file mode 100644
index 0000000000..4663aa712f
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui
@@ -0,0 +1,276 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 958
+ 508
+
+
+
+ ToolBit Library
+
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ <html><head/><body><p>Create a new library with an empty list of Tool Bits.</p></body></html>
+
+
+ ...
+
+
+
+ :/icons/document-new.svg:/icons/document-new.svg
+
+
+
+ -
+
+
+ <html><head/><body><p>Open an existing Tool Bit library.</p></body></html>
+
+
+ ...
+
+
+
+ :/icons/document-open.svg:/icons/document-open.svg
+
+
+
+ -
+
+
+ <html><head/><body><p>Save Tool Bit library.</p></body></html>
+
+
+ ...
+
+
+
+ :/icons/document-save.svg:/icons/document-save.svg
+
+
+
+ -
+
+
+ <html><head/><body><p>Save Tool Bit library under new name.</p></body></html>
+
+
+ ...
+
+
+
+ :/icons/document-save-as.svg:/icons/document-save-as.svg
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ <html><head/><body><p>Edit Tool Bit library editor settings.</p></body></html>
+
+
+ ...
+
+
+
+ :/icons/preferences-system.svg:/icons/preferences-system.svg
+
+
+
+
+
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ true
+
+
+ <html><head/><body><p>Table of Tool Bits of the library.</p></body></html>
+
+
+ true
+
+
+ QAbstractItemView::InternalMove
+
+
+ Qt::MoveAction
+
+
+ QAbstractItemView::SelectRows
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ <html><head/><body><p>Add another Tool Bit to this library.</p><p><br/></p></body></html>
+
+
+ Add ...
+
+
+
+ :/icons/list-add.svg:/icons/list-add.svg
+
+
+
+ -
+
+
+ <html><head/><body><p>Delete selected Tool Bit(s) from the library.</p><p><br/></p></body></html>
+
+
+ Delete
+
+
+
+ :/icons/list-remove.svg:/icons/list-remove.svg
+
+
+
+ -
+
+
+ <html><head/><body><p>Assigne numbers to each Tool Bit according to its current position in the library. The first Tool Bit is assigned the ID 1.</p></body></html>
+
+
+ Enumerate
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 115
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Dialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Dialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui
new file mode 100644
index 0000000000..4dde97be5a
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui
@@ -0,0 +1,119 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 588
+ 396
+
+
+
+ Dialog
+
+
+ -
+
+
+
-
+
+
+ <html><head/><body><p>Available Tool Bits to choose from.</p></body></html>
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+
+ -
+
+
+
-
+
+
+ <html><head/><body><p>Load an existing Tool Bit from a file.</p></body></html>
+
+
+ Load...
+
+
+
+ -
+
+
+ <html><head/><body><p>Create a new Tool Bit based on an existing shape.</p></body></html>
+
+
+ New
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Dialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Dialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/Mod/Path/Gui/Resources/preferences/PathJob.ui b/src/Mod/Path/Gui/Resources/preferences/PathJob.ui
index dad5353339..4b458788e5 100644
--- a/src/Mod/Path/Gui/Resources/preferences/PathJob.ui
+++ b/src/Mod/Path/Gui/Resources/preferences/PathJob.ui
@@ -24,8 +24,8 @@
0
0
- 422
- 558
+ 467
+ 448
@@ -142,8 +142,8 @@
0
0
- 406
- 360
+ 665
+ 449
@@ -348,8 +348,8 @@
0
0
- 422
- 558
+ 431
+ 718
@@ -620,6 +620,54 @@
+
+
+
+ 0
+ 0
+ 412
+ 461
+
+
+
+ Tools
+
+
+ -
+
+
+ <html><head/><body><p>Legacy Tools have no accurate shape representation and are stored in the user preferences of FreeCAD.</p></body></html>
+
+
+ Use Legacy Tools
+
+
+
+ -
+
+
+ <html><head/><body><p>References to Tool Bits and their shapes can either be stored with an absolute path or with a relative path to the search path.</p><p>Generally it is recommended to use relative paths due to their flexibility and robustness to layout changes.</p><p>Should multiple tools or tool shapes with the same name exist in different directories it can be required to use absolute paths. </p></body></html>
+
+
+ Store Absolute Paths
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py
index 83ff50ab2c..0657fac577 100644
--- a/src/Mod/Path/InitGui.py
+++ b/src/Mod/Path/InitGui.py
@@ -71,6 +71,16 @@ class PathWorkbench (Workbench):
FreeCADGui.addIconPath(":/icons")
from PathScripts import PathGuiInit
from PathScripts import PathJobCmd
+
+ from PathScripts import PathToolBitCmd
+ from PathScripts import PathToolBitLibraryCmd
+ if PathPreferences.experimentalFeaturesEnabled():
+ toolbitcmdlist = PathToolBitCmd.CommandList + ["Separator"] + PathToolBitLibraryCmd.CommandList + ["Path_ToolController", "Separator"]
+ self.toolbitctxmenu = ["Path_ToolBitLibraryLoad", "Path_ToolController"]
+ else:
+ toolbitcmdlist = []
+ self.toolbitctxmenu = []
+
import PathCommands
PathGuiInit.Startup()
@@ -112,7 +122,7 @@ class PathWorkbench (Workbench):
if extracmdlist:
self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist)
- self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"])
+ self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + toolbitcmdlist + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"])
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP(
"Path", "Path Dressup")], dressupcmdlist)
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP(
@@ -143,6 +153,7 @@ class PathWorkbench (Workbench):
def ContextMenu(self, recipient):
import PathScripts
+ menuAppended = False
if len(FreeCADGui.Selection.getSelection()) == 1:
obj = FreeCADGui.Selection.getSelection()[0]
if obj.isDerivedFrom("Path::Feature"):
@@ -152,9 +163,11 @@ class PathWorkbench (Workbench):
if "Remote" in selectedName:
self.appendContextMenu("", ["Refresh_Path"])
if "Job" in selectedName:
- self.appendContextMenu("", ["Path_ExportTemplate"])
- if isinstance (obj.Proxy, PathScripts.PathOp.ObjectOp):
+ self.appendContextMenu("", ["Path_ExportTemplate"] + self.toolbitctxmenu)
+ menuAppended = True
+ if isinstance(obj.Proxy, PathScripts.PathOp.ObjectOp):
self.appendContextMenu("", ["Path_OperationCopy", "Path_OpActiveToggle"])
+ menuAppended = True
if obj.isDerivedFrom("Path::Feature"):
if "Profile" in selectedName or "Contour" in selectedName or "Dressup" in selectedName:
self.appendContextMenu("", "Separator")
@@ -162,6 +175,12 @@ class PathWorkbench (Workbench):
#self.appendContextMenu("", ["Set_EndPoint"])
for cmd in self.dressupcmds:
self.appendContextMenu("", [cmd])
+ menuAppended = True
+ if isinstance(obj.Proxy, PathScripts.PathToolBit.ToolBit):
+ self.appendContextMenu("", ["Path_ToolBitSave", "Path_ToolBitSaveAs"])
+ menuAppended = True
+ if menuAppended:
+ self.appendContextMenu("", "Separator")
Gui.addWorkbench(PathWorkbench())
diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBase.py b/src/Mod/Path/PathScripts/PathCircularHoleBase.py
index 8310e9dcbf..12daca5567 100644
--- a/src/Mod/Path/PathScripts/PathCircularHoleBase.py
+++ b/src/Mod/Path/PathScripts/PathCircularHoleBase.py
@@ -143,7 +143,7 @@ class ObjectOp(PathOp.ObjectOp):
return shape.Curve.Radius * 2
if shape.ShapeType == 'Face':
- for i in range(len(shape.Edges)):
+ for i in range(len(shape.Edges)):
if (type(shape.Edges[i].Curve) == Part.Circle and
shape.Edges[i].Curve.Radius * 2 < shape.BoundBox.XLength*1.1 and
shape.Edges[i].Curve.Radius * 2 > shape.BoundBox.XLength*0.9):
@@ -384,7 +384,7 @@ class ObjectOp(PathOp.ObjectOp):
if 1 == len(self.model) and self.baseIsArchPanel(obj, self.model[0]):
panel = self.model[0]
holeshapes = panel.Proxy.getHoles(panel, transform=True)
- tooldiameter = obj.ToolController.Proxy.getTool(obj.ToolController).Diameter
+ tooldiameter = float(obj.ToolController.Proxy.getTool(obj.ToolController).Diameter)
for holeNr, hole in enumerate(holeshapes):
PathLog.debug('Entering new HoleShape')
for wireNr, wire in enumerate(hole.Wires):
@@ -405,7 +405,7 @@ class ObjectOp(PathOp.ObjectOp):
PathLog.track('obj: {} shape: {}'.format(obj, shape))
holelist = []
features = []
- # tooldiameter = obj.ToolController.Proxy.getTool(obj.ToolController).Diameter
+ # tooldiameter = float(obj.ToolController.Proxy.getTool(obj.ToolController).Diameter)
tooldiameter = None
PathLog.debug('search for holes larger than tooldiameter: {}: '.format(tooldiameter))
if DraftGeomUtils.isPlanar(shape):
diff --git a/src/Mod/Path/PathScripts/PathDeburr.py b/src/Mod/Path/PathScripts/PathDeburr.py
index 347e5c5bf7..109f4970ce 100644
--- a/src/Mod/Path/PathScripts/PathDeburr.py
+++ b/src/Mod/Path/PathScripts/PathDeburr.py
@@ -48,15 +48,15 @@ def translate(context, text, disambig=None):
def toolDepthAndOffset(width, extraDepth, tool):
'''toolDepthAndOffset(width, extraDepth, tool) ... return tuple for given parameters.'''
- angle = tool.CuttingEdgeAngle
+ angle = float(tool.CuttingEdgeAngle)
if 0 == angle:
angle = 180
tan = math.tan(math.radians(angle / 2))
toolDepth = 0 if 0 == tan else width / tan
depth = toolDepth + extraDepth
- toolOffset = tool.FlatRadius
- extraOffset = tool.Diameter / 2 - width if 180 == angle else extraDepth / tan
+ toolOffset = float(tool.FlatRadius)
+ extraOffset = float(tool.Diameter) / 2 - width if 180 == angle else extraDepth / tan
offset = toolOffset + extraOffset
return (depth, offset)
diff --git a/src/Mod/Path/PathScripts/PathDressupDogbone.py b/src/Mod/Path/PathScripts/PathDressupDogbone.py
index 9866cf8fc5..66f9930cdd 100644
--- a/src/Mod/Path/PathScripts/PathDressupDogbone.py
+++ b/src/Mod/Path/PathScripts/PathDressupDogbone.py
@@ -860,10 +860,10 @@ class ObjectDressup:
self.toolRadius = 5
else:
tool = tc.Proxy.getTool(tc) # PathUtils.getTool(obj, tc.ToolNumber)
- if not tool or tool.Diameter == 0:
+ if not tool or float(tool.Diameter) == 0:
self.toolRadius = 5
else:
- self.toolRadius = tool.Diameter / 2
+ self.toolRadius = float(tool.Diameter) / 2
self.shapes = {}
self.dbg = []
diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py
index 6c454e4415..cb24e5e393 100644
--- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py
+++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py
@@ -671,7 +671,7 @@ class PathData:
print("tag[%d]" % i)
if not i in fromObj.Disabled:
dist = self.baseWire.distToShape(Part.Vertex(FreeCAD.Vector(pos.x, pos.y, self.minZ)))
- if dist[0] < W:
+ if True or dist[0] < W:
print("tag[%d/%d]: (%.2f, %.2f, %.2f)" % (i, j, pos.x, pos.y, self.minZ))
at = dist[1][0][0]
tags.append(Tag(j, at.x, at.y, W, H, A, R, True))
@@ -759,7 +759,13 @@ class ObjectTagDressup:
obj.addProperty("App::PropertyIntegerList", "Disabled", "Tag", QtCore.QT_TRANSLATE_NOOP("Path_DressupTag", "IDs of disabled holding tags"))
obj.addProperty("App::PropertyInteger", "SegmentationFactor", "Tag", QtCore.QT_TRANSLATE_NOOP("Path_DressupTag", "Factor determining the # of segments used to approximate rounded tags."))
- self.__setstate__(obj)
+ # for pylint ...
+ self.obj = obj
+ self.solids = []
+ self.tags = []
+ self.pathData = None
+ self.toolRadius = None
+ self.mappers = []
obj.Proxy = self
obj.Base = base
@@ -1021,7 +1027,7 @@ class ObjectTagDressup:
# traceback.print_exc()
return None
- self.toolRadius = PathDressup.toolController(obj.Base).Tool.Diameter / 2
+ self.toolRadius = float(PathDressup.toolController(obj.Base).Tool.Diameter) / 2
self.pathData = pathData
if generate:
obj.Height = self.pathData.defaultTagHeight()
diff --git a/src/Mod/Path/PathScripts/PathDressupTag.py b/src/Mod/Path/PathScripts/PathDressupTag.py
index 9dab62d975..263ea19336 100644
--- a/src/Mod/Path/PathScripts/PathDressupTag.py
+++ b/src/Mod/Path/PathScripts/PathDressupTag.py
@@ -220,7 +220,7 @@ class ObjectDressup:
PathLog.track()
def toolRadius(self):
- return PathDressup.toolController(self.obj.Base).Tool.Diameter / 2.0
+ return float(PathDressup.toolController(self.obj.Base).Tool.Diameter) / 2.0
def addTagsToDocuemnt(self):
for i, solid in enumerate(self.solids):
diff --git a/src/Mod/Path/PathScripts/PathDrillingGui.py b/src/Mod/Path/PathScripts/PathDrillingGui.py
index 11504c4bc6..6d8d8249a4 100644
--- a/src/Mod/Path/PathScripts/PathDrillingGui.py
+++ b/src/Mod/Path/PathScripts/PathDrillingGui.py
@@ -51,6 +51,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
'''Controller for the drilling operation's page'''
def initPage(self, obj):
+ # pylint: disable=attribute-defined-outside-init
self.peckDepthSpinBox = PathGui.QuantitySpinBox(self.form.peckDepth, obj, 'PeckDepth')
self.peckRetractSpinBox = PathGui.QuantitySpinBox(self.form.peckRetractHeight, obj, 'RetractHeight')
self.dwellTimeSpinBox = PathGui.QuantitySpinBox(self.form.dwellTime, obj, 'DwellTime')
@@ -80,6 +81,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpDrillingEdit.ui")
def updateQuantitySpinBoxes(self, index = None):
+ # pylint: disable=unused-argument
self.peckDepthSpinBox.updateSpinBox()
self.peckRetractSpinBox.updateSpinBox()
self.dwellTimeSpinBox.updateSpinBox()
diff --git a/src/Mod/Path/PathScripts/PathGui.py b/src/Mod/Path/PathScripts/PathGui.py
index ed2e9e757d..98cf988106 100644
--- a/src/Mod/Path/PathScripts/PathGui.py
+++ b/src/Mod/Path/PathScripts/PathGui.py
@@ -25,6 +25,7 @@
import FreeCAD
import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
+import PathScripts.PathUtil as PathUtil
import PySide
@@ -44,34 +45,6 @@ if LOGLEVEL:
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
-def _getProperty(obj, prop):
- o = obj
- attr = obj
- name = None
- for name in prop.split('.'):
- o = attr
- if not hasattr(o, name):
- break
- attr = getattr(o, name)
-
- if o == attr:
- PathLog.warning(translate('PathGui', "%s has no property %s (%s))") % (obj.Label, prop, name))
- return (None, None, None)
-
- #PathLog.debug("found property %s of %s (%s: %s)" % (prop, obj.Label, name, attr))
- return(o, attr, name)
-
-def getProperty(obj, prop):
- '''getProperty(obj, prop) ... answer obj's property defined by its canonical name.'''
- o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
- return attr
-
-def setProperty(obj, prop, value):
- '''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.'''
- o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
- if o and name:
- setattr(o, name, value)
-
def updateInputField(obj, prop, widget, onBeforeChange=None):
'''updateInputField(obj, prop, widget) ... update obj's property prop with the value of widget.
The property's value is only assigned if the new value differs from the current value.
@@ -82,13 +55,13 @@ If onBeforeChange is specified it is called before a new value is assigned to th
Returns True if a new value was assigned, False otherwise (new value is the same as the current).
'''
value = FreeCAD.Units.Quantity(widget.text()).Value
- attr = getProperty(obj, prop)
+ attr = PathUtil.getProperty(obj, prop)
attrValue = attr.Value if hasattr(attr, 'Value') else attr
if not PathGeom.isRoughly(attrValue, value):
PathLog.debug("updateInputField(%s, %s): %.2f -> %.2f" % (obj.Label, prop, attr, value))
if onBeforeChange:
onBeforeChange(obj)
- setProperty(obj, prop, value)
+ PathUtil.setProperty(obj, prop, value)
return True
return False
@@ -107,7 +80,7 @@ The spin box gets bound to a given property and supports update in both directio
self.widget = widget
self.prop = prop
self.onBeforeChange = onBeforeChange
- attr = getProperty(self.obj, self.prop)
+ attr = PathUtil.getProperty(self.obj, self.prop)
if attr is not None:
if hasattr(attr, 'Value'):
widget.setProperty('unit', attr.getUserPreferred()[2])
@@ -134,7 +107,7 @@ If no value is provided the value of the bound property is used.
quantity can be of type Quantity or Float.'''
if self.valid:
if quantity is None:
- quantity = getProperty(self.obj, self.prop)
+ quantity = PathUtil.getProperty(self.obj, self.prop)
value = quantity.Value if hasattr(quantity, 'Value') else quantity
self.widget.setProperty('rawValue', value)
diff --git a/src/Mod/Path/PathScripts/PathJob.py b/src/Mod/Path/PathScripts/PathJob.py
index 509bb7d72b..9a97fc40d9 100644
--- a/src/Mod/Path/PathScripts/PathJob.py
+++ b/src/Mod/Path/PathScripts/PathJob.py
@@ -222,6 +222,7 @@ class ObjectJob:
PathLog.debug('taking down tool controller')
for tc in obj.ToolController:
PathUtil.clearExpressionEngine(tc)
+ tc.Proxy.onDelete(tc)
doc.removeObject(tc.Name)
obj.ToolController = []
# SetupSheet
diff --git a/src/Mod/Path/PathScripts/PathJobCmd.py b/src/Mod/Path/PathScripts/PathJobCmd.py
index d8449a0b1a..61fdd692c5 100644
--- a/src/Mod/Path/PathScripts/PathJobCmd.py
+++ b/src/Mod/Path/PathScripts/PathJobCmd.py
@@ -39,13 +39,8 @@ from PySide import QtCore, QtGui
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
-LOGLEVEL = False
-
-if LOGLEVEL:
- PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
- PathLog.trackModule(PathLog.thisModule())
-else:
- PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+#PathLog.trackModule(PathLog.thisModule())
class CommandJobCreate:
'''
@@ -186,5 +181,5 @@ if FreeCAD.GuiUp:
FreeCADGui.addCommand('Path_Job', CommandJobCreate())
FreeCADGui.addCommand('Path_ExportTemplate', CommandJobTemplateExport())
-FreeCAD.Console.PrintLog("Loading PathJobGui... done\n")
+FreeCAD.Console.PrintLog("Loading PathJobCmd... done\n")
diff --git a/src/Mod/Path/PathScripts/PathMillFace.py b/src/Mod/Path/PathScripts/PathMillFace.py
index 362fd18d17..3a32be9844 100644
--- a/src/Mod/Path/PathScripts/PathMillFace.py
+++ b/src/Mod/Path/PathScripts/PathMillFace.py
@@ -80,7 +80,8 @@ class ObjectFace(PathPocketBase.ObjectPocket):
# default depths calculation not correct for facing
if prop == "Base":
job = PathUtils.findParentJob(obj)
- obj.OpStartDepth = job.Stock.Shape.BoundBox.ZMax
+ if job:
+ obj.OpStartDepth = job.Stock.Shape.BoundBox.ZMax
if len(obj.Base) >= 1:
print('processing')
@@ -95,7 +96,7 @@ class ObjectFace(PathPocketBase.ObjectPocket):
# Otherwise, top of part.
obj.OpFinalDepth = Part.makeCompound(sublist).BoundBox.ZMax
- else:
+ elif job:
obj.OpFinalDepth = job.Proxy.modelBoundBox(job).ZMax
def areaOpShapes(self, obj):
@@ -130,7 +131,7 @@ class ObjectFace(PathPocketBase.ObjectPocket):
else:
holes.append((b[0].Shape, wire))
else:
- PathLog.error('The base subobject, "{}," is not a face. Ignoring "{}."'.format(sub, sub))
+ PathLog.error('The base subobject, "{0}," is not a face. Ignoring "{0}."'.format(sub))
if obj.ExcludeRaisedAreas is True and len(holes) > 0:
for shape, wire in holes:
diff --git a/src/Mod/Path/PathScripts/PathOp.py b/src/Mod/Path/PathScripts/PathOp.py
index ae84503f46..d90202a3b9 100644
--- a/src/Mod/Path/PathScripts/PathOp.py
+++ b/src/Mod/Path/PathScripts/PathOp.py
@@ -498,10 +498,10 @@ class ObjectOp(object):
self.vertRapid = tc.VertRapid.Value
self.horizRapid = tc.HorizRapid.Value
tool = tc.Proxy.getTool(tc)
- if not tool or tool.Diameter == 0:
+ if not tool or float(tool.Diameter) == 0:
FreeCAD.Console.PrintError("No Tool found or diameter is zero. We need a tool to build a Path.")
return
- self.radius = tool.Diameter/2
+ self.radius = float(tool.Diameter) /2
self.tool = tool
obj.OpToolDiameter = tool.Diameter
diff --git a/src/Mod/Path/PathScripts/PathPostProcessor.py b/src/Mod/Path/PathScripts/PathPostProcessor.py
index 82143bf998..605786f845 100644
--- a/src/Mod/Path/PathScripts/PathPostProcessor.py
+++ b/src/Mod/Path/PathScripts/PathPostProcessor.py
@@ -38,7 +38,7 @@ class PostProcessor:
def load(cls, processor):
PathLog.track(processor)
syspath = sys.path
- paths = PathPreferences.searchPaths()
+ paths = PathPreferences.searchPathsPost()
paths.extend(sys.path)
sys.path = paths
diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py
index b2c0de8d43..0525a3b3f5 100644
--- a/src/Mod/Path/PathScripts/PathPreferences.py
+++ b/src/Mod/Path/PathScripts/PathPreferences.py
@@ -41,6 +41,13 @@ PostProcessorBlacklist = "PostProcessorBlacklist"
PostProcessorOutputFile = "PostProcessorOutputFile"
PostProcessorOutputPolicy = "PostProcessorOutputPolicy"
+LastPathToolBit = "LastPathToolBit"
+LastPathToolLibrary = "LastPathToolLibrary"
+LastPathToolShape = "LastPathToolShape"
+
+UseLegacyTools = "UseLegacyTools"
+UseAbsoluteToolPaths = "UseAbsoluteToolPaths"
+
# Linear tolerance to use when generating Paths, eg when tessellating geometry
GeometryTolerance = "GeometryTolerance"
LibAreaCurveAccuracy = "LibAreaCurveAccuarcy"
@@ -52,14 +59,16 @@ def preferences():
return FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path")
def pathScriptsSourcePath():
- return FreeCAD.getHomePath() + ("Mod/Path/PathScripts/")
+ return os.path.join(FreeCAD.getHomePath(), "Mod/Path/PathScripts/")
-def pathScriptsPostSourcePath():
- return pathScriptsSourcePath() + ("/post/")
+def pathDefaultToolsPath(sub=None):
+ if sub:
+ return os.path.join(FreeCAD.getHomePath(), "Mod/Path/Tools/", sub)
+ return os.path.join(FreeCAD.getHomePath(), "Mod/Path/Tools/")
def allAvailablePostProcessors():
allposts = []
- for path in searchPaths():
+ for path in searchPathsPost():
posts = [ str(os.path.split(os.path.splitext(p)[0])[1][:-5]) for p in glob.glob(path + '/*_post.py')]
allposts.extend(posts)
allposts.sort()
@@ -108,10 +117,52 @@ def searchPaths():
if p:
paths.append(p)
paths.append(macroFilePath())
- paths.append(pathScriptsPostSourcePath())
+ return paths
+
+def searchPathsPost():
+ paths = []
+ p = defaultFilePath()
+ if p:
+ paths.append(p)
+ paths.append(macroFilePath())
+ paths.append(os.path.join(pathScriptsSourcePath(), "post/"))
paths.append(pathScriptsSourcePath())
return paths
+def searchPathsTool(sub='Bit'):
+ paths = []
+
+ if 'Bit' == sub:
+ paths.append(lastPathToolBit())
+ if 'Library' == sub:
+ paths.append(lastPathToolLibrary())
+ if 'Shape' == sub:
+ paths.append(lastPathToolShape())
+
+ def appendPath(p, sub):
+ if p:
+ paths.append(os.path.join(p, 'Tools', sub))
+ paths.append(os.path.join(p, sub))
+ paths.append(p)
+ appendPath(defaultFilePath(), sub)
+ appendPath(macroFilePath(), sub)
+ appendPath(os.path.join(FreeCAD.getHomePath(), "Mod/Path/"), sub)
+ return paths
+
+def toolsUseLegacyTools():
+ return preferences().GetBool(UseLegacyTools, True)
+
+def toolsReallyUseLegacyTools():
+ return toolsUseLegacyTools() or not experimentalFeaturesEnabled()
+
+def toolsStoreAbsolutePaths():
+ return preferences().GetBool(UseAbsoluteToolPaths, False)
+
+def setToolsSettings(legacy, relative):
+ pref = preferences()
+ pref.SetBool(UseLegacyTools, legacy)
+ pref.SetBool(UseAbsoluteToolPaths, relative)
+
def defaultJobTemplate():
template = preferences().GetString(DefaultJobTemplate)
if 'xml' not in template:
@@ -165,3 +216,19 @@ def setDefaultTaskPanelLayout(style):
def experimentalFeaturesEnabled():
return preferences().GetBool(EnableExperimentalFeatures, False)
+
+def lastPathToolBit():
+ return preferences().GetString(LastPathToolBit, pathDefaultToolsPath('Bit'))
+def setLastPathToolBit(path):
+ return preferences().SetString(LastPathToolBit, path)
+
+def lastPathToolLibrary():
+ return preferences().GetString(LastPathToolLibrary, pathDefaultToolsPath('Library'))
+def setLastPathToolLibrary(path):
+ return preferences().SetString(LastPathToolLibrary, path)
+
+def lastPathToolShape():
+ return preferences().GetString(LastPathToolShape, pathDefaultToolsPath('Shape'))
+def setLastPathToolShape(path):
+ return preferences().SetString(LastPathToolShape, path)
+
diff --git a/src/Mod/Path/PathScripts/PathPreferencesPathJob.py b/src/Mod/Path/PathScripts/PathPreferencesPathJob.py
index 1c5efdf1bf..0782efd67b 100644
--- a/src/Mod/Path/PathScripts/PathPreferencesPathJob.py
+++ b/src/Mod/Path/PathScripts/PathPreferencesPathJob.py
@@ -71,6 +71,7 @@ class JobPreferencesPage:
policy = str(self.form.cboOutputPolicy.currentText())
PathPreferences.setOutputFileDefaults(path, policy)
self.saveStockSettings()
+ self.saveToolsSettings()
def saveStockSettings(self):
if self.form.stockGroup.isChecked():
@@ -107,6 +108,9 @@ class JobPreferencesPage:
else:
PathPreferences.setDefaultStockTemplate('')
+ def saveToolsSettings(self):
+ PathPreferences.setToolsSettings(self.form.toolsUseLegacy.isChecked(), self.form.toolsAbsolutePaths.isChecked())
+
def selectComboEntry(self, widget, text):
index = widget.findText(text, QtCore.Qt.MatchFixedString)
if index >= 0:
@@ -167,6 +171,7 @@ class JobPreferencesPage:
self.form.tbOutputFile.clicked.connect(self.browseOutputFile)
self.loadStockSettings()
+ self.loadToolSettings()
def loadStockSettings(self):
stock = PathPreferences.defaultStockTemplate()
@@ -244,6 +249,10 @@ class JobPreferencesPage:
self.form.stockCreateBox.hide()
self.form.stockCreateCylinder.hide()
+ def loadToolSettings(self):
+ self.form.toolsUseLegacy.setChecked(PathPreferences.toolsUseLegacyTools())
+ self.form.toolsAbsolutePaths.setChecked(PathPreferences.toolsStoreAbsolutePaths())
+
def getPostProcessor(self, name):
if not name in self.processor.keys():
processor = PostProcessor.load(name)
diff --git a/src/Mod/Path/PathScripts/PathSetupSheet.py b/src/Mod/Path/PathScripts/PathSetupSheet.py
index 1378d91032..aaaca2732e 100644
--- a/src/Mod/Path/PathScripts/PathSetupSheet.py
+++ b/src/Mod/Path/PathScripts/PathSetupSheet.py
@@ -209,11 +209,7 @@ class SetupSheet:
for propName in op.properties():
prop = OpPropertyName(opName, propName)
if hasattr(self.obj, prop):
- attr = getattr(self.obj, prop)
- if hasattr(attr, 'UserString'):
- settings[propName] = attr.UserString
- else:
- settings[propName] = attr
+ settings[propName] = PathUtil.getPropertyValueString(self.obj, prop)
attrs[opName] = settings
return attrs
diff --git a/src/Mod/Path/PathScripts/PathSetupSheetGui.py b/src/Mod/Path/PathScripts/PathSetupSheetGui.py
index 1537f870c2..5b5a75b2d6 100644
--- a/src/Mod/Path/PathScripts/PathSetupSheetGui.py
+++ b/src/Mod/Path/PathScripts/PathSetupSheetGui.py
@@ -306,9 +306,9 @@ class GlobalEditor(object):
def getFields(self):
def updateExpression(name, widget):
value = str(widget.text())
- val = PathGui.getProperty(self.obj, name)
+ val = PathUtil.getProperty(self.obj, name)
if val != value:
- PathGui.setProperty(self.obj, name, value)
+ PathUtil.setProperty(self.obj, name, value)
updateExpression('StartDepthExpression', self.form.setupStartDepthExpr)
updateExpression('FinalDepthExpression', self.form.setupFinalDepthExpr)
diff --git a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py
index 146d70821a..fba3d33f0e 100644
--- a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py
+++ b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py
@@ -106,6 +106,10 @@ class PropertyQuantity(Property):
return Property.displayString(self)
return self.value.getUserPreferred()[0]
+class PropertyAngle(PropertyQuantity):
+ def typeString(self):
+ return "Angle"
+
class PropertyDistance(PropertyQuantity):
def typeString(self):
return "Distance"
@@ -122,14 +126,23 @@ class PropertyFloat(Property):
def typeString(self):
return "Float"
+ def valueFromString(self, string):
+ return float(string)
+
class PropertyInteger(Property):
def typeString(self):
return "Integer"
+ def valueFromString(self, string):
+ return int(string)
+
class PropertyBool(Property):
def typeString(self):
return "Bool"
+ def valueFromString(self, string):
+ return bool(string)
+
class PropertyString(Property):
def typeString(self):
return "String"
@@ -137,24 +150,25 @@ class PropertyString(Property):
class OpPrototype(object):
PropertyType = {
- 'App::PropertyBool': PropertyBool,
- 'App::PropertyDistance': PropertyDistance,
- 'App::PropertyEnumeration': PropertyEnumeration,
- 'App::PropertyFloat': PropertyFloat,
- 'App::PropertyFloatConstraint': Property,
- 'App::PropertyFloatList': Property,
- 'App::PropertyInteger': PropertyInteger,
- 'App::PropertyIntegerList': PropertyInteger,
- 'App::PropertyLength': PropertyLength,
- 'App::PropertyLink': Property,
- 'App::PropertyLinkList': Property,
- 'App::PropertyLinkSubListGlobal': Property,
- 'App::PropertyPercent': PropertyPercent,
- 'App::PropertyString': PropertyString,
- 'App::PropertyStringList': Property,
- 'App::PropertyVectorDistance': Property,
- 'App::PropertyVectorList': Property,
- 'Part::PropertyPartShape': Property,
+ 'App::PropertyAngle': PropertyAngle,
+ 'App::PropertyBool': PropertyBool,
+ 'App::PropertyDistance': PropertyDistance,
+ 'App::PropertyEnumeration': PropertyEnumeration,
+ 'App::PropertyFloat': PropertyFloat,
+ 'App::PropertyFloatConstraint': Property,
+ 'App::PropertyFloatList': Property,
+ 'App::PropertyInteger': PropertyInteger,
+ 'App::PropertyIntegerList': PropertyInteger,
+ 'App::PropertyLength': PropertyLength,
+ 'App::PropertyLink': Property,
+ 'App::PropertyLinkList': Property,
+ 'App::PropertyLinkSubListGlobal': Property,
+ 'App::PropertyPercent': PropertyPercent,
+ 'App::PropertyString': PropertyString,
+ 'App::PropertyStringList': Property,
+ 'App::PropertyVectorDistance': Property,
+ 'App::PropertyVectorList': Property,
+ 'Part::PropertyPartShape': Property,
}
def __init__(self, name):
diff --git a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototypeGui.py b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototypeGui.py
index c215d0057d..f89b7b40ed 100644
--- a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototypeGui.py
+++ b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototypeGui.py
@@ -112,6 +112,21 @@ class _PropertyStringEditor(_PropertyEditor):
def setModelData(self, widget):
self.prop.setValue(widget.text())
+class _PropertyAngleEditor(_PropertyEditor):
+ '''Editor for angle values - uses a line edit'''
+
+ def widget(self, parent):
+ return QtGui.QLineEdit(parent)
+
+ def setEditorData(self, widget):
+ quantity = self.prop.getValue()
+ if quantity is None:
+ quantity = FreeCAD.Units.Quantity(0, FreeCAD.Units.Angle)
+ widget.setText(quantity.getUserPreferred()[0])
+
+ def setModelData(self, widget):
+ self.prop.setValue(FreeCAD.Units.Quantity(widget.text()))
+
class _PropertyLengthEditor(_PropertyEditor):
'''Editor for length values - uses a line edit.'''
@@ -174,15 +189,16 @@ class _PropertyFloatEditor(_PropertyEditor):
self.prop.setValue(widget.value())
_EditorFactory = {
- PathSetupSheetOpPrototype.Property: None,
- PathSetupSheetOpPrototype.PropertyBool: _PropertyBoolEditor,
- PathSetupSheetOpPrototype.PropertyDistance: _PropertyLengthEditor,
- PathSetupSheetOpPrototype.PropertyEnumeration: _PropertyEnumEditor,
- PathSetupSheetOpPrototype.PropertyFloat: _PropertyFloatEditor,
- PathSetupSheetOpPrototype.PropertyInteger: _PropertyIntegerEditor,
- PathSetupSheetOpPrototype.PropertyLength: _PropertyLengthEditor,
- PathSetupSheetOpPrototype.PropertyPercent: _PropertyPercentEditor,
- PathSetupSheetOpPrototype.PropertyString: _PropertyStringEditor,
+ PathSetupSheetOpPrototype.Property: None,
+ PathSetupSheetOpPrototype.PropertyAngle: _PropertyAngleEditor,
+ PathSetupSheetOpPrototype.PropertyBool: _PropertyBoolEditor,
+ PathSetupSheetOpPrototype.PropertyDistance: _PropertyLengthEditor,
+ PathSetupSheetOpPrototype.PropertyEnumeration: _PropertyEnumEditor,
+ PathSetupSheetOpPrototype.PropertyFloat: _PropertyFloatEditor,
+ PathSetupSheetOpPrototype.PropertyInteger: _PropertyIntegerEditor,
+ PathSetupSheetOpPrototype.PropertyLength: _PropertyLengthEditor,
+ PathSetupSheetOpPrototype.PropertyPercent: _PropertyPercentEditor,
+ PathSetupSheetOpPrototype.PropertyString: _PropertyStringEditor,
}
def Editor(prop):
diff --git a/src/Mod/Path/PathScripts/PathSimulatorGui.py b/src/Mod/Path/PathScripts/PathSimulatorGui.py
index 7f3c3e2f05..2260d9ccc8 100644
--- a/src/Mod/Path/PathScripts/PathSimulatorGui.py
+++ b/src/Mod/Path/PathScripts/PathSimulatorGui.py
@@ -125,7 +125,7 @@ class PathSimulation:
# if hasattr(self.operation, "ToolController"):
# self.tool = self.operation.ToolController.Tool
if (self.tool is not None):
- toolProf = self.CreateToolProfile(self.tool, Vector(0, 1, 0), Vector(0, 0, 0), self.tool.Diameter / 2.0)
+ toolProf = self.CreateToolProfile(self.tool, Vector(0, 1, 0), Vector(0, 0, 0), float(self.tool.Diameter) / 2.0)
self.cutTool.Shape = Part.makeSolid(toolProf.revolve(Vector(0, 0, 0), Vector(0, 0, 1)))
self.cutTool.ViewObject.show()
self.voxSim.SetCurrentTool(self.tool)
@@ -298,7 +298,7 @@ class PathSimulation:
# except:
# return (None, e1.valueAt(e1.LastParameter))
# height = self.height
- # rad = tool.Diameter / 2.0 - 0.001 * curpos[2] # hack to overcome occ bug
+ # rad = float(tool.Diameter) / 2.0 - 0.001 * curpos[2] # hack to overcome occ bug
# if type(e1.Curve) is Part.Circle and e1.Curve.Radius <= rad: # hack to overcome occ bug
# rad = e1.Curve.Radius - 0.001
# # return (None, e1.valueAt(e1.LastParameter))
@@ -350,7 +350,7 @@ class PathSimulation:
# height = self.height
# hack to overcome occ bugs
- rad = tool.Diameter / 2.0 - 0.001 * pos[2]
+ rad = float(tool.Diameter) / 2.0 - 0.001 * pos[2]
# rad = rad + 0.001 * self.icmd
if type(toolPath.Curve) is Part.Circle and toolPath.Curve.Radius <= rad:
rad = toolPath.Curve.Radius - 0.01 * (pos[2] + 1)
@@ -386,7 +386,7 @@ class PathSimulation:
# create radial profile of the tool (90 degrees to the direction of the path)
def CreateToolProfile(self, tool, dir, pos, rad):
type = tool.ToolType
- # rad = tool.Diameter / 2.0 - 0.001 * pos[2] # hack to overcome occ bug
+ # rad = float(tool.Diameter) / 2.0 - 0.001 * pos[2] # hack to overcome occ bug
xf = dir[0] * rad
yf = dir[1] * rad
xp = pos[0]
diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py
index 9d97b4c63b..09d8d82219 100644
--- a/src/Mod/Path/PathScripts/PathSurface.py
+++ b/src/Mod/Path/PathScripts/PathSurface.py
@@ -1792,10 +1792,11 @@ class ObjectSurface(PathOp.ObjectOp):
def setOclCutter(self, obj):
# Set cutter details
# https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details
- diam_1 = obj.ToolController.Tool.Diameter
- lenOfst = obj.ToolController.Tool.LengthOffset
- FR = obj.ToolController.Tool.FlatRadius
- CEH = obj.ToolController.Tool.CuttingEdgeHeight
+ diam_1 = float(obj.ToolController.Tool.Diameter)
+ lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0
+ FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0
+ CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0
+ CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0
if obj.ToolController.Tool.ToolType == 'EndMill':
# Standard End Mill
@@ -1817,13 +1818,13 @@ class ObjectSurface(PathOp.ObjectOp):
# Bull Nose or Corner Radius cutter
# Reference: https://www.fine-tools.com/halbstabfraeser.html
# OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
- self.cutter = ocl.ConeCutter(diam_1, (obj.ToolController.Tool.CuttingEdgeAngle / 2), lenOfst)
+ self.cutter = ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
elif obj.ToolController.Tool.ToolType == 'ChamferMill':
# Bull Nose or Corner Radius cutter
# Reference: https://www.fine-tools.com/halbstabfraeser.html
# OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
- self.cutter = ocl.ConeCutter(diam_1, (obj.ToolController.Tool.CuttingEdgeAngle / 2), lenOfst)
+ self.cutter = ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
else:
# Default to standard end mill
self.cutter = ocl.CylCutter(diam_1, (CEH + lenOfst))
diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py
new file mode 100644
index 0000000000..eeae4a70ae
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathToolBit.py
@@ -0,0 +1,383 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2019 sliptonic *
+# * *
+# * 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 Part
+import PathScripts.PathGeom as PathGeom
+import PathScripts.PathLog as PathLog
+import PathScripts.PathPreferences as PathPreferences
+import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype
+import PathScripts.PathUtil as PathUtil
+import PySide
+import Sketcher
+import json
+import math
+import os
+import zipfile
+
+__title__ = "Tool bits."
+__author__ = "sliptonic (Brad Collette)"
+__url__ = "http://www.freecadweb.org"
+__doc__ = "Class to deal with and represent a tool bit."
+
+#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
+#PathLog.trackModule()
+
+def translate(context, text, disambig=None):
+ return PySide.QtCore.QCoreApplication.translate(context, text, disambig)
+
+ParameterTypeConstraint = {
+ 'Angle': 'App::PropertyAngle',
+ 'Distance': 'App::PropertyLength',
+ 'DistanceX': 'App::PropertyLength',
+ 'DistanceY': 'App::PropertyLength',
+ 'Radius': 'App::PropertyLength'
+ }
+
+
+def _findTool(path, typ, dbg=False):
+ if os.path.exists(path):
+ if dbg:
+ PathLog.debug("Found {} at {}".format(typ, path))
+ return path
+
+ def searchFor(pname, fname):
+ if dbg:
+ PathLog.debug("Looking for {}".format(pname))
+ if fname:
+ for p in PathPreferences.searchPathsTool(typ):
+ f = os.path.join(p, fname)
+ if dbg:
+ PathLog.debug(" Checking {}".format(f))
+ if os.path.exists(f):
+ if dbg:
+ PathLog.debug(" Found {} at {}".format(typ, f))
+ return f
+ if pname and os.path.sep != pname:
+ ppname, pfname = os.path.split(pname)
+ ffname = os.path.join(pfname, fname) if fname else pfname
+ return searchFor(ppname, ffname)
+ return None
+
+ return searchFor(path, '')
+
+def findShape(path):
+ '''findShape(path) ... search for path, full and partially in all known shape directories.'''
+ return _findTool(path, 'Shape')
+
+def findBit(path):
+ if path.endswith('.fctb'):
+ return _findTool(path, 'Bit')
+ return _findTool("{}.fctb".format(path), 'Bit')
+
+def findLibrary(path, dbg=False):
+ if path.endswith('.fctl'):
+ return _findTool(path, 'Library', dbg)
+ return _findTool("{}.fctl".format(path), 'Library', dbg)
+
+def _findRelativePath(path, typ):
+ relative = path
+ for p in PathPreferences.searchPathsTool(typ):
+ if path.startswith(p):
+ p = path[len(p):]
+ if os.path.sep == p[0]:
+ p = p[1:]
+ if len(p) < len(relative):
+ relative = p
+ return relative
+
+def findRelativePathShape(path):
+ return _findRelativePath(path, 'Shape')
+
+def findRelativePathTool(path):
+ return _findRelativePath(path, 'Bit')
+
+def findRelativePathLibrary(path):
+ return _findRelativePath(path, 'Library')
+
+def updateConstraint(sketch, name, value):
+ for i, constraint in enumerate(sketch.Constraints):
+ if constraint.Name.split(';')[0] == name:
+ constr = None
+ if constraint.Type in ['DistanceX', 'DistanceY', 'Distance', 'Radius', 'Angle']:
+ constr = Sketcher.Constraint(constraint.Type, constraint.First, constraint.FirstPos, constraint.Second, constraint.SecondPos, value)
+ else:
+ print(constraint.Name, constraint.Type)
+
+ if constr is not None:
+ if not PathGeom.isRoughly(constraint.Value, value.Value):
+ PathLog.track(name, constraint.Type, 'update', i, "(%.2f -> %.2f)" % (constraint.Value, value.Value))
+ sketch.delConstraint(i)
+ sketch.recompute()
+ n = sketch.addConstraint(constr)
+ sketch.renameConstraint(n, constraint.Name)
+ else:
+ PathLog.track(name, constraint.Type, 'unchanged')
+ break
+
+
+PropertyGroupBit = 'Bit'
+PropertyGroupAttribute = 'Attribute'
+
+class ToolBit(object):
+
+ def __init__(self, obj, shapeFile):
+ PathLog.track(obj.Label, shapeFile)
+ self.obj = obj
+ obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape'))
+ obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit'))
+ obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool'))
+ if shapeFile is None:
+ obj.BitShape = 'endmill.fcstd'
+ self._setupBitShape(obj)
+ self.unloadBitBody(obj)
+ else:
+ obj.BitShape = shapeFile
+ self._setupBitShape(obj)
+ self.onDocumentRestored(obj)
+
+ def __getstate__(self):
+ return None
+
+ def __setstate__(self, state):
+ for obj in FreeCAD.ActiveDocument.Objects:
+ if hasattr(obj, 'Proxy') and obj.Proxy == self:
+ self.obj = obj
+ break
+ return None
+
+ def propertyNamesBit(self, obj):
+ return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupBit]
+
+ def propertyNamesAttribute(self, obj):
+ return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupAttribute]
+
+ def onDocumentRestored(self, obj):
+ obj.setEditorMode('BitShape', 1)
+ obj.setEditorMode('BitBody', 2)
+ obj.setEditorMode('File', 1)
+ obj.setEditorMode('Shape', 2)
+
+ for prop in self.propertyNamesBit(obj):
+ obj.setEditorMode(prop, 1)
+ # I currently don't see why these need to be read-only
+ #for prop in self.propertyNamesAttribute(obj):
+ # obj.setEditorMode(prop, 1)
+
+ def onChanged(self, obj, prop):
+ PathLog.track(obj.Label, prop)
+ if prop == 'BitShape' and not 'Restore' in obj.State:
+ self._setupBitShape(obj)
+ #elif obj.getGroupOfProperty(prop) == PropertyGroupBit:
+ # self._updateBitShape(obj, [prop])
+
+ def onDelete(self, obj, arg2=None):
+ PathLog.track(obj.Label)
+ self.unloadBitBody(obj)
+
+ def _updateBitShape(self, obj, properties=None):
+ if not obj.BitBody is None:
+ if not properties:
+ properties = self.propertyNamesBit(obj)
+ for prop in properties:
+ for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']:
+ PathLog.track(obj.Label, sketch.Label, prop)
+ updateConstraint(sketch, prop, obj.getPropertyByName(prop))
+ self._copyBitShape(obj)
+
+ def _copyBitShape(self, obj):
+ obj.Document.recompute()
+ if obj.BitBody and obj.BitBody.Shape:
+ obj.Shape = obj.BitBody.Shape
+ else:
+ obj.Shape = Part.Shape()
+
+ def _loadBitBody(self, obj, path=None):
+ p = path if path else obj.BitShape
+ docOpened = False
+ doc = None
+ for d in FreeCAD.listDocuments():
+ if FreeCAD.getDocument(d).FileName == p:
+ doc = FreeCAD.getDocument(d)
+ break
+ if doc is None:
+ p = findShape(p)
+ if not path and p != obj.BitShape:
+ obj.BitShape = p
+ doc = FreeCAD.open(p)
+ docOpened = True
+ return (doc, docOpened)
+
+ def _removeBitBody(self, obj):
+ if obj.BitBody:
+ obj.BitBody.removeObjectsFromDocument()
+ obj.Document.removeObject(obj.BitBody.Name)
+ obj.BitBody = None
+
+ def _deleteBitSetup(self, obj):
+ PathLog.track(obj.Label)
+ self._removeBitBody(obj)
+ self._copyBitShape(obj)
+ for prop in self.propertyNamesBit(obj):
+ obj.removeProperty(prop)
+
+ def loadBitBody(self, obj, force=False):
+ if force or not obj.BitBody:
+ activeDoc = FreeCAD.ActiveDocument
+ if force:
+ self._removeBitBody(obj)
+ (doc, opened) = self._loadBitBody(obj)
+ obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True)
+ if opened:
+ FreeCAD.setActiveDocument(activeDoc.Name)
+ FreeCAD.closeDocument(doc.Name)
+ self._updateBitShape(obj)
+
+ def unloadBitBody(self, obj):
+ self._removeBitBody(obj)
+
+ def _setupBitShape(self, obj, path=None):
+ activeDoc = FreeCAD.ActiveDocument
+ (doc, docOpened) = self._loadBitBody(obj, path)
+
+ obj.Label = doc.RootObjects[0].Label
+ self._deleteBitSetup(obj)
+ obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True)
+ if docOpened:
+ FreeCAD.setActiveDocument(activeDoc.Name)
+ FreeCAD.closeDocument(doc.Name)
+
+ if obj.BitBody.ViewObject:
+ obj.BitBody.ViewObject.Visibility = False
+ self._copyBitShape(obj)
+
+ for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']:
+ for constraint in [c for c in sketch.Constraints if c.Name != '']:
+ typ = ParameterTypeConstraint.get(constraint.Type)
+ PathLog.track(constraint, typ)
+ if typ is not None:
+ parts = [p.strip() for p in constraint.Name.split(';')]
+ prop = parts[0]
+ desc = ''
+ if len(parts) > 1:
+ desc = parts[1]
+ obj.addProperty(typ, prop, PropertyGroupBit, desc)
+ obj.setEditorMode(prop, 1)
+ value = constraint.Value
+ if constraint.Type == 'Angle':
+ value = value * 180 / math.pi
+ PathUtil.setProperty(obj, prop, value)
+
+ def getBitThumbnail(self, obj):
+ if obj.BitShape:
+ path = findShape(obj.BitShape)
+ if path:
+ with open(path, 'rb') as fd:
+ zf = zipfile.ZipFile(fd)
+ pf = zf.open('thumbnails/Thumbnail.png', 'r')
+ data = pf.read()
+ pf.close()
+ return data
+ return None
+
+ def saveToFile(self, obj, path, setFile=True):
+ try:
+ with open(path, 'w') as fp:
+ json.dump(self.shapeAttrs(obj), fp, indent=' ')
+ if setFile:
+ obj.File = path
+ return True
+ except (OSError, IOError) as e:
+ PathLog.error("Could not save tool %s to %s (%s)" % (obj.Label, path, e))
+ raise
+
+ def shapeAttrs(self, obj):
+ attrs = {}
+ attrs['version'] = 2 # Path.Tool is version 1
+ attrs['name'] = obj.Label
+ if PathPreferences.toolsStoreAbsolutePaths():
+ attrs['shape'] = obj.BitShape
+ else:
+ attrs['shape'] = findRelativePathShape(obj.BitShape)
+ params = {}
+ for name in self.propertyNamesBit(obj):
+ params[name] = PathUtil.getPropertyValueString(obj, name)
+ attrs['parameter'] = params
+ params = {}
+ for name in self.propertyNamesAttribute(obj):
+ params[name] = PathUtil.getPropertyValueString(obj, name)
+ attrs['attribute'] = params
+ return attrs
+
+def Declaration(path):
+ with open(path, 'r') as fp:
+ return json.load(fp)
+
+class AttributePrototype(PathSetupSheetOpPrototype.OpPrototype):
+
+ def __init__(self):
+ PathSetupSheetOpPrototype.OpPrototype.__init__(self, 'ToolBitAttribute')
+ self.addProperty('App::PropertyEnumeration', 'Material', PropertyGroupAttribute, translate('PathToolBit', 'Tool bit material'))
+ self.Material = ['Carbide', 'CastAlloy', 'Ceramics', 'Diamond', 'HighCarbonToolSteel', 'HighSpeedSteel', 'Sialon']
+ self.addProperty('App::PropertyDistance', 'LengthOffset', PropertyGroupAttribute, translate('PathToolBit', 'Length offset in Z direction'))
+ self.addProperty('App::PropertyInteger', 'Flutes', PropertyGroupAttribute, translate('PathToolBit', 'The number of flutes'))
+ self.addProperty('App::PropertyDistance', 'ChipLoad', PropertyGroupAttribute, translate('PathToolBit', 'Chipload as per manufacturer'))
+
+
+class ToolBitFactory(object):
+
+ def CreateFromAttrs(self, attrs, name='ToolBit'):
+ # pylint: disable=protected-access
+ obj = Factory.Create(name, attrs['shape'])
+ obj.Label = attrs['name']
+ params = attrs['parameter']
+ for prop in params:
+ PathUtil.setProperty(obj, prop, params[prop])
+ obj.Proxy._updateBitShape(obj)
+ obj.Proxy.unloadBitBody(obj)
+ params = attrs['attribute']
+ proto = AttributePrototype()
+ for pname in params:
+ prop = proto.getProperty(pname)
+ val = prop.valueFromString(params[pname])
+ print("prop[%s] = %s (%s)" % (pname, params[pname], type(val)))
+ prop.setupProperty(obj, pname, PropertyGroupAttribute, prop.valueFromString(params[pname]))
+ return obj
+
+ def CreateFrom(self, path, name='ToolBit'):
+ try:
+ data = Declaration(path)
+ bit = Factory.CreateFromAttrs(data, name)
+ bit.File = path
+ return bit
+ except (OSError, IOError) as e:
+ PathLog.error("%s not a valid tool file (%s)" % (path, e))
+ raise
+
+ def Create(self, name='ToolBit', shapeFile=None):
+ obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name)
+ obj.Proxy = ToolBit(obj, shapeFile)
+ return obj
+
+Factory = ToolBitFactory()
diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py
new file mode 100644
index 0000000000..b172a0aa43
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2019 sliptonic *
+# * *
+# * 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 FreeCADGui
+import PathScripts
+import os
+
+from PySide import QtCore
+
+class CommandToolBitCreate:
+ '''
+ Command used to create a new Tool.
+ '''
+
+ def __init__(self):
+ pass
+
+ def GetResources(self):
+ return {'Pixmap': 'Path-ToolBit',
+ 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Create Tool"),
+ 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Creates a new ToolBit object")}
+
+ def IsActive(self):
+ return FreeCAD.ActiveDocument is not None
+
+ def Activated(self):
+ obj = PathScripts.PathToolBit.Factory.Create()
+ obj.ViewObject.Proxy.setCreate(obj.ViewObject)
+
+class CommandToolBitSave:
+ '''
+ Command used to save an existing Tool to a file.
+ '''
+
+ def __init__(self, saveAs):
+ self.saveAs = saveAs
+
+ def GetResources(self):
+ if self.saveAs:
+ menuTxt = QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Save Tool as...")
+ else:
+ menuTxt = QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Save Tool")
+ return {'Pixmap': 'Path-ToolBit',
+ 'MenuText': menuTxt,
+ 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Save an existing ToolBit object to a file")}
+
+ def selectedTool(self):
+ sel = FreeCADGui.Selection.getSelectionEx()
+ if 1 == len(sel) and isinstance(sel[0].Object.Proxy, PathScripts.PathToolBit.ToolBit):
+ return sel[0].Object
+ return None
+
+ def IsActive(self):
+ tool = self.selectedTool()
+ if tool:
+ if tool.File:
+ return True
+ return self.saveAs
+ return False
+
+ def Activated(self):
+ from PySide import QtGui
+ tool = self.selectedTool()
+ if tool:
+ path = None
+ if not tool.File or self.saveAs:
+ if tool.File:
+ fname = tool.File
+ else:
+ fname = os.path.join(PathScripts.PathPreferences.lastPathToolBit(), tool.Label + '.fctb')
+ foo = QtGui.QFileDialog.getSaveFileName(QtGui.QApplication.activeWindow(), "Tool", fname, "*.fctb")
+ if foo:
+ path = foo[0]
+ else:
+ path = tool.File
+
+ if path:
+ if not path.endswith('.fctb'):
+ path += '.fctb'
+ tool.Proxy.saveToFile(tool, path)
+ PathScripts.PathPreferences.setLastPathToolBit(os.path.dirname(path))
+
+class CommandToolBitLoad:
+ '''
+ Command used to load an existing Tool from a file into the current document.
+ '''
+
+ def __init__(self):
+ pass
+
+ def GetResources(self):
+ return {'Pixmap': 'Path-ToolBit',
+ 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Load Tool"),
+ 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Load an existing ToolBit object from a file")}
+
+ def selectedTool(self):
+ sel = FreeCADGui.Selection.getSelectionEx()
+ if 1 == len(sel) and isinstance(sel[0].Object.Proxy, PathScripts.PathToolBit.ToolBit):
+ return sel[0].Object
+ return None
+
+ def IsActive(self):
+ return FreeCAD.ActiveDocument is not None
+
+ def Activated(self):
+ if PathScripts.PathToolBitGui.LoadTools():
+ FreeCAD.ActiveDocument.recompute()
+
+if FreeCAD.GuiUp:
+ FreeCADGui.addCommand('Path_ToolBitCreate', CommandToolBitCreate())
+ FreeCADGui.addCommand('Path_ToolBitLoad', CommandToolBitLoad())
+ FreeCADGui.addCommand('Path_ToolBitSave', CommandToolBitSave(False))
+ FreeCADGui.addCommand('Path_ToolBitSaveAs', CommandToolBitSave(True))
+
+CommandList = ['Path_ToolBitCreate', 'Path_ToolBitLoad', 'Path_ToolBitSave', 'Path_ToolBitSaveAs']
+
+FreeCAD.Console.PrintLog("Loading PathToolBitCmd... done\n")
diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py
new file mode 100644
index 0000000000..fdea4e672a
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py
@@ -0,0 +1,197 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2019 sliptonic *
+# * *
+# * 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 FreeCADGui
+import PathScripts.PathGui as PathGui
+import PathScripts.PathLog as PathLog
+import PathScripts.PathPreferences as PathPreferences
+import PathScripts.PathSetupSheetGui as PathSetupSheetGui
+import PathScripts.PathToolBit as PathToolBit
+import os
+import re
+
+from PySide import QtCore, QtGui
+
+#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
+#PathLog.trackModule(PathLog.thisModule())
+
+# Qt translation handling
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+class ToolBitEditor(object):
+ '''UI and controller for editing a ToolBit.
+ The controller embeds the UI to the parentWidget which has to have a layout attached to it.
+ '''
+
+ def __init__(self, tool, parentWidget=None):
+ self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitEditor.ui")
+
+ if parentWidget:
+ self.form.setParent(parentWidget)
+ parentWidget.layout().addWidget(self.form)
+
+ self.tool = tool
+ if not tool.BitShape:
+ self.tool.BitShape = 'endmill.fcstd'
+ self.tool.Proxy.loadBitBody(self.tool)
+ self.setupTool(self.tool)
+ self.setupAttributes(self.tool)
+
+ def setupTool(self, tool):
+ layout = self.form.bitParams.layout()
+ for i in range(layout.rowCount() - 1, -1, -1):
+ layout.removeRow(i)
+ editor = {}
+ ui = FreeCADGui.UiLoader()
+ for name in tool.PropertiesList:
+ if tool.getGroupOfProperty(name) == PathToolBit.PropertyGroupBit:
+ qsb = ui.createWidget('Gui::QuantitySpinBox')
+ editor[name] = PathGui.QuantitySpinBox(qsb, tool, name)
+ label = QtGui.QLabel(re.sub('([A-Z][a-z]+)', r' \1', re.sub('([A-Z]+)', r' \1', name)))
+ #if parameter.get('Desc'):
+ # qsb.setToolTip(parameter['Desc'])
+ layout.addRow(label, qsb)
+ self.bitEditor = editor
+ img = tool.Proxy.getBitThumbnail(tool)
+ if img:
+ self.form.image.setPixmap(QtGui.QPixmap(QtGui.QImage.fromData(img)))
+ else:
+ self.form.image.setPixmap(QtGui.QPixmap())
+
+ def setupAttributes(self, tool):
+ self.proto = PathToolBit.AttributePrototype()
+ self.props = sorted(self.proto.properties)
+ self.delegate = PathSetupSheetGui.Delegate(self.form)
+ self.model = QtGui.QStandardItemModel(len(self.props), 3, self.form)
+ self.model.setHorizontalHeaderLabels(['Set', 'Property', 'Value'])
+
+ for i, name in enumerate(self.props):
+ prop = self.proto.getProperty(name)
+ isset = hasattr(tool, name)
+ if isset:
+ prop.setValue(getattr(tool, name))
+
+ self.model.setData(self.model.index(i, 0), isset, QtCore.Qt.EditRole)
+ self.model.setData(self.model.index(i, 1), name, QtCore.Qt.EditRole)
+ self.model.setData(self.model.index(i, 2), prop, PathSetupSheetGui.Delegate.PropertyRole)
+ self.model.setData(self.model.index(i, 2), prop.displayString(), QtCore.Qt.DisplayRole)
+
+ self.model.item(i, 0).setCheckable(True)
+ self.model.item(i, 0).setText('')
+ self.model.item(i, 1).setEditable(False)
+ self.model.item(i, 1).setToolTip(prop.info)
+ self.model.item(i, 2).setToolTip(prop.info)
+
+ if isset:
+ self.model.item(i, 0).setCheckState(QtCore.Qt.Checked)
+ else:
+ self.model.item(i, 0).setCheckState(QtCore.Qt.Unchecked)
+ self.model.item(i, 1).setEnabled(False)
+ self.model.item(i, 2).setEnabled(False)
+
+ self.form.attrTable.setModel(self.model)
+ self.form.attrTable.setItemDelegateForColumn(2, self.delegate)
+ self.form.attrTable.resizeColumnsToContents()
+ self.form.attrTable.verticalHeader().hide()
+
+ self.model.dataChanged.connect(self.updateData)
+
+ def updateData(self, topLeft, bottomRight):
+ # pylint: disable=unused-argument
+ if 0 == topLeft.column():
+ isset = self.model.item(topLeft.row(), 0).checkState() == QtCore.Qt.Checked
+ self.model.item(topLeft.row(), 1).setEnabled(isset)
+ self.model.item(topLeft.row(), 2).setEnabled(isset)
+
+ def accept(self):
+ self.refresh()
+ self.tool.Proxy.unloadBitBody(self.tool)
+
+ # get the attributes
+ for i, name in enumerate(self.props):
+ prop = self.proto.getProperty(name)
+ enabled = self.model.item(i, 0).checkState() == QtCore.Qt.Checked
+ if enabled and not prop.getValue() is None:
+ prop.setupProperty(self.tool, name, PathToolBit.PropertyGroupAttribute, prop.getValue())
+ elif hasattr(self.tool, name):
+ self.tool.removeProperty(name)
+
+ def reject(self):
+ self.tool.Proxy.unloadBitBody(self.tool)
+
+ def updateUI(self):
+ PathLog.track()
+ self.form.toolName.setText(self.tool.Label)
+ self.form.shapePath.setText(self.tool.BitShape)
+
+ for editor in self.bitEditor:
+ self.bitEditor[editor].updateSpinBox()
+
+ def updateShape(self):
+ self.tool.BitShape = str(self.form.shapePath.text())
+ self.setupTool(self.tool)
+ self.form.toolName.setText(self.tool.Label)
+
+ for editor in self.bitEditor:
+ self.bitEditor[editor].updateSpinBox()
+
+ def updateTool(self):
+ # pylint: disable=protected-access
+ PathLog.track()
+ self.tool.Label = str(self.form.toolName.text())
+ self.tool.BitShape = str(self.form.shapePath.text())
+
+ for editor in self.bitEditor:
+ self.bitEditor[editor].updateProperty()
+
+ self.tool.Proxy._updateBitShape(self.tool)
+
+ def refresh(self):
+ PathLog.track()
+ self.form.blockSignals(True)
+ self.updateTool()
+ self.updateUI()
+ self.form.blockSignals(False)
+
+ def selectShape(self):
+ path = self.tool.BitShape
+ if not path:
+ path = PathPreferences.lastPathToolShape()
+ foo = QtGui.QFileDialog.getOpenFileName(self.form,
+ "Path - Tool Shape",
+ path,
+ "*.fcstd")
+ if foo and foo[0]:
+ PathPreferences.setLastPathToolShape(os.path.dirname(foo[0]))
+ self.form.shapePath.setText(foo[0])
+ self.updateShape()
+
+ def setupUI(self):
+ PathLog.track()
+ self.updateUI()
+
+ self.form.toolName.editingFinished.connect(self.refresh)
+ self.form.shapePath.editingFinished.connect(self.updateShape)
+ self.form.shapeSet.clicked.connect(self.selectShape)
diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py
new file mode 100644
index 0000000000..ae21e05172
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathToolBitGui.py
@@ -0,0 +1,298 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2019 sliptonic *
+# * *
+# * 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 FreeCADGui
+import PathScripts.PathIconViewProvider as PathIconViewProvider
+import PathScripts.PathLog as PathLog
+import PathScripts.PathPreferences as PathPreferences
+import PathScripts.PathToolBit as PathToolBit
+import PathScripts.PathToolBitEdit as PathToolBitEdit
+import os
+
+from PySide import QtCore, QtGui
+
+__title__ = "Tool Bit UI"
+__author__ = "sliptonic (Brad Collette)"
+__url__ = "http://www.freecadweb.org"
+__doc__ = "Task panel editor for a ToolBit"
+
+# Qt translation handling
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
+#PathLog.trackModule(PathLog.thisModule())
+
+class ViewProvider(object):
+ '''ViewProvider for a ToolBit.
+ It's sole job is to provide an icon and invoke the TaskPanel on edit.'''
+
+ def __init__(self, vobj, name):
+ PathLog.track(name, vobj.Object)
+ self.panel = None
+ self.icon = name
+ self.obj = vobj.Object
+ self.vobj = vobj
+ vobj.Proxy = self
+
+ def attach(self, vobj):
+ PathLog.track(vobj.Object)
+ self.vobj = vobj
+ self.obj = vobj.Object
+
+ def getIcon(self):
+ png = self.obj.Proxy.getBitThumbnail(self.obj)
+ if png:
+ pixmap = QtGui.QPixmap()
+ pixmap.loadFromData(png, 'PNG')
+ return QtGui.QIcon(pixmap)
+ return ':/icons/Path-ToolBit.svg'
+
+ def __getstate__(self):
+ return None
+
+ def __setstate__(self, state):
+ # pylint: disable=unused-argument
+ return None
+
+ def onDelete(self, vobj, arg2=None):
+ PathLog.track(vobj.Object.Label)
+ vobj.Object.Proxy.onDelete(vobj.Object)
+
+ def getDisplayMode(self, mode):
+ # pylint: disable=unused-argument
+ return 'Default'
+
+ def _openTaskPanel(self, vobj, deleteOnReject):
+ PathLog.track()
+ self.panel = TaskPanel(vobj, deleteOnReject)
+ FreeCADGui.Control.closeDialog()
+ FreeCADGui.Control.showDialog(self.panel)
+ self.panel.setupUi()
+
+ def setCreate(self, vobj):
+ PathLog.track()
+ self._openTaskPanel(vobj, True)
+
+ def setEdit(self, vobj, mode=0):
+ # pylint: disable=unused-argument
+ self._openTaskPanel(vobj, False)
+ return True
+
+ def unsetEdit(self, vobj, mode):
+ # pylint: disable=unused-argument
+ FreeCADGui.Control.closeDialog()
+ self.panel = None
+ return
+
+ def claimChildren(self):
+ if self.obj.BitBody:
+ return [self.obj.BitBody]
+ return []
+
+ def doubleClicked(self, vobj):
+ self.setEdit(vobj)
+
+class TaskPanel:
+ '''TaskPanel for the SetupSheet - if it is being edited directly.'''
+
+ def __init__(self, vobj, deleteOnReject):
+ PathLog.track(vobj.Object.Label)
+ self.vobj = vobj
+ self.obj = vobj.Object
+ self.editor = PathToolBitEdit.ToolBitEditor(self.obj)
+ self.form = self.editor.form
+ self.deleteOnReject = deleteOnReject
+ FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Edit ToolBit'))
+
+ def reject(self):
+ FreeCAD.ActiveDocument.abortTransaction()
+ self.editor.reject()
+ FreeCADGui.Control.closeDialog()
+ if self.deleteOnReject:
+ FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Uncreate ToolBit'))
+ self.editor.reject()
+ FreeCAD.ActiveDocument.removeObject(self.obj.Name)
+ FreeCAD.ActiveDocument.commitTransaction()
+ FreeCAD.ActiveDocument.recompute()
+
+ def accept(self):
+ self.editor.accept()
+
+ FreeCAD.ActiveDocument.commitTransaction()
+ FreeCADGui.ActiveDocument.resetEdit()
+ FreeCADGui.Control.closeDialog()
+ FreeCAD.ActiveDocument.recompute()
+
+ def updateUI(self):
+ self.editor.updateUI()
+
+ def updateModel(self):
+ self.editor.updateTool()
+ FreeCAD.ActiveDocument.recompute()
+
+ def setupUi(self):
+ self.editor.setupUI()
+
+
+class ToolBitSelector(object):
+ ToolRole = QtCore.Qt.UserRole + 1
+
+ def __init__(self):
+ self.buttons = None
+ self.editor = None
+ self.dialog = None
+ self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitSelector.ui')
+ self.setupUI()
+
+ def updateTools(self, selected=None):
+ PathLog.track()
+ selItem = None
+ self.form.tools.setUpdatesEnabled(False)
+ if selected is None and self.form.tools.currentItem():
+ selected = self.form.tools.currentItem().text()
+ self.form.tools.clear()
+ for tool in sorted(self.loadedTools(), key=lambda t: t.Label):
+ icon = None
+ if tool.ViewObject and tool.ViewObject.Proxy:
+ icon = tool.ViewObject.Proxy.getIcon()
+ if icon and isinstance(icon, QtGui.QIcon):
+ item = QtGui.QListWidgetItem(icon, tool.Label)
+ else:
+ item = QtGui.QListWidgetItem(tool.Label)
+ item.setData(self.ToolRole, tool)
+ if selected == tool.Label:
+ selItem = item
+ self.form.tools.addItem(item)
+ if selItem:
+ self.form.tools.setCurrentItem(selItem)
+ self.updateSelection()
+ self.form.tools.setUpdatesEnabled(True)
+
+ def getTool(self):
+ PathLog.track()
+ self.updateTools()
+ res = self.form.exec_()
+ if 1 == res and self.form.tools.currentItem():
+ return self.form.tools.currentItem().data(self.ToolRole)
+ return None
+
+ def loadedTools(self):
+ PathLog.track()
+ if FreeCAD.ActiveDocument:
+ return [o for o in FreeCAD.ActiveDocument.Objects if hasattr(o, 'Proxy') and isinstance(o.Proxy, PathToolBit.ToolBit)]
+ return []
+
+ def loadTool(self):
+ PathLog.track()
+ tool = LoadTool(self.form)
+ if tool:
+ self.updateTools(tool.Label)
+
+ def createTool(self):
+ PathLog.track()
+ tool = PathToolBit.Factory.Create()
+
+ def accept():
+ self.editor.accept()
+ self.dialog.done(1)
+ self.updateTools(tool.Label)
+
+ def reject():
+ FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Uncreate ToolBit'))
+ self.editor.reject()
+ self.dialog.done(0)
+ FreeCAD.ActiveDocument.removeObject(tool.Name)
+ FreeCAD.ActiveDocument.commitTransaction()
+
+ self.dialog = QtGui.QDialog(self.form)
+ layout = QtGui.QVBoxLayout(self.dialog)
+ self.editor = PathToolBitEdit.ToolBitEditor(tool, self.dialog)
+ self.editor.setupUI()
+ self.buttons = QtGui.QDialogButtonBox(
+ QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel,
+ QtCore.Qt.Horizontal, self.dialog)
+ layout.addWidget(self.buttons)
+ self.buttons.accepted.connect(accept)
+ self.buttons.rejected.connect(reject)
+ print(self.dialog.exec_())
+
+ def updateSelection(self):
+ PathLog.track()
+ if self.form.tools.selectedItems():
+ self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(True)
+ else:
+ self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(False)
+
+ def setupUI(self):
+ PathLog.track()
+ self.form.toolCreate.clicked.connect(self.createTool)
+ self.form.toolLoad.clicked.connect(self.loadTool)
+ self.form.tools.itemSelectionChanged.connect(self.updateSelection)
+ self.form.tools.doubleClicked.connect(self.form.accept)
+
+class ToolBitGuiFactory(PathToolBit.ToolBitFactory):
+
+ def Create(self, name='ToolBit', shapeFile=None):
+ '''Create(name = 'ToolBit') ... creates a new tool bit.
+ It is assumed the tool will be edited immediately so the internal bit body is still attached.'''
+ FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit'))
+ tool = PathToolBit.ToolBitFactory.Create(self, name, shapeFile)
+ PathIconViewProvider.Attach(tool.ViewObject, name)
+ FreeCAD.ActiveDocument.commitTransaction()
+ return tool
+
+def GetToolFile(parent = None):
+ if parent is None:
+ parent = QtGui.QApplication.activeWindow()
+ foo = QtGui.QFileDialog.getOpenFileName(parent, 'Tool', PathPreferences.lastPathToolBit(), '*.fctb')
+ if foo and foo[0]:
+ PathPreferences.setLastPathToolBit(os.path.dirname(foo[0]))
+ return foo[0]
+ return None
+
+def GetToolFiles(parent = None):
+ if parent is None:
+ parent = QtGui.QApplication.activeWindow()
+ foo = QtGui.QFileDialog.getOpenFileNames(parent, 'Tool', PathPreferences.lastPathToolBit(), '*.fctb')
+ if foo and foo[0]:
+ PathPreferences.setLastPathToolBit(os.path.dirname(foo[0][0]))
+ return foo[0]
+ return []
+
+
+def LoadTool(parent = None):
+ '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.'''
+ foo = GetToolFile(parent)
+ return PathToolBit.Factory.CreateFrom(foo) if foo else foo
+
+def LoadTools(parent = None):
+ '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.'''
+ return [PathToolBit.Factory.CreateFrom(foo) for foo in GetToolFiles(parent)]
+
+# Set the factory so all tools are created with UI
+PathToolBit.Factory = ToolBitGuiFactory()
+
+PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider)
diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py b/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py
new file mode 100644
index 0000000000..ae5573013b
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2019 sliptonic *
+# * *
+# * 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 FreeCADGui
+import PySide.QtCore as QtCore
+
+class CommandToolBitLibraryOpen:
+ '''
+ Command to ToolBitLibrary editor.
+ '''
+
+ def __init__(self):
+ pass
+
+ def GetResources(self):
+ return {'Pixmap': 'Path-ToolTable',
+ 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Open ToolBit Library editor"),
+ 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Open an editor to manage ToolBit libraries")}
+
+ def IsActive(self):
+ return True
+
+ def Activated(self):
+ import PathScripts.PathToolBitLibraryGui as PathToolBitLibraryGui
+ library = PathToolBitLibraryGui.ToolBitLibrary()
+ library.open()
+
+class CommandToolBitLibraryLoad:
+ '''
+ Command used to load an entire ToolBitLibrary (or part of it) from a file into a job.
+ '''
+
+ def __init__(self):
+ pass
+
+ def GetResources(self):
+ return {'Pixmap': 'Path-ToolTable',
+ 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Load ToolBit Library"),
+ 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Load an entire ToolBit library or part of it into a job")}
+
+ def selectedJob(self):
+ if FreeCAD.ActiveDocument:
+ sel = FreeCADGui.Selection.getSelectionEx()
+ if sel and sel[0].Object.Name[:3] == 'Job':
+ return sel[0].Object
+ jobs = [o for o in FreeCAD.ActiveDocument.Objects if o.Name[:3] == 'Job']
+ if 1 == len(jobs):
+ return jobs[0]
+ return None
+
+ def IsActive(self):
+ return not self.selectedJob() is None
+
+ def Activated(self):
+ job = self.selectedJob()
+ self.Execute(job)
+
+ @classmethod
+ def Execute(cls, job):
+ import PathScripts.PathToolBitLibraryGui as PathToolBitLibraryGui
+ import PathScripts.PathToolControllerGui as PathToolControllerGui
+
+ library = PathToolBitLibraryGui.ToolBitLibrary()
+ if 1 == library.open(dialog=True) and job:
+ for nr, tool in library.selectedOrAllTools():
+ tc = PathToolControllerGui.Create("TC: {}".format(tool.Label), tool, nr)
+ job.Proxy.addToolController(tc)
+ FreeCAD.ActiveDocument.recompute()
+ return True
+ return False
+
+if FreeCAD.GuiUp:
+ FreeCADGui.addCommand('Path_ToolBitLibraryOpen', CommandToolBitLibraryOpen())
+ FreeCADGui.addCommand('Path_ToolBitLibraryLoad', CommandToolBitLibraryLoad())
+
+CommandList = ['Path_ToolBitLibraryOpen', 'Path_ToolBitLibraryLoad']
+
+FreeCAD.Console.PrintLog("Loading PathToolBitLibraryCmd... done\n")
diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py
new file mode 100644
index 0000000000..17b12638a2
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py
@@ -0,0 +1,306 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2019 sliptonic *
+# * *
+# * 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 FreeCADGui
+import PathScripts.PathLog as PathLog
+import PathScripts.PathPreferences as PathPreferences
+import PathScripts.PathToolBit as PathToolBit
+import PathScripts.PathToolBitGui as PathToolBitGui
+import PySide
+import json
+import os
+import traceback
+import uuid as UUID
+
+#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
+#PathLog.trackModule(PathLog.thisModule())
+
+_UuidRole = PySide.QtCore.Qt.UserRole + 1
+_PathRole = PySide.QtCore.Qt.UserRole + 2
+
+class _TableView(PySide.QtGui.QTableView):
+ '''Subclass of QTableView to support rearrange and copying of ToolBits'''
+
+ def __init__(self, parent):
+ PySide.QtGui.QTableView.__init__(self, parent)
+ self.setDragEnabled(True)
+ self.setAcceptDrops(True)
+ self.setDropIndicatorShown(True)
+ self.setDragDropMode(PySide.QtGui.QAbstractItemView.InternalMove)
+ self.setDefaultDropAction(PySide.QtCore.Qt.MoveAction)
+ self.setSortingEnabled(True)
+ self.setSelectionBehavior(PySide.QtGui.QAbstractItemView.SelectRows)
+ self.verticalHeader().hide()
+
+ def supportedDropActions(self):
+ return [PySide.QtCore.Qt.CopyAction, PySide.QtCore.Qt.MoveAction]
+
+ def _uuidOfRow(self, row):
+ model = self.model()
+ return model.data(model.index(row, 0), _UuidRole)
+
+ def _rowWithUuid(self, uuid):
+ model = self.model()
+ for row in range(model.rowCount()):
+ if self._uuidOfRow(row) == uuid:
+ return row
+ return None
+
+ def _copyTool(self, uuid_, dstRow):
+ model = self.model()
+ model.insertRow(dstRow)
+ srcRow = self._rowWithUuid(uuid_)
+ for col in range(model.columnCount()):
+ srcItem = model.item(srcRow, col)
+
+ model.setData(model.index(dstRow, col), srcItem.data(PySide.QtCore.Qt.EditRole), PySide.QtCore.Qt.EditRole)
+ if col == 0:
+ model.setData(model.index(dstRow, col), srcItem.data(_PathRole), _PathRole)
+ # Even a clone of a tool gets its own uuid so it can be identified when
+ # rearranging the order or inserting/deleting rows
+ model.setData(model.index(dstRow, col), UUID.uuid4(), _UuidRole)
+ else:
+ model.item(dstRow, col).setEditable(False)
+
+ def _copyTools(self, uuids, dst):
+ for i, uuid in enumerate(uuids):
+ self._copyTool(uuid, dst + i)
+
+ def dropEvent(self, event):
+ PathLog.track()
+ mime = event.mimeData()
+ data = mime.data('application/x-qstandarditemmodeldatalist')
+ stream = PySide.QtCore.QDataStream(data)
+ srcRows = []
+ while not stream.atEnd():
+ # pylint: disable=unused-variable
+ row = stream.readInt32()
+ srcRows.append(row)
+ col = stream.readInt32()
+ #PathLog.track(row, col)
+ cnt = stream.readInt32()
+ for i in range(cnt):
+ key = stream.readInt32()
+ val = stream.readQVariant()
+ #PathLog.track(' ', i, key, val, type(val))
+ # I have no idea what these three integers are,
+ # or if they even are three integers,
+ # but it seems to work out this way.
+ i0 = stream.readInt32()
+ i1 = stream.readInt32()
+ i2 = stream.readInt32()
+ #PathLog.track(' ', i0, i1, i2)
+
+ # get the uuids of all srcRows
+ model = self.model()
+ srcUuids = [self._uuidOfRow(row) for row in set(srcRows)]
+ destRow = self.rowAt(event.pos().y())
+
+ self._copyTools(srcUuids, destRow)
+ if PySide.QtCore.Qt.DropAction.MoveAction == event.proposedAction():
+ for uuid in srcUuids:
+ model.removeRow(self._rowWithUuid(uuid))
+
+class ToolBitLibrary(object):
+ '''ToolBitLibrary is the controller for displaying/selecting/creating/editing a collection of ToolBits.'''
+
+ def __init__(self, path=None):
+ self.path = path
+ self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitLibraryEdit.ui')
+ self.toolTableView = _TableView(self.form.toolTableGroup)
+ self.form.toolTableGroup.layout().replaceWidget(self.form.toolTable, self.toolTableView)
+ self.form.toolTable.hide()
+ self.setupUI()
+ self.title = self.form.windowTitle()
+ if path:
+ self.libraryLoad(path)
+
+ def _toolAdd(self, nr, tool, path):
+ toolNr = PySide.QtGui.QStandardItem()
+ toolNr.setData(nr, PySide.QtCore.Qt.EditRole)
+ toolNr.setData(path, _PathRole)
+ toolNr.setData(UUID.uuid4(), _UuidRole)
+
+ toolName = PySide.QtGui.QStandardItem()
+ toolName.setData(tool['name'], PySide.QtCore.Qt.EditRole)
+ toolName.setEditable(False)
+
+ toolShape = PySide.QtGui.QStandardItem()
+ toolShape.setData(os.path.splitext(os.path.basename(tool['shape']))[0], PySide.QtCore.Qt.EditRole)
+ toolShape.setEditable(False)
+
+ toolDiameter = PySide.QtGui.QStandardItem()
+ toolDiameter.setData(tool['parameter']['Diameter'], PySide.QtCore.Qt.EditRole)
+ toolDiameter.setEditable(False)
+
+ self.model.appendRow([toolNr, toolName, toolShape, toolDiameter])
+
+ def toolAdd(self):
+ PathLog.track()
+ # pylint: disable=broad-except
+ try:
+ nr = 0
+ for row in range(self.model.rowCount()):
+ itemNr = int(self.model.item(row, 0).data(PySide.QtCore.Qt.EditRole))
+ nr = max(nr, itemNr)
+ nr += 1
+
+ for i, foo in enumerate(PathToolBitGui.GetToolFiles(self.form)):
+ tool = PathToolBit.Declaration(foo)
+ self._toolAdd(nr + i, tool, foo)
+ self.toolTableView.resizeColumnsToContents()
+ except Exception:
+ PathLog.error('something happened')
+ PathLog.error(traceback.print_exc())
+
+ def selectedOrAllTools(self):
+ selectedRows = set([index.row() for index in self.toolTableView.selectedIndexes()])
+ if not selectedRows:
+ selectedRows = list(range(self.model.rowCount()))
+ tools = []
+ for row in selectedRows:
+ item = self.model.item(row, 0)
+ toolNr = int(item.data(PySide.QtCore.Qt.EditRole))
+ toolPath = item.data(_PathRole)
+ tools.append((toolNr, PathToolBit.Factory.CreateFrom(toolPath)))
+ return tools
+
+ def toolDelete(self):
+ PathLog.track()
+ selectedRows = set([index.row() for index in self.toolTableView.selectedIndexes()])
+ for row in sorted(list(selectedRows), key = lambda r: -r):
+ self.model.removeRows(row, 1)
+
+ def toolEnumerate(self):
+ PathLog.track()
+ for row in range(self.model.rowCount()):
+ self.model.setData(self.model.index(row, 0), row + 1, PySide.QtCore.Qt.EditRole)
+
+ def toolSelect(self, selected, deselected):
+ # pylint: disable=unused-argument
+ self.form.toolDelete.setEnabled(len(self.toolTableView.selectedIndexes()) > 0)
+
+ def open(self, path=None, dialog=False):
+ '''open(path=None, dialog=False) ... load library stored in path and bring up ui.
+ Returns 1 if user pressed OK, 0 otherwise.'''
+ if path:
+ fullPath = PathToolBit.findLibrary(path)
+ if fullPath:
+ self.libraryLoad(fullPath)
+ else:
+ self.libraryOpen()
+ elif dialog:
+ self.libraryOpen()
+ return self.form.exec_()
+
+ def updateToolbar(self):
+ if self.path:
+ self.form.librarySave.setEnabled(True)
+ else:
+ self.form.librarySave.setEnabled(False)
+
+ def libraryOpen(self):
+ PathLog.track()
+ foo = PySide.QtGui.QFileDialog.getOpenFileName(self.form, 'Tool Library', PathPreferences.lastPathToolLibrary(), '*.fctl')
+ if foo and foo[0]:
+ path = foo[0]
+ PathPreferences.setLastPathToolLibrary(os.path.dirname(path))
+ self.libraryLoad(path)
+
+ def libraryLoad(self, path):
+ self.toolTableView.setUpdatesEnabled(False)
+ self.model.clear()
+ self.model.setHorizontalHeaderLabels(self.columnNames())
+ if path:
+ with open(path) as fp:
+ library = json.load(fp)
+ for toolBit in library['tools']:
+ nr = toolBit['nr']
+ bit = PathToolBit.findBit(toolBit['path'])
+ if bit:
+ PathLog.track(bit)
+ tool = PathToolBit.Declaration(bit)
+ self._toolAdd(nr, tool, bit)
+ else:
+ PathLog.error("Could not find tool #{}: {}".format(nr, library['tools'][nr]))
+ self.toolTableView.resizeColumnsToContents()
+ self.toolTableView.setUpdatesEnabled(True)
+
+ self.form.setWindowTitle("{} - {}".format(self.title, os.path.basename(path) if path else ''))
+ self.path = path
+ self.updateToolbar()
+
+ def libraryNew(self):
+ self.libraryLoad(None)
+
+ def librarySave(self):
+ library = {}
+ tools = []
+ library['version'] = 1
+ library['tools'] = tools
+ for row in range(self.model.rowCount()):
+ toolNr = self.model.data(self.model.index(row, 0), PySide.QtCore.Qt.EditRole)
+ toolPath = self.model.data(self.model.index(row, 0), _PathRole)
+ if PathPreferences.toolsStoreAbsolutePaths():
+ tools.append({'nr': toolNr, 'path': toolPath})
+ else:
+ tools.append({'nr': toolNr, 'path': PathToolBit.findRelativePathTool(toolPath)})
+
+ with open(self.path, 'w') as fp:
+ json.dump(library, fp, sort_keys=True, indent=2)
+
+ def librarySaveAs(self):
+ foo = PySide.QtGui.QFileDialog.getSaveFileName(self.form, 'Tool Library', PathPreferences.lastPathToolLibrary(), '*.fctl')
+ if foo and foo[0]:
+ path = foo[0] if foo[0].endswith('.fctl') else "{}.fctl".format(foo[0])
+ PathPreferences.setLastPathToolLibrary(os.path.dirname(path))
+ self.path = path
+ self.librarySave()
+ self.updateToolbar()
+
+ def columnNames(self):
+ return ['Nr', 'Tool', 'Shape', 'Diameter']
+
+ def setupUI(self):
+ PathLog.track('+')
+ self.model = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()), self.toolTableView)
+ self.model.setHorizontalHeaderLabels(self.columnNames())
+
+ self.toolTableView.setModel(self.model)
+ self.toolTableView.resizeColumnsToContents()
+ self.toolTableView.selectionModel().selectionChanged.connect(self.toolSelect)
+
+ self.form.toolAdd.clicked.connect(self.toolAdd)
+ self.form.toolDelete.clicked.connect(self.toolDelete)
+ self.form.toolEnumerate.clicked.connect(self.toolEnumerate)
+
+ self.form.libraryNew.clicked.connect(self.libraryNew)
+ self.form.libraryOpen.clicked.connect(self.libraryOpen)
+ self.form.librarySave.clicked.connect(self.librarySave)
+ self.form.librarySaveAs.clicked.connect(self.librarySaveAs)
+
+ self.toolSelect([], [])
+ self.updateToolbar()
+ PathLog.track('-')
diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py
index a9ced44132..9ff19b9668 100644
--- a/src/Mod/Path/PathScripts/PathToolController.py
+++ b/src/Mod/Path/PathScripts/PathToolController.py
@@ -26,16 +26,13 @@
import FreeCAD
import Path
import PathScripts.PathLog as PathLog
+import PathScripts.PathPreferences as PathPreferences
+import PathScripts.PathToolBit as PathToolBit
from PySide import QtCore
-LOGLEVEL = False
-
-if LOGLEVEL:
- PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
- PathLog.trackModule(PathLog.thisModule())
-else:
- PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
+#PathLog.trackModule(PathLog.thisModule())
# Qt translation handling
def translate(context, text, disambig=None):
@@ -61,54 +58,74 @@ class ToolControllerTemplate:
VertRapid = 'vrapid'
class ToolController:
- def __init__(self, obj, tool=1):
- PathLog.track('tool: {}'.format(tool))
- obj.addProperty("App::PropertyIntegerConstraint", "ToolNumber", "Tool", QtCore.QT_TRANSLATE_NOOP("App::Property", "The active tool"))
+ def __init__(self, obj, cTool=False):
+ PathLog.track('tool: {}'.format(cTool))
+
+ obj.addProperty("App::PropertyIntegerConstraint", "ToolNumber", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The active tool"))
obj.ToolNumber = (0, 0, 10000, 1)
- obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("App::Property", "The tool used by this controller"))
-
- obj.addProperty("App::PropertyFloat", "SpindleSpeed", "Tool", QtCore.QT_TRANSLATE_NOOP("App::Property", "The speed of the cutting spindle in RPM"))
- obj.addProperty("App::PropertyEnumeration", "SpindleDir", "Tool", QtCore.QT_TRANSLATE_NOOP("App::Property", "Direction of spindle rotation"))
+ self.ensureUseLegacyTool(obj, cTool)
+ obj.addProperty("App::PropertyFloat", "SpindleSpeed", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The speed of the cutting spindle in RPM"))
+ obj.addProperty("App::PropertyEnumeration", "SpindleDir", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Direction of spindle rotation"))
obj.SpindleDir = ['Forward', 'Reverse']
- obj.addProperty("App::PropertySpeed", "VertFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("App::Property", "Feed rate for vertical moves in Z"))
- obj.addProperty("App::PropertySpeed", "HorizFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("App::Property", "Feed rate for horizontal moves"))
- obj.addProperty("App::PropertySpeed", "VertRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("App::Property", "Rapid rate for vertical moves in Z"))
- obj.addProperty("App::PropertySpeed", "HorizRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("App::Property", "Rapid rate for horizontal moves"))
- obj.Proxy = self
+ obj.addProperty("App::PropertySpeed", "VertFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Feed rate for vertical moves in Z"))
+ obj.addProperty("App::PropertySpeed", "HorizFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Feed rate for horizontal moves"))
+ obj.addProperty("App::PropertySpeed", "VertRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Rapid rate for vertical moves in Z"))
+ obj.addProperty("App::PropertySpeed", "HorizRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Rapid rate for horizontal moves"))
obj.setEditorMode('Placement', 2)
def onDocumentRestored(self, obj):
obj.setEditorMode('Placement', 2)
+ def onDelete(self, obj, arg2=None):
+ # pylint: disable=unused-argument
+ if not self.usesLegacyTool(obj):
+ if len(obj.Tool.InList) == 1:
+ if hasattr(obj.Tool.Proxy, 'onDelete'):
+ obj.Tool.Proxy.onDelete(obj.Tool)
+ obj.Document.removeObject(obj.Tool.Name)
+
def setFromTemplate(self, obj, template):
'''setFromTemplate(obj, xmlItem) ... extract properties from xmlItem and assign to receiver.'''
PathLog.track(obj.Name, template)
- if template.get(ToolControllerTemplate.Version) and 1 == int(template.get(ToolControllerTemplate.Version)):
- if template.get(ToolControllerTemplate.Label):
- obj.Label = template.get(ToolControllerTemplate.Label)
- if template.get(ToolControllerTemplate.VertFeed):
- obj.VertFeed = template.get(ToolControllerTemplate.VertFeed)
- if template.get(ToolControllerTemplate.HorizFeed):
- obj.HorizFeed = template.get(ToolControllerTemplate.HorizFeed)
- if template.get(ToolControllerTemplate.VertRapid):
- obj.VertRapid = template.get(ToolControllerTemplate.VertRapid)
- if template.get(ToolControllerTemplate.HorizRapid):
- obj.HorizRapid = template.get(ToolControllerTemplate.HorizRapid)
- if template.get(ToolControllerTemplate.SpindleSpeed):
- obj.SpindleSpeed = float(template.get(ToolControllerTemplate.SpindleSpeed))
- if template.get(ToolControllerTemplate.SpindleDir):
- obj.SpindleDir = template.get(ToolControllerTemplate.SpindleDir)
- if template.get(ToolControllerTemplate.ToolNumber):
- obj.ToolNumber = int(template.get(ToolControllerTemplate.ToolNumber))
- if template.get(ToolControllerTemplate.Tool):
- obj.Tool.setFromTemplate(template.get(ToolControllerTemplate.Tool))
- if template.get(ToolControllerTemplate.Expressions):
- for exprDef in template.get(ToolControllerTemplate.Expressions):
- if exprDef[ToolControllerTemplate.ExprExpr]:
- obj.setExpression(exprDef[ToolControllerTemplate.ExprProp], exprDef[ToolControllerTemplate.ExprExpr])
+ version = 0
+ if template.get(ToolControllerTemplate.Version):
+ version = int(template.get(ToolControllerTemplate.Version))
+ if version == 1 or version == 2:
+ if template.get(ToolControllerTemplate.Label):
+ obj.Label = template.get(ToolControllerTemplate.Label)
+ if template.get(ToolControllerTemplate.VertFeed):
+ obj.VertFeed = template.get(ToolControllerTemplate.VertFeed)
+ if template.get(ToolControllerTemplate.HorizFeed):
+ obj.HorizFeed = template.get(ToolControllerTemplate.HorizFeed)
+ if template.get(ToolControllerTemplate.VertRapid):
+ obj.VertRapid = template.get(ToolControllerTemplate.VertRapid)
+ if template.get(ToolControllerTemplate.HorizRapid):
+ obj.HorizRapid = template.get(ToolControllerTemplate.HorizRapid)
+ if template.get(ToolControllerTemplate.SpindleSpeed):
+ obj.SpindleSpeed = float(template.get(ToolControllerTemplate.SpindleSpeed))
+ if template.get(ToolControllerTemplate.SpindleDir):
+ obj.SpindleDir = template.get(ToolControllerTemplate.SpindleDir)
+ if template.get(ToolControllerTemplate.ToolNumber):
+ obj.ToolNumber = int(template.get(ToolControllerTemplate.ToolNumber))
+ if template.get(ToolControllerTemplate.Tool):
+ toolVersion = template.get(ToolControllerTemplate.Tool).get(ToolControllerTemplate.Version)
+ if toolVersion == 1:
+ self.ensureUseLegacyTool(obj, True)
+ obj.Tool.setFromTemplate(template.get(ToolControllerTemplate.Tool))
+ else:
+ self.ensureUseLegacyTool(obj, False)
+ obj.Tool = PathToolBit.Factory.CreateFromAttrs(template.get(ToolControllerTemplate.Tool))
+ if obj.Tool and obj.Tool.ViewObject and obj.Tool.ViewObject.Visibility:
+ obj.Tool.ViewObject.Visibility = False
+ if template.get(ToolControllerTemplate.Expressions):
+ for exprDef in template.get(ToolControllerTemplate.Expressions):
+ if exprDef[ToolControllerTemplate.ExprExpr]:
+ obj.setExpression(exprDef[ToolControllerTemplate.ExprProp], exprDef[ToolControllerTemplate.ExprExpr])
+ else:
+ PathLog.error(translate('PathToolController', "Unsupported PathToolController template version %s") % template.get(ToolControllerTemplate.Version))
else:
- PathLog.error(translate('PathToolController', "Unsupported PathToolController template version %s") % template.get(ToolControllerTemplate.Version))
+ PathLog.error(translate('PathToolController', 'PathToolController template has no version - corrupted template file?'))
def templateAttrs(self, obj):
'''templateAttrs(obj) ... answer a dictionary with all properties that should be stored for a template.'''
@@ -123,7 +140,10 @@ class ToolController:
attrs[ToolControllerTemplate.HorizRapid] = ("%s" % (obj.HorizRapid))
attrs[ToolControllerTemplate.SpindleSpeed] = obj.SpindleSpeed
attrs[ToolControllerTemplate.SpindleDir] = obj.SpindleDir
- attrs[ToolControllerTemplate.Tool] = obj.Tool.templateAttrs()
+ if self.usesLegacyTool(obj):
+ attrs[ToolControllerTemplate.Tool] = obj.Tool.templateAttrs()
+ else:
+ attrs[ToolControllerTemplate.Tool] = obj.Tool.Proxy.templateAttrs(obj.Tool)
expressions = []
for expr in obj.ExpressionEngine:
PathLog.debug('%s: %s' % (expr[0], expr[1]))
@@ -157,39 +177,61 @@ class ToolController:
PathLog.track()
return obj.Tool
+ def usesLegacyTool(self, obj):
+ '''returns True if the tool being controlled is a legacy tool'''
+ return isinstance(obj.Tool, Path.Tool)
+ def ensureUseLegacyTool(self, obj, legacy):
+ if not hasattr(obj, 'Tool') or (legacy != self.usesLegacyTool(obj)):
+ if legacy and hasattr(obj, 'Tool') and len(obj.Tool.InList) == 1:
+ if hasattr(obj.Tool.Proxy, 'onDelete'):
+ obj.Tool.Proxy.onDelete(obj.Tool)
+ obj.Document.removeObject(obj.Tool.Name)
+
+ if hasattr(obj, 'Tool'):
+ obj.removeProperty('Tool')
+
+ if legacy:
+ obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller"))
+ else:
+ obj.addProperty("App::PropertyLink", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller"))
def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=True):
- PathLog.track(tool, toolNumber)
+ legacyTool = PathPreferences.toolsReallyUseLegacyTools() if tool is None else isinstance(tool, Path.Tool)
+
+ PathLog.track(tool, toolNumber, legacyTool)
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
obj.Label = name
+ obj.Proxy = ToolController(obj, legacyTool)
- ToolController(obj)
if FreeCAD.GuiUp and assignViewProvider:
ViewProvider(obj.ViewObject)
if tool is None:
- tool = Path.Tool()
- tool.Diameter = 5.0
- tool.Name = "Default Tool"
- tool.CuttingEdgeHeight = 15.0
- tool.ToolType = "EndMill"
- tool.Material = "HighSpeedSteel"
+ if legacyTool:
+ tool = Path.Tool()
+ tool.Diameter = 5.0
+ tool.Name = "Default Tool"
+ tool.CuttingEdgeHeight = 15.0
+ tool.ToolType = "EndMill"
+ tool.Material = "HighSpeedSteel"
+ else:
+ tool = PathToolBit.Factory.Create()
+ if tool.ViewObject:
+ tool.ViewObject.Visibility = False
+
obj.Tool = tool
obj.ToolNumber = toolNumber
return obj
def FromTemplate(template, assignViewProvider=True):
+ # pylint: disable=unused-argument
PathLog.track()
name = template.get(ToolControllerTemplate.Name, ToolControllerTemplate.Label)
- obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
- tc = ToolController(obj)
- if FreeCAD.GuiUp and assignViewProvider:
- ViewProvider(obj.ViewObject)
-
- tc.setFromTemplate(obj, template)
+ obj = Create(name, assignViewProvider=True)
+ obj.Proxy.setFromTemplate(obj, template)
return obj
diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py
index 17821d500b..3a05fbe301 100644
--- a/src/Mod/Path/PathScripts/PathToolControllerGui.py
+++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py
@@ -28,6 +28,7 @@ import Part
import PathScripts
import PathScripts.PathGui as PathGui
import PathScripts.PathLog as PathLog
+import PathScripts.PathToolBitGui as PathToolBitGui
import PathScripts.PathToolEdit as PathToolEdit
import PathScripts.PathUtil as PathUtil
@@ -78,6 +79,7 @@ class ViewProvider:
def onDelete(self, vobj, args=None):
# pylint: disable=unused-argument
PathUtil.clearExpressionEngine(vobj.Object)
+ self.vobj.Object.Proxy.onDelete(vobj.Object, args)
return True
def updateData(self, vobj, prop):
@@ -113,11 +115,21 @@ class ViewProvider:
action.triggered.connect(self.setEdit)
menu.addAction(action)
+ def claimChildren(self):
+ obj = self.vobj.Object
+ if obj and obj.Proxy and not obj.Proxy.usesLegacyTool(obj):
+ return [obj.Tool]
+ return []
+
def Create(name = 'Default Tool', tool=None, toolNumber=1):
PathLog.track(tool, toolNumber)
obj = PathScripts.PathToolController.Create(name, tool, toolNumber)
ViewProvider(obj.ViewObject)
+ if not obj.Proxy.usesLegacyTool(obj):
+ # ToolBits are visible by default, which is typically not what the user wants
+ if tool and tool.ViewObject and tool.ViewObject.Visibility:
+ tool.ViewObject.Visibility = False
return obj
@@ -129,16 +141,35 @@ class CommandPathToolController(object):
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_ToolController", "Add Tool Controller to the Job"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_ToolController", "Add Tool Controller")}
+ def selectedJob(self):
+ if FreeCAD.ActiveDocument:
+ sel = FreeCADGui.Selection.getSelectionEx()
+ if sel and sel[0].Object.Name[:3] == 'Job':
+ return sel[0].Object
+ jobs = [o for o in FreeCAD.ActiveDocument.Objects if o.Name[:3] == 'Job']
+ if 1 == len(jobs):
+ return jobs[0]
+ return None
+
def IsActive(self):
- if FreeCAD.ActiveDocument is not None:
- for o in FreeCAD.ActiveDocument.Objects:
- if o.Name[:3] == "Job":
- return True
- return False
+ return self.selectedJob() is not None
def Activated(self):
PathLog.track()
- Create()
+ job = self.selectedJob()
+ if job:
+ tool = PathToolBitGui.ToolBitSelector().getTool()
+ if tool:
+ toolNr = None
+ for tc in job.ToolController:
+ if tc.Tool == tool:
+ toolNr = tc.ToolNumber
+ break
+ if not toolNr:
+ toolNr = max([tc.ToolNumber for tc in job.ToolController]) + 1
+ tc = Create("TC: {}".format(tool.Label), tool, toolNr)
+ job.Proxy.addToolController(tc)
+ FreeCAD.ActiveDocument.recompute()
class ToolControllerEditor(object):
@@ -153,7 +184,12 @@ class ToolControllerEditor(object):
self.vertRapid = PathGui.QuantitySpinBox(self.form.vertRapid, obj, 'VertRapid')
self.horizRapid = PathGui.QuantitySpinBox(self.form.horizRapid, obj, 'HorizRapid')
- self.editor = PathToolEdit.ToolEditor(obj.Tool, self.form.toolEditor)
+ if obj.Proxy.usesLegacyTool(obj):
+ self.editor = PathToolEdit.ToolEditor(obj.Tool, self.form.toolEditor)
+ else:
+ self.editor = None
+ self.form.toolBox.widget(1).hide()
+ self.form.toolBox.removeItem(1)
def updateUi(self):
tc = self.obj
@@ -168,7 +204,8 @@ class ToolControllerEditor(object):
if index >= 0:
self.form.spindleDirection.setCurrentIndex(index)
- self.editor.updateUI()
+ if self.editor:
+ self.editor.updateUI()
def updateToolController(self):
tc = self.obj
@@ -182,8 +219,9 @@ class ToolControllerEditor(object):
tc.SpindleSpeed = self.form.spindleSpeed.value()
tc.SpindleDir = self.form.spindleDirection.currentText()
- self.editor.updateTool()
- tc.Tool = self.editor.tool
+ if self.editor:
+ self.editor.updateTool()
+ tc.Tool = self.editor.tool
except Exception as e: # pylint: disable=broad-except
PathLog.error(translate("PathToolController", "Error updating TC: %s") % e)
@@ -196,7 +234,8 @@ class ToolControllerEditor(object):
self.form.blockSignals(False)
def setupUi(self):
- self.editor.setupUI()
+ if self.editor:
+ self.editor.setupUI()
self.form.tcName.editingFinished.connect(self.refresh)
self.form.horizFeed.editingFinished.connect(self.refresh)
@@ -219,13 +258,13 @@ class TaskPanel:
FreeCADGui.ActiveDocument.resetEdit()
FreeCADGui.Control.closeDialog()
- if self.toolrep is not None:
+ if self.toolrep:
FreeCAD.ActiveDocument.removeObject(self.toolrep.Name)
FreeCAD.ActiveDocument.recompute()
def reject(self):
FreeCADGui.Control.closeDialog()
- if self.toolrep is not None:
+ if self.toolrep:
FreeCAD.ActiveDocument.removeObject(self.toolrep.Name)
FreeCAD.ActiveDocument.recompute()
@@ -236,11 +275,12 @@ class TaskPanel:
def setFields(self):
self.editor.updateUi()
- tool = self.obj.Tool
- radius = tool.Diameter / 2
- length = tool.CuttingEdgeHeight
- t = Part.makeCylinder(radius, length)
- self.toolrep.Shape = t
+ if self.toolrep:
+ tool = self.obj.Tool
+ radius = float(tool.Diameter) / 2
+ length = tool.CuttingEdgeHeight
+ t = Part.makeCylinder(radius, length)
+ self.toolrep.Shape = t
def edit(self, item, column):
# pylint: disable=unused-argument
@@ -253,9 +293,10 @@ class TaskPanel:
FreeCAD.ActiveDocument.recompute()
def setupUi(self):
- t = Part.makeCylinder(1, 1)
- self.toolrep = FreeCAD.ActiveDocument.addObject("Part::Feature", "tool")
- self.toolrep.Shape = t
+ if self.editor.editor:
+ t = Part.makeCylinder(1, 1)
+ self.toolrep = FreeCAD.ActiveDocument.addObject("Part::Feature", "tool")
+ self.toolrep.Shape = t
self.setFields()
self.editor.setupUi()
diff --git a/src/Mod/Path/PathScripts/PathToolLibraryEditor.py b/src/Mod/Path/PathScripts/PathToolLibraryEditor.py
index 9e867f4ce6..bc51839658 100644
--- a/src/Mod/Path/PathScripts/PathToolLibraryEditor.py
+++ b/src/Mod/Path/PathScripts/PathToolLibraryEditor.py
@@ -29,6 +29,8 @@ import FreeCADGui
import Path
import PathScripts
import PathScripts.PathLog as PathLog
+import PathScripts.PathPreferences as PathPreferences
+import PathScripts.PathToolBitLibraryCmd as PathToolBitLibraryCmd
import PathScripts.PathToolEdit as PathToolEdit
import PathScripts.PathUtils as PathUtils
import PathScripts.PathToolLibraryManager as ToolLibraryManager
@@ -439,12 +441,14 @@ class CommandToolLibraryEdit():
pass
def edit(self, job=None, cb=None):
- editor = EditorPanel(job, cb)
- editor.setupUi()
-
- r = editor.form.exec_()
- if r:
- pass
+ if PathPreferences.toolsReallyUseLegacyTools():
+ editor = EditorPanel(job, cb)
+ editor.setupUi()
+ editor.form.exec_()
+ else:
+ if PathToolBitLibraryCmd.CommandToolBitLibraryLoad.Execute(job):
+ if cb:
+ cb()
def GetResources(self):
return {'Pixmap' : 'Path-ToolTable',
@@ -456,7 +460,6 @@ class CommandToolLibraryEdit():
return not FreeCAD.ActiveDocument is None
def Activated(self):
-
self.edit()
if FreeCAD.GuiUp:
diff --git a/src/Mod/Path/PathScripts/PathUtil.py b/src/Mod/Path/PathScripts/PathUtil.py
index 3fa449106c..2169d25840 100644
--- a/src/Mod/Path/PathScripts/PathUtil.py
+++ b/src/Mod/Path/PathScripts/PathUtil.py
@@ -34,14 +34,47 @@ other than PathLog, then it probably doesn't belong here.
import six
import PathScripts.PathLog as PathLog
+import PySide
-LOGLEVEL = False
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
-if LOGLEVEL:
- PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
- PathLog.trackModule(PathLog.thisModule())
-else:
- PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+def translate(context, text, disambig=None):
+ return PySide.QtCore.QCoreApplication.translate(context, text, disambig)
+
+def _getProperty(obj, prop):
+ o = obj
+ attr = obj
+ name = None
+ for name in prop.split('.'):
+ o = attr
+ if not hasattr(o, name):
+ break
+ attr = getattr(o, name)
+
+ if o == attr:
+ PathLog.warning(translate('PathGui', "%s has no property %s (%s))") % (obj.Label, prop, name))
+ return (None, None, None)
+
+ #PathLog.debug("found property %s of %s (%s: %s)" % (prop, obj.Label, name, attr))
+ return(o, attr, name)
+
+def getProperty(obj, prop):
+ '''getProperty(obj, prop) ... answer obj's property defined by its canonical name.'''
+ o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
+ return attr
+
+def getPropertyValueString(obj, prop):
+ '''getPropertyValueString(obj, prop) ... answer a string represntation of an object's property's value.'''
+ attr = getProperty(obj, prop)
+ if hasattr(attr, 'UserString'):
+ return attr.UserString
+ return str(attr)
+
+def setProperty(obj, prop, value):
+ '''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.'''
+ o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
+ if o and name:
+ setattr(o, name, value)
# NotValidBaseTypeIds = ['Sketcher::SketchObject']
NotValidBaseTypeIds = []
@@ -53,6 +86,9 @@ def isValidBaseObject(obj):
# Can't link to anything inside a geo feature group anymore
PathLog.debug("%s is inside a geo feature group" % obj.Label)
return False
+ if hasattr(obj, 'BitBody') and hasattr(obj, 'BitShape'):
+ # ToolBit's are not valid base objects
+ return False
if obj.TypeId in NotValidBaseTypeIds:
PathLog.debug("%s is blacklisted (%s)" % (obj.Label, obj.TypeId))
return False
diff --git a/src/Mod/Path/PathScripts/PathUtils.py b/src/Mod/Path/PathScripts/PathUtils.py
index 3ea148ee12..49091585ab 100644
--- a/src/Mod/Path/PathScripts/PathUtils.py
+++ b/src/Mod/Path/PathScripts/PathUtils.py
@@ -703,14 +703,14 @@ def guessDepths(objshape, subs=None):
def drillTipLength(tool):
"""returns the length of the drillbit tip."""
- if tool.CuttingEdgeAngle == 180 or tool.CuttingEdgeAngle == 0.0 or tool.Diameter == 0.0:
+ if tool.CuttingEdgeAngle == 180 or tool.CuttingEdgeAngle == 0.0 or float(tool.Diameter) == 0.0:
return 0.0
else:
if tool.CuttingEdgeAngle <= 0 or tool.CuttingEdgeAngle >= 180:
PathLog.error(translate("Path", "Invalid Cutting Edge Angle %.2f, must be >0° and <=180°") % tool.CuttingEdgeAngle)
return 0.0
theta = math.radians(tool.CuttingEdgeAngle)
- length = (tool.Diameter / 2) / math.tan(theta / 2)
+ length = (float(tool.Diameter) / 2) / math.tan(theta / 2)
if length < 0:
PathLog.error(translate("Path", "Cutting Edge Angle (%.2f) results in negative tool tip length") % tool.CuttingEdgeAngle)
return 0.0
diff --git a/src/Mod/Path/PathTests/TestPathHelix.py b/src/Mod/Path/PathTests/TestPathHelix.py
index de6c5527a7..de417c325c 100644
--- a/src/Mod/Path/PathTests/TestPathHelix.py
+++ b/src/Mod/Path/PathTests/TestPathHelix.py
@@ -34,8 +34,10 @@ PathLog.trackModule(PathLog.thisModule())
class TestPathHelix(PathTestUtils.PathTestBase):
+ RotateBy = 45
def setUp(self):
+ self.clone = None
self.doc = FreeCAD.open(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_holes00.fcstd')
self.job = PathJob.Create('Job', [self.doc.Body])
@@ -68,7 +70,7 @@ class TestPathHelix(PathTestUtils.PathTestBase):
proxy = op.Proxy
model = self.job.Model.Group[0]
- for deg in range(5, 360, 5):
+ for deg in range(self.RotateBy, 360, self.RotateBy):
model.Placement.Rotation = FreeCAD.Rotation(deg, 0, 0)
for base in op.Base:
model = base[0]
@@ -81,7 +83,7 @@ class TestPathHelix(PathTestUtils.PathTestBase):
def test03(self):
'''Verify Helix generates proper holes for rotated base model'''
- for deg in range(5, 360, 5):
+ for deg in range(self.RotateBy, 360, self.RotateBy):
self.tearDown()
self.doc = FreeCAD.open(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_holes00.fcstd')
self.doc.Body.Placement.Rotation = FreeCAD.Rotation(deg, 0, 0)
@@ -102,7 +104,7 @@ class TestPathHelix(PathTestUtils.PathTestBase):
def test04(self):
'''Verify Helix generates proper holes for rotated clone base model'''
- for deg in range(5, 360, 5):
+ for deg in range(self.RotateBy, 360, self.RotateBy):
self.tearDown()
self.doc = FreeCAD.open(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_holes00.fcstd')
self.clone = Draft.clone(self.doc.Body)
diff --git a/src/Mod/Path/PathTests/TestPathPreferences.py b/src/Mod/Path/PathTests/TestPathPreferences.py
new file mode 100644
index 0000000000..3696473125
--- /dev/null
+++ b/src/Mod/Path/PathTests/TestPathPreferences.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2019 sliptonic *
+# * *
+# * 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 PathScripts.PathPreferences as PathPreferences
+import PathTests.PathTestUtils as PathTestUtils
+
+class TestPathPreferences(PathTestUtils.PathTestBase):
+
+ def test00(self):
+ '''There is at least one search path.'''
+
+ paths = PathPreferences.searchPaths()
+ self.assertGreater(len(paths), 0)
+
+ def test01(self):
+ '''PathScripts is part of the posts search path.'''
+ paths = PathPreferences.searchPathsPost()
+ self.assertEqual(len([p for p in paths if p.endswith('/PathScripts/')]), 1)
+
+ def test02(self):
+ '''PathScripts/post is part of the posts search path.'''
+ paths = PathPreferences.searchPathsPost()
+ self.assertEqual(len([p for p in paths if p.endswith('/PathScripts/post/')]), 1)
+
+ def test03(self):
+ '''Available post processors include linuxcnc, grbl and opensbp.'''
+ posts = PathPreferences.allAvailablePostProcessors()
+ self.assertTrue('linuxcnc' in posts)
+ self.assertTrue('grbl' in posts)
+ self.assertTrue('opensbp' in posts)
+
+
+ def test10(self):
+ '''Default paths for tools are resolved correctly'''
+
+ self.assertTrue(PathPreferences.pathDefaultToolsPath().endswith('/Path/Tools/'))
+ self.assertTrue(PathPreferences.pathDefaultToolsPath('Bit').endswith('/Path/Tools/Bit'))
+ self.assertTrue(PathPreferences.pathDefaultToolsPath('Library').endswith('/Path/Tools/Library'))
+ self.assertTrue(PathPreferences.pathDefaultToolsPath('Template').endswith('/Path/Tools/Template'))
diff --git a/src/Mod/Path/PathTests/TestPathToolBit.py b/src/Mod/Path/PathTests/TestPathToolBit.py
new file mode 100644
index 0000000000..f5aac40ea6
--- /dev/null
+++ b/src/Mod/Path/PathTests/TestPathToolBit.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2019 sliptonic *
+# * *
+# * 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 PathScripts.PathToolBit as PathToolBit
+import PathTests.PathTestUtils as PathTestUtils
+
+
+class TestPathToolBit(PathTestUtils.PathTestBase):
+
+ def test00(self):
+ '''Find a tool shapee from file name'''
+
+ path = PathToolBit.findShape('endmill.fcstd')
+ self.assertIsNot(path, None)
+ self.assertNotEqual(path, 'endmill.fcstd')
+
+ def test01(self):
+ '''Find a tool shapee from an invalid absolute path.'''
+
+ path = PathToolBit.findShape('/this/is/unlikely/a/valid/path/v-bit.fcstd')
+ self.assertIsNot(path, None)
+ self.assertNotEqual(path, '/this/is/unlikely/a/valid/path/v-bit.fcstd')
+
+
+ def test10(self):
+ '''find the relative path of a tool bit'''
+ shape = 'endmill.fcstd'
+ path = PathToolBit.findShape(shape)
+ self.assertIsNot(path, None)
+ self.assertGreater(len(path), len(shape))
+ rel = PathToolBit.findRelativePathShape(path)
+ self.assertEqual(rel, shape)
+
+ def test11(self):
+ '''store full path if relative path isn't found'''
+ path = '/this/is/unlikely/a/valid/path/v-bit.fcstd'
+ rel = PathToolBit.findRelativePathShape(path)
+ self.assertEqual(rel, path)
+
diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py
index 55fbc78fa1..b5ac4e215c 100644
--- a/src/Mod/Path/TestPathApp.py
+++ b/src/Mod/Path/TestPathApp.py
@@ -25,6 +25,7 @@
import TestApp
from PathTests.TestPathLog import TestPathLog
+from PathTests.TestPathPreferences import TestPathPreferences
from PathTests.TestPathCore import TestPathCore
#from PathTests.TestPathPost import PathPostTestCases
from PathTests.TestPathGeom import TestPathGeom
@@ -35,6 +36,7 @@ from PathTests.TestPathDressupHoldingTags import TestHoldingTags
from PathTests.TestPathDressupDogbone import TestDressupDogbone
from PathTests.TestPathStock import TestPathStock
from PathTests.TestPathTool import TestPathTool
+from PathTests.TestPathToolBit import TestPathToolBit
from PathTests.TestPathTooltable import TestPathTooltable
from PathTests.TestPathToolController import TestPathToolController
from PathTests.TestPathSetupSheet import TestPathSetupSheet
@@ -58,4 +60,6 @@ False if TestPathToolController.__name__ else True
False if TestPathSetupSheet.__name__ else True
False if TestPathDeburr.__name__ else True
False if TestPathHelix.__name__ else True
+False if TestPathPreferences.__name__ else True
+False if TestPathToolBit.__name__ else True
diff --git a/src/Mod/Path/Tools/.gitignore b/src/Mod/Path/Tools/.gitignore
new file mode 100644
index 0000000000..334e20e2c9
--- /dev/null
+++ b/src/Mod/Path/Tools/.gitignore
@@ -0,0 +1,2 @@
+*.fcstd1
+*.FCStd1
diff --git a/src/Mod/Path/Tools/Bit/t1.fctb b/src/Mod/Path/Tools/Bit/t1.fctb
new file mode 100644
index 0000000000..9221229563
--- /dev/null
+++ b/src/Mod/Path/Tools/Bit/t1.fctb
@@ -0,0 +1,12 @@
+{
+ "version": 2,
+ "name": "T1",
+ "shape": "endmill.fcstd",
+ "attribute": {},
+ "parameter": {
+ "CuttingEdgeHeight": "30.000 mm",
+ "Diameter": "1.000 mm",
+ "Length": "50.000 mm",
+ "ShankDiameter": "3.000 mm"
+ }
+}
diff --git a/src/Mod/Path/Tools/Bit/t2.fctb b/src/Mod/Path/Tools/Bit/t2.fctb
new file mode 100644
index 0000000000..1c70485e5c
--- /dev/null
+++ b/src/Mod/Path/Tools/Bit/t2.fctb
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "name": "T2",
+ "shape": "endmill.fcstd",
+ "attribute": {},
+ "parameter": {
+ "CuttingEdgeHeight": "30.000 mm",
+ "Diameter": "2.000 mm",
+ "Length": "50.000 mm",
+ "ShankDiameter": "3.000 mm"
+ }
+}
diff --git a/src/Mod/Path/Tools/Bit/t3.fctb b/src/Mod/Path/Tools/Bit/t3.fctb
new file mode 100644
index 0000000000..86e6bf1110
--- /dev/null
+++ b/src/Mod/Path/Tools/Bit/t3.fctb
@@ -0,0 +1,12 @@
+{
+ "version": 2,
+ "name": "T3",
+ "shape": "endmill.fcstd",
+ "attribute": {},
+ "parameter": {
+ "CuttingEdgeHeight": "30.000 mm",
+ "Diameter": "3.000 mm",
+ "Length": "50.000 mm",
+ "ShankDiameter": "3.000 mm"
+ }
+}
diff --git a/src/Mod/Path/Tools/Bit/t4.fctb b/src/Mod/Path/Tools/Bit/t4.fctb
new file mode 100644
index 0000000000..c97b20feed
--- /dev/null
+++ b/src/Mod/Path/Tools/Bit/t4.fctb
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "name": "T4",
+ "shape": "endmill.fcstd",
+ "attribute": {},
+ "parameter": {
+ "CuttingEdgeHeight": "30.000 mm",
+ "Diameter": "4.000 mm",
+ "Length": "50.000 mm",
+ "ShankDiameter": "3.000 mm"
+ }
+}
diff --git a/src/Mod/Path/Tools/Bit/t5.fctb b/src/Mod/Path/Tools/Bit/t5.fctb
new file mode 100644
index 0000000000..014ebea50c
--- /dev/null
+++ b/src/Mod/Path/Tools/Bit/t5.fctb
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "name": "T5",
+ "shape": "endmill.fcstd",
+ "attribute": {},
+ "parameter": {
+ "CuttingEdgeHeight": "30.000 mm",
+ "Diameter": "5.000 mm",
+ "Length": "50.000 mm",
+ "ShankDiameter": "3.000 mm"
+ }
+}
diff --git a/src/Mod/Path/Tools/Bit/t6.fctb b/src/Mod/Path/Tools/Bit/t6.fctb
new file mode 100644
index 0000000000..521b489554
--- /dev/null
+++ b/src/Mod/Path/Tools/Bit/t6.fctb
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "name": "T6",
+ "shape": "endmill.fcstd",
+ "attribute": {},
+ "parameter": {
+ "CuttingEdgeHeight": "30.000 mm",
+ "Diameter": "6.000 mm",
+ "Length": "50.000 mm",
+ "ShankDiameter": "3.000 mm"
+ }
+}
diff --git a/src/Mod/Path/Tools/Bit/t7.fctb b/src/Mod/Path/Tools/Bit/t7.fctb
new file mode 100644
index 0000000000..b10067d4aa
--- /dev/null
+++ b/src/Mod/Path/Tools/Bit/t7.fctb
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "name": "T7",
+ "shape": "endmill.fcstd",
+ "attribute": {},
+ "parameter": {
+ "CuttingEdgeHeight": "30.000 mm",
+ "Diameter": "7.000 mm",
+ "Length": "50.000 mm",
+ "ShankDiameter": "3.000 mm"
+ }
+}
diff --git a/src/Mod/Path/Tools/Bit/t8.fctb b/src/Mod/Path/Tools/Bit/t8.fctb
new file mode 100644
index 0000000000..2ad54eb330
--- /dev/null
+++ b/src/Mod/Path/Tools/Bit/t8.fctb
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "name": "T8",
+ "shape": "endmill.fcstd",
+ "attribute": {},
+ "parameter": {
+ "CuttingEdgeHeight": "30.000 mm",
+ "Diameter": "8.000 mm",
+ "Length": "50.000 mm",
+ "ShankDiameter": "3.000 mm"
+ }
+}
diff --git a/src/Mod/Path/Tools/Bit/t9.fctb b/src/Mod/Path/Tools/Bit/t9.fctb
new file mode 100644
index 0000000000..3a3dbc3f78
--- /dev/null
+++ b/src/Mod/Path/Tools/Bit/t9.fctb
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "name": "T9",
+ "shape": "endmill.fcstd",
+ "attribute": {},
+ "parameter": {
+ "CuttingEdgeHeight": "30.000 mm",
+ "Diameter": "9.000 mm",
+ "Length": "50.000 mm",
+ "ShankDiameter": "3.000 mm"
+ }
+}
diff --git a/src/Mod/Path/Tools/Library/endmills.fctl b/src/Mod/Path/Tools/Library/endmills.fctl
new file mode 100644
index 0000000000..c443e6cd10
--- /dev/null
+++ b/src/Mod/Path/Tools/Library/endmills.fctl
@@ -0,0 +1,41 @@
+{
+ "tools": [
+ {
+ "nr": 1,
+ "path": "t1.fctb"
+ },
+ {
+ "nr": 2,
+ "path": "t2.fctb"
+ },
+ {
+ "nr": 3,
+ "path": "t3.fctb"
+ },
+ {
+ "nr": 4,
+ "path": "t4.fctb"
+ },
+ {
+ "nr": 5,
+ "path": "t5.fctb"
+ },
+ {
+ "nr": 6,
+ "path": "t6.fctb"
+ },
+ {
+ "nr": 7,
+ "path": "t7.fctb"
+ },
+ {
+ "nr": 8,
+ "path": "t8.fctb"
+ },
+ {
+ "nr": 9,
+ "path": "t9.fctb"
+ }
+ ],
+ "version": 1
+}
diff --git a/src/Mod/Path/Tools/README.md b/src/Mod/Path/Tools/README.md
new file mode 100644
index 0000000000..69f9e88a99
--- /dev/null
+++ b/src/Mod/Path/Tools/README.md
@@ -0,0 +1,87 @@
+# Tools
+
+Each tool is stored as a JSON file which has the template's path and values for all named constraints of the template.
+It also includes all additional parameters and their values.
+
+Storing a tool as a JSON file sounds great but eliminates the option of an accurate thumbnail. On the other hand,
+storing each tool as a `*.fcstd` file requires more space and does not allow for generating tools. If one has an
+extensive tool aresenal they might want to script the generation of tools which is easily done for a `*.json` file but
+practically impossible for `*.fcstd` files.
+
+When a tool is instantiated in a job the PDN body is created from the template and the constraints are set according
+to the values from the JSON file. All additional parameters are created as properties on the object. This provides the
+the correct shape and dimensions which can be used to generate a point cloud or mesh for advanced algorithms (and
+potentially simulation).
+
+# Tool Libraries
+
+Due to each tool being stored in its own file and the storage/organization of those files being quite flexible the
+importance of a tool library for organisational purposes is quite diminished. The user is free to organise their tools
+in whichever directory hierarchy they see fit and can also name them as best fits their use and organisation. A
+_tool library_ is nevertheless a great representation for a physical grouping of tools, such as in an automatic tool
+changer.
+
+A tool library is a (JSON) file with a mapping of tool id to the path of the tool file. As a consequence each tool
+can be in multiple libraries and doesn't have an `id` of it's own. The `id` is a property of the library.
+
+If a tool from a tool library (or an entire tool library) is added to a job it retains its `id` from the library as a
+property. Adding a tool bit directly rsults in the tool getting the next free id assigned.
+
+# Tool Controllers
+
+They largely stay the same as they are today. As an additional feature it should be possible to _copy_ a TC, which
+allows for easy feed/speed changes for the same tool.
+
+Above requirement highlights one change though, that the `id` should be a property of the Bit, and not of the TC.
+There are two requirements that are currently mapped to a single `id`. There needs to be an identification of which
+TC is being used by a certain op, and which tool number to use for a `M6` command.
+
+# Paths and Extensibility
+
+The following directory structure is used for supplied (shipped with FreeCAD) tools:
+```
+ Tools
+ + Bit
+ + Library
+ + Shape
+```
+
+Strictly speaking a user is free to store their tools wherever they want and however they want. By default the file
+dialog will open the corresponding directory (depending on context), or whichever directory the user opened last.
+
+Above directory structure with the most common default tools shipped with FreeCAD should be installed analogous to
+TechDraw's templates.
+
+## How to create a new tool
+
+1. Set the tool's Label, this will show up in the object tree
+1. Select a tool shape from the existing templates. If your tool doesn't exist, you'll have to create a new template,
+ see below for details.
+1. Each template has its own set of parameters, fill them with the tool's values.
+1. Select additional parameters
+1. Save the tool under path/file that makes sense to you
+
+
+## How to create a new tool bit Shape
+
+A tool bit template represents the physical shape of a tool. It does not completely desribe the bit - for that some
+additional parameters are needed which will be added when an actual bit is parametrized from the template.
+
+1. Create a new FreeCAD document
+1. Open the `PartDesign` workbench, create a body and give the body a label you want to show up in the bit selection.
+1. Create a sketch in the XZ plane and draw half the profile of the bit.
+ * Put the top center of the bit on the origin (0,0)
+1. For any constraint serving as a parameter for the tool (like overall Length) create a named constraint
+ * The name is the label of the input field
+ * Names are split at CamelCase boundaries into words in the edit dialog
+ * Use a `;` in the name to add help text which will show up as the entry fields tool tip
+ * If the tool is used by legacy ops it should at least have one constraint called `Diameter`
+ * Use construction lines for constraints that are not directly accessible, like `Diameter` and `Angle`
+1. Any unnamed constraint will not be editable for a specific tool
+1. Once the sketch is fully constrained, close the sketch
+1. Rotate the sketch around the z-axis
+1. Save the document as a new file in the Shape directory
+ * Before saving the document make sure you have _Save Thumbnail_ selected, and _Add program logo_ deselected in
+ FreeCAD's preferences.
+ * Also make sure to switch to _Front View_ and _Fit content to screen_
+ * Whatever you see when saving the document will end up being the visual representation of the template
diff --git a/src/Mod/Path/Tools/Shape/ballend.fcstd b/src/Mod/Path/Tools/Shape/ballend.fcstd
new file mode 100644
index 0000000000..bf7235e366
Binary files /dev/null and b/src/Mod/Path/Tools/Shape/ballend.fcstd differ
diff --git a/src/Mod/Path/Tools/Shape/bullnose.fcstd b/src/Mod/Path/Tools/Shape/bullnose.fcstd
new file mode 100644
index 0000000000..24b5445751
Binary files /dev/null and b/src/Mod/Path/Tools/Shape/bullnose.fcstd differ
diff --git a/src/Mod/Path/Tools/Shape/drill.fcstd b/src/Mod/Path/Tools/Shape/drill.fcstd
new file mode 100644
index 0000000000..275b401e34
Binary files /dev/null and b/src/Mod/Path/Tools/Shape/drill.fcstd differ
diff --git a/src/Mod/Path/Tools/Shape/endmill.fcstd b/src/Mod/Path/Tools/Shape/endmill.fcstd
new file mode 100644
index 0000000000..0fc51c64e7
Binary files /dev/null and b/src/Mod/Path/Tools/Shape/endmill.fcstd differ
diff --git a/src/Mod/Path/Tools/Shape/v-bit.fcstd b/src/Mod/Path/Tools/Shape/v-bit.fcstd
new file mode 100644
index 0000000000..a158b864d3
Binary files /dev/null and b/src/Mod/Path/Tools/Shape/v-bit.fcstd differ
diff --git a/src/Mod/Path/utils/path-lint.sh b/src/Mod/Path/utils/path-lint.sh
index f0106ca82d..def379183c 100755
--- a/src/Mod/Path/utils/path-lint.sh
+++ b/src/Mod/Path/utils/path-lint.sh
@@ -56,6 +56,7 @@ EXTERNAL_MODULES+=' Path'
EXTERNAL_MODULES+=' PySide'
EXTERNAL_MODULES+=' PySide.QtCore'
EXTERNAL_MODULES+=' PySide.QtGui'
+EXTERNAL_MODULES+=' Sketcher'
EXTERNAL_MODULES+=' TechDraw'
EXTERNAL_MODULES+=' TestSketcherApp'
EXTERNAL_MODULES+=' area'