Merge pull request #4294 from mlampert/feature/toolbit-free-properties
[Path] - Feature/toolbit free properties
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
78
src/Mod/Path/Gui/Resources/panels/PropertyBag.ui
Normal file
78
src/Mod/Path/Gui/Resources/panels/PropertyBag.ui
Normal 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>
|
||||
182
src/Mod/Path/Gui/Resources/panels/PropertyCreate.ui
Normal file
182
src/Mod/Path/Gui/Resources/panels/PropertyCreate.ui
Normal 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><html><head/><body><p>Name of property.</p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="propertyGroup">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>The category group the property belongs to.</p></body></html></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><html><head/><body><p>The type of the property value.</p></body></html></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><html><head/><body><p>ToolTip to be displayed when user hovers mouse over property.</p></body></html></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><html><head/><body><p>Check if you want to create several properties in a batch.</p></body></html></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>
|
||||
@@ -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><html><head/><body><p>The file which defines the type and shape of the Tool Bit.</p></body></html></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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
201
src/Mod/Path/PathScripts/PathProperty.py
Normal file
201
src/Mod/Path/PathScripts/PathProperty.py
Normal 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]
|
||||
113
src/Mod/Path/PathScripts/PathPropertyBag.py
Normal file
113
src/Mod/Path/PathScripts/PathPropertyBag.py
Normal 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
|
||||
|
||||
425
src/Mod/Path/PathScripts/PathPropertyBagGui.py
Normal file
425
src/Mod/Path/PathScripts/PathPropertyBagGui.py
Normal 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")
|
||||
228
src/Mod/Path/PathScripts/PathPropertyEditor.py
Normal file
228
src/Mod/Path/PathScripts/PathPropertyEditor.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
76
src/Mod/Path/PathTests/TestPathPropertyBag.py
Normal file
76
src/Mod/Path/PathTests/TestPathPropertyBag.py
Normal 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'])
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
@@ -11,4 +11,4 @@
|
||||
"ShankDiameter": "6.3500 mm"
|
||||
},
|
||||
"attribute": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"name": "Probe004",
|
||||
"name": "Probe",
|
||||
"shape": "probe.fcstd",
|
||||
"parameter": {
|
||||
"Diameter": "6.0000 mm",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
152
src/Mod/Path/Tools/toolbit-attributes.py
Executable file
152
src/Mod/Path/Tools/toolbit-attributes.py
Executable 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-')
|
||||
Reference in New Issue
Block a user