Merge pull request #4294 from mlampert/feature/toolbit-free-properties

[Path] - Feature/toolbit free properties
This commit is contained in:
sliptonic
2021-01-27 10:03:46 -06:00
committed by GitHub
64 changed files with 2133 additions and 454 deletions

View File

@@ -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}

View File

@@ -121,6 +121,8 @@
<file>panels/PageOpVcarveEdit.ui</file>
<file>panels/PathEdit.ui</file>
<file>panels/PointEdit.ui</file>
<file>panels/PropertyBag.ui</file>
<file>panels/PropertyCreate.ui</file>
<file>panels/SetupGlobal.ui</file>
<file>panels/SetupOp.ui</file>
<file>panels/ToolBitEditor.ui</file>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>552</width>
<height>651</height>
</rect>
</property>
<property name="windowTitle">
<string>Property Bag</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTableView" name="table">
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::AllEditTriggers</set>
</property>
<attribute name="horizontalHeaderCascadingSectionResizes">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="remove">
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="modify">
<property name="text">
<string>Modify...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="add">
<property name="text">
<string>Add...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>table</tabstop>
<tabstop>add</tabstop>
<tabstop>remove</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,182 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<height>452</height>
</rect>
</property>
<property name="windowTitle">
<string>Create Property</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="labelName">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="propertyName">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Name of property.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="propertyGroup">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The category group the property belongs to.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelGroup">
<property name="text">
<string>Group</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="propertyType">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The type of the property value.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelType">
<property name="text">
<string>Type</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="propertyEnum">
<property name="placeholderText">
<string>val1,val2,val3,...</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QTextEdit" name="propertyInfo">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;ToolTip to be displayed when user hovers mouse over property.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="tabChangesFocus">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelEnum">
<property name="text">
<string>Enums</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelInfo">
<property name="text">
<string>ToolTip</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QWidget" name="widget_2" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="createAnother">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Check if you want to create several properties in a batch.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Create another</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>propertyName</tabstop>
<tabstop>propertyGroup</tabstop>
<tabstop>propertyType</tabstop>
<tabstop>propertyEnum</tabstop>
<tabstop>propertyInfo</tabstop>
<tabstop>createAnother</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>401</width>
<width>489</width>
<height>715</height>
</rect>
</property>
@@ -17,7 +17,7 @@
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@@ -32,6 +32,12 @@
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Tool Bit</string>
</property>
@@ -65,6 +71,12 @@
</item>
<item row="1" column="1">
<widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
@@ -83,6 +95,9 @@
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The file which defines the type and shape of the Tool Bit.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="placeholderText">
<string>path</string>
</property>
</widget>
</item>
<item>
@@ -104,7 +119,7 @@
<item>
<widget class="QGroupBox" name="bitParams">
<property name="title">
<string>Bit Parameter</string>
<string>Parameter</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="fieldGrowthPolicy">
@@ -184,7 +199,7 @@
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTableView" name="attrTable">
<widget class="QTreeView" name="attrTree">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
@@ -200,9 +215,6 @@
<property name="editTriggers">
<set>QAbstractItemView::AllEditTriggers</set>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
</widget>
</item>
</layout>

View File

@@ -173,6 +173,12 @@
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item>

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2020 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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]

View File

@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2020 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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

View File

@@ -0,0 +1,425 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2020 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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")

View File

@@ -0,0 +1,228 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2020 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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])

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2021 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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'])

View File

@@ -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 <what?>'''
# 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())

View File

@@ -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": {}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -11,4 +11,4 @@
"ShankDiameter": "6.3500 mm"
},
"attribute": {}
}
}

View File

@@ -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": {}
}
}

View File

@@ -1,6 +1,6 @@
{
"version": 2,
"name": "Probe004",
"name": "Probe",
"shape": "probe.fcstd",
"parameter": {
"Diameter": "6.0000 mm",

View File

@@ -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"

View File

@@ -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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,152 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2021 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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='<group1>:<group2>', 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-')