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):