diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 444be1ecd4..1e1c4e305c 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -96,6 +96,10 @@ SET(PathScripts_SRCS PathScripts/PathProfileFaces.py PathScripts/PathProfileFacesGui.py PathScripts/PathProfileGui.py + PathScripts/PathProperty.py + PathScripts/PathPropertyBag.py + PathScripts/PathPropertyBagGui.py + PathScripts/PathPropertyEditor.py PathScripts/PathSanity.py PathScripts/PathSelection.py PathScripts/PathSetupSheet.py @@ -198,6 +202,7 @@ SET(PathTests_SRCS PathTests/TestPathOpTools.py PathTests/TestPathPost.py PathTests/TestPathPreferences.py + PathTests/TestPathPropertyBag.py PathTests/TestPathSetupSheet.py PathTests/TestPathStock.py PathTests/TestPathThreadMilling.py @@ -208,6 +213,9 @@ SET(PathTests_SRCS PathTests/TestPathUtil.py PathTests/TestPathVcarve.py PathTests/TestPathVoronoi.py + PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb + PathTests/Tools/Library/test-path-tool-bit-library-00.fctl + PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd PathTests/boxtest.fcstd PathTests/test_centroid_00.ngc PathTests/test_geomop.fcstd @@ -279,6 +287,14 @@ INSTALL( Mod/Path/PathTests ) +INSTALL( + DIRECTORY + PathTests/Tools + DESTINATION + Mod/Path/PathTests +) + + INSTALL( FILES ${PathScripts_post_SRCS} diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index c99246c667..832f74f868 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -121,6 +121,8 @@ panels/PageOpVcarveEdit.ui panels/PathEdit.ui panels/PointEdit.ui + panels/PropertyBag.ui + panels/PropertyCreate.ui panels/SetupGlobal.ui panels/SetupOp.ui panels/ToolBitEditor.ui diff --git a/src/Mod/Path/Gui/Resources/panels/PropertyBag.ui b/src/Mod/Path/Gui/Resources/panels/PropertyBag.ui new file mode 100644 index 0000000000..ac9af5c8d5 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/PropertyBag.ui @@ -0,0 +1,78 @@ + + + Form + + + + 0 + 0 + 552 + 651 + + + + Property Bag + + + + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::AllEditTriggers + + + true + + + true + + + false + + + + + + + + 0 + 0 + + + + + + + Remove + + + + + + + Modify... + + + + + + + Add... + + + + + + + + + + table + add + remove + + + + diff --git a/src/Mod/Path/Gui/Resources/panels/PropertyCreate.ui b/src/Mod/Path/Gui/Resources/panels/PropertyCreate.ui new file mode 100644 index 0000000000..c65c02ea82 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/PropertyCreate.ui @@ -0,0 +1,182 @@ + + + Dialog + + + + 0 + 0 + 480 + 452 + + + + Create Property + + + + + + Name + + + + + + + <html><head/><body><p>Name of property.</p></body></html> + + + + + + + <html><head/><body><p>The category group the property belongs to.</p></body></html> + + + true + + + + + + + Group + + + + + + + <html><head/><body><p>The type of the property value.</p></body></html> + + + + + + + Type + + + + + + + val1,val2,val3,... + + + + + + + <html><head/><body><p>ToolTip to be displayed when user hovers mouse over property.</p></body></html> + + + true + + + + + + + Enums + + + + + + + ToolTip + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + <html><head/><body><p>Check if you want to create several properties in a batch.</p></body></html> + + + Create another + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + propertyName + propertyGroup + propertyType + propertyEnum + propertyInfo + createAnother + + + + + 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/ToolBitEditor.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui index 1342da5649..d77a523a12 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui @@ -6,7 +6,7 @@ 0 0 - 401 + 489 715 @@ -17,7 +17,7 @@ - + 0 0 @@ -32,6 +32,12 @@ + + + 0 + 0 + + Tool Bit @@ -65,6 +71,12 @@ + + + 0 + 0 + + 0 @@ -83,6 +95,9 @@ <html><head/><body><p>The file which defines the type and shape of the Tool Bit.</p></body></html> + + path + @@ -104,7 +119,7 @@ - Bit Parameter + Parameter @@ -184,7 +199,7 @@ - + 0 @@ -200,9 +215,6 @@ QAbstractItemView::AllEditTriggers - - true - diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui index 70650bf817..805f7a3e3d 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui @@ -173,6 +173,12 @@ QFrame::Box + + QAbstractItemView::NoEditTriggers + + + false + diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index e26c2fe0da..20e35d8590 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -154,6 +154,10 @@ class PathWorkbench (Workbench): if extracmdlist: self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], extracmdlist) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], ["Separator"]) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP("Path", "Utils")], + ["Path_PropertyBag"]) + self.dressupcmds = dressupcmdlist curveAccuracy = PathPreferences.defaultLibAreaCurveAccuracy() diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py b/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py index 85dbb81e0a..c768fd53ab 100644 --- a/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py +++ b/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathLog as PathLog import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/PathCustomGui.py b/src/Mod/Path/PathScripts/PathCustomGui.py index 987bcfb50e..658468a4e0 100644 --- a/src/Mod/Path/PathScripts/PathCustomGui.py +++ b/src/Mod/Path/PathScripts/PathCustomGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathCustom as PathCustom import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/PathDeburrGui.py b/src/Mod/Path/PathScripts/PathDeburrGui.py index 1b0a0da690..d98642f4dd 100644 --- a/src/Mod/Path/PathScripts/PathDeburrGui.py +++ b/src/Mod/Path/PathScripts/PathDeburrGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathDeburr as PathDeburr import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py b/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py index 34cffb1d68..6152c51433 100644 --- a/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py +++ b/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathDressupPathBoundary as PathDressupPathBoundary import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathDressupTagGui.py b/src/Mod/Path/PathScripts/PathDressupTagGui.py index 0a6c01dc6b..ee7c0119a6 100644 --- a/src/Mod/Path/PathScripts/PathDressupTagGui.py +++ b/src/Mod/Path/PathScripts/PathDressupTagGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathGeom as PathGeom import PathScripts.PathGetPoint as PathGetPoint import PathScripts.PathDressupHoldingTags as PathDressupTag diff --git a/src/Mod/Path/PathScripts/PathDrillingGui.py b/src/Mod/Path/PathScripts/PathDrillingGui.py index 534d44e1a8..4ebee90d5f 100644 --- a/src/Mod/Path/PathScripts/PathDrillingGui.py +++ b/src/Mod/Path/PathScripts/PathDrillingGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui import PathScripts.PathDrilling as PathDrilling import PathScripts.PathGui as PathGui diff --git a/src/Mod/Path/PathScripts/PathEngraveGui.py b/src/Mod/Path/PathScripts/PathEngraveGui.py index 95d5ce9dc5..6301c4882d 100644 --- a/src/Mod/Path/PathScripts/PathEngraveGui.py +++ b/src/Mod/Path/PathScripts/PathEngraveGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathEngrave as PathEngrave import PathScripts.PathLog as PathLog import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/PathGui.py b/src/Mod/Path/PathScripts/PathGui.py index c9fee42062..fb37725bf3 100644 --- a/src/Mod/Path/PathScripts/PathGui.py +++ b/src/Mod/Path/PathScripts/PathGui.py @@ -48,6 +48,7 @@ def updateInputField(obj, prop, widget, onBeforeChange=None): If onBeforeChange is specified it is called before a new value is assigned to the property. Returns True if a new value was assigned, False otherwise (new value is the same as the current). ''' + PathLog.track() value = widget.property('rawValue') attr = PathUtil.getProperty(obj, prop) attrValue = attr.Value if hasattr(attr, 'Value') else attr @@ -98,10 +99,12 @@ class QuantitySpinBox: PathLog.track(widget) self.widget = widget self.onBeforeChange = onBeforeChange + self.prop = None self.attachTo(obj, prop) def attachTo(self, obj, prop = None): '''attachTo(obj, prop=None) ... use an existing editor for the given object and property''' + PathLog.track(self.prop, prop) self.obj = obj self.prop = prop if obj and prop: @@ -119,12 +122,14 @@ class QuantitySpinBox: def expression(self): '''expression() ... returns the expression if one is bound to the property''' + PathLog.track(self.prop, self.valid) if self.valid: return self.widget.property('expression') return '' def setMinimum(self, quantity): '''setMinimum(quantity) ... set the minimum''' + PathLog.track(self.prop, self.valid) if self.valid: value = quantity.Value if hasattr(quantity, 'Value') else quantity self.widget.setProperty('setMinimum', value) @@ -133,6 +138,7 @@ class QuantitySpinBox: '''updateSpinBox(quantity=None) ... update the display value of the spin box. If no value is provided the value of the bound property is used. quantity can be of type Quantity or Float.''' + PathLog.track(self.prop, self.valid) if self.valid: if quantity is None: quantity = PathUtil.getProperty(self.obj, self.prop) @@ -141,6 +147,7 @@ class QuantitySpinBox: def updateProperty(self): '''updateProperty() ... update the bound property with the value from the spin box''' + PathLog.track(self.prop, self.valid) if self.valid: return updateInputField(self.obj, self.prop, self.widget, self.onBeforeChange) return None diff --git a/src/Mod/Path/PathScripts/PathGuiInit.py b/src/Mod/Path/PathScripts/PathGuiInit.py index cd48d41309..405dcb4fcc 100644 --- a/src/Mod/Path/PathScripts/PathGuiInit.py +++ b/src/Mod/Path/PathScripts/PathGuiInit.py @@ -66,6 +66,7 @@ def Startup(): # from PathScripts import PathProfileEdgesGui # from PathScripts import PathProfileFacesGui from PathScripts import PathProfileGui + from PathScripts import PathPropertyBagGui from PathScripts import PathSanity from PathScripts import PathSetupSheetGui from PathScripts import PathSimpleCopy diff --git a/src/Mod/Path/PathScripts/PathHelixGui.py b/src/Mod/Path/PathScripts/PathHelixGui.py index 141cce33d9..b65048ee9c 100644 --- a/src/Mod/Path/PathScripts/PathHelixGui.py +++ b/src/Mod/Path/PathScripts/PathHelixGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui import PathScripts.PathHelix as PathHelix import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathJobGui.py b/src/Mod/Path/PathScripts/PathJobGui.py index f5d2fa9db2..5e78e944df 100644 --- a/src/Mod/Path/PathScripts/PathJobGui.py +++ b/src/Mod/Path/PathScripts/PathJobGui.py @@ -31,6 +31,7 @@ from PySide import QtCore, QtGui import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathJob as PathJob import PathScripts.PathJobCmd as PathJobCmd import PathScripts.PathJobDlg as PathJobDlg diff --git a/src/Mod/Path/PathScripts/PathOpGui.py b/src/Mod/Path/PathScripts/PathOpGui.py index 87e5f9b1e6..0aa304a5fc 100644 --- a/src/Mod/Path/PathScripts/PathOpGui.py +++ b/src/Mod/Path/PathScripts/PathOpGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathGeom as PathGeom import PathScripts.PathGetPoint as PathGetPoint import PathScripts.PathGui as PathGui diff --git a/src/Mod/Path/PathScripts/PathPocketBaseGui.py b/src/Mod/Path/PathScripts/PathPocketBaseGui.py index 915b6ea71a..68920b0a5e 100644 --- a/src/Mod/Path/PathScripts/PathPocketBaseGui.py +++ b/src/Mod/Path/PathScripts/PathPocketBaseGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathGui as PathGui import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/PathPocketShapeGui.py b/src/Mod/Path/PathScripts/PathPocketShapeGui.py index 53bc26c336..4e1b651148 100644 --- a/src/Mod/Path/PathScripts/PathPocketShapeGui.py +++ b/src/Mod/Path/PathScripts/PathPocketShapeGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathGeom as PathGeom import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py index d914568631..a1c1e58fc5 100644 --- a/src/Mod/Path/PathScripts/PathPreferences.py +++ b/src/Mod/Path/PathScripts/PathPreferences.py @@ -151,26 +151,9 @@ def searchPathsPost(): return paths -def searchPathsTool(sub='Bit'): +def searchPathsTool(sub): paths = [] - - if 'Bit' == sub: - paths.append("{}/Bit".format(os.path.dirname(lastPathToolLibrary()))) - 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) + paths.append(os.path.join(FreeCAD.getHomePath(), 'Mod', 'Path', 'Tools', sub)) return paths @@ -263,15 +246,19 @@ def setDefaultTaskPanelLayout(style): def experimentalFeaturesEnabled(): return preferences().GetBool(EnableExperimentalFeatures, False) + def suppressAllSpeedsWarning(): return preferences().GetBool(WarningSuppressAllSpeeds, True) + def suppressRapidSpeedsWarning(): return suppressAllSpeedsWarning() or preferences().GetBool(WarningSuppressRapidSpeeds, True) + def suppressSelectionModeWarning(): return preferences().GetBool(WarningSuppressSelectionMode, True) + def suppressOpenCamLibWarning(): return preferences().GetBool(WarningSuppressOpenCamLib, True) @@ -316,7 +303,8 @@ def lastPathToolLibrary(): def setLastPathToolLibrary(path): PathLog.track(path) curLib = lastFileToolLibrary() - if os.path.split(curLib)[0] != path: + PathLog.debug('curLib: {}'.format(curLib)) + if curLib and os.path.split(curLib)[0] != path: setLastFileToolLibrary('') # a path is known but not specific file return preferences().SetString(LastPathToolLibrary, path) diff --git a/src/Mod/Path/PathScripts/PathProbeGui.py b/src/Mod/Path/PathScripts/PathProbeGui.py index a00ec913e8..29c97d7adc 100644 --- a/src/Mod/Path/PathScripts/PathProbeGui.py +++ b/src/Mod/Path/PathScripts/PathProbeGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathProbe as PathProbe import PathScripts.PathOpGui as PathOpGui import PathScripts.PathGui as PathGui diff --git a/src/Mod/Path/PathScripts/PathProfileGui.py b/src/Mod/Path/PathScripts/PathProfileGui.py index a7f77d7062..8a85a6abfa 100644 --- a/src/Mod/Path/PathScripts/PathProfileGui.py +++ b/src/Mod/Path/PathScripts/PathProfileGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathGui as PathGui import PathScripts.PathOpGui as PathOpGui import PathScripts.PathProfile as PathProfile diff --git a/src/Mod/Path/PathScripts/PathProperty.py b/src/Mod/Path/PathScripts/PathProperty.py new file mode 100644 index 0000000000..7e3eeb1e6f --- /dev/null +++ b/src/Mod/Path/PathScripts/PathProperty.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2020 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.PathLog as PathLog + +__title__ = "Property type abstraction for editing purposes" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Prototype objects to allow extraction of setup sheet values and editing." + + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) + +class Property(object): + '''Base class for all prototype properties''' + def __init__(self, name, propType, category, info): + self.name = name + self.propType = propType + self.category = category + self.info = info + self.editorMode = 0 + self.value = None + + def setValue(self, value): + self.value = value + def getValue(self): + return self.value + + def setEditorMode(self, mode): + self.editorMode = mode + + def displayString(self): + if self.value is None: + t = self.typeString() + p = 'an' if t[0] in ['A', 'E', 'I', 'O', 'U'] else 'a' + return "%s %s" % (p, t) + return self.value + + def typeString(self): + return "Property" + + def setupProperty(self, obj, name, category, value): + created = False + if not hasattr(obj, name): + obj.addProperty(self.propType, name, category, self.info) + self.initProperty(obj, name) + created = True + setattr(obj, name, value) + return created + + def initProperty(self, obj, name): + pass + + def setValueFromString(self, string): + self.setValue(self.valueFromString(string)) + + def valueFromString(self, string): + return string + +class PropertyEnumeration(Property): + def typeString(self): + return "Enumeration" + + def setValue(self, value): + if list == type(value): + self.enums = value # pylint: disable=attribute-defined-outside-init + else: + super(PropertyEnumeration, self).setValue(value) + + def getEnumValues(self): + return self.enums + + def initProperty(self, obj, name): + setattr(obj, name, self.enums) + +class PropertyQuantity(Property): + def displayString(self): + if self.value is None: + 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" + +class PropertyLength(PropertyQuantity): + def typeString(self): + return "Length" + +class PropertyPercent(Property): + def typeString(self): + return "Percent" + +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" + +class PropertyMap(Property): + def typeString(self): + return "Map" + + def displayString(self, value): + return str(value) + +class OpPrototype(object): + + PropertyType = { + 'App::PropertyAngle': PropertyAngle, + 'App::PropertyBool': PropertyBool, + 'App::PropertyDistance': PropertyDistance, + 'App::PropertyEnumeration': PropertyEnumeration, + 'App::PropertyFile': PropertyString, + '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::PropertyMap': PropertyMap, + 'App::PropertyPercent': PropertyPercent, + 'App::PropertyString': PropertyString, + 'App::PropertyStringList': Property, + 'App::PropertyVectorDistance': Property, + 'App::PropertyVectorList': Property, + 'Part::PropertyPartShape': Property, + } + + def __init__(self, name): + self.Label = name + self.properties = {} + self.DoNotSetDefaultValues = True + self.Proxy = None + + def __setattr__(self, name, val): + if name in ['Label', 'DoNotSetDefaultValues', 'properties', 'Proxy']: + if name == 'Proxy': + val = None # make sure the proxy is never set + return super(OpPrototype, self).__setattr__(name, val) + self.properties[name].setValue(val) + + def addProperty(self, typeString, name, category, info = None): + prop = self.PropertyType[typeString](name, typeString, category, info) + self.properties[name] = prop + return self + + def setEditorMode(self, name, mode): + self.properties[name].setEditorMode(mode) + + def getProperty(self, name): + return self.properties[name] + + def setupProperties(self, setup): + return [p for p in self.properties if p.name in setup] diff --git a/src/Mod/Path/PathScripts/PathPropertyBag.py b/src/Mod/Path/PathScripts/PathPropertyBag.py new file mode 100644 index 0000000000..2b21be2c6b --- /dev/null +++ b/src/Mod/Path/PathScripts/PathPropertyBag.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2020 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 PySide + +__title__ = 'Generic property container to store some values.' +__author__ = 'sliptonic (Brad Collette)' +__url__ = 'https://www.freecadweb.org' +__doc__ = 'A generic container for typed properties in arbitrary categories.' + +def translate(context, text, disambig=None): + return PySide.QtCore.QCoreApplication.translate(context, text, disambig) + + +SupportedPropertyType = { + 'Angle' : 'App::PropertyAngle', + 'Bool' : 'App::PropertyBool', + 'Distance' : 'App::PropertyDistance', + 'Enumeration' : 'App::PropertyEnumeration', + 'File' : 'App::PropertyFile', + 'Float' : 'App::PropertyFloat', + 'Integer' : 'App::PropertyInteger', + 'Length' : 'App::PropertyLength', + 'Percent' : 'App::PropertyPercent', + 'String' : 'App::PropertyString', + } + +def getPropertyTypeName(o): + for typ in SupportedPropertyType: + if SupportedPropertyType[typ] == o: + return typ + raise IndexError() + +class PropertyBag(object): + '''Property container object.''' + + CustomPropertyGroups = 'CustomPropertyGroups' + CustomPropertyGroupDefault = 'User' + + def __init__(self, obj): + obj.addProperty('App::PropertyStringList', self.CustomPropertyGroups, 'Base', PySide.QtCore.QT_TRANSLATE_NOOP('PathPropertyBag', 'List of custom property groups')) + self.onDocumentRestored(obj) + + def __getstate__(self): + return None + + def __setstate__(self, state): + return None + + def onDocumentRestored(self, obj): + self.obj = obj + obj.setEditorMode(self.CustomPropertyGroups, 2) # hide + + def getCustomProperties(self): + '''getCustomProperties() ... Return a list of all custom properties created in this container.''' + return [p for p in self.obj.PropertiesList if self.obj.getGroupOfProperty(p) in self.obj.CustomPropertyGroups] + + def addCustomProperty(self, propertyType, name, group=None, desc=None): + '''addCustomProperty(propertyType, name, group=None, desc=None) ... adds a custom property and tracks its group.''' + if desc is None: + desc = '' + if group is None: + group = self.CustomPropertyGroupDefault + groups = self.obj.CustomPropertyGroups + if not group in groups: + groups.append(group) + self.obj.CustomPropertyGroups = groups + self.obj.addProperty(propertyType, name, group, desc) + + def refreshCustomPropertyGroups(self): + '''refreshCustomPropertyGroups() ... removes empty property groups, should be called after deleting properties.''' + customGroups = [] + for p in self.obj.PropertiesList: + group = self.obj.getGroupOfProperty(p) + if group in self.obj.CustomPropertyGroups and not group in customGroups: + customGroups.append(group) + self.obj.CustomPropertyGroups = customGroups + + +def Create(name = 'PropertyBag'): + obj = FreeCAD.ActiveDocument.addObject('App::FeaturePython', name) + obj.Proxy = PropertyBag(obj) + return obj + +def IsPropertyBag(obj): + '''Returns True if the supplied object is a property container (or its Proxy).''' + + if type(obj) == PropertyBag: + return True + if hasattr(obj, 'Proxy'): + return IsPropertyBag(obj.Proxy) + return False + diff --git a/src/Mod/Path/PathScripts/PathPropertyBagGui.py b/src/Mod/Path/PathScripts/PathPropertyBagGui.py new file mode 100644 index 0000000000..54f55d4a56 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathPropertyBagGui.py @@ -0,0 +1,425 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2020 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 PathGui as PGui # ensure Path/Gui/Resources are loaded +import PathScripts.PathIconViewProvider as PathIconViewProvider +import PathScripts.PathLog as PathLog +import PathScripts.PathPropertyBag as PathPropertyBag +import PathScripts.PathPropertyEditor as PathPropertyEditor +import PathScripts.PathUtil as PathUtil + +from PySide import QtCore, QtGui + +__title__ = "Property Bag Editor" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Task panel editor for a PropertyBag" + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + +class ViewProvider(object): + '''ViewProvider for a PropertyBag. + It's sole job is to provide an icon and invoke the TaskPanel on edit.''' + + def __init__(self, vobj, name): + PathLog.track(name) + vobj.Proxy = self + self.icon = name + # mode = 2 + self.obj = None + self.vobj = None + + def attach(self, vobj): + PathLog.track() + self.vobj = vobj + self.obj = vobj.Object + + def getIcon(self): + return ":/icons/Path-SetupSheet.svg" + + def __getstate__(self): + return None + + def __setstate__(self, state): + # pylint: disable=unused-argument + return None + + def getDisplayMode(self, mode): + # pylint: disable=unused-argument + return 'Default' + + def setEdit(self, vobj, mode=0): + # pylint: disable=unused-argument + PathLog.track() + taskPanel = TaskPanel(vobj) + FreeCADGui.Control.closeDialog() + FreeCADGui.Control.showDialog(taskPanel) + taskPanel.setupUi() + return True + + def unsetEdit(self, vobj, mode): + # pylint: disable=unused-argument + FreeCADGui.Control.closeDialog() + return + + def claimChildren(self): + return [] + + def doubleClicked(self, vobj): + self.setEdit(vobj) + +class Delegate(QtGui.QStyledItemDelegate): + RoleObject = QtCore.Qt.UserRole + 1 + RoleProperty = QtCore.Qt.UserRole + 2 + RoleEditor = QtCore.Qt.UserRole + 3 + + + #def paint(self, painter, option, index): + # #PathLog.track(index.column(), type(option)) + + def createEditor(self, parent, option, index): + # pylint: disable=unused-argument + editor = PathPropertyEditor.Editor(index.data(self.RoleObject), index.data(self.RoleProperty)) + index.model().setData(index, editor, self.RoleEditor) + return editor.widget(parent) + + def setEditorData(self, widget, index): + PathLog.track(index.row(), index.column()) + index.data(self.RoleEditor).setEditorData(widget) + + def setModelData(self, widget, model, index): + # pylint: disable=unused-argument + PathLog.track(index.row(), index.column()) + editor = index.data(self.RoleEditor) + editor.setModelData(widget) + index.model().setData(index, editor.displayString(), QtCore.Qt.DisplayRole) + + def updateEditorGeometry(self, widget, option, index): + # pylint: disable=unused-argument + widget.setGeometry(option.rect) + +class PropertyCreate(object): + + def __init__(self, obj, grp, typ, another): + self.obj = obj + self.form = FreeCADGui.PySideUic.loadUi(":panels/PropertyCreate.ui") + + obj.Proxy.refreshCustomPropertyGroups() + for g in sorted(obj.CustomPropertyGroups): + self.form.propertyGroup.addItem(g) + if grp: + self.form.propertyGroup.setCurrentText(grp) + + for t in sorted(PathPropertyBag.SupportedPropertyType): + self.form.propertyType.addItem(t) + if PathPropertyBag.SupportedPropertyType[t] == typ: + typ = t + if typ: + self.form.propertyType.setCurrentText(typ) + else: + self.form.propertyType.setCurrentText('String') + self.form.createAnother.setChecked(another) + + self.form.propertyGroup.currentTextChanged.connect(self.updateUI) + self.form.propertyGroup.currentIndexChanged.connect(self.updateUI) + self.form.propertyName.textChanged.connect(self.updateUI) + self.form.propertyType.currentIndexChanged.connect(self.updateUI) + self.form.propertyEnum.textChanged.connect(self.updateUI) + + def updateUI(self): + typeSet = True + if self.propertyIsEnumeration(): + self.form.labelEnum.setEnabled(True) + self.form.propertyEnum.setEnabled(True) + typeSet = self.form.propertyEnum.text().strip() != '' + else: + self.form.labelEnum.setEnabled(False) + self.form.propertyEnum.setEnabled(False) + if self.form.propertyEnum.text().strip(): + self.form.propertyEnum.setText('') + + ok = self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok) + + if typeSet and self.propertyName() and self.propertyGroup(): + ok.setEnabled(True) + else: + ok.setEnabled(False) + + def propertyName(self): + return self.form.propertyName.text().strip() + def propertyGroup(self): + return self.form.propertyGroup.currentText().strip() + def propertyType(self): + return PathPropertyBag.SupportedPropertyType[self.form.propertyType.currentText()].strip() + def propertyInfo(self): + return self.form.propertyInfo.toPlainText().strip() + def createAnother(self): + return self.form.createAnother.isChecked() + def propertyEnumerations(self): + return [s.strip() for s in self.form.propertyEnum.text().strip().split(',')] + def propertyIsEnumeration(self): + return self.propertyType() == 'App::PropertyEnumeration' + + def exec_(self, name): + if name: + # property exists - this is an edit operation + self.form.propertyName.setText(name) + if self.propertyIsEnumeration(): + self.form.propertyEnum.setText(','.join(self.obj.getEnumerationsOfProperty(name))) + self.form.propertyInfo.setText(self.obj.getDocumentationOfProperty(name)) + + self.form.labelName.setEnabled(False) + self.form.propertyName.setEnabled(False) + self.form.labelType.setEnabled(False) + self.form.propertyType.setEnabled(False) + self.form.createAnother.setEnabled(False) + + else: + self.form.propertyName.setText('') + self.form.propertyInfo.setText('') + self.form.propertyEnum.setText('') + #self.form.propertyName.setFocus() + + self.updateUI() + + return self.form.exec_() + +Panel = [] + +class TaskPanel(object): + ColumnName = 0 + #ColumnType = 1 + ColumnVal = 1 + #TableHeaders = ['Property', 'Type', 'Value'] + TableHeaders = ['Property', 'Value'] + + def __init__(self, vobj): + self.obj = vobj.Object + self.props = sorted(self.obj.Proxy.getCustomProperties()) + self.form = FreeCADGui.PySideUic.loadUi(":panels/PropertyBag.ui") + + # initialized later + self.model = None + self.delegate = None + FreeCAD.ActiveDocument.openTransaction(translate("PathPropertyBag", "Edit PropertyBag")) + Panel.append(self) + + def updateData(self, topLeft, bottomRight): + pass + + + def _setupProperty(self, i, name): + typ = PathPropertyBag.getPropertyTypeName(self.obj.getTypeIdOfProperty(name)) + val = PathUtil.getPropertyValueString(self.obj, name) + info = self.obj.getDocumentationOfProperty(name) + + self.model.setData(self.model.index(i, self.ColumnName), name, QtCore.Qt.EditRole) + #self.model.setData(self.model.index(i, self.ColumnType), typ, QtCore.Qt.EditRole) + self.model.setData(self.model.index(i, self.ColumnVal), self.obj, Delegate.RoleObject) + self.model.setData(self.model.index(i, self.ColumnVal), name, Delegate.RoleProperty) + self.model.setData(self.model.index(i, self.ColumnVal), val, QtCore.Qt.DisplayRole) + + self.model.setData(self.model.index(i, self.ColumnName), typ, QtCore.Qt.ToolTipRole) + #self.model.setData(self.model.index(i, self.ColumnType), info, QtCore.Qt.ToolTipRole) + self.model.setData(self.model.index(i, self.ColumnVal), info, QtCore.Qt.ToolTipRole) + + self.model.item(i, self.ColumnName).setEditable(False) + #self.model.item(i, self.ColumnType).setEditable(False) + + def setupUi(self): + PathLog.track() + + self.delegate = Delegate(self.form) + self.model = QtGui.QStandardItemModel(len(self.props), len(self.TableHeaders), self.form) + self.model.setHorizontalHeaderLabels(self.TableHeaders) + + for i,name in enumerate(self.props): + self._setupProperty(i, name) + + self.form.table.setModel(self.model) + self.form.table.setItemDelegateForColumn(self.ColumnVal, self.delegate) + self.form.table.resizeColumnsToContents() + + self.model.dataChanged.connect(self.updateData) + self.form.table.selectionModel().selectionChanged.connect(self.propertySelected) + self.form.add.clicked.connect(self.propertyAdd) + self.form.remove.clicked.connect(self.propertyRemove) + self.form.modify.clicked.connect(self.propertyModify) + self.form.table.doubleClicked.connect(self.propertyModifyIndex) + self.propertySelected([]) + + def accept(self): + FreeCAD.ActiveDocument.commitTransaction() + FreeCADGui.ActiveDocument.resetEdit() + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + + def reject(self): + FreeCAD.ActiveDocument.abortTransaction() + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + + def propertySelected(self, selection): + PathLog.track() + if selection: + self.form.modify.setEnabled(True) + self.form.remove.setEnabled(True) + else: + self.form.modify.setEnabled(False) + self.form.remove.setEnabled(False) + + def addCustomProperty(self, obj, dialog): + name = dialog.propertyName() + typ = dialog.propertyType() + grp = dialog.propertyGroup() + info = dialog.propertyInfo() + self.obj.Proxy.addCustomProperty(typ, name, grp, info) + if dialog.propertyIsEnumeration(): + setattr(self.obj, name, dialog.propertyEnumerations()) + return (name, info) + + def propertyAdd(self): + PathLog.track() + more = False + grp = None + typ = None + while True: + dialog = PropertyCreate(self.obj, grp, typ, more) + if dialog.exec_(None): + # if we block signals the view doesn't get updated, surprise, surprise + #self.model.blockSignals(True) + name, info = self.addCustomProperty(self.obj, dialog) + index = 0 + for i in range(self.model.rowCount()): + index = i + if self.model.item(i, self.ColumnName).data(QtCore.Qt.EditRole) > dialog.propertyName(): + break + self.model.insertRows(index, 1) + self._setupProperty(index, name) + self.form.table.selectionModel().setCurrentIndex(self.model.index(index, 0), QtCore.QItemSelectionModel.Rows) + #self.model.blockSignals(False) + more = dialog.createAnother() + else: + more = False + if not more: + break + + def propertyModifyIndex(self, index): + PathLog.track(index.row(), index.column()) + row = index.row() + + obj = self.model.item(row, self.ColumnVal).data(Delegate.RoleObject) + nam = self.model.item(row, self.ColumnVal).data(Delegate.RoleProperty) + grp = obj.getGroupOfProperty(nam) + typ = obj.getTypeIdOfProperty(nam) + + dialog = PropertyCreate(self.obj, grp, typ, False) + if dialog.exec_(nam): + val = getattr(obj, nam) + obj.removeProperty(nam) + name, info = self.addCustomProperty(self.obj, dialog) + try: + setattr(obj, nam, val) + except: + # this can happen if the old enumeration value doesn't exist anymore + pass + newVal = PathUtil.getPropertyValueString(obj, nam) + self.model.setData(self.model.index(row, self.ColumnVal), newVal, QtCore.Qt.DisplayRole) + + #self.model.setData(self.model.index(row, self.ColumnType), info, QtCore.Qt.ToolTipRole) + self.model.setData(self.model.index(row, self.ColumnVal), info, QtCore.Qt.ToolTipRole) + + def propertyModify(self): + PathLog.track() + rows = [] + for index in self.form.table.selectionModel().selectedIndexes(): + row = index.row() + if row in rows: + continue + rows.append(row) + + self.propertyModifyIndex(index) + + + def propertyRemove(self): + PathLog.track() + # first find all rows which need to be removed + rows = [] + for index in self.form.table.selectionModel().selectedIndexes(): + if not index.row() in rows: + rows.append(index.row()) + + # then remove them in reverse order so the indexes of the remaining rows + # to delete are still valid + for row in reversed(sorted(rows)): + self.obj.removeProperty(self.model.item(row).data(QtCore.Qt.EditRole)) + self.model.removeRow(row) + + +def Create(name = 'PropertyBag'): + '''Create(name = 'PropertyBag') ... creates a new setup sheet''' + FreeCAD.ActiveDocument.openTransaction(translate("PathPropertyBag", "Create PropertyBag")) + pcont = PathPropertyBag.Create(name) + PathIconViewProvider.Attach(pcont.ViewObject, name) + return pcont + +PathIconViewProvider.RegisterViewProvider('PropertyBag', ViewProvider) + +class PropertyBagCreateCommand(object): + '''Command to create a property container object''' + + def __init__(self): + pass + + def GetResources(self): + return {'MenuText': translate('PathPropertyBag', 'Property Bag'), + 'ToolTip': translate('PathPropertyBag', 'Creates an object which can be used to store reference properties.')} + + def IsActive(self): + return not FreeCAD.ActiveDocument is None + + def Activated(self): + sel = FreeCADGui.Selection.getSelectionEx() + obj = Create() + body = None + if sel: + if 'PartDesign::Body' == sel[0].Object.TypeId: + body = sel[0].Object + elif hasattr(sel[0].Object, 'getParentGeoFeatureGroup'): + body = sel[0].Object.getParentGeoFeatureGroup() + if body: + obj.Label = 'Attributes' + group = body.Group + group.append(obj) + body.Group = group + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('Path_PropertyBag', PropertyBagCreateCommand()) + +FreeCAD.Console.PrintLog("Loading PathPropertyBagGui ... done\n") diff --git a/src/Mod/Path/PathScripts/PathPropertyEditor.py b/src/Mod/Path/PathScripts/PathPropertyEditor.py new file mode 100644 index 0000000000..f376e351fd --- /dev/null +++ b/src/Mod/Path/PathScripts/PathPropertyEditor.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2020 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 PathScripts.PathLog as PathLog +import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype + +from PySide import QtCore, QtGui + +__title__ = "Path Property Editor" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Task panel editor for Properties" + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) + + +class _PropertyEditor(object): + '''Base class of all property editors - just outlines the TableView delegate interface.''' + def __init__(self, obj, prop): + self.obj = obj + self.prop = prop + + def widget(self, parent): + '''widget(parent) ... called by the delegate to get a new editor widget. + Must be implemented by subclasses and return the widget.''' + pass # pylint: disable=unnecessary-pass + + def setEditorData(self, widget): + '''setEditorData(widget) ... called by the delegate to initialize the editor. + The widget is the object returned by widget(). + Must be implemented by subclasses.''' + pass # pylint: disable=unnecessary-pass + + def setModelData(self, widget): + '''setModelData(widget) ... called by the delegate to store new values. + Must be implemented by subclasses.''' + pass # pylint: disable=unnecessary-pass + + def propertyValue(self): + return self.obj.getPropertyByName(self.prop) + + def setProperty(self, value): + setattr(self.obj, self.prop, value) + + def displayString(self): + return self.propertyValue() + +class _PropertyEditorBool(_PropertyEditor): + '''Editor for boolean values - uses a combo box.''' + + def widget(self, parent): + return QtGui.QComboBox(parent) + + def setEditorData(self, widget): + widget.clear() + widget.addItems([str(False), str(True)]) + index = 1 if self.propertyValue() else 0 + widget.setCurrentIndex(index) + + def setModelData(self, widget): + self.setProperty(widget.currentText() == str(True)) + +class _PropertyEditorString(_PropertyEditor): + '''Editor for string values - uses a line edit.''' + + def widget(self, parent): + return QtGui.QLineEdit(parent) + + def setEditorData(self, widget): + text = '' if self.propertyValue() is None else self.propertyValue() + widget.setText(text) + + def setModelData(self, widget): + self.setProperty(widget.text()) + +class _PropertyEditorQuantity(_PropertyEditor): + + def widget(self, parent): + return QtGui.QLineEdit(parent) + + def setEditorData(self, widget): + quantity = self.propertyValue() + if quantity is None: + quantity = self.defaultQuantity() + widget.setText(quantity.getUserPreferred()[0]) + + def defaultQuantity(self): + pass + + def setModelData(self, widget): + self.setProperty(FreeCAD.Units.Quantity(widget.text())) + + def displayString(self): + if self.propertyValue() is None: + return '' + return self.propertyValue().getUserPreferred()[0] + +class _PropertyEditorAngle(_PropertyEditorQuantity): + '''Editor for angle values - uses a line edit''' + + def defaultQuantity(self): + return FreeCAD.Units.Quantity(0, FreeCAD.Units.Angle) + +class _PropertyEditorLength(_PropertyEditorQuantity): + '''Editor for length values - uses a line edit.''' + + def defaultQuantity(self): + return FreeCAD.Units.Quantity(0, FreeCAD.Units.Length) + +class _PropertyEditorPercent(_PropertyEditor): + '''Editor for percent values - uses a spin box.''' + + def widget(self, parent): + return QtGui.QSpinBox(parent) + + def setEditorData(self, widget): + widget.setRange(0, 100) + value = self.propertyValue() + if value is None: + value = 0 + widget.setValue(value) + + def setModelData(self, widget): + self.setProperty(widget.value()) + +class _PropertyEditorInteger(_PropertyEditor): + '''Editor for integer values - uses a spin box.''' + + def widget(self, parent): + return QtGui.QSpinBox(parent) + + def setEditorData(self, widget): + value = self.propertyValue() + if value is None: + value = 0 + widget.setValue(value) + + def setModelData(self, widget): + self.setProperty(widget.value()) + +class _PropertyEditorFloat(_PropertyEditor): + '''Editor for float values - uses a double spin box.''' + + def widget(self, parent): + return QtGui.QDoubleSpinBox(parent) + + def setEditorData(self, widget): + value = self.propertyValue() + if value is None: + value = 0.0 + widget.setValue(value) + + def setModelData(self, widget): + self.setProperty(widget.value()) + +class _PropertyEditorFile(_PropertyEditor): + + def widget(self, parent): + return QtGui.QLineEdit(parent) + + def setEditorData(self, widget): + text = '' if self.propertyValue() is None else self.propertyValue() + widget.setText(text) + + def setModelData(self, widget): + self.setProperty(widget.text()) + +class _PropertyEditorEnumeration(_PropertyEditor): + + def widget(self, parent): + return QtGui.QComboBox(parent) + + def setEditorData(self, widget): + widget.clear() + widget.addItems(self.obj.getEnumerationsOfProperty(self.prop)) + widget.setCurrentText(self.propertyValue()) + + def setModelData(self, widget): + self.setProperty(widget.currentText()) + +_EditorFactory = { + 'App::PropertyAngle' : _PropertyEditorAngle, + 'App::PropertyBool' : _PropertyEditorBool, + 'App::PropertyDistance' : _PropertyEditorLength, + 'App::PropertyEnumeration' : _PropertyEditorEnumeration, + #'App::PropertyFile' : _PropertyEditorFile, + 'App::PropertyFloat' : _PropertyEditorFloat, + 'App::PropertyInteger' : _PropertyEditorInteger, + 'App::PropertyLength' : _PropertyEditorLength, + 'App::PropertyPercent' : _PropertyEditorPercent, + 'App::PropertyString' : _PropertyEditorString, + } + +def Types(): + '''Return the types of properties supported.''' + return [t for t in _EditorFactory] + +def Editor(obj, prop): + '''Returns an editor class to be used for the given property.''' + factory = _EditorFactory[obj.getTypeIdOfProperty(prop)] + if factory: + return factory(obj, prop) + return None diff --git a/src/Mod/Path/PathScripts/PathSetupSheetGui.py b/src/Mod/Path/PathScripts/PathSetupSheetGui.py index d2ceb3956b..1584b964a0 100644 --- a/src/Mod/Path/PathScripts/PathSetupSheetGui.py +++ b/src/Mod/Path/PathScripts/PathSetupSheetGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathGui as PathGui import PathScripts.PathIconViewProvider as PathIconViewProvider import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathSimulatorGui.py b/src/Mod/Path/PathScripts/PathSimulatorGui.py index 086db6b145..3a01944abc 100644 --- a/src/Mod/Path/PathScripts/PathSimulatorGui.py +++ b/src/Mod/Path/PathScripts/PathSimulatorGui.py @@ -22,6 +22,7 @@ import FreeCAD import Path +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathDressup as PathDressup import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathSlotGui.py b/src/Mod/Path/PathScripts/PathSlotGui.py index 397ec14fdf..518209e562 100644 --- a/src/Mod/Path/PathScripts/PathSlotGui.py +++ b/src/Mod/Path/PathScripts/PathSlotGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathSlot as PathSlot import PathScripts.PathGui as PathGui import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py index 038a670c52..25a5cea8c7 100644 --- a/src/Mod/Path/PathScripts/PathSurfaceGui.py +++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathSurface as PathSurface import PathScripts.PathGui as PathGui import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/PathThreadMillingGui.py b/src/Mod/Path/PathScripts/PathThreadMillingGui.py index fab21692a7..885f129a6b 100644 --- a/src/Mod/Path/PathScripts/PathThreadMillingGui.py +++ b/src/Mod/Path/PathScripts/PathThreadMillingGui.py @@ -23,6 +23,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui import PathScripts.PathThreadMilling as PathThreadMilling import PathScripts.PathGui as PathGui diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 5ffe7503dd..38e8a4ce4e 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -24,6 +24,7 @@ import FreeCAD import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathPreferences as PathPreferences +import PathScripts.PathPropertyBag as PathPropertyBag import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype import PathScripts.PathUtil as PathUtil import PySide @@ -42,74 +43,73 @@ __author__ = "sliptonic (Brad Collette)" __url__ = "https://www.freecadweb.org" __doc__ = "Class to deal with and represent a tool bit." -# PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) -# PathLog.trackModule() +PropertyGroupShape = 'Shape' + +_DebugFindTool = False + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule() def translate(context, text, disambig=None): return PySide.QtCore.QCoreApplication.translate(context, text, disambig) +def _findToolFile(name, containerFile, typ): + PathLog.track(name) + if os.path.exists(name): # absolute reference + return name -ParameterTypeConstraint = { - 'Angle': 'App::PropertyAngle', - 'Distance': 'App::PropertyLength', - 'DistanceX': 'App::PropertyLength', - 'DistanceY': 'App::PropertyLength', - 'Radius': 'App::PropertyLength'} + if containerFile: + rootPath = os.path.dirname(os.path.dirname(containerFile)) + paths = [os.path.join(rootPath, typ)] + else: + paths = [] + paths.extend(PathPreferences.searchPathsTool(typ)) + + def _findFile(path, name): + PathLog.track(path, name) + fullPath = os.path.join(path, name) + if os.path.exists(fullPath): + return (True, fullPath) + for root, ds, fs in os.walk(path): + for d in ds: + found, fullPath = _findFile(d, name) + if found: + return (True, fullPath) + return (False, None) -def _findTool(path, typ, dbg=False): - if os.path.exists(path): # absolute reference - if dbg: - PathLog.debug("Found {} at {}".format(typ, path)) - return path - - def searchFor(pname, fname): - # PathLog.debug("pname: {} fname: {}".format(pname, fname)) - if dbg: - PathLog.debug("Looking for {}".format(pname)) - if fname: - for p in PathPreferences.searchPathsTool(typ): - PathLog.track(p) - 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: - PathLog.track(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, '') + for p in paths: + found, path = _findFile(p, name) + if found: + return path + return None -def findShape(path): - ''' - findShape(path) ... search for path, full and partially - in all known shape directories. - ''' - return _findTool(path, 'Shape') +def findToolShape(name, path=None): + '''findToolShape(name, path) ... search for name, if relative path look in path''' + PathLog.track(name, path) + return _findToolFile(name, path, 'Shape') -def findBit(path): - PathLog.track(path) - if path.endswith('.fctb'): - return _findTool(path, 'Bit') - return _findTool("{}.fctb".format(path), 'Bit') +def findToolBit(name, path=None): + '''findToolBit(name, path) ... search for name, if relative path look in path''' + PathLog.track(name, path) + if name.endswith('.fctb'): + return _findToolFile(name, path, 'Bit') + return _findToolFile("{}.fctb".format(name), path, 'Bit') -def findLibrary(path, dbg=False): - if path.endswith('.fctl'): - return _findTool(path, 'Library', dbg) - return _findTool("{}.fctl".format(path), 'Library', dbg) +def findToolLibrary(name, path=None): + '''findToolLibrary(name, path) ... search for name, if relative path look in path''' + PathLog.track(name, path) + if name.endswith('.fctl'): + return _findToolFile(name, path, 'Library') + return _findToolFile("{}.fctl".format(name), path, 'Library') def _findRelativePath(path, typ): + PathLog.track(path, typ) relative = path for p in PathPreferences.searchPathsTool(typ): if path.startswith(p): @@ -132,48 +132,19 @@ def findRelativePathTool(path): 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) + def __init__(self, obj, shapeFile, path=None): + PathLog.track(obj.Label, shapeFile, path) 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')) - obj.addProperty('App::PropertyString', 'ShapeName', 'Base', - translate('PathToolBit', 'The name of the shape file')) + 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')) + obj.addProperty('App::PropertyString', 'ShapeName', 'Base', translate('PathToolBit', 'The name of the shape file')) + obj.addProperty('App::PropertyStringList', 'BitPropertyNames', 'Base', translate('PathToolBit', 'List of all properties inherited from the bit')) + + if path: + obj.File = path if shapeFile is None: obj.BitShape = 'endmill.fcstd' self._setupBitShape(obj) @@ -193,12 +164,6 @@ class ToolBit(object): 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): # when files are shared it is essential to be able to change/set the shape file, # otherwise the file is hard to use @@ -206,19 +171,39 @@ class ToolBit(object): obj.setEditorMode('BitBody', 2) obj.setEditorMode('File', 1) obj.setEditorMode('Shape', 2) + if not hasattr(obj, 'BitPropertyNames'): + obj.addProperty('App::PropertyStringList', 'BitPropertyNames', 'Base', translate('PathToolBit', 'List of all properties inherited from the bit')) + propNames = [] + for prop in obj.PropertiesList: + if obj.getGroupOfProperty(prop) == 'Bit': + val = obj.getPropertyByName(prop) + typ = obj.getTypeIdOfProperty(prop) + dsc = obj.getDocumentationOfProperty(prop) - 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) + obj.removeProperty(prop) + obj.addProperty(typ, prop, PropertyGroupShape, dsc) + + PathUtil.setProperty(obj, prop, val) + propNames.append(prop) + elif obj.getGroupOfProperty(prop) == 'Attribute': + propNames.append(prop) + obj.BitPropertyNames = propNames + obj.setEditorMode('BitPropertyNames', 2) + + for prop in obj.BitPropertyNames: + if obj.getGroupOfProperty(prop) == PropertyGroupShape: + # properties in the Shape group can only be modified while the actual + # shape is loaded, so we have to disable direct property editing + obj.setEditorMode(prop, 1) + else: + # all other custom properties can and should be edited directly in the + # property editor widget, not much value in re-implementing that + obj.setEditorMode(prop, 0) def onChanged(self, obj, prop): PathLog.track(obj.Label, prop) if prop == 'BitShape' and 'Restore' not 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) @@ -227,12 +212,19 @@ class ToolBit(object): def _updateBitShape(self, obj, properties=None): if obj.BitBody is not 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)) + for attributes in [o for o in obj.BitBody.Group if hasattr(o, 'Proxy') and hasattr(o.Proxy, 'getCustomProperties')]: + for prop in attributes.Proxy.getCustomProperties(): + # the property might not exist in our local object (new attribute in shape) + # for such attributes we just keep the default + if hasattr(obj, prop): + setattr(attributes, prop, obj.getPropertyByName(prop)) + else: + # if the template shape has a new attribute defined we should add that + # to the local object + self._setupProperty(obj, prop, attributes) + propNames = obj.BitPropertyNames + propNames.append(prop) + obj.BitPropertyNames = propNames self._copyBitShape(obj) def _copyBitShape(self, obj): @@ -243,6 +235,7 @@ class ToolBit(object): obj.Shape = Part.Shape() def _loadBitBody(self, obj, path=None): + PathLog.track(obj.Label, path) p = path if path else obj.BitShape docOpened = False doc = None @@ -251,12 +244,15 @@ class ToolBit(object): doc = FreeCAD.getDocument(d) break if doc is None: - p = findShape(p) + p = findToolShape(p, path if path else obj.File) if not path and p != obj.BitShape: obj.BitShape = p + PathLog.debug("ToolBit {} using shape file: {}".format(obj.Label, p)) doc = FreeCAD.openDocument(p, True) obj.ShapeName = doc.Name docOpened = True + else: + PathLog.debug("ToolBit {} already open: {}".format(obj.Label, doc)) return (doc, docOpened) def _removeBitBody(self, obj): @@ -269,7 +265,7 @@ class ToolBit(object): PathLog.track(obj.Label) self._removeBitBody(obj) self._copyBitShape(obj) - for prop in self.propertyNamesBit(obj): + for prop in obj.BitPropertyNames: obj.removeProperty(prop) def loadBitBody(self, obj, force=False): @@ -287,6 +283,20 @@ class ToolBit(object): def unloadBitBody(self, obj): self._removeBitBody(obj) + def _setupProperty(self, obj, prop, orig): + # extract property parameters and values so it can be copied + val = orig.getPropertyByName(prop) + typ = orig.getTypeIdOfProperty(prop) + grp = orig.getGroupOfProperty(prop) + dsc = orig.getDocumentationOfProperty(prop) + + obj.addProperty(typ, prop, grp, dsc) + if 'App::PropertyEnumeration' == typ: + setattr(obj, prop, orig.getEnumerationsOfProperty(prop)) + + obj.setEditorMode(prop, 1) + PathUtil.setProperty(obj, prop, val) + def _setupBitShape(self, obj, path=None): PathLog.track(obj.Label) @@ -296,6 +306,8 @@ class ToolBit(object): obj.Label = doc.RootObjects[0].Label self._deleteBitSetup(obj) bitBody = obj.Document.copyObject(doc.RootObjects[0], True) + + docName = doc.Name if docOpened: FreeCAD.setActiveDocument(activeDoc.Name) FreeCAD.closeDocument(doc.Name) @@ -303,29 +315,44 @@ class ToolBit(object): if bitBody.ViewObject: bitBody.ViewObject.Visibility = False - for sketch in [o for o in 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) + PathLog.debug("bitBody.{} ({}): {}".format(bitBody.Label, bitBody.Name, type(bitBody))) + + propNames = [] + for attributes in [o for o in bitBody.Group if PathPropertyBag.IsPropertyBag(o)]: + PathLog.debug("Process properties from {}".format(attributes.Label)) + for prop in attributes.Proxy.getCustomProperties(): + self._setupProperty(obj, prop, attributes) + propNames.append(prop) + if not propNames: + PathLog.error(translate('PathToolBit', 'Did not find a PropertyBag in {} - not a ToolBit shape?').format(docName)) + # has to happen last because it could trigger op.execute evaluations + obj.BitPropertyNames = propNames obj.BitBody = bitBody self._copyBitShape(obj) + def toolShapeProperties(self, obj): + '''toolShapeProperties(obj) ... return all properties defining it's shape''' + return sorted([prop for prop in obj.BitPropertyNames if obj.getGroupOfProperty(prop) == PropertyGroupShape]) + + def toolAdditionalProperties(self, obj): + '''toolShapeProperties(obj) ... return all properties unrelated to it's shape''' + return sorted([prop for prop in obj.BitPropertyNames if obj.getGroupOfProperty(prop) != PropertyGroupShape]) + + def toolGroupsAndProperties(self, obj, includeShape=True): + '''toolGroupsAndProperties(obj) ... returns a dictionary of group names with a list of property names.''' + category = {} + for prop in obj.BitPropertyNames: + group = obj.getGroupOfProperty(prop) + if includeShape or group != PropertyGroupShape: + properties = category.get(group, []) + properties.append(prop) + category[group] = properties + return category + def getBitThumbnail(self, obj): if obj.BitShape: - path = findShape(obj.BitShape) + path = findToolShape(obj.BitShape) if path: with open(path, 'rb') as fd: try: @@ -347,8 +374,7 @@ class ToolBit(object): obj.File = path return True except (OSError, IOError) as e: - PathLog.error("Could not save tool {} to {} ({})".format( - obj.Label, path, e)) + PathLog.error("Could not save tool {} to {} ({})".format(obj.Label, path, e)) raise def templateAttrs(self, obj): @@ -360,16 +386,10 @@ class ToolBit(object): else: attrs['shape'] = findRelativePathShape(obj.BitShape) params = {} - for name in self.propertyNamesBit(obj): + for name in obj.BitPropertyNames: params[name] = PathUtil.getPropertyValueString(obj, name) attrs['parameter'] = params params = {} - for name in self.propertyNamesAttribute(obj): - if name == "UserAttributes": - for key, value in obj.UserAttributes.items(): - params[key] = value - else: - params[name] = PathUtil.getPropertyValueString(obj, name) attrs['attribute'] = params return attrs @@ -380,73 +400,33 @@ def Declaration(path): 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')) - self.addProperty('App::PropertyMap', 'UserAttributes', - PropertyGroupAttribute, translate('PathToolBit', - 'User Defined Values')) - self.addProperty('App::PropertyBool', 'SpindlePower', - PropertyGroupAttribute, translate('PathToolBit', - 'Whether Spindle Power should be allowed')) - - class ToolBitFactory(object): - def CreateFromAttrs(self, attrs, name='ToolBit'): - PathLog.debug(attrs) - obj = Factory.Create(name, attrs['shape']) + def CreateFromAttrs(self, attrs, name='ToolBit', path=None): + PathLog.track(attrs, path) + obj = Factory.Create(name, attrs['shape'], path) 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() - uservals = {} - for pname in params: - try: - prop = proto.getProperty(pname) - prop.setupProperty(obj, pname, PropertyGroupAttribute, prop.valueFromString(params[pname])) - except Exception: - prop = proto.getProperty("UserAttributes") - uservals.update({pname: params[pname]}) - - if len(uservals.items()) > 0: - prop.setupProperty(obj, "UserAttributes", - PropertyGroupAttribute, uservals) - return obj def CreateFrom(self, path, name='ToolBit'): + PathLog.track(name, path) try: data = Declaration(path) - bit = Factory.CreateFromAttrs(data, name) - bit.File = path + bit = Factory.CreateFromAttrs(data, name, 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): + def Create(self, name='ToolBit', shapeFile=None, path=None): + PathLog.track(name, shapeFile, path) obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name) - obj.Proxy = ToolBit(obj, shapeFile) + obj.Proxy = ToolBit(obj, shapeFile, path) return obj diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index 3488bcc95d..3028bd7bb1 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -20,12 +20,14 @@ # * * # *************************************************************************** +import FreeCAD import FreeCADGui import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog import PathScripts.PathPreferences as PathPreferences -import PathScripts.PathSetupSheetGui as PathSetupSheetGui +import PathScripts.PathPropertyEditor as PathPropertyEditor import PathScripts.PathToolBit as PathToolBit +import PathScripts.PathUtil as PathUtil import os import re @@ -39,6 +41,31 @@ PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) +class _Delegate(QtGui.QStyledItemDelegate): + '''Handles the creation of an appropriate editing widget for a given property.''' + ObjectRole = QtCore.Qt.UserRole + 1 + PropertyRole = QtCore.Qt.UserRole + 2 + EditorRole = QtCore.Qt.UserRole + 3 + + def createEditor(self, parent, option, index): + editor = index.data(self.EditorRole) + if editor is None: + obj = index.data(self.ObjectRole) + prp = index.data(self.PropertyRole) + editor = PathPropertyEditor.Editor(obj, prp) + index.model().setData(index, editor, self.EditorRole) + return editor.widget(parent) + + def setEditorData(self, widget, index): + # called to update the widget with the current data + index.data(self.EditorRole).setEditorData(widget) + + def setModelData(self, widget, model, index): + # called to update the model with the data from the widget + editor = index.data(self.EditorRole) + editor.setModelData(widget) + index.model().setData(index, PathUtil.getPropertyValueString(editor.obj, editor.prop), QtCore.Qt.DisplayRole) + class ToolBitEditor(object): '''UI and controller for editing a ToolBit. @@ -84,32 +111,35 @@ class ToolBitEditor(object): layout = self.form.bitParams.layout() ui = FreeCADGui.UiLoader() - nr = 0 # for all properties either assign them to existing labels and editors # or create additional ones for them if not enough have already been # created. - for name in tool.PropertiesList: - if tool.getGroupOfProperty(name) == PathToolBit.PropertyGroupBit: - if nr < len(self.widgets): - PathLog.debug("re-use row: {} [{}]".format(nr, name)) - label, qsb, editor = self.widgets[nr] - label.setText(labelText(name)) - editor.attachTo(tool, name) - label.show() - qsb.show() - else: - qsb = ui.createWidget('Gui::QuantitySpinBox') - editor = PathGui.QuantitySpinBox(qsb, tool, name) - label = QtGui.QLabel(labelText(name)) - self.widgets.append((label, qsb, editor)) - PathLog.debug("create row: {} [{}]".format(nr, name)) - if nr >= layout.rowCount(): - layout.addRow(label, qsb) - nr = nr + 1 + usedRows = 0 + for nr, name in enumerate(tool.Proxy.toolShapeProperties(tool)): + if nr < len(self.widgets): + PathLog.debug("re-use row: {} [{}]".format(nr, name)) + label, qsb, editor = self.widgets[nr] + label.setText(labelText(name)) + editor.attachTo(tool, name) + label.show() + qsb.show() + else: + qsb = ui.createWidget('Gui::QuantitySpinBox') + editor = PathGui.QuantitySpinBox(qsb, tool, name) + label = QtGui.QLabel(labelText(name)) + self.widgets.append((label, qsb, editor)) + PathLog.debug("create row: {} [{}] {}".format(nr, name, type(qsb))) + if hasattr(qsb, 'editingFinished'): + qsb.editingFinished.connect(self.updateTool) + + if nr >= layout.rowCount(): + layout.addRow(label, qsb) + usedRows = usedRows + 1 # hide all rows which aren't being used - for i in range(nr, len(self.widgets)): + PathLog.track(usedRows, len(self.widgets)) + for i in range(usedRows, len(self.widgets)): label, qsb, editor = self.widgets[i] label.hide() qsb.hide() @@ -124,98 +154,48 @@ class ToolBitEditor(object): def setupAttributes(self, tool): PathLog.track() - self.proto = PathToolBit.AttributePrototype() - self.props = sorted(self.proto.properties) - self.delegate = PathSetupSheetGui.Delegate(self.form) - self.model = QtGui.QStandardItemModel(len(self.props)-1, 3, self.form) - self.model.setHorizontalHeaderLabels(['Set', 'Property', 'Value']) - for i, name in enumerate(self.props): - PathLog.debug("propname: %s " % name) + setup = True + if not hasattr(self, 'delegate'): + self.delegate = _Delegate(self.form.attrTree) + self.model = QtGui.QStandardItemModel(self.form.attrTree) + self.model.setHorizontalHeaderLabels(['Property', 'Value']) + else: + self.model.removeRows(0, self.model.rowCount()) + setup = False - prop = self.proto.getProperty(name) - isset = hasattr(tool, name) + attributes = tool.Proxy.toolGroupsAndProperties(tool, False) + for name in attributes: + group = QtGui.QStandardItem() + group.setData(name, QtCore.Qt.EditRole) + group.setEditable(False) + for prop in attributes[name]: + label = QtGui.QStandardItem() + label.setData(prop, QtCore.Qt.EditRole) + label.setEditable(False) - if isset: - prop.setValue(getattr(tool, name)) + value = QtGui.QStandardItem() + value.setData(PathUtil.getPropertyValueString(tool, prop), QtCore.Qt.DisplayRole) + value.setData(tool, _Delegate.ObjectRole) + value.setData(prop, _Delegate.PropertyRole) - if name == "UserAttributes": - continue + group.appendRow([label, value]) + self.model.appendRow(group) - else: - 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) - - if hasattr(tool, "UserAttributes"): - for key, value in tool.UserAttributes.items(): - PathLog.debug(key, value) - c1 = QtGui.QStandardItem() - c1.setCheckable(False) - c1.setEditable(False) - c1.setCheckState(QtCore.Qt.CheckState.Checked) - - c1.setText('') - c2 = QtGui.QStandardItem(key) - c2.setEditable(False) - c3 = QtGui.QStandardItem(value) - c3.setEditable(False) - - self.model.appendRow([c1, c2, c3]) - - 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): - PathLog.track() - 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) + if setup: + self.form.attrTree.setModel(self.model) + self.form.attrTree.setItemDelegateForColumn(1, self.delegate) + self.form.attrTree.expandAll() + self.form.attrTree.resizeColumnToContents(0) + self.form.attrTree.resizeColumnToContents(1) + #self.form.attrTree.collapseAll() def accept(self): PathLog.track() self.refresh() self.tool.Proxy.unloadBitBody(self.tool) - # get the attributes - for i, name in enumerate(self.props): - PathLog.debug('in accept: {}'.format(name)) - prop = self.proto.getProperty(name) - if self.model.item(i, 0) is not None: - 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): PathLog.track() self.tool.Proxy.unloadBitBody(self.tool) @@ -228,27 +208,45 @@ class ToolBitEditor(object): for lbl, qsb, editor in self.widgets: editor.updateSpinBox() + def _updateBitShape(self, shapePath): + # Only need to go through this exercise if the shape actually changed. + if self.tool.BitShape != shapePath: + # Before setting a new bitshape we need to make sure that none of + # editors fires an event and tries to access its old property, which + # might not exist anymore. + for lbl, qsb, editor in self.widgets: + editor.attachTo(self.tool, 'File') + self.tool.BitShape = shapePath + self.setupTool(self.tool) + self.form.toolName.setText(self.tool.Label) + if self.tool.BitBody and self.tool.BitBody.ViewObject: + if not self.tool.BitBody.ViewObject.Visibility: + self.tool.BitBody.ViewObject.Visibility = True + self.setupAttributes(self.tool) + return True + return False + def updateShape(self): PathLog.track() shapePath = str(self.form.shapePath.text()) # Only need to go through this exercise if the shape actually changed. - if self.tool.BitShape != shapePath: - self.tool.BitShape = shapePath - self.setupTool(self.tool) - self.form.toolName.setText(self.tool.Label) - + if self._updateBitShape(shapePath): for lbl, qsb, editor in self.widgets: editor.updateSpinBox() def updateTool(self): PathLog.track() - self.tool.Label = str(self.form.toolName.text()) - self.tool.BitShape = str(self.form.shapePath.text()) + + label = str(self.form.toolName.text()) + shape = str(self.form.shapePath.text()) + if self.tool.Label != label: + self.tool.Label = label + self._updateBitShape(shape) for lbl, qsb, editor in self.widgets: editor.updateProperty() - # self.tool.Proxy._updateBitShape(self.tool) + self.tool.Proxy._updateBitShape(self.tool) def refresh(self): PathLog.track() @@ -262,10 +260,7 @@ class ToolBitEditor(object): path = self.tool.BitShape if not path: path = PathPreferences.lastPathToolShape() - foo = QtGui.QFileDialog.getOpenFileName(self.form, - "Path - Tool Shape", - path, - "*.fcstd") + 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]) diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 1fe1df5370..1bf56aea68 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -170,13 +170,13 @@ class TaskPanel: class ToolBitGuiFactory(PathToolBit.ToolBitFactory): - def Create(self, name='ToolBit', shapeFile=None): + def Create(self, name='ToolBit', shapeFile=None, path=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) + PathLog.track(name, shapeFile, path) + FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit')) + tool = PathToolBit.ToolBitFactory.Create(self, name, shapeFile, path) PathIconViewProvider.Attach(tool.ViewObject, name) FreeCAD.ActiveDocument.commitTransaction() return tool @@ -247,7 +247,8 @@ def GetToolShapeFile(parent=None): location, '*.fcstd') if fname and fname[0]: if fname != location: - PathPreferences.setLastPathToolShape(location) + newloc = os.path.dirname(fname[0]) + PathPreferences.setLastPathToolShape(newloc) return fname[0] else: return None diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index 33e8fb8e39..621ad0e996 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -24,23 +24,25 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathLog as PathLog import PathScripts.PathPreferences as PathPreferences import PathScripts.PathToolBit as PathToolBit -import PathScripts.PathToolBitGui as PathToolBitGui import PathScripts.PathToolBitEdit as PathToolBitEdit +import PathScripts.PathToolBitGui as PathToolBitGui import PathScripts.PathToolControllerGui as PathToolControllerGui import PathScripts.PathUtilsGui as PathUtilsGui -from PySide import QtCore, QtGui import PySide +import glob import json import os -import glob -import uuid as UUID -from functools import partial import shutil +import uuid as UUID -# PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +from functools import partial + + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) # PathLog.trackModule(PathLog.thisModule()) _UuidRole = PySide.QtCore.Qt.UserRole + 1 @@ -50,6 +52,75 @@ _PathRole = PySide.QtCore.Qt.UserRole + 2 def translate(context, text, disambig=None): return PySide.QtCore.QCoreApplication.translate(context, text, disambig) +def checkWorkingDir(): + # users shouldn't use the example toolbits and libraries. + # working directory should be writable + PathLog.track() + + workingdir = os.path.dirname(PathPreferences.lastPathToolLibrary()) + defaultdir = os.path.dirname(PathPreferences.pathDefaultToolsPath()) + + PathLog.debug('workingdir: {} defaultdir: {}'.format(workingdir, defaultdir)) + + dirOK = lambda : workingdir != defaultdir and (os.access(workingdir, os.W_OK)) + + if dirOK(): + return True + + qm = PySide.QtGui.QMessageBox + ret = qm.question(None,'', "Toolbit working directory not set up. Do that now?", qm.Yes | qm.No) + + if ret == qm.No: + return False + + msg = translate("Path", "Choose a writable location for your toolbits", None) + while not dirOK(): + workingdir = PySide.QtGui.QFileDialog.getExistingDirectory(None, msg, + PathPreferences.filePath()) + + if workingdir[-8:] == os.path.sep + 'Library': + workingdir = workingdir[:-8] # trim off trailing /Library if user chose it + + PathPreferences.setLastPathToolLibrary("{}{}Library".format(workingdir, os.path.sep)) + PathPreferences.setLastPathToolBit("{}{}Bit".format(workingdir, os.path.sep)) + PathLog.debug('setting workingdir to: {}'.format(workingdir)) + + subdirlist = ['Bit', 'Library', 'Shape'] + mode = 0o777 + for dir in subdirlist: + subdir = "{}{}{}".format(workingdir, os.path.sep, dir) + if os.path.exists(subdir): + subdirlist.remove(dir) + + if len(subdirlist) >= 1: + needed = ', '.join([str(d) for d in subdirlist]) + qm = PySide.QtGui.QMessageBox + ret = qm.question(None,'', "Toolbit Working directory {} needs these sudirectories:\n {} \n Create them?".format(workingdir, needed), qm.Yes | qm.No) + + if ret == qm.No: + return False + else: + for dir in subdirlist: + subdir = "{}{}{}".format(workingdir, os.path.sep, dir) + os.mkdir(subdir, mode) + if dir != 'Shape': + qm = PySide.QtGui.QMessageBox + ret = qm.question(None,'', "Copy example files to new {} directory?".format(dir), qm.Yes | qm.No) + if ret == qm.Yes: + src="{}{}{}".format(defaultdir, os.path.sep, dir) + src_files = os.listdir(src) + for file_name in src_files: + full_file_name = os.path.join(src, file_name) + if os.path.isfile(full_file_name): + shutil.copy(full_file_name, subdir) + + + # if no library is set, choose the first one in the Library directory + if PathPreferences.lastFileToolLibrary() is None: + libFiles = [f for f in glob.glob(PathPreferences.lastPathToolLibrary() + os.path.sep + '*.fctl')] + PathPreferences.setLastFileToolLibrary(libFiles[0]) + + return True class _TableView(PySide.QtGui.QTableView): '''Subclass of QTableView to support rearrange and copying of ToolBits''' @@ -141,7 +212,7 @@ class ModelFactory(object): for toolBit in library['tools']: try: nr = toolBit['nr'] - bit = PathToolBit.findBit(toolBit['path']) + bit = PathToolBit.findToolBit(toolBit['path'], path) if bit: PathLog.track(bit) tool = PathToolBit.Declaration(bit) @@ -203,15 +274,15 @@ class ModelFactory(object): path = PathPreferences.lastPathToolLibrary() if os.path.isdir(path): # opening all tables in a directory - libFiles = [f for f in glob.glob(path + '/*.fctl')] + libFiles = [f for f in glob.glob(path + os.path.sep + '*.fctl')] libFiles.sort() for libFile in libFiles: loc, fnlong = os.path.split(libFile) fn, ext = os.path.splitext(fnlong) - libItem = QtGui.QStandardItem(fn) + libItem = PySide.QtGui.QStandardItem(fn) libItem.setToolTip(loc) libItem.setData(libFile, _PathRole) - libItem.setIcon(QtGui.QPixmap(':/icons/Path_ToolTable.svg')) + libItem.setIcon(PySide.QtGui.QPixmap(':/icons/Path_ToolTable.svg')) model.appendRow(libItem) PathLog.debug('model rows: {}'.format(model.rowCount())) @@ -241,6 +312,7 @@ class ToolBitSelector(object): '''Controller for displaying a library and creating ToolControllers''' def __init__(self): + checkWorkingDir() self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitSelector.ui') self.factory = ModelFactory() self.toolModel = PySide.QtGui.QStandardItemModel(0, len(self.columnNames())) @@ -250,20 +322,20 @@ class ToolBitSelector(object): def columnNames(self): return ['#', 'Tool'] - def curLib(self): + def currentLibrary(self, shortNameOnly): libfile = PathPreferences.lastFileToolLibrary() if libfile is None or libfile == "": return "" - else: - libfile = os.path.split(PathPreferences.lastFileToolLibrary())[1] - libfile = os.path.splitext(libfile)[0] + elif shortNameOnly: + return os.path.splitext(os.path.basename(libfile))[0] return libfile def loadData(self): PathLog.track() self.toolModel.clear() self.toolModel.setHorizontalHeaderLabels(self.columnNames()) - self.form.lblLibrary.setText(self.curLib()) + self.form.lblLibrary.setText(self.currentLibrary(True)) + self.form.lblLibrary.setToolTip(self.currentLibrary(False)) self.factory.libraryOpen(self.toolModel) self.toolModel.takeColumn(3) self.toolModel.takeColumn(2) @@ -271,7 +343,6 @@ class ToolBitSelector(object): def setupUI(self): PathLog.track() self.loadData() - self.form.tools.setModel(self.toolModel) self.form.tools.selectionModel().selectionChanged.connect(self.enableButtons) self.form.tools.doubleClicked.connect(partial(self.selectedOrAllToolControllers)) @@ -340,7 +411,7 @@ class ToolBitSelector(object): def open(self, path=None): ''' load library stored in path and bring up ui''' - docs = FreeCADGui.getMainWindow().findChildren(QtGui.QDockWidget) + docs = FreeCADGui.getMainWindow().findChildren(PySide.QtGui.QDockWidget) for doc in docs: if doc.objectName() == "ToolSelector": if doc.isVisible(): @@ -351,7 +422,7 @@ class ToolBitSelector(object): return mw = FreeCADGui.getMainWindow() - mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.form, + mw.addDockWidget(PySide.QtCore.Qt.RightDockWidgetArea, self.form, PySide.QtCore.Qt.Orientation.Vertical) @@ -361,9 +432,7 @@ class ToolBitLibrary(object): def __init__(self): PathLog.track() - if not self.checkWorkingDir(): - return - + checkWorkingDir() self.factory = ModelFactory() self.temptool = None self.toolModel = PySide.QtGui.QStandardItemModel(0, len(self.columnNames())) @@ -375,55 +444,6 @@ class ToolBitLibrary(object): self.setupUI() self.title = self.form.windowTitle() - def checkWorkingDir(self): - # users shouldn't use the example toolbits and libraries. - # working directory should be writable - PathLog.track() - - workingdir = os.path.dirname(PathPreferences.lastPathToolLibrary()) - defaultdir = os.path.dirname(PathPreferences.pathDefaultToolsPath()) - - dirOK = lambda : workingdir != defaultdir and (os.access(workingdir, os.W_OK)) - - if dirOK(): - return True - - qm = PySide.QtGui.QMessageBox - ret = qm.question(None,'', "Toolbit working directory not set up. Do that now?", qm.Yes | qm.No) - - if ret == qm.No: - return False - - msg = translate("Path", "Choose a writable location for your toolbits", None) - while not dirOK(): - workingdir = PySide.QtGui.QFileDialog.getExistingDirectory(None, msg, - PathPreferences.filePath()) - - PathPreferences.setLastPathToolLibrary("{}/Library".format(workingdir)) - - subdirlist = ['Bit', 'Library', 'Shape'] - mode = 0o777 - for dir in subdirlist: - subdir = "{}/{}".format(workingdir, dir) - if not os.path.exists(subdir): - qm = PySide.QtGui.QMessageBox - ret = qm.question(None,'', "Toolbit Working directory {} should contain a '{}' subdirectory. Create it?".format(workingdir, dir), qm.Yes | qm.No) - - if ret == qm.Yes: - os.mkdir(subdir, mode) - qm = PySide.QtGui.QMessageBox - ret = qm.question(None,'', "Copy example files to new {} directory?".format(dir), qm.Yes | qm.No) - if ret == qm.Yes: - src="{}/{}".format(defaultdir, dir) - src_files = os.listdir(src) - for file_name in src_files: - full_file_name = os.path.join(src, file_name) - if os.path.isfile(full_file_name): - shutil.copy(full_file_name, subdir) - - return True - - def toolBitNew(self): PathLog.track() @@ -439,7 +459,7 @@ class ToolBitLibrary(object): # Parse out the name of the file and write the structure loc, fil = os.path.split(filename) fname = os.path.splitext(fil)[0] - fullpath = "{}/{}.fctb".format(loc, fname) + fullpath = "{}{}{}.fctb".format(loc, os.path.sep, fname) PathLog.debug(fullpath) self.temptool = PathToolBit.ToolBitFactory().Create(name=fname) @@ -464,7 +484,7 @@ class ToolBitLibrary(object): loc, fil = os.path.split(f) fname = os.path.splitext(fil)[0] - fullpath = "{}/{}.fctb".format(loc, fname) + fullpath = "{}{}{}.fctb".format(loc, os.path.sep, fname) self.factory.newTool(self.toolModel, fullpath) @@ -557,8 +577,8 @@ class ToolBitLibrary(object): self.temptool = PathToolBit.ToolBitFactory().CreateFrom(tbpath, 'temptool') self.editor = PathToolBitEdit.ToolBitEditor(self.temptool, self.form.toolTableGroup, loadBitBody=False) - QBtn = QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel - buttonBox = QtGui.QDialogButtonBox(QBtn) + QBtn = PySide.QtGui.QDialogButtonBox.Ok | PySide.QtGui.QDialogButtonBox.Cancel + buttonBox = PySide.QtGui.QDialogButtonBox(QBtn) buttonBox.accepted.connect(self.accept) buttonBox.rejected.connect(self.reject) @@ -604,8 +624,9 @@ class ToolBitLibrary(object): 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) + if self.path is not None: + with open(self.path, 'w') as fp: + json.dump(library, fp, sort_keys=True, indent=2) def libraryOk(self): self.librarySave() @@ -652,7 +673,7 @@ class ToolBitLibrary(object): if curIndex: sm = self.form.TableList.selectionModel() - sm.select(curIndex, QtCore.QItemSelectionModel.Select) + sm.select(curIndex, PySide.QtCore.QItemSelectionModel.Select) self.toolTableView.setUpdatesEnabled(True) self.form.TableList.setUpdatesEnabled(True) diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 199d5cdf22..3f8405ccd3 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -250,7 +250,8 @@ def Create(name='TC: Default Tool', tool=None, toolNumber=1, assignViewProvider= if tool.ViewObject: tool.ViewObject.Visibility = False - obj.Tool = tool + if tool: + obj.Tool = tool obj.ToolNumber = toolNumber return obj @@ -260,7 +261,7 @@ def FromTemplate(template, assignViewProvider=True): PathLog.track() name = template.get(ToolControllerTemplate.Name, ToolControllerTemplate.Label) - obj = Create(name, assignViewProvider=True) + obj = Create(name, tool=False, 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 33e735a08d..905888026e 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -22,6 +22,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog diff --git a/src/Mod/Path/PathScripts/PathUtil.py b/src/Mod/Path/PathScripts/PathUtil.py index 40e59a78af..db43b466e5 100644 --- a/src/Mod/Path/PathScripts/PathUtil.py +++ b/src/Mod/Path/PathScripts/PathUtil.py @@ -71,6 +71,11 @@ def getPropertyValueString(obj, prop): 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 not attr is None and type(value) == str: + if type(attr) == int: + value = int(value, 0) + elif type(attr) == bool: + value = value.lower() in ['true', '1', 'yes', 'ok'] if o and name: setattr(o, name, value) diff --git a/src/Mod/Path/PathScripts/PathUtilsGui.py b/src/Mod/Path/PathScripts/PathUtilsGui.py index da33987017..a5df387eb3 100644 --- a/src/Mod/Path/PathScripts/PathUtilsGui.py +++ b/src/Mod/Path/PathScripts/PathUtilsGui.py @@ -21,6 +21,7 @@ # *************************************************************************** import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts import PathScripts.PathJobCmd as PathJobCmd import PathScripts.PathUtils as PathUtils diff --git a/src/Mod/Path/PathScripts/PathVcarveGui.py b/src/Mod/Path/PathScripts/PathVcarveGui.py index e77dab4b7f..286cce9b28 100644 --- a/src/Mod/Path/PathScripts/PathVcarveGui.py +++ b/src/Mod/Path/PathScripts/PathVcarveGui.py @@ -23,6 +23,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathVcarve as PathVcarve import PathScripts.PathLog as PathLog import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py index 07958536fd..089e15256a 100644 --- a/src/Mod/Path/PathScripts/PathWaterlineGui.py +++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py @@ -24,6 +24,7 @@ import FreeCAD import FreeCADGui +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathWaterline as PathWaterline import PathScripts.PathGui as PathGui import PathScripts.PathOpGui as PathOpGui diff --git a/src/Mod/Path/PathTests/TestPathPropertyBag.py b/src/Mod/Path/PathTests/TestPathPropertyBag.py new file mode 100644 index 0000000000..974ca3bd6c --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathPropertyBag.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2021 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 PathScripts.PathPropertyBag as PathPropertyBag +import PathTests.PathTestUtils as PathTestUtils + +class TestPathPropertyBag(PathTestUtils.PathTestBase): + + def setUp(self): + self.doc = FreeCAD.newDocument('test-property-bag') + + def tearDown(self): + FreeCAD.closeDocument(self.doc.Name) + + def test00(self): + '''basic PropertyBag creation and access test''' + bag = PathPropertyBag.Create() + self.assertTrue(hasattr(bag, 'Proxy')) + self.assertEqual(bag.Proxy.getCustomProperties(), []) + self.assertEqual(bag.CustomPropertyGroups, []) + + def test01(self): + '''adding properties to a PropertyBag is tracked properly''' + bag = PathPropertyBag.Create() + proxy = bag.Proxy + proxy.addCustomProperty('App::PropertyString', 'Title', 'Address', 'Some description') + self.assertTrue(hasattr(bag, 'Title')) + bag.Title = 'Madame' + self.assertEqual(bag.Title, 'Madame') + self.assertEqual(bag.Proxy.getCustomProperties(), ['Title']) + self.assertEqual(bag.CustomPropertyGroups, ['Address']) + + def test02(self): + '''refreshCustomPropertyGroups deletes empty groups''' + bag = PathPropertyBag.Create() + proxy = bag.Proxy + proxy.addCustomProperty('App::PropertyString', 'Title', 'Address', 'Some description') + bag.Title = 'Madame' + bag.removeProperty('Title') + proxy.refreshCustomPropertyGroups() + self.assertEqual(bag.Proxy.getCustomProperties(), []) + self.assertEqual(bag.CustomPropertyGroups, []) + + def test03(self): + '''refreshCustomPropertyGroups does not delete non-empty groups''' + bag = PathPropertyBag.Create() + proxy = bag.Proxy + proxy.addCustomProperty('App::PropertyString', 'Title', 'Address', 'Some description') + proxy.addCustomProperty('App::PropertyString', 'Gender', 'Attributes') + bag.Title = 'Madame' + bag.Gender = 'Female' + bag.removeProperty('Gender') + proxy.refreshCustomPropertyGroups() + self.assertEqual(bag.Proxy.getCustomProperties(), ['Title']) + self.assertEqual(bag.CustomPropertyGroups, ['Address']) + diff --git a/src/Mod/Path/PathTests/TestPathToolBit.py b/src/Mod/Path/PathTests/TestPathToolBit.py index dd82c7fc63..7a684b3697 100644 --- a/src/Mod/Path/PathTests/TestPathToolBit.py +++ b/src/Mod/Path/PathTests/TestPathToolBit.py @@ -22,37 +22,143 @@ import PathScripts.PathToolBit as PathToolBit import PathTests.PathTestUtils as PathTestUtils +import glob +import os +TestToolDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'Tools') +TestInvalidDir = os.path.join(TestToolDir, 'some', 'silly', 'path', 'that', 'should', 'not', 'exist') + +TestToolBitName = 'test-path-tool-bit-bit-00.fctb' +TestToolShapeName = 'test-path-tool-bit-shape-00.fcstd' +TestToolLibraryName = 'test-path-tool-bit-library-00.fctl' + +def testToolShape(path = TestToolDir, name = TestToolShapeName): + return os.path.join(path, 'Shape', name) + +def testToolBit(path = TestToolDir, name = TestToolBitName): + return os.path.join(path, 'Bit', name) + +def testToolLibrary(path = TestToolDir, name = TestToolLibraryName): + return os.path.join(path, 'Library', name) + +def printTree(path, indent): + print("{} {}".format(indent, os.path.basename(path))) + if os.path.isdir(path): + if os.path.basename(path).startswith('__'): + print("{} ...".format(indent)) + else: + for foo in sorted(glob.glob(os.path.join(path, '*'))): + printTree(foo, "{} ".format(indent)) class TestPathToolBit(PathTestUtils.PathTestBase): - def test00(self): - '''Find a tool shapee from file name''' + def test(self): + '''Log test setup directory structure''' + # Enable this test if there are errors showing up in the build system with the + # paths that work OK locally. It'll print out the directory tree, and if it + # doesn't look right you know where to look for it + print() + print("realpath : {}".format(os.path.realpath(__file__))) + print(" Tools : {}".format(TestToolDir)) + print(" dir : {}".format(os.path.dirname(os.path.realpath(__file__)))) + printTree(os.path.dirname(os.path.realpath(__file__)), " :") - path = PathToolBit.findShape('endmill.fcstd') + def test00(self): + '''Find a tool shape from file name''' + path = PathToolBit.findToolShape('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') + def test01(self): + '''Not find a relative path shape if not stored in default location''' + path = PathToolBit.findToolShape(TestToolShapeName) + self.assertIsNone(path) + + + def test02(self): + '''Find a relative path shape if it's local to a bit path''' + path = PathToolBit.findToolShape(TestToolShapeName, testToolBit()) self.assertIsNot(path, None) - self.assertNotEqual(path, '/this/is/unlikely/a/valid/path/v-bit.fcstd') + self.assertEqual(path, testToolShape()) + + + def test03(self): + '''Not find a tool shape from an invalid absolute path.''' + path = PathToolBit.findToolShape(testToolShape(TestInvalidDir)) + self.assertIsNone(path) + + + def test04(self): + '''Find a tool shape from a valid absolute path.''' + path = PathToolBit.findToolShape(testToolShape()) + self.assertIsNot(path, None) + self.assertEqual(path, testToolShape()) def test10(self): - '''find the relative path of a tool bit''' - shape = 'endmill.fcstd' - path = PathToolBit.findShape(shape) + '''Find a tool bit from file name''' + path = PathToolBit.findToolBit('5mm_Endmill.fctb') self.assertIsNot(path, None) - self.assertGreater(len(path), len(shape)) - rel = PathToolBit.findRelativePathShape(path) - self.assertEqual(rel, shape) + self.assertNotEqual(path, '5mm_Endmill.fctb') + 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) + '''Not find a relative path bit if not stored in default location''' + path = PathToolBit.findToolBit(TestToolBitName) + self.assertIsNone(path) + + + def test12(self): + '''Find a relative path bit if it's local to a library path''' + path = PathToolBit.findToolBit(TestToolBitName, testToolLibrary()) + self.assertIsNot(path, None) + self.assertEqual(path, testToolBit()) + + + def test13(self): + '''Not find a tool bit from an invalid absolute path.''' + path = PathToolBit.findToolBit(testToolBit(TestInvalidDir)) + self.assertIsNone(path) + + + def test14(self): + '''Find a tool bit from a valid absolute path.''' + path = PathToolBit.findToolBit(testToolBit()) + self.assertIsNot(path, None) + self.assertEqual(path, testToolBit()) + + + + def test20(self): + '''Find a tool library from file name''' + path = PathToolBit.findToolLibrary('Default.fctl') + self.assertIsNot(path, None) + self.assertNotEqual(path, 'Default.fctl') + + + def test21(self): + '''Not find a relative path library if not stored in default location''' + path = PathToolBit.findToolLibrary(TestToolLibraryName) + self.assertIsNone(path) + + + def test22(self): + '''[skipped] Find a relative path library if it's local to ''' + # this is not a valid test for libraries because t + self.assertTrue(True) + + + def test23(self): + '''Not find a tool library from an invalid absolute path.''' + path = PathToolBit.findToolLibrary(testToolLibrary(TestInvalidDir)) + self.assertIsNone(path) + + + def test24(self): + '''Find a tool library from a valid absolute path.''' + path = PathToolBit.findToolBit(testToolBit()) + self.assertIsNot(path, None) + self.assertEqual(path, testToolBit()) + diff --git a/src/Mod/Path/PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb b/src/Mod/Path/PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb new file mode 100644 index 0000000000..7cc72ba33c --- /dev/null +++ b/src/Mod/Path/PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb @@ -0,0 +1,12 @@ +{ + "version": 2, + "name": "5mm Endmill", + "shape": "endmill.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.0000 mm", + "Diameter": "5.0000 mm", + "Length": "50.0000 mm", + "ShankDiameter": "3.0000 mm" + }, + "attribute": {} +} diff --git a/src/Mod/Path/PathTests/Tools/Library/test-path-tool-bit-library-00.fctl b/src/Mod/Path/PathTests/Tools/Library/test-path-tool-bit-library-00.fctl new file mode 100644 index 0000000000..60de98e08e --- /dev/null +++ b/src/Mod/Path/PathTests/Tools/Library/test-path-tool-bit-library-00.fctl @@ -0,0 +1,41 @@ +{ + "tools": [ + { + "nr": 1, + "path": "5mm_Endmill.fctb" + }, + { + "nr": 2, + "path": "5mm_Drill.fctb" + }, + { + "nr": 3, + "path": "6mm_Ball_End.fctb" + }, + { + "nr": 4, + "path": "6mm_Bullnose.fctb" + }, + { + "nr": 5, + "path": "60degree_Vbit.fctb" + }, + { + "nr": 6, + "path": "45degree_chamfer.fctb" + }, + { + "nr": 7, + "path": "slittingsaw.fctb" + }, + { + "nr": 8, + "path": "probe.fctb" + }, + { + "nr": 9, + "path": "5mm-thread-cutter.fctb" + } + ], + "version": 1 +} diff --git a/src/Mod/Path/PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd b/src/Mod/Path/PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd new file mode 100644 index 0000000000..5b5a76dc41 Binary files /dev/null and b/src/Mod/Path/PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd differ diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 0adebedfc6..0be621b040 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -24,6 +24,7 @@ import TestApp from PathTests.TestPathLog import TestPathLog from PathTests.TestPathPreferences import TestPathPreferences +from PathTests.TestPathPropertyBag import TestPathPropertyBag from PathTests.TestPathCore import TestPathCore #from PathTests.TestPathPost import PathPostTestCases from PathTests.TestPathGeom import TestPathGeom @@ -66,4 +67,5 @@ False if TestPathToolBit.__name__ else True False if TestPathVoronoi.__name__ else True False if TestPathThreadMilling.__name__ else True False if TestPathVcarve.__name__ else True +False if TestPathPropertyBag.__name__ else True diff --git a/src/Mod/Path/Tools/Bit/45degree_chamfer.fctb b/src/Mod/Path/Tools/Bit/45degree_chamfer.fctb index 6c1231ed0f..77e53e523c 100644 --- a/src/Mod/Path/Tools/Bit/45degree_chamfer.fctb +++ b/src/Mod/Path/Tools/Bit/45degree_chamfer.fctb @@ -11,4 +11,4 @@ "ShankDiameter": "6.3500 mm" }, "attribute": {} -} \ No newline at end of file +} diff --git a/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb b/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb index 265978053b..2a5f0253cd 100644 --- a/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb +++ b/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb @@ -1,14 +1,14 @@ { "version": 2, - "name": "3mm-thread-cutter", + "name": "5mm-thread-cutter", "shape": "thread-mill.fcstd", "parameter": { "Crest": "0.10 mm", "Diameter": "5.00 mm", "Length": "50.00 mm", "NeckDiameter": "3.00 mm", - "NeckHeight": "20.00 mm", + "NeckLength": "20.00 mm", "ShankDiameter": "5.00 mm" }, "attribute": {} -} \ No newline at end of file +} diff --git a/src/Mod/Path/Tools/Bit/probe.fctb b/src/Mod/Path/Tools/Bit/probe.fctb index b92828e7ac..31fa354ff6 100644 --- a/src/Mod/Path/Tools/Bit/probe.fctb +++ b/src/Mod/Path/Tools/Bit/probe.fctb @@ -1,6 +1,6 @@ { "version": 2, - "name": "Probe004", + "name": "Probe", "shape": "probe.fcstd", "parameter": { "Diameter": "6.0000 mm", diff --git a/src/Mod/Path/Tools/Bit/slittingsaw.fctb b/src/Mod/Path/Tools/Bit/slittingsaw.fctb index e9d33fe571..5b63c060ed 100644 --- a/src/Mod/Path/Tools/Bit/slittingsaw.fctb +++ b/src/Mod/Path/Tools/Bit/slittingsaw.fctb @@ -4,8 +4,8 @@ "shape": "slittingsaw.fcstd", "parameter": { "BladeThickness": "3.0000 mm", - "BoltHeight": "3.0000 mm", - "BoltWidth": "8.0000 mm", + "CapHeight": "3.0000 mm", + "CapDiameter": "8.0000 mm", "Diameter": "76.2000 mm", "Length": "50.0000 mm", "ShankDiameter": "19.0500 mm" diff --git a/src/Mod/Path/Tools/README.md b/src/Mod/Path/Tools/README.md index cdaed5a5b4..8f46b421bc 100644 --- a/src/Mod/Path/Tools/README.md +++ b/src/Mod/Path/Tools/README.md @@ -1,6 +1,6 @@ # Tools -Each tool is stored as a JSON file which has the template's path and values for all named constraints of the template. +Each tool is stored as a JSON file which has the shape's path and values for all attributes of the shape. 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, @@ -8,10 +8,10 @@ storing each tool as a `*.fcstd` file requires more space and does not allow for 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). +When a tool is instantiated in a job the PDN body is created from the shape and the attributes and 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 @@ -55,33 +55,34 @@ 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, +1. Select a tool shape from the existing shape files. If your tool doesn't exist, you'll have to create a new shape, see below for details. -1. Each template has its own set of parameters, fill them with the tool's values. +1. Each tool bit shape 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 describe the bit - for that some -additional parameters are needed which will be added when an actual bit is parametrized from the template. +The shape file for a tool bit is expected to contain a PD body which represents the tool as a 3d solid. The PD body +should be parametric based on a a PropertyBag object so that, when the properties of the PropertyBag are changed the +solid is updated to the correct representation. 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. Open the Path workbench and (with the PD body selected) create a PropertyBag, + menu 'Path' -> 'Utils' -> 'Property Bag' + * this creates a PropertyBag object inside the Body (assuming it was selected) + * add properties to which define the tool bit's shape and put those into the group 'Shape' + * add any other properties to the bag which might be useful for the tool bit +1. Construct the body of the tool bit and assign experssions referencing properties from the PropertyBag (in the + `Shape` Group) for all constraints. + * Position the tip of the tool bit on the origin (0,0) 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 + * Whatever you see when saving the document will end up being the visual representation of tool bits with this shape + +Not that 'Shape' is the only property group which has special meaning for tool bits. All other property groups are +copied verbatim to the ToolBit object when one is created. diff --git a/src/Mod/Path/Tools/Shape/ballend.fcstd b/src/Mod/Path/Tools/Shape/ballend.fcstd index c3c5ee8b25..14b2a65efa 100644 Binary files a/src/Mod/Path/Tools/Shape/ballend.fcstd 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 index c35ae78d81..63a50c4969 100644 Binary files a/src/Mod/Path/Tools/Shape/bullnose.fcstd and b/src/Mod/Path/Tools/Shape/bullnose.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/chamfer.fcstd b/src/Mod/Path/Tools/Shape/chamfer.fcstd index 3470b4a3c6..59c7cb1a3a 100644 Binary files a/src/Mod/Path/Tools/Shape/chamfer.fcstd and b/src/Mod/Path/Tools/Shape/chamfer.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/drill.fcstd b/src/Mod/Path/Tools/Shape/drill.fcstd index aa2a626d02..da1e277778 100644 Binary files a/src/Mod/Path/Tools/Shape/drill.fcstd 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 index 98888db1e3..b3b1ae18a4 100644 Binary files a/src/Mod/Path/Tools/Shape/endmill.fcstd and b/src/Mod/Path/Tools/Shape/endmill.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/probe.fcstd b/src/Mod/Path/Tools/Shape/probe.fcstd index 5f853f415b..b4f20fa949 100644 Binary files a/src/Mod/Path/Tools/Shape/probe.fcstd and b/src/Mod/Path/Tools/Shape/probe.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/slittingsaw.fcstd b/src/Mod/Path/Tools/Shape/slittingsaw.fcstd index 81d3d7f3d9..6694d911f7 100644 Binary files a/src/Mod/Path/Tools/Shape/slittingsaw.fcstd and b/src/Mod/Path/Tools/Shape/slittingsaw.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/thread-mill.fcstd b/src/Mod/Path/Tools/Shape/thread-mill.fcstd index d97e56241f..c69e915396 100644 Binary files a/src/Mod/Path/Tools/Shape/thread-mill.fcstd and b/src/Mod/Path/Tools/Shape/thread-mill.fcstd differ diff --git a/src/Mod/Path/Tools/Shape/v-bit.fcstd b/src/Mod/Path/Tools/Shape/v-bit.fcstd index 3168e5fe8d..51db09d7c7 100644 Binary files a/src/Mod/Path/Tools/Shape/v-bit.fcstd and b/src/Mod/Path/Tools/Shape/v-bit.fcstd differ diff --git a/src/Mod/Path/Tools/toolbit-attributes.py b/src/Mod/Path/Tools/toolbit-attributes.py new file mode 100755 index 0000000000..2f04ed6a58 --- /dev/null +++ b/src/Mod/Path/Tools/toolbit-attributes.py @@ -0,0 +1,152 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2021 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 * +# * * +# *************************************************************************** +# +# Script for manipulating toolbit shapes in a batch. This is handy for making +# attributes consistent over multiple or all shape file. +# +# Most commands are straight forward, except for --set which can be used to +# set the actual value of a property, or, in the case of enumerations it can +# also be used to set the enum values. This might be particularly useful when +# adding more materials. +# +# The following example moves all properties from the "Extra" group into the +# "Attributes" group. Note that the Attributes group might or might not +# already exist. If it does exist the specified group gets merged in. +# +# ./toolbit-attributes.py --move 'Extra:Attributes' src/Mod/Path/Tools/Shape/*.fcstd +# +# This example sets the Flutes value of all Shapes to 0: +# +# ./toolbit-attributes.py --set Flutes=0 src/Mod/Path/Tools/Shape/*.fcstd +# +# Finally, this example sets the enumerations of the Material attribute: +# +# ./toolbit-attributes.py --set 'Material=[HSS,Carbide,Tool Steel,Titanium]' src/Mod/Path/Tools/Shape/*.fcstd +# +# After running this tool it might be necessary to open the shape files +# manually and make sure they are visible and the thumbprint image is +# saved. +# +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# !!! Final note: this is a dangerous tool and should not be shipped with FC. !!!! +# !!! It's sole purpose is to make life of the developers easier and ensure !!!! +# !!! consistent attributes across all toobit shapes. !!!! +# !!! A single typo can ruin a lot of toolbit shapes - make sure to only use !!!! +# !!! it if those shape files are under a version control system and you can !!!! +# !!! back out the changes easily. !!!! +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +import argparse +import os +import sys + +parser = argparse.ArgumentParser() +parser.add_argument('path', nargs='+', help='Shape file to process') +parser.add_argument('--move', metavar=':', help='Move attributes from group 1 into group 2') +parser.add_argument('--delete', metavar='prop', help='Delete the given attribute') +parser.add_argument('--set', metavar='prop=value', help='Set property value') +parser.add_argument('--print', action='store_true', help='If set attributes are printed as discovered') +parser.add_argument('--print-all', action='store_true', help='If set Shape attributes are also printed') +parser.add_argument('--print-groups', action='store_true', help='If set all custom property groups are printed') +parser.add_argument('--save-changes', action='store_true', help='Unless specified the file is not saved') +parser.add_argument('--freecad', help='Directory FreeCAD binaries (libFreeCAD.so) if not installed') +args = parser.parse_args() + +if args.freecad: + sys.path.append(args.freecad) + +import FreeCAD +import Path +import PathScripts.PathPropertyBag as PathPropertyBag +import PathScripts.PathUtil as PathUtil + +set_var=None +set_val=None + +GroupMap = {} +if args.move: + g = args.move.split(':') + if len(g) != 2: + print("ERROR: {} not a valid group mapping".format(args.move)) + sys.exit(1) + GroupMap[g[0]] = g[1] + +if args.set: + s = args.set.split('=') + if len(s) != 2: + print("ERROR: {} not a valid group mapping".format(args.move)) + sys.exit(1) + set_var = s[0] + set_val = s[1] + +for i, fname in enumerate(args.path): + #print(fname) + doc = FreeCAD.openDocument(fname, False) + print("{}:".format(doc.Name)) + for o in doc.Objects: + if PathPropertyBag.IsPropertyBag(o): + if args.print_groups: + print(" {}: {}".format(o.Label, sorted(o.CustomPropertyGroups))) + else: + print(" {}:".format(o.Label)) + for p in o.Proxy.getCustomProperties(): + grp = o.getGroupOfProperty(p) + typ = o.getTypeIdOfProperty(p) + ttp = PathPropertyBag.getPropertyTypeName(typ) + val = PathUtil.getProperty(o, p) + dsc = o.getDocumentationOfProperty(p) + enm = '' + enum = [] + if ttp == 'Enumeration': + enum = o.getEnumerationsOfProperty(p) + enm = "{}".format(','.join(enum)) + if GroupMap.get(grp): + group = GroupMap.get(grp) + print("move: {}.{} -> {}".format(grp, p, group)) + o.removeProperty(p) + o.Proxy.addCustomProperty(typ, p, group, dsc) + if enum: + print("enum {}.{}: {}".format(group, p, enum)) + setattr(o, p, enum) + PathUtil.setProperty(o, p, val) + if p == set_var: + print("set {}.{} = {}".format(grp, p, set_val)) + if ttp == 'Enumeration' and set_val[0] == '[': + enum = set_val[1:-1].split(',') + setattr(o, p, enum) + else: + PathUtil.setProperty(o, p, set_val) + if p == args.delete: + print("delete {}.{}".format(grp, p)) + o.removeProperty(p) + if not args.print_all and grp == 'Shape': + continue + if args.print or args.print_all: + print(" {:10} {:20} {:20} {:10} {}".format(grp, p, ttp, str(val), enm)) + o.Proxy.refreshCustomPropertyGroups() + if args.save_changes: + doc.recompute() + doc.save() + FreeCAD.closeDocument(doc.Name) + +print('-done-')