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 "keep out" zone.</p></body></html>
+
+
+ Constrained to Inside
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ Gui::InputField
+ QLineEdit
+
+
+
+
+ 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.'''