Merge branch 'master' into PD-Hole-DrillPoint

This commit is contained in:
Kurt Kremitzki
2021-01-29 19:02:34 -06:00
committed by GitHub
120 changed files with 10061 additions and 26778 deletions
+2 -2
View File
@@ -71,8 +71,8 @@ Gui::GuiNativeEvent* Gui::GuiNativeEvent::gMouseInput = 0;
#define _TRACE_RI_TYPE 0
#define _TRACE_RIDI_DEVICENAME 0
#define _TRACE_RIDI_DEVICEINFO 0
#define _TRACE_RI_RAWDATA 1
#define _TRACE_3DINPUT_PERIOD 1
#define _TRACE_RI_RAWDATA 0
#define _TRACE_3DINPUT_PERIOD 0
#ifdef _WIN64
typedef unsigned __int64 QWORD;
+7 -7
View File
@@ -345,12 +345,12 @@ const char circleLine11_marker[CIRCLE_LINE11_WIDTH * CIRCLE_LINE11_HEIGHT + 1] =
" "
" xxxxxx "
" xxxxxxxx "
" xx xx "
" xxx xxx"
" xx xx"
" xx xx"
" xx xx"
" xx xx"
" xx xx "
" xxx xxx"
" xxxxxxxx "
" xxxxxx "};
@@ -381,7 +381,7 @@ const char circleLine15_marker[CIRCLE_LINE15_WIDTH * CIRCLE_LINE15_HEIGHT + 1] =
" "
" xxxxxx "
" xxxxxxxxxx "
" xx xx "
" xxx xxx "
" xx xx "
" xx xx"
" xx xx"
@@ -390,7 +390,7 @@ const char circleLine15_marker[CIRCLE_LINE15_WIDTH * CIRCLE_LINE15_HEIGHT + 1] =
" xx xx"
" xx xx"
" xx xx "
" xx xxxx"
" xxx xxx "
" xxxxxxxxxx "
" xxxxxx "};
@@ -401,12 +401,12 @@ const char circleFilled11_marker[CIRCLE_FILLED11_WIDTH * CIRCLE_FILLED11_HEIGHT
" "
" xxxxxx "
" xxxxxxxx "
" xxxxxxxx "
" xxxxxxxxxx"
" xxxxxxxxxx"
" xxxxxxxxxx"
" xxxxxxxxxx"
" xxxxxxxx "
" xxxxxxxxxx"
" xxxxxxxxxx"
" xxxxxxxx "
" xxxxxx "};
@@ -446,7 +446,7 @@ const char circleFilled15_marker[CIRCLE_FILLED15_WIDTH * CIRCLE_FILLED15_HEIGHT
" xxxxxxxxxxxxxx"
" xxxxxxxxxxxxxx"
" xxxxxxxxxxxx "
" xxxxxxxxxxxx"
" xxxxxxxxxxxx "
" xxxxxxxxxx "
" xxxxxx "};
+30 -24
View File
@@ -774,47 +774,53 @@ bool View3DInventor::setCamera(const char* pCamera)
SoNode * Cam;
SoDB::read(&in,Cam);
if (!Cam){
if (!Cam || !Cam->isOfType(SoCamera::getClassTypeId())) {
throw Base::RuntimeError("Camera settings failed to read");
}
// this is to make sure to reliably delete the node
CoinPtr<SoNode> camPtr(Cam, true);
// toggle between perspective and orthographic camera
if (Cam->getTypeId() != CamViewer->getTypeId())
{
if (Cam->getTypeId() != CamViewer->getTypeId()) {
_viewer->setCameraType(Cam->getTypeId());
CamViewer = _viewer->getSoRenderManager()->getCamera();
}
SoPerspectiveCamera * CamViewerP = 0;
SoOrthographicCamera * CamViewerO = 0;
SoPerspectiveCamera * CamViewerP = nullptr;
SoOrthographicCamera * CamViewerO = nullptr;
if (CamViewer->getTypeId() == SoPerspectiveCamera::getClassTypeId()) {
CamViewerP = (SoPerspectiveCamera *)CamViewer; // safe downward cast, knows the type
} else if (CamViewer->getTypeId() == SoOrthographicCamera::getClassTypeId()) {
CamViewerO = (SoOrthographicCamera *)CamViewer; // safe downward cast, knows the type
CamViewerP = static_cast<SoPerspectiveCamera *>(CamViewer); // safe downward cast, knows the type
}
else if (CamViewer->getTypeId() == SoOrthographicCamera::getClassTypeId()) {
CamViewerO = static_cast<SoOrthographicCamera *>(CamViewer); // safe downward cast, knows the type
}
if (Cam->getTypeId() == SoPerspectiveCamera::getClassTypeId()) {
if (CamViewerP){
CamViewerP->position = ((SoPerspectiveCamera *)Cam)->position;
CamViewerP->orientation = ((SoPerspectiveCamera *)Cam)->orientation;
CamViewerP->nearDistance = ((SoPerspectiveCamera *)Cam)->nearDistance;
CamViewerP->farDistance = ((SoPerspectiveCamera *)Cam)->farDistance;
CamViewerP->focalDistance = ((SoPerspectiveCamera *)Cam)->focalDistance;
} else {
CamViewerP->position = static_cast<SoPerspectiveCamera *>(Cam)->position;
CamViewerP->orientation = static_cast<SoPerspectiveCamera *>(Cam)->orientation;
CamViewerP->nearDistance = static_cast<SoPerspectiveCamera *>(Cam)->nearDistance;
CamViewerP->farDistance = static_cast<SoPerspectiveCamera *>(Cam)->farDistance;
CamViewerP->focalDistance = static_cast<SoPerspectiveCamera *>(Cam)->focalDistance;
}
else {
throw Base::TypeError("Camera type mismatch");
}
} else if (Cam->getTypeId() == SoOrthographicCamera::getClassTypeId()) {
}
else if (Cam->getTypeId() == SoOrthographicCamera::getClassTypeId()) {
if (CamViewerO){
CamViewerO->viewportMapping = ((SoOrthographicCamera *)Cam)->viewportMapping;
CamViewerO->position = ((SoOrthographicCamera *)Cam)->position;
CamViewerO->orientation = ((SoOrthographicCamera *)Cam)->orientation;
CamViewerO->nearDistance = ((SoOrthographicCamera *)Cam)->nearDistance;
CamViewerO->farDistance = ((SoOrthographicCamera *)Cam)->farDistance;
CamViewerO->focalDistance = ((SoOrthographicCamera *)Cam)->focalDistance;
CamViewerO->aspectRatio = ((SoOrthographicCamera *)Cam)->aspectRatio ;
CamViewerO->height = ((SoOrthographicCamera *)Cam)->height;
} else {
CamViewerO->viewportMapping = static_cast<SoOrthographicCamera *>(Cam)->viewportMapping;
CamViewerO->position = static_cast<SoOrthographicCamera *>(Cam)->position;
CamViewerO->orientation = static_cast<SoOrthographicCamera *>(Cam)->orientation;
CamViewerO->nearDistance = static_cast<SoOrthographicCamera *>(Cam)->nearDistance;
CamViewerO->farDistance = static_cast<SoOrthographicCamera *>(Cam)->farDistance;
CamViewerO->focalDistance = static_cast<SoOrthographicCamera *>(Cam)->focalDistance;
CamViewerO->aspectRatio = static_cast<SoOrthographicCamera *>(Cam)->aspectRatio ;
CamViewerO->height = static_cast<SoOrthographicCamera *>(Cam)->height;
}
else {
throw Base::TypeError("Camera type mismatch");
}
}
-2
View File
@@ -68,13 +68,11 @@ void ViewProviderLine::attach ( App::DocumentObject *obj ) {
sep->addChild ( pCoords );
SoIndexedLineSet *pLines = new SoIndexedLineSet ();
pLines->ref();
pLines->coordIndex.setNum(3);
pLines->coordIndex.setValues(0, 3, lines);
sep->addChild ( pLines );
SoTranslation *textTranslation = new SoTranslation ();
textTranslation->ref ();
textTranslation->translation.setValue ( SbVec3f ( -size * 49. / 50., size / 30., 0 ) );
sep->addChild ( textTranslation );
-2
View File
@@ -70,13 +70,11 @@ void ViewProviderPlane::attach ( App::DocumentObject *obj ) {
sep->addChild ( pCoords );
SoIndexedLineSet *pLines = new SoIndexedLineSet ();
pLines->ref();
pLines->coordIndex.setNum(6);
pLines->coordIndex.setValues(0, 6, lines);
sep->addChild ( pLines );
SoTranslation *textTranslation = new SoTranslation ();
textTranslation->ref ();
textTranslation->translation.setValue ( SbVec3f ( -size * 49. / 50., size * 9./10., 0 ) );
sep->addChild ( textTranslation );
+1 -1
View File
@@ -220,7 +220,7 @@ class _ArchSchedule:
prop = args[0].upper()
fval = args[1].upper()
if prop == "TYPE":
prop == "IFCTYPE"
prop = "IFCTYPE"
if inv:
if prop in props:
csprop = o.PropertiesList[props.index(prop)]
+3 -9
View File
@@ -410,14 +410,8 @@ class Snapper:
if (not self.maxEdges) or (len(shape.Edges) <= self.maxEdges):
if "Edge" in comp:
# we are snapping to an edge
edge = None
if shape.ShapeType == "Edge":
edge = shape
else:
en = int(comp[4:])-1
if len(shape.Edges) > en:
edge = shape.Edges[en]
if edge:
snaps.extend(self.snapToEndpoints(edge))
snaps.extend(self.snapToMidpoint(edge))
snaps.extend(self.snapToPerpendicular(edge, lastpoint))
@@ -433,9 +427,9 @@ class Snapper:
# extra ellipse options
snaps.extend(self.snapToCenter(edge))
elif "Face" in comp:
en = int(comp[4:])-1
if len(shape.Faces) > en:
face = shape.Faces[en]
# we are snapping to a face
if shape.ShapeType == "Face":
face = shape
snaps.extend(self.snapToFace(face))
elif "Vertex" in comp:
# directly snapped to a vertex
+1 -1
View File
@@ -226,7 +226,7 @@ void PropertyGeometryList::Restore(Base::XMLReader &reader)
reader.readEndElement("GeometryList");
// assignment
setValues(values);
setValues(std::move(values));
}
App::Property *PropertyGeometryList::Copy(void) const
+4 -1
View File
@@ -1133,7 +1133,10 @@ void ViewProviderPartExt::updateVisual()
const TopoDS_Face &actFace = TopoDS::Face(faceMap(i));
// get the mesh of the shape
Handle (Poly_Triangulation) mesh = BRep_Tool::Triangulation(actFace,aLoc);
if (mesh.IsNull()) continue;
if (mesh.IsNull()) {
parts[ii] = 0;
continue;
}
// getting the transformation of the shape/face
gp_Trsf myTransf;
+4 -1
View File
@@ -43,7 +43,7 @@ pairs, or a placement, or a vector</UserDocu>
</Methode>
<Methode Name="setFromGCode">
<Documentation>
<UserDocu>toGCode(): returns a GCode representation of the command</UserDocu>
<UserDocu>setFromGCode(): sets the path from the contents of the given GCode string</UserDocu>
</Documentation>
</Methode>
<Methode Name="transform">
@@ -51,5 +51,8 @@ pairs, or a placement, or a vector</UserDocu>
<UserDocu>transform(Placement): returns a copy of this command transformed by the given placement</UserDocu>
</Documentation>
</Methode>
<ClassDeclarations>
mutable Py::Dict parameters_copy_dict;
</ClassDeclarations>
</PythonExport>
</GenerateModel>
+17 -4
View File
@@ -56,6 +56,11 @@ std::string CommandPy::representation(void) const
return str.str();
}
//
// Py::Dict parameters_copy_dict is now a class member to avoid delete/create/copy on every read access from python code
// Now the pre-filled Py::Dict is returned which is more consistent with normal python behaviour.
// It should be cleared whenever the c++ Parameters object is changed eg setParameters() or other objects invalidate its content, eg setPlacement()
// https://forum.freecadweb.org/viewtopic.php?f=15&t=50583
PyObject *CommandPy::PyMake(struct _typeobject *, PyObject *, PyObject *) // Python wrapper
{
@@ -117,6 +122,7 @@ int CommandPy::PyInit(PyObject* args, PyObject* kwd)
}
getCommandPtr()->Parameters[ckey]=cvalue;
}
parameters_copy_dict.clear();
return 0;
}
PyErr_Clear(); // set by PyArg_ParseTuple()
@@ -157,11 +163,13 @@ void CommandPy::setName(Py::String arg)
Py::Dict CommandPy::getParameters(void) const
{
Py::Dict dict;
for(std::map<std::string,double>::iterator i = getCommandPtr()->Parameters.begin(); i != getCommandPtr()->Parameters.end(); ++i) {
dict.setItem(i->first, Py::Float(i->second));
// dict now a class member , https://forum.freecadweb.org/viewtopic.php?f=15&t=50583
if (parameters_copy_dict.length()==0) {
for(std::map<std::string,double>::iterator i = getCommandPtr()->Parameters.begin(); i != getCommandPtr()->Parameters.end(); ++i) {
parameters_copy_dict.setItem(i->first, Py::Float(i->second));
}
}
return dict;
return parameters_copy_dict;
}
void CommandPy::setParameters(Py::Dict arg)
@@ -200,6 +208,7 @@ void CommandPy::setParameters(Py::Dict arg)
throw Py::TypeError("The dictionary can only contain number values");
}
getCommandPtr()->Parameters[ckey]=cvalue;
parameters_copy_dict.clear();
}
}
@@ -224,6 +233,7 @@ PyObject* CommandPy::setFromGCode(PyObject *args)
std::string gcode(pstr);
try {
getCommandPtr()->setFromGCode(gcode);
parameters_copy_dict.clear();
}
catch (const Base::Exception& e) {
PyErr_SetString(PyExc_ValueError, e.what());
@@ -249,6 +259,7 @@ void CommandPy::setPlacement(Py::Object arg)
Py::Type PlacementType(pyType.o);
if(arg.isType(PlacementType)) {
getCommandPtr()->setFromPlacement( *static_cast<Base::PlacementPy*>((*arg))->getPlacementPtr() );
parameters_copy_dict.clear();
} else
throw Py::TypeError("Argument must be a placement");
}
@@ -259,6 +270,7 @@ PyObject* CommandPy::transform(PyObject *args)
if ( PyArg_ParseTuple(args, "O!", &(Base::PlacementPy::Type), &placement) ) {
Base::PlacementPy *p = static_cast<Base::PlacementPy*>(placement);
Path::Command trCmd = getCommandPtr()->transform( *p->getPlacementPtr() );
parameters_copy_dict.clear();
return new CommandPy(new Path::Command(trCmd));
} else
throw Py::TypeError("Argument must be a placement");
@@ -302,6 +314,7 @@ int CommandPy::setCustomAttributes(const char* attr, PyObject* obj)
return 0;
}
getCommandPtr()->Parameters[satt]=cvalue;
parameters_copy_dict.clear();
return 1;
}
}
+16
View File
@@ -96,6 +96,10 @@ SET(PathScripts_SRCS
PathScripts/PathProfileFaces.py
PathScripts/PathProfileFacesGui.py
PathScripts/PathProfileGui.py
PathScripts/PathProperty.py
PathScripts/PathPropertyBag.py
PathScripts/PathPropertyBagGui.py
PathScripts/PathPropertyEditor.py
PathScripts/PathSanity.py
PathScripts/PathSelection.py
PathScripts/PathSetupSheet.py
@@ -198,6 +202,7 @@ SET(PathTests_SRCS
PathTests/TestPathOpTools.py
PathTests/TestPathPost.py
PathTests/TestPathPreferences.py
PathTests/TestPathPropertyBag.py
PathTests/TestPathSetupSheet.py
PathTests/TestPathStock.py
PathTests/TestPathThreadMilling.py
@@ -208,6 +213,9 @@ SET(PathTests_SRCS
PathTests/TestPathUtil.py
PathTests/TestPathVcarve.py
PathTests/TestPathVoronoi.py
PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb
PathTests/Tools/Library/test-path-tool-bit-library-00.fctl
PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd
PathTests/boxtest.fcstd
PathTests/test_centroid_00.ngc
PathTests/test_geomop.fcstd
@@ -279,6 +287,14 @@ INSTALL(
Mod/Path/PathTests
)
INSTALL(
DIRECTORY
PathTests/Tools
DESTINATION
Mod/Path/PathTests
)
INSTALL(
FILES
${PathScripts_post_SRCS}
+2
View File
@@ -121,6 +121,8 @@
<file>panels/PageOpVcarveEdit.ui</file>
<file>panels/PathEdit.ui</file>
<file>panels/PointEdit.ui</file>
<file>panels/PropertyBag.ui</file>
<file>panels/PropertyCreate.ui</file>
<file>panels/SetupGlobal.ui</file>
<file>panels/SetupOp.ui</file>
<file>panels/ToolBitEditor.ui</file>
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>250</height>
<width>476</width>
<height>342</height>
</rect>
</property>
<property name="sizePolicy">
@@ -23,94 +23,28 @@
<iconset resource="../Path.qrc">
<normaloff>:/icons/Path_BaseGeometry.svg</normaloff>:/icons/Path_BaseGeometry.svg</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="2">
<widget class="QPushButton" name="geometryImportButton">
<property name="text">
<string>Import</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="deleteBase">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Remove the selected list items from the list of base geometries. The operation will not be applied to them.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QComboBox" name="geometryImportList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;List of operations with Base Geometry in current Job.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="3">
<widget class="QListWidget" name="baseList">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select one or more features in the 3d view and press 'Add' to add them as the base items for this operation.&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;Selected features can be deleted entirely.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="clearBase">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Clears list of base geometries.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="addBase">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Add selected features to the list of base geometries for this operation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<widget class="QLabel" name="label">
<property name="text">
<string>All objects will be processed using the same operation properties</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0">
<widget class="QPushButton" name="addBase">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Add selected features to the list of base geometries for this operation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<item row="3" column="1">
<widget class="QPushButton" name="deleteBase">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Remove the selected list items from the list of base geometries. The operation will not be applied to them.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item row="7" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -123,14 +57,70 @@
</property>
</spacer>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="clearBase">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Clears list of base geometries.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
<item row="6" column="0" colspan="3">
<widget class="QLabel" name="label">
<property name="text">
<string>All objects will be processed using the same operation properties</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0" colspan="3">
<widget class="QListWidget" name="baseList">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select one or more features in the 3d view and press 'Add' to add them as the base items for this operation.&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;Selected features can be deleted entirely.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="geometryImportButton">
<property name="text">
<string>Import</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QComboBox" name="geometryImportList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;List of operations with Base Geometry in current Job.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>baseList</tabstop>
<tabstop>addBase</tabstop>
<tabstop>deleteBase</tabstop>
<tabstop>clearBase</tabstop>
</tabstops>
<resources>
<include location="../Path.qrc"/>
</resources>
@@ -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>
@@ -0,0 +1,182 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<height>452</height>
</rect>
</property>
<property name="windowTitle">
<string>Create Property</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="labelName">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="propertyName">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Name of property.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="propertyGroup">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The category group the property belongs to.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelGroup">
<property name="text">
<string>Group</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="propertyType">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The type of the property value.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelType">
<property name="text">
<string>Type</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="propertyEnum">
<property name="placeholderText">
<string>val1,val2,val3,...</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QTextEdit" name="propertyInfo">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;ToolTip to be displayed when user hovers mouse over property.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="tabChangesFocus">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelEnum">
<property name="text">
<string>Enums</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelInfo">
<property name="text">
<string>ToolTip</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QWidget" name="widget_2" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="createAnother">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Check if you want to create several properties in a batch.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Create another</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>propertyName</tabstop>
<tabstop>propertyGroup</tabstop>
<tabstop>propertyType</tabstop>
<tabstop>propertyEnum</tabstop>
<tabstop>propertyInfo</tabstop>
<tabstop>createAnother</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>401</width>
<width>489</width>
<height>715</height>
</rect>
</property>
@@ -17,7 +17,7 @@
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@@ -32,6 +32,12 @@
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Tool Bit</string>
</property>
@@ -65,6 +71,12 @@
</item>
<item row="1" column="1">
<widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
@@ -83,6 +95,9 @@
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The file which defines the type and shape of the Tool Bit.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="placeholderText">
<string>path</string>
</property>
</widget>
</item>
<item>
@@ -104,7 +119,7 @@
<item>
<widget class="QGroupBox" name="bitParams">
<property name="title">
<string>Bit Parameter</string>
<string>Parameter</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="fieldGrowthPolicy">
@@ -184,7 +199,7 @@
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTableView" name="attrTable">
<widget class="QTreeView" name="attrTree">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
@@ -200,9 +215,6 @@
<property name="editTriggers">
<set>QAbstractItemView::AllEditTriggers</set>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
</widget>
</item>
</layout>
@@ -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>
+4
View File
@@ -154,6 +154,10 @@ class PathWorkbench (Workbench):
if extracmdlist:
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], extracmdlist)
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], ["Separator"])
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP("Path", "Utils")],
["Path_PropertyBag"])
self.dressupcmds = dressupcmdlist
curveAccuracy = PathPreferences.defaultLibAreaCurveAccuracy()
@@ -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
@@ -418,7 +419,7 @@ class PathDressupTagViewProvider:
'''this makes sure that the base operation is added back to the job and visible'''
# pylint: disable=unused-argument
PathLog.track()
if self.obj.Base.ViewObject:
if self.obj.Base and self.obj.Base.ViewObject:
self.obj.Base.ViewObject.Visibility = True
job = PathUtils.findParentJob(self.obj)
if arg1.Object and arg1.Object.Base and job:
@@ -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
+7
View File
@@ -48,6 +48,7 @@ def updateInputField(obj, prop, widget, onBeforeChange=None):
If onBeforeChange is specified it is called before a new value is assigned to the property.
Returns True if a new value was assigned, False otherwise (new value is the same as the current).
'''
PathLog.track()
value = widget.property('rawValue')
attr = PathUtil.getProperty(obj, prop)
attrValue = attr.Value if hasattr(attr, 'Value') else attr
@@ -98,10 +99,12 @@ class QuantitySpinBox:
PathLog.track(widget)
self.widget = widget
self.onBeforeChange = onBeforeChange
self.prop = None
self.attachTo(obj, prop)
def attachTo(self, obj, prop = None):
'''attachTo(obj, prop=None) ... use an existing editor for the given object and property'''
PathLog.track(self.prop, prop)
self.obj = obj
self.prop = prop
if obj and prop:
@@ -119,12 +122,14 @@ class QuantitySpinBox:
def expression(self):
'''expression() ... returns the expression if one is bound to the property'''
PathLog.track(self.prop, self.valid)
if self.valid:
return self.widget.property('expression')
return ''
def setMinimum(self, quantity):
'''setMinimum(quantity) ... set the minimum'''
PathLog.track(self.prop, self.valid)
if self.valid:
value = quantity.Value if hasattr(quantity, 'Value') else quantity
self.widget.setProperty('setMinimum', value)
@@ -133,6 +138,7 @@ class QuantitySpinBox:
'''updateSpinBox(quantity=None) ... update the display value of the spin box.
If no value is provided the value of the bound property is used.
quantity can be of type Quantity or Float.'''
PathLog.track(self.prop, self.valid)
if self.valid:
if quantity is None:
quantity = PathUtil.getProperty(self.obj, self.prop)
@@ -141,6 +147,7 @@ class QuantitySpinBox:
def updateProperty(self):
'''updateProperty() ... update the bound property with the value from the spin box'''
PathLog.track(self.prop, self.valid)
if self.valid:
return updateInputField(self.obj, self.prop, self.widget, self.onBeforeChange)
return None
+1
View File
@@ -66,6 +66,7 @@ def Startup():
# from PathScripts import PathProfileEdgesGui
# from PathScripts import PathProfileFacesGui
from PathScripts import PathProfileGui
from PathScripts import PathPropertyBagGui
from PathScripts import PathSanity
from PathScripts import PathSetupSheetGui
from PathScripts import PathSimpleCopy
+1
View File
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui
import PathScripts.PathHelix as PathHelix
import PathScripts.PathLog as PathLog
+6 -2
View File
@@ -31,6 +31,7 @@ from PySide import QtCore, QtGui
import FreeCAD
import FreeCADGui
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathJob as PathJob
import PathScripts.PathJobCmd as PathJobCmd
import PathScripts.PathJobDlg as PathJobDlg
@@ -521,6 +522,7 @@ class StockCreateCylinderEdit(StockEdit):
class StockFromExistingEdit(StockEdit):
Index = 3
StockType = PathStock.StockType.Unknown
StockLabelPrefix = 'Stock'
def editorFrame(self):
return self.form.stockFromExisting
@@ -529,7 +531,7 @@ class StockFromExistingEdit(StockEdit):
stock = self.form.stockExisting.itemData(self.form.stockExisting.currentIndex())
if not (hasattr(obj.Stock, 'Objects') and len(obj.Stock.Objects) == 1 and obj.Stock.Objects[0] == stock):
if stock:
stock = PathJob.createResourceClone(obj, stock, 'Stock', 'Stock')
stock = PathJob.createResourceClone(obj, stock, self.StockLabelPrefix , 'Stock')
stock.ViewObject.Visibility = True
PathStock.SetupStockObject(stock, PathStock.StockType.Unknown)
stock.Proxy.execute(stock)
@@ -555,7 +557,9 @@ class StockFromExistingEdit(StockEdit):
index = -1
for i, solid in enumerate(self.candidates(obj)):
self.form.stockExisting.addItem(solid.Label, solid)
if solid.Label == stockName:
label="{}-{}".format(self.StockLabelPrefix, solid.Label)
if label == stockName:
index = i
self.form.stockExisting.setCurrentIndex(index if index != -1 else 0)
+4 -1
View File
@@ -422,7 +422,10 @@ class ObjectOp(object):
zmax = max(zmax, bb.ZMax)
for sub in sublist:
try:
fbb = base.Shape.getElement(sub).BoundBox
if sub:
fbb = base.Shape.getElement(sub).BoundBox
else:
fbb = base.Shape.BoundBox
zmin = max(zmin, faceZmin(bb, fbb))
zmax = max(zmax, fbb.ZMax)
except Part.OCCError as e:
+14 -10
View File
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathGeom as PathGeom
import PathScripts.PathGetPoint as PathGetPoint
import PathScripts.PathGui as PathGui
@@ -42,13 +43,8 @@ __author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Base classes and framework for Path operation's UI"
LOGLEVEL = False
if LOGLEVEL:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# PathLog.trackModule(PathLog.thisModule())
def translate(context, text, disambig=None):
@@ -566,6 +562,7 @@ class TaskPanelBaseGeometryPage(TaskPanelPage):
return False
def addBase(self):
PathLog.track()
if self.addBaseGeometry(FreeCADGui.Selection.getSelectionEx()):
# self.obj.Proxy.execute(self.obj)
self.setFields(self.obj)
@@ -636,11 +633,18 @@ class TaskPanelBaseGeometryPage(TaskPanelPage):
# Set base geometry list window to resize based on contents
# Code reference:
# https://stackoverflow.com/questions/6337589/qlistwidget-adjust-size-to-content
# ml: disabling this logic because I can't get it to work on HPD monitor.
# On my systems the values returned by the list object are also incorrect on
# creation, leading to a list object of size 15. count() always returns 0 until
# the list is actually displayed. The same is true for sizeHintForRow(0), which
# returns -1 until the widget is rendered. The widget claims to have a size of
# (100, 30), once it becomes visible the size is (535, 192).
# Leaving the framework here in case somebody figures out how to set this up
# properly.
qList = self.form.baseList
# qList.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
col = qList.width() # 300
row = (qList.count() + qList.frameWidth()) * 15
qList.setFixedSize(col, row)
#qList.setMinimumHeight(row)
PathLog.debug("baseList({}, {}) {} * {}".format(qList.size(), row, qList.count(), qList.sizeHintForRow(0)))
class TaskPanelBaseLocationPage(TaskPanelPage):
@@ -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
+8 -20
View File
@@ -151,26 +151,9 @@ def searchPathsPost():
return paths
def searchPathsTool(sub='Bit'):
def searchPathsTool(sub):
paths = []
if 'Bit' == sub:
paths.append("{}/Bit".format(os.path.dirname(lastPathToolLibrary())))
paths.append(lastPathToolBit())
if 'Library' == sub:
paths.append(lastPathToolLibrary())
if 'Shape' == sub:
paths.append(lastPathToolShape())
def appendPath(p, sub):
if p:
paths.append(os.path.join(p, 'Tools', sub))
paths.append(os.path.join(p, sub))
paths.append(p)
appendPath(defaultFilePath(), sub)
appendPath(macroFilePath(), sub)
appendPath(os.path.join(FreeCAD.getHomePath(), "Mod/Path/"), sub)
paths.append(os.path.join(FreeCAD.getHomePath(), 'Mod', 'Path', 'Tools', sub))
return paths
@@ -263,15 +246,19 @@ def setDefaultTaskPanelLayout(style):
def experimentalFeaturesEnabled():
return preferences().GetBool(EnableExperimentalFeatures, False)
def suppressAllSpeedsWarning():
return preferences().GetBool(WarningSuppressAllSpeeds, True)
def suppressRapidSpeedsWarning():
return suppressAllSpeedsWarning() or preferences().GetBool(WarningSuppressRapidSpeeds, True)
def suppressSelectionModeWarning():
return preferences().GetBool(WarningSuppressSelectionMode, True)
def suppressOpenCamLibWarning():
return preferences().GetBool(WarningSuppressOpenCamLib, True)
@@ -316,7 +303,8 @@ def lastPathToolLibrary():
def setLastPathToolLibrary(path):
PathLog.track(path)
curLib = lastFileToolLibrary()
if os.path.split(curLib)[0] != path:
PathLog.debug('curLib: {}'.format(curLib))
if curLib and os.path.split(curLib)[0] != path:
setLastFileToolLibrary('') # a path is known but not specific file
return preferences().SetString(LastPathToolLibrary, path)
+1
View File
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathProbe as PathProbe
import PathScripts.PathOpGui as PathOpGui
import PathScripts.PathGui as PathGui
@@ -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
View File
@@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2020 sliptonic <[email protected]> *
# * *
# * 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
View File
@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2020 sliptonic <[email protected]> *
# * *
# * 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
@@ -0,0 +1,425 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2020 sliptonic <[email protected]> *
# * *
# * 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")
@@ -0,0 +1,228 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2020 sliptonic <[email protected]> *
# * *
# * 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
+1 -2
View File
@@ -67,8 +67,7 @@ class CommandPathSimpleCopy:
FreeCADGui.addModule("PathScripts.PathUtils")
FreeCADGui.addModule("PathScripts.PathCustom")
FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","' + selection[0].Name + '_SimpleCopy")')
FreeCADGui.doCommand('PathScripts.PathCustom.ObjectCustom(obj)')
FreeCADGui.doCommand('obj = PathScripts.PathCustom.Create("' + selection[0].Name + '_SimpleCopy")')
FreeCADGui.doCommand('obj.ViewObject.Proxy = 0')
FreeCADGui.doCommand('obj.Gcode = [c.toGCode() for c in srcpath.Commands]')
FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)')
@@ -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
+1
View File
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathSlot as PathSlot
import PathScripts.PathGui as PathGui
import PathScripts.PathOpGui as PathOpGui
@@ -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
+165 -185
View File
@@ -24,6 +24,7 @@ import FreeCAD
import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathPropertyBag as PathPropertyBag
import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype
import PathScripts.PathUtil as PathUtil
import PySide
@@ -42,74 +43,73 @@ __author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Class to deal with and represent a tool bit."
# PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
# PathLog.trackModule()
PropertyGroupShape = 'Shape'
_DebugFindTool = False
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
#PathLog.trackModule()
def translate(context, text, disambig=None):
return PySide.QtCore.QCoreApplication.translate(context, text, disambig)
def _findToolFile(name, containerFile, typ):
PathLog.track(name)
if os.path.exists(name): # absolute reference
return name
ParameterTypeConstraint = {
'Angle': 'App::PropertyAngle',
'Distance': 'App::PropertyLength',
'DistanceX': 'App::PropertyLength',
'DistanceY': 'App::PropertyLength',
'Radius': 'App::PropertyLength'}
if containerFile:
rootPath = os.path.dirname(os.path.dirname(containerFile))
paths = [os.path.join(rootPath, typ)]
else:
paths = []
paths.extend(PathPreferences.searchPathsTool(typ))
def _findFile(path, name):
PathLog.track(path, name)
fullPath = os.path.join(path, name)
if os.path.exists(fullPath):
return (True, fullPath)
for root, ds, fs in os.walk(path):
for d in ds:
found, fullPath = _findFile(d, name)
if found:
return (True, fullPath)
return (False, None)
def _findTool(path, typ, dbg=False):
if os.path.exists(path): # absolute reference
if dbg:
PathLog.debug("Found {} at {}".format(typ, path))
return path
def searchFor(pname, fname):
# PathLog.debug("pname: {} fname: {}".format(pname, fname))
if dbg:
PathLog.debug("Looking for {}".format(pname))
if fname:
for p in PathPreferences.searchPathsTool(typ):
PathLog.track(p)
f = os.path.join(p, fname)
if dbg:
PathLog.debug(" Checking {}".format(f))
if os.path.exists(f):
if dbg:
PathLog.debug(" Found {} at {}".format(typ, f))
return f
if pname and os.path.sep != pname:
PathLog.track(pname)
ppname, pfname = os.path.split(pname)
ffname = os.path.join(pfname, fname) if fname else pfname
return searchFor(ppname, ffname)
return None
return searchFor(path, '')
for p in paths:
found, path = _findFile(p, name)
if found:
return path
return None
def findShape(path):
'''
findShape(path) ... search for path, full and partially
in all known shape directories.
'''
return _findTool(path, 'Shape')
def findToolShape(name, path=None):
'''findToolShape(name, path) ... search for name, if relative path look in path'''
PathLog.track(name, path)
return _findToolFile(name, path, 'Shape')
def findBit(path):
PathLog.track(path)
if path.endswith('.fctb'):
return _findTool(path, 'Bit')
return _findTool("{}.fctb".format(path), 'Bit')
def findToolBit(name, path=None):
'''findToolBit(name, path) ... search for name, if relative path look in path'''
PathLog.track(name, path)
if name.endswith('.fctb'):
return _findToolFile(name, path, 'Bit')
return _findToolFile("{}.fctb".format(name), path, 'Bit')
def findLibrary(path, dbg=False):
if path.endswith('.fctl'):
return _findTool(path, 'Library', dbg)
return _findTool("{}.fctl".format(path), 'Library', dbg)
def findToolLibrary(name, path=None):
'''findToolLibrary(name, path) ... search for name, if relative path look in path'''
PathLog.track(name, path)
if name.endswith('.fctl'):
return _findToolFile(name, path, 'Library')
return _findToolFile("{}.fctl".format(name), path, 'Library')
def _findRelativePath(path, typ):
PathLog.track(path, typ)
relative = path
for p in PathPreferences.searchPathsTool(typ):
if path.startswith(p):
@@ -132,48 +132,19 @@ def findRelativePathTool(path):
def findRelativePathLibrary(path):
return _findRelativePath(path, 'Library')
def updateConstraint(sketch, name, value):
for i, constraint in enumerate(sketch.Constraints):
if constraint.Name.split(';')[0] == name:
constr = None
if constraint.Type in ['DistanceX', 'DistanceY', 'Distance', 'Radius', 'Angle']:
constr = Sketcher.Constraint(constraint.Type, constraint.First, constraint.FirstPos, constraint.Second, constraint.SecondPos, value)
else:
print(constraint.Name, constraint.Type)
if constr is not None:
if not PathGeom.isRoughly(constraint.Value, value.Value):
PathLog.track(name, constraint.Type,
'update', i, "(%.2f -> %.2f)"
% (constraint.Value, value.Value))
sketch.delConstraint(i)
sketch.recompute()
n = sketch.addConstraint(constr)
sketch.renameConstraint(n, constraint.Name)
else:
PathLog.track(name, constraint.Type, 'unchanged')
break
PropertyGroupBit = 'Bit'
PropertyGroupAttribute = 'Attribute'
class ToolBit(object):
def __init__(self, obj, shapeFile):
PathLog.track(obj.Label, shapeFile)
def __init__(self, obj, shapeFile, path=None):
PathLog.track(obj.Label, shapeFile, path)
self.obj = obj
obj.addProperty('App::PropertyFile', 'BitShape', 'Base',
translate('PathToolBit', 'Shape for bit shape'))
obj.addProperty('App::PropertyLink', 'BitBody', 'Base',
translate('PathToolBit',
'The parametrized body representing the tool bit'))
obj.addProperty('App::PropertyFile', 'File', 'Base',
translate('PathToolBit', 'The file of the tool'))
obj.addProperty('App::PropertyString', 'ShapeName', 'Base',
translate('PathToolBit', 'The name of the shape file'))
obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape'))
obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit'))
obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool'))
obj.addProperty('App::PropertyString', 'ShapeName', 'Base', translate('PathToolBit', 'The name of the shape file'))
obj.addProperty('App::PropertyStringList', 'BitPropertyNames', 'Base', translate('PathToolBit', 'List of all properties inherited from the bit'))
if path:
obj.File = path
if shapeFile is None:
obj.BitShape = 'endmill.fcstd'
self._setupBitShape(obj)
@@ -193,12 +164,6 @@ class ToolBit(object):
break
return None
def propertyNamesBit(self, obj):
return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupBit]
def propertyNamesAttribute(self, obj):
return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupAttribute]
def onDocumentRestored(self, obj):
# when files are shared it is essential to be able to change/set the shape file,
# otherwise the file is hard to use
@@ -206,19 +171,39 @@ class ToolBit(object):
obj.setEditorMode('BitBody', 2)
obj.setEditorMode('File', 1)
obj.setEditorMode('Shape', 2)
if not hasattr(obj, 'BitPropertyNames'):
obj.addProperty('App::PropertyStringList', 'BitPropertyNames', 'Base', translate('PathToolBit', 'List of all properties inherited from the bit'))
propNames = []
for prop in obj.PropertiesList:
if obj.getGroupOfProperty(prop) == 'Bit':
val = obj.getPropertyByName(prop)
typ = obj.getTypeIdOfProperty(prop)
dsc = obj.getDocumentationOfProperty(prop)
for prop in self.propertyNamesBit(obj):
obj.setEditorMode(prop, 1)
# I currently don't see why these need to be read-only
# for prop in self.propertyNamesAttribute(obj):
# obj.setEditorMode(prop, 1)
obj.removeProperty(prop)
obj.addProperty(typ, prop, PropertyGroupShape, dsc)
PathUtil.setProperty(obj, prop, val)
propNames.append(prop)
elif obj.getGroupOfProperty(prop) == 'Attribute':
propNames.append(prop)
obj.BitPropertyNames = propNames
obj.setEditorMode('BitPropertyNames', 2)
for prop in obj.BitPropertyNames:
if obj.getGroupOfProperty(prop) == PropertyGroupShape:
# properties in the Shape group can only be modified while the actual
# shape is loaded, so we have to disable direct property editing
obj.setEditorMode(prop, 1)
else:
# all other custom properties can and should be edited directly in the
# property editor widget, not much value in re-implementing that
obj.setEditorMode(prop, 0)
def onChanged(self, obj, prop):
PathLog.track(obj.Label, prop)
if prop == 'BitShape' and 'Restore' not in obj.State:
self._setupBitShape(obj)
# elif obj.getGroupOfProperty(prop) == PropertyGroupBit:
# self._updateBitShape(obj, [prop])
def onDelete(self, obj, arg2=None):
PathLog.track(obj.Label)
@@ -227,12 +212,19 @@ class ToolBit(object):
def _updateBitShape(self, obj, properties=None):
if obj.BitBody is not None:
if not properties:
properties = self.propertyNamesBit(obj)
for prop in properties:
for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']:
PathLog.track(obj.Label, sketch.Label, prop)
updateConstraint(sketch, prop, obj.getPropertyByName(prop))
for attributes in [o for o in obj.BitBody.Group if hasattr(o, 'Proxy') and hasattr(o.Proxy, 'getCustomProperties')]:
for prop in attributes.Proxy.getCustomProperties():
# the property might not exist in our local object (new attribute in shape)
# for such attributes we just keep the default
if hasattr(obj, prop):
setattr(attributes, prop, obj.getPropertyByName(prop))
else:
# if the template shape has a new attribute defined we should add that
# to the local object
self._setupProperty(obj, prop, attributes)
propNames = obj.BitPropertyNames
propNames.append(prop)
obj.BitPropertyNames = propNames
self._copyBitShape(obj)
def _copyBitShape(self, obj):
@@ -243,6 +235,7 @@ class ToolBit(object):
obj.Shape = Part.Shape()
def _loadBitBody(self, obj, path=None):
PathLog.track(obj.Label, path)
p = path if path else obj.BitShape
docOpened = False
doc = None
@@ -251,12 +244,15 @@ class ToolBit(object):
doc = FreeCAD.getDocument(d)
break
if doc is None:
p = findShape(p)
p = findToolShape(p, path if path else obj.File)
if not path and p != obj.BitShape:
obj.BitShape = p
PathLog.debug("ToolBit {} using shape file: {}".format(obj.Label, p))
doc = FreeCAD.openDocument(p, True)
obj.ShapeName = doc.Name
docOpened = True
else:
PathLog.debug("ToolBit {} already open: {}".format(obj.Label, doc))
return (doc, docOpened)
def _removeBitBody(self, obj):
@@ -269,7 +265,7 @@ class ToolBit(object):
PathLog.track(obj.Label)
self._removeBitBody(obj)
self._copyBitShape(obj)
for prop in self.propertyNamesBit(obj):
for prop in obj.BitPropertyNames:
obj.removeProperty(prop)
def loadBitBody(self, obj, force=False):
@@ -287,6 +283,20 @@ class ToolBit(object):
def unloadBitBody(self, obj):
self._removeBitBody(obj)
def _setupProperty(self, obj, prop, orig):
# extract property parameters and values so it can be copied
val = orig.getPropertyByName(prop)
typ = orig.getTypeIdOfProperty(prop)
grp = orig.getGroupOfProperty(prop)
dsc = orig.getDocumentationOfProperty(prop)
obj.addProperty(typ, prop, grp, dsc)
if 'App::PropertyEnumeration' == typ:
setattr(obj, prop, orig.getEnumerationsOfProperty(prop))
obj.setEditorMode(prop, 1)
PathUtil.setProperty(obj, prop, val)
def _setupBitShape(self, obj, path=None):
PathLog.track(obj.Label)
@@ -296,6 +306,8 @@ class ToolBit(object):
obj.Label = doc.RootObjects[0].Label
self._deleteBitSetup(obj)
bitBody = obj.Document.copyObject(doc.RootObjects[0], True)
docName = doc.Name
if docOpened:
FreeCAD.setActiveDocument(activeDoc.Name)
FreeCAD.closeDocument(doc.Name)
@@ -303,29 +315,44 @@ class ToolBit(object):
if bitBody.ViewObject:
bitBody.ViewObject.Visibility = False
for sketch in [o for o in bitBody.Group if o.TypeId == 'Sketcher::SketchObject']:
for constraint in [c for c in sketch.Constraints if c.Name != '']:
typ = ParameterTypeConstraint.get(constraint.Type)
PathLog.track(constraint, typ)
if typ is not None:
parts = [p.strip() for p in constraint.Name.split(';')]
prop = parts[0]
desc = ''
if len(parts) > 1:
desc = parts[1]
obj.addProperty(typ, prop, PropertyGroupBit, desc)
obj.setEditorMode(prop, 1)
value = constraint.Value
if constraint.Type == 'Angle':
value = value * 180 / math.pi
PathUtil.setProperty(obj, prop, value)
PathLog.debug("bitBody.{} ({}): {}".format(bitBody.Label, bitBody.Name, type(bitBody)))
propNames = []
for attributes in [o for o in bitBody.Group if PathPropertyBag.IsPropertyBag(o)]:
PathLog.debug("Process properties from {}".format(attributes.Label))
for prop in attributes.Proxy.getCustomProperties():
self._setupProperty(obj, prop, attributes)
propNames.append(prop)
if not propNames:
PathLog.error(translate('PathToolBit', 'Did not find a PropertyBag in {} - not a ToolBit shape?').format(docName))
# has to happen last because it could trigger op.execute evaluations
obj.BitPropertyNames = propNames
obj.BitBody = bitBody
self._copyBitShape(obj)
def toolShapeProperties(self, obj):
'''toolShapeProperties(obj) ... return all properties defining it's shape'''
return sorted([prop for prop in obj.BitPropertyNames if obj.getGroupOfProperty(prop) == PropertyGroupShape])
def toolAdditionalProperties(self, obj):
'''toolShapeProperties(obj) ... return all properties unrelated to it's shape'''
return sorted([prop for prop in obj.BitPropertyNames if obj.getGroupOfProperty(prop) != PropertyGroupShape])
def toolGroupsAndProperties(self, obj, includeShape=True):
'''toolGroupsAndProperties(obj) ... returns a dictionary of group names with a list of property names.'''
category = {}
for prop in obj.BitPropertyNames:
group = obj.getGroupOfProperty(prop)
if includeShape or group != PropertyGroupShape:
properties = category.get(group, [])
properties.append(prop)
category[group] = properties
return category
def getBitThumbnail(self, obj):
if obj.BitShape:
path = findShape(obj.BitShape)
path = findToolShape(obj.BitShape)
if path:
with open(path, 'rb') as fd:
try:
@@ -347,8 +374,7 @@ class ToolBit(object):
obj.File = path
return True
except (OSError, IOError) as e:
PathLog.error("Could not save tool {} to {} ({})".format(
obj.Label, path, e))
PathLog.error("Could not save tool {} to {} ({})".format(obj.Label, path, e))
raise
def templateAttrs(self, obj):
@@ -360,16 +386,10 @@ class ToolBit(object):
else:
attrs['shape'] = findRelativePathShape(obj.BitShape)
params = {}
for name in self.propertyNamesBit(obj):
for name in obj.BitPropertyNames:
params[name] = PathUtil.getPropertyValueString(obj, name)
attrs['parameter'] = params
params = {}
for name in self.propertyNamesAttribute(obj):
if name == "UserAttributes":
for key, value in obj.UserAttributes.items():
params[key] = value
else:
params[name] = PathUtil.getPropertyValueString(obj, name)
attrs['attribute'] = params
return attrs
@@ -380,73 +400,33 @@ def Declaration(path):
return json.load(fp)
class AttributePrototype(PathSetupSheetOpPrototype.OpPrototype):
def __init__(self):
PathSetupSheetOpPrototype.OpPrototype.__init__(self, 'ToolBitAttribute')
self.addProperty('App::PropertyEnumeration', 'Material',
PropertyGroupAttribute,
translate('PathToolBit', 'Tool bit material'))
self.Material = ['Carbide', 'CastAlloy', 'Ceramics', 'Diamond',
'HighCarbonToolSteel', 'HighSpeedSteel', 'Sialon']
self.addProperty('App::PropertyDistance', 'LengthOffset',
PropertyGroupAttribute, translate('PathToolBit',
'Length offset in Z direction'))
self.addProperty('App::PropertyInteger', 'Flutes',
PropertyGroupAttribute, translate('PathToolBit',
'The number of flutes'))
self.addProperty('App::PropertyDistance', 'ChipLoad',
PropertyGroupAttribute, translate('PathToolBit',
'Chipload as per manufacturer'))
self.addProperty('App::PropertyMap', 'UserAttributes',
PropertyGroupAttribute, translate('PathToolBit',
'User Defined Values'))
self.addProperty('App::PropertyBool', 'SpindlePower',
PropertyGroupAttribute, translate('PathToolBit',
'Whether Spindle Power should be allowed'))
class ToolBitFactory(object):
def CreateFromAttrs(self, attrs, name='ToolBit'):
PathLog.debug(attrs)
obj = Factory.Create(name, attrs['shape'])
def CreateFromAttrs(self, attrs, name='ToolBit', path=None):
PathLog.track(attrs, path)
obj = Factory.Create(name, attrs['shape'], path)
obj.Label = attrs['name']
params = attrs['parameter']
for prop in params:
PathUtil.setProperty(obj, prop, params[prop])
obj.Proxy._updateBitShape(obj)
obj.Proxy.unloadBitBody(obj)
params = attrs['attribute']
proto = AttributePrototype()
uservals = {}
for pname in params:
try:
prop = proto.getProperty(pname)
prop.setupProperty(obj, pname, PropertyGroupAttribute, prop.valueFromString(params[pname]))
except Exception:
prop = proto.getProperty("UserAttributes")
uservals.update({pname: params[pname]})
if len(uservals.items()) > 0:
prop.setupProperty(obj, "UserAttributes",
PropertyGroupAttribute, uservals)
return obj
def CreateFrom(self, path, name='ToolBit'):
PathLog.track(name, path)
try:
data = Declaration(path)
bit = Factory.CreateFromAttrs(data, name)
bit.File = path
bit = Factory.CreateFromAttrs(data, name, path)
return bit
except (OSError, IOError) as e:
PathLog.error("%s not a valid tool file (%s)" % (path, e))
raise
def Create(self, name='ToolBit', shapeFile=None):
def Create(self, name='ToolBit', shapeFile=None, path=None):
PathLog.track(name, shapeFile, path)
obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name)
obj.Proxy = ToolBit(obj, shapeFile)
obj.Proxy = ToolBit(obj, shapeFile, path)
return obj
+108 -113
View File
@@ -20,12 +20,14 @@
# * *
# ***************************************************************************
import FreeCAD
import FreeCADGui
import PathScripts.PathGui as PathGui
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathSetupSheetGui as PathSetupSheetGui
import PathScripts.PathPropertyEditor as PathPropertyEditor
import PathScripts.PathToolBit as PathToolBit
import PathScripts.PathUtil as PathUtil
import os
import re
@@ -39,6 +41,31 @@ PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
class _Delegate(QtGui.QStyledItemDelegate):
'''Handles the creation of an appropriate editing widget for a given property.'''
ObjectRole = QtCore.Qt.UserRole + 1
PropertyRole = QtCore.Qt.UserRole + 2
EditorRole = QtCore.Qt.UserRole + 3
def createEditor(self, parent, option, index):
editor = index.data(self.EditorRole)
if editor is None:
obj = index.data(self.ObjectRole)
prp = index.data(self.PropertyRole)
editor = PathPropertyEditor.Editor(obj, prp)
index.model().setData(index, editor, self.EditorRole)
return editor.widget(parent)
def setEditorData(self, widget, index):
# called to update the widget with the current data
index.data(self.EditorRole).setEditorData(widget)
def setModelData(self, widget, model, index):
# called to update the model with the data from the widget
editor = index.data(self.EditorRole)
editor.setModelData(widget)
index.model().setData(index, PathUtil.getPropertyValueString(editor.obj, editor.prop), QtCore.Qt.DisplayRole)
class ToolBitEditor(object):
'''UI and controller for editing a ToolBit.
@@ -84,32 +111,35 @@ class ToolBitEditor(object):
layout = self.form.bitParams.layout()
ui = FreeCADGui.UiLoader()
nr = 0
# for all properties either assign them to existing labels and editors
# or create additional ones for them if not enough have already been
# created.
for name in tool.PropertiesList:
if tool.getGroupOfProperty(name) == PathToolBit.PropertyGroupBit:
if nr < len(self.widgets):
PathLog.debug("re-use row: {} [{}]".format(nr, name))
label, qsb, editor = self.widgets[nr]
label.setText(labelText(name))
editor.attachTo(tool, name)
label.show()
qsb.show()
else:
qsb = ui.createWidget('Gui::QuantitySpinBox')
editor = PathGui.QuantitySpinBox(qsb, tool, name)
label = QtGui.QLabel(labelText(name))
self.widgets.append((label, qsb, editor))
PathLog.debug("create row: {} [{}]".format(nr, name))
if nr >= layout.rowCount():
layout.addRow(label, qsb)
nr = nr + 1
usedRows = 0
for nr, name in enumerate(tool.Proxy.toolShapeProperties(tool)):
if nr < len(self.widgets):
PathLog.debug("re-use row: {} [{}]".format(nr, name))
label, qsb, editor = self.widgets[nr]
label.setText(labelText(name))
editor.attachTo(tool, name)
label.show()
qsb.show()
else:
qsb = ui.createWidget('Gui::QuantitySpinBox')
editor = PathGui.QuantitySpinBox(qsb, tool, name)
label = QtGui.QLabel(labelText(name))
self.widgets.append((label, qsb, editor))
PathLog.debug("create row: {} [{}] {}".format(nr, name, type(qsb)))
if hasattr(qsb, 'editingFinished'):
qsb.editingFinished.connect(self.updateTool)
if nr >= layout.rowCount():
layout.addRow(label, qsb)
usedRows = usedRows + 1
# hide all rows which aren't being used
for i in range(nr, len(self.widgets)):
PathLog.track(usedRows, len(self.widgets))
for i in range(usedRows, len(self.widgets)):
label, qsb, editor = self.widgets[i]
label.hide()
qsb.hide()
@@ -124,98 +154,48 @@ class ToolBitEditor(object):
def setupAttributes(self, tool):
PathLog.track()
self.proto = PathToolBit.AttributePrototype()
self.props = sorted(self.proto.properties)
self.delegate = PathSetupSheetGui.Delegate(self.form)
self.model = QtGui.QStandardItemModel(len(self.props)-1, 3, self.form)
self.model.setHorizontalHeaderLabels(['Set', 'Property', 'Value'])
for i, name in enumerate(self.props):
PathLog.debug("propname: %s " % name)
setup = True
if not hasattr(self, 'delegate'):
self.delegate = _Delegate(self.form.attrTree)
self.model = QtGui.QStandardItemModel(self.form.attrTree)
self.model.setHorizontalHeaderLabels(['Property', 'Value'])
else:
self.model.removeRows(0, self.model.rowCount())
setup = False
prop = self.proto.getProperty(name)
isset = hasattr(tool, name)
attributes = tool.Proxy.toolGroupsAndProperties(tool, False)
for name in attributes:
group = QtGui.QStandardItem()
group.setData(name, QtCore.Qt.EditRole)
group.setEditable(False)
for prop in attributes[name]:
label = QtGui.QStandardItem()
label.setData(prop, QtCore.Qt.EditRole)
label.setEditable(False)
if isset:
prop.setValue(getattr(tool, name))
value = QtGui.QStandardItem()
value.setData(PathUtil.getPropertyValueString(tool, prop), QtCore.Qt.DisplayRole)
value.setData(tool, _Delegate.ObjectRole)
value.setData(prop, _Delegate.PropertyRole)
if name == "UserAttributes":
continue
group.appendRow([label, value])
self.model.appendRow(group)
else:
self.model.setData(self.model.index(i, 0), isset,
QtCore.Qt.EditRole)
self.model.setData(self.model.index(i, 1), name,
QtCore.Qt.EditRole)
self.model.setData(self.model.index(i, 2), prop,
PathSetupSheetGui.Delegate.PropertyRole)
self.model.setData(self.model.index(i, 2),
prop.displayString(),
QtCore.Qt.DisplayRole)
self.model.item(i, 0).setCheckable(True)
self.model.item(i, 0).setText('')
self.model.item(i, 1).setEditable(False)
self.model.item(i, 1).setToolTip(prop.info)
self.model.item(i, 2).setToolTip(prop.info)
if isset:
self.model.item(i, 0).setCheckState(QtCore.Qt.Checked)
else:
self.model.item(i, 0).setCheckState(QtCore.Qt.Unchecked)
self.model.item(i, 1).setEnabled(False)
self.model.item(i, 2).setEnabled(False)
if hasattr(tool, "UserAttributes"):
for key, value in tool.UserAttributes.items():
PathLog.debug(key, value)
c1 = QtGui.QStandardItem()
c1.setCheckable(False)
c1.setEditable(False)
c1.setCheckState(QtCore.Qt.CheckState.Checked)
c1.setText('')
c2 = QtGui.QStandardItem(key)
c2.setEditable(False)
c3 = QtGui.QStandardItem(value)
c3.setEditable(False)
self.model.appendRow([c1, c2, c3])
self.form.attrTable.setModel(self.model)
self.form.attrTable.setItemDelegateForColumn(2, self.delegate)
self.form.attrTable.resizeColumnsToContents()
self.form.attrTable.verticalHeader().hide()
self.model.dataChanged.connect(self.updateData)
def updateData(self, topLeft, bottomRight):
PathLog.track()
if 0 == topLeft.column():
isset = self.model.item(topLeft.row(),
0).checkState() == QtCore.Qt.Checked
self.model.item(topLeft.row(), 1).setEnabled(isset)
self.model.item(topLeft.row(), 2).setEnabled(isset)
if setup:
self.form.attrTree.setModel(self.model)
self.form.attrTree.setItemDelegateForColumn(1, self.delegate)
self.form.attrTree.expandAll()
self.form.attrTree.resizeColumnToContents(0)
self.form.attrTree.resizeColumnToContents(1)
#self.form.attrTree.collapseAll()
def accept(self):
PathLog.track()
self.refresh()
self.tool.Proxy.unloadBitBody(self.tool)
# get the attributes
for i, name in enumerate(self.props):
PathLog.debug('in accept: {}'.format(name))
prop = self.proto.getProperty(name)
if self.model.item(i, 0) is not None:
enabled = self.model.item(i, 0).checkState() == QtCore.Qt.Checked
if enabled and not prop.getValue() is None:
prop.setupProperty(self.tool, name,
PathToolBit.PropertyGroupAttribute,
prop.getValue())
elif hasattr(self.tool, name):
self.tool.removeProperty(name)
def reject(self):
PathLog.track()
self.tool.Proxy.unloadBitBody(self.tool)
@@ -228,27 +208,45 @@ class ToolBitEditor(object):
for lbl, qsb, editor in self.widgets:
editor.updateSpinBox()
def _updateBitShape(self, shapePath):
# Only need to go through this exercise if the shape actually changed.
if self.tool.BitShape != shapePath:
# Before setting a new bitshape we need to make sure that none of
# editors fires an event and tries to access its old property, which
# might not exist anymore.
for lbl, qsb, editor in self.widgets:
editor.attachTo(self.tool, 'File')
self.tool.BitShape = shapePath
self.setupTool(self.tool)
self.form.toolName.setText(self.tool.Label)
if self.tool.BitBody and self.tool.BitBody.ViewObject:
if not self.tool.BitBody.ViewObject.Visibility:
self.tool.BitBody.ViewObject.Visibility = True
self.setupAttributes(self.tool)
return True
return False
def updateShape(self):
PathLog.track()
shapePath = str(self.form.shapePath.text())
# Only need to go through this exercise if the shape actually changed.
if self.tool.BitShape != shapePath:
self.tool.BitShape = shapePath
self.setupTool(self.tool)
self.form.toolName.setText(self.tool.Label)
if self._updateBitShape(shapePath):
for lbl, qsb, editor in self.widgets:
editor.updateSpinBox()
def updateTool(self):
PathLog.track()
self.tool.Label = str(self.form.toolName.text())
self.tool.BitShape = str(self.form.shapePath.text())
label = str(self.form.toolName.text())
shape = str(self.form.shapePath.text())
if self.tool.Label != label:
self.tool.Label = label
self._updateBitShape(shape)
for lbl, qsb, editor in self.widgets:
editor.updateProperty()
# self.tool.Proxy._updateBitShape(self.tool)
self.tool.Proxy._updateBitShape(self.tool)
def refresh(self):
PathLog.track()
@@ -262,10 +260,7 @@ class ToolBitEditor(object):
path = self.tool.BitShape
if not path:
path = PathPreferences.lastPathToolShape()
foo = QtGui.QFileDialog.getOpenFileName(self.form,
"Path - Tool Shape",
path,
"*.fcstd")
foo = QtGui.QFileDialog.getOpenFileName(self.form, "Path - Tool Shape", path, "*.fcstd")
if foo and foo[0]:
PathPreferences.setLastPathToolShape(os.path.dirname(foo[0]))
self.form.shapePath.setText(foo[0])
+6 -5
View File
@@ -170,13 +170,13 @@ class TaskPanel:
class ToolBitGuiFactory(PathToolBit.ToolBitFactory):
def Create(self, name='ToolBit', shapeFile=None):
def Create(self, name='ToolBit', shapeFile=None, path=None):
'''Create(name = 'ToolBit') ... creates a new tool bit.
It is assumed the tool will be edited immediately so the internal bit body is still attached.'''
FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit',
'Create ToolBit'))
tool = PathToolBit.ToolBitFactory.Create(self, name, shapeFile)
PathLog.track(name, shapeFile, path)
FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit'))
tool = PathToolBit.ToolBitFactory.Create(self, name, shapeFile, path)
PathIconViewProvider.Attach(tool.ViewObject, name)
FreeCAD.ActiveDocument.commitTransaction()
return tool
@@ -247,7 +247,8 @@ def GetToolShapeFile(parent=None):
location, '*.fcstd')
if fname and fname[0]:
if fname != location:
PathPreferences.setLastPathToolShape(location)
newloc = os.path.dirname(fname[0])
PathPreferences.setLastPathToolShape(newloc)
return fname[0]
else:
return None
@@ -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.copy():
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
+5
View File
@@ -71,6 +71,11 @@ def getPropertyValueString(obj, prop):
def setProperty(obj, prop, value):
'''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.'''
o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
if not attr is None and type(value) == str:
if type(attr) == int:
value = int(value, 0)
elif type(attr) == bool:
value = value.lower() in ['true', '1', 'yes', 'ok']
if o and name:
setattr(o, name, value)
+1
View File
@@ -21,6 +21,7 @@
# ***************************************************************************
import FreeCADGui
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts
import PathScripts.PathJobCmd as PathJobCmd
import PathScripts.PathUtils as PathUtils
+26 -33
View File
@@ -210,11 +210,6 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
QtCore.QT_TRANSLATE_NOOP("PathVcarve",
"Additional base objects to be engraved"))
obj.setEditorMode('BaseShapes', 2) # hide
if not hasattr(obj, 'BaseObject'):
obj.addProperty("App::PropertyLink", "BaseObject", "Path",
QtCore.QT_TRANSLATE_NOOP("PathVcarve",
"Additional base objects to be engraved"))
obj.setEditorMode('BaseObject', 2) # hide
def initOperation(self, obj):
'''initOperation(obj) ... create vcarve specific properties.'''
@@ -242,7 +237,7 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
edges.append(_getPartEdge(e, geom))
return edges
def buildPathMedial(self, obj, Faces):
def buildPathMedial(self, obj, faces):
'''constructs a medial axis path using openvoronoi'''
def insert_many_wires(vd, wires):
@@ -271,7 +266,7 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
VD.clear()
voronoiWires = []
for f in Faces:
for f in faces:
vd = Path.Voronoi()
insert_many_wires(vd, f.Wires)
@@ -311,41 +306,39 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
PathLog.track()
if not hasattr(obj.ToolController.Tool, "CuttingEdgeAngle"):
FreeCAD.Console.PrintError(
translate("Path_Vcarve", "VCarve requires an engraving \
cutter with CuttingEdgeAngle") + "\n")
PathLog.error(translate("Path_Vcarve", "VCarve requires an engraving cutter with CuttingEdgeAngle"))
if obj.ToolController.Tool.CuttingEdgeAngle >= 180.0:
FreeCAD.Console.PrintError(
translate("Path_Vcarve",
"Engraver Cutting Edge Angle must be < 180 degrees.") + "\n")
PathLog.error(translate("Path_Vcarve", "Engraver Cutting Edge Angle must be < 180 degrees."))
return
try:
if obj.Base:
PathLog.track()
for base in obj.Base:
faces = []
for sub in base[1]:
shape = getattr(base[0].Shape, sub)
if isinstance(shape, Part.Face):
faces.append(shape)
faces = []
modelshape = Part.makeCompound(faces)
for base in obj.BaseShapes:
faces.extend(base.Shape.Faces)
elif len(self.model) == 1 and self.model[0].isDerivedFrom('Sketcher::SketchObject') or \
self.model[0].isDerivedFrom('Part::Part2DObject'):
PathLog.track()
for base in obj.Base:
for sub in base[1]:
shape = getattr(base[0].Shape, sub)
if isinstance(shape, Part.Face):
faces.append(shape)
modelshape = self.model[0].Shape
self.buildPathMedial(obj, modelshape.Faces)
if not faces:
for model in self.model:
if model.isDerivedFrom('Sketcher::SketchObject') or model.isDerivedFrom('Part::Part2DObject'):
faces.extend(model.Shape.Faces)
if faces:
self.buildPathMedial(obj, faces)
else:
PathLog.error(translate('PathVcarve', 'The Job Base Object has no engraveable element. Engraving operation will produce no output.'))
except Exception as e:
PathLog.error(e)
traceback.print_exc()
PathLog.error(translate('PathVcarve', 'The Job Base Object has \
no engraveable element. Engraving \
operation will produce no output.'))
raise e
#PathLog.error(e)
#traceback.print_exc()
PathLog.error(translate('PathVcarve', 'Error processing Base object. Engraving operation will produce no output.'))
#raise e
def opUpdateDepths(self, obj, ignoreErrors=False):
'''updateDepths(obj) ... engraving is always done at the top most z-value'''
+8 -4
View File
@@ -23,6 +23,7 @@
import FreeCAD
import FreeCADGui
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathVcarve as PathVcarve
import PathScripts.PathLog as PathLog
import PathScripts.PathOpGui as PathOpGui
@@ -53,6 +54,7 @@ class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
return super(TaskPanelBaseGeometryPage, self)
def addBaseGeometry(self, selection):
PathLog.track(selection)
added = False
shapes = self.obj.BaseShapes
for sel in selection:
@@ -78,10 +80,12 @@ class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
shapes.append(base)
self.obj.BaseShapes = shapes
added = True
else:
# user wants us to engrave an edge of face of a base model
base = self.super().addBaseGeometry(selection)
added = added or base
if not added:
# user wants us to engrave an edge of face of a base model
PathLog.info(" call default")
base = self.super().addBaseGeometry(selection)
added = added or base
return added
@@ -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
+513
View File
@@ -0,0 +1,513 @@
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <[email protected]> *
# * Copyright (c) 2021 shadowbane1000 <[email protected]> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * FreeCAD 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 Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************/
from __future__ import print_function
import FreeCAD
from FreeCAD import Units
import Path
import argparse
import datetime
import shlex
import os.path
from PathScripts import PostUtils
from PathScripts import PathUtils
TOOLTIP = '''
This is a postprocessor file for the Path workbench. It is used to
take a pseudo-gcode fragment outputted by a Path object, and output
real GCode suitable should be suitable for most Fanuc controllers.
It has only been tested on a 21i-MB controller on a 3 axis mill.
This postprocessor, once placed in the appropriate PathScripts folder,
can be used directly from inside FreeCAD, via the GUI importer or via
python scripts with:
import fanuc_post
fanuc_post.export(object,"/path/to/file.ncc","")
'''
now = datetime.datetime.now()
parser = argparse.ArgumentParser(prog='fanuc', add_help=False)
parser.add_argument('--no-header', action='store_true', help='suppress header output')
parser.add_argument('--no-comments', action='store_true', help='suppress comment output')
parser.add_argument('--line-numbers', action='store_true', help='prefix with line numbers')
parser.add_argument('--no-show-editor', action='store_true', help='don\'t pop up editor before writing output')
parser.add_argument('--precision', default='3', help='number of digits of precision, default=3')
parser.add_argument('--preamble', help='set commands to be issued before the first command, default="G17\nG90"')
parser.add_argument('--postamble', help='set commands to be issued after the last command, default="M05\nG17 G90\nM2"')
parser.add_argument('--inches', action='store_true', help='Convert output for US imperial mode (G20)')
parser.add_argument('--no-modal', action='store_true', help='Don\'t output the Same G-command Name USE NonModal Mode')
parser.add_argument('--no-axis-modal', action='store_true', help='Don\'t output the Same Axis Value Mode')
parser.add_argument('--no-tlo', action='store_true', help='suppress tool length offset (G43) following tool changes')
TOOLTIP_ARGS = parser.format_help()
# These globals set common customization preferences
OUTPUT_COMMENTS = True
OUTPUT_HEADER = True
OUTPUT_LINE_NUMBERS = False
SHOW_EDITOR = True
MODAL = True # if true commands are suppressed if the same as previous line.
USE_TLO = True # if true G43 will be output following tool changes
OUTPUT_DOUBLES = False # if false duplicate axis values are suppressed if the same as previous line.
COMMAND_SPACE = " "
LINENR = 100 # line number starting value
# These globals will be reflected in the Machine configuration of the project
UNITS = "G21" # G21 for metric, G20 for us standard
UNIT_SPEED_FORMAT = 'mm/min'
UNIT_FORMAT = 'mm'
MACHINE_NAME = "fanuc"
CORNER_MIN = {'x': 0, 'y': 0, 'z': 0}
CORNER_MAX = {'x': 500, 'y': 300, 'z': 300}
PRECISION = 3
# this global is used to pass spindle speed from the tool command into the machining command for
# rigid tapping.
tapSpeed = 0
# Preamble text will appear at the beginning of the GCODE output file.
PREAMBLE = '''G17 G54 G40 G49 G80 G90
'''
# Postamble text will appear following the last operation.
POSTAMBLE = '''M05
G17 G54 G90 G80 G40
M6 T0
M2
'''
# Pre operation text will be inserted before every operation
PRE_OPERATION = ''''''
# Post operation text will be inserted after every operation
POST_OPERATION = ''''''
# Tool Change commands will be inserted before a tool change
TOOL_CHANGE = ''''''
# to distinguish python built-in open function from the one declared below
if open.__module__ in ['__builtin__','io']:
pythonopen = open
def processArguments(argstring):
# pylint: disable=global-statement
global OUTPUT_HEADER
global OUTPUT_COMMENTS
global OUTPUT_LINE_NUMBERS
global SHOW_EDITOR
global PRECISION
global PREAMBLE
global POSTAMBLE
global UNITS
global UNIT_SPEED_FORMAT
global UNIT_FORMAT
global MODAL
global USE_TLO
global OUTPUT_DOUBLES
try:
args = parser.parse_args(shlex.split(argstring))
if args.no_header:
OUTPUT_HEADER = False
if args.no_comments:
OUTPUT_COMMENTS = False
if args.line_numbers:
OUTPUT_LINE_NUMBERS = True
if args.no_show_editor:
SHOW_EDITOR = False
print("Show editor = %d" % SHOW_EDITOR)
PRECISION = args.precision
if args.preamble is not None:
PREAMBLE = args.preamble
if args.postamble is not None:
POSTAMBLE = args.postamble
if args.inches:
UNITS = 'G20'
UNIT_SPEED_FORMAT = 'in/min'
UNIT_FORMAT = 'in'
PRECISION = 4
if args.no_modal:
MODAL = False
if args.no_tlo:
USE_TLO = False
if args.no_axis_modal:
OUTPUT_DOUBLES = true
except Exception: # pylint: disable=broad-except
return False
return True
def export(objectslist, filename, argstring):
# pylint: disable=global-statement
if not processArguments(argstring):
return None
global UNITS
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
global HORIZRAPID
global VERTRAPID
for obj in objectslist:
if not hasattr(obj, "Path"):
print("the object " + obj.Name + " is not a path. Please select only path and Compounds.")
return None
print("postprocessing...")
gcode = ""
# write header
if OUTPUT_HEADER:
gcode += "%\n"
gcode += ";\n"
gcode += os.path.split(filename)[-1]+" ("+"FREECAD-FILENAME-GOES-HERE" + ", " + "JOB-NAME-GOES-HERE"+")\n"
gcode += linenumber() + "("+filename.upper()+",EXPORTED BY FREECAD!)\n"
gcode += linenumber() + "(POST PROCESSOR: " + __name__.upper() + ")\n"
gcode += linenumber() + "(OUTPUT TIME:" + str(now).upper() + ")\n"
# Write the preamble
if OUTPUT_COMMENTS:
gcode += linenumber() + "(BEGIN PREAMBLE)\n"
for line in PREAMBLE.splitlines(False):
gcode += linenumber() + line + "\n"
gcode += linenumber() + UNITS + "\n"
for obj in objectslist:
# Skip inactive operations
if hasattr(obj, 'Active'):
if not obj.Active:
continue
if hasattr(obj, 'Base') and hasattr(obj.Base, 'Active'):
if not obj.Base.Active:
continue
# fetch machine details
job = PathUtils.findParentJob(obj)
myMachine = 'not set'
if hasattr(job, "MachineName"):
myMachine = job.MachineName
if hasattr(job, "MachineUnits"):
if job.MachineUnits == "Metric":
UNITS = "G21"
UNIT_FORMAT = 'mm'
UNIT_SPEED_FORMAT = 'mm/min'
else:
UNITS = "G20"
UNIT_FORMAT = 'in'
UNIT_SPEED_FORMAT = 'in/min'
if hasattr(job, "SetupSheet"):
if hasattr(job.SetupSheet, "HorizRapid"):
HORIZRAPID = Units.Quantity(job.SetupSheet.HorizRapid, FreeCAD.Units.Velocity)
if hasattr(job.SetupSheet, "VertRapid"):
VERTRAPID = Units.Quantity(job.SetupSheet.HorizRapid, FreeCAD.Units.Velocity)
# do the pre_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(BEGIN OPERATION: %s)\n" % obj.Label.upper()
gcode += linenumber() + "(MACHINE: %s, %s)\n" % (myMachine.upper(), UNIT_SPEED_FORMAT.upper())
for line in PRE_OPERATION.splitlines(True):
gcode += linenumber() + line
# get coolant mode
coolantMode = 'None'
if hasattr(obj, "CoolantMode") or hasattr(obj, 'Base') and hasattr(obj.Base, "CoolantMode"):
if hasattr(obj, "CoolantMode"):
coolantMode = obj.CoolantMode
else:
coolantMode = obj.Base.CoolantMode
# turn coolant on if required
if OUTPUT_COMMENTS:
if not coolantMode == 'None':
gcode += linenumber() + '(COOLANT ON:' + coolantMode.upper() + ')\n'
if coolantMode == 'Flood':
gcode += linenumber() + 'M8' + '\n'
if coolantMode == 'Mist':
gcode += linenumber() + 'M7' + '\n'
# process the operation gcode
gcode += parse(obj)
# do the post_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(FINISH OPERATION: %s)\n" % obj.Label.upper()
for line in POST_OPERATION.splitlines(True):
gcode += linenumber() + line
# turn coolant off if required
if not coolantMode == 'None':
if OUTPUT_COMMENTS:
gcode += linenumber() + '(COOLANT OFF:' + coolantMode.upper() + ')\n'
gcode += linenumber() +'M9' + '\n'
# do the post_amble
if OUTPUT_COMMENTS:
gcode += "(BEGIN POSTAMBLE)\n"
for line in POSTAMBLE.splitlines(True):
gcode += linenumber() + line
gcode += "%\n"
if FreeCAD.GuiUp and SHOW_EDITOR:
dia = PostUtils.GCodeEditorDialog()
dia.editor.setText(gcode)
result = dia.exec_()
if result:
final = dia.editor.toPlainText()
else:
final = gcode
else:
final = gcode
print("done postprocessing.")
if not filename == '-':
gfile = pythonopen(filename, "w")
gfile.write(final)
gfile.close()
return final
def linenumber():
# pylint: disable=global-statement
global LINENR
if OUTPUT_LINE_NUMBERS is True:
LINENR += 10
return "N" + str(LINENR) + " "
return ""
def parse(pathobj):
# pylint: disable=global-statement
global PRECISION
global MODAL
global OUTPUT_DOUBLES
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
global tapSpeed
out = ""
lastcommand = None
precision_string = '.' + str(PRECISION) + 'f'
currLocation = {} # keep track for no doubles
print("Startup!")
# the order of parameters
# arcs need work. original code from mach3_4 doesn't want K properties on XY plane. Not sure
# what fanuc does here.
params = ['X', 'Y', 'Z', 'A', 'B', 'C', 'I', 'J', 'F', 'S', 'T', 'Q', 'R', 'L', 'H', 'D', 'P']
firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0})
currLocation.update(firstmove.Parameters) # set First location Parameters
if hasattr(pathobj, "Group"): # We have a compound or project.
# if OUTPUT_COMMENTS:
# out += linenumber() + "(compound: " + pathobj.Label + ")\n"
for p in pathobj.Group:
out += parse(p)
return out
else: # parsing simple path
# groups might contain non-path things like stock.
if not hasattr(pathobj, "Path"):
return out
# if OUTPUT_COMMENTS:
# out += linenumber() + "(" + pathobj.Label + ")\n"
adaptiveOp = False
opHorizRapid = 0
opVertRapid = 0
if 'Adaptive' in pathobj.Name:
adaptiveOp = True
if hasattr(pathobj, 'ToolController'):
if hasattr(pathobj.ToolController, 'HorizRapid') and pathobj.ToolController.HorizRapid > 0:
opHorizRapid = Units.Quantity(pathobj.ToolController.HorizRapid, FreeCAD.Units.Velocity)
else:
FreeCAD.Console.PrintWarning('Tool Controller Horizontal Rapid Values are unset'+ '\n')
if hasattr(pathobj.ToolController, 'VertRapid') and pathobj.ToolController.VertRapid > 0:
opVertRapid = Units.Quantity(pathobj.ToolController.VertRapid, FreeCAD.Units.Velocity)
else:
FreeCAD.Console.PrintWarning('Tool Controller Vertical Rapid Values are unset'+ '\n')
for index,c in enumerate(pathobj.Path.Commands):
outstring = []
command = c.Name
if index+1 == len(pathobj.Path.Commands):
nextcommand = ""
else:
nextcommand = pathobj.Path.Commands[index+1].Name
if adaptiveOp and c.Name in ["G0", "G00"]:
if opHorizRapid and opVertRapid:
command = 'G1'
else:
outstring.append('(TOOL CONTROLLER RAPID VALUES ARE UNSET)' + '\n')
# suppress moves in fixture selection
if pathobj.Label == "Fixture":
if command == "G0":
continue
# if it's a tap, we rigid tap, so don't start the spindle yet...
if command == "M03" or command == "M3":
if pathobj.Tool.ToolType == "Tap":
tapSpeed = int(pathobj.SpindleSpeed)
continue
# convert drill cycles to tap cycles if tool is a tap
if command == "G81" or command == "G83":
if hasattr(pathobj, 'ToolController') and pathobj.ToolController.Tool.ToolType == "Tap":
command = "G84"
out += linenumber() + "G95\n"
paramstring = ""
for param in [ "X", "Y" ]:
if param in c.Parameters:
if (not OUTPUT_DOUBLES) and (param in currLocation) and (currLocation[param] == c.Parameters[param]):
continue
else:
pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
paramstring += " " + param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string)
if paramstring != "":
out += linenumber() + "G00"+paramstring+"\n"
if "S" in c.Parameters:
tapSpeed = int(c.Parameters['S'])
out += "M29 S"+str(tapSpeed)+"\n"
for param in [ "Z", "R" ]:
if param in c.Parameters:
if (not OUTPUT_DOUBLES) and (param in currLocation) and (currLocation[param] == c.Parameters[param]):
continue
else:
pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
paramstring += " " + param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string)
# in this mode, F is the distance per revolution of the thread (pitch)
# P is the dwell time in seconds at the bottom of the thread
# Q is the peck depth of the threading operation
for param in [ "F", "P", "Q" ]:
if param in c.Parameters:
value = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
paramstring += " " + param + format(float(value.getValueAs(UNIT_FORMAT)), precision_string)
out += linenumber() + "G84" + paramstring + "\n"
out += linenumber() + "G80\n"
out += linenumber() + "G94\n"
continue
outstring.append(command)
# if modal: suppress the command if it is the same as the last one
if MODAL is True:
if command == lastcommand:
outstring.pop(0)
# suppress a G80 between two identical command
if command == "G80" and lastcommand == nextcommand:
continue
if c.Name[0] == '(' and not OUTPUT_COMMENTS: # command is a comment
continue
# Now add the remaining parameters in order
for param in params:
if param in c.Parameters:
if param == 'F' and (currLocation[param] != c.Parameters[param] or OUTPUT_DOUBLES):
if c.Name not in ["G0", "G00"]: # fanuc doesn't use rapid speeds
speed = Units.Quantity(c.Parameters['F'], FreeCAD.Units.Velocity)
if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0:
outstring.append(param + format(float(speed.getValueAs(UNIT_SPEED_FORMAT)), precision_string))
else:
continue
elif param == 'T':
outstring.append(param + str(int(c.Parameters['T'])))
elif param == 'H':
outstring.append(param + str(int(c.Parameters['H'])))
elif param == 'D':
outstring.append(param + str(int(c.Parameters['D'])))
elif param == 'S':
outstring.append(param + str(int(c.Parameters['S'])))
currentSpeed = int(c.Parameters['S'])
else:
if (not OUTPUT_DOUBLES) and (param in currLocation) and (currLocation[param] == c.Parameters[param]):
continue
else:
pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
outstring.append(
param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string))
if adaptiveOp and c.Name in ["G0", "G00"]:
if opHorizRapid and opVertRapid:
if 'Z' not in c.Parameters:
outstring.append('F' + format(float(opHorizRapid.getValueAs(UNIT_SPEED_FORMAT)), precision_string))
else:
outstring.append('F' + format(float(opVertRapid.getValueAs(UNIT_SPEED_FORMAT)), precision_string))
# store the latest command
lastcommand = command
currLocation.update(c.Parameters)
# Check for Tool Change:
if command == 'M6':
# stop the spindle
currentSpeed = 0
out += linenumber() + "M5\n"
for line in TOOL_CHANGE.splitlines(True):
out += linenumber() + line
# add height offset
if USE_TLO:
tool_height = '\nG43 H' + str(int(c.Parameters['T']))
outstring.append(tool_height)
if command == "message":
if OUTPUT_COMMENTS is False:
out = []
else:
outstring.pop(0) # remove the command
# prepend a line number and append a newline
if len(outstring) >= 1:
if OUTPUT_LINE_NUMBERS:
outstring.insert(0, (linenumber()))
# append the line to the final output
for w in outstring:
out += w.upper() + COMMAND_SPACE
out = out.strip() + "\n"
return out
# print(__name__ + " gcode postprocessor loaded.")
@@ -384,7 +384,7 @@ def export(objectslist, filename, argstring):
# #\better: append iff MODAL == False
# if command == lastcommand:
# outstring.pop(0)
if c.Parameters >= 1:
if len(c.Parameters) >= 1:
for param in params:
# test print("param: " + param + ", command: " + command)
if param in c.Parameters:
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2021 sliptonic <[email protected]> *
# * *
# * 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'])
+123 -17
View File
@@ -22,37 +22,143 @@
import PathScripts.PathToolBit as PathToolBit
import PathTests.PathTestUtils as PathTestUtils
import glob
import os
TestToolDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'Tools')
TestInvalidDir = os.path.join(TestToolDir, 'some', 'silly', 'path', 'that', 'should', 'not', 'exist')
TestToolBitName = 'test-path-tool-bit-bit-00.fctb'
TestToolShapeName = 'test-path-tool-bit-shape-00.fcstd'
TestToolLibraryName = 'test-path-tool-bit-library-00.fctl'
def testToolShape(path = TestToolDir, name = TestToolShapeName):
return os.path.join(path, 'Shape', name)
def testToolBit(path = TestToolDir, name = TestToolBitName):
return os.path.join(path, 'Bit', name)
def testToolLibrary(path = TestToolDir, name = TestToolLibraryName):
return os.path.join(path, 'Library', name)
def printTree(path, indent):
print("{} {}".format(indent, os.path.basename(path)))
if os.path.isdir(path):
if os.path.basename(path).startswith('__'):
print("{} ...".format(indent))
else:
for foo in sorted(glob.glob(os.path.join(path, '*'))):
printTree(foo, "{} ".format(indent))
class TestPathToolBit(PathTestUtils.PathTestBase):
def test00(self):
'''Find a tool shapee from file name'''
def test(self):
'''Log test setup directory structure'''
# Enable this test if there are errors showing up in the build system with the
# paths that work OK locally. It'll print out the directory tree, and if it
# doesn't look right you know where to look for it
print()
print("realpath : {}".format(os.path.realpath(__file__)))
print(" Tools : {}".format(TestToolDir))
print(" dir : {}".format(os.path.dirname(os.path.realpath(__file__))))
printTree(os.path.dirname(os.path.realpath(__file__)), " :")
path = PathToolBit.findShape('endmill.fcstd')
def test00(self):
'''Find a tool shape from file name'''
path = PathToolBit.findToolShape('endmill.fcstd')
self.assertIsNot(path, None)
self.assertNotEqual(path, 'endmill.fcstd')
def test01(self):
'''Find a tool shapee from an invalid absolute path.'''
path = PathToolBit.findShape('/this/is/unlikely/a/valid/path/v-bit.fcstd')
def test01(self):
'''Not find a relative path shape if not stored in default location'''
path = PathToolBit.findToolShape(TestToolShapeName)
self.assertIsNone(path)
def test02(self):
'''Find a relative path shape if it's local to a bit path'''
path = PathToolBit.findToolShape(TestToolShapeName, testToolBit())
self.assertIsNot(path, None)
self.assertNotEqual(path, '/this/is/unlikely/a/valid/path/v-bit.fcstd')
self.assertEqual(path, testToolShape())
def test03(self):
'''Not find a tool shape from an invalid absolute path.'''
path = PathToolBit.findToolShape(testToolShape(TestInvalidDir))
self.assertIsNone(path)
def test04(self):
'''Find a tool shape from a valid absolute path.'''
path = PathToolBit.findToolShape(testToolShape())
self.assertIsNot(path, None)
self.assertEqual(path, testToolShape())
def test10(self):
'''find the relative path of a tool bit'''
shape = 'endmill.fcstd'
path = PathToolBit.findShape(shape)
'''Find a tool bit from file name'''
path = PathToolBit.findToolBit('5mm_Endmill.fctb')
self.assertIsNot(path, None)
self.assertGreater(len(path), len(shape))
rel = PathToolBit.findRelativePathShape(path)
self.assertEqual(rel, shape)
self.assertNotEqual(path, '5mm_Endmill.fctb')
def test11(self):
'''store full path if relative path isn't found'''
path = '/this/is/unlikely/a/valid/path/v-bit.fcstd'
rel = PathToolBit.findRelativePathShape(path)
self.assertEqual(rel, path)
'''Not find a relative path bit if not stored in default location'''
path = PathToolBit.findToolBit(TestToolBitName)
self.assertIsNone(path)
def test12(self):
'''Find a relative path bit if it's local to a library path'''
path = PathToolBit.findToolBit(TestToolBitName, testToolLibrary())
self.assertIsNot(path, None)
self.assertEqual(path, testToolBit())
def test13(self):
'''Not find a tool bit from an invalid absolute path.'''
path = PathToolBit.findToolBit(testToolBit(TestInvalidDir))
self.assertIsNone(path)
def test14(self):
'''Find a tool bit from a valid absolute path.'''
path = PathToolBit.findToolBit(testToolBit())
self.assertIsNot(path, None)
self.assertEqual(path, testToolBit())
def test20(self):
'''Find a tool library from file name'''
path = PathToolBit.findToolLibrary('Default.fctl')
self.assertIsNot(path, None)
self.assertNotEqual(path, 'Default.fctl')
def test21(self):
'''Not find a relative path library if not stored in default location'''
path = PathToolBit.findToolLibrary(TestToolLibraryName)
self.assertIsNone(path)
def test22(self):
'''[skipped] Find a relative path library if it's local to <what?>'''
# this is not a valid test for libraries because t
self.assertTrue(True)
def test23(self):
'''Not find a tool library from an invalid absolute path.'''
path = PathToolBit.findToolLibrary(testToolLibrary(TestInvalidDir))
self.assertIsNone(path)
def test24(self):
'''Find a tool library from a valid absolute path.'''
path = PathToolBit.findToolBit(testToolBit())
self.assertIsNot(path, None)
self.assertEqual(path, testToolBit())
@@ -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
}
+2
View File
@@ -24,6 +24,7 @@ import TestApp
from PathTests.TestPathLog import TestPathLog
from PathTests.TestPathPreferences import TestPathPreferences
from PathTests.TestPathPropertyBag import TestPathPropertyBag
from PathTests.TestPathCore import TestPathCore
#from PathTests.TestPathPost import PathPostTestCases
from PathTests.TestPathGeom import TestPathGeom
@@ -66,4 +67,5 @@ False if TestPathToolBit.__name__ else True
False if TestPathVoronoi.__name__ else True
False if TestPathThreadMilling.__name__ else True
False if TestPathVcarve.__name__ else True
False if TestPathPropertyBag.__name__ else True
+1 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"version": 2,
"name": "Probe004",
"name": "Probe",
"shape": "probe.fcstd",
"parameter": {
"Diameter": "6.0000 mm",
+2 -2
View File
@@ -4,8 +4,8 @@
"shape": "slittingsaw.fcstd",
"parameter": {
"BladeThickness": "3.0000 mm",
"BoltHeight": "3.0000 mm",
"BoltWidth": "8.0000 mm",
"CapHeight": "3.0000 mm",
"CapDiameter": "8.0000 mm",
"Diameter": "76.2000 mm",
"Length": "50.0000 mm",
"ShankDiameter": "19.0500 mm"
+22 -21
View File
@@ -1,6 +1,6 @@
# Tools
Each tool is stored as a JSON file which has the template's path and values for all named constraints of the template.
Each tool is stored as a JSON file which has the shape's path and values for all attributes of the shape.
It also includes all additional parameters and their values.
Storing a tool as a JSON file sounds great but eliminates the option of an accurate thumbnail. On the other hand,
@@ -8,10 +8,10 @@ storing each tool as a `*.fcstd` file requires more space and does not allow for
extensive tool aresenal they might want to script the generation of tools which is easily done for a `*.json` file but
practically impossible for `*.fcstd` files.
When a tool is instantiated in a job the PDN body is created from the template and the constraints are set according
to the values from the JSON file. All additional parameters are created as properties on the object. This provides the
the correct shape and dimensions which can be used to generate a point cloud or mesh for advanced algorithms (and
potentially simulation).
When a tool is instantiated in a job the PDN body is created from the shape and the attributes and constraints are set
according to the values from the JSON file. All additional parameters are created as properties on the object. This
provides the the correct shape and dimensions which can be used to generate a point cloud or mesh for advanced
algorithms (and potentially simulation).
# Tool Libraries
@@ -55,33 +55,34 @@ TechDraw's templates.
## How to create a new tool
1. Set the tool's Label, this will show up in the object tree
1. Select a tool shape from the existing templates. If your tool doesn't exist, you'll have to create a new template,
1. Select a tool shape from the existing shape files. If your tool doesn't exist, you'll have to create a new shape,
see below for details.
1. Each template has its own set of parameters, fill them with the tool's values.
1. Each tool bit shape has its own set of parameters, fill them with the tool's values.
1. Select additional parameters
1. Save the tool under path/file that makes sense to you
## How to create a new tool bit Shape
A tool bit template represents the physical shape of a tool. It does not completely describe the bit - for that some
additional parameters are needed which will be added when an actual bit is parametrized from the template.
The shape file for a tool bit is expected to contain a PD body which represents the tool as a 3d solid. The PD body
should be parametric based on a a PropertyBag object so that, when the properties of the PropertyBag are changed the
solid is updated to the correct representation.
1. Create a new FreeCAD document
1. Open the `PartDesign` workbench, create a body and give the body a label you want to show up in the bit selection.
1. Create a sketch in the XZ plane and draw half the profile of the bit.
* Put the top center of the bit on the origin (0,0)
1. For any constraint serving as a parameter for the tool (like overall Length) create a named constraint
* The name is the label of the input field
* Names are split at CamelCase boundaries into words in the edit dialog
* Use a `;` in the name to add help text which will show up as the entry fields tool tip
* If the tool is used by legacy ops it should at least have one constraint called `Diameter`
* Use construction lines for constraints that are not directly accessible, like `Diameter` and `Angle`
1. Any unnamed constraint will not be editable for a specific tool
1. Once the sketch is fully constrained, close the sketch
1. Rotate the sketch around the z-axis
1. Open the Path workbench and (with the PD body selected) create a PropertyBag,
menu 'Path' -> 'Utils' -> 'Property Bag'
* this creates a PropertyBag object inside the Body (assuming it was selected)
* add properties to which define the tool bit's shape and put those into the group 'Shape'
* add any other properties to the bag which might be useful for the tool bit
1. Construct the body of the tool bit and assign experssions referencing properties from the PropertyBag (in the
`Shape` Group) for all constraints.
* Position the tip of the tool bit on the origin (0,0)
1. Save the document as a new file in the Shape directory
* Before saving the document make sure you have _Save Thumbnail_ selected, and _Add program logo_ deselected in
FreeCAD's preferences.
* Also make sure to switch to _Front View_ and _Fit content to screen_
* Whatever you see when saving the document will end up being the visual representation of the template
* Whatever you see when saving the document will end up being the visual representation of tool bits with this shape
Not that 'Shape' is the only property group which has special meaning for tool bits. All other property groups are
copied verbatim to the ToolBit object when one is created.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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
View File
@@ -0,0 +1,152 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2021 sliptonic <[email protected]> *
# * *
# * 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-')
+1 -2
View File
@@ -28,8 +28,6 @@
#include <Gui/Selection.h>
#include <boost_signals2.hpp>
class Ui_TaskSketcherGeneral;
namespace App {
class Property;
}
@@ -40,6 +38,7 @@ class ViewProvider;
namespace SketcherGui {
class Ui_TaskSketcherGeneral;
class ViewProviderSketch;
class SketcherGeneralWidget : public QWidget
+15 -17
View File
@@ -6493,6 +6493,16 @@ void ViewProviderSketch::UpdateSolverInformation()
.arg(tr("(click to select)"))
.arg(appendRedundantMsg(getSketchObject()->getLastRedundant())));
}
QString partiallyRedundantString;
if(hasPartiallyRedundant) {
partiallyRedundantString = QString::fromLatin1("<br/><font color='royalblue'><span style=\"background-color: #ececec;\">%1<a href=\"#partiallyredundant\"><span style=\" text-decoration: underline; color:#0000ff; background-color: #F8F8FF;\">%2</span></a><br/>%3</span></font><br/>")
.arg(tr("Sketch contains partially redundant constraints "))
.arg(tr("(click to select)"))
.arg(appendPartiallyRedundantMsg(getSketchObject()->getLastPartiallyRedundant()));
}
if (getSketchObject()->getLastSolverStatus() == 0) {
if (dofs == 0) {
// color the sketch as fully constrained if it has geometry (other than the axes)
@@ -6500,31 +6510,19 @@ void ViewProviderSketch::UpdateSolverInformation()
edit->FullyConstrained = true;
if (!hasRedundancies) {
signalSetUp(QString::fromLatin1("<font color='green'><span style=\"color:#008000; background-color: #ececec;\">%1</font></span>").arg(tr("Fully constrained sketch")));
signalSetUp(QString::fromLatin1("<font color='green'><span style=\"color:#008000; background-color: #ececec;\">%1</font></span> %2").arg(tr("Fully constrained sketch")).arg(partiallyRedundantString));
}
}
else if (!hasRedundancies) {
QString infoString;
if (dofs == 1)
infoString = tr("Under-constrained sketch with <a href=\"#dofs\"><span style=\" text-decoration: underline; color:#0000ff; background-color: #F8F8FF;\">1 degree</span></a> of freedom. %1")
.arg(hasPartiallyRedundant?
QString::fromLatin1("<br/><font color='orangered'>%1<a href=\"#partiallyredundant\"><span style=\" text-decoration: underline; color:#0000ff; background-color: #F8F8FF;\">%2</span></a><br/>%3</font><br/>")
.arg(tr("Sketch contains partially redundant constraints "))
.arg(tr("(click to select)"))
.arg(appendPartiallyRedundantMsg(getSketchObject()->getLastPartiallyRedundant()))
: QString());
signalSetUp(tr("Under-constrained sketch with <a href=\"#dofs\"><span style=\" text-decoration: underline; color:#0000ff; background-color: #F8F8FF;\">1 degree</span></a> of freedom. %1")
.arg(partiallyRedundantString));
else
infoString = tr("Under-constrained sketch with <a href=\"#dofs\"><span style=\" text-decoration: underline; color:#0000ff; background-color: #F8F8FF;\">%1 degrees</span></a> of freedom. %2")
signalSetUp(tr("Under-constrained sketch with <a href=\"#dofs\"><span style=\" text-decoration: underline; color:#0000ff; background-color: #F8F8FF;\">%1 degrees</span></a> of freedom. %2")
.arg(dofs)
.arg(hasPartiallyRedundant?
QString::fromLatin1("<br/><font color='orangered'>%1<a href=\"#partiallyredundant\"><span style=\" text-decoration: underline; color:#0000ff; background-color: #F8F8FF;\">%2</span></a><br/>%3</font><br/>")
.arg(tr("Sketch contains partially redundant constraints "))
.arg(tr("(click to select)"))
.arg(appendPartiallyRedundantMsg(getSketchObject()->getLastPartiallyRedundant()))
: QString());
signalSetUp(infoString);
.arg(partiallyRedundantString));
}
signalSolved(QString::fromLatin1("<font color='green'><span style=\"color:#008000; background-color: #ececec;\">%1</font></span>").arg(tr("Solved in %1 sec").arg(getSketchObject()->getLastSolveTime())));
+1 -1
View File
@@ -20,6 +20,6 @@
#* *
#***************************************************************************
import FreeCAD,FreeCADGui
import FreeCAD
rf=FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Start")
rf.SetBool("AllowDownload",True)
+1 -1
View File
@@ -20,7 +20,7 @@
#* *
#***************************************************************************
import FreeCADGui,sys
import FreeCADGui
# MRU will be given before this script is run
rf=FreeCAD.ParamGet("User parameter:BaseApp/Preferences/RecentFiles")
FreeCADGui.loadFile(rf.GetString("MRU"+str(MRU)))
+4 -4
View File
@@ -31,7 +31,7 @@ function load() {
if (allowDownloads == 1) {
// load latest commits
ddiv = document.getElementById("commits");
var ddiv = document.getElementById("commits");
ddiv.innerHTML = "Connecting...";
var tobj=new JSONscriptRequest('https://api.github.com/repos/FreeCAD/FreeCAD/commits?callback=printCommits');
tobj.buildScriptTag(); // Build the script tag
@@ -61,7 +61,7 @@ function printCommits(data) {
// json callback for git commits
ddiv = document.getElementById('commits');
var ddiv = document.getElementById('commits');
ddiv.innerHTML = "Received";
var html = ['<ul>'];
for (var i = 0; i < 25; i++) {
@@ -76,7 +76,7 @@ function printAddons(data) {
// json callback for addons list
ddiv = document.getElementById('addons');
var ddiv = document.getElementById('addons');
ddiv.innerHTML = "Received";
var html = ['<ul class="addonslist">'];
var blacklist = ['addons_installer.FCMacro','FreeCAD-Addon-Details.md','README.md'];
@@ -98,7 +98,7 @@ function printForum(data) {
// json callback for forum posts
ddiv = document.getElementById('forum');
var ddiv = document.getElementById('forum');
ddiv.innerHTML = "Received";
var html = ['<ul>'];
for (var i = 0; i < 25; i++) {
+7 -8
View File
@@ -25,7 +25,7 @@
# the html code of the start page. It is built only once per FreeCAD session for now...
import six
import sys,os,FreeCAD,FreeCADGui,tempfile,time,zipfile,urllib,re
import sys,os,FreeCAD,FreeCADGui,tempfile,time,zipfile,re
from . import TranslationTexts
from PySide import QtCore,QtGui
@@ -107,7 +107,7 @@ def getInfo(filename):
try:
import gnome.ui
import gnomevfs
except:
except Exception:
# alternative method
import hashlib
fhash = hashlib.md5(("file://"+path).encode("utf8")).hexdigest()
@@ -140,7 +140,7 @@ def getInfo(filename):
if filename.lower().endswith(".fcstd"):
try:
zfile=zipfile.ZipFile(filename)
except:
except Exception:
print("Cannot read file: ",filename)
return None
files=zfile.namelist()
@@ -366,7 +366,6 @@ def handle():
# build SECTION_RECENTFILES
SECTION_RECENTFILES = encode("")
rf = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/RecentFiles")
rfcount = rf.GetInt("RecentFiles",0)
SECTION_RECENTFILES = encode("<h2>"+TranslationTexts.T_RECENTFILES+"</h2>")
@@ -493,7 +492,7 @@ def handle():
img = os.path.join(resources_dir,"images/freecad.png")
iconbank[wb] = img
UL_WORKBENCHES += '<li>'
UL_WORKBENCHES += '<img src="file:///'+iconbank[wb]+'">&nbsp;'
UL_WORKBENCHES += '<img src="file:///'+img+'">&nbsp;'
UL_WORKBENCHES += '<a href="https://www.freecadweb.org/wiki/'+wn+'_Workbench">'+wn.replace("ReverseEngineering","ReverseEng")+'</a>'
UL_WORKBENCHES += '</li>'
UL_WORKBENCHES += '</ul>'
@@ -503,19 +502,19 @@ def handle():
try:
import dxfLibrary
except:
except Exception:
pass
else:
wblist.append("dxf-library")
try:
import RebarTools
except:
except Exception:
pass
else:
wblist.append("reinforcement")
try:
import CADExchangerIO
except:
except Exception:
pass
else:
wblist.append("cadexchanger")
+2 -2
View File
@@ -171,7 +171,7 @@ CmdSurfaceGeomFillSurface::CmdSurfaceGeomFillSurface()
sToolTipText = QT_TR_NOOP("Creates a surface from two, three or four boundary edges.");
sWhatsThis = "Surface_GeomFillSurface";
sStatusTip = sToolTipText;
sPixmap = "Surface_BSplineSurface";
sPixmap = "Surface_GeomFillSurface";
}
bool CmdSurfaceGeomFillSurface::isActive(void)
@@ -237,7 +237,7 @@ CmdSurfaceExtendFace::CmdSurfaceExtendFace()
"with its local U and V parameters.");
sWhatsThis = "Surface_ExtendFace";
sStatusTip = sToolTipText;
sPixmap = "Surface_Extend";
sPixmap = "Surface_ExtendFace";
}
void CmdSurfaceExtendFace::activated(int)
+2 -1
View File
@@ -4,8 +4,9 @@
<file>icons/Surface_BSplineSurface.svg</file>
<file>icons/Surface_CurveOnMesh.svg</file>
<file>icons/Surface_Cut.svg</file>
<file>icons/Surface_Extend.svg</file>
<file>icons/Surface_ExtendFace.svg</file>
<file>icons/Surface_Filling.svg</file>
<file>icons/Surface_GeomFillSurface.svg</file>
<file>icons/Surface_Sections.svg</file>
<file>icons/Surface_Sewing.svg</file>
<file>icons/Surface_Surface.svg</file>
File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 187 KiB

@@ -0,0 +1,211 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
id="svg2985"
height="64px"
width="64px">
<title
id="title889">Surface_ExtendFace</title>
<defs
id="defs2987">
<linearGradient
id="linearGradient4387">
<stop
id="stop4389"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop4391"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<linearGradient
id="linearGradient6321">
<stop
id="stop6323"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop6325"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<radialGradient
gradientTransform="translate(-0.23443224,0.23443198)"
gradientUnits="userSpaceOnUse"
r="19.467436"
fy="28.869568"
fx="45.883327"
cy="28.869568"
cx="45.883327"
id="radialGradient3692"
xlink:href="#linearGradient3377" />
<linearGradient
id="linearGradient3377">
<stop
style="stop-color:#faff2b;stop-opacity:1;"
offset="0"
id="stop3379" />
<stop
style="stop-color:#ffaa00;stop-opacity:1;"
offset="1"
id="stop3381" />
</linearGradient>
<linearGradient
id="linearGradient3377-3">
<stop
style="stop-color:#faff2b;stop-opacity:1;"
offset="0"
id="stop3379-8" />
<stop
style="stop-color:#ffaa00;stop-opacity:1;"
offset="1"
id="stop3381-3" />
</linearGradient>
<radialGradient
r="19.467436"
fy="28.869568"
fx="45.883327"
cy="28.869568"
cx="45.883327"
gradientTransform="matrix(0.67067175,0,0,0.64145918,-63.380792,0.83845403)"
gradientUnits="userSpaceOnUse"
id="radialGradient6412"
xlink:href="#linearGradient3377-3" />
<linearGradient
id="linearGradient3036">
<stop
style="stop-color:#ef2929;stop-opacity:1"
offset="0"
id="stop3038" />
<stop
style="stop-color:#a40000;stop-opacity:1"
offset="1"
id="stop3040" />
</linearGradient>
<linearGradient
gradientTransform="matrix(0.96812402,0,0,0.96755864,-0.72057496,-2.6783592)"
xlink:href="#linearGradient1189"
id="linearGradient2095"
x1="47"
y1="9"
x2="7"
y2="28"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1189">
<stop
style="stop-color:#204a87;stop-opacity:1"
offset="0"
id="stop1185" />
<stop
style="stop-color:#729fcf;stop-opacity:1"
offset="1"
id="stop1187" />
</linearGradient>
</defs>
<metadata
id="metadata2990">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Surface_ExtendFace</dc:title>
<dc:creator>
<cc:Agent>
<dc:title>[bitacovir]</dc:title>
</cc:Agent>
</dc:creator>
<dc:title>Part_Shape_from_Mesh</dc:title>
<dc:date>2020/10/03</dc:date>
<dc:relation>http://www.freecadweb.org/wiki/index.php?title=Artwork</dc:relation>
<dc:publisher>
<cc:Agent>
<dc:title>FreeCAD</dc:title>
</cc:Agent>
</dc:publisher>
<dc:identifier />
<dc:rights>
<cc:Agent>
<dc:title>FreeCAD LGPL2+</dc:title>
</cc:Agent>
</dc:rights>
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
<dc:contributor>
<cc:Agent>
<dc:title />
</cc:Agent>
</dc:contributor>
<dc:subject>
<rdf:Bag>
<rdf:li>surface</rdf:li>
</rdf:Bag>
</dc:subject>
<dc:description />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1">
<path
style="display:inline;fill:url(#linearGradient2095);fill-opacity:1;stroke:#0b1521;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 3.1519208,24.413282 17.899691,60.320494 c 14.52186,-34.832109 31.154371,7.93994 42.771854,-25.924611 L 38.972508,3.1269928 C 30.809307,30.407148 17.749067,-0.16084869 3.1519208,24.413282 Z"
id="path3820-1-9" />
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 13.603223,48.657681 Z"
id="path864" />
<path
id="path3820-1-9-6"
d="M 5.3492197,24.626663 18.113409,55.643482 C 32.677175,26.678261 49.470489,63.641047 58.406281,34.501823 L 39.63819,7.4361573 C 30.967406,29.589343 16.774125,4.5857267 5.3492197,24.626663 Z"
style="display:inline;fill:none;fill-opacity:1;stroke:#729fcf;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;stroke:#0b1521;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 9.4369591,17.432017 23.893747,49.402887"
id="path858" />
<path
style="fill:none;stroke:#0b1521;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 40.112066,21.625636 6.018626,-7.896527"
id="path868" />
<path
style="fill:none;stroke:#0b1521;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 34.776081,12.780487 54.665726,44.771608"
id="path860" />
<path
style="fill:none;stroke:#0b1521;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 14.039223,26.683244 7.5864126,33.920856"
id="path862" />
<path
style="fill:none;stroke:#0b1521;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 20.840834,39.937666 13.516022,48.22168"
id="path866" />
<path
style="fill:none;stroke:#0b1521;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 48.04728,34.356857 6.490744,-8.440886"
id="path870" />
<path
id="path856-7"
d="m 14.039223,26.770444 6.278411,13.690423 c 11.170658,-12.42446 17.282205,3.65439 28.078446,-6.01681 L 39.937666,21.102435 c -9.271964,9.611298 -19.414292,-4.508807 -25.898443,5.668009 z"
style="fill:none;stroke:#0b1521;stroke-width:4;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;stroke:#fce94f;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 14.039223,26.770444 6.278411,13.690423 c 11.170658,-12.42446 17.282205,3.65439 28.078446,-6.01681 L 39.937666,21.102435 c -9.271964,9.611298 -19.414292,-4.508807 -25.898443,5.668009 z"
id="path856" />
<path
style="fill:none;stroke:#204a87;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 42.16127,29.233848 -2.790405,-4.360007 c -3.15688,2.379363 -6.869436,2.749406 -10.703817,1.983804"
id="path889" />
<path
style="fill:none;stroke:#204a87;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 22.650238,41.703469 c 4.877832,-5.031355 7.162943,-3.688493 11.706619,-2.746805"
id="path891" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

@@ -0,0 +1,171 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
id="svg2985"
height="64px"
width="64px">
<title
id="title889">Surface_GeomFillSurface</title>
<defs
id="defs2987">
<linearGradient
id="linearGradient4387">
<stop
id="stop4389"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop4391"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<linearGradient
id="linearGradient6321">
<stop
id="stop6323"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop6325"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<radialGradient
gradientTransform="translate(-0.23443224,0.23443198)"
gradientUnits="userSpaceOnUse"
r="19.467436"
fy="28.869568"
fx="45.883327"
cy="28.869568"
cx="45.883327"
id="radialGradient3692"
xlink:href="#linearGradient3377" />
<linearGradient
id="linearGradient3377">
<stop
style="stop-color:#faff2b;stop-opacity:1;"
offset="0"
id="stop3379" />
<stop
style="stop-color:#ffaa00;stop-opacity:1;"
offset="1"
id="stop3381" />
</linearGradient>
<linearGradient
id="linearGradient3377-3">
<stop
style="stop-color:#faff2b;stop-opacity:1;"
offset="0"
id="stop3379-8" />
<stop
style="stop-color:#ffaa00;stop-opacity:1;"
offset="1"
id="stop3381-3" />
</linearGradient>
<radialGradient
r="19.467436"
fy="28.869568"
fx="45.883327"
cy="28.869568"
cx="45.883327"
gradientTransform="matrix(0.67067175,0,0,0.64145918,-63.380792,0.83845403)"
gradientUnits="userSpaceOnUse"
id="radialGradient6412"
xlink:href="#linearGradient3377-3" />
<linearGradient
id="linearGradient3036">
<stop
style="stop-color:#ef2929;stop-opacity:1"
offset="0"
id="stop3038" />
<stop
style="stop-color:#a40000;stop-opacity:1"
offset="1"
id="stop3040" />
</linearGradient>
<linearGradient
gradientTransform="matrix(0.96812402,0,0,0.96755864,-0.72057496,-2.6783592)"
xlink:href="#linearGradient1189"
id="linearGradient2095"
x1="47"
y1="9"
x2="7"
y2="28"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1189">
<stop
style="stop-color:#204a87;stop-opacity:1"
offset="0"
id="stop1185" />
<stop
style="stop-color:#729fcf;stop-opacity:1"
offset="1"
id="stop1187" />
</linearGradient>
</defs>
<metadata
id="metadata2990">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Surface_GeomFillSurface</dc:title>
<dc:creator>
<cc:Agent>
<dc:title>[bitacovir]</dc:title>
</cc:Agent>
</dc:creator>
<dc:title>Part_Shape_from_Mesh</dc:title>
<dc:date>2020/10/03</dc:date>
<dc:relation>http://www.freecadweb.org/wiki/index.php?title=Artwork</dc:relation>
<dc:publisher>
<cc:Agent>
<dc:title>FreeCAD</dc:title>
</cc:Agent>
</dc:publisher>
<dc:identifier />
<dc:rights>
<cc:Agent>
<dc:title>FreeCAD LGPL2+</dc:title>
</cc:Agent>
</dc:rights>
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
<dc:contributor>
<cc:Agent>
<dc:title />
</cc:Agent>
</dc:contributor>
<dc:subject>
<rdf:Bag>
<rdf:li>surface</rdf:li>
</rdf:Bag>
</dc:subject>
<dc:description />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1">
<path
style="display:inline;fill:url(#linearGradient2095);fill-opacity:1;stroke:#302b00;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 3.1519208,24.413282 17.899691,60.320494 c 14.52186,-34.832109 31.154371,7.93994 42.771854,-25.924611 L 38.972508,3.1269928 C 30.809307,30.407148 17.749067,-0.16084869 3.1519208,24.413282 Z"
id="path3820-1-9" />
<path
id="path3820-1-9-6"
d="M 6.0274776,24.811642 18.175069,54.286966 C 33.540412,25.445065 49.03887,62.222871 57.666363,34.995102 L 39.76151,8.7926732 C 30.227488,30.267601 17.452383,4.7707061 6.0274776,24.811642 Z"
style="display:inline;fill:none;fill-opacity:1;stroke:#fce94f;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path857"
d="M 8.3549048,24.972224 18.621264,49.266191 C 31.716699,27.213603 50.17526,55.741537 55.247193,35.269413 L 40.202199,12.856071 C 30.336022,29.759966 16.753608,10.080699 8.3549048,24.972224 Z"
style="fill:none;stroke:#302b00;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Some files were not shown because too many files have changed in this diff Show More