Merge pull request #4388 from Russ4262/feature_extensions_to_adaptive
[0.20] [Path] Improved `Extensions` feature and add to Adaptive operation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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><html><head/><body><p>Set the extent of the dimension -the default value is half the tool diameter.</p></body></html></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><html><head/><body><p>Extend the corner between two edges of a pocket. If selected adjacent edges are combined.</p></body></html></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><html><head/><body><p>Set the extent of the dimension -the default value is half the tool diameter.</p></body></html></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><html><head/><body><p>If selected all potential extensions are visualised. Enabled extensions in purple and not enabled extensions in yellow.</p></body></html></string>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
612
src/Mod/Path/PathScripts/PathFeatureExtensions.py
Normal file
612
src/Mod/Path/PathScripts/PathFeatureExtensions.py
Normal 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
|
||||
768
src/Mod/Path/PathScripts/PathFeatureExtensionsGui.py
Normal file
768
src/Mod/Path/PathScripts/PathFeatureExtensionsGui.py
Normal 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")
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -171,6 +171,7 @@ class OpPrototype(object):
|
||||
'App::PropertyVectorDistance': Property,
|
||||
'App::PropertyVectorList': Property,
|
||||
'Part::PropertyPartShape': Property,
|
||||
'App::PropertyPythonObject': PropertyString,
|
||||
}
|
||||
|
||||
def __init__(self, name):
|
||||
|
||||
Reference in New Issue
Block a user