diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index f8d7479b5ea2..83937b084fb5 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -40,6 +40,8 @@ SET(PathScripts_SRCS PathScripts/PathDressupDragknife.py PathScripts/PathDressupHoldingTags.py PathScripts/PathDressupLeadInOut.py + PathScripts/PathDressupPathBoundary.py + PathScripts/PathDressupPathBoundaryGui.py PathScripts/PathDressupRampEntry.py PathScripts/PathDressupTag.py PathScripts/PathDressupTagGui.py diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index ecca417fd280..19246750dfb8 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -88,6 +88,7 @@ panels/DlgToolEdit.ui panels/DlgTCChooser.ui panels/DogboneEdit.ui + panels/DressupPathBoundary.ui panels/HoldingTagsEdit.ui panels/PageBaseGeometryEdit.ui panels/PageBaseHoleGeometryEdit.ui diff --git a/src/Mod/Path/Gui/Resources/panels/DressupPathBoundary.ui b/src/Mod/Path/Gui/Resources/panels/DressupPathBoundary.ui new file mode 100644 index 000000000000..afb57b95c144 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/DressupPathBoundary.ui @@ -0,0 +1,307 @@ + + + Form + + + + 0 + 0 + 417 + 647 + + + + Form + + + + + + Boundary Body + + + + 0 + + + + + + + + + 0 + 0 + + + + <html><head/><body><p>Select what type of shape to use to constrain the underlying Path.</p></body></html> + + + 2 + + + + Create Box + + + + + Create Cylinder + + + + + Extend Model's Bound Box + + + + + Use Existing Solid + + + + + + + + + + + Qt::Horizontal + + + + 40 + 6 + + + + + + + + + + + <html><head/><body><p>Select the body to be used to constrain the underlying Path.</p></body></html> + + + + + + + + + + + + + <html><head/><body><p>Extension of BoundBox's MaxY.</p></body></html> + + + + + + + Ext. X + + + + + + + + 0 + 0 + + + + <html><head/><body><p>Extension of BoundBox's MaxX.</p></body></html> + + + + + + + Ext. Y + + + + + + + <html><head/><body><p>Extension of BoundBox's MinX.</p></body></html> + + + + + + + Ext. Z + + + + + + + <html><head/><body><p>Extension of BoundBox's MinZ.</p></body></html> + + + + + + + <html><head/><body><p>Extension of BoundBox's MaxZ.</p></body></html> + + + + + + + <html><head/><body><p>Extension of BoundBox's MinY.</p></body></html> + + + + + + + + + + + + + <html><head/><body><p>Radius of the Cylinder.</p></body></html> + + + + + + + <html><head/><body><p>Height of the Cylinder.</p></body></html> + + + + + + + Radius + + + + + + + Height + + + + + + + + + + + + + Length + + + + + + + Width + + + + + + + <html><head/><body><p>Length of the Box.</p></body></html> + + + + + + + <html><head/><body><p>Height of the Box.</p></body></html> + + + + + + + <html><head/><body><p>Width of the Box.</p></body></html> + + + + + + + Height + + + + + + + + + + + + + <html><head/><body><p>I checked the path is constrained by the solid. Otherwise the the volume of the solid describes a &quot;keep out&quot; zone.</p></body></html> + + + Constrained to Inside + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Gui::InputField + QLineEdit +
Gui/InputField.h
+
+
+ + stock + stockExisting + stockExtXneg + stockExtXpos + stockExtYneg + stockExtYpos + stockExtZneg + stockExtZpos + stockCylinderRadius + stockCylinderHeight + stockBoxLength + stockBoxWidth + stockBoxHeight + stockInside + + + +
diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index b6a8b613c842..83ff50ab2c83 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -82,7 +82,7 @@ def Initialize(self): threedopcmdlist = ["Path_Pocket_3D"] engravecmdlist = ["Path_Engrave", "Path_Deburr"] modcmdlist = ["Path_OperationCopy", "Path_Array", "Path_SimpleCopy" ] - dressupcmdlist = ["Path_DressupAxisMap", "Path_DressupDogbone", "Path_DressupDragKnife", "Path_DressupLeadInOut", "Path_DressupRampEntry", "Path_DressupTag"] + dressupcmdlist = ["Path_DressupAxisMap", "Path_DressupPathBoundary", "Path_DressupDogbone", "Path_DressupDragKnife", "Path_DressupLeadInOut", "Path_DressupRampEntry", "Path_DressupTag"] extracmdlist = [] #modcmdmore = ["Path_Hop",] #remotecmdlist = ["Path_Remote"] diff --git a/src/Mod/Path/PathScripts/PathDressupPathBoundary.py b/src/Mod/Path/PathScripts/PathDressupPathBoundary.py new file mode 100644 index 000000000000..64f144a4c3a6 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathDressupPathBoundary.py @@ -0,0 +1,199 @@ +# -*- 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 Path +import PathScripts.PathGeom as PathGeom +import PathScripts.PathLog as PathLog +import PathScripts.PathStock as PathStock +import PathScripts.PathUtil as PathUtil +import PathScripts.PathUtils as PathUtils + +from PySide import QtCore + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) + +def _vstr(v): + if v: + return "(%.2f, %.2f, %.2f)" % (v.x, v.y, v.z) + return '-' + +class DressupPathBoundary(object): + + def __init__(self, obj, base, job): + obj.addProperty("App::PropertyLink", "Base", "Base", QtCore.QT_TRANSLATE_NOOP("Path_DressupPathBoundary", "The base path to modify")) + obj.Base = base + obj.addProperty("App::PropertyLink", "Stock", "Boundary", QtCore.QT_TRANSLATE_NOOP("Path_DressupPathBoundary", "Solid object to be used to limit the generated Path.")) + obj.Stock = PathStock.CreateFromBase(job) + obj.addProperty("App::PropertyBool", "Inside", "Boundary", QtCore.QT_TRANSLATE_NOOP("Path_DressupPathBoundary", "Determines if Boundary describes an inclusion or exclusion mask.")) + obj.Inside = True + + self.obj = obj + self.safeHeight = None + self.clearanceHeight = None + + def __getstate__(self): + return None + def __setstate__(self, state): + return None + + def onDcoumentRestored(self, obj): + self.obj = obj + + def onDelete(self, obj, args): + if obj.Base: + job = PathUtils.findParentJob(obj) + job.Proxy.addOperation(obj.Base, obj) + if obj.Base.ViewObject: + obj.Base.ViewObject.Visibility = True + obj.Base = None + if obj.Stock: + obj.Document.removeObject(obj.Stock.Name) + obj.Stock = None + return True + + def boundaryCommands(self, obj, begin, end): + PathLog.track(_vstr(begin), _vstr(end)) + if end and PathGeom.pointsCoincide(begin, end): + return [] + cmds = [] + if begin.z < self.safeHeight: + cmds.append(Path.Command('G1', {'Z': self.safeHeight})) + if begin.z < self.clearanceHeight: + cmds.append(Path.Command('G0', {'Z': self.clearanceHeight})) + if end: + cmds.append(Path.Command('G0', {'X': end.x, 'Y': end.y})) + if end.z < self.clearanceHeight: + cmds.append(Path.Command('G0', {'Z': max(self.safeHeight, end.z)})) + if end.z < self.safeHeight: + cmds.append(Path.Command('G1', {'Z': end.z})) + return cmds + + def execute(self, obj): + if not obj.Base or not obj.Base.isDerivedFrom('Path::Feature') or not obj.Base.Path: + return + + if len(obj.Base.Path.Commands) > 0: + self.safeHeight = float(PathUtil.opProperty(obj.Base, 'SafeHeight')) + self.clearanceHeight = float(PathUtil.opProperty(obj.Base, 'ClearanceHeight')) + + boundary = obj.Stock.Shape + cmd = obj.Base.Path.Commands[0] + pos = cmd.Placement.Base + commands = [cmd] + lastExit = None + for cmd in obj.Base.Path.Commands[1:]: + if cmd.Name in PathGeom.CmdMoveAll: + edge = PathGeom.edgeForCmd(cmd, pos) + inside = edge.common(boundary).Edges + outside = edge.cut(boundary).Edges + if not obj.Inside: + t = inside + inside = outside + outside = t + # it's really a shame that one cannot trust the sequence and/or + # orientation of edges + if 1 == len(inside) and 0 == len(outside): + PathLog.track(_vstr(pos), _vstr(lastExit), ' + ', cmd) + # cmd fully included by boundary + if lastExit: + commands.extend(self.boundaryCommands(obj, lastExit, pos)) + lastExit = None + commands.append(cmd) + pos = PathGeom.commandEndPoint(cmd, pos) + elif 0 == len(inside) and 1 == len(outside): + PathLog.track(_vstr(pos), _vstr(lastExit), ' - ', cmd) + # cmd fully excluded by boundary + if not lastExit: + lastExit = pos + pos = PathGeom.commandEndPoint(cmd, pos) + else: + PathLog.track(_vstr(pos), _vstr(lastExit), len(inside), len(outside), cmd) + # cmd pierces boundary + while inside or outside: + ie = [e for e in inside if PathGeom.edgeConnectsTo(e, pos)] + PathLog.track(ie) + if ie: + e = ie[0] + ptL = e.valueAt(e.LastParameter) + flip = PathGeom.pointsCoincide(pos, ptL) + newPos = e.valueAt(e.FirstParameter) if flip else ptL + # inside edges are taken at this point (see swap of inside/outside + # above - so we can just connect the dots ... + if lastExit: + commands.extend(self.boundaryCommands(obj, lastExit, pos)) + lastExit = None + PathLog.track(e, flip) + commands.extend(PathGeom.cmdsForEdge(e, flip, False)) + inside.remove(e) + pos = newPos + lastExit = newPos + else: + oe = [e for e in outside if PathGeom.edgeConnectsTo(e, pos)] + PathLog.track(oe) + if oe: + e = oe[0] + ptL = e.valueAt(e.LastParameter) + flip = PathGeom.pointsCoincide(pos, ptL) + newPos = e.valueAt(e.FirstParameter) if flip else ptL + # outside edges are never taken at this point (see swap of + # inside/oustide above) - so just move along ... + outside.remove(e) + pos = newPos + else: + PathLog.error('huh?') + import Part + Part.show(Part.Vertex(pos), 'pos') + for e in inside: + Part.show(e, 'ei') + for e in outside: + Part.show(e, 'eo') + raise Exception('This is not supposed to happen') + #pos = PathGeom.commandEndPoint(cmd, pos) + else: + PathLog.track('no-move', cmd) + commands.append(cmd) + if lastExit: + commands.extend(self.boundaryCommands(obj, lastExit, None)) + lastExit = None + else: + PathLog.warning("No Path Commands for %s" % obj.Base.Label) + commands = [] + PathLog.track(commands) + obj.Path = Path.Path(commands) + + +def Create(base, name='DressupPathBoundary'): + '''Create(base, name='DressupPathBoundary') ... creates a dressup limiting base's Path to a boundary.''' + + if not base.isDerivedFrom('Path::Feature'): + PathLog.error(translate('Path_DressupPathBoundary', 'The selected object is not a path')+'\n') + return None + + obj = FreeCAD.ActiveDocument.addObject('Path::FeaturePython', name) + job = PathUtils.findParentJob(base) + obj.Proxy = DressupPathBoundary(obj, base, job) + job.Proxy.addOperation(obj, base, True) + return obj diff --git a/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py b/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py new file mode 100644 index 000000000000..1e17ffad0b99 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py @@ -0,0 +1,243 @@ +# -*- 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.PathDressupPathBoundary as PathDressupPathBoundary +import PathScripts.PathJobGui as PathJobGui +import PathScripts.PathLog as PathLog + +from PySide import QtGui, QtCore + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +# PathLog.trackModule() + + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + +class TaskPanel(object): + + def __init__(self, obj, viewProvider): + self.obj = obj + self.viewProvider = viewProvider + self.form = FreeCADGui.PySideUic.loadUi(':/panels/DressupPathBoundary.ui') + if obj.Stock: + self.visibilityBoundary = obj.Stock.ViewObject.Visibility + obj.Stock.ViewObject.Visibility = True + else: + self.visibilityBoundary = False + + self.stockFromBase = None + self.stockFromExisting = None + self.stockCreateBox = None + self.stockCreateCylinder = None + self.stockEdit = None + + def getStandardButtons(self): + return int(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Cancel) + + def clicked(self, button): + # callback for standard buttons + if button == QtGui.QDialogButtonBox.Apply: + self.obj.Proxy.execute(self.obj) + + def abort(self): + FreeCAD.ActiveDocument.abortTransaction() + self.cleanup(False) + + def reject(self): + FreeCAD.ActiveDocument.abortTransaction() + self.cleanup(True) + + def accept(self): + FreeCAD.ActiveDocument.commitTransaction() + self.cleanup(True) + #if self.isDirty: + # self.getFields() + # FreeCAD.ActiveDocument.recompute() + + def cleanup(self, gui): + self.viewProvider.clearTaskPanel() + if gui: + FreeCADGui.ActiveDocument.resetEdit() + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + if self.obj.Stock: + self.obj.Stock.ViewObject.Visibility = self.visibilityBoundary + + def getFields(self): + pass + def setFields(self): + pass + + def updateStockEditor(self, index, force=False): + import PathScripts.PathStock as PathStock + + def setupFromBaseEdit(): + PathLog.track(index, force) + if force or not self.stockFromBase: + self.stockFromBase = PathJobGui.StockFromBaseBoundBoxEdit(self.obj, self.form, force) + self.stockEdit = self.stockFromBase + + def setupCreateBoxEdit(): + PathLog.track(index, force) + if force or not self.stockCreateBox: + self.stockCreateBox = PathJobGui.StockCreateBoxEdit(self.obj, self.form, force) + self.stockEdit = self.stockCreateBox + + def setupCreateCylinderEdit(): + PathLog.track(index, force) + if force or not self.stockCreateCylinder: + self.stockCreateCylinder = PathJobGui.StockCreateCylinderEdit(self.obj, self.form, force) + self.stockEdit = self.stockCreateCylinder + + def setupFromExisting(): + PathLog.track(index, force) + if force or not self.stockFromExisting: + self.stockFromExisting = PathJobGui.StockFromExistingEdit(self.obj, self.form, force) + if self.stockFromExisting.candidates(self.obj): + self.stockEdit = self.stockFromExisting + return True + return False + + if index == -1: + if self.obj.Stock is None or PathJobGui.StockFromBaseBoundBoxEdit.IsStock(self.obj): + setupFromBaseEdit() + elif PathJobGui.StockCreateBoxEdit.IsStock(self.obj): + setupCreateBoxEdit() + elif PathJobGui.StockCreateCylinderEdit.IsStock(self.obj): + setupCreateCylinderEdit() + elif PathJobGui.StockFromExistingEdit.IsStock(self.obj): + setupFromExisting() + else: + PathLog.error(translate('PathJob', "Unsupported stock object %s") % self.obj.Stock.Label) + else: + if index == PathJobGui.StockFromBaseBoundBoxEdit.Index: + setupFromBaseEdit() + elif index == PathJobGui.StockCreateBoxEdit.Index: + setupCreateBoxEdit() + elif index == PathJobGui.StockCreateCylinderEdit.Index: + setupCreateCylinderEdit() + elif index == PathJobGui.StockFromExistingEdit.Index: + if not setupFromExisting(): + setupFromBaseEdit() + index = -1 + else: + PathLog.error(translate('PathJob', "Unsupported stock type %s (%d)") % (self.form.stock.currentText(), index)) + self.stockEdit.activate(self.obj, index == -1) + + def setupUi(self): + self.updateStockEditor(-1, False) + + self.form.stock.currentIndexChanged.connect(self.updateStockEditor) + + +class DressupPathBoundaryViewProvider(object): + + def __init__(self, vobj): + self.attach(vobj) + + def __getstate__(self): + return None + def __setstate_(self, state): + return None + + + def attach(self, vobj): + self.vobj = vobj + self.obj = vobj.Object + self.panel = None + + def claimChildren(self): + return [self.obj.Base, self.obj.Stock] + + def onDelete(self, vobj, args=None): + vobj.Object.Proxy.onDelete(vobj.Object, args) + return True + + def setEdit(self, vobj, mode=0): + panel = TaskPanel(vobj.Object, self) + self.setupTaskPanel(panel) + return True + + def unsetEdit(self, vobj, mode=0): + if self.panel: + self.panel.abort() + + def setupTaskPanel(self, panel): + self.panel = panel + FreeCADGui.Control.closeDialog() + FreeCADGui.Control.showDialog(panel) + panel.setupUi() + + def clearTaskPanel(self): + self.panel = None + + +def Create(base, name='DressupPathBoundary'): + FreeCAD.ActiveDocument.openTransaction(translate('Path_DressupPathBoundary', 'Create a Boundary dressup')) + obj = PathDressupPathBoundary.Create(base, name) + obj.ViewObject.Proxy = DressupPathBoundaryViewProvider(obj.ViewObject) + obj.Base.ViewObject.Visibility = False + obj.Stock.ViewObject.Visibility = False + FreeCAD.ActiveDocument.commitTransaction() + obj.ViewObject.Document.setEdit(obj.ViewObject, 0) + return obj + +class CommandPathDressupPathBoundary: + # pylint: disable=no-init + + def GetResources(self): + return {'Pixmap': 'Path-Dressup', + 'MenuText': QtCore.QT_TRANSLATE_NOOP('Path_DressupPathBoundary', 'Boundary Dress-up'), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP('Path_DressupPathBoundary', 'Creates a Path Boundary Dress-up object from a selected path')} + + def IsActive(self): + if FreeCAD.ActiveDocument is not None: + for o in FreeCAD.ActiveDocument.Objects: + if o.Name[:3] == 'Job': + return True + return False + + def Activated(self): + # check that the selection contains exactly what we want + selection = FreeCADGui.Selection.getSelection() + if len(selection) != 1: + PathLog.error(translate('Path_DressupPathBoundary', 'Please select one path object')+'\n') + return + baseObject = selection[0] + + # everything ok! + FreeCAD.ActiveDocument.openTransaction(translate('Path_DressupPathBoundary', 'Create Path Boundary Dress-up')) + FreeCADGui.addModule('PathScripts.PathDressupPathBoundaryGui') + FreeCADGui.doCommand("PathScripts.PathDressupPathBoundaryGui.Create(App.ActiveDocument.%s)" % baseObject.Name) + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.recompute() + +if FreeCAD.GuiUp: + # register the FreeCAD command + FreeCADGui.addCommand('Path_DressupPathBoundary', CommandPathDressupPathBoundary()) + +PathLog.notice('Loading PathDressupPathBoundaryGui... done\n') diff --git a/src/Mod/Path/PathScripts/PathGeom.py b/src/Mod/Path/PathScripts/PathGeom.py index 6f73304c1572..0415b9238eba 100644 --- a/src/Mod/Path/PathScripts/PathGeom.py +++ b/src/Mod/Path/PathScripts/PathGeom.py @@ -87,6 +87,7 @@ def of(cls, ptRef, pt): CmdMoveCCW = ['G3', 'G03'] CmdMoveArc = CmdMoveCW + CmdMoveCCW CmdMove = CmdMoveStraight + CmdMoveArc +CmdMoveAll = CmdMove + CmdMoveRapid def isRoughly(float1, float2, error=Tolerance): """isRoughly(float1, float2, [error=Tolerance]) diff --git a/src/Mod/Path/PathScripts/PathGuiInit.py b/src/Mod/Path/PathScripts/PathGuiInit.py index da66403398bc..57f302fc3a28 100644 --- a/src/Mod/Path/PathScripts/PathGuiInit.py +++ b/src/Mod/Path/PathScripts/PathGuiInit.py @@ -48,6 +48,7 @@ def Startup(): from PathScripts import PathDressupDogbone from PathScripts import PathDressupDragknife from PathScripts import PathDressupRampEntry + from PathScripts import PathDressupPathBoundaryGui from PathScripts import PathDressupTagGui from PathScripts import PathDressupLeadInOut from PathScripts import PathDrillingGui diff --git a/src/Mod/Path/PathScripts/PathJob.py b/src/Mod/Path/PathScripts/PathJob.py index 9d559d6f0965..509bb7d72b21 100644 --- a/src/Mod/Path/PathScripts/PathJob.py +++ b/src/Mod/Path/PathScripts/PathJob.py @@ -344,12 +344,14 @@ def __setstate__(self, state): def execute(self, obj): obj.Path = obj.Operations.Path - def addOperation(self, op, before = None): + def addOperation(self, op, before = None, removeBefore = False): group = self.obj.Operations.Group if op not in group: if before: try: group.insert(group.index(before), op) + if removeBefore: + group.remove(before) except Exception as e: # pylint: disable=broad-except PathLog.error(e) group.append(op) diff --git a/src/Mod/Path/PathScripts/PathJobGui.py b/src/Mod/Path/PathScripts/PathJobGui.py index 2c6107d7115b..186b2066bd5e 100644 --- a/src/Mod/Path/PathScripts/PathJobGui.py +++ b/src/Mod/Path/PathScripts/PathJobGui.py @@ -527,12 +527,16 @@ def getFields(self, obj): def candidates(self, obj): solids = [o for o in obj.Document.Objects if PathUtil.isSolid(o)] - for base in obj.Model.Group: - if base in solids and PathJob.isResourceClone(obj, base, 'Model'): + if hasattr(obj, 'Model'): + job = obj + else: + job = PathUtils.findParentJob(obj) + for base in job.Model.Group: + if base in solids and PathJob.isResourceClone(job, base, 'Model'): solids.remove(base) - if obj.Stock in solids: + if job.Stock in solids: # regardless, what stock is/was, it's not a valid choice - solids.remove(obj.Stock) + solids.remove(job.Stock) return sorted(solids, key=lambda c: c.Label) def setFields(self, obj): diff --git a/src/Mod/Path/PathScripts/PathStock.py b/src/Mod/Path/PathScripts/PathStock.py index 50e4e96e438f..7cabb4f7ff94 100644 --- a/src/Mod/Path/PathScripts/PathStock.py +++ b/src/Mod/Path/PathScripts/PathStock.py @@ -233,9 +233,22 @@ def SetupStockObject(obj, stockType): obj.ViewObject.Transparency = 90 obj.ViewObject.DisplayMode = 'Wireframe' +class FakeJob(object): + def __init__(self, base): + self.Group = [base] + +def _getBase(job): + if job and hasattr(job, 'Model'): + return job.Model + if job: + import PathScripts.PathUtils as PathUtils + job = PathUtils.findParentJob(job) + return job.Model if job else None + return None + def CreateFromBase(job, neg=None, pos=None, placement=None): PathLog.track(job.Label, neg, pos, placement) - base = job.Model if job else None + base = _getBase(job) obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', 'Stock') obj.Proxy = StockFromBase(obj, base) @@ -258,7 +271,7 @@ def CreateFromBase(job, neg=None, pos=None, placement=None): return obj def CreateBox(job, extent=None, placement=None): - base = job.Model if job else None + base = _getBase(job) obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', 'Stock') obj.Proxy = StockCreateBox(obj) @@ -283,7 +296,7 @@ def CreateBox(job, extent=None, placement=None): return obj def CreateCylinder(job, radius=None, height=None, placement=None): - base = job.Model if job else None + base = _getBase(job) obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', 'Stock') obj.Proxy = StockCreateCylinder(obj) diff --git a/src/Mod/Path/PathScripts/PathUtil.py b/src/Mod/Path/PathScripts/PathUtil.py index d040ce2295bb..3fa449106cb5 100644 --- a/src/Mod/Path/PathScripts/PathUtil.py +++ b/src/Mod/Path/PathScripts/PathUtil.py @@ -68,15 +68,19 @@ def isSolid(obj): shape = Part.getShape(obj) return not shape.isNull() and shape.Volume and shape.isClosed() +def opProperty(op, prop): + '''opProperty(op, prop) ... return the value of property prop of the underlying operation (or None if prop does not exist)''' + if hasattr(op, prop): + return getattr(op, prop) + if hasattr(op, 'Base'): + return opProperty(op.Base, prop) + return None + def toolControllerForOp(op): '''toolControllerForOp(op) ... return the tool controller used by the op. If the op doesn't have its own tool controller but has a Base object, return its tool controller. Otherwise return None.''' - if hasattr(op, 'ToolController'): - return op.ToolController - if hasattr(op, 'Base'): - return toolControllerForOp(op.Base) - return None + return opProperty(op, 'ToolController') def getPublicObject(obj): '''getPublicObject(obj) ... returns the object which should be used to reference a feature of the given object.'''