diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt
index ddfcaa519b..6b78b5ea38 100644
--- a/src/Mod/Path/CMakeLists.txt
+++ b/src/Mod/Path/CMakeLists.txt
@@ -55,6 +55,8 @@ SET(PathScripts_SRCS
PathScripts/PathEngrave.py
PathScripts/PathEngraveBase.py
PathScripts/PathEngraveGui.py
+ PathScripts/PathFeatureExtensions.py
+ PathScripts/PathFeatureExtensionsGui.py
PathScripts/PathFixture.py
PathScripts/PathGeom.py
PathScripts/PathGetPoint.py
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpPocketExtEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpPocketExtEdit.ui
index 6341e33bdd..19a00cb96a 100644
--- a/src/Mod/Path/Gui/Resources/panels/PageOpPocketExtEdit.ui
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpPocketExtEdit.ui
@@ -14,36 +14,62 @@
Form
+ -
+
+
-
+
+
+ Click to disable Extensions
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Ignore Edges and Wires
+
+
+ true
+
+
+ true
+
+
+
+
+
+ -
+
+
+ ---
+
+
+
-
-
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
0
-
-
-
-
-
- Default Length
-
-
-
- -
-
-
- <html><head/><body><p>Set the extent of the dimension -the default value is half the tool diameter.</p></body></html>
-
-
- -999999999.000000000000000
-
-
- 999999999.000000000000000
-
-
-
- -
+
-
<html><head/><body><p>Extend the corner between two edges of a pocket. If selected adjacent edges are combined.</p></body></html>
@@ -56,7 +82,27 @@
- -
+
-
+
+
+ <html><head/><body><p>Set the extent of the dimension -the default value is half the tool diameter.</p></body></html>
+
+
+ -999999999.000000000000000
+
+
+ 999999999.000000000000000
+
+
+
+ -
+
+
+ Default Length
+
+
+
+ -
<html><head/><body><p>If selected all potential extensions are visualised. Enabled extensions in purple and not enabled extensions in yellow.</p></body></html>
diff --git a/src/Mod/Path/PathScripts/PathAdaptive.py b/src/Mod/Path/PathScripts/PathAdaptive.py
index 18eed0ae14..06cc36774f 100644
--- a/src/Mod/Path/PathScripts/PathAdaptive.py
+++ b/src/Mod/Path/PathScripts/PathAdaptive.py
@@ -24,24 +24,41 @@
import PathScripts.PathOp as PathOp
import PathScripts.PathUtils as PathUtils
+import PathScripts.PathLog as PathLog
+import PathScripts.PathGeom as PathGeom
import Path
import FreeCAD
-import FreeCADGui
-from FreeCAD import Console
import time
import json
import math
import area
-from pivy import coin
+
+from PySide import QtCore
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
Part = LazyLoader('Part', globals(), 'Part')
-TechDraw = LazyLoader('TechDraw', globals(), 'TechDraw')
+# TechDraw = LazyLoader('TechDraw', globals(), 'TechDraw')
+FeatureExtensions = LazyLoader('PathScripts.PathFeatureExtensions',
+ globals(),
+ 'PathScripts.PathFeatureExtensions')
+
+if FreeCAD.GuiUp:
+ from pivy import coin
+ import FreeCADGui
__doc__ = "Class and implementation of the Adaptive path operation."
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+# PathLog.trackModule(PathLog.thisModule())
+
+
+# Qt translation handling
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+
def convertTo2d(pathArray):
output = []
for path in pathArray:
@@ -386,22 +403,25 @@ def Execute(op, obj):
global sceneGraph
global topZ
- sceneGraph = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
+ if FreeCAD.GuiUp:
+ sceneGraph = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
- Console.PrintMessage("*** Adaptive toolpath processing started...\n")
+ PathLog.info("*** Adaptive toolpath processing started...\n")
# hide old toolpaths during recalculation
obj.Path = Path.Path("(Calculating...)")
- # store old visibility state
- job = op.getJob(obj)
- oldObjVisibility = obj.ViewObject.Visibility
- oldJobVisibility = job.ViewObject.Visibility
+ if FreeCAD.GuiUp:
+ #store old visibility state
+ job = op.getJob(obj)
+ oldObjVisibility = obj.ViewObject.Visibility
+ oldJobVisibility = job.ViewObject.Visibility
- obj.ViewObject.Visibility = False
- job.ViewObject.Visibility = False
+ obj.ViewObject.Visibility = False
+ job.ViewObject.Visibility = False
+
+ FreeCADGui.updateGui()
- FreeCADGui.updateGui()
try:
helixDiameter = obj.HelixDiameterLimit.Value
topZ = op.stock.Shape.BoundBox.ZMax
@@ -411,12 +431,15 @@ def Execute(op, obj):
obj.Tolerance = 0.001
# Get list of working edges for adaptive algorithm
- pathArray = _get_working_edges(op, obj)
+ pathArray = op.pathArray
+ if not pathArray:
+ PathLog.error("No wire data returned.")
+ return
path2d = convertTo2d(pathArray)
stockPaths = []
- if op.stock.StockType == "CreateCylinder":
+ if hasattr(op.stock, "StockType") and op.stock.StockType == "CreateCylinder":
stockPaths.append([discretize(op.stock.Shape.Edges[0])])
else:
@@ -479,14 +502,15 @@ def Execute(op, obj):
# progress callback fn, if return true it will stop processing
def progressFn(tpaths):
- for path in tpaths: # path[0] contains the MotionType, #path[1] contains list of points
- if path[0] == area.AdaptiveMotionType.Cutting:
- sceneDrawPath(path[1], (0, 0, 1))
+ if FreeCAD.GuiUp:
+ for path in tpaths: #path[0] contains the MotionType, #path[1] contains list of points
+ if path[0] == area.AdaptiveMotionType.Cutting:
+ sceneDrawPath(path[1],(0,0,1))
- else:
- sceneDrawPath(path[1], (1, 0, 1))
+ else:
+ sceneDrawPath(path[1],(1,0,1))
- FreeCADGui.updateGui()
+ FreeCADGui.updateGui()
return obj.StopProcessing
@@ -520,17 +544,18 @@ def Execute(op, obj):
GenerateGCode(op, obj, adaptiveResults, helixDiameter)
if not obj.StopProcessing:
- Console.PrintMessage("*** Done. Elapsed time: %f sec\n\n" % (time.time()-start))
+ PathLog.info("*** Done. Elapsed time: %f sec\n\n" % (time.time()-start))
obj.AdaptiveOutputState = adaptiveResults
obj.AdaptiveInputState = inputStateObject
else:
- Console.PrintMessage("*** Processing cancelled (after: %f sec).\n\n" % (time.time()-start))
+ PathLog.info("*** Processing cancelled (after: %f sec).\n\n" % (time.time()-start))
finally:
- obj.ViewObject.Visibility = oldObjVisibility
- job.ViewObject.Visibility = oldJobVisibility
- sceneClean()
+ if FreeCAD.GuiUp:
+ obj.ViewObject.Visibility = oldObjVisibility
+ job.ViewObject.Visibility = oldJobVisibility
+ sceneClean()
def _get_working_edges(op, obj):
@@ -540,26 +565,52 @@ def _get_working_edges(op, obj):
Additional modifications to selected region(face), such as extensions,
should be placed within this function.
"""
- pathArray = list()
+ regions = list()
+ all_regions = list()
+ edge_list = list()
+ avoidFeatures = list()
+ # Get extensions and identify faces to avoid
+ extensions = FeatureExtensions.getExtensions(obj)
+ for e in extensions:
+ if e.avoid:
+ avoidFeatures.append(e.feature)
+
+ # Get faces selected by user
for base, subs in obj.Base:
for sub in subs:
- if obj.UseOutline:
- face = base.Shape.getElement(sub)
- zmin = face.BoundBox.ZMin
- # get face outline with same method in PocketShape
- wire = TechDraw.findShapeOutline(face, 1, FreeCAD.Vector(0.0, 0.0, 1.0))
- shape = Part.Face(wire)
- # translate to face height if necessary
- if shape.BoundBox.ZMin != zmin:
- shape.translate(FreeCAD.Vector(0.0, 0.0, zmin - shape.BoundBox.ZMin))
- else:
- shape = base.Shape.getElement(sub)
+ if sub not in avoidFeatures:
+ if obj.UseOutline:
+ face = base.Shape.getElement(sub)
+ # get outline with wire_A method used in PocketShape, but it does not play nicely later
+ # wire_A = TechDraw.findShapeOutline(face, 1, FreeCAD.Vector(0.0, 0.0, 1.0))
+ wire_B = face.Wires[0]
+ shape = Part.Face(wire_B)
+ else:
+ shape = base.Shape.getElement(sub)
+ regions.append(shape)
+ # Efor
- for edge in shape.Edges:
- pathArray.append([discretize(edge)])
+ # Return Extend Outline extension, OR regular edge extension
+ all_regions = regions
+ # Apply regular Extensions
+ op.exts = [] # pylint: disable=attribute-defined-outside-init
+ for ext in extensions:
+ if not ext.avoid:
+ wire = ext.getWire()
+ if wire:
+ for f in ext.getExtensionFaces(wire):
+ op.exts.append(f)
+ all_regions.append(f)
- return pathArray
+ # Second face-combining method attempted
+ horizontal = PathGeom.combineHorizontalFaces(all_regions)
+ for f in horizontal:
+ for w in f.Wires:
+ for e in w.Edges:
+ edge_list.append([discretize(e)])
+
+ return edge_list
class PathAdaptive(PathOp.ObjectOp):
@@ -567,7 +618,9 @@ class PathAdaptive(PathOp.ObjectOp):
'''opFeatures(obj) ... returns the OR'ed list of features used and supported by the operation.
The default implementation returns "FeatureTool | FeatureDepths | FeatureHeights | FeatureStartPoint"
Should be overwritten by subclasses.'''
- return PathOp.FeatureTool | PathOp.FeatureBaseEdges | PathOp.FeatureDepths | PathOp.FeatureFinishDepth | PathOp.FeatureStepDown | PathOp.FeatureHeights | PathOp.FeatureBaseGeometry | PathOp.FeatureCoolant
+ return PathOp.FeatureTool | PathOp.FeatureBaseEdges | PathOp.FeatureDepths \
+ | PathOp.FeatureFinishDepth | PathOp.FeatureStepDown | PathOp.FeatureHeights \
+ | PathOp.FeatureBaseGeometry | PathOp.FeatureCoolant | PathOp.FeatureLocations
def initOperation(self, obj):
'''initOperation(obj) ... implement to create additional properties.
@@ -609,6 +662,8 @@ class PathAdaptive(PathOp.ObjectOp):
obj.addProperty("App::PropertyBool", "UseOutline", "Adaptive", "Uses the outline of the base geometry.")
+ FeatureExtensions.initialize_properties(obj)
+
def opSetDefaultValues(self, obj, job):
obj.Side = "Inside"
obj.OperationType = "Clearing"
@@ -629,11 +684,14 @@ class PathAdaptive(PathOp.ObjectOp):
obj.KeepToolDownRatio = 3.0
obj.UseHelixArcs = False
obj.UseOutline = False
+ FeatureExtensions.set_default_property_values(obj, job)
def opExecute(self, obj):
'''opExecute(obj) ... called whenever the receiver needs to be recalculated.
See documentation of execute() for a list of base functionality provided.
Should be overwritten by subclasses.'''
+
+ self.pathArray = _get_working_edges(self, obj)
Execute(self, obj)
def opOnDocumentRestored(self, obj):
@@ -645,6 +703,17 @@ class PathAdaptive(PathOp.ObjectOp):
"UseOutline",
"Adaptive",
"Uses the outline of the base geometry.")
+ FeatureExtensions.initialize_properties(obj)
+
+
+def SetupProperties():
+ setup = ["Side", "OperationType", "Tolerance", "StepOver",
+ "LiftDistance", "KeepToolDownRatio", "StockToLeave",
+ "ForceInsideOut", "FinishingProfile", "Stopped",
+ "StopProcessing", "UseHelixArcs", "AdaptiveInputState",
+ "AdaptiveOutputState", "HelixAngle", "HelixConeAngle",
+ "HelixDiameterLimit", "UseOutline"]
+ return setup
def Create(name, obj=None):
diff --git a/src/Mod/Path/PathScripts/PathAdaptiveGui.py b/src/Mod/Path/PathScripts/PathAdaptiveGui.py
index 372c4c081e..daa2335926 100644
--- a/src/Mod/Path/PathScripts/PathAdaptiveGui.py
+++ b/src/Mod/Path/PathScripts/PathAdaptiveGui.py
@@ -24,6 +24,7 @@
import PathScripts.PathOpGui as PathOpGui
from PySide import QtCore, QtGui
import PathScripts.PathAdaptive as PathAdaptive
+import PathScripts.PathFeatureExtensionsGui as PathFeatureExtensionsGui
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
@@ -241,11 +242,16 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
obj.setEditorMode('StopProcessing', 2) # hide this property
obj.setEditorMode('Stopped', 2) # hide this property
+ def taskPanelBaseLocationPage(self, obj, features):
+ if not hasattr(self, 'extensionsPanel'):
+ self.extensionsPanel = PathFeatureExtensionsGui.TaskPanelExtensionPage(obj, features) # pylint: disable=attribute-defined-outside-init
+ return self.extensionsPanel
+
Command = PathOpGui.SetupOperation('Adaptive',
PathAdaptive.Create,
TaskPanelOpPage,
'Path_Adaptive',
QtCore.QT_TRANSLATE_NOOP("Path_Adaptive", "Adaptive"),
- QtCore.QT_TRANSLATE_NOOP("Path_Adaptive", "Adaptive clearing and profiling")
- )
+ QtCore.QT_TRANSLATE_NOOP("Path_Adaptive", "Adaptive clearing and profiling"),
+ PathAdaptive.SetupProperties)
diff --git a/src/Mod/Path/PathScripts/PathFeatureExtensions.py b/src/Mod/Path/PathScripts/PathFeatureExtensions.py
new file mode 100644
index 0000000000..6422f89e3f
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathFeatureExtensions.py
@@ -0,0 +1,612 @@
+# -*- 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 PathScripts.PathGeom as PathGeom
+import PathScripts.PathLog as PathLog
+import Part
+import math
+
+# lazily loaded modules
+from lazy_loader.lazy_loader import LazyLoader
+PathUtils = LazyLoader('PathScripts.PathUtils', globals(), 'PathScripts.PathUtils')
+
+from PySide import QtCore
+
+__title__ = "Path Features Extensions"
+__author__ = "sliptonic (Brad Collette)"
+__url__ = "https://www.freecadweb.org"
+__doc__ = "Class and implementation of face extensions features."
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+# PathLog.trackModule(PathLog.thisModule())
+
+
+# Qt translation handling
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+
+def endPoints(edgeOrWire):
+ '''endPoints(edgeOrWire) ... return the first and last point of the wire or the edge, assuming the argument is not a closed wire.'''
+ if Part.Wire == type(edgeOrWire):
+ # edges = edgeOrWire.Edges
+ pts = [e.valueAt(e.FirstParameter) for e in edgeOrWire.Edges]
+ pts.extend([e.valueAt(e.LastParameter) for e in edgeOrWire.Edges])
+ unique = []
+ for p in pts:
+ cnt = len([p2 for p2 in pts if PathGeom.pointsCoincide(p, p2)])
+ if 1 == cnt:
+ unique.append(p)
+
+ return unique
+
+ pfirst = edgeOrWire.valueAt(edgeOrWire.FirstParameter)
+ plast = edgeOrWire.valueAt(edgeOrWire.LastParameter)
+ if PathGeom.pointsCoincide(pfirst, plast):
+ return None
+
+ return [pfirst, plast]
+
+
+def includesPoint(p, pts):
+ '''includesPoint(p, pts) ... answer True if the collection of pts includes the point p'''
+ for pt in pts:
+ if PathGeom.pointsCoincide(p, pt):
+ return True
+
+ return False
+
+
+def selectOffsetWire(feature, wires):
+ '''selectOffsetWire(feature, wires) ... returns the Wire in wires which is does not intersect with feature'''
+ closest = None
+ for w in wires:
+ dist = feature.distToShape(w)[0]
+ if closest is None or dist > closest[0]: # pylint: disable=unsubscriptable-object
+ closest = (dist, w)
+
+ if closest is not None:
+ return closest[1]
+
+ return None
+
+
+def extendWire(feature, wire, length):
+ '''extendWire(wire, length) ... return a closed Wire which extends wire by length'''
+ PathLog.track(length)
+
+ if not length or length == 0:
+ return None
+
+ try:
+ off2D = wire.makeOffset2D(length)
+ except FreeCAD.Base.FreeCADError as ee:
+ return None
+ endPts = endPoints(wire) # Asumes wire is NOT closed
+ if endPts:
+ edges = [e for e in off2D.Edges if Part.Circle != type(e.Curve) or not includesPoint(e.Curve.Center, endPts)]
+ wires = [Part.Wire(e) for e in Part.sortEdges(edges)]
+ offset = selectOffsetWire(feature, wires)
+ ePts = endPoints(offset)
+ if ePts and len(ePts) > 1:
+ l0 = (ePts[0] - endPts[0]).Length
+ l1 = (ePts[1] - endPts[0]).Length
+ edges = wire.Edges
+ if l0 < l1:
+ edges.append(Part.Edge(Part.LineSegment(endPts[0], ePts[0])))
+ edges.extend(offset.Edges)
+ edges.append(Part.Edge(Part.LineSegment(endPts[1], ePts[1])))
+ else:
+ edges.append(Part.Edge(Part.LineSegment(endPts[1], ePts[0])))
+ edges.extend(offset.Edges)
+ edges.append(Part.Edge(Part.LineSegment(endPts[0], ePts[1])))
+
+ return Part.Wire(edges)
+
+ return None
+
+
+def createExtension(obj, extObj, extFeature, extSub):
+ return Extension(obj,
+ extObj,
+ extFeature,
+ extSub,
+ obj.ExtensionLengthDefault,
+ Extension.DirectionNormal)
+
+
+def readObjExtensionFeature(obj):
+ """readObjExtensionFeature(obj)...
+ Return three item string tuples (base name, feature, subfeature) extracted from obj.ExtensionFeature"""
+ extensions = []
+
+ for extObj, features in obj.ExtensionFeature:
+ for sub in features:
+ extFeature, extSub = sub.split(':')
+ extensions.append((extObj.Name, extFeature, extSub))
+ return extensions
+
+
+def getExtensions(obj):
+ PathLog.debug("getExtenstions()")
+ extensions = []
+ i = 0
+
+ for extObj, features in obj.ExtensionFeature:
+ for sub in features:
+ extFeature, extSub = sub.split(':')
+ extensions.append(createExtension(obj, extObj, extFeature, extSub))
+ i = i + 1
+ return extensions
+
+
+def setExtensions(obj, extensions):
+ PathLog.track(obj.Label, len(extensions))
+ obj.ExtensionFeature = [(ext.obj, ext.getSubLink()) for ext in extensions]
+
+
+class Extension(object):
+ DirectionNormal = 0
+ DirectionX = 1
+ DirectionY = 2
+
+ def __init__(self, op, obj, feature, sub, length, direction):
+ PathLog.debug("Extension(%s, %s, %s, %.2f, %s" % (obj.Label, feature, sub, length, direction))
+ self.op = op
+ self.obj = obj
+ self.feature = feature
+ self.sub = sub
+ self.length = length
+ self.direction = direction
+ self.extFaces = None
+ self.isDebug = True if PathLog.getLevel(PathLog.thisModule()) == 4 else False
+
+ self.avoid = False
+ if sub.startswith("Avoid_"):
+ self.avoid = True
+
+ self.wire = None
+
+ def getSubLink(self):
+ return "%s:%s" % (self.feature, self.sub)
+
+ def _extendEdge(self, feature, e0, direction):
+ PathLog.track(feature, e0, direction)
+ if isinstance(e0.Curve, Part.Line) or isinstance(e0.Curve, Part.LineSegment):
+ e2 = e0.copy()
+ off = self.length.Value * direction
+ e2.translate(off)
+ e2 = PathGeom.flipEdge(e2)
+ e1 = Part.Edge(Part.LineSegment(e0.valueAt(e0.LastParameter), e2.valueAt(e2.FirstParameter)))
+ e3 = Part.Edge(Part.LineSegment(e2.valueAt(e2.LastParameter), e0.valueAt(e0.FirstParameter)))
+ wire = Part.Wire([e0, e1, e2, e3])
+ self.wire = wire
+ return wire
+
+ return extendWire(feature, Part.Wire([e0]), self.length.Value)
+
+ def _getEdgeNumbers(self):
+ if 'Wire' in self.sub:
+ numbers = [nr for nr in self.sub[5:-1].split(',')]
+ else:
+ numbers = [self.sub[4:]]
+
+ PathLog.debug("_getEdgeNumbers() -> %s" % numbers)
+ return numbers
+
+ def _getEdgeNames(self):
+ return ["Edge%s" % nr for nr in self._getEdgeNumbers()]
+
+ def _getEdges(self):
+ return [self.obj.Shape.getElement(sub) for sub in self._getEdgeNames()]
+
+ def _getDirectedNormal(self, p0, normal):
+ poffPlus = p0 + 0.01 * normal
+ poffMinus = p0 - 0.01 * normal
+ if not self.obj.Shape.isInside(poffPlus, 0.005, True):
+ return normal
+
+ if not self.obj.Shape.isInside(poffMinus, 0.005, True):
+ return normal.negative()
+
+ return None
+
+ def _getDirection(self, wire):
+ e0 = wire.Edges[0]
+ midparam = e0.FirstParameter + 0.5 * (e0.LastParameter - e0.FirstParameter)
+ tangent = e0.tangentAt(midparam)
+ PathLog.track('tangent', tangent, self.feature, self.sub)
+ normal = tangent.cross(FreeCAD.Vector(0, 0, 1))
+ if PathGeom.pointsCoincide(normal, FreeCAD.Vector(0, 0, 0)):
+ return None
+
+ return self._getDirectedNormal(e0.valueAt(midparam), normal.normalize())
+
+ def getExtensionFaces(self, extensionWire):
+ '''getExtensionFace(extensionWire)...
+ A public helper method to retrieve the requested extension as a face,
+ rather than a wire becuase some extensions require a face shape
+ for definition that allows for two wires for boundary definition.
+ '''
+
+ if self.extFaces:
+ return self.extFaces
+
+ return [Part.Face(extensionWire)]
+
+ def getWire(self):
+ '''getWire()... Public method to retrieve the extension area, pertaining to the feature
+ and sub element provided at class instantiation, as a closed wire. If no closed wire
+ is possible, a `None` value is returned.'''
+
+ if self.sub[:6] == "Avoid_":
+ feature = self.obj.Shape.getElement(self.feature)
+ self.extFaces = [Part.Face(feature.Wires[0])]
+ return feature.Wires[0]
+ if self.sub[:7] == "Extend_":
+ rtn = self._getOutlineWire()
+ if rtn:
+ return rtn
+ else:
+ PathLog.debug("no Outline Wire")
+ return None
+ if self.sub[:10] == "Waterline_":
+ rtn = self._getWaterlineWire()
+ if rtn:
+ return rtn
+ else:
+ PathLog.debug("no Waterline Wire")
+ return None
+ else:
+ return self._getRegularWire()
+
+ def _getRegularWire(self):
+ '''_getRegularWire()... Private method to retrieve the extension area, pertaining to the feature
+ and sub element provided at class instantiation, as a closed wire. If no closed wire
+ is possible, a `None` value is returned.'''
+ PathLog.track()
+
+ length = self.length.Value
+ if PathGeom.isRoughly(0, length) or not self.sub:
+ PathLog.debug("no extension, length=%.2f, sub=%s" % (length, self.sub))
+ return None
+
+ feature = self.obj.Shape.getElement(self.feature)
+ edges = self._getEdges()
+ sub = Part.Wire(Part.sortEdges(edges)[0])
+
+ if 1 == len(edges):
+ PathLog.debug("Extending single edge wire")
+ edge = edges[0]
+ if Part.Circle == type(edge.Curve):
+ circle = edge.Curve
+ # for a circle we have to figure out if it's a hole or a cylinder
+ p0 = edge.valueAt(edge.FirstParameter)
+ normal = (edge.Curve.Center - p0).normalize()
+ direction = self._getDirectedNormal(p0, normal)
+ if direction is None:
+ return None
+
+ if PathGeom.pointsCoincide(normal, direction):
+ r = circle.Radius - length
+ else:
+ r = circle.Radius + length
+
+ # assuming the offset produces a valid circle - go for it
+ if r > 0:
+ e3 = Part.makeCircle(r, circle.Center, circle.Axis, edge.FirstParameter * 180 / math.pi, edge.LastParameter * 180 / math.pi)
+ if endPoints(edge):
+ # need to construct the arc slice
+ e0 = Part.makeLine(edge.valueAt(edge.FirstParameter), e3.valueAt(e3.FirstParameter))
+ e2 = Part.makeLine(edge.valueAt(edge.LastParameter), e3.valueAt(e3.LastParameter))
+ return Part.Wire([e0, edge, e2, e3])
+
+ extWire = Part.Wire([e3])
+ self.extFaces = [self._makeCircularExtFace(edge, extWire)]
+ return extWire
+
+ # the extension is bigger than the hole - so let's just cover the whole hole
+ if endPoints(edge):
+ # if the resulting arc is smaller than the radius, create a pie slice
+ PathLog.track()
+ center = circle.Center
+ e0 = Part.makeLine(center, edge.valueAt(edge.FirstParameter))
+ e2 = Part.makeLine(edge.valueAt(edge.LastParameter), center)
+ return Part.Wire([e0, edge, e2])
+
+ PathLog.track()
+ return Part.Wire([edge])
+
+ else:
+ PathLog.track(self.feature, self.sub, type(edge.Curve), endPoints(edge))
+ direction = self._getDirection(sub)
+ if direction is None:
+ return None
+
+ # return self._extendEdge(feature, edge, direction)
+ return self._extendEdge(feature, edges[0], direction)
+ elif sub.isClosed():
+ PathLog.debug("Extending multi-edge closed wire")
+ subFace = Part.Face(sub)
+ featFace = Part.Face(feature.Wires[0])
+ isOutside = True
+ if not PathGeom.isRoughly(featFace.Area, subFace.Area):
+ length = -1.0 * length
+ isOutside = False
+
+ try:
+ off2D = sub.makeOffset2D(length)
+ except FreeCAD.Base.FreeCADError as ee:
+ return None
+
+ if isOutside:
+ self.extFaces = [Part.Face(off2D).cut(featFace)]
+ else:
+ self.extFaces = [subFace.cut(Part.Face(off2D))]
+ return off2D
+
+ PathLog.debug("Extending multi-edge open wire")
+ return extendWire(feature, sub, length)
+
+ def _getOutlineWire(self):
+ '''_getOutlineWire()... Private method to retrieve an extended outline extension area,
+ pertaining to the feature and sub element provided at class instantiation, as a closed wire.
+ If no closed wire is possible, a `None` value is returned.'''
+ PathLog.track()
+
+ baseShape = self.obj.Shape
+ face = baseShape.getElement(self.feature)
+ useOutline = False
+ msg = translate("PathAdaptive", "Extend Outline error")
+
+ if hasattr(self.op, "UseOutline"):
+ useOutline = self.op.UseOutline
+
+ if useOutline:
+ outFace = Part.Face(face.Wires[0])
+ rawFace = getExtendOutlineFace(baseShape, outFace, self.length)
+
+ if rawFace:
+ extFace = rawFace.cut(outFace)
+ else:
+ PathLog.error(msg + " 1")
+ extFace = outFace
+ else:
+ rawFace = getExtendOutlineFace(baseShape, face, self.length)
+
+ if rawFace:
+ extFace = rawFace.cut(face)
+ else:
+ PathLog.error(msg + " 2")
+ extFace = face
+
+ # Debug
+ # Part.show(extFace)
+ # FreeCAD.ActiveDocument.ActiveObject.Label = "outline_wire"
+
+ if len(extFace.Wires) > 0:
+ self.extFaces = [f for f in extFace.Faces]
+ return extFace.Wires[0]
+
+ return None
+
+ def _getWaterlineWire(self):
+ '''_getWaterlineWire()... Private method to retrieve a waterline extension area,
+ pertaining to the feature and sub element provided at class instantiation, as a closed wire.
+ Only waterline faces touching source face are returned as part of the waterline extension area.
+ If no closed wire is possible, a `None` value is returned.'''
+ PathLog.track()
+
+ msg = translate("PathFeatureExtensions", "Waterline error")
+ useOutline = False
+ if hasattr(self.op, "UseOutline"):
+ useOutline = self.op.UseOutline
+
+ baseShape = self.obj.Shape
+ if useOutline:
+ face = Part.Face(baseShape.getElement(self.feature).Wire1)
+ else:
+ face = baseShape.getElement(self.feature)
+
+ rawFace = getWaterlineFace(baseShape, face)
+
+ if not rawFace:
+ PathLog.error(msg + " 1")
+ return None
+
+ if rawFace:
+ extFace = rawFace.cut(face)
+ else:
+ PathLog.error(msg + " 2")
+ extFace = face
+
+ # Debug
+ # Part.show(extFace)
+ # FreeCAD.ActiveDocument.ActiveObject.Label = "waterline_face"
+
+ if len(extFace.Wires) > 0:
+ self.extFaces = [f for f in extFace.Faces]
+ return extFace.Wires[0]
+
+ return None
+
+ def _makeCircularExtFace(self, edge, extWire):
+ '''_makeCircularExtensionFace(edge, extWire)...
+ Create proper circular extension face shape. Incoming edge is expected to be a circle.
+ '''
+ # Add original outer wire to cut faces if necessary
+ edgeFace = Part.Face(Part.Wire([edge]))
+ edgeFace.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - edgeFace.BoundBox.ZMin))
+ extWireFace = Part.Face(extWire)
+ extWireFace.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - extWireFace.BoundBox.ZMin))
+
+ if extWireFace.Area >= edgeFace.Area:
+ extensionFace = extWireFace.cut(edgeFace)
+ else:
+ extensionFace = edgeFace.cut(extWireFace)
+ extensionFace.translate(FreeCAD.Vector(0.0, 0.0, edge.BoundBox.ZMin))
+
+ return extensionFace
+# Eclass
+
+
+def initialize_properties(obj):
+ """initialize_properties(obj)... Adds feature propeties to object argument"""
+ if not hasattr(obj, 'ExtensionLengthDefault'):
+ obj.addProperty('App::PropertyDistance', 'ExtensionLengthDefault', 'Extension', QtCore.QT_TRANSLATE_NOOP('PathPocketShape', 'Default length of extensions.'))
+ if not hasattr(obj, 'ExtensionFeature'):
+ obj.addProperty('App::PropertyLinkSubListGlobal', 'ExtensionFeature', 'Extension', QtCore.QT_TRANSLATE_NOOP('PathPocketShape', 'List of features to extend.'))
+ if not hasattr(obj, 'ExtensionCorners'):
+ obj.addProperty('App::PropertyBool', 'ExtensionCorners', 'Extension', QtCore.QT_TRANSLATE_NOOP('PathPocketShape', 'When enabled connected extension edges are combined to wires.'))
+ obj.ExtensionCorners = True
+
+ obj.setEditorMode('ExtensionFeature', 2)
+
+
+def set_default_property_values(obj, job):
+ """set_default_property_values(obj, job) ... set default values for feature properties"""
+ obj.ExtensionCorners = True
+ obj.setExpression('ExtensionLengthDefault', 'OpToolDiameter / 2.0')
+
+
+def SetupProperties():
+ """SetupProperties()... Returns list of feature property names"""
+ setup = ['ExtensionLengthDefault', 'ExtensionFeature',
+ 'ExtensionCorners']
+ return setup
+
+
+# Extend outline face generation function
+def getExtendOutlineFace(base_shape, face, extension, remHoles=False, offset_tolerance=1e-4):
+ """getExtendOutlineFace(obj, base_shape, face, extension, remHoles) ...
+ Creates an extended face for the pocket, taking into consideration lateral
+ collision with the greater base shape.
+ Arguments are:
+ parent base shape of face,
+ target face,
+ extension magnitude,
+ remove holes boolean,
+ offset tolerance = 1e-4 default
+ The default value of 1e-4 for offset tolerance is the same default value
+ at getOffsetArea() function definition.
+ Return is an all access face extending the specified extension value from the source face.
+ """
+
+ # Make offset face per user-specified extension distance so as to allow full clearing of face where possible.
+ offset_face = PathUtils.getOffsetArea(face,
+ extension,
+ removeHoles=remHoles,
+ plane=face,
+ tolerance=offset_tolerance)
+ if not offset_face:
+ PathLog.error("Failed to offset a selected face.")
+ return None
+
+ # Apply collision detection by limiting extended face using base shape
+ depth = 0.2
+ offset_ext = offset_face.extrude(FreeCAD.Vector(0.0, 0.0, depth))
+ face_del = offset_face.extrude(FreeCAD.Vector(0.0, 0.0, -1.0 * depth))
+ clear = base_shape.cut(face_del)
+ available = offset_ext.cut(clear)
+ available.removeSplitter()
+
+ # Debug
+ # Part.show(available)
+ # FreeCAD.ActiveDocument.ActiveObject.Label = "available"
+
+ # Identify bottom face of available volume
+ zmin = available.BoundBox.ZMax
+ bottom_faces = list()
+ for f in available.Faces:
+ bbx = f.BoundBox
+ zNorm = abs(f.normalAt(0.0, 0.0).z)
+ if (PathGeom.isRoughly(zNorm, 1.0) and
+ PathGeom.isRoughly(bbx.ZMax - bbx.ZMin, 0.0) and
+ PathGeom.isRoughly(bbx.ZMin, face.BoundBox.ZMin)):
+ if bbx.ZMin < zmin:
+ bottom_faces.append(f)
+
+ if bottom_faces:
+ extended = None
+ for bf in bottom_faces:
+ # Drop travel face to same height as source face
+ diff = face.BoundBox.ZMax - bf.BoundBox.ZMax
+ bf.translate(FreeCAD.Vector(0.0, 0.0, diff))
+ cmn = bf.common(face)
+ if hasattr(cmn, "Area") and cmn.Area > 0.0:
+ extended = bf
+
+ return extended
+
+ PathLog.error("No bottom face for extend outline.")
+ return None
+
+# Waterline extension face generation function
+def getWaterlineFace(base_shape, face):
+ """getWaterlineFace(base_shape, face) ...
+ Creates a waterline extension face for the target face,
+ taking into consideration the greater base shape.
+ Arguments are: parent base shape and target face.
+ Return is a waterline face at height of the target face.
+ """
+ faceHeight = face.BoundBox.ZMin
+
+ # Get envelope of model to height of face, then fuse with model and refine the shape
+ baseBB = base_shape.BoundBox
+ depthparams = PathUtils.depth_params(
+ clearance_height=faceHeight,
+ safe_height=faceHeight,
+ start_depth=faceHeight,
+ step_down=math.floor(faceHeight - baseBB.ZMin + 2.0),
+ z_finish_step=0.0,
+ final_depth=baseBB.ZMin,
+ user_depths=None)
+ env = PathUtils.getEnvelope(partshape=base_shape, subshape=None, depthparams=depthparams)
+ # Get top face(s) of envelope at face height
+ rawList = list()
+ for f in env.Faces:
+ if PathGeom.isRoughly(f.BoundBox.ZMin, faceHeight):
+ rawList.append(f)
+ # make compound and extrude downward
+ rawComp = Part.makeCompound(rawList)
+ rawCompExtNeg = rawComp.extrude(FreeCAD.Vector(0.0, 0.0, baseBB.ZMin - faceHeight - 1.0))
+ # Cut off bottom of base shape at face height
+ topSolid = base_shape.cut(rawCompExtNeg)
+
+ # Get intersection with base shape
+ # The commented version returns waterlines that only intersects horizontal faces at same height as target face
+ # cmn = base_shape.common(rawComp)
+ # waterlineShape = cmn.cut(topSolid)
+ # return waterlineShape
+
+ # This version returns more of a true waterline flowing from target face
+ waterlineShape = rawComp.cut(topSolid)
+ faces = list()
+ for f in waterlineShape.Faces:
+ cmn = face.common(f)
+ if hasattr(cmn, "Area") and cmn.Area > 0.0:
+ faces.append(f)
+ if faces:
+ return Part.makeCompound(faces)
+
+ return None
diff --git a/src/Mod/Path/PathScripts/PathFeatureExtensionsGui.py b/src/Mod/Path/PathScripts/PathFeatureExtensionsGui.py
new file mode 100644
index 0000000000..6e1badc19a
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathFeatureExtensionsGui.py
@@ -0,0 +1,768 @@
+# -*- 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.PathGeom as PathGeom
+import PathScripts.PathGui as PathGui
+import PathScripts.PathLog as PathLog
+import PathScripts.PathOpGui as PathOpGui
+import PathScripts.PathFeatureExtensions as FeatureExtensions
+
+from PySide import QtCore, QtGui
+from pivy import coin
+
+# lazily loaded modules
+from lazy_loader.lazy_loader import LazyLoader
+Part = LazyLoader('Part', globals(), 'Part')
+
+__title__ = "Path Feature Extensions UI"
+__author__ = "sliptonic (Brad Collette)"
+__url__ = "https://www.freecadweb.org"
+__doc__ = "Extensions feature page controller."
+
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+#PathLog.trackModule(PathLog.thisModule())
+
+class _Extension(object):
+ ColourEnabled = (1.0, .5, 1.0)
+ ColourDisabled = (1.0, 1.0, .5)
+ TransparencySelected = 0.0
+ TransparencyDeselected = 0.7
+
+ def __init__(self, obj, base, face, edge):
+ self.obj = obj
+ self.base = base
+ self.face = face
+ self.edge = edge
+ self.ext = None
+
+ if edge:
+ self.ext = FeatureExtensions.createExtension(obj, base, face, edge)
+
+ self.switch = self.createExtensionSoSwitch(self.ext)
+ self.root = self.switch
+
+ def createExtensionSoSwitch(self, ext):
+ if not ext:
+ return None
+
+ sep = coin.SoSeparator()
+ pos = coin.SoTranslation()
+ mat = coin.SoMaterial()
+ crd = coin.SoCoordinate3()
+ fce = coin.SoFaceSet()
+ hnt = coin.SoShapeHints()
+ numVert = list() # track number of verticies in each polygon face
+
+ try:
+ wire = ext.getWire()
+ except FreeCAD.Base.FreeCADError:
+ wire = None
+
+ if not wire:
+ return None
+
+ if isinstance(wire, (list, tuple)):
+ p0 = [p for p in wire[0].discretize(Deflection=0.02)]
+ p1 = [p for p in wire[1].discretize(Deflection=0.02)]
+ p2 = list(reversed(p1))
+ polygon = [(p.x, p.y, p.z) for p in (p0 + p2)]
+ else:
+ if ext.extFaces:
+ # Create polygon for each extension face in compound extensions
+ allPolys = list()
+ extFaces = ext.getExtensionFaces(wire)
+ for f in extFaces:
+ pCnt = 0
+ wCnt = 0
+ for w in f.Wires:
+ if wCnt == 0:
+ poly = [p for p in w.discretize(Deflection=0.01)]
+ else:
+ poly = [p for p in w.discretize(Deflection=0.01)][:-1]
+ pCnt += len(poly)
+ allPolys.extend(poly)
+ numVert.append(pCnt)
+ polygon = [(p.x, p.y, p.z) for p in allPolys]
+ else:
+ # poly = [p for p in wire.discretize(Deflection=0.02)][:-1]
+ poly = [p for p in wire.discretize(Deflection=0.02)]
+ polygon = [(p.x, p.y, p.z) for p in poly]
+ crd.point.setValues(polygon)
+
+ mat.diffuseColor = self.ColourDisabled
+ mat.transparency = self.TransparencyDeselected
+
+ hnt.faceType = coin.SoShapeHints.UNKNOWN_FACE_TYPE
+ hnt.vertexOrdering = coin.SoShapeHints.CLOCKWISE
+
+ if numVert:
+ # Transfer vertex counts for polygon faces
+ fce.numVertices.setValues(tuple(numVert))
+
+ sep.addChild(pos)
+ sep.addChild(mat)
+ sep.addChild(hnt)
+ sep.addChild(crd)
+ sep.addChild(fce)
+
+ # Finalize SoSwitch
+ switch = coin.SoSwitch()
+ switch.addChild(sep)
+ switch.whichChild = coin.SO_SWITCH_NONE
+
+ self.material = mat
+
+ return switch
+
+ def _setColour(self, r, g, b):
+ self.material.diffuseColor = (r, g, b)
+
+ def isValid(self):
+ return not self.root is None
+
+ def show(self):
+ if self.switch:
+ self.switch.whichChild = coin.SO_SWITCH_ALL
+
+ def hide(self):
+ if self.switch:
+ self.switch.whichChild = coin.SO_SWITCH_NONE
+
+ def enable(self, ena = True):
+ if ena:
+ self.material.diffuseColor = self.ColourEnabled
+ else:
+ self.disable()
+
+ def disable(self):
+ self.material.diffuseColor = self.ColourDisabled
+
+ def select(self):
+ self.material.transparency = self.TransparencySelected
+
+ def deselect(self):
+ self.material.transparency = self.TransparencyDeselected
+
+
+class TaskPanelExtensionPage(PathOpGui.TaskPanelPage):
+ DataObject = QtCore.Qt.ItemDataRole.UserRole
+ DataSwitch = QtCore.Qt.ItemDataRole.UserRole + 2
+
+ Direction = {
+ FeatureExtensions.Extension.DirectionNormal: translate('PathPocket', 'Normal'),
+ FeatureExtensions.Extension.DirectionX: translate('PathPocket', 'X'),
+ FeatureExtensions.Extension.DirectionY: translate('PathPocket', 'Y')
+ }
+
+ def initPage(self, obj):
+ self.setTitle("Extensions")
+ self.OpIcon = ":/icons/view-axonometric.svg"
+ self.setIcon(self.OpIcon)
+ self.initialEdgeCount = -1
+ self.edgeCountThreshold = 30
+ self.fieldsSet = False
+ self.useOutlineCheckbox = None
+ self.useOutline = -1
+ self.extensionsCache = dict()
+ self.extensionsReady = False
+ self.enabled = True
+
+ self.extensions = list()
+
+ self.defaultLength = PathGui.QuantitySpinBox(self.form.defaultLength, obj, 'ExtensionLengthDefault') # pylint: disable=attribute-defined-outside-init
+
+ self.form.extensionTree.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
+ self.form.extensionTree.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
+
+ self.switch = coin.SoSwitch() # pylint: disable=attribute-defined-outside-init
+ self.obj.ViewObject.RootNode.addChild(self.switch)
+ self.switch.whichChild = coin.SO_SWITCH_ALL
+
+ self.model = QtGui.QStandardItemModel(self.form.extensionTree) # pylint: disable=attribute-defined-outside-init
+ self.model.setHorizontalHeaderLabels(['Base', 'Extension'])
+
+ """
+ # russ4262: This `if` block shows all available extensions upon edit of operation with any extension enabled.
+ # This can cause the model(s) to overly obscured due to previews of extensions.
+ # Would be great if only enabled extensions were shown.
+ if 0 < len(obj.ExtensionFeature):
+ self.form.showExtensions.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.form.showExtensions.setCheckState(QtCore.Qt.Unchecked)
+ """
+ self.form.showExtensions.setCheckState(QtCore.Qt.Unchecked)
+
+ self.blockUpdateData = False # pylint: disable=attribute-defined-outside-init
+
+ def cleanupPage(self, obj):
+ try:
+ self.obj.ViewObject.RootNode.removeChild(self.switch)
+ except ReferenceError:
+ PathLog.debug("obj already destroyed - no cleanup required")
+
+ def getForm(self):
+ form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpPocketExtEdit.ui")
+ # Hide warning label by default
+ form.enableExtensionsWarning.hide()
+ return form
+
+ def forAllItemsCall(self, cb):
+ for modelRow in range(self.model.rowCount()):
+ model = self.model.item(modelRow, 0)
+ for featureRow in range(model.rowCount()):
+ feature = model.child(featureRow, 0)
+ for edgeRow in range(feature.rowCount()):
+ item = feature.child(edgeRow, 0)
+ ext = item.data(self.DataObject)
+ cb(item, ext)
+
+ def currentExtensions(self):
+ PathLog.debug("currentExtensions()")
+ extensions = []
+ def extractExtension(item, ext):
+ if ext and ext.edge and item.checkState() == QtCore.Qt.Checked:
+ extensions.append(ext.ext)
+ if self.form.enableExtensions.isChecked():
+ self.forAllItemsCall(extractExtension)
+ PathLog.track('extensions', extensions)
+ return extensions
+
+ def updateProxyExtensions(self, obj):
+ PathLog.debug("updateProxyExtensions()")
+ self.extensions = self.currentExtensions() # pylint: disable=attribute-defined-outside-init
+ FeatureExtensions.setExtensions(obj, self.extensions)
+
+ def getFields(self, obj):
+ PathLog.track(obj.Label, self.model.rowCount(), self.model.columnCount())
+ self.blockUpdateData = True # pylint: disable=attribute-defined-outside-init
+
+ if obj.ExtensionCorners != self.form.extendCorners.isChecked():
+ obj.ExtensionCorners = self.form.extendCorners.isChecked()
+ self.defaultLength.updateProperty()
+
+ self.updateProxyExtensions(obj)
+ self.blockUpdateData = False # pylint: disable=attribute-defined-outside-init
+
+ def setFields(self, obj):
+ PathLog.track(obj.Label)
+ # PathLog.debug("setFields()")
+
+ if obj.ExtensionCorners != self.form.extendCorners.isChecked():
+ self.form.extendCorners.toggle()
+
+ self._autoEnableExtensions() # Check edge count for auto-disable Extensions on initial Task Panel loading
+ self._initializeExtensions(obj) # Efficiently initialize Extensions
+ self.defaultLength.updateSpinBox()
+ self._getUseOutlineState() # Find `useOutline` checkbox and get its boolean value
+ self.fieldsSet = True # flag to identify initial values set
+
+ def _initializeExtensions(self, obj):
+ """_initializeExtensions()...
+ Subroutine called inside `setFields()` to initialize Extensions efficiently."""
+ if self.enabled:
+ self.extensions = FeatureExtensions.getExtensions(obj)
+ elif len(obj.ExtensionFeature) > 0: # latter test loads pre-existing extensions (editting of existing operation)
+ noEdges = True
+ for (__, __, subFeat) in FeatureExtensions.readObjExtensionFeature(obj):
+ if subFeat.startswith("Edge") or subFeat.startswith("Wire"):
+ noEdges = False
+ break
+ self.extensions = FeatureExtensions.getExtensions(obj)
+ self.form.enableExtensions.setChecked(True)
+ if noEdges:
+ self._enableExtensions()
+ else:
+ self.form.includeEdges.setChecked(True)
+ self._includeEdgesAndWires()
+ self.setExtensions(self.extensions)
+
+ def updateQuantitySpinBoxes(self, index=None):
+ prevValue = self.form.defaultLength.text()
+ self.defaultLength.updateSpinBox()
+ postValue = self.form.defaultLength.text()
+
+ if postValue != prevValue:
+ PathLog.debug("updateQuantitySpinBoxes() post != prev value")
+ self._resetCachedExtensions() # Reset extension cache because extension dimensions likely changed
+ self._enableExtensions() # Recalculate extensions
+
+ def createItemForBaseModel(self, base, sub, edges, extensions):
+ PathLog.track(base.Label, sub, '+', len(edges), len(base.Shape.getElement(sub).Edges))
+ # PathLog.debug("createItemForBaseModel() label: {}, sub: {}, {}, edgeCnt: {}, subEdges: {}".format(base.Label, sub, '+', len(edges), len(base.Shape.getElement(sub).Edges)))
+
+ extendCorners = self.form.extendCorners.isChecked()
+ includeEdges = self.form.includeEdges.isChecked()
+ subShape = base.Shape.getElement(sub)
+
+ def createSubItem(label, ext0):
+ if ext0.root:
+ self.switch.addChild(ext0.root)
+ item0 = QtGui.QStandardItem()
+ item0.setData(label, QtCore.Qt.EditRole)
+ item0.setData(ext0, self.DataObject)
+ item0.setCheckable(True)
+ for e in extensions:
+ if e.obj == base and e.sub == label:
+ item0.setCheckState(QtCore.Qt.Checked)
+ ext0.enable()
+ break
+ item.appendRow([item0])
+
+ # ext = self._cachedExtension(self.obj, base, sub, None)
+ ext = None
+ item = QtGui.QStandardItem()
+ item.setData(sub, QtCore.Qt.EditRole)
+ item.setData(ext, self.DataObject)
+ item.setSelectable(False)
+
+ extensionEdges = {}
+ if includeEdges:
+ if self.useOutline == 1 and sub.startswith('Face'):
+ # Only show exterior extensions if `Use Outline` is True
+ subEdges = subShape.Wires[0].Edges
+ else:
+ # Show all exterior and interior extensions if `Use Outline` is False
+ subEdges = subShape.Edges
+
+ for edge in subEdges:
+ for (e, label) in edges:
+ if edge.isSame(e):
+ ext1 = self._cachedExtension(self.obj, base, sub, label)
+ if ext1.isValid():
+ extensionEdges[e] = label[4:] # isolate edge number
+ if not extendCorners:
+ createSubItem(label, ext1)
+
+ if extendCorners and includeEdges:
+ def edgesMatchShape(e0, e1):
+ flipped = PathGeom.flipEdge(e1)
+ if flipped:
+ return PathGeom.edgesMatch(e0, e1) or PathGeom.edgesMatch(e0, flipped)
+ else:
+ return PathGeom.edgesMatch(e0, e1)
+
+ self.extensionEdges = extensionEdges
+ PathLog.debug("extensionEdges.values(): {}".format(extensionEdges.values()))
+ for edgeList in Part.sortEdges(list(extensionEdges.keys())): # Identify connected edges that form wires
+ self.edgeList = edgeList
+ if len(edgeList) == 1:
+ label = "Edge%s" % [extensionEdges[keyEdge] for keyEdge in extensionEdges.keys() if edgesMatchShape(keyEdge, edgeList[0])][0]
+ else:
+ label = "Wire(%s)" % ','.join(sorted([extensionEdges[keyEdge] for e in edgeList for keyEdge in extensionEdges.keys() if edgesMatchShape(e, keyEdge)], key=lambda s: int(s))) # pylint: disable=unnecessary-lambda
+ ext2 = self._cachedExtension(self.obj, base, sub, label)
+ createSubItem(label, ext2)
+
+ # Only add these subItems for horizontally oriented faces, not edges or vertical faces (from vertical face loops)
+ if sub.startswith('Face') and PathGeom.isHorizontal(subShape):
+ # Add entry to extend outline of face
+ label = "Extend_" + sub
+ ext3 = self._cachedExtension(self.obj, base, sub, label)
+ createSubItem(label, ext3)
+
+ # Add entry for waterline at face
+ label = "Waterline_" + sub
+ ext4 = self._cachedExtension(self.obj, base, sub, label)
+ createSubItem(label, ext4)
+
+ # Add entry for avoid face
+ label = "Avoid_" + sub
+ ext5 = self._cachedExtension(self.obj, base, sub, label)
+ createSubItem(label, ext5)
+
+ return item
+
+ def setExtensions(self, extensions):
+ PathLog.track(len(extensions))
+ PathLog.debug("setExtensions()")
+
+ if self.extensionsReady:
+ PathLog.debug("setExtensions() returning per `extensionsReady` flag")
+ return
+
+ self.form.extensionTree.blockSignals(True)
+
+ # remember current visual state
+ if hasattr(self, 'selectionModel'):
+ selectedExtensions = [self.model.itemFromIndex(index).data(self.DataObject).ext for index in self.selectionModel.selectedIndexes()]
+ else:
+ selectedExtensions = []
+ collapsedModels = []
+ collapsedFeatures = []
+ for modelRow in range(self.model.rowCount()):
+ model = self.model.item(modelRow, 0)
+ modelName = model.data(QtCore.Qt.EditRole)
+ if not self.form.extensionTree.isExpanded(model.index()):
+ collapsedModels.append(modelName)
+ for featureRow in range(model.rowCount()):
+ feature = model.child(featureRow, 0)
+ if not self.form.extensionTree.isExpanded(feature.index()):
+ collapsedFeatures.append("%s.%s" % (modelName, feature.data(QtCore.Qt.EditRole)))
+
+ # remove current extensions and all their visuals
+ def removeItemSwitch(item, ext):
+ # pylint: disable=unused-argument
+ ext.hide()
+ if ext.root:
+ self.switch.removeChild(ext.root)
+ self.forAllItemsCall(removeItemSwitch)
+ self.model.clear()
+
+ # create extensions for model and given argument
+ if self.enabled:
+ for base in self.obj.Base:
+ show = False
+ edges = [(edge, "Edge%d" % (i + 1)) for i, edge in enumerate(base[0].Shape.Edges)]
+ baseItem = QtGui.QStandardItem()
+ baseItem.setData(base[0].Label, QtCore.Qt.EditRole)
+ baseItem.setSelectable(False)
+ for sub in sorted(base[1]):
+ if sub.startswith('Face') or True:
+ show = True
+ baseItem.appendRow(self.createItemForBaseModel(base[0], sub, edges, extensions))
+ if show:
+ self.model.appendRow(baseItem)
+
+ self.form.extensionTree.setModel(self.model)
+ self.form.extensionTree.expandAll()
+ self.form.extensionTree.resizeColumnToContents(0)
+
+ # restore previous state - at least the parts that are still valid
+ for modelRow in range(self.model.rowCount()):
+ model = self.model.item(modelRow, 0)
+ modelName = model.data(QtCore.Qt.EditRole)
+ if modelName in collapsedModels:
+ self.form.extensionTree.setExpanded(model.index(), False)
+ for featureRow in range(model.rowCount()):
+ feature = model.child(featureRow, 0)
+ featureName = "%s.%s" % (modelName, feature.data(QtCore.Qt.EditRole))
+ if featureName in collapsedFeatures:
+ self.form.extensionTree.setExpanded(feature.index(), False)
+ if hasattr(self, 'selectionModel') and selectedExtensions:
+ self.restoreSelection(selectedExtensions)
+
+ self.form.extensionTree.blockSignals(False)
+ self.extensionsReady = True
+ PathLog.debug(" setExtensions() finished and setting `extensionsReady=True`")
+
+ def updateData(self, obj, prop):
+ PathLog.track(obj.Label, prop, self.blockUpdateData)
+ # PathLog.debug("updateData({})".format(prop))
+
+ if not self.blockUpdateData:
+ if self.fieldsSet:
+ if self.form.enableExtensions.isChecked():
+ if prop == 'ExtensionLengthDefault':
+ self.updateQuantitySpinBoxes()
+ elif prop == 'Base':
+ self.extensionsReady = False
+ self.setExtensions(FeatureExtensions.getExtensions(obj))
+ elif prop == 'UseOutline':
+ self._getUseOutlineState() # Find `useOutline` checkbox and get its boolean value
+ self._includeEdgesAndWires()
+ elif prop == 'Base':
+ self.extensionsReady = False
+
+ def restoreSelection(self, selection):
+ PathLog.debug("restoreSelection()")
+ PathLog.track()
+ if 0 == self.model.rowCount():
+ PathLog.track('-')
+ self.form.buttonClear.setEnabled(False)
+ self.form.buttonDisable.setEnabled(False)
+ self.form.buttonEnable.setEnabled(False)
+ else:
+ self.form.buttonClear.setEnabled(True)
+
+ if selection or self.selectionModel.selectedIndexes():
+ self.form.buttonDisable.setEnabled(True)
+ self.form.buttonEnable.setEnabled(True)
+ else:
+ self.form.buttonDisable.setEnabled(False)
+ self.form.buttonEnable.setEnabled(False)
+
+ FreeCADGui.Selection.clearSelection()
+
+ def selectItem(item, ext):
+ # pylint: disable=unused-argument
+ for sel in selection:
+ if ext.base == sel.obj and ext.edge == sel.sub:
+ return True
+ return False
+
+ def setSelectionVisuals(item, ext):
+ if selectItem(item, ext):
+ self.selectionModel.select(item.index(), QtCore.QItemSelectionModel.Select)
+
+ selected = self.selectionModel.isSelected(item.index())
+ if selected:
+ FreeCADGui.Selection.addSelection(ext.base, ext.face)
+ ext.select()
+ else:
+ ext.deselect()
+
+ if self.form.showExtensions.isChecked() or selected:
+ ext.show()
+ else:
+ ext.hide()
+ self.forAllItemsCall(setSelectionVisuals)
+
+ def selectionChanged(self):
+ PathLog.debug("selectionChanged()")
+ self.restoreSelection([])
+
+ def extensionsClear(self):
+ PathLog.debug("extensionsClear()")
+ def disableItem(item, ext):
+ item.setCheckState(QtCore.Qt.Unchecked)
+ ext.disable()
+
+ self.forAllItemsCall(disableItem)
+ self.setDirty()
+
+ def _extensionsSetState(self, state):
+ PathLog.debug("_extensionsSetState()")
+ PathLog.track(state)
+ for index in self.selectionModel.selectedIndexes():
+ item = self.model.itemFromIndex(index)
+ ext = item.data(self.DataObject)
+ if ext.edge:
+ item.setCheckState(state)
+ ext.enable(state == QtCore.Qt.Checked)
+ self.setDirty()
+
+ def extensionsDisable(self):
+ self._extensionsSetState(QtCore.Qt.Unchecked)
+
+ def extensionsEnable(self):
+ self._extensionsSetState(QtCore.Qt.Checked)
+
+ def updateItemEnabled(self, item):
+ PathLog.track(item)
+ ext = item.data(self.DataObject)
+ if item.checkState() == QtCore.Qt.Checked:
+ ext.enable()
+ else:
+ ext.disable()
+ self.updateProxyExtensions(self.obj)
+ self.setDirty()
+
+ def showHideExtension(self):
+ if self.form.showExtensions.isChecked():
+ def enableExtensionEdit(item, ext):
+ # pylint: disable=unused-argument
+ ext.show()
+ self.forAllItemsCall(enableExtensionEdit)
+ else:
+ def disableExtensionEdit(item, ext):
+ if not self.selectionModel.isSelected(item.index()):
+ ext.hide()
+ self.forAllItemsCall(disableExtensionEdit)
+ #self.setDirty()
+
+ def toggleExtensionCorners(self):
+ PathLog.debug("toggleExtensionCorners()")
+ PathLog.track()
+ self.extensionsReady = False
+ extensions = FeatureExtensions.getExtensions(self.obj)
+ self.setExtensions(extensions)
+ self.selectionChanged()
+ self.setDirty()
+
+ def getSignalsForUpdate(self, obj):
+ PathLog.track(obj.Label)
+ signals = []
+ signals.append(self.form.defaultLength.editingFinished)
+ signals.append(self.form.enableExtensions.toggled)
+ signals.append(self.form.includeEdges.toggled)
+ return signals
+
+ def registerSignalHandlers(self, obj):
+ self.form.showExtensions.clicked.connect(self.showHideExtension)
+ self.form.extendCorners.clicked.connect(self.toggleExtensionCorners)
+ self.form.buttonClear.clicked.connect(self.extensionsClear)
+ self.form.buttonDisable.clicked.connect(self.extensionsDisable)
+ self.form.buttonEnable.clicked.connect(self.extensionsEnable)
+ self.form.defaultLength.editingFinished.connect(self.updateQuantitySpinBoxes)
+ self.form.enableExtensions.toggled.connect(self._enableExtensions)
+ self.form.includeEdges.toggled.connect(self._includeEdgesAndWires)
+
+ self.model.itemChanged.connect(self.updateItemEnabled)
+
+ self.selectionModel = self.form.extensionTree.selectionModel() # pylint: disable=attribute-defined-outside-init
+ self.selectionModel.selectionChanged.connect(self.selectionChanged)
+ self.selectionChanged()
+
+ # Support methods
+ def _getUseOutlineState(self):
+ """_getUseOutlineState() ...
+ This method locates the `useOutline` form checkbox in the `Operation` tab,
+ and saves that reference to self.useOutlineInput. If found, then the boolean
+ value of the checkbox is saved to self.useOutline.
+ """
+ if self.useOutlineCheckbox:
+ self.useOutline = self.useOutlineCheckbox.isChecked()
+
+ if hasattr(self, 'parent'):
+ parent = getattr(self, 'parent')
+ if parent and hasattr(parent, 'featurePages'):
+ for page in parent.featurePages:
+ if hasattr(page, 'panelTitle'):
+ if page.panelTitle == 'Operation' and hasattr(page.form, 'useOutline'):
+ PathLog.debug("Found useOutline checkbox")
+ self.useOutlineCheckbox = page.form.useOutline
+ if page.form.useOutline.isChecked():
+ self.useOutline = 1
+ return
+ else:
+ self.useOutline = 0
+ return
+
+ self.useOutline = -1
+
+ # Methods for enable and disablement of Extensions feature
+ def _autoEnableExtensions(self):
+ """_autoEnableExtensions() ...
+ This method is called to determine if the Extensions feature should be enabled,
+ or auto disabled due to total edge count of selected faces.
+ The auto enable/disable feature is designed to allow quicker access
+ to operations that implement the Extensions feature when selected faces contain
+ large numbers of edges, which require long computation times for preparation.
+
+ The return value is a simple boolean to communicate whether or not Extensions
+ are be enabled.
+ """
+ enabled = True
+
+ if self.initialEdgeCount < 1:
+ self.initialEdgeCount = 0
+ for base in self.obj.Base:
+ for sub in sorted(base[1]):
+ self.initialEdgeCount += len(base[0].Shape.getElement(sub).Edges)
+ if self.initialEdgeCount > self.edgeCountThreshold:
+ # Block signals
+ self.form.enableExtensions.blockSignals(True)
+ self.form.enableExtensionsWarning.blockSignals(True)
+ self.form.includeEdges.blockSignals(True)
+
+ # Make changes to form
+ msg = translate("PathPocketShape",
+ "Edge count greater than threshold of" + " " +
+ str(self.edgeCountThreshold) + ": " +
+ str(self.initialEdgeCount))
+ self.form.enableExtensionsWarning.setText(msg)
+ self.form.enableExtensions.setChecked(False)
+ self.form.enableExtensionsWarning.show()
+ msg = translate("PathFeatureExtensions", "Click to enable Extensions")
+ self.form.enableExtensions.setText(msg)
+ self.form.extensionEdit.setDisabled(True)
+ self.form.includeEdges.setChecked(False)
+ msg = translate("PathFeatureExtensions", "Click to include Edges/Wires")
+ self.form.includeEdges.setText(msg)
+
+ # Unblock signals
+ self.form.enableExtensions.blockSignals(False)
+ self.form.enableExtensionsWarning.blockSignals(False)
+ self.form.includeEdges.blockSignals(False)
+
+ enabled = False
+ elif not self.form.enableExtensions.isChecked():
+ enabled = False
+
+ PathLog.debug("_autoEnableExtensions() is {}".format(enabled))
+ self.enabled = enabled
+
+ def _enableExtensions(self):
+ """_enableExtensions() ...
+ This method is called when the enableExtensions push button is toggled.
+ This method manages the enabled or disabled state of the extensionsEdit
+ Task Panel input group.
+ """
+ PathLog.debug("_enableExtensions()")
+
+ if self.form.enableExtensions.isChecked():
+ self.enabled = True
+ self.extensionsReady = False
+ msg = translate("PathFeatureExtensions", "Extensions enabled")
+ self.form.enableExtensions.setText(msg)
+ self.form.enableExtensionsWarning.hide()
+ self.form.extensionEdit.setEnabled(True)
+ self.extensions = FeatureExtensions.getExtensions(self.obj)
+ self.setExtensions(self.extensions)
+ else:
+ msg = translate("PathFeatureExtensions", "Click to enable Extensions")
+ self.form.enableExtensions.setText(msg)
+ self.form.extensionEdit.setDisabled(True)
+ self.enabled = False
+
+ def _includeEdgesAndWires(self):
+ """_includeEdgesAndWires() ...
+ This method is called when the includeEdges push button is toggled.
+ This method manages the state of the button and the message thereof.
+ """
+ self._getUseOutlineState() # Find `useOutline` checkbox and get its boolean value
+ PathLog.debug("_includeEdgesAndWires()")
+ if self.form.includeEdges.isChecked():
+ msg = translate("PathFeatureExtensions", "Including Edges/Wires")
+ self.form.includeEdges.setText(msg)
+ else:
+ msg = translate("PathFeatureExtensions", "Click to include Edges/Wires")
+ self.form.includeEdges.setText(msg)
+ self.extensionsReady = False
+ self._enableExtensions()
+
+ # Methods for creating and managing cached extensions
+ def _cachedExtension(self, obj, base, sub, label):
+ """_cachedExtension(obj, base, sub, label)...
+ This method creates a new _Extension object if none is found within
+ the extensionCache dictionary."""
+
+ if label:
+ cacheLabel = base.Name + "_" + sub + "_" + label
+ else:
+ cacheLabel = base.Name + "_" + sub + "_None"
+
+ if cacheLabel in self.extensionsCache.keys():
+ # PathLog.debug("return _cachedExtension({})".format(cacheLabel))
+ return self.extensionsCache[cacheLabel]
+ else:
+ # PathLog.debug("_cachedExtension({}) created".format(cacheLabel))
+ _ext = _Extension(obj, base, sub, label)
+ self.extensionsCache[cacheLabel] = _ext # cache the extension
+ return _ext
+
+ def _resetCachedExtensions(self):
+ PathLog.error("_resetCachedExtensions()")
+ reset = dict()
+ # Keep waterline extensions as they will not change
+ for k in self.extensionsCache.keys():
+ if k.startswith("Waterline"):
+ reset[k] = self.extensionsCache[k]
+ self.extensionsCache = reset
+ self.extensionsReady = False
+# Eclass
+
+FreeCAD.Console.PrintLog("Loading PathFeatureExtensionsGui... done\n")
diff --git a/src/Mod/Path/PathScripts/PathGeom.py b/src/Mod/Path/PathScripts/PathGeom.py
index 31031954ca..e89c20fb18 100644
--- a/src/Mod/Path/PathScripts/PathGeom.py
+++ b/src/Mod/Path/PathScripts/PathGeom.py
@@ -551,3 +551,101 @@ def flipWire(wire):
PathLog.debug(edges)
return Part.Wire(edges)
+def makeBoundBoxFace(bBox, offset=0.0, zHeight=0.0):
+ '''makeBoundBoxFace(bBox, offset=0.0, zHeight=0.0)...
+ Function to create boundbox face, with possible extra offset and custom Z-height.'''
+ p1 = FreeCAD.Vector(bBox.XMin - offset, bBox.YMin - offset, zHeight)
+ p2 = FreeCAD.Vector(bBox.XMax + offset, bBox.YMin - offset, zHeight)
+ p3 = FreeCAD.Vector(bBox.XMax + offset, bBox.YMax + offset, zHeight)
+ p4 = FreeCAD.Vector(bBox.XMin - offset, bBox.YMax + offset, zHeight)
+
+ L1 = Part.makeLine(p1, p2)
+ L2 = Part.makeLine(p2, p3)
+ L3 = Part.makeLine(p3, p4)
+ L4 = Part.makeLine(p4, p1)
+
+ return Part.Face(Part.Wire([L1, L2, L3, L4]))
+
+# Method to combine faces if connected
+def combineHorizontalFaces(faces):
+ '''combineHorizontalFaces(faces)...
+ This function successfully identifies and combines multiple connected faces and
+ works on multiple independent faces with multiple connected faces within the list.
+ The return value is list of simplifed faces.
+ The Adaptive op is not concerned with which hole edges belong to which face.
+
+ Attempts to do the same shape connecting failed with TechDraw.findShapeOutline() and
+ PathGeom.combineConnectedShapes(), so this algorithm was created.
+ '''
+ horizontal = list()
+ offset = 10.0
+ topFace = None
+ innerFaces = list()
+
+ # Verify all incomming faces are at Z=0.0
+ for f in faces:
+ if f.BoundBox.ZMin != 0.0:
+ f.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - f.BoundBox.ZMin))
+
+ # Make offset compound boundbox solid and cut incoming face extrusions from it
+ allFaces = Part.makeCompound(faces)
+ if hasattr(allFaces, "Area") and isRoughly(allFaces.Area, 0.0):
+ msg = translate('PathGeom',
+ 'Zero working area to process. Check your selection and settings.')
+ PathLog.info(msg)
+ return horizontal
+
+ afbb = allFaces.BoundBox
+ bboxFace = makeBoundBoxFace(afbb, offset, -5.0)
+ bboxSolid = bboxFace.extrude(FreeCAD.Vector(0.0, 0.0, 10.0))
+ extrudedFaces = list()
+ for f in faces:
+ extrudedFaces.append(f.extrude(FreeCAD.Vector(0.0, 0.0, 6.0)))
+
+ # Fuse all extruded faces together
+ allFacesSolid = extrudedFaces.pop()
+ for i in range(len(extrudedFaces)):
+ temp = extrudedFaces.pop().fuse(allFacesSolid)
+ allFacesSolid = temp
+ cut = bboxSolid.cut(allFacesSolid)
+
+ # Debug
+ # Part.show(cut)
+ # FreeCAD.ActiveDocument.ActiveObject.Label = "cut"
+
+ # Identify top face and floating inner faces that are the holes in incoming faces
+ for f in cut.Faces:
+ fbb = f.BoundBox
+ if isRoughly(fbb.ZMin, 5.0) and isRoughly(fbb.ZMax, 5.0):
+ if (isRoughly(afbb.XMin - offset, fbb.XMin) and
+ isRoughly(afbb.XMax + offset, fbb.XMax) and
+ isRoughly(afbb.YMin - offset, fbb.YMin) and
+ isRoughly(afbb.YMax + offset, fbb.YMax)):
+ topFace = f
+ else:
+ innerFaces.append(f)
+
+ if not topFace:
+ return horizontal
+
+ outer = [Part.Face(w) for w in topFace.Wires[1:]]
+
+ if outer:
+ for f in outer:
+ f.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - f.BoundBox.ZMin))
+
+ if innerFaces:
+ # inner = [Part.Face(f.Wire1) for f in innerFaces]
+ inner = innerFaces
+
+ for f in inner:
+ f.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - f.BoundBox.ZMin))
+ innerComp = Part.makeCompound(inner)
+ outerComp = Part.makeCompound(outer)
+ cut = outerComp.cut(innerComp)
+ for f in cut.Faces:
+ horizontal.append(f)
+ else:
+ horizontal = outer
+
+ return horizontal
diff --git a/src/Mod/Path/PathScripts/PathPocketShape.py b/src/Mod/Path/PathScripts/PathPocketShape.py
index ed59357020..902fb21c44 100644
--- a/src/Mod/Path/PathScripts/PathPocketShape.py
+++ b/src/Mod/Path/PathScripts/PathPocketShape.py
@@ -33,6 +33,10 @@ from lazy_loader.lazy_loader import LazyLoader
Part = LazyLoader('Part', globals(), 'Part')
TechDraw = LazyLoader('TechDraw', globals(), 'TechDraw')
math = LazyLoader('math', globals(), 'math')
+PathUtils = LazyLoader('PathScripts.PathUtils', globals(), 'PathScripts.PathUtils')
+FeatureExtensions = LazyLoader('PathScripts.PathFeatureExtensions',
+ globals(),
+ 'PathScripts.PathFeatureExtensions')
__title__ = "Path Pocket Shape Operation"
@@ -50,239 +54,6 @@ def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
-def endPoints(edgeOrWire):
- '''endPoints(edgeOrWire) ... return the first and last point of the wire or the edge, assuming the argument is not a closed wire.'''
- if Part.Wire == type(edgeOrWire):
- # edges = edgeOrWire.Edges
- pts = [e.valueAt(e.FirstParameter) for e in edgeOrWire.Edges]
- pts.extend([e.valueAt(e.LastParameter) for e in edgeOrWire.Edges])
- unique = []
- for p in pts:
- cnt = len([p2 for p2 in pts if PathGeom.pointsCoincide(p, p2)])
- if 1 == cnt:
- unique.append(p)
- return unique
-
- pfirst = edgeOrWire.valueAt(edgeOrWire.FirstParameter)
- plast = edgeOrWire.valueAt(edgeOrWire.LastParameter)
- if PathGeom.pointsCoincide(pfirst, plast):
- return None
-
- return [pfirst, plast]
-
-
-def includesPoint(p, pts):
- '''includesPoint(p, pts) ... answer True if the collection of pts includes the point p'''
- for pt in pts:
- if PathGeom.pointsCoincide(p, pt):
- return True
- return False
-
-
-def selectOffsetWire(feature, wires):
- '''selectOffsetWire(feature, wires) ... returns the Wire in wires which is does not intersect with feature'''
- closest = None
- for w in wires:
- dist = feature.distToShape(w)[0]
- if closest is None or dist > closest[0]:
- closest = (dist, w)
- if not closest is None:
- return closest[1]
- return None
-
-
-def extendWire(feature, wire, length):
- '''extendWire(wire, length) ... return a closed Wire which extends wire by length'''
- PathLog.track(length)
- if length and length != 0:
- try:
- off2D = wire.makeOffset2D(length)
- except FreeCAD.Base.FreeCADError:
- return None
-
- endPts = endPoints(wire)
- if endPts:
- edges = [e for e in off2D.Edges if Part.Circle != type(e.Curve) or not includesPoint(e.Curve.Center, endPts)]
- wires = [Part.Wire(e) for e in Part.sortEdges(edges)]
- offset = selectOffsetWire(feature, wires)
- ePts = endPoints(offset)
- if ePts and len(ePts) > 1:
- l0 = (ePts[0] - endPts[0]).Length
- l1 = (ePts[1] - endPts[0]).Length
- edges = wire.Edges
- if l0 < l1:
- edges.append(Part.Edge(Part.LineSegment(endPts[0], ePts[0])))
- edges.extend(offset.Edges)
- edges.append(Part.Edge(Part.LineSegment(endPts[1], ePts[1])))
- else:
- edges.append(Part.Edge(Part.LineSegment(endPts[1], ePts[0])))
- edges.extend(offset.Edges)
- edges.append(Part.Edge(Part.LineSegment(endPts[0], ePts[1])))
- return Part.Wire(edges)
- return None
-
-
-class Extension(object):
- DirectionNormal = 0
- DirectionX = 1
- DirectionY = 2
-
- def __init__(self, obj, feature, sub, length, direction):
- PathLog.debug("Extension(%s, %s, %s, %.2f, %s" % (obj.Label, feature, sub, length, direction))
- self.obj = obj
- self.feature = feature
- self.sub = sub
- self.length = length
- self.direction = direction
- self.extFaces = list()
-
- def getSubLink(self):
- return "%s:%s" % (self.feature, self.sub)
-
- def _extendEdge(self, feature, e0, direction):
- PathLog.track(feature, e0, direction)
- if isinstance(e0.Curve, Part.Line) or isinstance(e0.Curve, Part.LineSegment):
- e2 = e0.copy()
- off = self.length.Value * direction
- e2.translate(off)
- e2 = PathGeom.flipEdge(e2)
- e1 = Part.Edge(Part.LineSegment(e0.valueAt(e0.LastParameter), e2.valueAt(e2.FirstParameter)))
- e3 = Part.Edge(Part.LineSegment(e2.valueAt(e2.LastParameter), e0.valueAt(e0.FirstParameter)))
- wire = Part.Wire([e0, e1, e2, e3])
- self.wire = wire
- return wire
- return extendWire(feature, Part.Wire([e0]), self.length.Value)
-
- def _getEdgeNumbers(self):
- if 'Wire' in self.sub:
- numbers = [nr for nr in self.sub[5:-1].split(',')]
- else:
- numbers = [self.sub[4:]]
- PathLog.debug("_getEdgeNumbers() -> %s" % numbers)
-
- return numbers
-
- def _getEdgeNames(self):
- return ["Edge%s" % nr for nr in self._getEdgeNumbers()]
-
- def _getEdges(self):
- return [self.obj.Shape.getElement(sub) for sub in self._getEdgeNames()]
-
- def _getDirectedNormal(self, p0, normal):
- poffPlus = p0 + 0.01 * normal
- poffMinus = p0 - 0.01 * normal
- if not self.obj.Shape.isInside(poffPlus, 0.005, True):
- return normal
- if not self.obj.Shape.isInside(poffMinus, 0.005, True):
- return normal.negative()
- return None
-
- def _getDirection(self, wire):
- e0 = wire.Edges[0]
- midparam = e0.FirstParameter + 0.5 * (e0.LastParameter - e0.FirstParameter)
- tangent = e0.tangentAt(midparam)
- PathLog.track('tangent', tangent, self.feature, self.sub)
- normal = tangent.cross(FreeCAD.Vector(0, 0, 1))
-
- if PathGeom.pointsCoincide(normal, FreeCAD.Vector(0, 0, 0)):
- return None
-
- return self._getDirectedNormal(e0.valueAt(midparam), normal.normalize())
-
- def getExtensionFaces(self, extensionWire):
- '''getExtensionFace(extensionWire)...
- A public helper method to retrieve the requested extension as a face,
- rather than a wire because some extensions require a face shape
- for definition that allows for two wires for boundary definition.
- '''
-
- if self.extFaces:
- return self.extFaces
-
- return [Part.Face(extensionWire)]
-
- def getWire(self):
- '''getWire()... Public method to retrieve the extension area, pertaining to the feature
- and sub element provided at class instantiation, as a closed wire. If no closed wire
- is possible, a `None` value is returned.'''
- PathLog.track()
- if PathGeom.isRoughly(0, self.length.Value) or not self.sub:
- PathLog.debug("no extension, length=%.2f, sub=%s" % (self.length.Value, self.sub))
- return None
-
- feature = self.obj.Shape.getElement(self.feature)
- edges = self._getEdges()
- sub = Part.Wire(Part.sortEdges(edges)[0])
-
- if 1 == len(edges):
- PathLog.debug("Extending single edge wire")
- edge = edges[0]
- if Part.Circle == type(edge.Curve):
- circle = edge.Curve
- # for a circle we have to figure out if it's a hole or a cylinder
- p0 = edge.valueAt(edge.FirstParameter)
- normal = (edge.Curve.Center - p0).normalize()
- direction = self._getDirectedNormal(p0, normal)
- if direction is None:
- return None
-
- if PathGeom.pointsCoincide(normal, direction):
- r = circle.Radius - self.length.Value
- else:
- r = circle.Radius + self.length.Value
- # assuming the offset produces a valid circle - go for it
- if r > 0:
- e3 = Part.makeCircle(r, circle.Center, circle.Axis, edge.FirstParameter * 180 / math.pi, edge.LastParameter * 180 / math.pi)
- if endPoints(edge):
- # need to construct the arc slice
- e0 = Part.makeLine(edge.valueAt(edge.FirstParameter), e3.valueAt(e3.FirstParameter))
- e2 = Part.makeLine(edge.valueAt(edge.LastParameter), e3.valueAt(e3.LastParameter))
- return Part.Wire([e0, edge, e2, e3])
-
- extWire = Part.Wire([e3])
- self.extFaces = [self._makeCircularExtFace(edge, extWire)]
- return extWire
-
- # the extension is bigger than the hole - so let's just cover the whole hole
- if endPoints(edge):
- # if the resulting arc is smaller than the radius, create a pie slice
- PathLog.track()
- center = circle.Center
- e0 = Part.makeLine(center, edge.valueAt(edge.FirstParameter))
- e2 = Part.makeLine(edge.valueAt(edge.LastParameter), center)
- return Part.Wire([e0, edge, e2])
- PathLog.track()
- return Part.Wire([edge])
-
- else:
- PathLog.track(self.feature, self.sub, type(edge.Curve), endPoints(edge))
- direction = self._getDirection(sub)
- if direction is None:
- return None
- # return self._extendEdge(feature, edge, direction)
- return self._extendEdge(feature, edges[0], direction)
- return extendWire(feature, sub, self.length.Value)
-
- def _makeCircularExtFace(self, edge, extWire):
- '''_makeCircularExtensionFace(edge, extWire)...
- Create proper circular extension face shape. Incoming edge is expected to be a circle.
- '''
- # Add original outer wire to cut faces if necessary
- edgeFace = Part.Face(Part.Wire([edge]))
- edgeFace.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - edgeFace.BoundBox.ZMin))
- extWireFace = Part.Face(extWire)
- extWireFace.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - extWireFace.BoundBox.ZMin))
-
- if extWireFace.Area >= edgeFace.Area:
- extensionFace = extWireFace.cut(edgeFace)
- else:
- extensionFace = edgeFace.cut(extWireFace)
- extensionFace.translate(FreeCAD.Vector(0.0, 0.0, edge.BoundBox.ZMin))
-
- return extensionFace
-# Eclass
-
-
class ObjectPocket(PathPocketBase.ObjectPocket):
'''Proxy object for Pocket operation.'''
@@ -293,16 +64,8 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
'''initPocketOp(obj) ... setup receiver'''
if not hasattr(obj, 'UseOutline'):
obj.addProperty('App::PropertyBool', 'UseOutline', 'Pocket', QtCore.QT_TRANSLATE_NOOP('PathPocketShape', 'Uses the outline of the base geometry.'))
- obj.UseOutline = False
- if not hasattr(obj, 'ExtensionLengthDefault'):
- obj.addProperty('App::PropertyDistance', 'ExtensionLengthDefault', 'Extension', QtCore.QT_TRANSLATE_NOOP('PathPocketShape', 'Default length of extensions.'))
- if not hasattr(obj, 'ExtensionFeature'):
- obj.addProperty('App::PropertyLinkSubListGlobal', 'ExtensionFeature', 'Extension', QtCore.QT_TRANSLATE_NOOP('PathPocketShape', 'List of features to extend.'))
- if not hasattr(obj, 'ExtensionCorners'):
- obj.addProperty('App::PropertyBool', 'ExtensionCorners', 'Extension', QtCore.QT_TRANSLATE_NOOP('PathPocketShape', 'When enabled connected extension edges are combined to wires.'))
- obj.ExtensionCorners = True
- obj.setEditorMode('ExtensionFeature', 2)
+ FeatureExtensions.initialize_properties(obj)
def areaOpOnDocumentRestored(self, obj):
'''opOnDocumentRestored(obj) ... adds the UseOutline property if it doesn't exist.'''
@@ -311,74 +74,72 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
def pocketInvertExtraOffset(self):
return False
+ def areaOpSetDefaultValues(self, obj, job):
+ '''areaOpSetDefaultValues(obj, job) ... set default values'''
+ obj.StepOver = 100
+ obj.ZigZagAngle = 45
+ obj.UseOutline = False
+ FeatureExtensions.set_default_property_values(obj, job)
+
def areaOpShapes(self, obj):
'''areaOpShapes(obj) ... return shapes representing the solids to be removed.'''
PathLog.track()
+ # self.isDebug = True if PathLog.getLevel(PathLog.thisModule()) == 4 else False
+ self.removalshapes = []
+ avoidFeatures = list()
+
+ # Get extensions and identify faces to avoid
+ extensions = FeatureExtensions.getExtensions(obj)
+ for e in extensions:
+ if e.avoid:
+ avoidFeatures.append(e.feature)
+
if obj.Base:
PathLog.debug('base items exist. Processing...')
- self.removalshapes = []
self.horiz = []
- vertical = []
- for o in obj.Base:
- PathLog.debug('Base item: {}'.format(o))
- base = o[0]
- for sub in o[1]:
+ self.vert = []
+ for (base, subList) in obj.Base:
+ for sub in subList:
if 'Face' in sub:
- face = base.Shape.getElement(sub)
- if type(face.Surface) == Part.Plane and PathGeom.isVertical(face.Surface.Axis):
- # it's a flat horizontal face
- self.horiz.append(face)
- elif type(face.Surface) == Part.Cylinder and PathGeom.isVertical(face.Surface.Axis):
- # vertical cylinder wall
- if any(e.isClosed() for e in face.Edges):
- # complete cylinder
- circle = Part.makeCircle(face.Surface.Radius, face.Surface.Center)
- disk = Part.Face(Part.Wire(circle))
- self.horiz.append(disk)
- else:
- # partial cylinder wall
- vertical.append(face)
- elif type(face.Surface) == Part.Plane and PathGeom.isHorizontal(face.Surface.Axis):
- vertical.append(face)
- else:
+ if sub not in avoidFeatures and not self.clasifySub(base, sub):
PathLog.error(translate('PathPocket', 'Pocket does not support shape %s.%s') % (base.Label, sub))
- self.vertical = PathGeom.combineConnectedShapes(vertical)
- self.vWires = [TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) for shape in self.vertical]
- for wire in self.vWires:
- w = PathGeom.removeDuplicateEdges(wire)
- face = Part.Face(w)
- face.tessellate(0.1)
- if PathGeom.isRoughly(face.Area, 0):
- PathLog.error(translate('PathPocket', 'Vertical faces do not form a loop - ignoring'))
- else:
- self.horiz.append(face)
+ # Convert horizontal faces to use outline only if requested
+ if obj.UseOutline and self.horiz:
+ horiz = [Part.Face(f.Wire1) for f in self.horiz]
+ self.horiz = horiz
- # add faces for extensions
- self.exts = [] # pylint: disable=attribute-defined-outside-init
- for ext in self.getExtensions(obj):
- wire = ext.getWire()
- if wire:
- for face in ext.getExtensionFaces(wire):
+ # Check if selected vertical faces form a loop
+ if len(self.vert) > 0:
+ self.vertical = PathGeom.combineConnectedShapes(self.vert)
+ self.vWires = [TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) for shape in self.vertical]
+ for wire in self.vWires:
+ w = PathGeom.removeDuplicateEdges(wire)
+ face = Part.Face(w)
+ # face.tessellate(0.1)
+ if PathGeom.isRoughly(face.Area, 0):
+ PathLog.error(translate('PathPocket', 'Vertical faces do not form a loop - ignoring'))
+ else:
self.horiz.append(face)
- self.exts.append(face)
- # Place all self.horiz faces into same working plane
- for h in self.horiz:
- h.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - h.BoundBox.ZMin))
+ # Add faces for extensions
+ self.exts = [] # pylint: disable=attribute-defined-outside-init
+ for ext in extensions:
+ if not ext.avoid:
+ wire = ext.getWire()
+ if wire:
+ faces = ext.getExtensionFaces(wire)
+ for f in faces:
+ self.horiz.append(f)
+ self.exts.append(f)
- # check all faces and see if they are touching/overlapping and combine those into a compound
- self.horizontal = []
- for shape in PathGeom.combineConnectedShapes(self.horiz):
- shape.sewShape()
- shape.tessellate(0.05) # originally 0.1
- if obj.UseOutline:
- wire = TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1))
- wire.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - wire.BoundBox.ZMin))
- self.horizontal.append(Part.Face(wire))
- else:
- self.horizontal.append(shape)
+ # check all faces and see if they are touching/overlapping and combine and simplify
+ self.horizontal = PathGeom.combineHorizontalFaces(self.horiz)
+
+ # Move all faces to final depth before extrusion
+ for h in self.horizontal:
+ h.translate(FreeCAD.Vector(0.0, 0.0, obj.FinalDepth.Value - h.BoundBox.ZMin))
# extrude all faces up to StartDepth and those are the removal shapes
extent = FreeCAD.Vector(0, 0, obj.StartDepth.Value - obj.FinalDepth.Value)
@@ -389,7 +150,6 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
self.outlines = [Part.Face(TechDraw.findShapeOutline(base.Shape, 1, FreeCAD.Vector(0, 0, 1))) for base in self.model]
stockBB = self.stock.Shape.BoundBox
- self.removalshapes = []
self.bodies = []
for outline in self.outlines:
outline.translate(FreeCAD.Vector(0, 0, stockBB.ZMin - 1))
@@ -397,49 +157,88 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
self.bodies.append(body)
self.removalshapes.append((self.stock.Shape.cut(body), False))
- for (shape, hole) in self.removalshapes:
- shape.tessellate(0.05) # originally 0.1
+ # Tessellate all working faces
+ # for (shape, hole) in self.removalshapes:
+ # shape.tessellate(0.05) # originally 0.1
if self.removalshapes:
obj.removalshape = self.removalshapes[0][0]
return self.removalshapes
- def areaOpSetDefaultValues(self, obj, job):
- '''areaOpSetDefaultValues(obj, job) ... set default values'''
- obj.StepOver = 100
- obj.ZigZagAngle = 45
- obj.UseOutline = False
- obj.ExtensionCorners = True
- if job and job.Stock:
- bb = job.Stock.Shape.BoundBox
- obj.OpFinalDepth = bb.ZMin
- obj.OpStartDepth = bb.ZMax
- obj.setExpression('ExtensionLengthDefault', 'OpToolDiameter / 2')
+ # Support methods
+ def isVerticalExtrusionFace(self, face):
+ fBB = face.BoundBox
+ if PathGeom.isRoughly(fBB.ZLength, 0.0):
+ return False
+ extr = face.extrude(FreeCAD.Vector(0.0, 0.0, fBB.ZLength))
+ if hasattr(extr, "Volume"):
+ if PathGeom.isRoughly(extr.Volume, 0.0):
+ return True
+ return False
- def createExtension(self, obj, extObj, extFeature, extSub):
- return Extension(extObj, extFeature, extSub, obj.ExtensionLengthDefault, Extension.DirectionNormal)
+ def clasifySub(self, bs, sub):
+ '''clasifySub(bs, sub)...
+ Given a base and a sub-feature name, returns True
+ if the sub-feature is a horizontally oriented flat face.
+ '''
+ face = bs.Shape.getElement(sub)
- def getExtensions(self, obj):
- extensions = []
- i = 0
- for extObj, features in obj.ExtensionFeature:
- for sub in features:
- extFeature, extSub = sub.split(':')
- extensions.append(self.createExtension(obj, extObj, extFeature, extSub))
- i = i + 1
- return extensions
+ if type(face.Surface) == Part.Plane:
+ PathLog.debug('type() == Part.Plane')
+ if PathGeom.isVertical(face.Surface.Axis):
+ PathLog.debug(' -isVertical()')
+ # it's a flat horizontal face
+ self.horiz.append(face)
+ return True
- def setExtensions(self, obj, extensions):
- PathLog.track(obj.Label, len(extensions))
- obj.ExtensionFeature = [(ext.obj, ext.getSubLink()) for ext in extensions]
+ elif PathGeom.isHorizontal(face.Surface.Axis):
+ PathLog.debug(' -isHorizontal()')
+ self.vert.append(face)
+ return True
+
+ else:
+ return False
+
+ elif type(face.Surface) == Part.Cylinder and PathGeom.isVertical(face.Surface.Axis):
+ PathLog.debug('type() == Part.Cylinder')
+ # vertical cylinder wall
+ if any(e.isClosed() for e in face.Edges):
+ PathLog.debug(' -e.isClosed()')
+ # complete cylinder
+ circle = Part.makeCircle(face.Surface.Radius, face.Surface.Center)
+ disk = Part.Face(Part.Wire(circle))
+ disk.translate(FreeCAD.Vector(0, 0, face.BoundBox.ZMin - disk.BoundBox.ZMin))
+ self.horiz.append(disk)
+ return True
+
+ else:
+ PathLog.debug(' -none isClosed()')
+ # partial cylinder wall
+ self.vert.append(face)
+ return True
+
+ elif type(face.Surface) == Part.SurfaceOfExtrusion:
+ # extrusion wall
+ PathLog.debug('type() == Part.SurfaceOfExtrusion')
+ # Save face to self.horiz for processing or display error
+ if self.isVerticalExtrusionFace(face):
+ self.vert.append(face)
+ return True
+ else:
+ PathLog.error(translate("Path", "Failed to identify vertical face from {}.".format(sub)))
+
+ else:
+ PathLog.debug(' -type(face.Surface): {}'.format(type(face.Surface)))
+ return False
+# Eclass
def SetupProperties():
- setup = PathPocketBase.SetupProperties()
+ setup = PathPocketBase.SetupProperties() # Add properties from PocketBase module
+ setup.extend(FeatureExtensions.SetupProperties()) # Add properties from Extensions Feature
+
+ # Add properties initialized here in PocketShape
setup.append('UseOutline')
- setup.append('ExtensionLengthDefault')
- setup.append('ExtensionFeature')
- setup.append('ExtensionCorners')
return setup
diff --git a/src/Mod/Path/PathScripts/PathPocketShapeGui.py b/src/Mod/Path/PathScripts/PathPocketShapeGui.py
index c7e96cc796..b7e24d8fbb 100644
--- a/src/Mod/Path/PathScripts/PathPocketShapeGui.py
+++ b/src/Mod/Path/PathScripts/PathPocketShapeGui.py
@@ -29,6 +29,8 @@ import PathScripts.PathLog as PathLog
import PathScripts.PathOpGui as PathOpGui
import PathScripts.PathPocketShape as PathPocketShape
import PathScripts.PathPocketBaseGui as PathPocketBaseGui
+import PathScripts.PathFeatureExtensions as FeatureExtensions
+import PathScripts.PathFeatureExtensionsGui as PathFeatureExtensionsGui
from PySide import QtCore, QtGui
from pivy import coin
@@ -48,447 +50,6 @@ def translate(context, text, disambig=None):
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
#PathLog.trackModule(PathLog.thisModule())
-class _Extension(object):
- ColourEnabled = (1.0, .5, 1.0)
- ColourDisabled = (1.0, 1.0, .5)
- TransparencySelected = 0.0
- TransparencyDeselected = 0.7
-
- def __init__(self, obj, base, face, edge):
- self.obj = obj
- self.base = base
- self.face = face
- self.edge = edge
- if edge is None:
- self.ext = None
- else:
- self.ext = obj.Proxy.createExtension(obj, base, face, edge)
- self.switch = self.createExtensionSoSwitch(self.ext)
- self.root = self.switch
-
- def createExtensionSoSwitch(self, ext):
- sep = coin.SoSeparator()
- pos = coin.SoTranslation()
- mat = coin.SoMaterial()
- crd = coin.SoCoordinate3()
- fce = coin.SoFaceSet()
- hnt = coin.SoShapeHints()
-
- if not ext is None:
- numVert = list() # track number of vertices in each polygon face
- try:
- wire = ext.getWire()
- except FreeCAD.Base.FreeCADError:
- wire = None
- if wire:
- if isinstance(wire, (list, tuple)):
- p0 = [p for p in wire[0].discretize(Deflection=0.02)]
- p1 = [p for p in wire[1].discretize(Deflection=0.02)]
- p2 = list(reversed(p1))
- polygon = [(p.x, p.y, p.z) for p in (p0 + p2)]
- else:
- if ext.extFaces:
- # Create polygon for each extension face in compound extensions
- allPolys = list()
- extFaces = ext.getExtensionFaces(wire)
- for f in extFaces:
- pCnt = 0
- for w in f.Wires:
- poly = [p for p in w.discretize(Deflection=0.01)]
- pCnt += len(poly)
- allPolys.extend(poly)
- numVert.append(pCnt)
- polygon = [(p.x, p.y, p.z) for p in allPolys]
- else:
- # poly = [p for p in wire.discretize(Deflection=0.02)][:-1]
- poly = [p for p in wire.discretize(Deflection=0.02)]
- polygon = [(p.x, p.y, p.z) for p in poly]
- crd.point.setValues(polygon)
- else:
- return None
-
- mat.diffuseColor = self.ColourDisabled
- mat.transparency = self.TransparencyDeselected
-
- hnt.faceType = coin.SoShapeHints.UNKNOWN_FACE_TYPE
- hnt.vertexOrdering = coin.SoShapeHints.CLOCKWISE
-
- if numVert:
- # Transfer vertex counts for polygon faces
- fce.numVertices.setValues(tuple(numVert))
-
- sep.addChild(pos)
- sep.addChild(mat)
- sep.addChild(hnt)
- sep.addChild(crd)
- sep.addChild(fce)
-
- switch = coin.SoSwitch()
- switch.addChild(sep)
- switch.whichChild = coin.SO_SWITCH_NONE
-
- self.material = mat
-
- return switch
-
- def _setColour(self, r, g, b):
- self.material.diffuseColor = (r, g, b)
-
- def isValid(self):
- return not self.root is None
-
- def show(self):
- if self.switch:
- self.switch.whichChild = coin.SO_SWITCH_ALL
-
- def hide(self):
- if self.switch:
- self.switch.whichChild = coin.SO_SWITCH_NONE
-
- def enable(self, ena = True):
- if ena:
- self.material.diffuseColor = self.ColourEnabled
- else:
- self.disable()
-
- def disable(self):
- self.material.diffuseColor = self.ColourDisabled
-
- def select(self):
- self.material.transparency = self.TransparencySelected
-
- def deselect(self):
- self.material.transparency = self.TransparencyDeselected
-
-class TaskPanelExtensionPage(PathOpGui.TaskPanelPage):
- DataObject = QtCore.Qt.ItemDataRole.UserRole
- DataSwitch = QtCore.Qt.ItemDataRole.UserRole + 2
-
- Direction = {
- PathPocketShape.Extension.DirectionNormal: translate('PathPocket', 'Normal'),
- PathPocketShape.Extension.DirectionX: translate('PathPocket', 'X'),
- PathPocketShape.Extension.DirectionY: translate('PathPocket', 'Y')
- }
-
- def initPage(self, obj):
- self.setTitle("Extensions")
- self.extensions = obj.Proxy.getExtensions(obj) # pylint: disable=attribute-defined-outside-init
-
- self.defaultLength = PathGui.QuantitySpinBox(self.form.defaultLength, obj, 'ExtensionLengthDefault') # pylint: disable=attribute-defined-outside-init
-
- self.form.extensionTree.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
- self.form.extensionTree.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
-
- self.switch = coin.SoSwitch() # pylint: disable=attribute-defined-outside-init
- self.obj.ViewObject.RootNode.addChild(self.switch)
- self.switch.whichChild = coin.SO_SWITCH_ALL
-
- self.model = QtGui.QStandardItemModel(self.form.extensionTree) # pylint: disable=attribute-defined-outside-init
- self.model.setHorizontalHeaderLabels(['Base', 'Extension'])
-
- if 0 < len(obj.ExtensionFeature):
- self.form.showExtensions.setCheckState(QtCore.Qt.Checked)
- else:
- self.form.showExtensions.setCheckState(QtCore.Qt.Unchecked)
-
- self.blockUpdateData = False # pylint: disable=attribute-defined-outside-init
-
- def cleanupPage(self, obj):
- try:
- self.obj.ViewObject.RootNode.removeChild(self.switch)
- except ReferenceError:
- PathLog.debug("obj already destroyed - no cleanup required")
-
- def getForm(self):
- return FreeCADGui.PySideUic.loadUi(":/panels/PageOpPocketExtEdit.ui")
-
- def forAllItemsCall(self, cb):
- for modelRow in range(self.model.rowCount()):
- model = self.model.item(modelRow, 0)
- for featureRow in range(model.rowCount()):
- feature = model.child(featureRow, 0)
- for edgeRow in range(feature.rowCount()):
- item = feature.child(edgeRow, 0)
- ext = item.data(self.DataObject)
- cb(item, ext)
-
- def currentExtensions(self):
- extensions = []
- def extractExtension(item, ext):
- if ext and ext.edge and item.checkState() == QtCore.Qt.Checked:
- extensions.append(ext.ext)
- self.forAllItemsCall(extractExtension)
- PathLog.track('extensions', extensions)
- return extensions
-
- def updateProxyExtensions(self, obj):
- self.extensions = self.currentExtensions() # pylint: disable=attribute-defined-outside-init
- obj.Proxy.setExtensions(obj, self.extensions)
-
- def getFields(self, obj):
- PathLog.track(obj.Label, self.model.rowCount(), self.model.columnCount())
- self.blockUpdateData = True # pylint: disable=attribute-defined-outside-init
-
- if obj.ExtensionCorners != self.form.extendCorners.isChecked():
- obj.ExtensionCorners = self.form.extendCorners.isChecked()
- self.defaultLength.updateProperty()
-
- self.updateProxyExtensions(obj)
- self.blockUpdateData = False # pylint: disable=attribute-defined-outside-init
-
- def setFields(self, obj):
- PathLog.track(obj.Label)
-
- if obj.ExtensionCorners != self.form.extendCorners.isChecked():
- self.form.extendCorners.toggle()
- self.updateQuantitySpinBoxes()
- self.extensions = obj.Proxy.getExtensions(obj) # pylint: disable=attribute-defined-outside-init
- self.setExtensions(self.extensions)
-
- def createItemForBaseModel(self, base, sub, edges, extensions):
- PathLog.track(base.Label, sub, '+', len(edges), len(base.Shape.getElement(sub).Edges))
- ext = _Extension(self.obj, base, sub, None)
- item = QtGui.QStandardItem()
- item.setData(sub, QtCore.Qt.EditRole)
- item.setData(ext, self.DataObject)
- item.setSelectable(False)
-
- extendCorners = self.form.extendCorners.isChecked()
-
- def createSubItem(label, ext0):
- if ext0.root:
- self.switch.addChild(ext0.root)
- item0 = QtGui.QStandardItem()
- item0.setData(label, QtCore.Qt.EditRole)
- item0.setData(ext0, self.DataObject)
- item0.setCheckable(True)
- for e in extensions:
- if e.obj == base and e.sub == label:
- item0.setCheckState(QtCore.Qt.Checked)
- ext0.enable()
- break
- item.appendRow([item0])
-
- extensionEdges = {}
- for edge in base.Shape.getElement(sub).Edges:
- for (e, label) in edges:
- if edge.isSame(e):
- ext0 = _Extension(self.obj, base, sub, label)
- if ext0.isValid():
- extensionEdges[e] = label[4:]
- if not extendCorners:
- createSubItem(label, ext0)
- break
-
- if extendCorners:
- def edgesMatchShape(e0, e1):
- flipped = PathGeom.flipEdge(e1)
- if flipped:
- return PathGeom.edgesMatch(e0, e1) or PathGeom.edgesMatch(e0, flipped)
- else:
- return PathGeom.edgesMatch(e0, e1)
-
- self.extensionEdges = extensionEdges # pylint: disable=attribute-defined-outside-init
- for edgeList in Part.sortEdges(list(extensionEdges.keys())):
- self.edgeList = edgeList # pylint: disable=attribute-defined-outside-init
- if len(edgeList) == 1:
- label = "Edge%s" % [extensionEdges[keyEdge] for keyEdge in extensionEdges.keys() if edgesMatchShape(keyEdge, edgeList[0])][0]
- else:
- label = "Wire(%s)" % ','.join(sorted([extensionEdges[keyEdge] for e in edgeList for keyEdge in extensionEdges.keys() if edgesMatchShape(e, keyEdge)], key=lambda s: int(s))) # pylint: disable=unnecessary-lambda
- ext0 = _Extension(self.obj, base, sub, label)
- createSubItem(label, ext0)
-
- return item
-
- def setExtensions(self, extensions):
- PathLog.track(len(extensions))
- self.form.extensionTree.blockSignals(True)
-
- # remember current visual state
- if hasattr(self, 'selectionModel'):
- selectedExtensions = [self.model.itemFromIndex(index).data(self.DataObject).ext for index in self.selectionModel.selectedIndexes()]
- else:
- selectedExtensions = []
- collapsedModels = []
- collapsedFeatures = []
- for modelRow in range(self.model.rowCount()):
- model = self.model.item(modelRow, 0)
- modelName = model.data(QtCore.Qt.EditRole)
- if not self.form.extensionTree.isExpanded(model.index()):
- collapsedModels.append(modelName)
- for featureRow in range(model.rowCount()):
- feature = model.child(featureRow, 0)
- if not self.form.extensionTree.isExpanded(feature.index()):
- collapsedFeatures.append("%s.%s" % (modelName, feature.data(QtCore.Qt.EditRole)))
-
- # remove current extensions and all their visuals
- def removeItemSwitch(item, ext):
- # pylint: disable=unused-argument
- ext.hide()
- if ext.root:
- self.switch.removeChild(ext.root)
- self.forAllItemsCall(removeItemSwitch)
- self.model.clear()
-
- # create extensions for model and given argument
- for base in self.obj.Base:
- edges = [(edge, "Edge%d" % (i + 1)) for i, edge in enumerate(base[0].Shape.Edges)]
- baseItem = QtGui.QStandardItem()
- baseItem.setData(base[0].Label, QtCore.Qt.EditRole)
- baseItem.setSelectable(False)
- for sub in sorted(base[1]):
- baseItem.appendRow(self.createItemForBaseModel(base[0], sub, edges, extensions))
- self.model.appendRow(baseItem)
-
- self.form.extensionTree.setModel(self.model)
- self.form.extensionTree.expandAll()
- self.form.extensionTree.resizeColumnToContents(0)
-
- # restore previous state - at least the parts that are still valid
- for modelRow in range(self.model.rowCount()):
- model = self.model.item(modelRow, 0)
- modelName = model.data(QtCore.Qt.EditRole)
- if modelName in collapsedModels:
- self.form.extensionTree.setExpanded(model.index(), False)
- for featureRow in range(model.rowCount()):
- feature = model.child(featureRow, 0)
- featureName = "%s.%s" % (modelName, feature.data(QtCore.Qt.EditRole))
- if featureName in collapsedFeatures:
- self.form.extensionTree.setExpanded(feature.index(), False)
- if hasattr(self, 'selectionModel') and selectedExtensions:
- self.restoreSelection(selectedExtensions)
-
- self.form.extensionTree.blockSignals(False)
-
- def updateQuantitySpinBoxes(self, index = None):
- self.defaultLength.updateSpinBox()
-
- def updateData(self, obj, prop):
- PathLog.track(obj.Label, prop, self.blockUpdateData)
- if not self.blockUpdateData:
- if prop in ['Base', 'ExtensionLengthDefault']:
- self.setExtensions(obj.Proxy.getExtensions(obj))
- self.updateQuantitySpinBoxes()
-
- def restoreSelection(self, selection):
- PathLog.track()
- if 0 == self.model.rowCount():
- PathLog.track('-')
- self.form.buttonClear.setEnabled(False)
- self.form.buttonDisable.setEnabled(False)
- self.form.buttonEnable.setEnabled(False)
- else:
- self.form.buttonClear.setEnabled(True)
-
- if selection or self.selectionModel.selectedIndexes():
- self.form.buttonDisable.setEnabled(True)
- self.form.buttonEnable.setEnabled(True)
- else:
- self.form.buttonDisable.setEnabled(False)
- self.form.buttonEnable.setEnabled(False)
-
- FreeCADGui.Selection.clearSelection()
-
- def selectItem(item, ext):
- # pylint: disable=unused-argument
- for sel in selection:
- if ext.base == sel.obj and ext.edge == sel.sub:
- return True
- return False
-
- def setSelectionVisuals(item, ext):
- if selectItem(item, ext):
- self.selectionModel.select(item.index(), QtCore.QItemSelectionModel.Select)
-
- selected = self.selectionModel.isSelected(item.index())
- if selected:
- FreeCADGui.Selection.addSelection(ext.base, ext.face)
- ext.select()
- else:
- ext.deselect()
-
- if self.form.showExtensions.isChecked() or selected:
- ext.show()
- else:
- ext.hide()
- self.forAllItemsCall(setSelectionVisuals)
-
- def selectionChanged(self):
- self.restoreSelection([])
-
- def extensionsClear(self):
- def disableItem(item, ext):
- item.setCheckState(QtCore.Qt.Unchecked)
- ext.disable()
-
- self.forAllItemsCall(disableItem)
- self.setDirty()
-
- def _extensionsSetState(self, state):
- PathLog.track(state)
- for index in self.selectionModel.selectedIndexes():
- item = self.model.itemFromIndex(index)
- ext = item.data(self.DataObject)
- if ext.edge:
- item.setCheckState(state)
- ext.enable(state == QtCore.Qt.Checked)
- self.setDirty()
-
- def extensionsDisable(self):
- self._extensionsSetState(QtCore.Qt.Unchecked)
-
- def extensionsEnable(self):
- self._extensionsSetState(QtCore.Qt.Checked)
-
- def updateItemEnabled(self, item):
- PathLog.track(item)
- ext = item.data(self.DataObject)
- if item.checkState() == QtCore.Qt.Checked:
- ext.enable()
- else:
- ext.disable()
- self.updateProxyExtensions(self.obj)
- self.setDirty()
-
- def showHideExtension(self):
- if self.form.showExtensions.isChecked():
- def enableExtensionEdit(item, ext):
- # pylint: disable=unused-argument
- ext.show()
- self.forAllItemsCall(enableExtensionEdit)
- else:
- def disableExtensionEdit(item, ext):
- if not self.selectionModel.isSelected(item.index()):
- ext.hide()
- self.forAllItemsCall(disableExtensionEdit)
- #self.setDirty()
-
- def toggleExtensionCorners(self):
- PathLog.track()
- self.setExtensions(self.obj.Proxy.getExtensions(self.obj))
- self.selectionChanged()
- self.setDirty()
-
- def getSignalsForUpdate(self, obj):
- PathLog.track(obj.Label)
- signals = []
- signals.append(self.form.defaultLength.editingFinished)
- return signals
-
- def registerSignalHandlers(self, obj):
- self.form.showExtensions.clicked.connect(self.showHideExtension)
- self.form.extendCorners.clicked.connect(self.toggleExtensionCorners)
- self.form.buttonClear.clicked.connect(self.extensionsClear)
- self.form.buttonDisable.clicked.connect(self.extensionsDisable)
- self.form.buttonEnable.clicked.connect(self.extensionsEnable)
- self.form.defaultLength.editingFinished.connect(self.updateQuantitySpinBoxes)
-
- self.model.itemChanged.connect(self.updateItemEnabled)
-
- self.selectionModel = self.form.extensionTree.selectionModel() # pylint: disable=attribute-defined-outside-init
- self.selectionModel.selectionChanged.connect(self.selectionChanged)
- self.selectionChanged()
-
class TaskPanelOpPage(PathPocketBaseGui.TaskPanelOpPage):
'''Page controller class for Pocket operation'''
@@ -498,7 +59,7 @@ class TaskPanelOpPage(PathPocketBaseGui.TaskPanelOpPage):
def taskPanelBaseLocationPage(self, obj, features):
if not hasattr(self, 'extensionsPanel'):
- self.extensionsPanel = TaskPanelExtensionPage(obj, features) # pylint: disable=attribute-defined-outside-init
+ self.extensionsPanel = PathFeatureExtensionsGui.TaskPanelExtensionPage(obj, features) # pylint: disable=attribute-defined-outside-init
return self.extensionsPanel
Command = PathOpGui.SetupOperation('Pocket Shape',
diff --git a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py
index 05c5c9bd73..bb007e5658 100644
--- a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py
+++ b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py
@@ -171,6 +171,7 @@ class OpPrototype(object):
'App::PropertyVectorDistance': Property,
'App::PropertyVectorList': Property,
'Part::PropertyPartShape': Property,
+ 'App::PropertyPythonObject': PropertyString,
}
def __init__(self, name):