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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Path-ToolTable + 2015-07-04 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-ToolTable.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + 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 +
Gui/InputField.h
+
+
+ + +
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'