From dac1dd5fc5ad0f7df16fa311dc713b6df2958e22 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 26 Aug 2019 19:47:08 -0700 Subject: [PATCH 01/52] Added tool bits concept and some initial templates --- src/Mod/Path/Tools/.gitignore | 2 + src/Mod/Path/Tools/README.md | 81 ++++++++++++++++++ .../Path/Tools/Template/drill-straight.fcstd | Bin 0 -> 10098 bytes .../Tools/Template/endmill-straight.fcstd | Bin 0 -> 10496 bytes src/Mod/Path/Tools/Template/v-bit.fcstd | Bin 0 -> 11847 bytes 5 files changed, 83 insertions(+) create mode 100644 src/Mod/Path/Tools/.gitignore create mode 100644 src/Mod/Path/Tools/README.md create mode 100644 src/Mod/Path/Tools/Template/drill-straight.fcstd create mode 100644 src/Mod/Path/Tools/Template/endmill-straight.fcstd create mode 100644 src/Mod/Path/Tools/Template/v-bit.fcstd diff --git a/src/Mod/Path/Tools/.gitignore b/src/Mod/Path/Tools/.gitignore new file mode 100644 index 0000000000..334e20e2c9 --- /dev/null +++ b/src/Mod/Path/Tools/.gitignore @@ -0,0 +1,2 @@ +*.fcstd1 +*.FCStd1 diff --git a/src/Mod/Path/Tools/README.md b/src/Mod/Path/Tools/README.md new file mode 100644 index 0000000000..9a5b356b3f --- /dev/null +++ b/src/Mod/Path/Tools/README.md @@ -0,0 +1,81 @@ +# Tools + +Each tool is stored as a JSON file which has the template's path and values for all named constaints of the template. +It also includes all additional parameters and their values. + +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 a +body with the correct shape and dimensions which can be used to generate a point cloud or mesh for advanced algorithms +(and potentially simulation). + +# Tool Libraries + +Due to each tool being stored in its own file and the storage/organization of those files being quite flexible the +importance of a tool library for organisational purposes is quite diminished. The user is free to organise their tools +in whichever directory hierarchy they see fit and can also name them as best fits their use and organisation. A +_tool library_ is nevertheless a great representation for a physical grouping of tools, such as in an automatic tool +changer. + +A tool library is a (JSON) file with a mapping of tool id to the path of the tool file. As a consequence each tool +can be in multiple libraries and doesn't have an `id` of it's own. The `id` is a property of the library. + +If a tool from a tool library (or an entire tool library) is added to a job it retains its `id` from the library as a +property. Adding a tool bit directly rsults in the tool getting the next free id assigned. + +# Tool Controllers + +They largely stay the same as they are today. As an additional feature it should be possible to _copy_ a TC, which +allows for easy feed/speed changes for the same tool. + +Abover requirement highlights one change though, that the `id` should be a property of the Bit, and not of the TC. +There are two requirements that are currently mapped to a single `id`. There needs to be an identification of which +TC is being used by a certain op, and which tool number to use for a `M6` command. + +# Paths and Extensibility + +The following directory structure is used for supplied (shipped with FreeCAD) tools: +``` + Tools + + Bit + + Library + + Template +``` +Strictly speaking a user is free to store their tools wherever they want and however they want. By default the file +dialog will open the corresponding directory (depending on context), or whichever directory the user opened last. + +Above directory structure with the most common default tools shipped with FreeCAD should be installed analogous to +TechDraw's templates. + +## How to create a new tool + +1. Set the tool's Label, this will show up in the object tree +1. Select a tool shape from the existing templates. If your tool doesn't exist, you'll have to create a new template, + see below for details. +1. Each template has its own set of parameters, fill them with the tool's values. +1. Select additional parameters +1. Save the tool under the name that makes sense to you + + +## How to create a new tool bit Template + +A tool bit template represents the physical shape of a tool. It does not completely desribe the bit, for that some +additional parameters are needed which will be added when an actual bit is parametrized from the template. + +1. Create a new FreeCAD document +1. Open the `PartDesign` workbench, create a body and give the body the name you want to show up in the bit selection. +1. Create a sketch in the XZ plane and draw half the profile of the bit. + * Put the top center of the bit on the origin (0,0) +1. For any constraint serving as a parameter for the tool (like overall Length) create a named constraint + * The name is the label of the input field + * Names are split at CamelCase boundaries into words in the edit dialog + * Use a `;` in the name to add help text which will show up as the entry fields tool tip + * If the tool is used by legacy ops it should at least have one constraint called `Diameter` + * Use construction lines for constraints that are not directly accessible, like `Diameter` and `Angle` +1. Any unnamed constraint will not be editable for a specific tool +1. Once the sketch is fully constrained, close the sketch +1. Rotate the sketch around the z-axis +1. Save the document as a new file in the Template directory + * Before saving the document make sure you have _Save Thumbnail_ selected, and _Add program logo_ deselected in + FreeCAD's preferences. + * Also make sure to switch to _Front View_ and _Fit content to screen_ + * Whatever you see when saving the document will end up being the visual representation of the template diff --git a/src/Mod/Path/Tools/Template/drill-straight.fcstd b/src/Mod/Path/Tools/Template/drill-straight.fcstd new file mode 100644 index 0000000000000000000000000000000000000000..0e33b9a2d54061b3a11e42b369b98dd9d6c7dadf GIT binary patch literal 10098 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 W06wqhAOpngjEqEtJ`n#Ey8i>$S;$}j literal 0 HcmV?d00001 diff --git a/src/Mod/Path/Tools/Template/endmill-straight.fcstd b/src/Mod/Path/Tools/Template/endmill-straight.fcstd new file mode 100644 index 0000000000000000000000000000000000000000..696296467b4a8449d5aa880632963add0f8a160f GIT binary patch literal 10496 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 v=-*WTSNM0;gw+40`i+K!`ZX?;x7X-fX; Date: Wed, 4 Sep 2019 22:56:28 -0700 Subject: [PATCH 02/52] Basic editor and shape update. --- src/Mod/Path/CMakeLists.txt | 1 + src/Mod/Path/Gui/Resources/Path.qrc | 1 + .../Gui/Resources/panels/ToolBitEditor.ui | 170 +++++++++++++++ src/Mod/Path/PathScripts/PathGui.py | 37 +--- src/Mod/Path/PathScripts/PathMillFace.py | 5 +- src/Mod/Path/PathScripts/PathSetupSheetGui.py | 4 +- .../PathScripts/PathSetupSheetOpPrototype.py | 41 ++-- .../PathSetupSheetOpPrototypeGui.py | 34 ++- src/Mod/Path/PathScripts/PathToolBit.py | 202 ++++++++++++++++++ src/Mod/Path/PathScripts/PathToolBitEdit.py | 127 +++++++++++ src/Mod/Path/PathScripts/PathToolBitGui.py | 153 +++++++++++++ src/Mod/Path/PathScripts/PathUtil.py | 38 +++- .../Path/Tools/Template/drill-straight.fcstd | Bin 10098 -> 10158 bytes .../Tools/Template/endmill-straight.fcstd | Bin 10496 -> 10501 bytes src/Mod/Path/Tools/Template/v-bit.fcstd | Bin 11847 -> 11989 bytes 15 files changed, 744 insertions(+), 69 deletions(-) create mode 100644 src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui create mode 100644 src/Mod/Path/PathScripts/PathToolBit.py create mode 100644 src/Mod/Path/PathScripts/PathToolBitEdit.py create mode 100644 src/Mod/Path/PathScripts/PathToolBitGui.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 83937b084f..15d65c9db1 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -101,6 +101,7 @@ SET(PathScripts_SRCS PathScripts/PathStop.py PathScripts/PathSurface.py PathScripts/PathSurfaceGui.py + PathScripts/PathToolBitEdit.py PathScripts/PathToolController.py PathScripts/PathToolControllerGui.py PathScripts/PathToolEdit.py diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index 19246750df..e6eae76dbc 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -107,6 +107,7 @@ panels/PointEdit.ui panels/SetupGlobal.ui panels/SetupOp.ui + panels/ToolBitEditor.ui panels/ToolEditor.ui panels/ToolLibraryEditor.ui panels/TaskPathSimulator.ui diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui new file mode 100644 index 0000000000..06b0ccad45 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui @@ -0,0 +1,170 @@ + + + Form + + + + 0 + 0 + 411 + 886 + + + + Form + + + + + + Tool Bit + + + + + + Name + + + + + + + 50 + + + Display Name + + + + + + + Type + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + ... + + + + + + + + + + + + + Bit Parameter + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Point/Tip Angle + + + + + + + 180° + + + ° + + + + + + + Cutting Edge Height + + + + + + + 0.00 + + + mm + + + + + + + + + + + + + + 210 + 297 + + + + Image + + + Qt::AlignCenter + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Gui::InputField + QLineEdit +

Gui/InputField.h
+ + + + + diff --git a/src/Mod/Path/PathScripts/PathGui.py b/src/Mod/Path/PathScripts/PathGui.py index ed2e9e757d..98cf988106 100644 --- a/src/Mod/Path/PathScripts/PathGui.py +++ b/src/Mod/Path/PathScripts/PathGui.py @@ -25,6 +25,7 @@ import FreeCAD import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog +import PathScripts.PathUtil as PathUtil import PySide @@ -44,34 +45,6 @@ if LOGLEVEL: else: PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -def _getProperty(obj, prop): - o = obj - attr = obj - name = None - for name in prop.split('.'): - o = attr - if not hasattr(o, name): - break - attr = getattr(o, name) - - if o == attr: - PathLog.warning(translate('PathGui', "%s has no property %s (%s))") % (obj.Label, prop, name)) - return (None, None, None) - - #PathLog.debug("found property %s of %s (%s: %s)" % (prop, obj.Label, name, attr)) - return(o, attr, name) - -def getProperty(obj, prop): - '''getProperty(obj, prop) ... answer obj's property defined by its canonical name.''' - o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable - return attr - -def setProperty(obj, prop, value): - '''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.''' - o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable - if o and name: - setattr(o, name, value) - def updateInputField(obj, prop, widget, onBeforeChange=None): '''updateInputField(obj, prop, widget) ... update obj's property prop with the value of widget. The property's value is only assigned if the new value differs from the current value. @@ -82,13 +55,13 @@ If onBeforeChange is specified it is called before a new value is assigned to th Returns True if a new value was assigned, False otherwise (new value is the same as the current). ''' value = FreeCAD.Units.Quantity(widget.text()).Value - attr = getProperty(obj, prop) + attr = PathUtil.getProperty(obj, prop) attrValue = attr.Value if hasattr(attr, 'Value') else attr if not PathGeom.isRoughly(attrValue, value): PathLog.debug("updateInputField(%s, %s): %.2f -> %.2f" % (obj.Label, prop, attr, value)) if onBeforeChange: onBeforeChange(obj) - setProperty(obj, prop, value) + PathUtil.setProperty(obj, prop, value) return True return False @@ -107,7 +80,7 @@ The spin box gets bound to a given property and supports update in both directio self.widget = widget self.prop = prop self.onBeforeChange = onBeforeChange - attr = getProperty(self.obj, self.prop) + attr = PathUtil.getProperty(self.obj, self.prop) if attr is not None: if hasattr(attr, 'Value'): widget.setProperty('unit', attr.getUserPreferred()[2]) @@ -134,7 +107,7 @@ If no value is provided the value of the bound property is used. quantity can be of type Quantity or Float.''' if self.valid: if quantity is None: - quantity = getProperty(self.obj, self.prop) + quantity = PathUtil.getProperty(self.obj, self.prop) value = quantity.Value if hasattr(quantity, 'Value') else quantity self.widget.setProperty('rawValue', value) diff --git a/src/Mod/Path/PathScripts/PathMillFace.py b/src/Mod/Path/PathScripts/PathMillFace.py index 362fd18d17..cb6c02f080 100644 --- a/src/Mod/Path/PathScripts/PathMillFace.py +++ b/src/Mod/Path/PathScripts/PathMillFace.py @@ -80,7 +80,8 @@ class ObjectFace(PathPocketBase.ObjectPocket): # default depths calculation not correct for facing if prop == "Base": job = PathUtils.findParentJob(obj) - obj.OpStartDepth = job.Stock.Shape.BoundBox.ZMax + if job: + obj.OpStartDepth = job.Stock.Shape.BoundBox.ZMax if len(obj.Base) >= 1: print('processing') @@ -95,7 +96,7 @@ class ObjectFace(PathPocketBase.ObjectPocket): # Otherwise, top of part. obj.OpFinalDepth = Part.makeCompound(sublist).BoundBox.ZMax - else: + elif job: obj.OpFinalDepth = job.Proxy.modelBoundBox(job).ZMax def areaOpShapes(self, obj): diff --git a/src/Mod/Path/PathScripts/PathSetupSheetGui.py b/src/Mod/Path/PathScripts/PathSetupSheetGui.py index 1537f870c2..5b5a75b2d6 100644 --- a/src/Mod/Path/PathScripts/PathSetupSheetGui.py +++ b/src/Mod/Path/PathScripts/PathSetupSheetGui.py @@ -306,9 +306,9 @@ class GlobalEditor(object): def getFields(self): def updateExpression(name, widget): value = str(widget.text()) - val = PathGui.getProperty(self.obj, name) + val = PathUtil.getProperty(self.obj, name) if val != value: - PathGui.setProperty(self.obj, name, value) + PathUtil.setProperty(self.obj, name, value) updateExpression('StartDepthExpression', self.form.setupStartDepthExpr) updateExpression('FinalDepthExpression', self.form.setupFinalDepthExpr) diff --git a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py index 146d70821a..8c5c3e4b8d 100644 --- a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py +++ b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py @@ -106,6 +106,10 @@ class PropertyQuantity(Property): return Property.displayString(self) return self.value.getUserPreferred()[0] +class PropertyAngle(PropertyQuantity): + def typeString(self): + return "Angle" + class PropertyDistance(PropertyQuantity): def typeString(self): return "Distance" @@ -137,24 +141,25 @@ class PropertyString(Property): class OpPrototype(object): PropertyType = { - 'App::PropertyBool': PropertyBool, - 'App::PropertyDistance': PropertyDistance, - 'App::PropertyEnumeration': PropertyEnumeration, - 'App::PropertyFloat': PropertyFloat, - 'App::PropertyFloatConstraint': Property, - 'App::PropertyFloatList': Property, - 'App::PropertyInteger': PropertyInteger, - 'App::PropertyIntegerList': PropertyInteger, - 'App::PropertyLength': PropertyLength, - 'App::PropertyLink': Property, - 'App::PropertyLinkList': Property, - 'App::PropertyLinkSubListGlobal': Property, - 'App::PropertyPercent': PropertyPercent, - 'App::PropertyString': PropertyString, - 'App::PropertyStringList': Property, - 'App::PropertyVectorDistance': Property, - 'App::PropertyVectorList': Property, - 'Part::PropertyPartShape': Property, + 'App::PropertyAngle': PropertyAngle, + 'App::PropertyBool': PropertyBool, + 'App::PropertyDistance': PropertyDistance, + 'App::PropertyEnumeration': PropertyEnumeration, + 'App::PropertyFloat': PropertyFloat, + 'App::PropertyFloatConstraint': Property, + 'App::PropertyFloatList': Property, + 'App::PropertyInteger': PropertyInteger, + 'App::PropertyIntegerList': PropertyInteger, + 'App::PropertyLength': PropertyLength, + 'App::PropertyLink': Property, + 'App::PropertyLinkList': Property, + 'App::PropertyLinkSubListGlobal': Property, + 'App::PropertyPercent': PropertyPercent, + 'App::PropertyString': PropertyString, + 'App::PropertyStringList': Property, + 'App::PropertyVectorDistance': Property, + 'App::PropertyVectorList': Property, + 'Part::PropertyPartShape': Property, } def __init__(self, name): diff --git a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototypeGui.py b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototypeGui.py index c215d0057d..f89b7b40ed 100644 --- a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototypeGui.py +++ b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototypeGui.py @@ -112,6 +112,21 @@ class _PropertyStringEditor(_PropertyEditor): def setModelData(self, widget): self.prop.setValue(widget.text()) +class _PropertyAngleEditor(_PropertyEditor): + '''Editor for angle values - uses a line edit''' + + def widget(self, parent): + return QtGui.QLineEdit(parent) + + def setEditorData(self, widget): + quantity = self.prop.getValue() + if quantity is None: + quantity = FreeCAD.Units.Quantity(0, FreeCAD.Units.Angle) + widget.setText(quantity.getUserPreferred()[0]) + + def setModelData(self, widget): + self.prop.setValue(FreeCAD.Units.Quantity(widget.text())) + class _PropertyLengthEditor(_PropertyEditor): '''Editor for length values - uses a line edit.''' @@ -174,15 +189,16 @@ class _PropertyFloatEditor(_PropertyEditor): self.prop.setValue(widget.value()) _EditorFactory = { - PathSetupSheetOpPrototype.Property: None, - PathSetupSheetOpPrototype.PropertyBool: _PropertyBoolEditor, - PathSetupSheetOpPrototype.PropertyDistance: _PropertyLengthEditor, - PathSetupSheetOpPrototype.PropertyEnumeration: _PropertyEnumEditor, - PathSetupSheetOpPrototype.PropertyFloat: _PropertyFloatEditor, - PathSetupSheetOpPrototype.PropertyInteger: _PropertyIntegerEditor, - PathSetupSheetOpPrototype.PropertyLength: _PropertyLengthEditor, - PathSetupSheetOpPrototype.PropertyPercent: _PropertyPercentEditor, - PathSetupSheetOpPrototype.PropertyString: _PropertyStringEditor, + PathSetupSheetOpPrototype.Property: None, + PathSetupSheetOpPrototype.PropertyAngle: _PropertyAngleEditor, + PathSetupSheetOpPrototype.PropertyBool: _PropertyBoolEditor, + PathSetupSheetOpPrototype.PropertyDistance: _PropertyLengthEditor, + PathSetupSheetOpPrototype.PropertyEnumeration: _PropertyEnumEditor, + PathSetupSheetOpPrototype.PropertyFloat: _PropertyFloatEditor, + PathSetupSheetOpPrototype.PropertyInteger: _PropertyIntegerEditor, + PathSetupSheetOpPrototype.PropertyLength: _PropertyLengthEditor, + PathSetupSheetOpPrototype.PropertyPercent: _PropertyPercentEditor, + PathSetupSheetOpPrototype.PropertyString: _PropertyStringEditor, } def Editor(prop): diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py new file mode 100644 index 0000000000..03f400f449 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +import Part +import PathScripts.PathGeom as PathGeom +import PathScripts.PathLog as PathLog +import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype +import PathScripts.PathUtil as PathUtil +import PySide +import Sketcher +import math +import zipfile + +__title__ = "Tool bits." +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Class to deal with and represent a tool bit." + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule() + +def translate(context, text, disambig=None): + return PySide.QtCore.QCoreApplication.translate(context, text, disambig) + +ParameterTypeConstraint = { + 'Angle': 'App::PropertyAngle', + 'Distance': 'App::PropertyLength', + 'DistanceX': 'App::PropertyLength', + 'DistanceY': 'App::PropertyLength', + 'Radius': 'App::PropertyLength' + } + + +def updateConstraint(sketch, name, value): + for i, constraint in enumerate(sketch.Constraints): + if constraint.Name.split(';')[0] == name: + if constraint.Type in ['DistanceX', 'DistanceY', 'Distance', 'Radius']: + if not PathGeom.isRoughly(constraint.Value, value.Value): + PathLog.track(name, constraint.Type, 'update', i) + constr = Sketcher.Constraint(constraint.Type, constraint.First, value) + sketch.delConstraint(i) + sketch.recompute() + n = sketch.addConstraint(constr) + sketch.renameConstraint(n, constraint.Name) + else: + PathLog.track(name, constraint.Type, 'unchanged') + else: + print(constraint.Name, constraint.Type) + break + +PropertyGroupBit = 'Bit' + +class ToolBit(object): + + def __init__(self, obj, templateFile): + PathLog.track(obj.Label, templateFile) + self.obj = obj + obj.addProperty('App::PropertyFile', 'BitTemplate', 'Base', translate('PathToolBit', 'Template for bit shape')) + obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) + if templateFile is not None: + obj.BitTemplate = templateFile + self._setupBitFromTemplate(obj) + self.onDocumentRestored(obj) + + def __getstate__(self): + return None + + def __setstate__(self, state): + for obj in FreeCAD.ActiveDocument.Objects: + if hasattr(obj, 'Proxy') and obj.Proxy == self: + self.obj = obj + break + return None + + def bitPropertyNames(self, obj): + return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupBit] + + def onDocumentRestored(self, obj): + obj.setEditorMode('BitTemplate', 1) + obj.setEditorMode('BitBody', 2) + obj.setEditorMode('Shape', 2) + + for prop in self.bitPropertyNames(obj): + obj.setEditorMode(prop, 1) + + def onChanged(self, obj, prop): + PathLog.track(obj.Label, prop) + if prop == 'BitTemplate' and not 'Restore' in obj.State: + self._setupBitFromTemplate(obj) + #elif obj.getGroupOfProperty(prop) == PropertyGroupBit: + # self._updateBitShape(obj, [prop]) + + def _updateBitShape(self, obj, properties=None): + if not properties: + properties = self.bitPropertyNames(obj) + for prop in properties: + for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']: + PathLog.track(obj.Label, sketch.Label, prop) + updateConstraint(sketch, prop, obj.getPropertyByName(prop)) + self._copyBitShape(obj) + + def _copyBitShape(self, obj): + obj.Document.recompute() + if obj.BitBody and obj.BitBody.Shape: + obj.Shape = obj.BitBody.Shape + else: + obj.Shape = Part.Shape() + + def _loadBitBody(self, obj, path=None): + if not path: + path = obj.BitTemplate + docOpened = False + doc = None + for d in FreeCAD.listDocuments(): + if FreeCAD.getDocument(d).FileName == path: + doc = FreeCAD.getDocument(d) + break + if doc is None: + doc = FreeCAD.open(path) + docOpened = True + return (doc, docOpened) + + def _removeBitBody(self, obj): + if obj.BitBody: + obj.BitBody.removeObjectsFromDocument() + obj.Document.removeObject(obj.BitBody.Name) + obj.BitBody = None + + def _deleteBitSetup(self, obj): + PathLog.track(obj.Label) + self._removeBitBody(obj) + self._copyBitShape(obj) + for prop in self.bitPropertyNames(obj): + obj.removeProperty(prop) + + def _setupBitFromTemplate(self, obj, path=None): + (doc, docOpened) = self._loadBitBody(obj, path) + + obj.Label = doc.RootObjects[0].Label + self._deleteBitSetup(obj) + obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) + if docOpened: + FreeCAD.closeDocument(doc.Name) + + if obj.BitBody.ViewObject: + obj.BitBody.ViewObject.Visibility = False + self._copyBitShape(obj) + + for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']: + for constraint in [c for c in sketch.Constraints if c.Name != '']: + typ = ParameterTypeConstraint.get(constraint.Type) + PathLog.track(constraint, typ) + if typ is not None: + parts = [p.strip() for p in constraint.Name.split(';')] + prop = parts[0] + desc = '' + if len(parts) > 1: + desc = parts[1] + obj.addProperty(typ, prop, PropertyGroupBit, desc) + value = constraint.Value + if constraint.Type == 'Angle': + value = value * 180 / math.pi + PathUtil.setProperty(obj, prop, constraint.Value) + + def getBitThumbnail(self, obj): + if obj.BitTemplate: + with open(obj.BitTemplate, 'rb') as fd: + zf = zipfile.ZipFile(fd) + pf = zf.open('thumbnails/Thumbnail.png', 'r') + data = pf.read() + pf.close() + return data + else: + return None + + +def Create(name = 'ToolBit', templateFile=None): + obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name) + obj.Proxy = ToolBit(obj, templateFile) + return obj diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py new file mode 100644 index 0000000000..f2fe552580 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +import FreeCADGui +import Path +import PathScripts.PathGui as PathGui +import PathScripts.PathLog as PathLog +import PathScripts.PathToolBit as PathToolBit +import copy +import math +import re + +from PySide import QtGui + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +ParameterTypeConstraint = { + 'Angle': 'Angle', + 'Distance': 'Length', + 'DistanceX': 'Length', + 'DistanceY': 'Length', + 'Radius': 'Length' + } + +ParameterTypeProperty = { + 'Length': 'App::PropertyLength', + 'Angle': 'App::PropertyAngle' + } + +class ToolBitEditor: + '''UI and controller for editing a ToolBit. + The controller embeds the UI to the parentWidget which has to have a layout attached to it. + ''' + + def __init__(self, tool, parentWidget=None): + self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitEditor.ui") + + if parentWidget: + self.form.setParent(parentWidget) + parentWidget.layout().addWidget(self.form) + + self.tool = tool + if not tool.BitTemplate: + self.tool.BitTemplate = 'src/Mod/Path/Tools/Template/endmill-straight.fcstd' + self.setupTool(self.tool) + + def setupTool(self, tool): + layout = self.form.bitParams.layout() + for i in range(layout.rowCount() - 1, -1, -1): + layout.removeRow(i) + editor = {} + ui = FreeCADGui.UiLoader() + for name in tool.PropertiesList: + if tool.getGroupOfProperty(name) == PathToolBit.PropertyGroupBit: + qsb = ui.createWidget('Gui::QuantitySpinBox') + editor[name] = PathGui.QuantitySpinBox(qsb, tool, name) + label = QtGui.QLabel(re.sub('([A-Z][a-z]+)', r' \1', re.sub('([A-Z]+)', r' \1', name))) + #if parameter.get('Desc'): + # qsb.setToolTip(parameter['Desc']) + layout.addRow(label, qsb) + self.bitEditor = editor + + def accept(self): + self.refresh() + + def reject(self): + pass + + def updateUI(self): + PathLog.track() + self.form.toolName.setText(self.tool.Label) + self.form.templatePath.setText(self.tool.BitTemplate) + + for editor in self.bitEditor: + self.bitEditor[editor].updateSpinBox() + + def updateTemplate(self): + self.tool.BitTemplate = str(self.form.templatePath.text()) + self.setupTool(self.tool) + + def updateTool(self): + PathLog.track() + self.tool.Label = str(self.form.toolName.text()) + self.tool.BitTemplate = str(self.form.templatePath.text()) + + for editor in self.bitEditor: + self.bitEditor[editor].updateProperty() + + self.tool.Proxy._updateBitShape(self.tool) + + def refresh(self): + PathLog.track() + self.form.blockSignals(True) + self.updateTool() + self.updateUI() + self.form.blockSignals(False) + + def setupUI(self): + PathLog.track() + self.updateUI() + + self.form.toolName.editingFinished.connect(self.refresh) + self.form.templatePath.editingFinished.connect(self.updateTemplate) diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py new file mode 100644 index 0000000000..cec2d86980 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +import FreeCADGui +import PathScripts.PathGui as PathGui +import PathScripts.PathIconViewProvider as PathIconViewProvider +import PathScripts.PathLog as PathLog +import PathScripts.PathToolBit as PathToolBit +import PathScripts.PathToolBitEdit as PathToolBitEdit +import PathScripts.PathUtil as PathUtil + +from PySide import QtCore, QtGui + +__title__ = "Tool Bit UI" +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Task panel editor for a ToolBit" + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + +class ViewProvider(object): + '''ViewProvider for a ToolBit. + It's sole job is to provide an icon and invoke the TaskPanel on edit.''' + + def __init__(self, vobj, name): + PathLog.track(name, vobj.Object) + self.icon = name + self.obj = vobj.Object + self.vobj = vobj + vobj.Proxy = self + + def attach(self, vobj): + PathLog.track(vobj.Object) + self.vobj = vobj + self.obj = vobj.Object + + def getIcon(self): + png = self.obj.Proxy.getBitThumbnail(self.obj) + if png: + pixmap = QtGui.QPixmap() + pixmap.loadFromData(png, "PNG") + return QtGui.QIcon(pixmap) + return ':/icons/Path-ToolChange.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): + if self.obj.BitBody: + return [self.obj.BitBody] + return [] + + def doubleClicked(self, vobj): + self.setEdit(vobj) + +class TaskPanel: + '''TaskPanel for the SetupSheet - if it is being edited directly.''' + + def __init__(self, vobj): + PathLog.track(vobj.Object.Label) + self.vobj = vobj + self.obj = vobj.Object + self.editor = PathToolBitEdit.ToolBitEditor(self.obj) + self.form = self.editor.form + FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Edit ToolBit")) + + def reject(self): + self.editor.reject() + FreeCAD.ActiveDocument.abortTransaction() + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + + def accept(self): + self.editor.accept() + + FreeCAD.ActiveDocument.commitTransaction() + FreeCADGui.ActiveDocument.resetEdit() + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + + def getFields(self): + self.editor.getFields() + + def updateUI(self): + self.editor.updateUI() + + def updateModel(self): + self.editor.updateTool() + FreeCAD.ActiveDocument.recompute() + + def setFields(self): + self.editor.setFields() + + def setupUi(self): + self.editor.setupUI() + +def Create(name = 'ToolBit'): + '''Create(name = 'ToolBit') ... creates a new tool bit''' + FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Create ToolBit")) + tool = PathToolBit.Create(name) + PathIconViewProvider.Attach(tool.ViewObject, name) + return tool + +PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider) diff --git a/src/Mod/Path/PathScripts/PathUtil.py b/src/Mod/Path/PathScripts/PathUtil.py index 3fa449106c..9633c6833d 100644 --- a/src/Mod/Path/PathScripts/PathUtil.py +++ b/src/Mod/Path/PathScripts/PathUtil.py @@ -34,14 +34,40 @@ other than PathLog, then it probably doesn't belong here. import six import PathScripts.PathLog as PathLog +import PySide -LOGLEVEL = False +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -if LOGLEVEL: - PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) - PathLog.trackModule(PathLog.thisModule()) -else: - PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +def translate(context, text, disambig=None): + return PySide.QtCore.QCoreApplication.translate(context, text, disambig) + +def _getProperty(obj, prop): + o = obj + attr = obj + name = None + for name in prop.split('.'): + o = attr + if not hasattr(o, name): + break + attr = getattr(o, name) + + if o == attr: + PathLog.warning(translate('PathGui', "%s has no property %s (%s))") % (obj.Label, prop, name)) + return (None, None, None) + + #PathLog.debug("found property %s of %s (%s: %s)" % (prop, obj.Label, name, attr)) + return(o, attr, name) + +def getProperty(obj, prop): + '''getProperty(obj, prop) ... answer obj's property defined by its canonical name.''' + o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable + return attr + +def setProperty(obj, prop, value): + '''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.''' + o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable + if o and name: + setattr(o, name, value) # NotValidBaseTypeIds = ['Sketcher::SketchObject'] NotValidBaseTypeIds = [] diff --git a/src/Mod/Path/Tools/Template/drill-straight.fcstd b/src/Mod/Path/Tools/Template/drill-straight.fcstd index 0e33b9a2d54061b3a11e42b369b98dd9d6c7dadf..1de5f441816c2e0cf7755070e633971d4ba8a2f0 100644 GIT binary patch delta 8019 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 delta 6478 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 F{|~dq|9}7h diff --git a/src/Mod/Path/Tools/Template/v-bit.fcstd b/src/Mod/Path/Tools/Template/v-bit.fcstd index 3fb173c4b626830a26e78f10e4300c279f9752de..b9370436666181512e652be6ceddf43b37683653 100644 GIT binary patch delta 10641 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 NlY@zyoteGs{{v5<6jcBK delta 10519 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 From 5db8710721c9168bb5974016b543afa49ea26070 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 6 Sep 2019 21:38:08 -0700 Subject: [PATCH 03/52] Set property editor mode on creation. --- src/Mod/Path/PathScripts/PathToolBit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 03f400f449..d15dfe0969 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -179,6 +179,7 @@ class ToolBit(object): 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 From 9bc978b52a9b8e1ffa3df7312f6fcdd89e4ea159 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 8 Sep 2019 22:14:12 -0700 Subject: [PATCH 04/52] Added editing functionality to the tool editor --- src/Mod/Path/PathScripts/PathToolBit.py | 15 ++++--- src/Mod/Path/PathScripts/PathToolBitEdit.py | 37 ++++++++++++------ src/Mod/Path/PathScripts/PathToolBitGui.py | 7 ++-- .../Path/Tools/Template/drill-straight.fcstd | Bin 10158 -> 10111 bytes src/Mod/Path/Tools/Template/v-bit.fcstd | Bin 11989 -> 11981 bytes 5 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index d15dfe0969..881746910a 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -56,18 +56,23 @@ ParameterTypeConstraint = { 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']: + constr = Sketcher.Constraint(constraint.Type, constraint.First, value) + elif constraint.Type in ['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) - constr = Sketcher.Constraint(constraint.Type, constraint.First, 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') - else: - print(constraint.Name, constraint.Type) break PropertyGroupBit = 'Bit' @@ -183,7 +188,7 @@ class ToolBit(object): value = constraint.Value if constraint.Type == 'Angle': value = value * 180 / math.pi - PathUtil.setProperty(obj, prop, constraint.Value) + PathUtil.setProperty(obj, prop, value) def getBitThumbnail(self, obj): if obj.BitTemplate: diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index f2fe552580..0464a2b019 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -37,19 +37,7 @@ from PySide import QtGui PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) PathLog.trackModule(PathLog.thisModule()) - -ParameterTypeConstraint = { - 'Angle': 'Angle', - 'Distance': 'Length', - 'DistanceX': 'Length', - 'DistanceY': 'Length', - 'Radius': 'Length' - } - -ParameterTypeProperty = { - 'Length': 'App::PropertyLength', - 'Angle': 'App::PropertyAngle' - } +LastPath = 'src/Mod/Path/Tools/Template' class ToolBitEditor: '''UI and controller for editing a ToolBit. @@ -83,6 +71,11 @@ class ToolBitEditor: # qsb.setToolTip(parameter['Desc']) layout.addRow(label, qsb) self.bitEditor = editor + img = tool.Proxy.getBitThumbnail(tool) + if img: + self.form.image.setPixmap(QtGui.QPixmap(QtGui.QImage.fromData(img))) + else: + self.form.image.setPixmap(QtGui.QPixmap()) def accept(self): self.refresh() @@ -101,6 +94,10 @@ class ToolBitEditor: def updateTemplate(self): self.tool.BitTemplate = str(self.form.templatePath.text()) self.setupTool(self.tool) + self.form.toolName.setText(self.tool.Label) + + for editor in self.bitEditor: + self.bitEditor[editor].updateSpinBox() def updateTool(self): PathLog.track() @@ -119,9 +116,23 @@ class ToolBitEditor: self.updateUI() self.form.blockSignals(False) + def selectTemplate(self): + global LastPath + path = self.tool.BitTemplate + if not path: + path = LastPath + foo = QtGui.QFileDialog.getOpenFileName(QtGui.QApplication.activeWindow(), + "Path - Tool Template", + path, + "*.fcstd")[0] + if foo: + self.form.templatePath.setText(foo) + self.updateTemplate() + def setupUI(self): PathLog.track() self.updateUI() self.form.toolName.editingFinished.connect(self.refresh) self.form.templatePath.editingFinished.connect(self.updateTemplate) + self.form.templateSet.clicked.connect(self.selectTemplate) diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index cec2d86980..92e08cd0c7 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -83,15 +83,16 @@ class ViewProvider(object): def setEdit(self, vobj, mode=0): # pylint: disable=unused-argument PathLog.track() - taskPanel = TaskPanel(vobj) + self.taskPanel = TaskPanel(vobj) FreeCADGui.Control.closeDialog() - FreeCADGui.Control.showDialog(taskPanel) - taskPanel.setupUi() + FreeCADGui.Control.showDialog(self.taskPanel) + self.taskPanel.setupUi() return True def unsetEdit(self, vobj, mode): # pylint: disable=unused-argument FreeCADGui.Control.closeDialog() + self.taskPanel = None return def claimChildren(self): diff --git a/src/Mod/Path/Tools/Template/drill-straight.fcstd b/src/Mod/Path/Tools/Template/drill-straight.fcstd index 1de5f441816c2e0cf7755070e633971d4ba8a2f0..275b401e34853c53e168a00bd8a7df7cd7e156d3 100644 GIT binary patch delta 6662 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 HANultF0@e= diff --git a/src/Mod/Path/Tools/Template/v-bit.fcstd b/src/Mod/Path/Tools/Template/v-bit.fcstd index b9370436666181512e652be6ceddf43b37683653..97fee679d9eb013331688959c7f1bd1e8595ec3f 100644 GIT binary patch delta 7650 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

2s9 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 Date: Sun, 8 Sep 2019 22:20:53 -0700 Subject: [PATCH 05/52] Fixed typo --- src/Mod/Path/Tools/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Mod/Path/Tools/README.md b/src/Mod/Path/Tools/README.md index 9a5b356b3f..654ca2b258 100644 --- a/src/Mod/Path/Tools/README.md +++ b/src/Mod/Path/Tools/README.md @@ -27,7 +27,7 @@ property. Adding a tool bit directly rsults in the tool getting the next free id They largely stay the same as they are today. As an additional feature it should be possible to _copy_ a TC, which allows for easy feed/speed changes for the same tool. -Abover requirement highlights one change though, that the `id` should be a property of the Bit, and not of the TC. +Above requirement highlights one change though, that the `id` should be a property of the Bit, and not of the TC. There are two requirements that are currently mapped to a single `id`. There needs to be an identification of which TC is being used by a certain op, and which tool number to use for a `M6` command. @@ -40,6 +40,7 @@ The following directory structure is used for supplied (shipped with FreeCAD) to + Library + Template ``` + Strictly speaking a user is free to store their tools wherever they want and however they want. By default the file dialog will open the corresponding directory (depending on context), or whichever directory the user opened last. From e2deb45df55e13d16950bc39dfccdd7929d8ae23 Mon Sep 17 00:00:00 2001 From: markus Date: Sat, 28 Sep 2019 21:46:16 -0700 Subject: [PATCH 06/52] Load template during editing but unload it afterwards --- src/Mod/Path/PathScripts/PathToolBit.py | 30 ++++++++++++++------- src/Mod/Path/PathScripts/PathToolBitEdit.py | 3 +++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 881746910a..fefd634bd9 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -57,9 +57,7 @@ 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']: - constr = Sketcher.Constraint(constraint.Type, constraint.First, value) - elif constraint.Type in ['Angle']: + 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) @@ -118,13 +116,14 @@ class ToolBit(object): # self._updateBitShape(obj, [prop]) def _updateBitShape(self, obj, properties=None): - if not properties: - properties = self.bitPropertyNames(obj) - for prop in properties: - for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']: - PathLog.track(obj.Label, sketch.Label, prop) - updateConstraint(sketch, prop, obj.getPropertyByName(prop)) - self._copyBitShape(obj) + if not obj.BitBody is None: + if not properties: + properties = self.bitPropertyNames(obj) + for prop in properties: + for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']: + PathLog.track(obj.Label, sketch.Label, prop) + updateConstraint(sketch, prop, obj.getPropertyByName(prop)) + self._copyBitShape(obj) def _copyBitShape(self, obj): obj.Document.recompute() @@ -160,6 +159,17 @@ class ToolBit(object): for prop in self.bitPropertyNames(obj): obj.removeProperty(prop) + def loadBitBody(self, obj): + self._removeBitBody(obj) + (doc, opened) = self._loadBitBody(obj) + obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) + if opened: + FreeCAD.closeDocument(doc.Name) + self._updateBitShape(obj) + + def unloadBitBody(self, obj): + self._removeBitBody(obj) + def _setupBitFromTemplate(self, obj, path=None): (doc, docOpened) = self._loadBitBody(obj, path) diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index 0464a2b019..4c546477cb 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -54,6 +54,7 @@ class ToolBitEditor: self.tool = tool if not tool.BitTemplate: self.tool.BitTemplate = 'src/Mod/Path/Tools/Template/endmill-straight.fcstd' + self.tool.Proxy.loadBitBody(self.tool) self.setupTool(self.tool) def setupTool(self, tool): @@ -79,8 +80,10 @@ class ToolBitEditor: def accept(self): self.refresh() + self.tool.Proxy.unloadBitBody(self.tool) def reject(self): + self.tool.Proxy.unloadBitBody(self.tool) pass def updateUI(self): From c824200cbe97ac46feb90cf593f39870fa258aef Mon Sep 17 00:00:00 2001 From: markus Date: Sun, 29 Sep 2019 00:01:24 -0700 Subject: [PATCH 07/52] Add separator after path context menues --- src/Mod/Path/InitGui.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 83ff50ab2c..bca94eef9f 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -143,6 +143,7 @@ class PathWorkbench (Workbench): def ContextMenu(self, recipient): import PathScripts + menuAppended = False if len(FreeCADGui.Selection.getSelection()) == 1: obj = FreeCADGui.Selection.getSelection()[0] if obj.isDerivedFrom("Path::Feature"): @@ -153,8 +154,10 @@ class PathWorkbench (Workbench): self.appendContextMenu("", ["Refresh_Path"]) if "Job" in selectedName: self.appendContextMenu("", ["Path_ExportTemplate"]) + menuAppended = True if isinstance (obj.Proxy, PathScripts.PathOp.ObjectOp): self.appendContextMenu("", ["Path_OperationCopy", "Path_OpActiveToggle"]) + menuAppended = True if obj.isDerivedFrom("Path::Feature"): if "Profile" in selectedName or "Contour" in selectedName or "Dressup" in selectedName: self.appendContextMenu("", "Separator") @@ -162,6 +165,9 @@ class PathWorkbench (Workbench): #self.appendContextMenu("", ["Set_EndPoint"]) for cmd in self.dressupcmds: self.appendContextMenu("", [cmd]) + menuAppended = True + if menuAppended: + self.appendContextMenu("", "Separator") Gui.addWorkbench(PathWorkbench()) From ece4d52962ec288f63cb6786d76124e1f1506ce2 Mon Sep 17 00:00:00 2001 From: markus Date: Sun, 29 Sep 2019 00:38:50 -0700 Subject: [PATCH 08/52] Updated readme --- src/Mod/Path/Tools/README.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Mod/Path/Tools/README.md b/src/Mod/Path/Tools/README.md index 654ca2b258..c080052065 100644 --- a/src/Mod/Path/Tools/README.md +++ b/src/Mod/Path/Tools/README.md @@ -1,12 +1,17 @@ # Tools -Each tool is stored as a JSON file which has the template's path and values for all named constaints of the template. +Each tool is stored as a JSON file which has the template's path and values for all named constraints of the template. It also includes all additional parameters and their values. +Storing a tool as a JSON file sounds great but eliminates the option of an accurate thumbnail. On the other hand, +storing each tool as a `*.fcstd` file requires more space and does not allow for generating tools. If one has an +extensive tool aresenal they might want to script the generation of tools which is easily done for a `*.json` file but +practically impossible for `*.fcstd` files. + When a tool is instantiated in a job the PDN body is created from the template and the constraints are set according -to the values from the JSON file. All additional parameters are created as properties on the object. This provides a -body with the correct shape and dimensions which can be used to generate a point cloud or mesh for advanced algorithms -(and potentially simulation). +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 @@ -54,16 +59,16 @@ TechDraw's templates. see below for details. 1. Each template has its own set of parameters, fill them with the tool's values. 1. Select additional parameters -1. Save the tool under the name that makes sense to you +1. Save the tool under path/file that makes sense to you ## How to create a new tool bit Template -A tool bit template represents the physical shape of a tool. It does not completely desribe the bit, for that some +A tool bit template represents the physical shape of a tool. It does not completely desribe the bit - for that some additional parameters are needed which will be added when an actual bit is parametrized from the template. 1. Create a new FreeCAD document -1. Open the `PartDesign` workbench, create a body and give the body the name you want to show up in the bit selection. +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 From 815ecee63072b22db914749b75f73c1ff6ae656b Mon Sep 17 00:00:00 2001 From: markus Date: Sun, 29 Sep 2019 19:30:46 -0700 Subject: [PATCH 09/52] Added storing and loading of tools in json files --- src/Mod/Path/PathScripts/PathToolBit.py | 53 +++++++++++++++++++--- src/Mod/Path/PathScripts/PathToolBitGui.py | 12 ++++- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index fefd634bd9..b3956e8e8a 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -30,6 +30,7 @@ import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype import PathScripts.PathUtil as PathUtil import PySide import Sketcher +import json import math import zipfile @@ -82,6 +83,7 @@ class ToolBit(object): self.obj = obj obj.addProperty('App::PropertyFile', 'BitTemplate', 'Base', translate('PathToolBit', 'Template for bit shape')) obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) + obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) if templateFile is not None: obj.BitTemplate = templateFile self._setupBitFromTemplate(obj) @@ -103,6 +105,7 @@ class ToolBit(object): def onDocumentRestored(self, obj): obj.setEditorMode('BitTemplate', 1) obj.setEditorMode('BitBody', 2) + obj.setEditorMode('File', 1) obj.setEditorMode('Shape', 2) for prop in self.bitPropertyNames(obj): @@ -159,13 +162,15 @@ class ToolBit(object): for prop in self.bitPropertyNames(obj): obj.removeProperty(prop) - def loadBitBody(self, obj): - self._removeBitBody(obj) - (doc, opened) = self._loadBitBody(obj) - obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) - if opened: - FreeCAD.closeDocument(doc.Name) - self._updateBitShape(obj) + def loadBitBody(self, obj, force=False): + if force or not obj.BitBody: + if force: + self._removeBitBody(obj) + (doc, opened) = self._loadBitBody(obj) + obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) + if opened: + FreeCAD.closeDocument(doc.Name) + self._updateBitShape(obj) def unloadBitBody(self, obj): self._removeBitBody(obj) @@ -211,6 +216,40 @@ class ToolBit(object): else: return None + def saveToFile(self, obj, path, setFile=True): + try: + data = {} + data['version'] = 1 + data['name'] = obj.Label + data['template'] = obj.BitTemplate + params = {} + for prop in self.bitPropertyNames(obj): + params[prop] = PathUtil.getProperty(obj, prop).UserString + data['parameter'] = params + with open(path, 'w') as fp: + json.dump(data, fp, indent=' ') + if setFile: + obj.File = path + return True + except (OSError, IOError) as e: + PathLog.error("Could not save tool %s to %s (%s)" % (obj.Label, path, e)) + raise + +def CreateFrom(path, name = 'ToolBit'): + try: + with open(path, 'r') as fp: + data = json.load(fp) + obj = Create(name, data['template']) + obj.Label = data['name'] + params = data['parameter'] + for prop in params: + PathUtil.setProperty(obj, prop, params[prop]) + obj.Proxy._updateBitShape(obj) + obj.Proxy.unloadBitBody(obj) + return obj + except (OSError, IOError) as e: + PathLog.error("%s not a valid tool file (%s)" % (path, e)) + raise def Create(name = 'ToolBit', templateFile=None): obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name) diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 92e08cd0c7..1fe0932d07 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -145,10 +145,20 @@ class TaskPanel: self.editor.setupUI() def Create(name = 'ToolBit'): - '''Create(name = 'ToolBit') ... creates a new tool bit''' + '''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.Create(name) PathIconViewProvider.Attach(tool.ViewObject, name) + FreeCAD.ActiveDocument.commitTransaction() + return tool + +def CreateFrom(path, name = 'ToolBit'): + '''CreateFrom(path, name = 'ToolBit') ... creates an instance of a tool stored in path''' + FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit instance')) + tool = PathToolBit.CreateFrom(path, name) + PathIconViewProvider.Attach(tool.ViewObject, name) + FreeCAD.ActiveDocument.commitTransaction() return tool PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider) From 54390967c626b7053550e642d076e141eb94ce1d Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Wed, 16 Oct 2019 21:32:44 -0700 Subject: [PATCH 10/52] Added ToolBit sources to installation files --- src/Mod/Path/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 15d65c9db1..cb1cc64b79 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -101,7 +101,9 @@ SET(PathScripts_SRCS PathScripts/PathStop.py PathScripts/PathSurface.py PathScripts/PathSurfaceGui.py + PathScripts/PathToolBit.py PathScripts/PathToolBitEdit.py + PathScripts/PathToolBitGui.py PathScripts/PathToolController.py PathScripts/PathToolControllerGui.py PathScripts/PathToolEdit.py From f8887d5e123bfc10323f8d369abc9475f2304eb6 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Thu, 17 Oct 2019 20:15:38 -0700 Subject: [PATCH 11/52] Add support for ToolBit to ToolController --- .../Path/PathScripts/PathToolController.py | 40 ++++++++-------- .../Path/PathScripts/PathToolControllerGui.py | 46 +++++++++++++------ 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index a9ced44132..1e0b42c07b 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -61,21 +61,24 @@ class ToolControllerTemplate: VertRapid = 'vrapid' class ToolController: - def __init__(self, obj, tool=1): - PathLog.track('tool: {}'.format(tool)) - obj.addProperty("App::PropertyIntegerConstraint", "ToolNumber", "Tool", QtCore.QT_TRANSLATE_NOOP("App::Property", "The active tool")) + def __init__(self, obj, cTool=False): + PathLog.track('tool: {}'.format(cTool)) + + obj.addProperty("App::PropertyIntegerConstraint", "ToolNumber", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The active tool")) obj.ToolNumber = (0, 0, 10000, 1) - obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("App::Property", "The tool used by this controller")) + if cTool: + obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) + else: + obj.addProperty("App::PropertyLink", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) - obj.addProperty("App::PropertyFloat", "SpindleSpeed", "Tool", QtCore.QT_TRANSLATE_NOOP("App::Property", "The speed of the cutting spindle in RPM")) - obj.addProperty("App::PropertyEnumeration", "SpindleDir", "Tool", QtCore.QT_TRANSLATE_NOOP("App::Property", "Direction of spindle rotation")) + obj.addProperty("App::PropertyFloat", "SpindleSpeed", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The speed of the cutting spindle in RPM")) + obj.addProperty("App::PropertyEnumeration", "SpindleDir", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Direction of spindle rotation")) obj.SpindleDir = ['Forward', 'Reverse'] - obj.addProperty("App::PropertySpeed", "VertFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("App::Property", "Feed rate for vertical moves in Z")) - obj.addProperty("App::PropertySpeed", "HorizFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("App::Property", "Feed rate for horizontal moves")) - obj.addProperty("App::PropertySpeed", "VertRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("App::Property", "Rapid rate for vertical moves in Z")) - obj.addProperty("App::PropertySpeed", "HorizRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("App::Property", "Rapid rate for horizontal moves")) - obj.Proxy = self + obj.addProperty("App::PropertySpeed", "VertFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Feed rate for vertical moves in Z")) + obj.addProperty("App::PropertySpeed", "HorizFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Feed rate for horizontal moves")) + obj.addProperty("App::PropertySpeed", "VertRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Rapid rate for vertical moves in Z")) + obj.addProperty("App::PropertySpeed", "HorizRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Rapid rate for horizontal moves")) obj.setEditorMode('Placement', 2) def onDocumentRestored(self, obj): @@ -157,15 +160,17 @@ class ToolController: PathLog.track() return obj.Tool - + def usesLegacyTool(self, obj): + '''returns True if the tool being controlled is a legacy tool''' + return isinstance(obj.Tool, Path.Tool) def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=True): PathLog.track(tool, toolNumber) obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) obj.Label = name + obj.Proxy = ToolController(obj, tool is None or isinstance(tool, Path.Tool)) - ToolController(obj) if FreeCAD.GuiUp and assignViewProvider: ViewProvider(obj.ViewObject) @@ -176,6 +181,7 @@ def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=Tr tool.CuttingEdgeHeight = 15.0 tool.ToolType = "EndMill" tool.Material = "HighSpeedSteel" + obj.Tool = tool obj.ToolNumber = toolNumber return obj @@ -184,12 +190,8 @@ def FromTemplate(template, assignViewProvider=True): PathLog.track() name = template.get(ToolControllerTemplate.Name, ToolControllerTemplate.Label) - obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) - tc = ToolController(obj) - if FreeCAD.GuiUp and assignViewProvider: - ViewProvider(obj.ViewObject) - - tc.setFromTemplate(obj, template) + obj = Create(name, assignViewProvider=True) + obj.Proxy.setFromTemplate(obj, template) return obj diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 17821d500b..bfc87c6495 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -113,6 +113,12 @@ class ViewProvider: action.triggered.connect(self.setEdit) menu.addAction(action) + def claimChildren(self): + obj = self.vobj.Object + if not obj.Proxy.usesLegacyTool(obj): + return [obj.Tool] + return [] + def Create(name = 'Default Tool', tool=None, toolNumber=1): PathLog.track(tool, toolNumber) @@ -153,7 +159,12 @@ class ToolControllerEditor(object): self.vertRapid = PathGui.QuantitySpinBox(self.form.vertRapid, obj, 'VertRapid') self.horizRapid = PathGui.QuantitySpinBox(self.form.horizRapid, obj, 'HorizRapid') - self.editor = PathToolEdit.ToolEditor(obj.Tool, self.form.toolEditor) + if obj.Proxy.usesLegacyTool(obj): + self.editor = PathToolEdit.ToolEditor(obj.Tool, self.form.toolEditor) + else: + self.editor = None + self.form.toolBox.widget(1).hide() + self.form.toolBox.removeItem(1) def updateUi(self): tc = self.obj @@ -168,7 +179,8 @@ class ToolControllerEditor(object): if index >= 0: self.form.spindleDirection.setCurrentIndex(index) - self.editor.updateUI() + if self.editor: + self.editor.updateUI() def updateToolController(self): tc = self.obj @@ -182,8 +194,9 @@ class ToolControllerEditor(object): tc.SpindleSpeed = self.form.spindleSpeed.value() tc.SpindleDir = self.form.spindleDirection.currentText() - self.editor.updateTool() - tc.Tool = self.editor.tool + if self.editor: + self.editor.updateTool() + tc.Tool = self.editor.tool except Exception as e: # pylint: disable=broad-except PathLog.error(translate("PathToolController", "Error updating TC: %s") % e) @@ -196,7 +209,8 @@ class ToolControllerEditor(object): self.form.blockSignals(False) def setupUi(self): - self.editor.setupUI() + if self.editor: + self.editor.setupUI() self.form.tcName.editingFinished.connect(self.refresh) self.form.horizFeed.editingFinished.connect(self.refresh) @@ -219,13 +233,13 @@ class TaskPanel: FreeCADGui.ActiveDocument.resetEdit() FreeCADGui.Control.closeDialog() - if self.toolrep is not None: + if self.toolrep: FreeCAD.ActiveDocument.removeObject(self.toolrep.Name) FreeCAD.ActiveDocument.recompute() def reject(self): FreeCADGui.Control.closeDialog() - if self.toolrep is not None: + if self.toolrep: FreeCAD.ActiveDocument.removeObject(self.toolrep.Name) FreeCAD.ActiveDocument.recompute() @@ -236,11 +250,12 @@ class TaskPanel: def setFields(self): self.editor.updateUi() - tool = self.obj.Tool - radius = tool.Diameter / 2 - length = tool.CuttingEdgeHeight - t = Part.makeCylinder(radius, length) - self.toolrep.Shape = t + if self.toolrep: + tool = self.obj.Tool + radius = tool.Diameter / 2 + length = tool.CuttingEdgeHeight + t = Part.makeCylinder(radius, length) + self.toolrep.Shape = t def edit(self, item, column): # pylint: disable=unused-argument @@ -253,9 +268,10 @@ class TaskPanel: FreeCAD.ActiveDocument.recompute() def setupUi(self): - t = Part.makeCylinder(1, 1) - self.toolrep = FreeCAD.ActiveDocument.addObject("Part::Feature", "tool") - self.toolrep.Shape = t + if self.editor.editor: + t = Part.makeCylinder(1, 1) + self.toolrep = FreeCAD.ActiveDocument.addObject("Part::Feature", "tool") + self.toolrep.Shape = t self.setFields() self.editor.setupUi() From 2e1bea0237c278c748125fb2e3966b51cd627d4f Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Thu, 17 Oct 2019 22:25:49 -0700 Subject: [PATCH 12/52] Added search path and preferences support for tools --- src/Mod/Path/CMakeLists.txt | 35 +++++++++++ src/Mod/Path/PathScripts/PathPostProcessor.py | 2 +- src/Mod/Path/PathScripts/PathPreferences.py | 60 +++++++++++++++++-- src/Mod/Path/PathScripts/PathToolBit.py | 34 +++++++++-- src/Mod/Path/PathTests/TestPathPreferences.py | 60 +++++++++++++++++++ src/Mod/Path/PathTests/TestPathToolBit.py | 45 ++++++++++++++ src/Mod/Path/TestPathApp.py | 4 ++ 7 files changed, 230 insertions(+), 10 deletions(-) create mode 100644 src/Mod/Path/PathTests/TestPathPreferences.py create mode 100644 src/Mod/Path/PathTests/TestPathToolBit.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index cb1cc64b79..f700376e8c 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -136,6 +136,17 @@ SET(PathScripts_post_SRCS PathScripts/post/smoothie_post.py ) +SET(Tools_Bit_SRCS +) + +SET(Tools_Library_SRCS +) + +SET(Tools_Template_SRCS + Tools/Template/drill-straight.fcstd + Tools/Template/endmill-straight.fcstd + Tools/Template/v-bit.fcstd +) SET(PathTests_SRCS PathTests/__init__.py @@ -181,6 +192,9 @@ SET(Path_Images SET(all_files ${PathScripts_SRCS} ${PathScripts_post_SRCS} + ${Tools_Bit_SRCS} + ${Tools_Library_SRCS} + ${Tools_Template_SRCS} ${Path_Images} ) @@ -221,6 +235,27 @@ INSTALL( Mod/Path/PathScripts/post ) +INSTALL( + FILES + ${Tools_Bit_SRCS} + DESTINATION + Mod/Path/Tools/Bit +) + +INSTALL( + FILES + ${Tools_Library_SRCS} + DESTINATION + Mod/Path/Tools/Library +) + +INSTALL( + FILES + ${Tools_Template_SRCS} + DESTINATION + Mod/Path/Tools/Template +) + INSTALL( FILES ${PathImages_Ops} diff --git a/src/Mod/Path/PathScripts/PathPostProcessor.py b/src/Mod/Path/PathScripts/PathPostProcessor.py index 82143bf998..605786f845 100644 --- a/src/Mod/Path/PathScripts/PathPostProcessor.py +++ b/src/Mod/Path/PathScripts/PathPostProcessor.py @@ -38,7 +38,7 @@ class PostProcessor: def load(cls, processor): PathLog.track(processor) syspath = sys.path - paths = PathPreferences.searchPaths() + paths = PathPreferences.searchPathsPost() paths.extend(sys.path) sys.path = paths diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py index b2c0de8d43..9aa1c13f7d 100644 --- a/src/Mod/Path/PathScripts/PathPreferences.py +++ b/src/Mod/Path/PathScripts/PathPreferences.py @@ -41,6 +41,10 @@ PostProcessorBlacklist = "PostProcessorBlacklist" PostProcessorOutputFile = "PostProcessorOutputFile" PostProcessorOutputPolicy = "PostProcessorOutputPolicy" +LastPathToolBit = "LastPathToolBit" +LastPathToolLibrary = "LastPathToolLibrary" +LastPathToolTemplate = "LastPathToolTemplate" + # Linear tolerance to use when generating Paths, eg when tessellating geometry GeometryTolerance = "GeometryTolerance" LibAreaCurveAccuracy = "LibAreaCurveAccuarcy" @@ -52,14 +56,16 @@ def preferences(): return FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path") def pathScriptsSourcePath(): - return FreeCAD.getHomePath() + ("Mod/Path/PathScripts/") + return os.path.join(FreeCAD.getHomePath(), "Mod/Path/PathScripts/") -def pathScriptsPostSourcePath(): - return pathScriptsSourcePath() + ("/post/") +def pathDefaultToolsPath(sub=None): + if sub: + return os.path.join(FreeCAD.getHomePath(), "Mod/Path/Tools/", sub) + return os.path.join(FreeCAD.getHomePath(), "Mod/Path/Tools/") def allAvailablePostProcessors(): allposts = [] - for path in searchPaths(): + for path in searchPathsPost(): posts = [ str(os.path.split(os.path.splitext(p)[0])[1][:-5]) for p in glob.glob(path + '/*_post.py')] allposts.extend(posts) allposts.sort() @@ -108,10 +114,38 @@ def searchPaths(): if p: paths.append(p) paths.append(macroFilePath()) - paths.append(pathScriptsPostSourcePath()) + return paths + +def searchPathsPost(): + paths = [] + p = defaultFilePath() + if p: + paths.append(p) + paths.append(macroFilePath()) + paths.append(os.path.join(pathScriptsSourcePath(), "post/")) paths.append(pathScriptsSourcePath()) return paths +def searchPathsTool(sub='Bit'): + paths = [] + + if 'Bit' == sub: + paths.append(lastPathToolBit()) + if 'Library' == sub: + paths.append(lastPathToolLibrary()) + if 'Template' == sub: + paths.append(lastPathToolTemplate()) + + def appendPath(p, sub): + if p: + paths.append(os.path.join(p, 'Tools', sub)) + paths.append(os.path.join(p, sub)) + paths.append(p) + appendPath(defaultFilePath(), sub) + appendPath(macroFilePath(), sub) + appendPath(os.path.join(FreeCAD.getHomePath(), "Mod/Path/"), sub) + return paths + def defaultJobTemplate(): template = preferences().GetString(DefaultJobTemplate) if 'xml' not in template: @@ -165,3 +199,19 @@ def setDefaultTaskPanelLayout(style): def experimentalFeaturesEnabled(): return preferences().GetBool(EnableExperimentalFeatures, False) + +def lastPathToolBit(): + return preferences().GetString(LastPathToolBit, pathDefaultToolsPath('Bit')) +def setLastPathToolBit(path): + return preferences().SetString(LastPathToolBit, path) + +def lastPathToolLibrary(): + return preferences().GetString(LastPathToolLibrary, pathDefaultToolsPath('Library')) +def setLastPathToolLibrary(path): + return preferences().SetString(LastPathToolLibrary, path) + +def lastPathToolTemplate(): + return preferences().GetString(LastPathToolTemplate, pathDefaultToolsPath('Template')) +def setLastPathToolTemplate(path): + return preferences().SetString(LastPathToolTemplate, path) + diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index b3956e8e8a..25825a1e67 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -26,12 +26,14 @@ import FreeCAD import Part import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog +import PathScripts.PathPreferences as PathPreferences import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype import PathScripts.PathUtil as PathUtil import PySide import Sketcher import json import math +import os import zipfile __title__ = "Tool bits." @@ -54,6 +56,28 @@ ParameterTypeConstraint = { } +def _findTool(path, typ): + if os.path.exists(path): + return path + + def searchFor(pname, fname): + if fname: + for p in PathPreferences.searchPathsTool(typ): + f = os.path.join(p, fname) + if os.path.exists(f): + return f + if pname and '/' != pname: + ppname, pfname = os.path.split(pname) + ffname = os.path.join(pfname, fname) if fname else pfname + return searchFor(ppname, ffname) + return None + + return searchFor(path, '') + +def findTemplate(path): + '''findTemplate(path) ... search for path, full and partially in all known template directories.''' + return _findTool(path, 'Template') + def updateConstraint(sketch, name, value): for i, constraint in enumerate(sketch.Constraints): if constraint.Name.split(';')[0] == name: @@ -136,16 +160,18 @@ class ToolBit(object): obj.Shape = Part.Shape() def _loadBitBody(self, obj, path=None): - if not path: - path = obj.BitTemplate + p = path if path else obj.BitTemplate docOpened = False doc = None for d in FreeCAD.listDocuments(): - if FreeCAD.getDocument(d).FileName == path: + if FreeCAD.getDocument(d).FileName == p: doc = FreeCAD.getDocument(d) break if doc is None: - doc = FreeCAD.open(path) + p = findTemplate(p) + if not path and p != obj.BitTemplate: + obj.BitTemplate = p + doc = FreeCAD.open(p) docOpened = True return (doc, docOpened) diff --git a/src/Mod/Path/PathTests/TestPathPreferences.py b/src/Mod/Path/PathTests/TestPathPreferences.py new file mode 100644 index 0000000000..3696473125 --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathPreferences.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import PathScripts.PathPreferences as PathPreferences +import PathTests.PathTestUtils as PathTestUtils + +class TestPathPreferences(PathTestUtils.PathTestBase): + + def test00(self): + '''There is at least one search path.''' + + paths = PathPreferences.searchPaths() + self.assertGreater(len(paths), 0) + + def test01(self): + '''PathScripts is part of the posts search path.''' + paths = PathPreferences.searchPathsPost() + self.assertEqual(len([p for p in paths if p.endswith('/PathScripts/')]), 1) + + def test02(self): + '''PathScripts/post is part of the posts search path.''' + paths = PathPreferences.searchPathsPost() + self.assertEqual(len([p for p in paths if p.endswith('/PathScripts/post/')]), 1) + + def test03(self): + '''Available post processors include linuxcnc, grbl and opensbp.''' + posts = PathPreferences.allAvailablePostProcessors() + self.assertTrue('linuxcnc' in posts) + self.assertTrue('grbl' in posts) + self.assertTrue('opensbp' in posts) + + + def test10(self): + '''Default paths for tools are resolved correctly''' + + self.assertTrue(PathPreferences.pathDefaultToolsPath().endswith('/Path/Tools/')) + self.assertTrue(PathPreferences.pathDefaultToolsPath('Bit').endswith('/Path/Tools/Bit')) + self.assertTrue(PathPreferences.pathDefaultToolsPath('Library').endswith('/Path/Tools/Library')) + self.assertTrue(PathPreferences.pathDefaultToolsPath('Template').endswith('/Path/Tools/Template')) diff --git a/src/Mod/Path/PathTests/TestPathToolBit.py b/src/Mod/Path/PathTests/TestPathToolBit.py new file mode 100644 index 0000000000..1b1f2ba714 --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathToolBit.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import PathScripts.PathToolBit as PathToolBit +import PathTests.PathTestUtils as PathTestUtils + + +class TestPathToolBit(PathTestUtils.PathTestBase): + + def test00(self): + '''Find a tool template from file name''' + + path = PathToolBit.findTemplate('endmill-straight.fcstd') + self.assertIsNot(path, None) + self.assertNotEqual(path, 'endmill-straight.fcstd') + + def test01(self): + '''Find a tool template from an invalid absolute path.''' + + path = PathToolBit.findTemplate('/this/is/unlikely/a/valid/path/v-bit.fcstd') + self.assertIsNot(path, None) + self.assertNotEqual(path, '/this/is/unlikely/a/valid/path/v-bit.fcstd') + + diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 55fbc78fa1..b5ac4e215c 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -25,6 +25,7 @@ import TestApp from PathTests.TestPathLog import TestPathLog +from PathTests.TestPathPreferences import TestPathPreferences from PathTests.TestPathCore import TestPathCore #from PathTests.TestPathPost import PathPostTestCases from PathTests.TestPathGeom import TestPathGeom @@ -35,6 +36,7 @@ from PathTests.TestPathDressupHoldingTags import TestHoldingTags from PathTests.TestPathDressupDogbone import TestDressupDogbone from PathTests.TestPathStock import TestPathStock from PathTests.TestPathTool import TestPathTool +from PathTests.TestPathToolBit import TestPathToolBit from PathTests.TestPathTooltable import TestPathTooltable from PathTests.TestPathToolController import TestPathToolController from PathTests.TestPathSetupSheet import TestPathSetupSheet @@ -58,4 +60,6 @@ False if TestPathToolController.__name__ else True False if TestPathSetupSheet.__name__ else True False if TestPathDeburr.__name__ else True False if TestPathHelix.__name__ else True +False if TestPathPreferences.__name__ else True +False if TestPathToolBit.__name__ else True From 0bf89998b06e2ab9ec3694ab6aad5798154d6e62 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 20 Oct 2019 18:36:20 -0700 Subject: [PATCH 13/52] Fixed typo --- src/Mod/Path/PathScripts/PathJobCmd.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathJobCmd.py b/src/Mod/Path/PathScripts/PathJobCmd.py index d8449a0b1a..61fdd692c5 100644 --- a/src/Mod/Path/PathScripts/PathJobCmd.py +++ b/src/Mod/Path/PathScripts/PathJobCmd.py @@ -39,13 +39,8 @@ from PySide import QtCore, QtGui def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) -LOGLEVEL = False - -if LOGLEVEL: - PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) - PathLog.trackModule(PathLog.thisModule()) -else: - PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) class CommandJobCreate: ''' @@ -186,5 +181,5 @@ if FreeCAD.GuiUp: FreeCADGui.addCommand('Path_Job', CommandJobCreate()) FreeCADGui.addCommand('Path_ExportTemplate', CommandJobTemplateExport()) -FreeCAD.Console.PrintLog("Loading PathJobGui... done\n") +FreeCAD.Console.PrintLog("Loading PathJobCmd... done\n") From 126fdd42d8e4780db85c2c428621e6c15ebf8f80 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 20 Oct 2019 20:09:22 -0700 Subject: [PATCH 14/52] Added command to create a ToolBit --- src/Mod/Path/CMakeLists.txt | 1 + src/Mod/Path/Gui/Resources/Path.qrc | 3 +- .../Path/Gui/Resources/icons/Path-ToolBit.svg | 933 ++++++++++++++++++ src/Mod/Path/InitGui.py | 3 +- src/Mod/Path/PathScripts/PathToolBitCmd.py | 53 + src/Mod/Path/PathScripts/PathToolBitGui.py | 25 +- 6 files changed, 1010 insertions(+), 8 deletions(-) create mode 100644 src/Mod/Path/Gui/Resources/icons/Path-ToolBit.svg create mode 100644 src/Mod/Path/PathScripts/PathToolBitCmd.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index f700376e8c..392eabda92 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -102,6 +102,7 @@ SET(PathScripts_SRCS PathScripts/PathSurface.py PathScripts/PathSurfaceGui.py PathScripts/PathToolBit.py + PathScripts/PathToolBitCmd.py PathScripts/PathToolBitEdit.py PathScripts/PathToolBitGui.py PathScripts/PathToolController.py diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index e6eae76dbc..26f744bec8 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -48,9 +48,10 @@ icons/Path-Speed.svg icons/Path-Stock.svg icons/Path-Stop.svg + icons/Path-ToolBit.svg icons/Path-ToolChange.svg icons/Path-ToolController.svg - icons/Path-ToolDuplicate.svg + icons/Path-ToolDuplicate.svg icons/Path-Toolpath.svg icons/Path-ToolTable.svg icons/Path-Area.svg diff --git a/src/Mod/Path/Gui/Resources/icons/Path-ToolBit.svg b/src/Mod/Path/Gui/Resources/icons/Path-ToolBit.svg new file mode 100644 index 0000000000..025637f1bc --- /dev/null +++ b/src/Mod/Path/Gui/Resources/icons/Path-ToolBit.svg @@ -0,0 +1,933 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Path-ToolTable + 2015-07-04 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-ToolTable.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index bca94eef9f..04bc791896 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -71,6 +71,7 @@ class PathWorkbench (Workbench): FreeCADGui.addIconPath(":/icons") from PathScripts import PathGuiInit from PathScripts import PathJobCmd + from PathScripts import PathToolBitCmd import PathCommands PathGuiInit.Startup() @@ -112,7 +113,7 @@ class PathWorkbench (Workbench): if extracmdlist: self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist) - self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Path_ToolBitCreate", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( "Path", "Path Dressup")], dressupcmdlist) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py new file mode 100644 index 0000000000..e3a81a3f24 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +from PySide import QtCore + +class CommandToolBitCreate: + ''' + Command used to create a new Tool. + ''' + + def __init__(self): + pass + + def GetResources(self): + return {'Pixmap': 'Path-ToolBit', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Tool", "Create Tool"), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Job", "Creates a new ToolBit object")} + + def IsActive(self): + return FreeCAD.ActiveDocument is not None + + def Activated(self): + import PathScripts.PathToolBitGui as PathToolBitGui + obj = PathToolBitGui.Create() + obj.ViewObject.Proxy.setCreate(obj.ViewObject) + +if FreeCAD.GuiUp: + import FreeCADGui + FreeCADGui.addCommand('Path_ToolBitCreate', CommandToolBitCreate()) + +FreeCAD.Console.PrintLog("Loading PathToolBitCmd... done\n") diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 1fe0932d07..ac8c51536f 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -67,7 +67,7 @@ class ViewProvider(object): pixmap = QtGui.QPixmap() pixmap.loadFromData(png, "PNG") return QtGui.QIcon(pixmap) - return ':/icons/Path-ToolChange.svg' + return ':/icons/Path-ToolBit.svg' def __getstate__(self): return None @@ -80,13 +80,20 @@ class ViewProvider(object): # pylint: disable=unused-argument return 'Default' - def setEdit(self, vobj, mode=0): - # pylint: disable=unused-argument + def _openTaskPanel(self, vobj, deleteOnReject): PathLog.track() - self.taskPanel = TaskPanel(vobj) + self.taskPanel = TaskPanel(vobj, deleteOnReject) FreeCADGui.Control.closeDialog() FreeCADGui.Control.showDialog(self.taskPanel) self.taskPanel.setupUi() + + def setCreate(self, vobj): + PathLog.track() + self._openTaskPanel(vobj, True) + + def setEdit(self, vobj, mode=0): + # pylint: disable=unused-argument + self._openTaskPanel(vobj, False) return True def unsetEdit(self, vobj, mode): @@ -106,18 +113,24 @@ class ViewProvider(object): class TaskPanel: '''TaskPanel for the SetupSheet - if it is being edited directly.''' - def __init__(self, vobj): + def __init__(self, vobj, deleteOnReject): PathLog.track(vobj.Object.Label) self.vobj = vobj self.obj = vobj.Object self.editor = PathToolBitEdit.ToolBitEditor(self.obj) self.form = self.editor.form + self.deleteOnReject = deleteOnReject FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Edit ToolBit")) def reject(self): - self.editor.reject() FreeCAD.ActiveDocument.abortTransaction() + self.editor.reject() FreeCADGui.Control.closeDialog() + if self.deleteOnReject: + FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Uncreate ToolBit")) + self.editor.reject() + FreeCAD.ActiveDocument.removeObject(self.obj.Name) + FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute() def accept(self): From 458ae24f114daec42f15ac94f328d26e0ea6e141 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 20 Oct 2019 21:57:44 -0700 Subject: [PATCH 15/52] Added save and save as menu to ToolBit --- src/Mod/Path/InitGui.py | 7 ++- src/Mod/Path/PathScripts/PathToolBitCmd.py | 66 +++++++++++++++++++++- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 04bc791896..10e1669aff 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -113,7 +113,7 @@ class PathWorkbench (Workbench): if extracmdlist: self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist) - self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Path_ToolBitCreate", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + PathToolBitCmd.CommandList + ["Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( "Path", "Path Dressup")], dressupcmdlist) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( @@ -156,7 +156,7 @@ class PathWorkbench (Workbench): if "Job" in selectedName: self.appendContextMenu("", ["Path_ExportTemplate"]) menuAppended = True - if isinstance (obj.Proxy, PathScripts.PathOp.ObjectOp): + if isinstance(obj.Proxy, PathScripts.PathOp.ObjectOp): self.appendContextMenu("", ["Path_OperationCopy", "Path_OpActiveToggle"]) menuAppended = True if obj.isDerivedFrom("Path::Feature"): @@ -167,6 +167,9 @@ class PathWorkbench (Workbench): for cmd in self.dressupcmds: self.appendContextMenu("", [cmd]) menuAppended = True + if isinstance(obj.Proxy, PathScripts.PathToolBit.ToolBit): + self.appendContextMenu("", ["Path_ToolBitSave", "Path_ToolBitSaveAs"]) + menuAppended = True if menuAppended: self.appendContextMenu("", "Separator") diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py index e3a81a3f24..5f6768731a 100644 --- a/src/Mod/Path/PathScripts/PathToolBitCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -23,6 +23,10 @@ # *************************************************************************** import FreeCAD +import FreeCADGui +import PathScripts +import os + from PySide import QtCore class CommandToolBitCreate: @@ -35,8 +39,8 @@ class CommandToolBitCreate: def GetResources(self): return {'Pixmap': 'Path-ToolBit', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Tool", "Create Tool"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Job", "Creates a new ToolBit object")} + 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Create Tool"), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Creates a new ToolBit object")} def IsActive(self): return FreeCAD.ActiveDocument is not None @@ -46,8 +50,64 @@ class CommandToolBitCreate: obj = PathToolBitGui.Create() obj.ViewObject.Proxy.setCreate(obj.ViewObject) +class CommandToolBitSave: + ''' + Command used to save an existing Tool to a file. + ''' + + def __init__(self, saveAs): + self.saveAs = saveAs + + def GetResources(self): + if self.saveAs: + menuTxt = QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Save Tool as...") + else: + menuTxt = QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Save Tool") + return {'Pixmap': 'Path-ToolBit', + 'MenuText': menuTxt, + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Save an existing ToolBit object to a file")} + + def selectedTool(self): + sel = FreeCADGui.Selection.getSelectionEx() + if 1 == len(sel) and isinstance(sel[0].Object.Proxy, PathScripts.PathToolBit.ToolBit): + return sel[0].Object + return None + + def IsActive(self): + tool = self.selectedTool() + if tool: + if tool.File: + return True + return self.saveAs + return False + + def Activated(self): + from PySide import QtGui + tool = self.selectedTool() + if tool: + path = None + if not tool.File or self.saveAs: + if tool.File: + fname = tool.File + else: + fname = os.path.join(PathScripts.PathPreferences.lastPathToolBit(), tool.Label + '.fctb') + foo = QtGui.QFileDialog.getSaveFileName(QtGui.QApplication.activeWindow(), "Tool", fname, "*.fctb") + if foo: + path = foo[0] + else: + path = tool.File + + if path: + if not path.endswith('.fctb'): + path += '.fctb' + tool.Proxy.saveToFile(tool, path) + PathScripts.PathPreferences.setLastPathToolBit(os.path.dirname(path)) + if FreeCAD.GuiUp: - import FreeCADGui FreeCADGui.addCommand('Path_ToolBitCreate', CommandToolBitCreate()) + FreeCADGui.addCommand('Path_ToolBitSave', CommandToolBitSave(False)) + FreeCADGui.addCommand('Path_ToolBitSaveAs', CommandToolBitSave(True)) + +CommandList = ['Path_ToolBitCreate', 'Path_ToolBitSave', 'Path_ToolBitSaveAs'] FreeCAD.Console.PrintLog("Loading PathToolBitCmd... done\n") From c078c68abf5c5e93fe1fa3604f9d2c28fb0d81cf Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 20 Oct 2019 22:12:06 -0700 Subject: [PATCH 16/52] Added loading of existing ToolBits --- src/Mod/Path/PathScripts/PathToolBitCmd.py | 31 +++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py index 5f6768731a..7a45d2b50f 100644 --- a/src/Mod/Path/PathScripts/PathToolBitCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -103,11 +103,40 @@ class CommandToolBitSave: tool.Proxy.saveToFile(tool, path) PathScripts.PathPreferences.setLastPathToolBit(os.path.dirname(path)) +class CommandToolBitLoad: + ''' + Command used to load an existing Tool from a file into the current document. + ''' + + def __init__(self): + pass + + def GetResources(self): + return {'Pixmap': 'Path-ToolBit', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Load Tool"), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Load an existing ToolBit object from a file")} + + def selectedTool(self): + sel = FreeCADGui.Selection.getSelectionEx() + if 1 == len(sel) and isinstance(sel[0].Object.Proxy, PathScripts.PathToolBit.ToolBit): + return sel[0].Object + return None + + def IsActive(self): + return FreeCAD.ActiveDocument is not None + + def Activated(self): + from PySide import QtGui + foo = QtGui.QFileDialog.getOpenFileName(QtGui.QApplication.activeWindow(), "Tool", PathScripts.PathPreferences.lastPathToolBit(), "*.fctb") + if foo: + PathScripts.PathToolBitGui.CreateFrom(foo[0]) + if FreeCAD.GuiUp: FreeCADGui.addCommand('Path_ToolBitCreate', CommandToolBitCreate()) + FreeCADGui.addCommand('Path_ToolBitLoad', CommandToolBitLoad()) FreeCADGui.addCommand('Path_ToolBitSave', CommandToolBitSave(False)) FreeCADGui.addCommand('Path_ToolBitSaveAs', CommandToolBitSave(True)) -CommandList = ['Path_ToolBitCreate', 'Path_ToolBitSave', 'Path_ToolBitSaveAs'] +CommandList = ['Path_ToolBitCreate', 'Path_ToolBitLoad', 'Path_ToolBitSave', 'Path_ToolBitSaveAs'] FreeCAD.Console.PrintLog("Loading PathToolBitCmd... done\n") From e9f3a15fc77d3d4e55adf5d48215faab93273e7d Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 21 Oct 2019 20:32:42 -0700 Subject: [PATCH 17/52] Basic ToolBitSelector dialog --- src/Mod/Path/Gui/Resources/Path.qrc | 1 + .../Gui/Resources/panels/ToolBitSelector.ui | 110 ++++++++++++++++++ src/Mod/Path/InitGui.py | 2 +- src/Mod/Path/PathScripts/PathToolBitGui.py | 58 +++++++++ .../Path/PathScripts/PathToolControllerGui.py | 22 +++- 5 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index 26f744bec8..cb19bd7a15 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -109,6 +109,7 @@ panels/SetupGlobal.ui panels/SetupOp.ui panels/ToolBitEditor.ui + panels/ToolBitSelector.ui panels/ToolEditor.ui panels/ToolLibraryEditor.ui panels/TaskPathSimulator.ui diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui new file mode 100644 index 0000000000..7013e8bf2f --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui @@ -0,0 +1,110 @@ + + + Dialog + + + + 0 + 0 + 588 + 396 + + + + Dialog + + + + + + + + + QAbstractItemView::NoEditTriggers + + + + + + + + + + Load... + + + + + + + New + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 10e1669aff..192cd18300 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -113,7 +113,7 @@ class PathWorkbench (Workbench): if extracmdlist: self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist) - self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + PathToolBitCmd.CommandList + ["Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + PathToolBitCmd.CommandList + ["Path_ToolController", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( "Path", "Path Dressup")], dressupcmdlist) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index ac8c51536f..af128ea83a 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -157,6 +157,64 @@ class TaskPanel: def setupUi(self): self.editor.setupUI() + +class ToolBitSelector(object): + ToolRole = QtCore.Qt.UserRole + 1 + + def __init__(self): + self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitSelector.ui") + self.setupUI() + + def getTool(self): + selected = None + selItem = None + self.form.tools.setUpdatesEnabled(False) + if self.form.tools.currentItem(): + selected = self.form.tools.currentItem().text() + self.form.tools.clear() + for tool in sorted(self.loadedTools(), key=lambda t: t.Label): + icon = None + if tool.ViewObject and tool.ViewObject.Proxy: + icon = tool.ViewObject.Proxy.getIcon() + if icon: + item = QtGui.QListWidgetItem(icon, tool.Label) + else: + item = QtGui.QListWidgetItem(tool.Label) + item.setData(self.ToolRole, tool) + if selected == tool.Label: + selItem = item + self.form.tools.addItem(item) + if selItem: + self.form.tools.setCurrentItem(selItem) + self.updateSelection() + self.form.tools.setUpdatesEnabled(True) + res = self.form.exec_() + if 1 == res and self.form.tools.currentItem(): + return self.form.tools.currentItem().data(self.ToolRole) + return None + + def loadedTools(self): + if FreeCAD.ActiveDocument: + return [o for o in FreeCAD.ActiveDocument.Objects if hasattr(o, 'Proxy') and isinstance(o.Proxy, PathToolBit.ToolBit)] + return [] + + def loadTool(self): + pass + + def createTool(self): + pass + + def updateSelection(self): + if self.form.tools.selectedItems(): + self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(True) + else: + self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(False) + + def setupUI(self): + self.form.toolCreate.clicked.connect(self.createTool) + self.form.toolLoad.clicked.connect(self.loadTool) + self.form.tools.itemSelectionChanged.connect(self.updateSelection) + def Create(name = 'ToolBit'): '''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.''' diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index bfc87c6495..a18cd423c9 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -28,6 +28,8 @@ import Part import PathScripts import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog +import PathScripts.PathToolBit as PathToolBit +import PathScripts.PathToolBitGui as PathToolBitGui import PathScripts.PathToolEdit as PathToolEdit import PathScripts.PathUtil as PathUtil @@ -135,16 +137,24 @@ class CommandPathToolController(object): 'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_ToolController", "Add Tool Controller to the Job"), 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_ToolController", "Add Tool Controller")} + def selectedJob(self): + if FreeCAD.ActiveDocument: + sel = FreeCADGui.Selection.getSelectionEx() + if sel and sel[0].Object.Name[:3] == 'Job': + return sel[0].Object + return None + def IsActive(self): - if FreeCAD.ActiveDocument is not None: - for o in FreeCAD.ActiveDocument.Objects: - if o.Name[:3] == "Job": - return True - return False + return self.selectedJob() is not None def Activated(self): PathLog.track() - Create() + job = self.selectedJob() + if job: + tool = PathToolBitGui.ToolBitSelector().getTool() + if tool: + tc = Create(tool) + job.addToolController(tc) class ToolControllerEditor(object): From a5d12039806ba6d1df884f695ec80f113b1d93a9 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 21 Oct 2019 22:13:29 -0700 Subject: [PATCH 18/52] Added ToolController creation --- .../Path/PathScripts/PathCircularHoleBase.py | 4 +- src/Mod/Path/PathScripts/PathDeburr.py | 2 +- .../Path/PathScripts/PathDressupDogbone.py | 4 +- .../PathScripts/PathDressupHoldingTags.py | 2 +- src/Mod/Path/PathScripts/PathDressupTag.py | 2 +- src/Mod/Path/PathScripts/PathOp.py | 4 +- src/Mod/Path/PathScripts/PathSimulatorGui.py | 8 +-- src/Mod/Path/PathScripts/PathSurface.py | 8 +-- src/Mod/Path/PathScripts/PathToolBitCmd.py | 6 +- src/Mod/Path/PathScripts/PathToolBitEdit.py | 8 +-- src/Mod/Path/PathScripts/PathToolBitGui.py | 58 +++++++++++++++++-- .../Path/PathScripts/PathToolControllerGui.py | 10 +++- src/Mod/Path/PathScripts/PathUtils.py | 4 +- 13 files changed, 84 insertions(+), 36 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBase.py b/src/Mod/Path/PathScripts/PathCircularHoleBase.py index 8310e9dcbf..7983b5efb3 100644 --- a/src/Mod/Path/PathScripts/PathCircularHoleBase.py +++ b/src/Mod/Path/PathScripts/PathCircularHoleBase.py @@ -384,7 +384,7 @@ class ObjectOp(PathOp.ObjectOp): if 1 == len(self.model) and self.baseIsArchPanel(obj, self.model[0]): panel = self.model[0] holeshapes = panel.Proxy.getHoles(panel, transform=True) - tooldiameter = obj.ToolController.Proxy.getTool(obj.ToolController).Diameter + tooldiameter = float(obj.ToolController.Proxy.getTool(obj.ToolController).Diameter) for holeNr, hole in enumerate(holeshapes): PathLog.debug('Entering new HoleShape') for wireNr, wire in enumerate(hole.Wires): @@ -405,7 +405,7 @@ class ObjectOp(PathOp.ObjectOp): PathLog.track('obj: {} shape: {}'.format(obj, shape)) holelist = [] features = [] - # tooldiameter = obj.ToolController.Proxy.getTool(obj.ToolController).Diameter + # tooldiameter = float(obj.ToolController.Proxy.getTool(obj.ToolController).Diameter) tooldiameter = None PathLog.debug('search for holes larger than tooldiameter: {}: '.format(tooldiameter)) if DraftGeomUtils.isPlanar(shape): diff --git a/src/Mod/Path/PathScripts/PathDeburr.py b/src/Mod/Path/PathScripts/PathDeburr.py index 347e5c5bf7..c112eb496a 100644 --- a/src/Mod/Path/PathScripts/PathDeburr.py +++ b/src/Mod/Path/PathScripts/PathDeburr.py @@ -56,7 +56,7 @@ def toolDepthAndOffset(width, extraDepth, tool): toolDepth = 0 if 0 == tan else width / tan depth = toolDepth + extraDepth toolOffset = tool.FlatRadius - extraOffset = tool.Diameter / 2 - width if 180 == angle else extraDepth / tan + extraOffset = float(tool.Diameter) / 2 - width if 180 == angle else extraDepth / tan offset = toolOffset + extraOffset return (depth, offset) diff --git a/src/Mod/Path/PathScripts/PathDressupDogbone.py b/src/Mod/Path/PathScripts/PathDressupDogbone.py index 9866cf8fc5..66f9930cdd 100644 --- a/src/Mod/Path/PathScripts/PathDressupDogbone.py +++ b/src/Mod/Path/PathScripts/PathDressupDogbone.py @@ -860,10 +860,10 @@ class ObjectDressup: self.toolRadius = 5 else: tool = tc.Proxy.getTool(tc) # PathUtils.getTool(obj, tc.ToolNumber) - if not tool or tool.Diameter == 0: + if not tool or float(tool.Diameter) == 0: self.toolRadius = 5 else: - self.toolRadius = tool.Diameter / 2 + self.toolRadius = float(tool.Diameter) / 2 self.shapes = {} self.dbg = [] diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index 6c454e4415..1a0b30e920 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -1021,7 +1021,7 @@ class ObjectTagDressup: # traceback.print_exc() return None - self.toolRadius = PathDressup.toolController(obj.Base).Tool.Diameter / 2 + self.toolRadius = float(PathDressup.toolController(obj.Base).Tool.Diameter) / 2 self.pathData = pathData if generate: obj.Height = self.pathData.defaultTagHeight() diff --git a/src/Mod/Path/PathScripts/PathDressupTag.py b/src/Mod/Path/PathScripts/PathDressupTag.py index 9dab62d975..263ea19336 100644 --- a/src/Mod/Path/PathScripts/PathDressupTag.py +++ b/src/Mod/Path/PathScripts/PathDressupTag.py @@ -220,7 +220,7 @@ class ObjectDressup: PathLog.track() def toolRadius(self): - return PathDressup.toolController(self.obj.Base).Tool.Diameter / 2.0 + return float(PathDressup.toolController(self.obj.Base).Tool.Diameter) / 2.0 def addTagsToDocuemnt(self): for i, solid in enumerate(self.solids): diff --git a/src/Mod/Path/PathScripts/PathOp.py b/src/Mod/Path/PathScripts/PathOp.py index ae84503f46..d90202a3b9 100644 --- a/src/Mod/Path/PathScripts/PathOp.py +++ b/src/Mod/Path/PathScripts/PathOp.py @@ -498,10 +498,10 @@ class ObjectOp(object): self.vertRapid = tc.VertRapid.Value self.horizRapid = tc.HorizRapid.Value tool = tc.Proxy.getTool(tc) - if not tool or tool.Diameter == 0: + if not tool or float(tool.Diameter) == 0: FreeCAD.Console.PrintError("No Tool found or diameter is zero. We need a tool to build a Path.") return - self.radius = tool.Diameter/2 + self.radius = float(tool.Diameter) /2 self.tool = tool obj.OpToolDiameter = tool.Diameter diff --git a/src/Mod/Path/PathScripts/PathSimulatorGui.py b/src/Mod/Path/PathScripts/PathSimulatorGui.py index 7f3c3e2f05..2260d9ccc8 100644 --- a/src/Mod/Path/PathScripts/PathSimulatorGui.py +++ b/src/Mod/Path/PathScripts/PathSimulatorGui.py @@ -125,7 +125,7 @@ class PathSimulation: # if hasattr(self.operation, "ToolController"): # self.tool = self.operation.ToolController.Tool if (self.tool is not None): - toolProf = self.CreateToolProfile(self.tool, Vector(0, 1, 0), Vector(0, 0, 0), self.tool.Diameter / 2.0) + toolProf = self.CreateToolProfile(self.tool, Vector(0, 1, 0), Vector(0, 0, 0), float(self.tool.Diameter) / 2.0) self.cutTool.Shape = Part.makeSolid(toolProf.revolve(Vector(0, 0, 0), Vector(0, 0, 1))) self.cutTool.ViewObject.show() self.voxSim.SetCurrentTool(self.tool) @@ -298,7 +298,7 @@ class PathSimulation: # except: # return (None, e1.valueAt(e1.LastParameter)) # height = self.height - # rad = tool.Diameter / 2.0 - 0.001 * curpos[2] # hack to overcome occ bug + # rad = float(tool.Diameter) / 2.0 - 0.001 * curpos[2] # hack to overcome occ bug # if type(e1.Curve) is Part.Circle and e1.Curve.Radius <= rad: # hack to overcome occ bug # rad = e1.Curve.Radius - 0.001 # # return (None, e1.valueAt(e1.LastParameter)) @@ -350,7 +350,7 @@ class PathSimulation: # height = self.height # hack to overcome occ bugs - rad = tool.Diameter / 2.0 - 0.001 * pos[2] + rad = float(tool.Diameter) / 2.0 - 0.001 * pos[2] # rad = rad + 0.001 * self.icmd if type(toolPath.Curve) is Part.Circle and toolPath.Curve.Radius <= rad: rad = toolPath.Curve.Radius - 0.01 * (pos[2] + 1) @@ -386,7 +386,7 @@ class PathSimulation: # create radial profile of the tool (90 degrees to the direction of the path) def CreateToolProfile(self, tool, dir, pos, rad): type = tool.ToolType - # rad = tool.Diameter / 2.0 - 0.001 * pos[2] # hack to overcome occ bug + # rad = float(tool.Diameter) / 2.0 - 0.001 * pos[2] # hack to overcome occ bug xf = dir[0] * rad yf = dir[1] * rad xp = pos[0] diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index 6dfcf5206c..c32dedcd9b 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -1792,10 +1792,10 @@ class ObjectSurface(PathOp.ObjectOp): def setOclCutter(self, obj): # Set cutter details # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details - diam_1 = obj.ToolController.Tool.Diameter - lenOfst = obj.ToolController.Tool.LengthOffset - FR = obj.ToolController.Tool.FlatRadius - CEH = obj.ToolController.Tool.CuttingEdgeHeight + diam_1 = float(obj.ToolController.Tool.Diameter) + lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0 + FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0 + CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 if obj.ToolController.Tool.ToolType == 'EndMill': # Standard End Mill diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py index 7a45d2b50f..b87569f879 100644 --- a/src/Mod/Path/PathScripts/PathToolBitCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -126,10 +126,8 @@ class CommandToolBitLoad: return FreeCAD.ActiveDocument is not None def Activated(self): - from PySide import QtGui - foo = QtGui.QFileDialog.getOpenFileName(QtGui.QApplication.activeWindow(), "Tool", PathScripts.PathPreferences.lastPathToolBit(), "*.fctb") - if foo: - PathScripts.PathToolBitGui.CreateFrom(foo[0]) + if PathScripts.PathToolBitGui.LoadTool(): + FreeCAD.ActiveDocument.recompute() if FreeCAD.GuiUp: FreeCADGui.addCommand('Path_ToolBitCreate', CommandToolBitCreate()) diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index 4c546477cb..f7ca4f2ab8 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -124,12 +124,12 @@ class ToolBitEditor: path = self.tool.BitTemplate if not path: path = LastPath - foo = QtGui.QFileDialog.getOpenFileName(QtGui.QApplication.activeWindow(), + foo = QtGui.QFileDialog.getOpenFileName(self.form, "Path - Tool Template", path, - "*.fcstd")[0] - if foo: - self.form.templatePath.setText(foo) + "*.fcstd") + if foo and foo[0]: + self.form.templatePath.setText(foo[0]) self.updateTemplate() def setupUI(self): diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index af128ea83a..d6d6c2b7a6 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -27,6 +27,7 @@ import FreeCADGui import PathScripts.PathGui as PathGui import PathScripts.PathIconViewProvider as PathIconViewProvider import PathScripts.PathLog as PathLog +import PathScripts.PathPreferences as PathPreferences import PathScripts.PathToolBit as PathToolBit import PathScripts.PathToolBitEdit as PathToolBitEdit import PathScripts.PathUtil as PathUtil @@ -165,18 +166,18 @@ class ToolBitSelector(object): self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitSelector.ui") self.setupUI() - def getTool(self): - selected = None + def updateTools(self, selected=None): + PathLog.track() selItem = None self.form.tools.setUpdatesEnabled(False) - if self.form.tools.currentItem(): + if selected is None and self.form.tools.currentItem(): selected = self.form.tools.currentItem().text() self.form.tools.clear() for tool in sorted(self.loadedTools(), key=lambda t: t.Label): icon = None if tool.ViewObject and tool.ViewObject.Proxy: icon = tool.ViewObject.Proxy.getIcon() - if icon: + if icon and isinstance(icon, QtGui.QIcon): item = QtGui.QListWidgetItem(icon, tool.Label) else: item = QtGui.QListWidgetItem(tool.Label) @@ -188,32 +189,68 @@ class ToolBitSelector(object): self.form.tools.setCurrentItem(selItem) self.updateSelection() self.form.tools.setUpdatesEnabled(True) + + def getTool(self): + PathLog.track() + self.updateTools() res = self.form.exec_() if 1 == res and self.form.tools.currentItem(): return self.form.tools.currentItem().data(self.ToolRole) return None def loadedTools(self): + PathLog.track() if FreeCAD.ActiveDocument: return [o for o in FreeCAD.ActiveDocument.Objects if hasattr(o, 'Proxy') and isinstance(o.Proxy, PathToolBit.ToolBit)] return [] def loadTool(self): - pass + PathLog.track() + tool = LoadTool(self.form) + if tool: + self.updateTools(tool.Label) def createTool(self): - pass + PathLog.track() + tool = Create() + + def accept(): + self.editor.accept() + self.dialog.done(1) + self.updateTools(tool.Label) + + def reject(): + FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Uncreate ToolBit")) + self.editor.reject() + self.dialog.done(0) + FreeCAD.ActiveDocument.removeObject(tool.Name) + FreeCAD.ActiveDocument.commitTransaction() + + self.dialog = QtGui.QDialog(self.form) + layout = QtGui.QVBoxLayout(self.dialog) + self.editor = PathToolBitEdit.ToolBitEditor(tool, self.dialog) + self.editor.setupUI() + self.buttons = QtGui.QDialogButtonBox( + QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel, + QtCore.Qt.Horizontal, self.dialog) + layout.addWidget(self.buttons) + self.buttons.accepted.connect(accept) + self.buttons.rejected.connect(reject) + print(self.dialog.exec_()) def updateSelection(self): + PathLog.track() if self.form.tools.selectedItems(): self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(True) else: self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(False) def setupUI(self): + PathLog.track() self.form.toolCreate.clicked.connect(self.createTool) self.form.toolLoad.clicked.connect(self.loadTool) self.form.tools.itemSelectionChanged.connect(self.updateSelection) + self.form.tools.doubleClicked.connect(self.form.accept) def Create(name = 'ToolBit'): '''Create(name = 'ToolBit') ... creates a new tool bit. @@ -232,4 +269,13 @@ def CreateFrom(path, name = 'ToolBit'): FreeCAD.ActiveDocument.commitTransaction() return tool +def LoadTool(parent = None): + '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' + if parent is None: + parent = QtGui.QApplication.activeWindow() + foo = QtGui.QFileDialog.getOpenFileName(parent, "Tool", PathPreferences.lastPathToolBit(), "*.fctb") + if foo and foo[0]: + return CreateFrom(foo[0]) + return None + PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index a18cd423c9..19e4c72272 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -142,6 +142,9 @@ class CommandPathToolController(object): sel = FreeCADGui.Selection.getSelectionEx() if sel and sel[0].Object.Name[:3] == 'Job': return sel[0].Object + jobs = [o for o in FreeCAD.ActiveDocument.Objects if o.Name[:3] == 'Job'] + if 1 == len(jobs): + return jobs[0] return None def IsActive(self): @@ -153,8 +156,9 @@ class CommandPathToolController(object): if job: tool = PathToolBitGui.ToolBitSelector().getTool() if tool: - tc = Create(tool) - job.addToolController(tc) + tc = Create("TC: {}".format(tool.Label), tool) + job.Proxy.addToolController(tc) + FreeCAD.ActiveDocument.recompute() class ToolControllerEditor(object): @@ -262,7 +266,7 @@ class TaskPanel: if self.toolrep: tool = self.obj.Tool - radius = tool.Diameter / 2 + radius = float(tool.Diameter) / 2 length = tool.CuttingEdgeHeight t = Part.makeCylinder(radius, length) self.toolrep.Shape = t diff --git a/src/Mod/Path/PathScripts/PathUtils.py b/src/Mod/Path/PathScripts/PathUtils.py index 4c47649177..945dac2159 100644 --- a/src/Mod/Path/PathScripts/PathUtils.py +++ b/src/Mod/Path/PathScripts/PathUtils.py @@ -703,14 +703,14 @@ def guessDepths(objshape, subs=None): def drillTipLength(tool): """returns the length of the drillbit tip.""" - if tool.CuttingEdgeAngle == 180 or tool.CuttingEdgeAngle == 0.0 or tool.Diameter == 0.0: + if tool.CuttingEdgeAngle == 180 or tool.CuttingEdgeAngle == 0.0 or float(tool.Diameter) == 0.0: return 0.0 else: if tool.CuttingEdgeAngle <= 0 or tool.CuttingEdgeAngle >= 180: PathLog.error(translate("Path", "Invalid Cutting Edge Angle %.2f, must be >0° and <=180°") % tool.CuttingEdgeAngle) return 0.0 theta = math.radians(tool.CuttingEdgeAngle) - length = (tool.Diameter / 2) / math.tan(theta / 2) + length = (float(tool.Diameter) / 2) / math.tan(theta / 2) if length < 0: PathLog.error(translate("Path", "Cutting Edge Angle (%.2f) results in negative tool tip length") % tool.CuttingEdgeAngle) return 0.0 From 115ddeac89cbd2e054b4b5e4a6a93565dcf7e8b4 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 21 Oct 2019 22:17:46 -0700 Subject: [PATCH 19/52] Added some sample tools for playing around --- src/Mod/Path/CMakeLists.txt | 9 +++++++++ src/Mod/Path/Tools/Bit/t1.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t2.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t3.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t4.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t5.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t6.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t7.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t8.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t9.fctb | 11 +++++++++++ 10 files changed, 108 insertions(+) create mode 100644 src/Mod/Path/Tools/Bit/t1.fctb create mode 100644 src/Mod/Path/Tools/Bit/t2.fctb create mode 100644 src/Mod/Path/Tools/Bit/t3.fctb create mode 100644 src/Mod/Path/Tools/Bit/t4.fctb create mode 100644 src/Mod/Path/Tools/Bit/t5.fctb create mode 100644 src/Mod/Path/Tools/Bit/t6.fctb create mode 100644 src/Mod/Path/Tools/Bit/t7.fctb create mode 100644 src/Mod/Path/Tools/Bit/t8.fctb create mode 100644 src/Mod/Path/Tools/Bit/t9.fctb diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 392eabda92..7253280206 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -138,6 +138,15 @@ SET(PathScripts_post_SRCS ) SET(Tools_Bit_SRCS + Tools/Bit/t1.fctb + Tools/Bit/t2.fctb + Tools/Bit/t3.fctb + Tools/Bit/t4.fctb + Tools/Bit/t5.fctb + Tools/Bit/t6.fctb + Tools/Bit/t7.fctb + Tools/Bit/t8.fctb + Tools/Bit/t9.fctb ) SET(Tools_Library_SRCS diff --git a/src/Mod/Path/Tools/Bit/t1.fctb b/src/Mod/Path/Tools/Bit/t1.fctb new file mode 100644 index 0000000000..8a028c0b70 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t1.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T1", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "1.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t2.fctb b/src/Mod/Path/Tools/Bit/t2.fctb new file mode 100644 index 0000000000..72fa57f3a0 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t2.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T2", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "2.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t3.fctb b/src/Mod/Path/Tools/Bit/t3.fctb new file mode 100644 index 0000000000..8f8821f5f5 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t3.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T3", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "3.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t4.fctb b/src/Mod/Path/Tools/Bit/t4.fctb new file mode 100644 index 0000000000..094814a74c --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t4.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T4", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "4.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t5.fctb b/src/Mod/Path/Tools/Bit/t5.fctb new file mode 100644 index 0000000000..a716a91029 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t5.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T5", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "5.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t6.fctb b/src/Mod/Path/Tools/Bit/t6.fctb new file mode 100644 index 0000000000..54cc8743b3 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t6.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T6", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "6.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t7.fctb b/src/Mod/Path/Tools/Bit/t7.fctb new file mode 100644 index 0000000000..e5df250dd6 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t7.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T7", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "7.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t8.fctb b/src/Mod/Path/Tools/Bit/t8.fctb new file mode 100644 index 0000000000..746a1338a9 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t8.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T8", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "8.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t9.fctb b/src/Mod/Path/Tools/Bit/t9.fctb new file mode 100644 index 0000000000..4f156d972c --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t9.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T9", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "9.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} From d30abb6121077393359512731013b532e690878f Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Wed, 23 Oct 2019 22:20:31 -0700 Subject: [PATCH 20/52] Ignore distance check when copying holding tags --- src/Mod/Path/PathScripts/PathDeburr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathDeburr.py b/src/Mod/Path/PathScripts/PathDeburr.py index c112eb496a..109f4970ce 100644 --- a/src/Mod/Path/PathScripts/PathDeburr.py +++ b/src/Mod/Path/PathScripts/PathDeburr.py @@ -48,14 +48,14 @@ def translate(context, text, disambig=None): def toolDepthAndOffset(width, extraDepth, tool): '''toolDepthAndOffset(width, extraDepth, tool) ... return tuple for given parameters.''' - angle = tool.CuttingEdgeAngle + angle = float(tool.CuttingEdgeAngle) if 0 == angle: angle = 180 tan = math.tan(math.radians(angle / 2)) toolDepth = 0 if 0 == tan else width / tan depth = toolDepth + extraDepth - toolOffset = tool.FlatRadius + toolOffset = float(tool.FlatRadius) extraOffset = float(tool.Diameter) / 2 - width if 180 == angle else extraDepth / tan offset = toolOffset + extraOffset return (depth, offset) From 541633297a3617b03bc8a153cf7c9c06e8db11e7 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Wed, 23 Oct 2019 23:24:10 -0700 Subject: [PATCH 21/52] Fixed Deburr op and v-bit template to communicate properly --- .../PathScripts/PathDressupHoldingTags.py | 2 +- src/Mod/Path/Tools/Template/v-bit.fcstd | Bin 11981 -> 12187 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index 1a0b30e920..38297e917f 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -671,7 +671,7 @@ class PathData: print("tag[%d]" % i) if not i in fromObj.Disabled: dist = self.baseWire.distToShape(Part.Vertex(FreeCAD.Vector(pos.x, pos.y, self.minZ))) - if dist[0] < W: + if True or dist[0] < W: print("tag[%d/%d]: (%.2f, %.2f, %.2f)" % (i, j, pos.x, pos.y, self.minZ)) at = dist[1][0][0] tags.append(Tag(j, at.x, at.y, W, H, A, R, True)) diff --git a/src/Mod/Path/Tools/Template/v-bit.fcstd b/src/Mod/Path/Tools/Template/v-bit.fcstd index 97fee679d9eb013331688959c7f1bd1e8595ec3f..7df97e11d626bd5bd9f75a05251b8565868e08c8 100644 GIT binary patch literal 12187 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 literal 11981 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 oyXvCKBu!~g&Q From 62964ce199b62f30dc0b3fb5a685f801ce8702f2 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Thu, 24 Oct 2019 19:54:45 -0700 Subject: [PATCH 22/52] Assign unique ToolNumber to newly created TC --- src/Mod/Path/PathScripts/PathToolControllerGui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 19e4c72272..932bda4e43 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -156,7 +156,8 @@ class CommandPathToolController(object): if job: tool = PathToolBitGui.ToolBitSelector().getTool() if tool: - tc = Create("TC: {}".format(tool.Label), tool) + toolNr = max([tc.ToolNumber for tc in job.ToolController]) + 1 + tc = Create("TC: {}".format(tool.Label), tool, toolNr) job.Proxy.addToolController(tc) FreeCAD.ActiveDocument.recompute() From 5d6fc167e9de51a086e0bfb549703c92eaabdfb7 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Thu, 24 Oct 2019 20:03:51 -0700 Subject: [PATCH 23/52] Use same ToolNumber if TC is for identical Tool as another TC in the same job. --- src/Mod/Path/PathScripts/PathToolControllerGui.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 932bda4e43..4b89972013 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -156,7 +156,13 @@ class CommandPathToolController(object): if job: tool = PathToolBitGui.ToolBitSelector().getTool() if tool: - toolNr = max([tc.ToolNumber for tc in job.ToolController]) + 1 + toolNr = None + for tc in job.ToolController: + if tc.Tool == tool: + toolNr = tc.ToolNumber + break + if not toolNr: + toolNr = max([tc.ToolNumber for tc in job.ToolController]) + 1 tc = Create("TC: {}".format(tool.Label), tool, toolNr) job.Proxy.addToolController(tc) FreeCAD.ActiveDocument.recompute() From 97904d0bf26df8fa04e1d7ac4b3e23cf4b07680b Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 27 Oct 2019 02:00:18 -0700 Subject: [PATCH 24/52] Basic ToolBitLibrary edit dialog --- src/Mod/Path/Gui/Resources/Path.qrc | 1 + .../Resources/panels/ToolBitLibraryEdit.ui | 249 ++++++++++++++++++ src/Mod/Path/PathScripts/PathToolBit.py | 7 +- src/Mod/Path/PathScripts/PathToolBitGui.py | 12 +- .../Path/PathScripts/PathToolBitLibraryGui.py | 117 ++++++++ 5 files changed, 380 insertions(+), 6 deletions(-) create mode 100644 src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui create mode 100644 src/Mod/Path/PathScripts/PathToolBitLibraryGui.py diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index cb19bd7a15..b461c8cdf6 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -109,6 +109,7 @@ panels/SetupGlobal.ui panels/SetupOp.ui panels/ToolBitEditor.ui + panels/ToolBitLibraryEdit.ui panels/ToolBitSelector.ui panels/ToolEditor.ui panels/ToolLibraryEditor.ui diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui new file mode 100644 index 0000000000..7f0f2b040f --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui @@ -0,0 +1,249 @@ + + + Dialog + + + + 0 + 0 + 958 + 508 + + + + ToolBit Library + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + ... + + + + :/icons/document-new.svg:/icons/document-new.svg + + + + + + + ... + + + + :/icons/document-open.svg:/icons/document-open.svg + + + + + + + ... + + + + :/icons/document-save.svg:/icons/document-save.svg + + + + + + + ... + + + + :/icons/document-save-as.svg:/icons/document-save-as.svg + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + :/icons/preferences-system.svg:/icons/preferences-system.svg + + + + + + + + + + + 0 + + + 0 + + + + + QAbstractItemView::SelectRows + + + true + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Add ... + + + + :/icons/list-add.svg:/icons/list-add.svg + + + + + + + Delete + + + + :/icons/list-remove.svg:/icons/list-remove.svg + + + + + + + Up + + + + :/icons/button_up.svg:/icons/button_up.svg + + + + + + + Down + + + + :/icons/button_down.svg:/icons/button_down.svg + + + + + + + Qt::Vertical + + + + 20 + 115 + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 25825a1e67..4d5ae87a79 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -261,10 +261,13 @@ class ToolBit(object): PathLog.error("Could not save tool %s to %s (%s)" % (obj.Label, path, e)) raise +def Declaration(path): + with open(path, 'r') as fp: + return json.load(fp) + def CreateFrom(path, name = 'ToolBit'): try: - with open(path, 'r') as fp: - data = json.load(fp) + data = Declaration(path) obj = Create(name, data['template']) obj.Label = data['name'] params = data['parameter'] diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index d6d6c2b7a6..35e13f5c9a 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -163,7 +163,7 @@ class ToolBitSelector(object): ToolRole = QtCore.Qt.UserRole + 1 def __init__(self): - self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitSelector.ui") + self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitSelector.ui') self.setupUI() def updateTools(self, selected=None): @@ -269,13 +269,17 @@ def CreateFrom(path, name = 'ToolBit'): FreeCAD.ActiveDocument.commitTransaction() return tool -def LoadTool(parent = None): - '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' +def GetToolFile(parent = None): if parent is None: parent = QtGui.QApplication.activeWindow() foo = QtGui.QFileDialog.getOpenFileName(parent, "Tool", PathPreferences.lastPathToolBit(), "*.fctb") if foo and foo[0]: - return CreateFrom(foo[0]) + return foo[0] return None +def LoadTool(parent = None): + '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' + foo = GetToolFile(parent) + return CreateFrom(foo) if foo else foo + PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider) diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py new file mode 100644 index 0000000000..00bc56104f --- /dev/null +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + + +import FreeCADGui +import PathScripts.PathLog as PathLog +import PathScripts.PathToolBit as PathToolBit +import PathScripts.PathToolBitGui as PathToolBitGui +import PySide + +import os +import traceback + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + +class Delegate(PySide.QtGui.QStyledItemDelegate): + + def createEditor(self, parent, option, index): + PathLog.track(index) + return None + def setEditorData(self, widget, index): + PathLog.track(index) + def setModelData(self, widget, model, index): + PathLog.track(index) + def updateEditorGeometry(self, widget, option, index): + PathLog.track(index) + widget.setGeometry(option.rect) + +class ToolBitLibrary(object): + + def __init__(self): + self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitLibraryEdit.ui') + #self.form = FreeCADGui.PySideUic.loadUi('src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui') + self.setupUI() + + def toolAdd(self): + PathLog.track() + try: + foo = PathToolBitGui.GetToolFile(self.form) + if foo: + tool = PathToolBit.Declaration(foo) + nr = 0 + for row in range(self.model.rowCount()): + itemNr = int(self.model.item(row, 0).data(PySide.QtCore.Qt.EditRole)) + nr = max(nr, itemNr) + + toolNr = PySide.QtGui.QStandardItem() + toolNr.setData(nr + 1, PySide.QtCore.Qt.EditRole) + + toolName = PySide.QtGui.QStandardItem() + toolName.setData(tool['name'], PySide.QtCore.Qt.EditRole) + toolName.setEditable(False) + + toolTemplate = PySide.QtGui.QStandardItem() + toolTemplate.setData(os.path.splitext(os.path.basename(tool['template']))[0], PySide.QtCore.Qt.EditRole) + toolTemplate.setEditable(False) + + toolDiameter = PySide.QtGui.QStandardItem() + toolDiameter.setData(tool['parameter']['Diameter'], PySide.QtCore.Qt.EditRole) + toolDiameter.setEditable(False) + + self.model.appendRow([toolNr, toolName, toolTemplate, toolDiameter]) + + self.form.toolTable.resizeColumnsToContents() + else: + PathLog.info("no tool") + except: + PathLog.error('something happened') + PathLog.error(traceback.print_exc()) + + def toolDelete(self): + PathLog.track() + def toolUp(self): + PathLog.track() + def toolDown(self): + PathLog.track() + + def columnNames(self): + return ['Nr', 'Tool', 'Template', 'Diameter'] + + def setupUI(self): + PathLog.track('+') + self.delegate = Delegate(self.form) + self.model = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()), self.form) + self.model.setHorizontalHeaderLabels(self.columnNames()) + + self.form.toolTable.setModel(self.model) + self.form.toolTable.resizeColumnsToContents() + + self.form.toolAdd.clicked.connect(self.toolAdd) + self.form.toolDelete.clicked.connect(self.toolDelete) + self.form.toolUp.clicked.connect(self.toolUp) + self.form.toolDown.clicked.connect(self.toolDown) + + PathLog.track('-') From bb07dc1c58074fbc5885ff5c0dfe7e0398ebdb8b Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 27 Oct 2019 13:12:48 -0700 Subject: [PATCH 25/52] Added getting/loading of multiple tools --- src/Mod/Path/PathScripts/PathToolBitCmd.py | 2 +- src/Mod/Path/PathScripts/PathToolBitGui.py | 28 +++++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py index b87569f879..e3e9a2d7ab 100644 --- a/src/Mod/Path/PathScripts/PathToolBitCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -126,7 +126,7 @@ class CommandToolBitLoad: return FreeCAD.ActiveDocument is not None def Activated(self): - if PathScripts.PathToolBitGui.LoadTool(): + if PathScripts.PathToolBitGui.LoadTools(): FreeCAD.ActiveDocument.recompute() if FreeCAD.GuiUp: diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 35e13f5c9a..977a3fa683 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -31,6 +31,7 @@ import PathScripts.PathPreferences as PathPreferences import PathScripts.PathToolBit as PathToolBit import PathScripts.PathToolBitEdit as PathToolBitEdit import PathScripts.PathUtil as PathUtil +import os from PySide import QtCore, QtGui @@ -66,7 +67,7 @@ class ViewProvider(object): png = self.obj.Proxy.getBitThumbnail(self.obj) if png: pixmap = QtGui.QPixmap() - pixmap.loadFromData(png, "PNG") + pixmap.loadFromData(png, 'PNG') return QtGui.QIcon(pixmap) return ':/icons/Path-ToolBit.svg' @@ -121,14 +122,14 @@ class TaskPanel: self.editor = PathToolBitEdit.ToolBitEditor(self.obj) self.form = self.editor.form self.deleteOnReject = deleteOnReject - FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Edit ToolBit")) + FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Edit ToolBit')) def reject(self): FreeCAD.ActiveDocument.abortTransaction() self.editor.reject() FreeCADGui.Control.closeDialog() if self.deleteOnReject: - FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Uncreate ToolBit")) + FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Uncreate ToolBit')) self.editor.reject() FreeCAD.ActiveDocument.removeObject(self.obj.Name) FreeCAD.ActiveDocument.commitTransaction() @@ -220,7 +221,7 @@ class ToolBitSelector(object): self.updateTools(tool.Label) def reject(): - FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Uncreate ToolBit")) + FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Uncreate ToolBit')) self.editor.reject() self.dialog.done(0) FreeCAD.ActiveDocument.removeObject(tool.Name) @@ -255,7 +256,7 @@ class ToolBitSelector(object): def Create(name = 'ToolBit'): '''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")) + FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit')) tool = PathToolBit.Create(name) PathIconViewProvider.Attach(tool.ViewObject, name) FreeCAD.ActiveDocument.commitTransaction() @@ -272,14 +273,29 @@ def CreateFrom(path, name = 'ToolBit'): def GetToolFile(parent = None): if parent is None: parent = QtGui.QApplication.activeWindow() - foo = QtGui.QFileDialog.getOpenFileName(parent, "Tool", PathPreferences.lastPathToolBit(), "*.fctb") + foo = QtGui.QFileDialog.getOpenFileName(parent, 'Tool', PathPreferences.lastPathToolBit(), '*.fctb') if foo and foo[0]: + PathPreferences.setLastPathToolBit(os.path.dirname(foo[0])) return foo[0] return None +def GetToolFiles(parent = None): + if parent is None: + parent = QtGui.QApplication.activeWindow() + foo = QtGui.QFileDialog.getOpenFileNames(parent, 'Tool', PathPreferences.lastPathToolBit(), '*.fctb') + if foo and foo[0]: + PathPreferences.setLastPathToolBit(os.path.dirname(foo[0][0])) + return foo[0] + return [] + + def LoadTool(parent = None): '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' foo = GetToolFile(parent) return CreateFrom(foo) if foo else foo +def LoadTools(parent = None): + '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' + return [CreateFrom(foo) for foo in GetToolFiles(parent)] + PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider) From d0b8f34ba5ebd413d90426cc229c8bb39a46afb1 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 27 Oct 2019 23:58:07 -0700 Subject: [PATCH 26/52] Added library commands and drag&drop for rearranging and copying of tools --- .../Resources/panels/ToolBitLibraryEdit.ui | 36 +-- src/Mod/Path/PathScripts/PathToolBit.py | 20 +- .../Path/PathScripts/PathToolBitLibraryGui.py | 269 ++++++++++++++---- src/Mod/Path/Tools/Library/endmills.fctl | 14 + 4 files changed, 272 insertions(+), 67 deletions(-) create mode 100644 src/Mod/Path/Tools/Library/endmills.fctl diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui index 7f0f2b040f..48a32c705f 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui @@ -101,7 +101,7 @@ - + 0 @@ -111,12 +111,27 @@ + + true + + + true + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + QAbstractItemView::SelectRows true + + false + @@ -157,24 +172,9 @@ - + - Up - - - - :/icons/button_up.svg:/icons/button_up.svg - - - - - - - Down - - - - :/icons/button_down.svg:/icons/button_down.svg + Enumerate diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 4d5ae87a79..1813b4c2b8 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -56,15 +56,23 @@ ParameterTypeConstraint = { } -def _findTool(path, typ): +def _findTool(path, typ, dbg=False): if os.path.exists(path): + if dbg: + PathLog.debug("Found {} at {}".format(typ, path)) return path def searchFor(pname, fname): + if dbg: + PathLog.debug("Looking for {}".format(pname)) if fname: for p in PathPreferences.searchPathsTool(typ): f = os.path.join(p, fname) + if dbg: + PathLog.debug(" Checking {}".format(f)) if os.path.exists(f): + if dbg: + PathLog.debug(" Found {} at {}".format(typ, f)) return f if pname and '/' != pname: ppname, pfname = os.path.split(pname) @@ -78,6 +86,16 @@ def findTemplate(path): '''findTemplate(path) ... search for path, full and partially in all known template directories.''' return _findTool(path, 'Template') +def findBit(path): + if path.endswith('.fctb'): + return _findTool(path, 'Bit') + return _findTool("{}.fctb".format(path), 'Bit') + +def findLibrary(path, dbg=False): + if path.endswith('.fctl'): + return _findTool(path, 'Library', dbg) + return _findTool("{}.fctl".format(path), 'Library', dbg) + def updateConstraint(sketch, name, value): for i, constraint in enumerate(sketch.Constraints): if constraint.Name.split(';')[0] == name: diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index 00bc56104f..b652cce0ca 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -25,93 +25,266 @@ import FreeCADGui import PathScripts.PathLog as PathLog +import PathScripts.PathPreferences as PathPreferences import PathScripts.PathToolBit as PathToolBit import PathScripts.PathToolBitGui as PathToolBitGui import PySide - +import json import os import traceback +import uuid PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) PathLog.trackModule(PathLog.thisModule()) -class Delegate(PySide.QtGui.QStyledItemDelegate): +_UuidRole = PySide.QtCore.Qt.UserRole + 1 +_PathRole = PySide.QtCore.Qt.UserRole + 2 - def createEditor(self, parent, option, index): - PathLog.track(index) +class TableView(PySide.QtGui.QTableView): + + def __init__(self, parent): + PySide.QtGui.QTableView.__init__(self, parent) + self.setDragEnabled(True) + self.setAcceptDrops(True) + self.setDropIndicatorShown(True) + self.setDragDropMode(PySide.QtGui.QAbstractItemView.InternalMove) + self.setDefaultDropAction(PySide.QtCore.Qt.MoveAction) + self.setSortingEnabled(True) + self.setSelectionBehavior(PySide.QtGui.QAbstractItemView.SelectRows) + self.verticalHeader().hide() + + def supportedDropActions(self): + return [PySide.QtCore.Qt.CopyAction, PySide.QtCore.Qt.MoveAction] + + def _uuidOfRow(self, row): + model = self.model() + return model.data(model.index(row, 0), _UuidRole) + + def _rowWithUuid(self, uuid): + model = self.model() + for row in range(model.rowCount()): + if self._uuidOfRow(row) == uuid: + return row return None - def setEditorData(self, widget, index): - PathLog.track(index) - def setModelData(self, widget, model, index): - PathLog.track(index) - def updateEditorGeometry(self, widget, option, index): - PathLog.track(index) - widget.setGeometry(option.rect) + + def _copyTool(self, uuid_, dstRow): + model = self.model() + items = [] + model.insertRow(dstRow) + srcRow = self._rowWithUuid(uuid_) + for col in range(model.columnCount()): + srcItem = model.item(srcRow, col) + + model.setData(model.index(dstRow, col), srcItem.data(PySide.QtCore.Qt.EditRole), PySide.QtCore.Qt.EditRole) + if col == 0: + model.setData(model.index(dstRow, col), srcItem.data(_PathRole), _PathRole) + model.setData(model.index(dstRow, col), uuid.uuid4(), _UuidRole) + else: + model.item(dstRow, col).setEditable(False) + + def _copyTools(self, uuids, dst): + for i, uuid in enumerate(uuids): + self._copyTool(uuid, dst + i) + + def dropEvent(self, event): + PathLog.track() + mime = event.mimeData() + data = mime.data('application/x-qstandarditemmodeldatalist') + stream = PySide.QtCore.QDataStream(data) + srcRows = [] + while not stream.atEnd(): + row = stream.readInt32() + srcRows.append(row) + col = stream.readInt32() + #PathLog.track(row, col) + cnt = stream.readInt32() + for i in range(cnt): + key = stream.readInt32() + val = stream.readQVariant() + #PathLog.track(' ', i, key, val, type(val)) + # I have no idea what these three integers are, + # or if they even are three integers, + # but it seems to work out this way. + i0 = stream.readInt32() + i1 = stream.readInt32() + i2 = stream.readInt32() + #PathLog.track(' ', i0, i1, i2) + + # get the uuids of all srcRows + model = self.model() + srcUuids = [self._uuidOfRow(row) for row in set(srcRows)] + destRow = self.rowAt(event.pos().y()) + + self._copyTools(srcUuids, destRow) + if PySide.QtCore.Qt.DropAction.MoveAction == event.proposedAction(): + for uuid in srcUuids: + model.removeRow(self._rowWithUuid(uuid)) + +#class ToolTableModel(PySide.QtGui.QStandardItemModel): + class ToolBitLibrary(object): - def __init__(self): + def __init__(self, path=None): + self.path = path self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitLibraryEdit.ui') - #self.form = FreeCADGui.PySideUic.loadUi('src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui') + self.toolTableView = TableView(self.form.toolTableGroup) + self.form.toolTableGroup.layout().replaceWidget(self.form.toolTable, self.toolTableView) + self.form.toolTable.hide() self.setupUI() + self.title = self.form.windowTitle() + if path: + self.libraryLoad(path) + + def _toolAdd(self, nr, tool, path): + toolNr = PySide.QtGui.QStandardItem() + toolNr.setData(nr, PySide.QtCore.Qt.EditRole) + toolNr.setData(path, _PathRole) + toolNr.setData(uuid.uuid4(), _UuidRole) + + toolName = PySide.QtGui.QStandardItem() + toolName.setData(tool['name'], PySide.QtCore.Qt.EditRole) + toolName.setEditable(False) + + toolTemplate = PySide.QtGui.QStandardItem() + toolTemplate.setData(os.path.splitext(os.path.basename(tool['template']))[0], PySide.QtCore.Qt.EditRole) + toolTemplate.setEditable(False) + + toolDiameter = PySide.QtGui.QStandardItem() + toolDiameter.setData(tool['parameter']['Diameter'], PySide.QtCore.Qt.EditRole) + toolDiameter.setEditable(False) + + self.model.appendRow([toolNr, toolName, toolTemplate, toolDiameter]) def toolAdd(self): PathLog.track() try: - foo = PathToolBitGui.GetToolFile(self.form) - if foo: + nr = 0 + for row in range(self.model.rowCount()): + itemNr = int(self.model.item(row, 0).data(PySide.QtCore.Qt.EditRole)) + nr = max(nr, itemNr) + nr += 1 + + for i, foo in enumerate(PathToolBitGui.GetToolFiles(self.form)): tool = PathToolBit.Declaration(foo) - nr = 0 - for row in range(self.model.rowCount()): - itemNr = int(self.model.item(row, 0).data(PySide.QtCore.Qt.EditRole)) - nr = max(nr, itemNr) - - toolNr = PySide.QtGui.QStandardItem() - toolNr.setData(nr + 1, PySide.QtCore.Qt.EditRole) - - toolName = PySide.QtGui.QStandardItem() - toolName.setData(tool['name'], PySide.QtCore.Qt.EditRole) - toolName.setEditable(False) - - toolTemplate = PySide.QtGui.QStandardItem() - toolTemplate.setData(os.path.splitext(os.path.basename(tool['template']))[0], PySide.QtCore.Qt.EditRole) - toolTemplate.setEditable(False) - - toolDiameter = PySide.QtGui.QStandardItem() - toolDiameter.setData(tool['parameter']['Diameter'], PySide.QtCore.Qt.EditRole) - toolDiameter.setEditable(False) - - self.model.appendRow([toolNr, toolName, toolTemplate, toolDiameter]) - - self.form.toolTable.resizeColumnsToContents() - else: - PathLog.info("no tool") + self._toolAdd(nr + i, tool, foo) + self.toolTableView.resizeColumnsToContents() except: PathLog.error('something happened') PathLog.error(traceback.print_exc()) def toolDelete(self): PathLog.track() - def toolUp(self): + selectedRows = set([index.row() for index in self.toolTableView.selectedIndexes()]) + for row in sorted(list(selectedRows), key = lambda r: -r): + self.model.removeRows(row, 1) + + def toolEnumerate(self): PathLog.track() - def toolDown(self): + for row in range(self.model.rowCount()): + self.model.setData(self.model.index(row, 0), row + 1, PySide.QtCore.Qt.EditRole) + + def toolSelect(self, selected, deselected): + self.form.toolDelete.setEnabled(len(self.toolTableView.selectedIndexes()) > 0) + + def open(self, path=None): + if path: + fullPath = PathToolBit.findLibrary(path) + if fullPath: + self.libraryLoad(fullPath) + else: + self.libraryOpen() + return self.form.exec_() + + def updateToolbar(self): + if self.path: + self.form.librarySave.setEnabled(True) + else: + self.form.librarySave.setEnabled(False) + + def libraryOpen(self): PathLog.track() + foo = PySide.QtGui.QFileDialog.getOpenFileName(self.form, 'Tool Library', PathPreferences.lastPathToolLibrary(), '*.fctl') + if foo and foo[0]: + path = foo[0] + PathPreferences.setLastPathToolLibrary(os.path.dirname(path)) + self.libraryLoad(path) + + def libraryLoad(self, path): + self.toolTableView.setUpdatesEnabled(False) + self.model.clear() + if path: + with open(path) as fp: + library = json.load(fp) + for nr in library['tools']: + bit = PathToolBit.findBit(library['tools'][nr]) + if bit: + PathLog.track(bit) + tool = PathToolBit.Declaration(bit) + self._toolAdd(nr, tool, bit) + else: + PathLog.error("Could not find tool #{}: {}".format(nr, library['tools'][nr])) + self.toolTableView.resizeColumnsToContents() + self.toolTableView.setUpdatesEnabled(True) + + self.form.setWindowTitle("{} - {}".format(self.title, os.path.basename(path) if path else '')) + self.path = path + self.updateToolbar() + + def libraryNew(self): + self.libraryLoad(None) + + def librarySave(self): + library = {} + tools = {} + library['version'] = 1 + library['tools'] = tools + for row in range(self.model.rowCount()): + toolNr = self.model.data(self.model.index(row, 0), PySide.QtCore.Qt.EditRole) + toolPath = self.model.data(self.model.index(row, 0), _PathRole) + tools[toolNr] = toolPath + + with open(self.path, 'w') as fp: + json.dump(library, fp, sort_keys=True, indent=2) + + def librarySaveAs(self): + foo = PySide.QtGui.QFileDialog.getSaveFileName(self.form, 'Tool Library', PathPreferences.lastPathToolLibrary(), '*.fctl') + if foo and foo[0]: + path = foo[0] if foo[0].endswith('.fctl') else "{}.fctl".format(foo[0]) + PathPreferences.setLastPathToolLibrary(os.path.dirname(path)) + self.path = path + self.librarySave() + self.updateToolbar() def columnNames(self): return ['Nr', 'Tool', 'Template', 'Diameter'] + def rowsMoved(self, parent, start, end, dest, row): + PathLog.track(parent, start, end, dest, row) + + def rowsAboutToBeMoved(self, srcParent, srcStart, srcEnd, destParent, destRow): + PathLog.track(srcParent, srcStart, srcEnd, destParent, destRow) + def setupUI(self): PathLog.track('+') - self.delegate = Delegate(self.form) - self.model = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()), self.form) + self.model = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()), self.toolTableView) self.model.setHorizontalHeaderLabels(self.columnNames()) + self.model.rowsAboutToBeMoved.connect(self.rowsAboutToBeMoved) + self.model.rowsMoved.connect(self.rowsMoved) - self.form.toolTable.setModel(self.model) - self.form.toolTable.resizeColumnsToContents() + self.toolTableView.setModel(self.model) + self.toolTableView.resizeColumnsToContents() + self.toolTableView.selectionModel().selectionChanged.connect(self.toolSelect) self.form.toolAdd.clicked.connect(self.toolAdd) self.form.toolDelete.clicked.connect(self.toolDelete) - self.form.toolUp.clicked.connect(self.toolUp) - self.form.toolDown.clicked.connect(self.toolDown) + self.form.toolEnumerate.clicked.connect(self.toolEnumerate) + self.form.libraryNew.clicked.connect(self.libraryNew) + self.form.libraryOpen.clicked.connect(self.libraryOpen) + self.form.librarySave.clicked.connect(self.librarySave) + self.form.librarySaveAs.clicked.connect(self.librarySaveAs) + + self.toolSelect([], []) + self.updateToolbar() PathLog.track('-') diff --git a/src/Mod/Path/Tools/Library/endmills.fctl b/src/Mod/Path/Tools/Library/endmills.fctl new file mode 100644 index 0000000000..ae9c37c9f8 --- /dev/null +++ b/src/Mod/Path/Tools/Library/endmills.fctl @@ -0,0 +1,14 @@ +{ + "tools": { + "1": "t1", + "2": "t2", + "3": "t3", + "4": "t4", + "5": "t5", + "6": "t6", + "7": "t7", + "8": "t8", + "9": "t9" + }, + "version": 1 +} From 7dd17b62a6187828cfcc05316618302fde5e424f Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 28 Oct 2019 19:45:45 -0700 Subject: [PATCH 27/52] Fixed ToolBitLibrary json format --- .../Path/PathScripts/PathToolBitLibraryGui.py | 9 ++-- src/Mod/Path/Tools/Library/endmills.fctl | 51 ++++++++++++++----- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index b652cce0ca..0c4fcf9091 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -216,8 +216,9 @@ class ToolBitLibrary(object): if path: with open(path) as fp: library = json.load(fp) - for nr in library['tools']: - bit = PathToolBit.findBit(library['tools'][nr]) + for toolBit in library['tools']: + nr = toolBit['nr'] + bit = PathToolBit.findBit(toolBit['path']) if bit: PathLog.track(bit) tool = PathToolBit.Declaration(bit) @@ -236,13 +237,13 @@ class ToolBitLibrary(object): def librarySave(self): library = {} - tools = {} + tools = [] library['version'] = 1 library['tools'] = tools for row in range(self.model.rowCount()): toolNr = self.model.data(self.model.index(row, 0), PySide.QtCore.Qt.EditRole) toolPath = self.model.data(self.model.index(row, 0), _PathRole) - tools[toolNr] = toolPath + tools.append({'nr': toolNr, 'path': toolPath}) with open(self.path, 'w') as fp: json.dump(library, fp, sort_keys=True, indent=2) diff --git a/src/Mod/Path/Tools/Library/endmills.fctl b/src/Mod/Path/Tools/Library/endmills.fctl index ae9c37c9f8..f3e9b37c34 100644 --- a/src/Mod/Path/Tools/Library/endmills.fctl +++ b/src/Mod/Path/Tools/Library/endmills.fctl @@ -1,14 +1,41 @@ { - "tools": { - "1": "t1", - "2": "t2", - "3": "t3", - "4": "t4", - "5": "t5", - "6": "t6", - "7": "t7", - "8": "t8", - "9": "t9" - }, + "tools": [ + { + "nr": 1, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t1.fctb" + }, + { + "nr": 2, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t2.fctb" + }, + { + "nr": 3, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t3.fctb" + }, + { + "nr": 4, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t4.fctb" + }, + { + "nr": 5, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t5.fctb" + }, + { + "nr": 6, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t6.fctb" + }, + { + "nr": 7, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t7.fctb" + }, + { + "nr": 8, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t8.fctb" + }, + { + "nr": 9, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t9.fctb" + } + ], "version": 1 -} +} \ No newline at end of file From 3fdd505e3f22b27d798b94bd195c5fee2e5885f3 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 28 Oct 2019 19:47:38 -0700 Subject: [PATCH 28/52] Fixed horizontal headers and removed obsolete test slots --- src/Mod/Path/PathScripts/PathToolBitLibraryGui.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index 0c4fcf9091..db29af808b 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -213,6 +213,7 @@ class ToolBitLibrary(object): def libraryLoad(self, path): self.toolTableView.setUpdatesEnabled(False) self.model.clear() + self.model.setHorizontalHeaderLabels(self.columnNames()) if path: with open(path) as fp: library = json.load(fp) @@ -260,18 +261,10 @@ class ToolBitLibrary(object): def columnNames(self): return ['Nr', 'Tool', 'Template', 'Diameter'] - def rowsMoved(self, parent, start, end, dest, row): - PathLog.track(parent, start, end, dest, row) - - def rowsAboutToBeMoved(self, srcParent, srcStart, srcEnd, destParent, destRow): - PathLog.track(srcParent, srcStart, srcEnd, destParent, destRow) - def setupUI(self): PathLog.track('+') self.model = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()), self.toolTableView) self.model.setHorizontalHeaderLabels(self.columnNames()) - self.model.rowsAboutToBeMoved.connect(self.rowsAboutToBeMoved) - self.model.rowsMoved.connect(self.rowsMoved) self.toolTableView.setModel(self.model) self.toolTableView.resizeColumnsToContents() From adc5aec98faaba7ff2ddb37fa20d09e642aa8ca9 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 28 Oct 2019 21:05:22 -0700 Subject: [PATCH 29/52] Added ToolBit library commands --- src/Mod/Path/CMakeLists.txt | 3 + src/Mod/Path/InitGui.py | 5 +- .../Path/PathScripts/PathToolBitLibraryCmd.py | 93 +++++++++++++++++++ .../Path/PathScripts/PathToolBitLibraryGui.py | 29 ++++-- 4 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 7253280206..1b854bf776 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -105,6 +105,8 @@ SET(PathScripts_SRCS PathScripts/PathToolBitCmd.py PathScripts/PathToolBitEdit.py PathScripts/PathToolBitGui.py + PathScripts/PathToolBitLibraryCmd.py + PathScripts/PathToolBitLibraryGui.py PathScripts/PathToolController.py PathScripts/PathToolControllerGui.py PathScripts/PathToolEdit.py @@ -150,6 +152,7 @@ SET(Tools_Bit_SRCS ) SET(Tools_Library_SRCS + Tools/Library/endmills.fctl ) SET(Tools_Template_SRCS diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 192cd18300..25494fbf87 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -72,6 +72,7 @@ class PathWorkbench (Workbench): from PathScripts import PathGuiInit from PathScripts import PathJobCmd from PathScripts import PathToolBitCmd + from PathScripts import PathToolBitLibraryCmd import PathCommands PathGuiInit.Startup() @@ -113,7 +114,7 @@ class PathWorkbench (Workbench): if extracmdlist: self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist) - self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + PathToolBitCmd.CommandList + ["Path_ToolController", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + PathToolBitCmd.CommandList + ["Separator"] + PathToolBitLibraryCmd.CommandList + ["Path_ToolController", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( "Path", "Path Dressup")], dressupcmdlist) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( @@ -154,7 +155,7 @@ class PathWorkbench (Workbench): if "Remote" in selectedName: self.appendContextMenu("", ["Refresh_Path"]) if "Job" in selectedName: - self.appendContextMenu("", ["Path_ExportTemplate"]) + self.appendContextMenu("", ["Path_ExportTemplate", "Path_ToolBitLibraryLoad", "Path_ToolController"]) menuAppended = True if isinstance(obj.Proxy, PathScripts.PathOp.ObjectOp): self.appendContextMenu("", ["Path_OperationCopy", "Path_OpActiveToggle"]) diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py b/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py new file mode 100644 index 0000000000..0583f77db5 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +import FreeCADGui +import PySide.QtCore as QtCore + +class CommandToolBitLibraryOpen: + ''' + Command to ToolBitLibrary editor. + ''' + + def __init__(self): + pass + + def GetResources(self): + return {'Pixmap': 'Path-ToolTable', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Open ToolBit Library editor"), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Open an editor to manage ToolBit libraries")} + + def IsActive(self): + return True + + def Activated(self): + import PathScripts.PathToolBitLibraryGui as PathToolBitLibraryGui + library = PathToolBitLibraryGui.ToolBitLibrary() + library.open() + +class CommandToolBitLibraryLoad: + ''' + Command used to load an entire ToolBitLibrary (or part of it) from a file into a job. + ''' + + def __init__(self): + pass + + def GetResources(self): + return {'Pixmap': 'Path-ToolTable', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Load ToolBit Library"), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Load an entire ToolBit library or part of it into a job")} + + def selectedJob(self): + if FreeCAD.ActiveDocument: + sel = FreeCADGui.Selection.getSelectionEx() + if sel and sel[0].Object.Name[:3] == 'Job': + return sel[0].Object + jobs = [o for o in FreeCAD.ActiveDocument.Objects if o.Name[:3] == 'Job'] + if 1 == len(jobs): + return jobs[0] + return None + + def IsActive(self): + return not self.selectedJob() is None + + def Activated(self): + import PathScripts.PathToolBitLibraryGui as PathToolBitLibraryGui + import PathScripts.PathToolControllerGui as PathToolControllerGui + job = self.selectedJob() + library = PathToolBitLibraryGui.ToolBitLibrary() + if 1 == library.open(dialog=True): + for nr, tool in library.selectedOrAllTools(): + tc = PathToolControllerGui.Create("TC: {}".format(tool.Label), tool, nr) + job.Proxy.addToolController(tc) + FreeCAD.ActiveDocument.recompute() + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('Path_ToolBitLibraryOpen', CommandToolBitLibraryOpen()) + FreeCADGui.addCommand('Path_ToolBitLibraryLoad', CommandToolBitLibraryLoad()) + +CommandList = ['Path_ToolBitLibraryOpen', 'Path_ToolBitLibraryLoad'] + +FreeCAD.Console.PrintLog("Loading PathToolBitLibraryCmd... done\n") diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index db29af808b..bcea963cc6 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -40,7 +40,8 @@ PathLog.trackModule(PathLog.thisModule()) _UuidRole = PySide.QtCore.Qt.UserRole + 1 _PathRole = PySide.QtCore.Qt.UserRole + 2 -class TableView(PySide.QtGui.QTableView): +class _TableView(PySide.QtGui.QTableView): + '''Subclass of QTableView to support rearrange and copying of ToolBits''' def __init__(self, parent): PySide.QtGui.QTableView.__init__(self, parent) @@ -78,6 +79,8 @@ class TableView(PySide.QtGui.QTableView): model.setData(model.index(dstRow, col), srcItem.data(PySide.QtCore.Qt.EditRole), PySide.QtCore.Qt.EditRole) if col == 0: model.setData(model.index(dstRow, col), srcItem.data(_PathRole), _PathRole) + # Even a clone of a tool gets its own uuid so it can be identified when + # rearranging the order or inserting/deleting rows model.setData(model.index(dstRow, col), uuid.uuid4(), _UuidRole) else: model.item(dstRow, col).setEditable(False) @@ -120,15 +123,13 @@ class TableView(PySide.QtGui.QTableView): for uuid in srcUuids: model.removeRow(self._rowWithUuid(uuid)) -#class ToolTableModel(PySide.QtGui.QStandardItemModel): - - class ToolBitLibrary(object): + '''ToolBitLibrary is the controller for displaying/selecting/creating/editing a collection of ToolBits.''' def __init__(self, path=None): self.path = path self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitLibraryEdit.ui') - self.toolTableView = TableView(self.form.toolTableGroup) + self.toolTableView = _TableView(self.form.toolTableGroup) self.form.toolTableGroup.layout().replaceWidget(self.form.toolTable, self.toolTableView) self.form.toolTable.hide() self.setupUI() @@ -173,6 +174,18 @@ class ToolBitLibrary(object): PathLog.error('something happened') PathLog.error(traceback.print_exc()) + def selectedOrAllTools(self): + selectedRows = set([index.row() for index in self.toolTableView.selectedIndexes()]) + if not selectedRows: + selectedRows = list(range(self.model.rowCount())) + tools = [] + for row in selectedRows: + item = self.model.item(row, 0) + toolNr = int(item.data(PySide.QtCore.Qt.EditRole)) + toolPath = item.data(_PathRole) + tools.append((toolNr, PathToolBitGui.CreateFrom(toolPath))) + return tools + def toolDelete(self): PathLog.track() selectedRows = set([index.row() for index in self.toolTableView.selectedIndexes()]) @@ -187,13 +200,17 @@ class ToolBitLibrary(object): def toolSelect(self, selected, deselected): self.form.toolDelete.setEnabled(len(self.toolTableView.selectedIndexes()) > 0) - def open(self, path=None): + def open(self, path=None, dialog=False): + '''open(path=None, dialog=False) ... load library stored in path and bring up ui. + Returns 1 if user pressed OK, 0 otherwise.''' if path: fullPath = PathToolBit.findLibrary(path) if fullPath: self.libraryLoad(fullPath) else: self.libraryOpen() + elif dialog: + self.libraryOpen() return self.form.exec_() def updateToolbar(self): From 1395749dc227be29f7e21a017f7b56d52456a3c3 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 28 Oct 2019 21:31:55 -0700 Subject: [PATCH 30/52] Delete ToolBit if ToolController is deleted --- src/Mod/Path/PathScripts/PathToolController.py | 5 +++++ src/Mod/Path/PathScripts/PathToolControllerGui.py | 1 + 2 files changed, 6 insertions(+) diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 1e0b42c07b..46bb4fabfc 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -84,6 +84,11 @@ class ToolController: def onDocumentRestored(self, obj): obj.setEditorMode('Placement', 2) + def onDelete(self, obj, arg2=None): + if not self.usesLegacyTool(obj): + if len(obj.Tool.InList) == 1: + obj.Document.removeObject(obj.Tool.Name) + def setFromTemplate(self, obj, template): '''setFromTemplate(obj, xmlItem) ... extract properties from xmlItem and assign to receiver.''' PathLog.track(obj.Name, template) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 4b89972013..81952c8511 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -80,6 +80,7 @@ class ViewProvider: def onDelete(self, vobj, args=None): # pylint: disable=unused-argument PathUtil.clearExpressionEngine(vobj.Object) + self.vobj.Object.Proxy.onDelete(vobj.Object, args) return True def updateData(self, vobj, prop): From c8afefa05d01c44a7e188e8098aa2c8ec35f7b6e Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 28 Oct 2019 21:37:24 -0700 Subject: [PATCH 31/52] Hide ToolBit by default if managed by a ToolController --- src/Mod/Path/PathScripts/PathToolControllerGui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 81952c8511..f0f088e445 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -127,6 +127,10 @@ def Create(name = 'Default Tool', tool=None, toolNumber=1): obj = PathScripts.PathToolController.Create(name, tool, toolNumber) ViewProvider(obj.ViewObject) + if not obj.Proxy.usesLegacyTool(obj): + # ToolBits are visible by default, which is typically not what the user wants + if tool and tool.ViewObject and tool.ViewObject.Visibility: + tool.ViewObject.Visibility = False return obj From 09a6201e87a889bc5150c54ab649556e0a696e60 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Tue, 29 Oct 2019 21:54:15 -0700 Subject: [PATCH 32/52] Invoke TC.onDelete when deleting a job --- src/Mod/Path/PathScripts/PathJob.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/Path/PathScripts/PathJob.py b/src/Mod/Path/PathScripts/PathJob.py index 509bb7d72b..9a97fc40d9 100644 --- a/src/Mod/Path/PathScripts/PathJob.py +++ b/src/Mod/Path/PathScripts/PathJob.py @@ -222,6 +222,7 @@ class ObjectJob: PathLog.debug('taking down tool controller') for tc in obj.ToolController: PathUtil.clearExpressionEngine(tc) + tc.Proxy.onDelete(tc) doc.removeObject(tc.Name) obj.ToolController = [] # SetupSheet From d625303b71ab33ea14bed1e5f1c99c8e52abfbb8 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Tue, 29 Oct 2019 22:11:10 -0700 Subject: [PATCH 33/52] Added JobTemplate support for ToolBit --- src/Mod/Path/PathScripts/PathToolBit.py | 54 +++++++----- src/Mod/Path/PathScripts/PathToolBitCmd.py | 2 +- src/Mod/Path/PathScripts/PathToolBitGui.py | 33 ++++---- .../Path/PathScripts/PathToolBitLibraryGui.py | 2 +- .../Path/PathScripts/PathToolController.py | 82 ++++++++++++------- .../Path/PathScripts/PathToolControllerGui.py | 2 +- 6 files changed, 102 insertions(+), 73 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 1813b4c2b8..703859aaee 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -262,16 +262,8 @@ class ToolBit(object): def saveToFile(self, obj, path, setFile=True): try: - data = {} - data['version'] = 1 - data['name'] = obj.Label - data['template'] = obj.BitTemplate - params = {} - for prop in self.bitPropertyNames(obj): - params[prop] = PathUtil.getProperty(obj, prop).UserString - data['parameter'] = params with open(path, 'w') as fp: - json.dump(data, fp, indent=' ') + json.dump(self.templateAttrs(obj), fp, indent=' ') if setFile: obj.File = path return True @@ -279,26 +271,44 @@ class ToolBit(object): PathLog.error("Could not save tool %s to %s (%s)" % (obj.Label, path, e)) raise + def templateAttrs(self, obj): + attrs = {} + attrs['version'] = 2 # Path.Tool is version 1 + attrs['name'] = obj.Label + attrs['template'] = obj.BitTemplate + params = {} + for prop in self.bitPropertyNames(obj): + params[prop] = PathUtil.getProperty(obj, prop).UserString + attrs['parameter'] = params + return attrs + def Declaration(path): with open(path, 'r') as fp: return json.load(fp) -def CreateFrom(path, name = 'ToolBit'): - try: - data = Declaration(path) - obj = Create(name, data['template']) - obj.Label = data['name'] - params = data['parameter'] +class ToolBitFactory(object): + + def CreateFromAttrs(self, attrs, name='ToolBit'): + obj = Factory.Create(name, attrs['template']) + 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) return obj - except (OSError, IOError) as e: - PathLog.error("%s not a valid tool file (%s)" % (path, e)) - raise -def Create(name = 'ToolBit', templateFile=None): - obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name) - obj.Proxy = ToolBit(obj, templateFile) - return obj + def CreateFrom(self, path, name='ToolBit'): + try: + data = Declaration(path) + return Factory.CreateFromAttrs(data, name) + except (OSError, IOError) as e: + PathLog.error("%s not a valid tool file (%s)" % (path, e)) + raise + + def Create(self, name='ToolBit', templateFile=None): + obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name) + obj.Proxy = ToolBit(obj, templateFile) + return obj + +Factory = ToolBitFactory() diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py index e3e9a2d7ab..b651997aac 100644 --- a/src/Mod/Path/PathScripts/PathToolBitCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -47,7 +47,7 @@ class CommandToolBitCreate: def Activated(self): import PathScripts.PathToolBitGui as PathToolBitGui - obj = PathToolBitGui.Create() + obj = PathToolBit.Factory.Create() obj.ViewObject.Proxy.setCreate(obj.ViewObject) class CommandToolBitSave: diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 977a3fa683..9b35fb69ba 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -213,7 +213,7 @@ class ToolBitSelector(object): def createTool(self): PathLog.track() - tool = Create() + tool = PathToolBit.Factory.Create() def accept(): self.editor.accept() @@ -253,22 +253,16 @@ class ToolBitSelector(object): self.form.tools.itemSelectionChanged.connect(self.updateSelection) self.form.tools.doubleClicked.connect(self.form.accept) -def Create(name = 'ToolBit'): - '''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.Create(name) - PathIconViewProvider.Attach(tool.ViewObject, name) - FreeCAD.ActiveDocument.commitTransaction() - return tool +class ToolBitGuiFactory(PathToolBit.ToolBitFactory): -def CreateFrom(path, name = 'ToolBit'): - '''CreateFrom(path, name = 'ToolBit') ... creates an instance of a tool stored in path''' - FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit instance')) - tool = PathToolBit.CreateFrom(path, name) - PathIconViewProvider.Attach(tool.ViewObject, name) - FreeCAD.ActiveDocument.commitTransaction() - return tool + def Create(self, name='ToolBit', templateFile=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, templateFile) + PathIconViewProvider.Attach(tool.ViewObject, name) + FreeCAD.ActiveDocument.commitTransaction() + return tool def GetToolFile(parent = None): if parent is None: @@ -292,10 +286,13 @@ def GetToolFiles(parent = None): def LoadTool(parent = None): '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' foo = GetToolFile(parent) - return CreateFrom(foo) if foo else foo + return PathToolBit.Factory.CreateFrom(foo) if foo else foo def LoadTools(parent = None): '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' - return [CreateFrom(foo) for foo in GetToolFiles(parent)] + return [PathToolBit.Factory.CreateFrom(foo) for foo in GetToolFiles(parent)] + +# Set the factory so all tools are created with UI +PathToolBit.Factory = ToolBitGuiFactory() PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider) diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index bcea963cc6..bd976dd404 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -183,7 +183,7 @@ class ToolBitLibrary(object): item = self.model.item(row, 0) toolNr = int(item.data(PySide.QtCore.Qt.EditRole)) toolPath = item.data(_PathRole) - tools.append((toolNr, PathToolBitGui.CreateFrom(toolPath))) + tools.append((toolNr, PathToolBit.Factory.CreateFrom(toolPath))) return tools def toolDelete(self): diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 46bb4fabfc..59e6fcc86a 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -26,6 +26,7 @@ import FreeCAD import Path import PathScripts.PathLog as PathLog +import PathScripts.PathToolBit as PathToolBit from PySide import QtCore @@ -67,11 +68,7 @@ class ToolController: obj.addProperty("App::PropertyIntegerConstraint", "ToolNumber", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The active tool")) obj.ToolNumber = (0, 0, 10000, 1) - if cTool: - obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) - else: - obj.addProperty("App::PropertyLink", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) - + self.ensureUseLegacyTool(obj, cTool) obj.addProperty("App::PropertyFloat", "SpindleSpeed", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The speed of the cutting spindle in RPM")) obj.addProperty("App::PropertyEnumeration", "SpindleDir", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Direction of spindle rotation")) obj.SpindleDir = ['Forward', 'Reverse'] @@ -92,31 +89,44 @@ class ToolController: def setFromTemplate(self, obj, template): '''setFromTemplate(obj, xmlItem) ... extract properties from xmlItem and assign to receiver.''' PathLog.track(obj.Name, template) - if template.get(ToolControllerTemplate.Version) and 1 == int(template.get(ToolControllerTemplate.Version)): - if template.get(ToolControllerTemplate.Label): - obj.Label = template.get(ToolControllerTemplate.Label) - if template.get(ToolControllerTemplate.VertFeed): - obj.VertFeed = template.get(ToolControllerTemplate.VertFeed) - if template.get(ToolControllerTemplate.HorizFeed): - obj.HorizFeed = template.get(ToolControllerTemplate.HorizFeed) - if template.get(ToolControllerTemplate.VertRapid): - obj.VertRapid = template.get(ToolControllerTemplate.VertRapid) - if template.get(ToolControllerTemplate.HorizRapid): - obj.HorizRapid = template.get(ToolControllerTemplate.HorizRapid) - if template.get(ToolControllerTemplate.SpindleSpeed): - obj.SpindleSpeed = float(template.get(ToolControllerTemplate.SpindleSpeed)) - if template.get(ToolControllerTemplate.SpindleDir): - obj.SpindleDir = template.get(ToolControllerTemplate.SpindleDir) - if template.get(ToolControllerTemplate.ToolNumber): - obj.ToolNumber = int(template.get(ToolControllerTemplate.ToolNumber)) - if template.get(ToolControllerTemplate.Tool): - obj.Tool.setFromTemplate(template.get(ToolControllerTemplate.Tool)) - if template.get(ToolControllerTemplate.Expressions): - for exprDef in template.get(ToolControllerTemplate.Expressions): - if exprDef[ToolControllerTemplate.ExprExpr]: - obj.setExpression(exprDef[ToolControllerTemplate.ExprProp], exprDef[ToolControllerTemplate.ExprExpr]) + version = 0 + if template.get(ToolControllerTemplate.Version): + version = int(template.get(ToolControllerTemplate.Version)) + if version == 1 or version == 2: + if template.get(ToolControllerTemplate.Label): + obj.Label = template.get(ToolControllerTemplate.Label) + if template.get(ToolControllerTemplate.VertFeed): + obj.VertFeed = template.get(ToolControllerTemplate.VertFeed) + if template.get(ToolControllerTemplate.HorizFeed): + obj.HorizFeed = template.get(ToolControllerTemplate.HorizFeed) + if template.get(ToolControllerTemplate.VertRapid): + obj.VertRapid = template.get(ToolControllerTemplate.VertRapid) + if template.get(ToolControllerTemplate.HorizRapid): + obj.HorizRapid = template.get(ToolControllerTemplate.HorizRapid) + if template.get(ToolControllerTemplate.SpindleSpeed): + obj.SpindleSpeed = float(template.get(ToolControllerTemplate.SpindleSpeed)) + if template.get(ToolControllerTemplate.SpindleDir): + obj.SpindleDir = template.get(ToolControllerTemplate.SpindleDir) + if template.get(ToolControllerTemplate.ToolNumber): + obj.ToolNumber = int(template.get(ToolControllerTemplate.ToolNumber)) + if template.get(ToolControllerTemplate.Tool): + toolVersion = template.get(ToolControllerTemplate.Tool).get(ToolControllerTemplate.Version) + if toolVersion == 1: + self.ensureUseLegacyTool(obj, True) + obj.Tool.setFromTemplate(template.get(ToolControllerTemplate.Tool)) + else: + self.ensureUseLegacyTool(obj, False) + obj.Tool = PathToolBit.Factory.CreateFromAttrs(template.get(ToolControllerTemplate.Tool)) + if obj.Tool and obj.Tool.ViewObject and obj.Tool.ViewObject.Visibility: + obj.ViewObject.Visibility = False + if template.get(ToolControllerTemplate.Expressions): + for exprDef in template.get(ToolControllerTemplate.Expressions): + if exprDef[ToolControllerTemplate.ExprExpr]: + obj.setExpression(exprDef[ToolControllerTemplate.ExprProp], exprDef[ToolControllerTemplate.ExprExpr]) + else: + PathLog.error(translate('PathToolController', "Unsupported PathToolController template version %s") % template.get(ToolControllerTemplate.Version)) else: - PathLog.error(translate('PathToolController', "Unsupported PathToolController template version %s") % template.get(ToolControllerTemplate.Version)) + PathLog.error(translate('PathToolController', 'PathToolController template has no version - corrupted template file?')) def templateAttrs(self, obj): '''templateAttrs(obj) ... answer a dictionary with all properties that should be stored for a template.''' @@ -131,7 +141,10 @@ class ToolController: attrs[ToolControllerTemplate.HorizRapid] = ("%s" % (obj.HorizRapid)) attrs[ToolControllerTemplate.SpindleSpeed] = obj.SpindleSpeed attrs[ToolControllerTemplate.SpindleDir] = obj.SpindleDir - attrs[ToolControllerTemplate.Tool] = obj.Tool.templateAttrs() + if self.usesLegacyTool(obj): + attrs[ToolControllerTemplate.Tool] = obj.Tool.templateAttrs() + else: + attrs[ToolControllerTemplate.Tool] = obj.Tool.Proxy.templateAttrs(obj.Tool) expressions = [] for expr in obj.ExpressionEngine: PathLog.debug('%s: %s' % (expr[0], expr[1])) @@ -169,6 +182,15 @@ class ToolController: '''returns True if the tool being controlled is a legacy tool''' return isinstance(obj.Tool, Path.Tool) + def ensureUseLegacyTool(self, obj, legacy): + if not hasattr(obj, 'Tool') or (legacy != self.usesLegacyTool(obj)): + if hasattr(obj, 'Tool'): + obj.removeProperty('Tool') + if legacy: + obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) + else: + obj.addProperty("App::PropertyLink", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) + def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=True): PathLog.track(tool, toolNumber) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index f0f088e445..3a82f0fc41 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -118,7 +118,7 @@ class ViewProvider: def claimChildren(self): obj = self.vobj.Object - if not obj.Proxy.usesLegacyTool(obj): + if obj and obj.Proxy and not obj.Proxy.usesLegacyTool(obj): return [obj.Tool] return [] From bb76d2755b79b7f68c8792ae783c9fb7da5dcdcd Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Wed, 30 Oct 2019 20:27:22 -0700 Subject: [PATCH 34/52] Fixed creating a ToolBit --- src/Mod/Path/PathScripts/PathToolBitCmd.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py index b651997aac..b172a0aa43 100644 --- a/src/Mod/Path/PathScripts/PathToolBitCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -46,8 +46,7 @@ class CommandToolBitCreate: return FreeCAD.ActiveDocument is not None def Activated(self): - import PathScripts.PathToolBitGui as PathToolBitGui - obj = PathToolBit.Factory.Create() + obj = PathScripts.PathToolBit.Factory.Create() obj.ViewObject.Proxy.setCreate(obj.ViewObject) class CommandToolBitSave: From 54e2ff2fabeef5a81a373bd4f3a700eb04c93a3d Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 13:21:40 -0700 Subject: [PATCH 35/52] Fixed tool visibility on TC creation --- src/Mod/Path/PathScripts/PathToolController.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 59e6fcc86a..57fbf2e87c 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -118,7 +118,7 @@ class ToolController: self.ensureUseLegacyTool(obj, False) obj.Tool = PathToolBit.Factory.CreateFromAttrs(template.get(ToolControllerTemplate.Tool)) if obj.Tool and obj.Tool.ViewObject and obj.Tool.ViewObject.Visibility: - obj.ViewObject.Visibility = False + obj.Tool.ViewObject.Visibility = False if template.get(ToolControllerTemplate.Expressions): for exprDef in template.get(ToolControllerTemplate.Expressions): if exprDef[ToolControllerTemplate.ExprExpr]: From 5c3bff1e014ecf9b53a108df5cc4a7e1adf23122 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 13:42:37 -0700 Subject: [PATCH 36/52] Added arbitrary attributes to ToolBit - currently re-creating the existing ones. --- .../Gui/Resources/panels/ToolBitEditor.ui | 335 ++++++++++-------- src/Mod/Path/PathScripts/PathSetupSheet.py | 6 +- .../PathScripts/PathSetupSheetOpPrototype.py | 9 + src/Mod/Path/PathScripts/PathToolBit.py | 49 ++- src/Mod/Path/PathScripts/PathToolBitEdit.py | 68 +++- src/Mod/Path/PathScripts/PathUtil.py | 7 + 6 files changed, 313 insertions(+), 161 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui index 06b0ccad45..ab057b2dda 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui @@ -6,156 +6,209 @@ 0 0 - 411 - 886 + 587 + 744 Form - - - - - Tool Bit + + + + + 0 - - - - - Name - - - - - - - 50 - - - Display Name - - - - - - - Type - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - ... - - - - - - - - - - - - - Bit Parameter - - - - QFormLayout::AllNonFixedFieldsGrow + + + + 0 + 0 + 559 + 626 + - - - - Point/Tip Angle - - - - - - - 180° - - - ° - - - - - - - Cutting Edge Height - - - - - - - 0.00 - - - mm - - - - + + Shape + + + + + + Tool Bit + + + + + + Name + + + + + + + 50 + + + Display Name + + + + + + + Type + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + ... + + + + + + + + + + + + + Bit Parameter + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Point/Tip Angle + + + + + + + 180° + + + ° + + + + + + + Cutting Edge Height + + + + + + + 0.00 + + + mm + + + + + + + + + + + 210 + 297 + + + + Image + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 277 + + + + + + + + + + 0 + 0 + 559 + 626 + + + + Attributes + + + + + + + 0 + 2 + + + + + 0 + 300 + + + + QAbstractItemView::AllEditTriggers + + + true + + + + + - - - - - - - - 210 - 297 - - - - Image - - - Qt::AlignCenter - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/src/Mod/Path/PathScripts/PathSetupSheet.py b/src/Mod/Path/PathScripts/PathSetupSheet.py index 1378d91032..aaaca2732e 100644 --- a/src/Mod/Path/PathScripts/PathSetupSheet.py +++ b/src/Mod/Path/PathScripts/PathSetupSheet.py @@ -209,11 +209,7 @@ class SetupSheet: for propName in op.properties(): prop = OpPropertyName(opName, propName) if hasattr(self.obj, prop): - attr = getattr(self.obj, prop) - if hasattr(attr, 'UserString'): - settings[propName] = attr.UserString - else: - settings[propName] = attr + settings[propName] = PathUtil.getPropertyValueString(self.obj, prop) attrs[opName] = settings return attrs diff --git a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py index 8c5c3e4b8d..fba3d33f0e 100644 --- a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py +++ b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py @@ -126,14 +126,23 @@ class PropertyFloat(Property): def typeString(self): return "Float" + def valueFromString(self, string): + return float(string) + class PropertyInteger(Property): def typeString(self): return "Integer" + def valueFromString(self, string): + return int(string) + class PropertyBool(Property): def typeString(self): return "Bool" + def valueFromString(self, string): + return bool(string) + class PropertyString(Property): def typeString(self): return "String" diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 703859aaee..cfe40f387b 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -116,7 +116,8 @@ def updateConstraint(sketch, name, value): PathLog.track(name, constraint.Type, 'unchanged') break -PropertyGroupBit = 'Bit' +PropertyGroupBit = 'Bit' +PropertyGroupAttribute = 'Attribute' class ToolBit(object): @@ -128,7 +129,7 @@ class ToolBit(object): obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) if templateFile is not None: obj.BitTemplate = templateFile - self._setupBitFromTemplate(obj) + self._setupBitShape(obj) self.onDocumentRestored(obj) def __getstate__(self): @@ -141,29 +142,35 @@ class ToolBit(object): break return None - def bitPropertyNames(self, obj): + def propertyNamesBit(self, obj): return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupBit] + def propertyNamesAttribute(self, obj): + return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupAttribute] + def onDocumentRestored(self, obj): obj.setEditorMode('BitTemplate', 1) obj.setEditorMode('BitBody', 2) obj.setEditorMode('File', 1) obj.setEditorMode('Shape', 2) - for prop in self.bitPropertyNames(obj): + for prop in self.propertyNamesBit(obj): obj.setEditorMode(prop, 1) + # I currently don't see why these need to be read-only + #for prop in self.propertyNamesAttribute(obj): + # obj.setEditorMode(prop, 1) def onChanged(self, obj, prop): PathLog.track(obj.Label, prop) if prop == 'BitTemplate' and not 'Restore' in obj.State: - self._setupBitFromTemplate(obj) + self._setupBitShape(obj) #elif obj.getGroupOfProperty(prop) == PropertyGroupBit: # self._updateBitShape(obj, [prop]) def _updateBitShape(self, obj, properties=None): if not obj.BitBody is None: if not properties: - properties = self.bitPropertyNames(obj) + 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) @@ -203,7 +210,7 @@ class ToolBit(object): PathLog.track(obj.Label) self._removeBitBody(obj) self._copyBitShape(obj) - for prop in self.bitPropertyNames(obj): + for prop in self.propertyNamesBit(obj): obj.removeProperty(prop) def loadBitBody(self, obj, force=False): @@ -219,7 +226,7 @@ class ToolBit(object): def unloadBitBody(self, obj): self._removeBitBody(obj) - def _setupBitFromTemplate(self, obj, path=None): + def _setupBitShape(self, obj, path=None): (doc, docOpened) = self._loadBitBody(obj, path) obj.Label = doc.RootObjects[0].Label @@ -277,15 +284,30 @@ class ToolBit(object): attrs['name'] = obj.Label attrs['template'] = obj.BitTemplate params = {} - for prop in self.bitPropertyNames(obj): - params[prop] = PathUtil.getProperty(obj, prop).UserString + for name in self.propertyNamesBit(obj): + params[name] = PathUtil.getPropertyValueString(obj, name) attrs['parameter'] = params + params = {} + for name in self.propertyNamesAttribute(obj): + params[name] = PathUtil.getPropertyValueString(obj, name) + attrs['attribute'] = params return attrs def Declaration(path): with open(path, 'r') as fp: return json.load(fp) +class AttributePrototype(PathSetupSheetOpPrototype.OpPrototype): + + def __init__(self): + PathSetupSheetOpPrototype.OpPrototype.__init__(self, 'ToolBitAttribute') + self.addProperty('App::PropertyEnumeration', 'Material', PropertyGroupAttribute, translate('PathToolBit', 'Tool bit material')) + self.Material = ['Carbide', 'CastAlloy', 'Ceramics', 'Diamond', 'HighCarbonToolSteel', 'HighSpeedSteel', 'Sialon'] + self.addProperty('App::PropertyDistance', 'LengthOffset', PropertyGroupAttribute, translate('PathToolBit', 'Length offset in Z direction')) + self.addProperty('App::PropertyInteger', 'Flutes', PropertyGroupAttribute, translate('PathToolBit', 'The number of flutes')) + self.addProperty('App::PropertyDistance', 'ChipLoad', PropertyGroupAttribute, translate('PathToolBit', 'Chipload as per manufacturer')) + + class ToolBitFactory(object): def CreateFromAttrs(self, attrs, name='ToolBit'): @@ -296,6 +318,13 @@ class ToolBitFactory(object): PathUtil.setProperty(obj, prop, params[prop]) obj.Proxy._updateBitShape(obj) obj.Proxy.unloadBitBody(obj) + params = attrs['attribute'] + proto = AttributePrototype() + for pname in params: + prop = proto.getProperty(pname) + val = prop.valueFromString(params[pname]) + print("prop[%s] = %s (%s)" % (pname, params[pname], type(val))) + prop.setupProperty(obj, pname, PropertyGroupAttribute, prop.valueFromString(params[pname])) return obj def CreateFrom(self, path, name='ToolBit'): diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index f7ca4f2ab8..d10c66ae8c 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -27,19 +27,23 @@ import FreeCADGui import Path import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog +import PathScripts.PathPreferences as PathPreferences +import PathScripts.PathSetupSheetGui as PathSetupSheetGui import PathScripts.PathToolBit as PathToolBit import copy import math import re -from PySide import QtGui +from PySide import QtCore, QtGui PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) PathLog.trackModule(PathLog.thisModule()) -LastPath = 'src/Mod/Path/Tools/Template' +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) -class ToolBitEditor: +class ToolBitEditor(object): '''UI and controller for editing a ToolBit. The controller embeds the UI to the parentWidget which has to have a layout attached to it. ''' @@ -56,6 +60,7 @@ class ToolBitEditor: self.tool.BitTemplate = 'src/Mod/Path/Tools/Template/endmill-straight.fcstd' self.tool.Proxy.loadBitBody(self.tool) self.setupTool(self.tool) + self.setupAttributes(self.tool) def setupTool(self, tool): layout = self.form.bitParams.layout() @@ -78,10 +83,63 @@ class ToolBitEditor: else: self.form.image.setPixmap(QtGui.QPixmap()) + def setupAttributes(self, tool): + self.proto = PathToolBit.AttributePrototype() + self.props = sorted(self.proto.properties) + self.delegate = PathSetupSheetGui.Delegate(self.form) + self.model = QtGui.QStandardItemModel(len(self.props), 3, self.form) + self.model.setHorizontalHeaderLabels(['Set', 'Property', 'Value']) + + for i, name in enumerate(self.props): + prop = self.proto.getProperty(name) + isset = hasattr(tool, name) + if isset: + prop.setValue(getattr(tool, name)) + + self.model.setData(self.model.index(i, 0), isset, QtCore.Qt.EditRole) + self.model.setData(self.model.index(i, 1), name, QtCore.Qt.EditRole) + self.model.setData(self.model.index(i, 2), prop, PathSetupSheetGui.Delegate.PropertyRole) + self.model.setData(self.model.index(i, 2), prop.displayString(), QtCore.Qt.DisplayRole) + + self.model.item(i, 0).setCheckable(True) + self.model.item(i, 0).setText('') + self.model.item(i, 1).setEditable(False) + self.model.item(i, 1).setToolTip(prop.info) + self.model.item(i, 2).setToolTip(prop.info) + + if isset: + self.model.item(i, 0).setCheckState(QtCore.Qt.Checked) + else: + self.model.item(i, 0).setCheckState(QtCore.Qt.Unchecked) + self.model.item(i, 1).setEnabled(False) + self.model.item(i, 2).setEnabled(False) + + self.form.attrTable.setModel(self.model) + self.form.attrTable.setItemDelegateForColumn(2, self.delegate) + self.form.attrTable.resizeColumnsToContents() + self.form.attrTable.verticalHeader().hide() + + self.model.dataChanged.connect(self.updateData) + + def updateData(self, topLeft, bottomRight): + if 0 == topLeft.column(): + isset = self.model.item(topLeft.row(), 0).checkState() == QtCore.Qt.Checked + self.model.item(topLeft.row(), 1).setEnabled(isset) + self.model.item(topLeft.row(), 2).setEnabled(isset) + def accept(self): self.refresh() self.tool.Proxy.unloadBitBody(self.tool) + # get the attributes + for i, name in enumerate(self.props): + prop = self.proto.getProperty(name) + enabled = self.model.item(i, 0).checkState() == QtCore.Qt.Checked + if enabled and not prop.getValue() is None: + prop.setupProperty(self.tool, name, PathToolBit.PropertyGroupAttribute, prop.getValue()) + elif hasattr(self.tool, name): + self.obj.removeProperty(name) + def reject(self): self.tool.Proxy.unloadBitBody(self.tool) pass @@ -120,15 +178,15 @@ class ToolBitEditor: self.form.blockSignals(False) def selectTemplate(self): - global LastPath path = self.tool.BitTemplate if not path: - path = LastPath + path = PathPreferences.lastPathToolTemplate() foo = QtGui.QFileDialog.getOpenFileName(self.form, "Path - Tool Template", path, "*.fcstd") if foo and foo[0]: + PathPreferences.setLastPathToolTemplate(os.path.dirname(foo[0])) self.form.templatePath.setText(foo[0]) self.updateTemplate() diff --git a/src/Mod/Path/PathScripts/PathUtil.py b/src/Mod/Path/PathScripts/PathUtil.py index 9633c6833d..952ced9b2f 100644 --- a/src/Mod/Path/PathScripts/PathUtil.py +++ b/src/Mod/Path/PathScripts/PathUtil.py @@ -63,6 +63,13 @@ def getProperty(obj, prop): o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable return attr +def getPropertyValueString(obj, prop): + '''getPropertyValueString(obj, prop) ... answer a string represntation of an object's property's value.''' + attr = getProperty(obj, prop) + if hasattr(attr, 'UserString'): + return attr.UserString + return str(attr) + def setProperty(obj, prop, value): '''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.''' o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable From fee1ddf73a542047bd54ba465c4e76ed31b7ca86 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 13:51:42 -0700 Subject: [PATCH 37/52] Remove ToolBit as valid base objects for Jobs --- src/Mod/Path/PathScripts/PathUtil.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Mod/Path/PathScripts/PathUtil.py b/src/Mod/Path/PathScripts/PathUtil.py index 952ced9b2f..6cc131d71a 100644 --- a/src/Mod/Path/PathScripts/PathUtil.py +++ b/src/Mod/Path/PathScripts/PathUtil.py @@ -86,6 +86,9 @@ def isValidBaseObject(obj): # Can't link to anything inside a geo feature group anymore PathLog.debug("%s is inside a geo feature group" % obj.Label) return False + if hasattr(obj, 'BitBody') and hasattr(obj, 'BitTemplate'): + # ToolBit's are not valid base objects + return False if obj.TypeId in NotValidBaseTypeIds: PathLog.debug("%s is blacklisted (%s)" % (obj.Label, obj.TypeId)) return False From db1f97b16e43d2a89d1cef91c8272e3b4186167a Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 14:04:34 -0700 Subject: [PATCH 38/52] Renamed BitTemplate to BitShape for clarity --- src/Mod/Path/CMakeLists.txt | 14 +++--- .../Gui/Resources/panels/ToolBitEditor.ui | 4 +- src/Mod/Path/PathScripts/PathPreferences.py | 14 +++--- src/Mod/Path/PathScripts/PathToolBit.py | 44 +++++++++--------- src/Mod/Path/PathScripts/PathToolBitEdit.py | 30 ++++++------ src/Mod/Path/PathScripts/PathToolBitGui.py | 4 +- .../Path/PathScripts/PathToolBitLibraryGui.py | 10 ++-- src/Mod/Path/PathScripts/PathUtil.py | 2 +- src/Mod/Path/Tools/Bit/t1.fctb | 2 +- src/Mod/Path/Tools/Bit/t2.fctb | 2 +- src/Mod/Path/Tools/Bit/t3.fctb | 2 +- src/Mod/Path/Tools/Bit/t4.fctb | 2 +- src/Mod/Path/Tools/Bit/t5.fctb | 2 +- src/Mod/Path/Tools/Bit/t6.fctb | 2 +- src/Mod/Path/Tools/Bit/t7.fctb | 2 +- src/Mod/Path/Tools/Bit/t8.fctb | 2 +- src/Mod/Path/Tools/Bit/t9.fctb | 2 +- src/Mod/Path/Tools/README.md | 6 +-- .../{Template => Shape}/drill-straight.fcstd | Bin .../endmill-straight.fcstd | Bin .../Tools/{Template => Shape}/v-bit.fcstd | Bin 21 files changed, 73 insertions(+), 73 deletions(-) rename src/Mod/Path/Tools/{Template => Shape}/drill-straight.fcstd (100%) rename src/Mod/Path/Tools/{Template => Shape}/endmill-straight.fcstd (100%) rename src/Mod/Path/Tools/{Template => Shape}/v-bit.fcstd (100%) diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 1b854bf776..f266fce952 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -155,10 +155,10 @@ SET(Tools_Library_SRCS Tools/Library/endmills.fctl ) -SET(Tools_Template_SRCS - Tools/Template/drill-straight.fcstd - Tools/Template/endmill-straight.fcstd - Tools/Template/v-bit.fcstd +SET(Tools_Shape_SRCS + Tools/Shape/drill-straight.fcstd + Tools/Shape/endmill-straight.fcstd + Tools/Shape/v-bit.fcstd ) SET(PathTests_SRCS @@ -207,7 +207,7 @@ SET(all_files ${PathScripts_post_SRCS} ${Tools_Bit_SRCS} ${Tools_Library_SRCS} - ${Tools_Template_SRCS} + ${Tools_Shape_SRCS} ${Path_Images} ) @@ -264,9 +264,9 @@ INSTALL( INSTALL( FILES - ${Tools_Template_SRCS} + ${Tools_Shape_SRCS} DESTINATION - Mod/Path/Tools/Template + Mod/Path/Tools/Shape ) INSTALL( diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui index ab057b2dda..29b490323b 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui @@ -78,10 +78,10 @@ 0 - + - + ... diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py index 9aa1c13f7d..f03141ef86 100644 --- a/src/Mod/Path/PathScripts/PathPreferences.py +++ b/src/Mod/Path/PathScripts/PathPreferences.py @@ -43,7 +43,7 @@ PostProcessorOutputPolicy = "PostProcessorOutputPolicy" LastPathToolBit = "LastPathToolBit" LastPathToolLibrary = "LastPathToolLibrary" -LastPathToolTemplate = "LastPathToolTemplate" +LastPathToolShape = "LastPathToolShape" # Linear tolerance to use when generating Paths, eg when tessellating geometry GeometryTolerance = "GeometryTolerance" @@ -133,8 +133,8 @@ def searchPathsTool(sub='Bit'): paths.append(lastPathToolBit()) if 'Library' == sub: paths.append(lastPathToolLibrary()) - if 'Template' == sub: - paths.append(lastPathToolTemplate()) + if 'Shape' == sub: + paths.append(lastPathToolShape()) def appendPath(p, sub): if p: @@ -210,8 +210,8 @@ def lastPathToolLibrary(): def setLastPathToolLibrary(path): return preferences().SetString(LastPathToolLibrary, path) -def lastPathToolTemplate(): - return preferences().GetString(LastPathToolTemplate, pathDefaultToolsPath('Template')) -def setLastPathToolTemplate(path): - return preferences().SetString(LastPathToolTemplate, path) +def lastPathToolShape(): + return preferences().GetString(LastPathToolShape, pathDefaultToolsPath('Shape')) +def setLastPathToolShape(path): + return preferences().SetString(LastPathToolShape, path) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index cfe40f387b..2aaa54c0a8 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -82,9 +82,9 @@ def _findTool(path, typ, dbg=False): return searchFor(path, '') -def findTemplate(path): - '''findTemplate(path) ... search for path, full and partially in all known template directories.''' - return _findTool(path, 'Template') +def findShape(path): + '''findShape(path) ... search for path, full and partially in all known shape directories.''' + return _findTool(path, 'Shape') def findBit(path): if path.endswith('.fctb'): @@ -121,14 +121,14 @@ PropertyGroupAttribute = 'Attribute' class ToolBit(object): - def __init__(self, obj, templateFile): - PathLog.track(obj.Label, templateFile) + def __init__(self, obj, shapeFile): + PathLog.track(obj.Label, shapeFile) self.obj = obj - obj.addProperty('App::PropertyFile', 'BitTemplate', 'Base', translate('PathToolBit', 'Template for bit shape')) + obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape')) obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) - if templateFile is not None: - obj.BitTemplate = templateFile + if shapeFile is not None: + obj.BitShape = shapeFile self._setupBitShape(obj) self.onDocumentRestored(obj) @@ -149,7 +149,7 @@ class ToolBit(object): return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupAttribute] def onDocumentRestored(self, obj): - obj.setEditorMode('BitTemplate', 1) + obj.setEditorMode('BitShape', 1) obj.setEditorMode('BitBody', 2) obj.setEditorMode('File', 1) obj.setEditorMode('Shape', 2) @@ -162,7 +162,7 @@ class ToolBit(object): def onChanged(self, obj, prop): PathLog.track(obj.Label, prop) - if prop == 'BitTemplate' and not 'Restore' in obj.State: + if prop == 'BitShape' and not 'Restore' in obj.State: self._setupBitShape(obj) #elif obj.getGroupOfProperty(prop) == PropertyGroupBit: # self._updateBitShape(obj, [prop]) @@ -185,7 +185,7 @@ class ToolBit(object): obj.Shape = Part.Shape() def _loadBitBody(self, obj, path=None): - p = path if path else obj.BitTemplate + p = path if path else obj.BitShape docOpened = False doc = None for d in FreeCAD.listDocuments(): @@ -193,9 +193,9 @@ class ToolBit(object): doc = FreeCAD.getDocument(d) break if doc is None: - p = findTemplate(p) - if not path and p != obj.BitTemplate: - obj.BitTemplate = p + p = findShape(p) + if not path and p != obj.BitShape: + obj.BitShape = p doc = FreeCAD.open(p) docOpened = True return (doc, docOpened) @@ -257,8 +257,8 @@ class ToolBit(object): PathUtil.setProperty(obj, prop, value) def getBitThumbnail(self, obj): - if obj.BitTemplate: - with open(obj.BitTemplate, 'rb') as fd: + if obj.BitShape: + with open(obj.BitShape, 'rb') as fd: zf = zipfile.ZipFile(fd) pf = zf.open('thumbnails/Thumbnail.png', 'r') data = pf.read() @@ -270,7 +270,7 @@ class ToolBit(object): def saveToFile(self, obj, path, setFile=True): try: with open(path, 'w') as fp: - json.dump(self.templateAttrs(obj), fp, indent=' ') + json.dump(self.shapeAttrs(obj), fp, indent=' ') if setFile: obj.File = path return True @@ -278,11 +278,11 @@ class ToolBit(object): PathLog.error("Could not save tool %s to %s (%s)" % (obj.Label, path, e)) raise - def templateAttrs(self, obj): + def shapeAttrs(self, obj): attrs = {} attrs['version'] = 2 # Path.Tool is version 1 attrs['name'] = obj.Label - attrs['template'] = obj.BitTemplate + attrs['shape'] = obj.BitShape params = {} for name in self.propertyNamesBit(obj): params[name] = PathUtil.getPropertyValueString(obj, name) @@ -311,7 +311,7 @@ class AttributePrototype(PathSetupSheetOpPrototype.OpPrototype): class ToolBitFactory(object): def CreateFromAttrs(self, attrs, name='ToolBit'): - obj = Factory.Create(name, attrs['template']) + obj = Factory.Create(name, attrs['shape']) obj.Label = attrs['name'] params = attrs['parameter'] for prop in params: @@ -335,9 +335,9 @@ class ToolBitFactory(object): PathLog.error("%s not a valid tool file (%s)" % (path, e)) raise - def Create(self, name='ToolBit', templateFile=None): + def Create(self, name='ToolBit', shapeFile=None): obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name) - obj.Proxy = ToolBit(obj, templateFile) + obj.Proxy = ToolBit(obj, shapeFile) return obj Factory = ToolBitFactory() diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index d10c66ae8c..141cb898e5 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -56,8 +56,8 @@ class ToolBitEditor(object): parentWidget.layout().addWidget(self.form) self.tool = tool - if not tool.BitTemplate: - self.tool.BitTemplate = 'src/Mod/Path/Tools/Template/endmill-straight.fcstd' + if not tool.BitShape: + self.tool.BitShape = 'src/Mod/Path/Tools/Shape/endmill-straight.fcstd' self.tool.Proxy.loadBitBody(self.tool) self.setupTool(self.tool) self.setupAttributes(self.tool) @@ -147,13 +147,13 @@ class ToolBitEditor(object): def updateUI(self): PathLog.track() self.form.toolName.setText(self.tool.Label) - self.form.templatePath.setText(self.tool.BitTemplate) + self.form.shapePath.setText(self.tool.BitShape) for editor in self.bitEditor: self.bitEditor[editor].updateSpinBox() - def updateTemplate(self): - self.tool.BitTemplate = str(self.form.templatePath.text()) + def updateShape(self): + self.tool.BitShape = str(self.form.shapePath.text()) self.setupTool(self.tool) self.form.toolName.setText(self.tool.Label) @@ -163,7 +163,7 @@ class ToolBitEditor(object): def updateTool(self): PathLog.track() self.tool.Label = str(self.form.toolName.text()) - self.tool.BitTemplate = str(self.form.templatePath.text()) + self.tool.BitShape = str(self.form.shapePath.text()) for editor in self.bitEditor: self.bitEditor[editor].updateProperty() @@ -177,23 +177,23 @@ class ToolBitEditor(object): self.updateUI() self.form.blockSignals(False) - def selectTemplate(self): - path = self.tool.BitTemplate + def selectShape(self): + path = self.tool.BitShape if not path: - path = PathPreferences.lastPathToolTemplate() + path = PathPreferences.lastPathToolShape() foo = QtGui.QFileDialog.getOpenFileName(self.form, - "Path - Tool Template", + "Path - Tool Shape", path, "*.fcstd") if foo and foo[0]: - PathPreferences.setLastPathToolTemplate(os.path.dirname(foo[0])) - self.form.templatePath.setText(foo[0]) - self.updateTemplate() + PathPreferences.setLastPathToolShape(os.path.dirname(foo[0])) + self.form.shapePath.setText(foo[0]) + self.updateShape() def setupUI(self): PathLog.track() self.updateUI() self.form.toolName.editingFinished.connect(self.refresh) - self.form.templatePath.editingFinished.connect(self.updateTemplate) - self.form.templateSet.clicked.connect(self.selectTemplate) + self.form.shapePath.editingFinished.connect(self.updateShape) + self.form.shapeSet.clicked.connect(self.selectShape) diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 9b35fb69ba..f1d6ec9f5f 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -255,11 +255,11 @@ class ToolBitSelector(object): class ToolBitGuiFactory(PathToolBit.ToolBitFactory): - def Create(self, name='ToolBit', templateFile=None): + def Create(self, name='ToolBit', shapeFile=None): '''Create(name = 'ToolBit') ... creates a new tool bit. It is assumed the tool will be edited immediately so the internal bit body is still attached.''' FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit')) - tool = PathToolBit.ToolBitFactory.Create(self, name, templateFile) + tool = PathToolBit.ToolBitFactory.Create(self, name, shapeFile) PathIconViewProvider.Attach(tool.ViewObject, name) FreeCAD.ActiveDocument.commitTransaction() return tool diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index bd976dd404..b5aba5def2 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -147,15 +147,15 @@ class ToolBitLibrary(object): toolName.setData(tool['name'], PySide.QtCore.Qt.EditRole) toolName.setEditable(False) - toolTemplate = PySide.QtGui.QStandardItem() - toolTemplate.setData(os.path.splitext(os.path.basename(tool['template']))[0], PySide.QtCore.Qt.EditRole) - toolTemplate.setEditable(False) + toolShape = PySide.QtGui.QStandardItem() + toolShape.setData(os.path.splitext(os.path.basename(tool['shape']))[0], PySide.QtCore.Qt.EditRole) + toolShape.setEditable(False) toolDiameter = PySide.QtGui.QStandardItem() toolDiameter.setData(tool['parameter']['Diameter'], PySide.QtCore.Qt.EditRole) toolDiameter.setEditable(False) - self.model.appendRow([toolNr, toolName, toolTemplate, toolDiameter]) + self.model.appendRow([toolNr, toolName, toolShape, toolDiameter]) def toolAdd(self): PathLog.track() @@ -276,7 +276,7 @@ class ToolBitLibrary(object): self.updateToolbar() def columnNames(self): - return ['Nr', 'Tool', 'Template', 'Diameter'] + return ['Nr', 'Tool', 'Shape', 'Diameter'] def setupUI(self): PathLog.track('+') diff --git a/src/Mod/Path/PathScripts/PathUtil.py b/src/Mod/Path/PathScripts/PathUtil.py index 6cc131d71a..2169d25840 100644 --- a/src/Mod/Path/PathScripts/PathUtil.py +++ b/src/Mod/Path/PathScripts/PathUtil.py @@ -86,7 +86,7 @@ def isValidBaseObject(obj): # Can't link to anything inside a geo feature group anymore PathLog.debug("%s is inside a geo feature group" % obj.Label) return False - if hasattr(obj, 'BitBody') and hasattr(obj, 'BitTemplate'): + if hasattr(obj, 'BitBody') and hasattr(obj, 'BitShape'): # ToolBit's are not valid base objects return False if obj.TypeId in NotValidBaseTypeIds: diff --git a/src/Mod/Path/Tools/Bit/t1.fctb b/src/Mod/Path/Tools/Bit/t1.fctb index 8a028c0b70..6034b99db6 100644 --- a/src/Mod/Path/Tools/Bit/t1.fctb +++ b/src/Mod/Path/Tools/Bit/t1.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T1", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "1.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t2.fctb b/src/Mod/Path/Tools/Bit/t2.fctb index 72fa57f3a0..c101df5c5f 100644 --- a/src/Mod/Path/Tools/Bit/t2.fctb +++ b/src/Mod/Path/Tools/Bit/t2.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T2", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "2.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t3.fctb b/src/Mod/Path/Tools/Bit/t3.fctb index 8f8821f5f5..3f0fa57bdc 100644 --- a/src/Mod/Path/Tools/Bit/t3.fctb +++ b/src/Mod/Path/Tools/Bit/t3.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T3", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "3.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t4.fctb b/src/Mod/Path/Tools/Bit/t4.fctb index 094814a74c..dca2a58584 100644 --- a/src/Mod/Path/Tools/Bit/t4.fctb +++ b/src/Mod/Path/Tools/Bit/t4.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T4", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "4.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t5.fctb b/src/Mod/Path/Tools/Bit/t5.fctb index a716a91029..be429741e1 100644 --- a/src/Mod/Path/Tools/Bit/t5.fctb +++ b/src/Mod/Path/Tools/Bit/t5.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T5", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "5.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t6.fctb b/src/Mod/Path/Tools/Bit/t6.fctb index 54cc8743b3..e6683365fc 100644 --- a/src/Mod/Path/Tools/Bit/t6.fctb +++ b/src/Mod/Path/Tools/Bit/t6.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T6", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "6.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t7.fctb b/src/Mod/Path/Tools/Bit/t7.fctb index e5df250dd6..7929d4372f 100644 --- a/src/Mod/Path/Tools/Bit/t7.fctb +++ b/src/Mod/Path/Tools/Bit/t7.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T7", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "7.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t8.fctb b/src/Mod/Path/Tools/Bit/t8.fctb index 746a1338a9..14648ba07b 100644 --- a/src/Mod/Path/Tools/Bit/t8.fctb +++ b/src/Mod/Path/Tools/Bit/t8.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T8", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "8.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t9.fctb b/src/Mod/Path/Tools/Bit/t9.fctb index 4f156d972c..9e5f4e8966 100644 --- a/src/Mod/Path/Tools/Bit/t9.fctb +++ b/src/Mod/Path/Tools/Bit/t9.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T9", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "9.000 mm", diff --git a/src/Mod/Path/Tools/README.md b/src/Mod/Path/Tools/README.md index c080052065..69f9e88a99 100644 --- a/src/Mod/Path/Tools/README.md +++ b/src/Mod/Path/Tools/README.md @@ -43,7 +43,7 @@ The following directory structure is used for supplied (shipped with FreeCAD) to Tools + Bit + Library - + Template + + Shape ``` Strictly speaking a user is free to store their tools wherever they want and however they want. By default the file @@ -62,7 +62,7 @@ TechDraw's templates. 1. Save the tool under path/file that makes sense to you -## How to create a new tool bit Template +## How to create a new tool bit Shape A tool bit template represents the physical shape of a tool. It does not completely desribe the bit - for that some additional parameters are needed which will be added when an actual bit is parametrized from the template. @@ -80,7 +80,7 @@ additional parameters are needed which will be added when an actual bit is param 1. Any unnamed constraint will not be editable for a specific tool 1. Once the sketch is fully constrained, close the sketch 1. Rotate the sketch around the z-axis -1. Save the document as a new file in the Template directory +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_ diff --git a/src/Mod/Path/Tools/Template/drill-straight.fcstd b/src/Mod/Path/Tools/Shape/drill-straight.fcstd similarity index 100% rename from src/Mod/Path/Tools/Template/drill-straight.fcstd rename to src/Mod/Path/Tools/Shape/drill-straight.fcstd diff --git a/src/Mod/Path/Tools/Template/endmill-straight.fcstd b/src/Mod/Path/Tools/Shape/endmill-straight.fcstd similarity index 100% rename from src/Mod/Path/Tools/Template/endmill-straight.fcstd rename to src/Mod/Path/Tools/Shape/endmill-straight.fcstd diff --git a/src/Mod/Path/Tools/Template/v-bit.fcstd b/src/Mod/Path/Tools/Shape/v-bit.fcstd similarity index 100% rename from src/Mod/Path/Tools/Template/v-bit.fcstd rename to src/Mod/Path/Tools/Shape/v-bit.fcstd From 9910600799f3a86b84b082fa04b4e7380d9eeef1 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 14:41:45 -0700 Subject: [PATCH 39/52] Fixed some pylint issues and whitelisted some --- .../Path/PathScripts/PathCircularHoleBase.py | 2 +- .../PathScripts/PathDressupHoldingTags.py | 8 +++++++- src/Mod/Path/PathScripts/PathDrillingGui.py | 2 ++ src/Mod/Path/PathScripts/PathMillFace.py | 2 +- src/Mod/Path/PathScripts/PathToolBit.py | 1 + src/Mod/Path/PathScripts/PathToolBitEdit.py | 10 ++++------ src/Mod/Path/PathScripts/PathToolBitGui.py | 20 ++++++++----------- .../Path/PathScripts/PathToolBitLibraryGui.py | 12 ++++++----- .../Path/PathScripts/PathToolController.py | 2 ++ .../Path/PathScripts/PathToolControllerGui.py | 1 - src/Mod/Path/PathTests/TestPathHelix.py | 1 + src/Mod/Path/PathTests/TestPathToolBit.py | 8 ++++---- src/Mod/Path/utils/path-lint.sh | 1 + 13 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBase.py b/src/Mod/Path/PathScripts/PathCircularHoleBase.py index 7983b5efb3..12daca5567 100644 --- a/src/Mod/Path/PathScripts/PathCircularHoleBase.py +++ b/src/Mod/Path/PathScripts/PathCircularHoleBase.py @@ -143,7 +143,7 @@ class ObjectOp(PathOp.ObjectOp): return shape.Curve.Radius * 2 if shape.ShapeType == 'Face': - for i in range(len(shape.Edges)): + for i in range(len(shape.Edges)): if (type(shape.Edges[i].Curve) == Part.Circle and shape.Edges[i].Curve.Radius * 2 < shape.BoundBox.XLength*1.1 and shape.Edges[i].Curve.Radius * 2 > shape.BoundBox.XLength*0.9): diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index 38297e917f..cb24e5e393 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -759,7 +759,13 @@ class ObjectTagDressup: obj.addProperty("App::PropertyIntegerList", "Disabled", "Tag", QtCore.QT_TRANSLATE_NOOP("Path_DressupTag", "IDs of disabled holding tags")) obj.addProperty("App::PropertyInteger", "SegmentationFactor", "Tag", QtCore.QT_TRANSLATE_NOOP("Path_DressupTag", "Factor determining the # of segments used to approximate rounded tags.")) - self.__setstate__(obj) + # for pylint ... + self.obj = obj + self.solids = [] + self.tags = [] + self.pathData = None + self.toolRadius = None + self.mappers = [] obj.Proxy = self obj.Base = base diff --git a/src/Mod/Path/PathScripts/PathDrillingGui.py b/src/Mod/Path/PathScripts/PathDrillingGui.py index 11504c4bc6..6d8d8249a4 100644 --- a/src/Mod/Path/PathScripts/PathDrillingGui.py +++ b/src/Mod/Path/PathScripts/PathDrillingGui.py @@ -51,6 +51,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): '''Controller for the drilling operation's page''' def initPage(self, obj): + # pylint: disable=attribute-defined-outside-init self.peckDepthSpinBox = PathGui.QuantitySpinBox(self.form.peckDepth, obj, 'PeckDepth') self.peckRetractSpinBox = PathGui.QuantitySpinBox(self.form.peckRetractHeight, obj, 'RetractHeight') self.dwellTimeSpinBox = PathGui.QuantitySpinBox(self.form.dwellTime, obj, 'DwellTime') @@ -80,6 +81,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): return FreeCADGui.PySideUic.loadUi(":/panels/PageOpDrillingEdit.ui") def updateQuantitySpinBoxes(self, index = None): + # pylint: disable=unused-argument self.peckDepthSpinBox.updateSpinBox() self.peckRetractSpinBox.updateSpinBox() self.dwellTimeSpinBox.updateSpinBox() diff --git a/src/Mod/Path/PathScripts/PathMillFace.py b/src/Mod/Path/PathScripts/PathMillFace.py index cb6c02f080..3a32be9844 100644 --- a/src/Mod/Path/PathScripts/PathMillFace.py +++ b/src/Mod/Path/PathScripts/PathMillFace.py @@ -131,7 +131,7 @@ class ObjectFace(PathPocketBase.ObjectPocket): else: holes.append((b[0].Shape, wire)) else: - PathLog.error('The base subobject, "{}," is not a face. Ignoring "{}."'.format(sub, sub)) + PathLog.error('The base subobject, "{0}," is not a face. Ignoring "{0}."'.format(sub)) if obj.ExcludeRaisedAreas is True and len(holes) > 0: for shape, wire in holes: diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 2aaa54c0a8..9b26ed849b 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -311,6 +311,7 @@ class AttributePrototype(PathSetupSheetOpPrototype.OpPrototype): class ToolBitFactory(object): def CreateFromAttrs(self, attrs, name='ToolBit'): + # pylint: disable=protected-access obj = Factory.Create(name, attrs['shape']) obj.Label = attrs['name'] params = attrs['parameter'] diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index 141cb898e5..9cddec80bc 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -22,16 +22,13 @@ # * * # *************************************************************************** -import FreeCAD import FreeCADGui -import Path import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog import PathScripts.PathPreferences as PathPreferences import PathScripts.PathSetupSheetGui as PathSetupSheetGui import PathScripts.PathToolBit as PathToolBit -import copy -import math +import os import re from PySide import QtCore, QtGui @@ -122,6 +119,7 @@ class ToolBitEditor(object): self.model.dataChanged.connect(self.updateData) def updateData(self, topLeft, bottomRight): + # pylint: disable=unused-argument if 0 == topLeft.column(): isset = self.model.item(topLeft.row(), 0).checkState() == QtCore.Qt.Checked self.model.item(topLeft.row(), 1).setEnabled(isset) @@ -138,11 +136,10 @@ class ToolBitEditor(object): if enabled and not prop.getValue() is None: prop.setupProperty(self.tool, name, PathToolBit.PropertyGroupAttribute, prop.getValue()) elif hasattr(self.tool, name): - self.obj.removeProperty(name) + self.tool.removeProperty(name) def reject(self): self.tool.Proxy.unloadBitBody(self.tool) - pass def updateUI(self): PathLog.track() @@ -161,6 +158,7 @@ class ToolBitEditor(object): self.bitEditor[editor].updateSpinBox() def updateTool(self): + # pylint: disable=protected-access PathLog.track() self.tool.Label = str(self.form.toolName.text()) self.tool.BitShape = str(self.form.shapePath.text()) diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index f1d6ec9f5f..abab173c7c 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -24,13 +24,11 @@ import FreeCAD import FreeCADGui -import PathScripts.PathGui as PathGui import PathScripts.PathIconViewProvider as PathIconViewProvider import PathScripts.PathLog as PathLog import PathScripts.PathPreferences as PathPreferences import PathScripts.PathToolBit as PathToolBit import PathScripts.PathToolBitEdit as PathToolBitEdit -import PathScripts.PathUtil as PathUtil import os from PySide import QtCore, QtGui @@ -53,6 +51,7 @@ class ViewProvider(object): def __init__(self, vobj, name): PathLog.track(name, vobj.Object) + self.panel = None self.icon = name self.obj = vobj.Object self.vobj = vobj @@ -84,10 +83,10 @@ class ViewProvider(object): def _openTaskPanel(self, vobj, deleteOnReject): PathLog.track() - self.taskPanel = TaskPanel(vobj, deleteOnReject) + self.panel = TaskPanel(vobj, deleteOnReject) FreeCADGui.Control.closeDialog() - FreeCADGui.Control.showDialog(self.taskPanel) - self.taskPanel.setupUi() + FreeCADGui.Control.showDialog(self.panel) + self.panel.setupUi() def setCreate(self, vobj): PathLog.track() @@ -101,7 +100,7 @@ class ViewProvider(object): def unsetEdit(self, vobj, mode): # pylint: disable=unused-argument FreeCADGui.Control.closeDialog() - self.taskPanel = None + self.panel = None return def claimChildren(self): @@ -143,9 +142,6 @@ class TaskPanel: FreeCADGui.Control.closeDialog() FreeCAD.ActiveDocument.recompute() - def getFields(self): - self.editor.getFields() - def updateUI(self): self.editor.updateUI() @@ -153,9 +149,6 @@ class TaskPanel: self.editor.updateTool() FreeCAD.ActiveDocument.recompute() - def setFields(self): - self.editor.setFields() - def setupUi(self): self.editor.setupUI() @@ -164,6 +157,9 @@ class ToolBitSelector(object): ToolRole = QtCore.Qt.UserRole + 1 def __init__(self): + self.buttons = None + self.editor = None + self.dialog = None self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitSelector.ui') self.setupUI() diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index b5aba5def2..92f2d381a9 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -32,7 +32,7 @@ import PySide import json import os import traceback -import uuid +import uuid as UUID PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) PathLog.trackModule(PathLog.thisModule()) @@ -70,7 +70,6 @@ class _TableView(PySide.QtGui.QTableView): def _copyTool(self, uuid_, dstRow): model = self.model() - items = [] model.insertRow(dstRow) srcRow = self._rowWithUuid(uuid_) for col in range(model.columnCount()): @@ -81,7 +80,7 @@ class _TableView(PySide.QtGui.QTableView): model.setData(model.index(dstRow, col), srcItem.data(_PathRole), _PathRole) # Even a clone of a tool gets its own uuid so it can be identified when # rearranging the order or inserting/deleting rows - model.setData(model.index(dstRow, col), uuid.uuid4(), _UuidRole) + model.setData(model.index(dstRow, col), UUID.uuid4(), _UuidRole) else: model.item(dstRow, col).setEditable(False) @@ -96,6 +95,7 @@ class _TableView(PySide.QtGui.QTableView): stream = PySide.QtCore.QDataStream(data) srcRows = [] while not stream.atEnd(): + # pylint: disable=unused-variable row = stream.readInt32() srcRows.append(row) col = stream.readInt32() @@ -141,7 +141,7 @@ class ToolBitLibrary(object): toolNr = PySide.QtGui.QStandardItem() toolNr.setData(nr, PySide.QtCore.Qt.EditRole) toolNr.setData(path, _PathRole) - toolNr.setData(uuid.uuid4(), _UuidRole) + toolNr.setData(UUID.uuid4(), _UuidRole) toolName = PySide.QtGui.QStandardItem() toolName.setData(tool['name'], PySide.QtCore.Qt.EditRole) @@ -159,6 +159,7 @@ class ToolBitLibrary(object): def toolAdd(self): PathLog.track() + # pylint: disable=broad-except try: nr = 0 for row in range(self.model.rowCount()): @@ -170,7 +171,7 @@ class ToolBitLibrary(object): tool = PathToolBit.Declaration(foo) self._toolAdd(nr + i, tool, foo) self.toolTableView.resizeColumnsToContents() - except: + except Exception: PathLog.error('something happened') PathLog.error(traceback.print_exc()) @@ -198,6 +199,7 @@ class ToolBitLibrary(object): self.model.setData(self.model.index(row, 0), row + 1, PySide.QtCore.Qt.EditRole) def toolSelect(self, selected, deselected): + # pylint: disable=unused-argument self.form.toolDelete.setEnabled(len(self.toolTableView.selectedIndexes()) > 0) def open(self, path=None, dialog=False): diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 57fbf2e87c..96cc49e8e3 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -82,6 +82,7 @@ class ToolController: obj.setEditorMode('Placement', 2) def onDelete(self, obj, arg2=None): + # pylint: disable=unused-argument if not self.usesLegacyTool(obj): if len(obj.Tool.InList) == 1: obj.Document.removeObject(obj.Tool.Name) @@ -214,6 +215,7 @@ def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=Tr return obj def FromTemplate(template, assignViewProvider=True): + # pylint: disable=unused-argument PathLog.track() name = template.get(ToolControllerTemplate.Name, ToolControllerTemplate.Label) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 3a82f0fc41..3a05fbe301 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -28,7 +28,6 @@ import Part import PathScripts import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog -import PathScripts.PathToolBit as PathToolBit import PathScripts.PathToolBitGui as PathToolBitGui import PathScripts.PathToolEdit as PathToolEdit import PathScripts.PathUtil as PathUtil diff --git a/src/Mod/Path/PathTests/TestPathHelix.py b/src/Mod/Path/PathTests/TestPathHelix.py index de6c5527a7..9f72ab4cc2 100644 --- a/src/Mod/Path/PathTests/TestPathHelix.py +++ b/src/Mod/Path/PathTests/TestPathHelix.py @@ -36,6 +36,7 @@ PathLog.trackModule(PathLog.thisModule()) class TestPathHelix(PathTestUtils.PathTestBase): def setUp(self): + self.clone = None self.doc = FreeCAD.open(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_holes00.fcstd') self.job = PathJob.Create('Job', [self.doc.Body]) diff --git a/src/Mod/Path/PathTests/TestPathToolBit.py b/src/Mod/Path/PathTests/TestPathToolBit.py index 1b1f2ba714..b7e27c1842 100644 --- a/src/Mod/Path/PathTests/TestPathToolBit.py +++ b/src/Mod/Path/PathTests/TestPathToolBit.py @@ -29,16 +29,16 @@ import PathTests.PathTestUtils as PathTestUtils class TestPathToolBit(PathTestUtils.PathTestBase): def test00(self): - '''Find a tool template from file name''' + '''Find a tool shapee from file name''' - path = PathToolBit.findTemplate('endmill-straight.fcstd') + path = PathToolBit.findShape('endmill-straight.fcstd') self.assertIsNot(path, None) self.assertNotEqual(path, 'endmill-straight.fcstd') def test01(self): - '''Find a tool template from an invalid absolute path.''' + '''Find a tool shapee from an invalid absolute path.''' - path = PathToolBit.findTemplate('/this/is/unlikely/a/valid/path/v-bit.fcstd') + path = PathToolBit.findShape('/this/is/unlikely/a/valid/path/v-bit.fcstd') self.assertIsNot(path, None) self.assertNotEqual(path, '/this/is/unlikely/a/valid/path/v-bit.fcstd') diff --git a/src/Mod/Path/utils/path-lint.sh b/src/Mod/Path/utils/path-lint.sh index f0106ca82d..def379183c 100755 --- a/src/Mod/Path/utils/path-lint.sh +++ b/src/Mod/Path/utils/path-lint.sh @@ -56,6 +56,7 @@ EXTERNAL_MODULES+=' Path' EXTERNAL_MODULES+=' PySide' EXTERNAL_MODULES+=' PySide.QtCore' EXTERNAL_MODULES+=' PySide.QtGui' +EXTERNAL_MODULES+=' Sketcher' EXTERNAL_MODULES+=' TechDraw' EXTERNAL_MODULES+=' TestSketcherApp' EXTERNAL_MODULES+=' area' From e4d62af5e7e101042d516ec3aed161bcaf3627d3 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 22:23:18 -0700 Subject: [PATCH 40/52] Updated tools --- src/Mod/Path/Tools/Bit/t1.fctb | 3 ++- src/Mod/Path/Tools/Bit/t2.fctb | 3 ++- src/Mod/Path/Tools/Bit/t3.fctb | 3 ++- src/Mod/Path/Tools/Bit/t4.fctb | 3 ++- src/Mod/Path/Tools/Bit/t5.fctb | 3 ++- src/Mod/Path/Tools/Bit/t6.fctb | 3 ++- src/Mod/Path/Tools/Bit/t7.fctb | 3 ++- src/Mod/Path/Tools/Bit/t8.fctb | 3 ++- src/Mod/Path/Tools/Bit/t9.fctb | 3 ++- 9 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Mod/Path/Tools/Bit/t1.fctb b/src/Mod/Path/Tools/Bit/t1.fctb index 6034b99db6..27f271ff99 100644 --- a/src/Mod/Path/Tools/Bit/t1.fctb +++ b/src/Mod/Path/Tools/Bit/t1.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T1", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "1.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t2.fctb b/src/Mod/Path/Tools/Bit/t2.fctb index c101df5c5f..931c833964 100644 --- a/src/Mod/Path/Tools/Bit/t2.fctb +++ b/src/Mod/Path/Tools/Bit/t2.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T2", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "2.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t3.fctb b/src/Mod/Path/Tools/Bit/t3.fctb index 3f0fa57bdc..b4e204e13f 100644 --- a/src/Mod/Path/Tools/Bit/t3.fctb +++ b/src/Mod/Path/Tools/Bit/t3.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T3", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "3.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t4.fctb b/src/Mod/Path/Tools/Bit/t4.fctb index dca2a58584..dc3042205f 100644 --- a/src/Mod/Path/Tools/Bit/t4.fctb +++ b/src/Mod/Path/Tools/Bit/t4.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T4", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "4.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t5.fctb b/src/Mod/Path/Tools/Bit/t5.fctb index be429741e1..9db3300c7a 100644 --- a/src/Mod/Path/Tools/Bit/t5.fctb +++ b/src/Mod/Path/Tools/Bit/t5.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T5", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "5.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t6.fctb b/src/Mod/Path/Tools/Bit/t6.fctb index e6683365fc..b1c2f58c61 100644 --- a/src/Mod/Path/Tools/Bit/t6.fctb +++ b/src/Mod/Path/Tools/Bit/t6.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T6", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "6.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t7.fctb b/src/Mod/Path/Tools/Bit/t7.fctb index 7929d4372f..10a3fc10a6 100644 --- a/src/Mod/Path/Tools/Bit/t7.fctb +++ b/src/Mod/Path/Tools/Bit/t7.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T7", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "7.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t8.fctb b/src/Mod/Path/Tools/Bit/t8.fctb index 14648ba07b..995b5012e2 100644 --- a/src/Mod/Path/Tools/Bit/t8.fctb +++ b/src/Mod/Path/Tools/Bit/t8.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T8", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "8.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t9.fctb b/src/Mod/Path/Tools/Bit/t9.fctb index 9e5f4e8966..ab49b17dbc 100644 --- a/src/Mod/Path/Tools/Bit/t9.fctb +++ b/src/Mod/Path/Tools/Bit/t9.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T9", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "9.000 mm", From a9e6c21486436d6a013547a39b2d3d20a49a0a4d Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 22:40:20 -0700 Subject: [PATCH 41/52] Use findPath to look for an icon and fail silently if it cannot be found. --- src/Mod/Path/PathScripts/PathToolBit.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 9b26ed849b..c58f11514a 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -258,14 +258,15 @@ class ToolBit(object): def getBitThumbnail(self, obj): if obj.BitShape: - with open(obj.BitShape, 'rb') as fd: - zf = zipfile.ZipFile(fd) - pf = zf.open('thumbnails/Thumbnail.png', 'r') - data = pf.read() - pf.close() - return data - else: - return None + path = findShape(obj.BitShape) + if path: + with open(path, 'rb') as fd: + zf = zipfile.ZipFile(fd) + pf = zf.open('thumbnails/Thumbnail.png', 'r') + data = pf.read() + pf.close() + return data + return None def saveToFile(self, obj, path, setFile=True): try: From 119730e4a8ba88602e547e1e28d68077359cb6c7 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 3 Nov 2019 18:04:16 -0800 Subject: [PATCH 42/52] Reduced log level for tool bits --- src/Mod/Path/PathScripts/PathToolBit.py | 4 ++-- src/Mod/Path/PathScripts/PathToolBitEdit.py | 4 ++-- src/Mod/Path/PathScripts/PathToolBitGui.py | 4 ++-- src/Mod/Path/PathScripts/PathToolBitLibraryGui.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index c58f11514a..6dec07e999 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -41,8 +41,8 @@ __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" __doc__ = "Class to deal with and represent a tool bit." -PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) -PathLog.trackModule() +#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +#PathLog.trackModule() def translate(context, text, disambig=None): return PySide.QtCore.QCoreApplication.translate(context, text, disambig) diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index 9cddec80bc..2baba5ba26 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -33,8 +33,8 @@ import re from PySide import QtCore, QtGui -PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) -PathLog.trackModule(PathLog.thisModule()) +#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) # Qt translation handling def translate(context, text, disambig=None): diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index abab173c7c..5dc7ba98cd 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -42,8 +42,8 @@ __doc__ = "Task panel editor for a ToolBit" def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) -PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) -PathLog.trackModule(PathLog.thisModule()) +#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) class ViewProvider(object): '''ViewProvider for a ToolBit. diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index 92f2d381a9..68e159eff0 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -34,8 +34,8 @@ import os import traceback import uuid as UUID -PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) -PathLog.trackModule(PathLog.thisModule()) +#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) _UuidRole = PySide.QtCore.Qt.UserRole + 1 _PathRole = PySide.QtCore.Qt.UserRole + 2 From 0a84f3168eb20406b4ea4ee76283fb99d7ba0b84 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sat, 9 Nov 2019 14:57:01 -0800 Subject: [PATCH 43/52] Added tooltips to dialogs --- .../Gui/Resources/panels/ToolBitEditor.ui | 12 ++++++++- .../Resources/panels/ToolBitLibraryEdit.ui | 27 +++++++++++++++++++ .../Gui/Resources/panels/ToolBitSelector.ui | 9 +++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui index 29b490323b..1ffb408a8a 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui @@ -47,6 +47,9 @@ + + <html><head/><body><p>Display name of the Tool Bit (initial value taken from the shape file).</p></body></html> + 50 @@ -78,10 +81,17 @@ 0 - + + + <html><head/><body><p>The file which defines the type and shape of the Tool Bit.</p></body></html> + + + + <html><head/><body><p>Change file defining type and shape of Tool Bit.</p></body></html> + ... diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui index 48a32c705f..4663aa712f 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui @@ -31,6 +31,9 @@ + + <html><head/><body><p>Create a new library with an empty list of Tool Bits.</p></body></html> + ... @@ -42,6 +45,9 @@ + + <html><head/><body><p>Open an existing Tool Bit library.</p></body></html> + ... @@ -53,6 +59,9 @@ + + <html><head/><body><p>Save Tool Bit library.</p></body></html> + ... @@ -64,6 +73,9 @@ + + <html><head/><body><p>Save Tool Bit library under new name.</p></body></html> + ... @@ -88,6 +100,9 @@ + + <html><head/><body><p>Edit Tool Bit library editor settings.</p></body></html> + ... @@ -114,6 +129,9 @@ true + + <html><head/><body><p>Table of Tool Bits of the library.</p></body></html> + true @@ -151,6 +169,9 @@ + + <html><head/><body><p>Add another Tool Bit to this library.</p><p><br/></p></body></html> + Add ... @@ -162,6 +183,9 @@ + + <html><head/><body><p>Delete selected Tool Bit(s) from the library.</p><p><br/></p></body></html> + Delete @@ -173,6 +197,9 @@ + + <html><head/><body><p>Assigne numbers to each Tool Bit according to its current position in the library. The first Tool Bit is assigned the ID 1.</p></body></html> + Enumerate diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui index 7013e8bf2f..4dde97be5a 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui @@ -19,6 +19,9 @@ + + <html><head/><body><p>Available Tool Bits to choose from.</p></body></html> + QAbstractItemView::NoEditTriggers @@ -29,6 +32,9 @@ + + <html><head/><body><p>Load an existing Tool Bit from a file.</p></body></html> + Load... @@ -36,6 +42,9 @@ + + <html><head/><body><p>Create a new Tool Bit based on an existing shape.</p></body></html> + New From a2935d5ff078bb6e27cb7c81f91c15657869aa33 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sat, 9 Nov 2019 17:09:30 -0800 Subject: [PATCH 44/52] Added preference to choose between legacy tools and new tool bits. --- .../Path/Gui/Resources/preferences/PathJob.ui | 55 +++++++++++++++++-- src/Mod/Path/PathScripts/PathPreferences.py | 16 +++++- .../PathScripts/PathPreferencesPathJob.py | 9 +++ .../Path/PathScripts/PathToolBitLibraryCmd.py | 11 +++- .../Path/PathScripts/PathToolController.py | 20 ++++--- .../Path/PathScripts/PathToolLibraryEditor.py | 17 +++--- 6 files changed, 105 insertions(+), 23 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/preferences/PathJob.ui b/src/Mod/Path/Gui/Resources/preferences/PathJob.ui index 2e84541bbc..7d88c71040 100644 --- a/src/Mod/Path/Gui/Resources/preferences/PathJob.ui +++ b/src/Mod/Path/Gui/Resources/preferences/PathJob.ui @@ -24,8 +24,8 @@ 0 0 - 422 - 558 + 467 + 448 @@ -142,8 +142,8 @@ 0 0 - 406 - 360 + 665 + 449 @@ -348,8 +348,8 @@ 0 0 - 422 - 558 + 431 + 718 @@ -620,6 +620,49 @@ + + + Tools + + + + + + <html><head/><body><p>Legacy Tools have no accurate shape representation and are stored in the user preferences of FreeCAD.</p></body></html> + + + Use Legacy Tools + + + + + + + <html><head/><body><p>References to Tool Bits and their shapes can either be stored with an absolute path or with a relative path to the search path.</p><p><br/></p><p>Generally it is recommended to use relative paths due to their flexibility and robustness to layout changes.</p><p><br/></p><p>Should multiple tools or tool shapes with the same name exist in different directories it can be required to use absolute paths. </p></body></html> + + + Store Relative Paths + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py index f03141ef86..37e16d98ab 100644 --- a/src/Mod/Path/PathScripts/PathPreferences.py +++ b/src/Mod/Path/PathScripts/PathPreferences.py @@ -43,7 +43,10 @@ PostProcessorOutputPolicy = "PostProcessorOutputPolicy" LastPathToolBit = "LastPathToolBit" LastPathToolLibrary = "LastPathToolLibrary" -LastPathToolShape = "LastPathToolShape" +LastPathToolShape = "LastPathToolShape" + +UseLegacyTools = "UseLegacyTools" +UseRelativeToolPaths = "UseRelativeToolPaths" # Linear tolerance to use when generating Paths, eg when tessellating geometry GeometryTolerance = "GeometryTolerance" @@ -146,6 +149,17 @@ def searchPathsTool(sub='Bit'): appendPath(os.path.join(FreeCAD.getHomePath(), "Mod/Path/"), sub) return paths +def toolsUseLegacyTools(): + return preferences().GetBool(UseLegacyTools, True) + +def toolsStoreRelativePaths(): + return preferences().GetBool(UseRelativeToolPaths, True) + +def setToolsSettings(legacy, relative): + pref = preferences() + pref.SetBool(UseLegacyTools, legacy) + pref.SetBool(UseRelativeToolPaths, relative) + def defaultJobTemplate(): template = preferences().GetString(DefaultJobTemplate) if 'xml' not in template: diff --git a/src/Mod/Path/PathScripts/PathPreferencesPathJob.py b/src/Mod/Path/PathScripts/PathPreferencesPathJob.py index 1c5efdf1bf..525ba9c6e1 100644 --- a/src/Mod/Path/PathScripts/PathPreferencesPathJob.py +++ b/src/Mod/Path/PathScripts/PathPreferencesPathJob.py @@ -71,6 +71,7 @@ class JobPreferencesPage: policy = str(self.form.cboOutputPolicy.currentText()) PathPreferences.setOutputFileDefaults(path, policy) self.saveStockSettings() + self.saveToolsSettings() def saveStockSettings(self): if self.form.stockGroup.isChecked(): @@ -107,6 +108,9 @@ class JobPreferencesPage: else: PathPreferences.setDefaultStockTemplate('') + def saveToolsSettings(self): + PathPreferences.setToolsSettings(self.form.toolsUseLegacy.isChecked(), self.form.toolsRelativePaths.isChecked()) + def selectComboEntry(self, widget, text): index = widget.findText(text, QtCore.Qt.MatchFixedString) if index >= 0: @@ -167,6 +171,7 @@ class JobPreferencesPage: self.form.tbOutputFile.clicked.connect(self.browseOutputFile) self.loadStockSettings() + self.loadToolSettings() def loadStockSettings(self): stock = PathPreferences.defaultStockTemplate() @@ -244,6 +249,10 @@ class JobPreferencesPage: self.form.stockCreateBox.hide() self.form.stockCreateCylinder.hide() + def loadToolSettings(self): + self.form.toolsUseLegacy.setChecked(PathPreferences.toolsUseLegacyTools()) + self.form.toolsRelativePaths.setChecked(PathPreferences.toolsStoreRelativePaths()) + def getPostProcessor(self, name): if not name in self.processor.keys(): processor = PostProcessor.load(name) diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py b/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py index 0583f77db5..ae5573013b 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py @@ -74,15 +74,22 @@ class CommandToolBitLibraryLoad: return not self.selectedJob() is None def Activated(self): + job = self.selectedJob() + self.Execute(job) + + @classmethod + def Execute(cls, job): import PathScripts.PathToolBitLibraryGui as PathToolBitLibraryGui import PathScripts.PathToolControllerGui as PathToolControllerGui - job = self.selectedJob() + library = PathToolBitLibraryGui.ToolBitLibrary() - if 1 == library.open(dialog=True): + if 1 == library.open(dialog=True) and job: for nr, tool in library.selectedOrAllTools(): tc = PathToolControllerGui.Create("TC: {}".format(tool.Label), tool, nr) job.Proxy.addToolController(tc) FreeCAD.ActiveDocument.recompute() + return True + return False if FreeCAD.GuiUp: FreeCADGui.addCommand('Path_ToolBitLibraryOpen', CommandToolBitLibraryOpen()) diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 96cc49e8e3..5cf6eead07 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -26,6 +26,7 @@ import FreeCAD import Path import PathScripts.PathLog as PathLog +import PathScripts.PathPreferences as PathPreferences import PathScripts.PathToolBit as PathToolBit from PySide import QtCore @@ -195,20 +196,25 @@ class ToolController: def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=True): PathLog.track(tool, toolNumber) + legacyTool = PathPreferences.toolsUseLegacyTools() if tool is None else isinstance(tool, Path.Tool) + obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) obj.Label = name - obj.Proxy = ToolController(obj, tool is None or isinstance(tool, Path.Tool)) + obj.Proxy = ToolController(obj, legacyTool) if FreeCAD.GuiUp and assignViewProvider: ViewProvider(obj.ViewObject) if tool is None: - tool = Path.Tool() - tool.Diameter = 5.0 - tool.Name = "Default Tool" - tool.CuttingEdgeHeight = 15.0 - tool.ToolType = "EndMill" - tool.Material = "HighSpeedSteel" + if legacyTool: + tool = Path.Tool() + tool.Diameter = 5.0 + tool.Name = "Default Tool" + tool.CuttingEdgeHeight = 15.0 + tool.ToolType = "EndMill" + tool.Material = "HighSpeedSteel" + else: + tool = PathToolBit.Factory.Create() obj.Tool = tool obj.ToolNumber = toolNumber diff --git a/src/Mod/Path/PathScripts/PathToolLibraryEditor.py b/src/Mod/Path/PathScripts/PathToolLibraryEditor.py index 9e867f4ce6..367955a288 100644 --- a/src/Mod/Path/PathScripts/PathToolLibraryEditor.py +++ b/src/Mod/Path/PathScripts/PathToolLibraryEditor.py @@ -29,6 +29,8 @@ import FreeCADGui import Path import PathScripts import PathScripts.PathLog as PathLog +import PathScripts.PathPreferences as PathPreferences +import PathScripts.PathToolBitLibraryCmd as PathToolBitLibraryCmd import PathScripts.PathToolEdit as PathToolEdit import PathScripts.PathUtils as PathUtils import PathScripts.PathToolLibraryManager as ToolLibraryManager @@ -439,12 +441,14 @@ class CommandToolLibraryEdit(): pass def edit(self, job=None, cb=None): - editor = EditorPanel(job, cb) - editor.setupUi() - - r = editor.form.exec_() - if r: - pass + if PathPreferences.toolsUseLegacyTools(): + editor = EditorPanel(job, cb) + editor.setupUi() + editor.form.exec_() + else: + if PathToolBitLibraryCmd.CommandToolBitLibraryLoad.Execute(job): + if cb: + cb() def GetResources(self): return {'Pixmap' : 'Path-ToolTable', @@ -456,7 +460,6 @@ class CommandToolLibraryEdit(): return not FreeCAD.ActiveDocument is None def Activated(self): - self.edit() if FreeCAD.GuiUp: From c6f14b98366b5fc3b62bfe2ecc5528ab22ca8da3 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sat, 9 Nov 2019 19:57:58 -0800 Subject: [PATCH 45/52] Added support for relative/absolute path mgmt for stored files. --- .../Path/Gui/Resources/preferences/PathJob.ui | 17 +++++---- src/Mod/Path/PathScripts/PathPreferences.py | 8 ++--- .../PathScripts/PathPreferencesPathJob.py | 4 +-- src/Mod/Path/PathScripts/PathToolBit.py | 36 ++++++++++++++++--- .../Path/PathScripts/PathToolBitLibraryGui.py | 5 ++- src/Mod/Path/PathTests/TestPathToolBit.py | 15 ++++++++ src/Mod/Path/Tools/Bit/t1.fctb | 10 +++--- src/Mod/Path/Tools/Bit/t2.fctb | 2 +- src/Mod/Path/Tools/Bit/t3.fctb | 10 +++--- src/Mod/Path/Tools/Bit/t4.fctb | 2 +- src/Mod/Path/Tools/Bit/t5.fctb | 2 +- src/Mod/Path/Tools/Bit/t6.fctb | 2 +- src/Mod/Path/Tools/Bit/t7.fctb | 2 +- src/Mod/Path/Tools/Bit/t8.fctb | 2 +- src/Mod/Path/Tools/Bit/t9.fctb | 2 +- src/Mod/Path/Tools/Library/endmills.fctl | 20 +++++------ 16 files changed, 94 insertions(+), 45 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/preferences/PathJob.ui b/src/Mod/Path/Gui/Resources/preferences/PathJob.ui index 7d88c71040..ea59592362 100644 --- a/src/Mod/Path/Gui/Resources/preferences/PathJob.ui +++ b/src/Mod/Path/Gui/Resources/preferences/PathJob.ui @@ -621,6 +621,14 @@ + + + 0 + 0 + 412 + 461 + + Tools @@ -636,15 +644,12 @@ - + - <html><head/><body><p>References to Tool Bits and their shapes can either be stored with an absolute path or with a relative path to the search path.</p><p><br/></p><p>Generally it is recommended to use relative paths due to their flexibility and robustness to layout changes.</p><p><br/></p><p>Should multiple tools or tool shapes with the same name exist in different directories it can be required to use absolute paths. </p></body></html> + <html><head/><body><p>References to Tool Bits and their shapes can either be stored with an absolute path or with a relative path to the search path.</p><p>Generally it is recommended to use relative paths due to their flexibility and robustness to layout changes.</p><p>Should multiple tools or tool shapes with the same name exist in different directories it can be required to use absolute paths. </p></body></html> - Store Relative Paths - - - true + Store Absolute Paths diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py index 37e16d98ab..2fc5bf261b 100644 --- a/src/Mod/Path/PathScripts/PathPreferences.py +++ b/src/Mod/Path/PathScripts/PathPreferences.py @@ -46,7 +46,7 @@ LastPathToolLibrary = "LastPathToolLibrary" LastPathToolShape = "LastPathToolShape" UseLegacyTools = "UseLegacyTools" -UseRelativeToolPaths = "UseRelativeToolPaths" +UseAbsoluteToolPaths = "UseAbsoluteToolPaths" # Linear tolerance to use when generating Paths, eg when tessellating geometry GeometryTolerance = "GeometryTolerance" @@ -152,13 +152,13 @@ def searchPathsTool(sub='Bit'): def toolsUseLegacyTools(): return preferences().GetBool(UseLegacyTools, True) -def toolsStoreRelativePaths(): - return preferences().GetBool(UseRelativeToolPaths, True) +def toolsStoreAbsolutePaths(): + return preferences().GetBool(UseAbsoluteToolPaths, False) def setToolsSettings(legacy, relative): pref = preferences() pref.SetBool(UseLegacyTools, legacy) - pref.SetBool(UseRelativeToolPaths, relative) + pref.SetBool(UseAbsoluteToolPaths, relative) def defaultJobTemplate(): template = preferences().GetString(DefaultJobTemplate) diff --git a/src/Mod/Path/PathScripts/PathPreferencesPathJob.py b/src/Mod/Path/PathScripts/PathPreferencesPathJob.py index 525ba9c6e1..0782efd67b 100644 --- a/src/Mod/Path/PathScripts/PathPreferencesPathJob.py +++ b/src/Mod/Path/PathScripts/PathPreferencesPathJob.py @@ -109,7 +109,7 @@ class JobPreferencesPage: PathPreferences.setDefaultStockTemplate('') def saveToolsSettings(self): - PathPreferences.setToolsSettings(self.form.toolsUseLegacy.isChecked(), self.form.toolsRelativePaths.isChecked()) + PathPreferences.setToolsSettings(self.form.toolsUseLegacy.isChecked(), self.form.toolsAbsolutePaths.isChecked()) def selectComboEntry(self, widget, text): index = widget.findText(text, QtCore.Qt.MatchFixedString) @@ -251,7 +251,7 @@ class JobPreferencesPage: def loadToolSettings(self): self.form.toolsUseLegacy.setChecked(PathPreferences.toolsUseLegacyTools()) - self.form.toolsRelativePaths.setChecked(PathPreferences.toolsStoreRelativePaths()) + self.form.toolsAbsolutePaths.setChecked(PathPreferences.toolsStoreAbsolutePaths()) def getPostProcessor(self, name): if not name in self.processor.keys(): diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 6dec07e999..b3c50c5484 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -96,6 +96,26 @@ def findLibrary(path, dbg=False): return _findTool(path, 'Library', dbg) return _findTool("{}.fctl".format(path), 'Library', dbg) +def _findRelativePath(path, typ): + relative = path + for p in PathPreferences.searchPathsTool(typ): + if path.startswith(p): + p = path[len(p):] + if '/' == p[0]: + p = p[1:] + if len(p) < len(relative): + relative = p + return relative + +def findRelativePathShape(path): + return _findRelativePath(path, 'Shape') + +def findRelativePathTool(path): + return _findRelativePath(path, 'Bit') + +def findRelativePathLibrary(path): + return _findRelativePath(path, 'Library') + def updateConstraint(sketch, name, value): for i, constraint in enumerate(sketch.Constraints): if constraint.Name.split(';')[0] == name: @@ -116,6 +136,7 @@ def updateConstraint(sketch, name, value): PathLog.track(name, constraint.Type, 'unchanged') break + PropertyGroupBit = 'Bit' PropertyGroupAttribute = 'Attribute' @@ -124,9 +145,9 @@ class ToolBit(object): def __init__(self, obj, shapeFile): PathLog.track(obj.Label, shapeFile) self.obj = obj - obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape')) - obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) - obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) + obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape')) + obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) + obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) if shapeFile is not None: obj.BitShape = shapeFile self._setupBitShape(obj) @@ -283,7 +304,10 @@ class ToolBit(object): attrs = {} attrs['version'] = 2 # Path.Tool is version 1 attrs['name'] = obj.Label - attrs['shape'] = obj.BitShape + if PathPreferences.toolsStoreAbsolutePaths(): + attrs['shape'] = obj.BitShape + else: + attrs['shape'] = findRelativePathShape(obj.BitShape) params = {} for name in self.propertyNamesBit(obj): params[name] = PathUtil.getPropertyValueString(obj, name) @@ -332,7 +356,9 @@ class ToolBitFactory(object): def CreateFrom(self, path, name='ToolBit'): try: data = Declaration(path) - return Factory.CreateFromAttrs(data, name) + bit = Factory.CreateFromAttrs(data, name) + bit.File = path + return bit except (OSError, IOError) as e: PathLog.error("%s not a valid tool file (%s)" % (path, e)) raise diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index 68e159eff0..17b12638a2 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -263,7 +263,10 @@ class ToolBitLibrary(object): for row in range(self.model.rowCount()): toolNr = self.model.data(self.model.index(row, 0), PySide.QtCore.Qt.EditRole) toolPath = self.model.data(self.model.index(row, 0), _PathRole) - tools.append({'nr': toolNr, 'path': toolPath}) + if PathPreferences.toolsStoreAbsolutePaths(): + tools.append({'nr': toolNr, 'path': toolPath}) + else: + tools.append({'nr': toolNr, 'path': PathToolBit.findRelativePathTool(toolPath)}) with open(self.path, 'w') as fp: json.dump(library, fp, sort_keys=True, indent=2) diff --git a/src/Mod/Path/PathTests/TestPathToolBit.py b/src/Mod/Path/PathTests/TestPathToolBit.py index b7e27c1842..2eb2bbfa4b 100644 --- a/src/Mod/Path/PathTests/TestPathToolBit.py +++ b/src/Mod/Path/PathTests/TestPathToolBit.py @@ -43,3 +43,18 @@ class TestPathToolBit(PathTestUtils.PathTestBase): self.assertNotEqual(path, '/this/is/unlikely/a/valid/path/v-bit.fcstd') + def test10(self): + '''find the relative path of a tool bit''' + shape = 'endmill-straight.fcstd' + path = PathToolBit.findShape(shape) + self.assertIsNot(path, None) + self.assertGreater(len(path), len(shape)) + rel = PathToolBit.findRelativePathShape(path) + self.assertEqual(rel, shape) + + def test11(self): + '''store full path if relative path isn't found''' + path = '/this/is/unlikely/a/valid/path/v-bit.fcstd' + rel = PathToolBit.findRelativePathShape(path) + self.assertEqual(rel, path) + diff --git a/src/Mod/Path/Tools/Bit/t1.fctb b/src/Mod/Path/Tools/Bit/t1.fctb index 27f271ff99..0fe10f2e03 100644 --- a/src/Mod/Path/Tools/Bit/t1.fctb +++ b/src/Mod/Path/Tools/Bit/t1.fctb @@ -1,12 +1,12 @@ { - "version": 1, + "version": 2, "name": "T1", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", - "attribute": {}, + "shape": "endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", - "Diameter": "1.000 mm", + "Diameter": "5.000 mm", "Length": "50.000 mm", "ShankDiameter": "3.000 mm" - } + }, + "attribute": {} } diff --git a/src/Mod/Path/Tools/Bit/t2.fctb b/src/Mod/Path/Tools/Bit/t2.fctb index 931c833964..b947d7f4f5 100644 --- a/src/Mod/Path/Tools/Bit/t2.fctb +++ b/src/Mod/Path/Tools/Bit/t2.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T2", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t3.fctb b/src/Mod/Path/Tools/Bit/t3.fctb index b4e204e13f..0986533ddd 100644 --- a/src/Mod/Path/Tools/Bit/t3.fctb +++ b/src/Mod/Path/Tools/Bit/t3.fctb @@ -1,12 +1,12 @@ { - "version": 1, + "version": 2, "name": "T3", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", - "attribute": {}, + "shape": "endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", - "Diameter": "3.000 mm", + "Diameter": "5.000 mm", "Length": "50.000 mm", "ShankDiameter": "3.000 mm" - } + }, + "attribute": {} } diff --git a/src/Mod/Path/Tools/Bit/t4.fctb b/src/Mod/Path/Tools/Bit/t4.fctb index dc3042205f..df4edfaaa9 100644 --- a/src/Mod/Path/Tools/Bit/t4.fctb +++ b/src/Mod/Path/Tools/Bit/t4.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T4", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t5.fctb b/src/Mod/Path/Tools/Bit/t5.fctb index 9db3300c7a..9f8bdb0a6a 100644 --- a/src/Mod/Path/Tools/Bit/t5.fctb +++ b/src/Mod/Path/Tools/Bit/t5.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T5", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t6.fctb b/src/Mod/Path/Tools/Bit/t6.fctb index b1c2f58c61..61efb7d319 100644 --- a/src/Mod/Path/Tools/Bit/t6.fctb +++ b/src/Mod/Path/Tools/Bit/t6.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T6", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t7.fctb b/src/Mod/Path/Tools/Bit/t7.fctb index 10a3fc10a6..600de9ded1 100644 --- a/src/Mod/Path/Tools/Bit/t7.fctb +++ b/src/Mod/Path/Tools/Bit/t7.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T7", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t8.fctb b/src/Mod/Path/Tools/Bit/t8.fctb index 995b5012e2..b957c96871 100644 --- a/src/Mod/Path/Tools/Bit/t8.fctb +++ b/src/Mod/Path/Tools/Bit/t8.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T8", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t9.fctb b/src/Mod/Path/Tools/Bit/t9.fctb index ab49b17dbc..2135c0a9b6 100644 --- a/src/Mod/Path/Tools/Bit/t9.fctb +++ b/src/Mod/Path/Tools/Bit/t9.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T9", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Library/endmills.fctl b/src/Mod/Path/Tools/Library/endmills.fctl index f3e9b37c34..c443e6cd10 100644 --- a/src/Mod/Path/Tools/Library/endmills.fctl +++ b/src/Mod/Path/Tools/Library/endmills.fctl @@ -2,40 +2,40 @@ "tools": [ { "nr": 1, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t1.fctb" + "path": "t1.fctb" }, { "nr": 2, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t2.fctb" + "path": "t2.fctb" }, { "nr": 3, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t3.fctb" + "path": "t3.fctb" }, { "nr": 4, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t4.fctb" + "path": "t4.fctb" }, { "nr": 5, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t5.fctb" + "path": "t5.fctb" }, { "nr": 6, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t6.fctb" + "path": "t6.fctb" }, { "nr": 7, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t7.fctb" + "path": "t7.fctb" }, { "nr": 8, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t8.fctb" + "path": "t8.fctb" }, { "nr": 9, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t9.fctb" + "path": "t9.fctb" } ], "version": 1 -} \ No newline at end of file +} From 1dc9a9afc4be01119a723c05e457b480838821ed Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 17 Nov 2019 12:38:06 -0800 Subject: [PATCH 46/52] Added Ballend and Bullnose shapes and consolidated shape names. --- src/Mod/Path/CMakeLists.txt | 6 ++++-- src/Mod/Path/PathScripts/PathSurface.py | 5 +++-- src/Mod/Path/PathScripts/PathToolBitEdit.py | 2 +- src/Mod/Path/PathTests/TestPathToolBit.py | 6 +++--- src/Mod/Path/Tools/Bit/t1.fctb | 2 +- src/Mod/Path/Tools/Bit/t2.fctb | 2 +- src/Mod/Path/Tools/Bit/t3.fctb | 2 +- src/Mod/Path/Tools/Bit/t4.fctb | 2 +- src/Mod/Path/Tools/Bit/t5.fctb | 2 +- src/Mod/Path/Tools/Bit/t6.fctb | 2 +- src/Mod/Path/Tools/Bit/t7.fctb | 2 +- src/Mod/Path/Tools/Bit/t8.fctb | 2 +- src/Mod/Path/Tools/Bit/t9.fctb | 2 +- src/Mod/Path/Tools/Shape/ballend.fcstd | Bin 0 -> 12864 bytes src/Mod/Path/Tools/Shape/bullnose.fcstd | Bin 0 -> 12669 bytes .../{drill-straight.fcstd => drill.fcstd} | Bin .../Path/Tools/Shape/endmill-straight.fcstd | Bin 10501 -> 0 bytes src/Mod/Path/Tools/Shape/endmill.fcstd | Bin 0 -> 10226 bytes src/Mod/Path/Tools/Shape/v-bit.fcstd | Bin 12187 -> 13649 bytes 19 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 src/Mod/Path/Tools/Shape/ballend.fcstd create mode 100644 src/Mod/Path/Tools/Shape/bullnose.fcstd rename src/Mod/Path/Tools/Shape/{drill-straight.fcstd => drill.fcstd} (100%) delete mode 100644 src/Mod/Path/Tools/Shape/endmill-straight.fcstd create mode 100644 src/Mod/Path/Tools/Shape/endmill.fcstd diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index f266fce952..09ad75deab 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -156,8 +156,10 @@ SET(Tools_Library_SRCS ) SET(Tools_Shape_SRCS - Tools/Shape/drill-straight.fcstd - Tools/Shape/endmill-straight.fcstd + Tools/Shape/ballend.fcstd + Tools/Shape/bullnose.fcstd + Tools/Shape/drill.fcstd + Tools/Shape/endmill.fcstd Tools/Shape/v-bit.fcstd ) diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index c32dedcd9b..1b1e053948 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -1796,6 +1796,7 @@ class ObjectSurface(PathOp.ObjectOp): lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0 FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0 CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 + CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0 if obj.ToolController.Tool.ToolType == 'EndMill': # Standard End Mill @@ -1817,13 +1818,13 @@ class ObjectSurface(PathOp.ObjectOp): # Bull Nose or Corner Radius cutter # Reference: https://www.fine-tools.com/halbstabfraeser.html # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - self.cutter = ocl.ConeCutter(diam_1, (obj.ToolController.Tool.CuttingEdgeAngle / 2), lenOfst) + self.cutter = ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) elif obj.ToolController.Tool.ToolType == 'ChamferMill': # Bull Nose or Corner Radius cutter # Reference: https://www.fine-tools.com/halbstabfraeser.html # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - self.cutter = ocl.ConeCutter(diam_1, (obj.ToolController.Tool.CuttingEdgeAngle / 2), lenOfst) + self.cutter = ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) else: # Default to standard end mill self.cutter = ocl.CylCutter(diam_1, (CEH + lenOfst)) diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index 2baba5ba26..fdea4e672a 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -54,7 +54,7 @@ class ToolBitEditor(object): self.tool = tool if not tool.BitShape: - self.tool.BitShape = 'src/Mod/Path/Tools/Shape/endmill-straight.fcstd' + self.tool.BitShape = 'endmill.fcstd' self.tool.Proxy.loadBitBody(self.tool) self.setupTool(self.tool) self.setupAttributes(self.tool) diff --git a/src/Mod/Path/PathTests/TestPathToolBit.py b/src/Mod/Path/PathTests/TestPathToolBit.py index 2eb2bbfa4b..f5aac40ea6 100644 --- a/src/Mod/Path/PathTests/TestPathToolBit.py +++ b/src/Mod/Path/PathTests/TestPathToolBit.py @@ -31,9 +31,9 @@ class TestPathToolBit(PathTestUtils.PathTestBase): def test00(self): '''Find a tool shapee from file name''' - path = PathToolBit.findShape('endmill-straight.fcstd') + path = PathToolBit.findShape('endmill.fcstd') self.assertIsNot(path, None) - self.assertNotEqual(path, 'endmill-straight.fcstd') + self.assertNotEqual(path, 'endmill.fcstd') def test01(self): '''Find a tool shapee from an invalid absolute path.''' @@ -45,7 +45,7 @@ class TestPathToolBit(PathTestUtils.PathTestBase): def test10(self): '''find the relative path of a tool bit''' - shape = 'endmill-straight.fcstd' + shape = 'endmill.fcstd' path = PathToolBit.findShape(shape) self.assertIsNot(path, None) self.assertGreater(len(path), len(shape)) diff --git a/src/Mod/Path/Tools/Bit/t1.fctb b/src/Mod/Path/Tools/Bit/t1.fctb index 0fe10f2e03..7968a74a9c 100644 --- a/src/Mod/Path/Tools/Bit/t1.fctb +++ b/src/Mod/Path/Tools/Bit/t1.fctb @@ -1,7 +1,7 @@ { "version": 2, "name": "T1", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "5.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t2.fctb b/src/Mod/Path/Tools/Bit/t2.fctb index b947d7f4f5..1c70485e5c 100644 --- a/src/Mod/Path/Tools/Bit/t2.fctb +++ b/src/Mod/Path/Tools/Bit/t2.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T2", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t3.fctb b/src/Mod/Path/Tools/Bit/t3.fctb index 0986533ddd..0f2614a2b0 100644 --- a/src/Mod/Path/Tools/Bit/t3.fctb +++ b/src/Mod/Path/Tools/Bit/t3.fctb @@ -1,7 +1,7 @@ { "version": 2, "name": "T3", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "5.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t4.fctb b/src/Mod/Path/Tools/Bit/t4.fctb index df4edfaaa9..c97b20feed 100644 --- a/src/Mod/Path/Tools/Bit/t4.fctb +++ b/src/Mod/Path/Tools/Bit/t4.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T4", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t5.fctb b/src/Mod/Path/Tools/Bit/t5.fctb index 9f8bdb0a6a..014ebea50c 100644 --- a/src/Mod/Path/Tools/Bit/t5.fctb +++ b/src/Mod/Path/Tools/Bit/t5.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T5", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t6.fctb b/src/Mod/Path/Tools/Bit/t6.fctb index 61efb7d319..521b489554 100644 --- a/src/Mod/Path/Tools/Bit/t6.fctb +++ b/src/Mod/Path/Tools/Bit/t6.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T6", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t7.fctb b/src/Mod/Path/Tools/Bit/t7.fctb index 600de9ded1..b10067d4aa 100644 --- a/src/Mod/Path/Tools/Bit/t7.fctb +++ b/src/Mod/Path/Tools/Bit/t7.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T7", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t8.fctb b/src/Mod/Path/Tools/Bit/t8.fctb index b957c96871..2ad54eb330 100644 --- a/src/Mod/Path/Tools/Bit/t8.fctb +++ b/src/Mod/Path/Tools/Bit/t8.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T8", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t9.fctb b/src/Mod/Path/Tools/Bit/t9.fctb index 2135c0a9b6..3a3dbc3f78 100644 --- a/src/Mod/Path/Tools/Bit/t9.fctb +++ b/src/Mod/Path/Tools/Bit/t9.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T9", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Shape/ballend.fcstd b/src/Mod/Path/Tools/Shape/ballend.fcstd new file mode 100644 index 0000000000000000000000000000000000000000..bf7235e366c96361d43854e8beceec4db1ee89d1 GIT binary patch literal 12864 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 hP_W`xV8K`Beu2K*Y|_P>^5f%fGDqe*hWxVY&bS literal 0 HcmV?d00001 diff --git a/src/Mod/Path/Tools/Shape/bullnose.fcstd b/src/Mod/Path/Tools/Shape/bullnose.fcstd new file mode 100644 index 0000000000000000000000000000000000000000..24b54457511dc84145ceaf747edec9f96e9726cf GIT binary patch literal 12669 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 P*xtxUfKL$rZ|nXaQO?a` diff --git a/src/Mod/Path/Tools/Shape/endmill.fcstd b/src/Mod/Path/Tools/Shape/endmill.fcstd new file mode 100644 index 0000000000000000000000000000000000000000..0fc51c64e7b9871e8336a776fda2fb2d9d0b9164 GIT binary patch literal 10226 zcma*N1yo$i7OvY^aJS$t!QGt%cXxLh_h2D7!QCymySuwvfZ*-~4|d5pW51oS-`V$8 zj~=~7kNSH3RjdBFYR+DAl3?Iy0000A&{JtC>%aviKnM>2n0o>Ma4+A!w=s0GGPZW4 zbF~5<<7qoAah$Za3+*PXjHUzSSAURy7f;DaNxF*b#)Uj&%o{wLNZr zPx}Z&Cgi!hYEOA@uN8kv91tHeGr{M;v==6y zjYr7bmWSJ(NZDyXIIXlIuepF*nJysW;X)C#G~(d9M9v{wdo01@e&Te?C!O6SzY$N# z_-x6)z9}xCdswqA(j=q=wpRZ;mJL z;R6%9_g=NT{Txb#TsbHw+&09tLs4+nS0j_yc$P>3hYDQCZ)S7M-3IP6vv~=|I}9rF zE51!r8?K1hmZOJW1)%&s%v_qHvi=!f;Me{T{Dln81KPDQn#?J^=+o)-y@z#=M*h2X zaP&_e+A8ZyRWQ13E4s2)?4qX-9-en~HAAW*aZP(-Z^S2wwpDbX-3Eg!=VStOtDQr> zG+pf%SkB2-Dph+41@31T62`KgWe}PC%spDk5rC7N@0)@-*BG;#4>688@-DL?SP)#} zX?WyYhcL=`|HQqy8D1n|FFN2X`Ek@bbFYiUYF}|mLwye0St2t(vM$ynrmjN2pL>~FGn-ILCYb^%oxt@Wak9is;&^a=RAdyhCxfM)DjAiy%d1`XDXK9C zEbH*Iqn|*$En^~CDos|5`AC~Mmu6<{kMb5MFucVvu}YMTapawE5lENfks^d^)(@O? zbZZ|tZNmDC=}T3}cOu%@Gjg}l1pR7Yjqi>fmhI@R)E6}AVWKDVlS#T8KyK-=*+XC` zrb+Dxz+5*+_4B?JNeY_BkZP+csZ+!Bl+5~T$gM1a@CDM|ESuWU88nMxk=d!rV-1%@ z68mH!Vj#a?=eTDCd*sXZVaD9?gO_SVQC4yS6VIf!-%Q`a6;d-`DAxObnpVHnfB8qlY z4w5`b@^9>fy>I!DqFJRv7{94w*vL=;ZHh~fse-XSZI8wbs|Y) zV3RZFI(Cw~bFcJF(dX0p9}-kRo=W%=sAYQFuRJF_KG6qX;M=VXr{2H-``(_VRut>n zb+m7M@`hMrZW@O=Q>HXKGaFZ!!ybI_ujTnj+>2{Xpo~PWXUl(*T42`_X>BH=pCo{mtx0_p1X5XkVCx#+&Amyu0y#zj>K$?sv8zzBaC&>NokTCQj zF5AgJm-ML7b|q23BC!6Xfk&dIKU1SwTsp+bNKs|=U9x8b4BQe_Pw5yNv|VvE{Cf{Q zjvZnzu<-LB#OLTYqOHy~wbBf|(c7*`G@4=5e1#NJtK$aCDo z=&FkT$Pwf6?7)pEp*>}J2(BX=>6FB8QgWT~z=t2gIkikki z7?MjU=}UrR;57j!JV~lvA8Ec{^#&5P=cO}&rmVy%4$!E6z+s8#WPaEorghL~_XJ~n z+Xf4fhrPL!?3ul67sHkEOoDtoY^B=O`V!x@jM7P&`UZ>+MoRUVc$$SqJYK1L23-!7{gHg1U+h4c3ksD%i_^l=kl zqS20W>Q?B?L`p*K80oMNDfnTp=i$sg3di$&k{8l@xZP4{bKrAvg0XAdi%!F>XpFUo zkXlbR=FV`Y(r-Y)Si_1qyGVRGKkQaW{T#rv9ZbC@{&w+ee{_P!`Bfk8mg=Pp$(jE1 z8wJ1&7kBZ#jMTj}GAef-^Vg2#B0$dJfbO*g6Ep*Ns<&N1@5KNqG#WxWh+AH3nP~@P zkZ|zl`R@!@=9)d<n8os289M1HE90YflV{XEwO{$mc8QCc1fgO^?1uq>8A zm>ivXu0DNRHaxbji-@TmPzjshBJP=S5QkzF$cmky^jI3=&nK9rGaJ+l*ScXU7NtZ2 zE9C(RI}dEeRh3A?XPN@%US~_Sw!|leNJai=P|&(GnQ&QKc2)d-pqo~dNz5ov*vzW3 zXzC9`HT8;+#KT1NweJKLGj8E$U5F0&I%E47q$qw`hkG<6TI9?JtEkQ)H?-|X zBZ#*|`VfM)XC4waQl~`ciNyoW^)wP_KRL$?osc;bboHLf#~+cX~Wq#SqNEWMTyB&g~`x?pp+q@wAtYhljMw|Q3{f&z7QLP zV^~yn;VMkSaA&m<8!hn?aQWtt8u}9wGGt?C=pFNX*0u7q!3Y~Zjro2zQaM|sT!eP3 zM~plkTQ`uSe&Yy>1%#O^k8N^J1tG%Q&PeD9S9t#Ar(0+7>RN7G7CCfZOfWw~z=?|n zSevcU0D=uAeugjlYk5#j5w)fPsj6~%c3SWlPquT|5;j?chUl<)_(!^tqzX+pAs;Bh z6^r99d2`F!=F`p9W}2!j2!#V;2buz;qzxk@<{-i}ShLZ1tB4Dmps6e*w;X1MalXwD zOIPnEA!JwcuGZ!gR6@GbW~A*Bhu{#51P(`|kSWKTDhyrLuWX@ZjZrXU)uPUxSFz1c+NTND_`SvtJ=S}(uR1mYFMyjCH52(!!Wz;St|mCQ?>Fj9$&BWQ0m zi3sJ36mHpGA*;1fRgP}Fx?o%ZyL$;mHjzsfEr1zhd{{F=s7+&;Dkha`e*$_C-Rk7>^jI!bc$cXkKT6E&`SguqI* z4>N0!oeNr}y(Nfnq!|gm1r*f9geBi3dp6SbKC)sE*Nqq76|7*C^BovFon1LY;tZ?y z$mTR*3ewu%fiKs0i8mZ}UCDcS;GogdZu#SEKpst}!Ex`~`buR3#%l5Sx>myRA(^Jq z(q(zMn+G>RFy|%N)s4s6O){Z7V7^hTbCndejNtB$4yCZDoPXD4goI&tInGn8c*^b+ zx60T1V6~-(H)6^>ZrV&#Zh}zcXcu;Y=-%DF{`nJ&x)%e%jt#}|yIDJ??w*aB^u&et za2@eKuuPb~)4_LqJ{(jTVUs-xS=&m?V~3n%8LJ9#9F0}Z1EH4Qk0QP2$BNL0S<(U6 zd?Mq8bR?Wy;%Pb;C4KUvJ8b=M5 zv*2C1?W|_#Qx+20mmzJWPbwzD!T+C>rJ1 zmY4h-OSS^>=19M6LYl7{`!WvO&Sb)6qnVlrtfHS_IA;kCTww%lp z&TQ=vKe67g3eS(>ctZgrera-9R__7<@w`cQJNG8Q?maF7?FgHh91j}I?IZQ?$>Rsp zmlB@TY{kU5m?<0PZRrV;w;D2E;Gfir;ouV6_#P-dr=TW`^OH>A(oBA~bo$Q}%Ar(1 zQS@#S`2YSFVUzlOlSLJ;6QGAePYej)p84hwa(kb^c>4L;xtY@ zY{R~{rCskCrk$uQCb_~`a8w}!By>mG{8W$}`>$g}Or3_}TPVWif6^S17GK?e>aqo} zuI~@Pup#17^H~s&45k`=n4c1al3c|n;Ut8sV2CeuE|MEKov))99Q}|dt*ij%S}B_{6@=Pc=2nl+Q8<{ zVlePxeno~Z75qU41nah`;h6yPwuo#SoBgzv7$k?Si`1JEzmjbc%KG#sA9zs(2}Vr# z!6jwX!iIKD2UP$k>|PO!x1rJZF{jmvr{qqt!CrUd5t{S9fdt?BlV6Mrf*vW-TH(8{ z(Jc4twfTo$R(PUoj4(r87{?^1^Nvu58VZPzlMmvN-Qfb_hBq#_Q-fDfqZ1(88P6lw z>F<+DCQ<3%!;JQRJ>8oWsLPIO9%+tcwa2_&_9wp>_mNOu%Q*hlbEka3`%Y>xU0Q*8 z2CE7SHtEQQEhh$%IMkEv`$gonkKLf9SSQ#w1pKlB-`xtt5oAF*CkTN8(zQk^LIv}^YaEzJ zH;eY7qxf5h-oqAp$Z$D}qmT2z7Y!>viTa0!O$YhH*u2YCrWXjlT&{ zItmx)1;kN0J$Q>h;6y)B?jr_R93}8wd?`&{fOKMSRkg8J=HRuZt`{Gk**!bRxAyl{ zQ17tmSK=SiVg>U^!pIZa@>B9iicB3AtfFJPFjJwbCWtSBj6a=76mdvipCv7soYM}_ zi6K-H8w%Wz&v&<967l~cDK6|Iw=A(S$$zYcf*2RSLD%?$e6f>jBh332*nH;3)A5r( zboIh6s>!2r3_-f1!%W+w@!iL6fIH;oWWDAFH%_;amt)=|ElFoF$RpT<4WJME3%{Aa z35G=6{WZ&t?VJ8glDaPY_u$2QvU{$}k+y)oFyW=X28ku84A!#|^XPHxZd3rjb)L3y z{)felF4%=eX`Je5Wc%wxY2|fjP5$|6=JI!*VArBiX;)n^A5RT_P=`9^Ei~J=K6vib zs7qhca>S&6=3r@#9;!U5I(?Y=JdXB>3v>~US7Z#>KCu~W5TvcBEmKGag3ICR6yBDEVnE{UJ5OFAJQi6gHNX{W1MZMfiOjced zRqjfJX^GzK{78l2Qf+`0VOHMY{?wRSvU6u&r+qXAgWb0@o8cj`m^=qW-a_wz$s(#|VFC93mW64PX!s+@o+MX0bW*FP0KSHW*m)FKh1OIU|bIrZJwe9CL zcQ@uKy^0SSB_HBlRuq%Me^^0!jh}8#WR@_``;>F`_V+pH^>Hq8BCDiDLV^(C zbWy>t=+?5lO`SN~sYv$movn{R`CLz+^I0G?(SYqiP(+b@awPPtl<7+D5#@B$cny>1 z>NP{>K>bx?{yCXEkH3^k#=Sr!X#2D1395bogG5ai?#;QlMDsqER4fAu_<-U5a+Qf%MEI1s*6NK5gh_MN~BzrPC9OX zX7q^0uMu)_PN4h9mb!;Z#D~KIG$xk=F~C+o)XKDbzY1vD(&EmiFRm!UdgTh{zg&UM z_{$Y%hQo6M*Htt;B9VMo>)X^xUJtA7f+o$e8ItZQ=1Yim|X-Mkm(Dhg<=kBdg z>leY(Z12Ldp+I|u@bsD5#~eO5{-W|;1C>3mN=p3x3p>tS`t1s}HT^RCDx(yWXVIjX z6*(R$=5o>u!@Q6{#+d#L?)!&S1sU_=5%+{c<&#^R#~IBr{L|%QE%m^9 zWTH_0)%_qHbi79B#>%oJyaE^qlt7ZOJcA)6Y_j>&tE@TeCK(HEbqo6xmui`%+}cQ< zY$OH3X9_bFDOL*U!0lfC%yO+vg%3SnK>bGGcjVn+m0%IrRgYoD_N9OywM6HBnFH_J zxR`HjGPNw(sDcjegeC-7u@MkPVk}gH@7$WYQ$1vPeJmulrJ2d57q)Ssc%k-Cl+z76 zZn`F-b>rvV{iOvPUEbGiBToz_7PT=i3}IJGmC6s1)IqF5jcGwQUmsi!5-pF7bHm-l z65JZqawcEgL0^~G!iOT}rX2H#3#(&dO0+7O!Hb6WjYJ#%`Iek-&dji7^lV%Fk!034$Y$2 z$vD2`Nd(pjvTsV`2h?0lv+g78Y(Z;&XkFX)1SJE{LPR4Bz48ET66dVz zI0-#rR)8rAmx4tS5*pzGI?3Mg*e9p{O}rd(g=7i~kNz<=#Mn2HrM`0-C376ji-j?m zNu~m%wQ+qs@Qz4}7hGWX&g9yI!1=E+yNzHBE&=XT>}IylOB<+=90C-GO>b_^9u2Du z_A^PlLUz0>-ukIdaH-_z{&}yb6zbA4d%4dCI{*NPFZX&;C-Xo4Nzk@(*kD6?O4rS5 zVjfKps(^nxWy?;wrNcEyiaNDCoPYxYx(v2Nl0sTD{k#$f^d%x+82A(ulp<~)PeX56 z>U=YKaFRd^?IgWqfm_r&MMSJJf08tpX~%K?(|>XO-UX7P9SsI@4~%T9+>I>oQ{LHN6ST*Xn4rsw`WeI;~K1G zn^z*;-C6Yg-ibaLx@M3(t9SZhKD(Gk2U=RJrtk`BE_CnFWagtG4T%O_3-PJ1db1En zs|xx>NPmPU`v}<%+vfyzCABdo0^zl+&QE%u||WhAtR(j5Hv%}MZ+)6FQ!Y+!OVHLlZGm%8Qx+QclA zwy9r!f^$UAl#_MmOtweS$RKRKKrD;Qis>*3n2<1xWcG}sCLy1DpyM@o2JiIk8O+BNZeZ^_}^Hr)85c#36yMUa-_LE z+pf$Gv3^yUl<2yMKy?e|6_SSAz#^%?=Vg(`YSAkYt6p8u9Yp z__^#8@U333Ot>iqIZsd$8l${&ziG#F0kf<@c$$fJql-y_3Ka2Hw0K@3xy<|wZ((Cw z4@Sdy=p#6uN&EW`b-XnMe46!{ZYU)V&8UzU8Ll7sLV-sgRO*WK-zHm>Xn4Eb*d{Ae z^W7Xc)#epyl%6i6sn*3)#Wq!4xR#AQXu zZsN7)71{QVp=!?(i=(H?RoEVpqCprzd-s3Qq5qw$GRWW7aG4^`Xnz%w%Ln9cWPJxc-xJGwtqW zy@Z_(yCtDE-{zw}{PJ@LYOUD6lSFTIOs*s@)+EfJmeNZMy89@!4%UI;5|AK;P(nr~ zjXoQS`N@FvO^}KLIxJEgYpszH2-2t-P$6U!Z}VG=Yy}&c1Rc9M#vpam>DRr2rfW&N^-6^8AKP($8X*k`*C^mh6w(+#?hz- zS*fWGD!X)5RG9M1*QRQDr!6p*exBE3&=SOM+%*|%y|2-hS8KTnz7{K$Z*&?D1CSB# z{n-C$dhxD)4a|gm9tTt=7{cA$Lw|4u+)iV7ah`S&hgjww<#^to;}}>g9>`IO9*p}k zOmk`KwsI^xi7o#VNV~-(%9b}h`-k-e^CUn)sV^fa(dt5yj9<*JQ*yZCh`MR~#Y~{j zmxtM7$w-9*RE-<0d&N)?v99ZF6)YYOOt=~E0-AI|TP{FRv~`t)w(xS`t5cWdRQEYT&=wSlZ0A5YuR)ZMR-f-rq znAV<(_QIa)wxri(=kJ%$@J0GG-{8&&S-{rfH z%QX@lO=UEGo_|}sh}vIiUe;2p$>a^@Z(n&*M1U(^pHtm=Eifa27bY#7E96HO0Xix-$h4$3hYe*n)DbO4uGp z?iinpNMgwlc0)TO$q-g2+9&rqRw1bjAPB(8h+lR>bD&HardZn!b)rLXI|x)Vvos5C zwzoeP#Xkw`;g87~kzbQJ$uZjX-w(aFr*O8c6_iPlM8?efqBb1ardyCPkghtDk$NSP@_@F{$67$qGftzbg zQd$~o!(!i0vVApEDl#T%L9X_lXu)CQfXSGzZ+2xNpd}oE62U<+xh6)ZOVArE`7!*^ z@BKhA9ZDg(PLz0x@C5niri5p>(;NMz0D=6nXMag#0gh%)RtDDk=0FE}rN0j8Y^_ZX z;m#a(x1%=<^z_!QE^VPfr{D=kI>AXj4cbOQ6jCAZqk7@sHiQUA68WA{8{eoNh9Zj@Z{Xz30!hX>*u787;?FO0-c`?xBYIMLmXStc-D4BwSN7bT zn@YF#dbrH4O+On;93;#@Uljyk@-lm%U#9S7i}&BhM9fW0oE(gWZGblRhDLzT%%)>6 z70*BFp%Wr#AAWWJ_5M}6Z+{)arYXJ(hb=I)>JUMM}iTSV|zllMFH z|0C}=3Ir4l?0-K3^fI#lv>KmZAHQ$oGiMtzLM(S;D!D@#4x>9 z{ksqUR2~0<{yEeC3je8k_@Am@Xoxqz#)b6q8U_OZKuCWbi`W|*3k$v{{L8xk2in^R Ah5!Hn literal 0 HcmV?d00001 diff --git a/src/Mod/Path/Tools/Shape/v-bit.fcstd b/src/Mod/Path/Tools/Shape/v-bit.fcstd index 7df97e11d626bd5bd9f75a05251b8565868e08c8..a158b864d3c29628d49fc229d06774c65c15aa85 100644 GIT binary patch delta 12090 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

pUkgIkms+{{nj6c3D1SLmga8<>I=hgs2d<& ziRo%2P}HQ`Q?dQZ`WN`a)H#WNT-Lz|eUT@+xc~ILEH0v*%fwwoV>N+KSu9E+i%NY+ zG(oRLY&FrJdQr=OPqL>d-Z`@fc={49ygu_wN)#np{7C4i@!ni&`k4(`=!oY#u&M0();NB&HMVQy zcmA4alOF;JssTuyvT>Qw6H=;sdn@FdncCMq)G=H)W2!e-jtV_i2ru9W8;RDG^W&X37xVmyzU(KH zUp4p7s$pf0-=`#3f%Ma6`*;N0ji5`yA0zu!H#xjF`~5w%SM(xHK^G;-IHFs0;Q~m3 zE<{%f^mzg!D*sG&$k7k(%&-7}w>toU@+aA4JZ&YN%{?70ojjO*939RL^)@WLd33Dc2U;9#ZKF@W{9<1a2SRl~$^D%BaklvOAe4>p0Z~F6260tbHSNL! znTD^aYKo69=$#WkAEwv9PCxF|Ol(W0|76+F?vCKud{f}Zc^9zB`(>=Bhxn-*md~+K zb?oAcOh34b;u|XG_BtQGU_d|stPC$`?|*o+Y#cmpJjfYM{3Z zfFUCGyO*`7^dmQ^7=zsrhSyXMp@T1HPl&fa>|1>hm<DC5m5~xQr|tIjI*i!wG9p^*mk8Q~y-iG>>=RBCCVirGS@Xkg@PTSeMJPK4ykO)X zQcU1bfdZUhn9hr;B@ZY~k(ZA<3VdlHUj@)OuodM$GOp1TyqMaU6JMj#FW4>6YJWR6 zcJ_NrgZ{{8=Rv`I%kPwDts9z&@!V{PwiqHq9ik6v7|Q4vQ7JpvV|vT3eMC}%mag5b zs(qYuv7`rq7TBt+6@e)OI@lukXUY|Oi@h$E*U;UmqDIDH1MkI*owbiQE)}(pqhJJp z=)mKq@MJv1B|A0{YP$vgrUPm4af1Cf>HF!%Lm6442@3TBcLC)6!dpq#1K|ZJLqXM- zPWC4b*X4>0FT8@P;8)$8aqN2*j_TiA$rCVL24c_ftH4ALZXon|*~E)&#tC^lupMK% z6aJQBq(;E>_A+?}$0UpWY4nJppo?+F$npyMj&Q^t1GDvgJWLl-dx%Pt!2?)u6!Z+U zlPxCi(4}d_V%ueVC)MZ}dmAdBww$GyM&;I}bAdFuACKQ~xImaRQZT{RB+$4o9>LPU zi^*-Fk)Ft{1msYroz)WgeCH&BL|I!JM2f;m3lVFY>fNVYLa5{)dpCaR!jOd1DQN$e z)hdVnxkFKq4d?D?`WJ<@++i*8f^?wz@ktSO-&fxXTY*z`WJ_^lhx=V(^;@yI0l_rK zNfYJJ!9GIA{kurE5s=Z!IB{Fe)|7@JJT3a*I%?F` zaG!?u%n>b8G`f*NLTWgCJGy2yQ&f zMwVXqhtM_~G3?ZDn#^Vm-I*v?;~UO z7_V$A90HB1tu>vbYjKvoVLCeVQLQO}w!z}GU#%6gnMPCx#tj;)q7ga}hUxZV*|TU! z#4SDdnadDbwUSR4)Qc>J`FUGJ{V7CfD(0@#q1VXqN91KW;J&)Wh)KW`K7JB$3$K9F zZWxL;uQM31XdJ#;UOO%SEsBER^6KK6t^QH&lNvvQp`+26wS@8qfu_hc1XYjQt(Sm zEb&SRG`a+=LNFCO_dDpJCI=GE6%^Qrkasby%2<8PK2Fk`rghkFPiwV5TEs?e6uolj zHU#4OZhB3yZuRCwuclpz!t2JhG{nK#W;qoIVBlT3uGUX4$+XxTq#bsEn&44z?(DE9 zDjQ>v6%gEnw|5fmY@vk0kyZ33F5F`0C-VF7M($F3Ol?Od;rQDX@;lUAc%S1)!h7fu z7wvzvu=7xk>nW{qxWlk5zj)RLX(VXqeE}*HyT*8IZoyx8gyR7tgt+*IvU^laO9to& zB(lC+|K_h~SQ?!*Ts8Hd(``LQEn(t@G-ko##v}Qa2;Y{xm|N5_K7bzA8>Ds=Alm6q7pBD^Di9Pgfa^DOD8mFB5Fe?JJ zCc|-rchbXk?gJgWVmwNpj#1~@Gs!>K8g^wD3JTBwz%?`|mXa7aq4}&c3xoNdFK1a; zoE)FR8K0gxnWd+0W+yK8J;D+?!bU+!E;gXZ_6GwA2Yp?D$Ry{8)=Bj|C)WfSHarz6 z^_)W`>Id7|5rhns)NUnBh6bCrTjV@M2@GHTlc#76&;)`BNN* zEp{^_)p&zd^U0t5s`I>TD>wC%`G>w+Laexm`bC!wV{r17L#^fyGJ!t^l#%3r{@>0gQ+cEUF1e05;j* za4x}+<>getPgZWr+FqR!MWYMNaopaYAj`3vX7WC+Cbo9(^z=l^xoiB&Rm_WOW}pVi z?pO}(o&|Qjp^;55U+3_Ow&vKYCVeueC`erV@H!CeHT@fS{X+Bmz2VdJ#GyS}=%d`0 zqGtQ+a~9Bmigq|C$HS0*gP!*Js=B2meZq=|8(ES|PKyIR9K2HZ>OFvlWI_CO2?w94 ztB0@Iv@ZfCkqVcSp*+Dv=>BjfI3ew|u)T5#84^i8!Ei>y+1nD)w`lf&kt(O>lQKre zI6#&b5EPjlA6V<~Ft-*K7jlF{wotV%-Yirkg$Z1s#KWBiKY3942nN~6xROMMMSsb@ zjbxi6K*QpSFA_#7`f#c`*4Gh=ngpI^qdi1a|11Hn)Za-mDlNgN3QzH*^4fDU?8G%d z6{j~djZz)=W-YGcDxPf#roZu0GQzn)%#23AXm?;lu61BSqD!XIv_~?~X*T?nh2++4 z)C2_c$!k0-^G<4&BM;lH!8tP+@*l6O6s&eu_vU$I;%zxxCKR2uwSEW-j-E!HMb=Cq z3Ys&5ZD^3<;%<&3F@F@JLK z?mxqZXd2-K_YUpu?4#z#It$58KU8I$xB}#04c%qh5Fx5Z3;h{{4L@rVr;(d(MrLj^PU3Kwzf%P=ddTT<417c)!R8W~->cXpaY)v$V@vRXsuRz9Fg zmsR#p$g%LQy#2SE+Rw=YWtUia+ZQ+&zsH{)VaST51;)lKaKROsST7gSuQKF~e5;vW z&$rzd@|mT3``6cYB7B?Oq;-dVkh_5b0aElMMQTWVYWAz!`EI|Q=CI}wt~TTr_IyR0 zQNiVYlvmdbyPRKN8a})W2+3?hJ^+ER2Bc*=e!@Caoqprrz3j?P?xveQvy`ZHB9z{e zVn=7g=-3fZOOX^udQ43X52IrI@IDiqZj+}o@LRPiSH*mYKgQ4U&x_i(P2#LdDJxXs z=_uV=28PO-ncLr&`!+iE*FVB>>6;DBee@VAlX>x`eqTo}w~tR9a6itU*V6^|R3Syg z5BzFi-c8$E|<14S#QX{aw94%{Rb|$UguLUf}*==ZEUT*6f!K{#I21KQHkl$EZXF-{;k}Co%Jk3a8#hi=2jT`6((JC;%HbozIFS_^hSM)splp2hF zq@Dn06;EfSZZVrx)z1u5@#tBp&mWUOJ(7ezHc{I)?$~e_iQ0G-EB8SV4#F6=(a@f= z{PT+oU;Mov?(cJH00812DQRnE<>_uI;q2h-2C8Pkfc=YJ#eC=R292>0;+mK|qI~$* z-oxLG?<_3o9Gc9|Vv&EBoIvjg{kX$-+tmI%UQD8+}?DQwjvbMuY)?l#`SD zCpsvNm6Vv>+~Pk?5RjNq|2jqo@?>Mv`)ic{y9q)B03iLD%73o<5T8FJ)&Fu-e^FGb z3jYN9UrHJNe+aGrcI8cCCnEU=#Z3Hf6#3?{M{{zvu*bo2! From 154e26b54bdb383563a40ff1746b3fdc93ab44c4 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 17 Nov 2019 12:40:50 -0800 Subject: [PATCH 47/52] Track and restore active document and tool loading, otherwise FC gets confused during unit tests. --- src/Mod/Path/PathScripts/PathToolBit.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index b3c50c5484..a78443d63b 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -148,9 +148,10 @@ class ToolBit(object): obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape')) obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) - if shapeFile is not None: - obj.BitShape = shapeFile - self._setupBitShape(obj) + if shapeFile is None: + shapeFile = 'endmill.fcstd' + obj.BitShape = shapeFile + self._setupBitShape(obj) self.onDocumentRestored(obj) def __getstate__(self): @@ -238,9 +239,11 @@ class ToolBit(object): if force or not obj.BitBody: if force: self._removeBitBody(obj) + activeDoc = FreeCAD.ActiveDocument (doc, opened) = self._loadBitBody(obj) obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) if opened: + FreeCAD.setActiveDocument(activeDoc.Name) FreeCAD.closeDocument(doc.Name) self._updateBitShape(obj) @@ -248,12 +251,14 @@ class ToolBit(object): self._removeBitBody(obj) def _setupBitShape(self, obj, path=None): + activeDoc = FreeCAD.ActiveDocument (doc, docOpened) = self._loadBitBody(obj, path) obj.Label = doc.RootObjects[0].Label self._deleteBitSetup(obj) obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) if docOpened: + FreeCAD.setActiveDocument(activeDoc.Name) FreeCAD.closeDocument(doc.Name) if obj.BitBody.ViewObject: From 2a96a7d796f76a748254433866a56bdbc0d81b6b Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 17 Nov 2019 13:43:24 -0800 Subject: [PATCH 48/52] Moved usage of ToolBit into the experimental features. --- src/Mod/Path/InitGui.py | 12 ++++++++-- src/Mod/Path/PathScripts/PathPreferences.py | 3 +++ src/Mod/Path/PathScripts/PathToolBit.py | 15 ++++++++---- src/Mod/Path/PathScripts/PathToolBitGui.py | 4 ++++ .../Path/PathScripts/PathToolController.py | 23 +++++++++++-------- .../Path/PathScripts/PathToolLibraryEditor.py | 2 +- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 25494fbf87..0657fac577 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -71,8 +71,16 @@ class PathWorkbench (Workbench): FreeCADGui.addIconPath(":/icons") from PathScripts import PathGuiInit from PathScripts import PathJobCmd + from PathScripts import PathToolBitCmd from PathScripts import PathToolBitLibraryCmd + if PathPreferences.experimentalFeaturesEnabled(): + toolbitcmdlist = PathToolBitCmd.CommandList + ["Separator"] + PathToolBitLibraryCmd.CommandList + ["Path_ToolController", "Separator"] + self.toolbitctxmenu = ["Path_ToolBitLibraryLoad", "Path_ToolController"] + else: + toolbitcmdlist = [] + self.toolbitctxmenu = [] + import PathCommands PathGuiInit.Startup() @@ -114,7 +122,7 @@ class PathWorkbench (Workbench): if extracmdlist: self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist) - self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + PathToolBitCmd.CommandList + ["Separator"] + PathToolBitLibraryCmd.CommandList + ["Path_ToolController", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + toolbitcmdlist + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( "Path", "Path Dressup")], dressupcmdlist) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( @@ -155,7 +163,7 @@ class PathWorkbench (Workbench): if "Remote" in selectedName: self.appendContextMenu("", ["Refresh_Path"]) if "Job" in selectedName: - self.appendContextMenu("", ["Path_ExportTemplate", "Path_ToolBitLibraryLoad", "Path_ToolController"]) + self.appendContextMenu("", ["Path_ExportTemplate"] + self.toolbitctxmenu) menuAppended = True if isinstance(obj.Proxy, PathScripts.PathOp.ObjectOp): self.appendContextMenu("", ["Path_OperationCopy", "Path_OpActiveToggle"]) diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py index 2fc5bf261b..0525a3b3f5 100644 --- a/src/Mod/Path/PathScripts/PathPreferences.py +++ b/src/Mod/Path/PathScripts/PathPreferences.py @@ -152,6 +152,9 @@ def searchPathsTool(sub='Bit'): def toolsUseLegacyTools(): return preferences().GetBool(UseLegacyTools, True) +def toolsReallyUseLegacyTools(): + return toolsUseLegacyTools() or not experimentalFeaturesEnabled() + def toolsStoreAbsolutePaths(): return preferences().GetBool(UseAbsoluteToolPaths, False) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index a78443d63b..e9e9de4fdd 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -149,9 +149,12 @@ class ToolBit(object): obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) if shapeFile is None: - shapeFile = 'endmill.fcstd' - obj.BitShape = shapeFile - self._setupBitShape(obj) + obj.BitShape = 'endmill.fcstd' + self._setupBitShape(obj) + self.unloadBitBody(obj) + else: + obj.BitShape = shapeFile + self._setupBitShape(obj) self.onDocumentRestored(obj) def __getstate__(self): @@ -189,6 +192,10 @@ class ToolBit(object): #elif obj.getGroupOfProperty(prop) == PropertyGroupBit: # self._updateBitShape(obj, [prop]) + def onDelete(self, obj, arg2=None): + PathLog.track(obj.Label) + self.unloadBitBody(obj) + def _updateBitShape(self, obj, properties=None): if not obj.BitBody is None: if not properties: @@ -237,9 +244,9 @@ class ToolBit(object): def loadBitBody(self, obj, force=False): if force or not obj.BitBody: + activeDoc = FreeCAD.ActiveDocument if force: self._removeBitBody(obj) - activeDoc = FreeCAD.ActiveDocument (doc, opened) = self._loadBitBody(obj) obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) if opened: diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 5dc7ba98cd..ae21e05172 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -77,6 +77,10 @@ class ViewProvider(object): # pylint: disable=unused-argument return None + def onDelete(self, vobj, arg2=None): + PathLog.track(vobj.Object.Label) + vobj.Object.Proxy.onDelete(vobj.Object) + def getDisplayMode(self, mode): # pylint: disable=unused-argument return 'Default' diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 5cf6eead07..9ff19b9668 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -31,13 +31,8 @@ import PathScripts.PathToolBit as PathToolBit from PySide import QtCore -LOGLEVEL = False - -if LOGLEVEL: - PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) - PathLog.trackModule(PathLog.thisModule()) -else: - PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) # Qt translation handling def translate(context, text, disambig=None): @@ -86,6 +81,8 @@ class ToolController: # pylint: disable=unused-argument if not self.usesLegacyTool(obj): if len(obj.Tool.InList) == 1: + if hasattr(obj.Tool.Proxy, 'onDelete'): + obj.Tool.Proxy.onDelete(obj.Tool) obj.Document.removeObject(obj.Tool.Name) def setFromTemplate(self, obj, template): @@ -186,17 +183,23 @@ class ToolController: def ensureUseLegacyTool(self, obj, legacy): if not hasattr(obj, 'Tool') or (legacy != self.usesLegacyTool(obj)): + if legacy and hasattr(obj, 'Tool') and len(obj.Tool.InList) == 1: + if hasattr(obj.Tool.Proxy, 'onDelete'): + obj.Tool.Proxy.onDelete(obj.Tool) + obj.Document.removeObject(obj.Tool.Name) + if hasattr(obj, 'Tool'): obj.removeProperty('Tool') + if legacy: obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) else: obj.addProperty("App::PropertyLink", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=True): - PathLog.track(tool, toolNumber) + legacyTool = PathPreferences.toolsReallyUseLegacyTools() if tool is None else isinstance(tool, Path.Tool) - legacyTool = PathPreferences.toolsUseLegacyTools() if tool is None else isinstance(tool, Path.Tool) + PathLog.track(tool, toolNumber, legacyTool) obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) obj.Label = name @@ -215,6 +218,8 @@ def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=Tr tool.Material = "HighSpeedSteel" else: tool = PathToolBit.Factory.Create() + if tool.ViewObject: + tool.ViewObject.Visibility = False obj.Tool = tool obj.ToolNumber = toolNumber diff --git a/src/Mod/Path/PathScripts/PathToolLibraryEditor.py b/src/Mod/Path/PathScripts/PathToolLibraryEditor.py index 367955a288..bc51839658 100644 --- a/src/Mod/Path/PathScripts/PathToolLibraryEditor.py +++ b/src/Mod/Path/PathScripts/PathToolLibraryEditor.py @@ -441,7 +441,7 @@ class CommandToolLibraryEdit(): pass def edit(self, job=None, cb=None): - if PathPreferences.toolsUseLegacyTools(): + if PathPreferences.toolsReallyUseLegacyTools(): editor = EditorPanel(job, cb) editor.setupUi() editor.form.exec_() From 71c80610bf38320b39bd85e6aa079c20937a832d Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Wed, 20 Nov 2019 17:54:56 -0800 Subject: [PATCH 49/52] Fixed generated tools. --- src/Mod/Path/Tools/Bit/t1.fctb | 6 +++--- src/Mod/Path/Tools/Bit/t3.fctb | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Mod/Path/Tools/Bit/t1.fctb b/src/Mod/Path/Tools/Bit/t1.fctb index 7968a74a9c..9221229563 100644 --- a/src/Mod/Path/Tools/Bit/t1.fctb +++ b/src/Mod/Path/Tools/Bit/t1.fctb @@ -2,11 +2,11 @@ "version": 2, "name": "T1", "shape": "endmill.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", - "Diameter": "5.000 mm", + "Diameter": "1.000 mm", "Length": "50.000 mm", "ShankDiameter": "3.000 mm" - }, - "attribute": {} + } } diff --git a/src/Mod/Path/Tools/Bit/t3.fctb b/src/Mod/Path/Tools/Bit/t3.fctb index 0f2614a2b0..86e6bf1110 100644 --- a/src/Mod/Path/Tools/Bit/t3.fctb +++ b/src/Mod/Path/Tools/Bit/t3.fctb @@ -2,11 +2,11 @@ "version": 2, "name": "T3", "shape": "endmill.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", - "Diameter": "5.000 mm", + "Diameter": "3.000 mm", "Length": "50.000 mm", "ShankDiameter": "3.000 mm" - }, - "attribute": {} + } } From 57e241702029840edfe40517dfa7db2ba1ef1120 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Wed, 27 Nov 2019 20:23:36 -0800 Subject: [PATCH 50/52] Added new unit tests to CMakeLists.txt --- src/Mod/Path/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 09ad75deab..3b515b4f2a 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -176,9 +176,11 @@ SET(PathTests_SRCS PathTests/TestPathLog.py PathTests/TestPathOpTools.py PathTests/TestPathPost.py + PathTests/TestPathPreferences.py PathTests/TestPathSetupSheet.py PathTests/TestPathStock.py PathTests/TestPathTool.py + PathTests/TestPathToolBit.py PathTests/TestPathToolController.py PathTests/TestPathTooltable.py PathTests/TestPathUtil.py From a575f9549409944cb9dec61ff9f3d370bc7babf0 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Thu, 28 Nov 2019 17:31:08 -0800 Subject: [PATCH 51/52] made path acrobatic windows compatible --- src/Mod/Path/PathScripts/PathToolBit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index e9e9de4fdd..eeae4a70ae 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -74,7 +74,7 @@ def _findTool(path, typ, dbg=False): if dbg: PathLog.debug(" Found {} at {}".format(typ, f)) return f - if pname and '/' != pname: + if pname and os.path.sep != pname: ppname, pfname = os.path.split(pname) ffname = os.path.join(pfname, fname) if fname else pfname return searchFor(ppname, ffname) @@ -101,7 +101,7 @@ def _findRelativePath(path, typ): for p in PathPreferences.searchPathsTool(typ): if path.startswith(p): p = path[len(p):] - if '/' == p[0]: + if os.path.sep == p[0]: p = p[1:] if len(p) < len(relative): relative = p From 5c4447032e01e6909850c560fda91e4da1021871 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Thu, 28 Nov 2019 17:41:23 -0800 Subject: [PATCH 52/52] Reduced helix tests - they never caught anything. --- src/Mod/Path/PathTests/TestPathHelix.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Mod/Path/PathTests/TestPathHelix.py b/src/Mod/Path/PathTests/TestPathHelix.py index 9f72ab4cc2..de417c325c 100644 --- a/src/Mod/Path/PathTests/TestPathHelix.py +++ b/src/Mod/Path/PathTests/TestPathHelix.py @@ -34,6 +34,7 @@ PathLog.trackModule(PathLog.thisModule()) class TestPathHelix(PathTestUtils.PathTestBase): + RotateBy = 45 def setUp(self): self.clone = None @@ -69,7 +70,7 @@ class TestPathHelix(PathTestUtils.PathTestBase): proxy = op.Proxy model = self.job.Model.Group[0] - for deg in range(5, 360, 5): + for deg in range(self.RotateBy, 360, self.RotateBy): model.Placement.Rotation = FreeCAD.Rotation(deg, 0, 0) for base in op.Base: model = base[0] @@ -82,7 +83,7 @@ class TestPathHelix(PathTestUtils.PathTestBase): def test03(self): '''Verify Helix generates proper holes for rotated base model''' - for deg in range(5, 360, 5): + for deg in range(self.RotateBy, 360, self.RotateBy): self.tearDown() self.doc = FreeCAD.open(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_holes00.fcstd') self.doc.Body.Placement.Rotation = FreeCAD.Rotation(deg, 0, 0) @@ -103,7 +104,7 @@ class TestPathHelix(PathTestUtils.PathTestBase): def test04(self): '''Verify Helix generates proper holes for rotated clone base model''' - for deg in range(5, 360, 5): + for deg in range(self.RotateBy, 360, self.RotateBy): self.tearDown() self.doc = FreeCAD.open(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_holes00.fcstd') self.clone = Draft.clone(self.doc.Body)