Path: Refactor and upgradeExtensions feature, and apply to Adaptive op

Path: Refactor `Extensions` Gui code into independent module. Move the `Extensions` Gui code to independent module so access to other operations will be easier.
Path: Add `Extensions` feature to Adaptive operation
Path: Isolate Adaptive GUI elements in preparation of Adaptive unit tests
Path: Implement `PathLog` debug module
Path: Implement `translate()` for multi-language message support
Path: Fix `StockType`check bug
Path: Relocate `getCutRegionWires()` to `FeatureExtensions` module
Path: Add `Extensions` property checks on document restored
Path: Improve `Extend Outline` feature implementation
Path: Initialize a waterline type extension
Path: Add enable/disable extensions feature. It is quite possible that many complex faces exist that have large quantities of both simple and complex edges.  For this reason, a manual push button to enable Extensions is useful so the users machine is not bogged down with extra or unnecessary computing time.  Extensions are not necessary at all times. This commit also includes an edge count threshold that will disable the Extensions feature temporarily upon initial loading of the Task Panel.  The manual enable button will do just that.
Path: Add enable extensions warning label
Path: Shorten enable/disable Extensions button message
Path: Remove run-time added Task Panel elements - this QButton and QLabel were moved to UI panel directly.
Path: Add include/ignore Edges button
Path: Improve extension preview rendering
Path: Fixes for `useOutline` modification and updates
Path: Add internal feature to cache calculated extensions for reuse
Path: Add `SetupProperties()` function and connect to GUI command
Path: Add `Avoid Face` extension to ignore base face. This feature allows for some simple access to the exterior of a selected face without clearing the face itself.  This will allow for an exterior clearing operation in a simple manner.
Path: Fix bug restricting extensions on internal closed-wires
This commit is contained in:
Russell Johnson
2021-06-04 09:48:30 -05:00
parent 1fb45a0c5e
commit fd6298d542
10 changed files with 1799 additions and 837 deletions

View File

@@ -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

View File

@@ -14,36 +14,62 @@
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="enableExtensions">
<property name="text">
<string>Click to disable Extensions</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="includeEdges">
<property name="text">
<string>Ignore Edges and Wires</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="enableExtensionsWarning">
<property name="text">
<string>---</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="extensionEdit" native="true">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="widget_2" native="true">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Default Length</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::QuantitySpinBox" name="defaultLength">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Set the extent of the dimension -the default value is half the tool diameter.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="minimum">
<double>-999999999.000000000000000</double>
</property>
<property name="maximum">
<double>999999999.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="1">
<item row="2" column="1">
<widget class="QCheckBox" name="extendCorners">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Extend the corner between two edges of a pocket. If selected adjacent edges are combined.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@@ -56,7 +82,27 @@
</property>
</widget>
</item>
<item row="0" column="0">
<item row="3" column="1">
<widget class="Gui::QuantitySpinBox" name="defaultLength">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Set the extent of the dimension -the default value is half the tool diameter.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="minimum">
<double>-999999999.000000000000000</double>
</property>
<property name="maximum">
<double>999999999.000000000000000</double>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Default Length</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="showExtensions">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If selected all potential extensions are visualised. Enabled extensions in purple and not enabled extensions in yellow.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>

View File

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

View File

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

View File

@@ -0,0 +1,612 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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

View File

@@ -0,0 +1,768 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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")

View File

@@ -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

View File

@@ -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

View File

@@ -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',

View File

@@ -171,6 +171,7 @@ class OpPrototype(object):
'App::PropertyVectorDistance': Property,
'App::PropertyVectorList': Property,
'Part::PropertyPartShape': Property,
'App::PropertyPythonObject': PropertyString,
}
def __init__(self, name):