diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui
index bb14461cc4..45902b44ff 100644
--- a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui
@@ -7,7 +7,7 @@
0
0
368
- 400
+ 442
@@ -57,142 +57,24 @@
-
-
-
-
-
- <html><head/><body><p>Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.</p></body></html>
-
-
-
-
- Planar
-
-
- -
-
- Rotational
-
-
-
-
- -
-
-
- <html><head/><body><p>Complete the operation in a single pass at depth, or mulitiple passes to final depth.</p></body></html>
-
-
-
-
- Single-pass
-
-
- -
-
- Multi-pass
-
-
-
-
- -
-
-
- <html><head/><body><p>The amount by which the tool is laterally displaced on each cycle of the pattern, specified in percent of the tool diameter.</p><p>A step over of 100% results in no overlap between two different cycles.</p></body></html>
-
-
- 1
-
-
- 100
-
-
- 10
-
-
- 100
-
-
-
- -
-
+
-
+
- Step over
+ Cut Pattern
- -
+
-
Sample interval
- -
-
-
- Layer Mode
-
-
-
- -
-
-
- <html><head/><body><p>Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.</p></body></html>
-
-
- Optimize Linear Paths
-
-
-
- -
-
-
- Drop Cutter Direction
-
-
-
- -
-
-
- BoundBox extra offset X, Y
-
-
-
- -
-
-
- <html><head/><body><p>Make True, if specifying a Start Point</p></body></html>
-
-
- Use Start Point
-
-
-
- -
-
-
- Scan Type
-
-
-
- -
-
-
- BoundBox
-
-
-
- -
-
-
- <html><head/><body><p>Set the Z-axis depth offset from the target surface.</p></body></html>
-
-
- mm
-
-
-
- -
+
-
-
-
+
0
@@ -208,7 +90,7 @@
-
-
+
<html><head/><body><p>Additional offset to the selected bounding box along the Y axis."</p></body></html>
@@ -219,8 +101,8 @@
- -
-
+
-
+
<html><head/><body><p>Set the sampling resolution. Smaller values quickly increase processing time.</p></body></html>
@@ -229,28 +111,21 @@
- -
-
+
-
+
+
+ <html><head/><body><p>Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.</p></body></html>
+
- Depth offset
+ Optimize Linear Paths
- -
-
-
- <html><head/><body><p>Dropcutter lines are created parallel to this axis.</p></body></html>
+
-
+
+
+ BoundBox
-
-
-
- X
-
-
- -
-
- Y
-
-
-
@@ -270,7 +145,7 @@
- -
+
-
<html><head/><body><p>Enable separate optimization of transitions between, and breaks within, each step over path.</p></body></html>
@@ -280,10 +155,37 @@
- -
-
+
-
+
+
+ <html><head/><body><p>Profile the edges of the selection.</p></body></html>
+
+
-
+
+ None
+
+
+ -
+
+ Only
+
+
+ -
+
+ First
+
+
+ -
+
+ Last
+
+
+
+
+ -
+
- Cut Pattern
+ Step over
@@ -324,6 +226,146 @@
+ -
+
+
+ <html><head/><body><p>Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.</p></body></html>
+
+
-
+
+ Planar
+
+
+ -
+
+ Rotational
+
+
+
+
+ -
+
+
+ BoundBox extra offset X, Y
+
+
+
+ -
+
+
+ Depth offset
+
+
+
+ -
+
+
+ <html><head/><body><p>Complete the operation in a single pass at depth, or mulitiple passes to final depth.</p></body></html>
+
+
-
+
+ Single-pass
+
+
+ -
+
+ Multi-pass
+
+
+
+
+ -
+
+
+ Layer Mode
+
+
+
+ -
+
+
+ Scan Type
+
+
+
+ -
+
+
+ <html><head/><body><p>Dropcutter lines are created parallel to this axis.</p></body></html>
+
+
-
+
+ X
+
+
+ -
+
+ Y
+
+
+
+
+ -
+
+
+ <html><head/><body><p>Set the Z-axis depth offset from the target surface.</p></body></html>
+
+
+ mm
+
+
+
+ -
+
+
+ Drop Cutter Direction
+
+
+
+ -
+
+
+ <html><head/><body><p>Make True, if specifying a Start Point</p></body></html>
+
+
+ Use Start Point
+
+
+
+ -
+
+
+ <html><head/><body><p>Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.</p></body></html>
+
+
+
+ -
+
+
+ Profile Edges
+
+
+
+ -
+
+
+ Avoid Last X Faces
+
+
+
+ -
+
+
+ <html><head/><body><p>The amount by which the tool is laterally displaced on each cycle of the pattern, specified in percent of the tool diameter.</p><p>A step over of 100% results in no overlap between two different cycles.</p></body></html>
+
+
+ 1
+
+
+ 100
+
+
+
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
index 82533fe061..412b32b6d0 100644
--- a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
@@ -194,12 +194,6 @@
100
-
- 10
-
-
- 100
-
-
diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py
index 9c6f2d3c0c..841e61d219 100644
--- a/src/Mod/Path/PathScripts/PathSurface.py
+++ b/src/Mod/Path/PathScripts/PathSurface.py
@@ -54,7 +54,6 @@ import math
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
-MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart')
Part = LazyLoader('Part', globals(), 'Part')
if FreeCAD.GuiUp:
@@ -72,18 +71,18 @@ def translate(context, text, disambig=None):
class ObjectSurface(PathOp.ObjectOp):
'''Proxy object for Surfacing operation.'''
- def baseObject(self):
- '''baseObject() ... returns super of receiver
- Used to call base implementation in overwritten functions.'''
- return super(self.__class__, self)
-
def opFeatures(self, obj):
- '''opFeatures(obj) ... return all standard features and edges based geometries'''
- return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces
+ '''opFeatures(obj) ... return all standard features'''
+ return PathOp.FeatureTool | PathOp.FeatureDepths \
+ | PathOp.FeatureHeights | PathOp.FeatureStepDown \
+ | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces
def initOperation(self, obj):
- '''initPocketOp(obj) ... create operation specific properties'''
- self.initOpProperties(obj)
+ '''initOperation(obj) ... Initialize the operation by
+ managing property creation and property editor status.'''
+ self.propertiesReady = False
+
+ self.initOpProperties(obj) # Initialize operation-specific properties
# For debugging
if PathLog.getLevel(PathLog.thisModule()) != 4:
@@ -94,28 +93,30 @@ class ObjectSurface(PathOp.ObjectOp):
def initOpProperties(self, obj, warn=False):
'''initOpProperties(obj) ... create operation specific properties'''
- missing = list()
+ self.addNewProps = list()
- for (prtyp, nm, grp, tt) in self.opProperties():
+ for (prtyp, nm, grp, tt) in self.opPropertyDefinitions():
if not hasattr(obj, nm):
obj.addProperty(prtyp, nm, grp, tt)
- missing.append(nm)
- if warn:
- newPropMsg = translate('PathSurface', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. '
- newPropMsg += translate('PathSurface', 'Check its default value.')
- PathLog.warning(newPropMsg)
+ self.addNewProps.append(nm)
# Set enumeration lists for enumeration properties
- if len(missing) > 0:
- ENUMS = self.propertyEnumerations()
+ if len(self.addNewProps) > 0:
+ ENUMS = self.opPropertyEnumerations()
for n in ENUMS:
- if n in missing:
+ if n in self.addNewProps:
setattr(obj, n, ENUMS[n])
- self.addedAllProperties = True
+ if warn:
+ newPropMsg = translate('PathSurface', 'New property added to')
+ newPropMsg += ' "{}": {}'.format(obj.Label, self.addNewProps) + '. '
+ newPropMsg += translate('PathSurface', 'Check default value(s).')
+ FreeCAD.Console.PrintWarning(newPropMsg + '\n')
- def opProperties(self):
- '''opProperties(obj) ... Store operation specific properties'''
+ self.propertiesReady = True
+
+ def opPropertyDefinitions(self):
+ '''opPropertyDefinitions(obj) ... Store operation specific properties'''
return [
("App::PropertyBool", "ShowTempObjects", "Debug",
@@ -179,7 +180,7 @@ class ObjectSurface(PathOp.ObjectOp):
QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")),
("App::PropertyDistance", "SampleInterval", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")),
- ("App::PropertyPercent", "StepOver", "Clearing Options",
+ ("App::PropertyFloat", "StepOver", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")),
("App::PropertyBool", "OptimizeLinearPaths", "Optimization",
@@ -199,7 +200,7 @@ class ObjectSurface(PathOp.ObjectOp):
QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"))
]
- def propertyEnumerations(self):
+ def opPropertyEnumerations(self):
# Enumeration lists for App::PropertyEnumeration properties
return {
'BoundBox': ['BaseBoundBox', 'Stock'],
@@ -214,6 +215,59 @@ class ObjectSurface(PathOp.ObjectOp):
'ScanType': ['Planar', 'Rotational']
}
+ def opPropertyDefaults(self, obj, job):
+ '''opPropertyDefaults(obj, job) ... returns a dictionary of default values
+ for the operation's properties.'''
+ defaults = {
+ 'OptimizeLinearPaths': True,
+ 'InternalFeaturesCut': True,
+ 'OptimizeStepOverTransitions': False,
+ 'CircularUseG2G3': False,
+ 'BoundaryEnforcement': True,
+ 'UseStartPoint': False,
+ 'AvoidLastX_InternalFeatures': True,
+ 'CutPatternReversed': False,
+ 'StartPoint': FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value),
+ 'ProfileEdges': 'None',
+ 'LayerMode': 'Single-pass',
+ 'ScanType': 'Planar',
+ 'RotationAxis': 'X',
+ 'CutMode': 'Conventional',
+ 'CutPattern': 'Line',
+ 'HandleMultipleFeatures': 'Collectively',
+ 'PatternCenterAt': 'CenterOfMass',
+ 'GapSizes': 'No gaps identified.',
+ 'StepOver': 100.0,
+ 'CutPatternAngle': 0.0,
+ 'CutterTilt': 0.0,
+ 'StartIndex': 0.0,
+ 'StopIndex': 360.0,
+ 'SampleInterval': 1.0,
+ 'BoundaryAdjustment': 0.0,
+ 'InternalFeaturesAdjustment': 0.0,
+ 'AvoidLastX_Faces': 0,
+ 'PatternCenterCustom': FreeCAD.Vector(0.0, 0.0, 0.0),
+ 'GapThreshold': 0.005,
+ 'AngularDeflection': 0.25,
+ 'LinearDeflection': 0.0001,
+ # For debugging
+ 'ShowTempObjects': False
+ }
+
+ warn = True
+ if hasattr(job, 'GeometryTolerance'):
+ if job.GeometryTolerance.Value != 0.0:
+ warn = False
+ defaults['LinearDeflection'] = job.GeometryTolerance.Value
+ if warn:
+ msg = translate('PathSurface',
+ 'The GeometryTolerance for this Job is 0.0.')
+ msg += translate('PathSurface',
+ 'Initializing LinearDeflection to 0.0001 mm.')
+ FreeCAD.Console.PrintWarning(msg + '\n')
+
+ return defaults
+
def setEditorProperties(self, obj):
# Used to hide inputs in properties list
@@ -241,23 +295,23 @@ class ObjectSurface(PathOp.ObjectOp):
obj.setEditorMode('PatternCenterCustom', P2)
def onChanged(self, obj, prop):
- if hasattr(self, 'addedAllProperties'):
- if self.addedAllProperties is True:
- if prop == 'ScanType':
- self.setEditorProperties(obj)
- if prop == 'CutPattern':
+ if hasattr(self, 'propertiesReady'):
+ if self.propertiesReady:
+ if prop in ['ScanType', 'CutPattern']:
self.setEditorProperties(obj)
def opOnDocumentRestored(self, obj):
- self.initOpProperties(obj, warn=True)
+ self.propertiesReady = False
+ job = PathUtils.findParentJob(obj)
- if PathLog.getLevel(PathLog.thisModule()) != 4:
- obj.setEditorMode('ShowTempObjects', 2) # hide
- else:
- obj.setEditorMode('ShowTempObjects', 0) # show
+ self.initOpProperties(obj, warn=True)
+ self.opApplyPropertyDefaults(obj, job, self.addNewProps)
+
+ mode = 2 if PathLog.getLevel(PathLog.thisModule()) != 4 else 0
+ obj.setEditorMode('ShowTempObjects', mode)
# Repopulate enumerations in case of changes
- ENUMS = self.propertyEnumerations()
+ ENUMS = self.opPropertyEnumerations()
for n in ENUMS:
restore = False
if hasattr(obj, n):
@@ -269,51 +323,28 @@ class ObjectSurface(PathOp.ObjectOp):
self.setEditorProperties(obj)
+ def opApplyPropertyDefaults(self, obj, job, propList):
+ # Set standard property defaults
+ PROP_DFLTS = self.opPropertyDefaults(obj, job)
+ for n in PROP_DFLTS:
+ if n in propList:
+ prop = getattr(obj, n)
+ val = PROP_DFLTS[n]
+ setVal = False
+ if hasattr(prop, 'Value'):
+ if isinstance(val, int) or isinstance(val, float):
+ setVal = True
+ if setVal:
+ propVal = getattr(prop, 'Value')
+ setattr(prop, 'Value', val)
+ else:
+ setattr(obj, n, val)
+
def opSetDefaultValues(self, obj, job):
'''opSetDefaultValues(obj, job) ... initialize defaults'''
job = PathUtils.findParentJob(obj)
- obj.OptimizeLinearPaths = True
- obj.InternalFeaturesCut = True
- obj.OptimizeStepOverTransitions = False
- obj.CircularUseG2G3 = False
- obj.BoundaryEnforcement = True
- obj.UseStartPoint = False
- obj.AvoidLastX_InternalFeatures = True
- obj.CutPatternReversed = False
- obj.StartPoint.x = 0.0
- obj.StartPoint.y = 0.0
- obj.StartPoint.z = obj.ClearanceHeight.Value
- obj.ProfileEdges = 'None'
- obj.LayerMode = 'Single-pass'
- obj.ScanType = 'Planar'
- obj.RotationAxis = 'X'
- obj.CutMode = 'Conventional'
- obj.CutPattern = 'Line'
- obj.HandleMultipleFeatures = 'Collectively' # 'Individually'
- obj.PatternCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom'
- obj.GapSizes = 'No gaps identified.'
- obj.StepOver = 100
- obj.CutPatternAngle = 0.0
- obj.CutterTilt = 0.0
- obj.StartIndex = 0.0
- obj.StopIndex = 360.0
- obj.SampleInterval.Value = 1.0
- obj.BoundaryAdjustment.Value = 0.0
- obj.InternalFeaturesAdjustment.Value = 0.0
- obj.AvoidLastX_Faces = 0
- obj.PatternCenterCustom.x = 0.0
- obj.PatternCenterCustom.y = 0.0
- obj.PatternCenterCustom.z = 0.0
- obj.GapThreshold.Value = 0.005
- obj.AngularDeflection.Value = 0.25
- obj.LinearDeflection.Value = job.GeometryTolerance.Value
- # For debugging
- obj.ShowTempObjects = False
-
- if job.GeometryTolerance.Value == 0.0:
- PathLog.warning(translate('PathSurface', 'The GeometryTolerance for this Job is 0.0. Initializing LinearDeflection to 0.0001 mm.'))
- obj.LinearDeflection.Value = 0.0001
+ self.opApplyPropertyDefaults(obj, job, self.addNewProps)
# need to overwrite the default depth calculations for facing
d = None
@@ -373,10 +404,10 @@ class ObjectSurface(PathOp.ObjectOp):
PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +- 360 degrees.'))
# Limit StepOver to natural number percentage
- if obj.StepOver > 100:
- obj.StepOver = 100
- if obj.StepOver < 1:
- obj.StepOver = 1
+ if obj.StepOver > 100.0:
+ obj.StepOver = 100.0
+ if obj.StepOver < 1.0:
+ obj.StepOver = 1.0
# Limit AvoidLastX_Faces to zero and positive values
if obj.AvoidLastX_Faces < 0:
@@ -403,6 +434,7 @@ class ObjectSurface(PathOp.ObjectOp):
self.closedGap = False
self.tmpCOM = None
self.gaps = [0.1, 0.2, 0.3]
+ self.cancelOperation = False
CMDS = list()
modelVisibility = list()
FCAD = FreeCAD.ActiveDocument
@@ -423,7 +455,6 @@ class ObjectSurface(PathOp.ObjectOp):
self.showDebugObjects = False
# mark beginning of operation and identify parent Job
- PathLog.info('\nBegin 3D Surface operation...')
startTime = time.time()
# Identify parent Job
@@ -531,18 +562,18 @@ class ObjectSurface(PathOp.ObjectOp):
PSF.radius = self.radius
PSF.depthParams = self.depthParams
pPM = PSF.preProcessModel(self.module)
+
# Process selected faces, if available
- if pPM is False:
- PathLog.error('Unable to pre-process obj.Base.')
- else:
+ if pPM:
+ self.cancelOperation = False
(FACES, VOIDS) = pPM
self.modelSTLs = PSF.modelSTLs
self.profileShapes = PSF.profileShapes
- # Create OCL.stl model objects
- self._prepareModelSTLs(JOB, obj)
-
for m in range(0, len(JOB.Model.Group)):
+ # Create OCL.stl model objects
+ PathSurfaceSupport._prepareModelSTLs(self, JOB, obj, m, ocl)
+
Mdl = JOB.Model.Group[m]
if FACES[m] is False:
PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label))
@@ -553,7 +584,7 @@ class ObjectSurface(PathOp.ObjectOp):
CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label))
# make stock-model-voidShapes STL model for avoidance detection on transitions
- self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m])
+ PathSurfaceSupport._makeSafeSTL(self, JOB, obj, m, FACES[m], VOIDS[m], ocl)
# Process model/faces - OCL objects must be ready
CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m]))
@@ -622,118 +653,22 @@ class ObjectSurface(PathOp.ObjectOp):
del self.midDep
execTime = time.time() - startTime
- PathLog.info('Operation time: {} sec.'.format(execTime))
+ if execTime > 60.0:
+ tMins = math.floor(execTime / 60.0)
+ tSecs = execTime - (tMins * 60.0)
+ exTime = str(tMins) + ' min. ' + str(round(tSecs, 5)) + ' sec.'
+ else:
+ exTime = str(round(execTime, 5)) + ' sec.'
+ FreeCAD.Console.PrintMessage('3D Surface operation time is {}\n'.format(exTime))
+
+ if self.cancelOperation:
+ FreeCAD.ActiveDocument.openTransaction(translate("PathSurface", "Canceled 3D Surface operation."))
+ FreeCAD.ActiveDocument.removeObject(obj.Name)
+ FreeCAD.ActiveDocument.commitTransaction()
return True
- # Methods for constructing the cut area
- def _prepareModelSTLs(self, JOB, obj):
- PathLog.debug('_prepareModelSTLs()')
- for m in range(0, len(JOB.Model.Group)):
- M = JOB.Model.Group[m]
-
- # PathLog.debug(f" -self.modelTypes[{m}] == 'M'")
- if self.modelTypes[m] == 'M':
- # TODO: test if this works
- facets = M.Mesh.Facets.Points
- else:
- facets = Part.getFacets(M.Shape)
-
- if self.modelSTLs[m] is True:
- stl = ocl.STLSurf()
-
- for tri in facets:
- t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
- ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
- ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
- stl.addTriangle(t)
- self.modelSTLs[m] = stl
- return
-
- def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes):
- '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)...
- Creates and OCL.stl object with combined data with waste stock,
- model, and avoided faces. Travel lines can be checked against this
- STL object to determine minimum travel height to clear stock and model.'''
- PathLog.debug('_makeSafeSTL()')
-
- fuseShapes = list()
- Mdl = JOB.Model.Group[mdlIdx]
- mBB = Mdl.Shape.BoundBox
- sBB = JOB.Stock.Shape.BoundBox
-
- # add Model shape to safeSTL shape
- fuseShapes.append(Mdl.Shape)
-
- if obj.BoundBox == 'BaseBoundBox':
- cont = False
- extFwd = (sBB.ZLength)
- zmin = mBB.ZMin
- zmax = mBB.ZMin + extFwd
- stpDwn = (zmax - zmin) / 4.0
- dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin)
-
- try:
- envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape
- cont = True
- except Exception as ee:
- PathLog.error(str(ee))
- shell = Mdl.Shape.Shells[0]
- solid = Part.makeSolid(shell)
- try:
- envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape
- cont = True
- except Exception as eee:
- PathLog.error(str(eee))
-
- if cont:
- stckWst = JOB.Stock.Shape.cut(envBB)
- if obj.BoundaryAdjustment > 0.0:
- cmpndFS = Part.makeCompound(faceShapes)
- baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape
- adjStckWst = stckWst.cut(baBB)
- else:
- adjStckWst = stckWst
- fuseShapes.append(adjStckWst)
- else:
- PathLog.warning('Path transitions might not avoid the model. Verify paths.')
- else:
- # If boundbox is Job.Stock, add hidden pad under stock as base plate
- toolDiam = self.cutter.getDiameter()
- zMin = JOB.Stock.Shape.BoundBox.ZMin
- xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam
- yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam
- bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam)
- bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam)
- bH = 1.0
- crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0)
- B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1))
- fuseShapes.append(B)
-
- if voidShapes is not False:
- voidComp = Part.makeCompound(voidShapes)
- voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape
- fuseShapes.append(voidEnv)
-
- fused = Part.makeCompound(fuseShapes)
-
- if self.showDebugObjects:
- T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape')
- T.Shape = fused
- T.purgeTouched()
- self.tempGroup.addObject(T)
-
- facets = Part.getFacets(fused)
-
- stl = ocl.STLSurf()
- for tri in facets:
- t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
- ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
- ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
- stl.addTriangle(t)
-
- self.safeSTLs[mdlIdx] = stl
-
+ # Methods for constructing the cut area and creating path geometry
def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS):
'''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)...
This method applies any avoided faces or regions to the selected faces.
@@ -784,10 +719,9 @@ class ObjectSurface(PathOp.ObjectOp):
return final
- # Methods for creating path geometry
def _processPlanarOp(self, JOB, obj, mdlIdx, cmpdShp, fsi):
- '''_processPlanarOp(JOB, obj, mdlIdx, cmpdShp)...
- This method compiles the main components for the procedural portion of a planar operation (non-rotational).
+ '''_processPlanarOp(JOB, obj, mdlIdx, cmpdShp)...
+ This method compiles the main components for the procedural portion of a planar operation (non-rotational).
It creates the OCL PathDropCutter objects: model and safeTravel.
It makes the necessary facial geometries for the actual cut area.
It calls the correct Single or Multi-pass method as needed.
diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py
index 7ff1342360..a26b290827 100644
--- a/src/Mod/Path/PathScripts/PathSurfaceGui.py
+++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py
@@ -141,11 +141,17 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
return signals
- def updateVisibility(self):
+ def updateVisibility(self, sentObj=None):
+ '''updateVisibility(sentObj=None)... Updates visibility of Tasks panel objects.'''
if self.form.scanType.currentText() == 'Planar':
self.form.cutPattern.show()
self.form.cutPattern_label.show()
self.form.optimizeStepOverTransitions.show()
+ if hasattr(self.form, 'profileEdges'):
+ self.form.profileEdges.show()
+ self.form.profileEdges_label.show()
+ self.form.avoidLastX_Faces.show()
+ self.form.avoidLastX_Faces_label.show()
self.form.boundBoxExtraOffsetX.hide()
self.form.boundBoxExtraOffsetY.hide()
@@ -156,6 +162,11 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
self.form.cutPattern.hide()
self.form.cutPattern_label.hide()
self.form.optimizeStepOverTransitions.hide()
+ if hasattr(self.form, 'profileEdges'):
+ self.form.profileEdges.hide()
+ self.form.profileEdges_label.hide()
+ self.form.avoidLastX_Faces.hide()
+ self.form.avoidLastX_Faces_label.hide()
self.form.boundBoxExtraOffsetX.show()
self.form.boundBoxExtraOffsetY.show()
diff --git a/src/Mod/Path/PathScripts/PathSurfaceSupport.py b/src/Mod/Path/PathScripts/PathSurfaceSupport.py
index 0e0ca7cdfc..fc66645f1b 100644
--- a/src/Mod/Path/PathScripts/PathSurfaceSupport.py
+++ b/src/Mod/Path/PathScripts/PathSurfaceSupport.py
@@ -28,7 +28,6 @@ __title__ = "Path Surface Support Module"
__author__ = "russ4262 (Russell Johnson)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Support functions and classes for 3D Surface and Waterline operations."
-# __name__ = "PathSurfaceSupport"
__contributors__ = ""
import FreeCAD
@@ -37,7 +36,11 @@ import Path
import PathScripts.PathLog as PathLog
import PathScripts.PathUtils as PathUtils
import math
-import Part
+
+# lazily loaded modules
+from lazy_loader.lazy_loader import LazyLoader
+# MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart')
+Part = LazyLoader('Part', globals(), 'Part')
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
@@ -54,7 +57,7 @@ class PathGeometryGenerator:
PathGeometryGenerator(obj, shape, pattern)
`obj` is the operation object, `shape` is the horizontal planar shape object,
and `pattern` is the name of the geometric pattern to apply.
- First, call the getCenterOfPattern() method for the CenterOfMass for patterns allowing a custom center.
+ Frist, call the getCenterOfPattern() method for the CenterOfMass for patterns allowing a custom center.
Next, call the generatePathGeometry() method to request the path geometry shape.'''
# Register valid patterns here by name
@@ -91,16 +94,16 @@ class PathGeometryGenerator:
if shape.BoundBox.ZLength == 0.0:
self.shape = shape
else:
- PathLog.warning('Shape appears to not be horizontal planar. ZMax is {}.'.format(shape.BoundBox.ZMax))
+ FreeCAD.Console.PrintWarning('Shape appears to not be horizontal planar. ZMax is {}.\n'.format(shape.BoundBox.ZMax))
self._prepareConstants()
def _prepareConstants(self):
# Apply drop cutter extra offset and set the max and min XY area of the operation
- xmin = self.shape.BoundBox.XMin
- xmax = self.shape.BoundBox.XMax
- ymin = self.shape.BoundBox.YMin
- ymax = self.shape.BoundBox.YMax
+ # xmin = self.shape.BoundBox.XMin
+ # xmax = self.shape.BoundBox.XMax
+ # ymin = self.shape.BoundBox.YMin
+ # ymax = self.shape.BoundBox.YMax
# Compute weighted center of mass of all faces combined
if self.pattern in ['Circular', 'CircularZigZag', 'Spiral']:
@@ -115,7 +118,8 @@ class PathGeometryGenerator:
fCnt += 1
zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF))
if fCnt == 0:
- PathLog.error(translate(self.module, 'Cannot calculate the Center Of Mass. Using Center of Boundbox instead.'))
+ msg = translate(self.module, 'Cannot calculate the Center Of Mass. Using Center of Boundbox instead.')
+ FreeCAD.Console.PrintError(msg + '\n')
bbC = self.shape.BoundBox.Center
zeroCOM = FreeCAD.Vector(bbC.x, bbC.y, 0.0)
else:
@@ -152,11 +156,11 @@ class PathGeometryGenerator:
'''generatePathGeometry()...
Call this function to obtain the path geometry shape, generated by this class.'''
if self.pattern == 'None':
- PathLog.warning('PGG: No pattern set.')
+ # FreeCAD.Console.PrintWarning('PGG: No pattern set.\n')
return False
if self.shape is None:
- PathLog.warning('PGG: No shape set.')
+ # FreeCAD.Console.PrintWarning('PGG: No shape set.\n')
return False
cmd = 'self._' + self.pattern + '()'
@@ -229,17 +233,17 @@ class PathGeometryGenerator:
cAng = math.atan(self.deltaX / self.deltaY) # BoundaryBox angle
# Determine end points and create top lines
- x1 = centRot.x - self.halfDiag
- x2 = centRot.x + self.halfDiag
- diag = None
- if self.obj.CutPatternAngle == 0 or self.obj.CutPatternAngle == 180:
- diag = self.deltaY
- elif self.obj.CutPatternAngle == 90 or self.obj.CutPatternAngle == 270:
- diag = self.deltaX
- else:
- perpDist = math.cos(cAng - math.radians(self.obj.CutPatternAngle)) * self.deltaC
- diag = perpDist
- y1 = centRot.y + diag
+ # x1 = centRot.x - self.halfDiag
+ # x2 = centRot.x + self.halfDiag
+ # diag = None
+ # if self.obj.CutPatternAngle == 0 or self.obj.CutPatternAngle == 180:
+ # diag = self.deltaY
+ # elif self.obj.CutPatternAngle == 90 or self.obj.CutPatternAngle == 270:
+ # diag = self.deltaX
+ # else:
+ # perpDist = math.cos(cAng - math.radians(self.obj.CutPatternAngle)) * self.deltaC
+ # diag = perpDist
+ # y1 = centRot.y + diag
# y2 = y1
# Create end points for set of lines to intersect with cross-section face
@@ -404,8 +408,9 @@ class PathGeometryGenerator:
while cont:
ofstArea = self._getFaceOffset(shape, ofst)
if not ofstArea:
- PathLog.warning('PGG: No offset clearing area returned.')
+ # FreeCAD.Console.PrintWarning('PGG: No offset clearing area returned.\n')
cont = False
+ True if cont else False # cont used for LGTM
break
for F in ofstArea.Faces:
faces.append(F)
@@ -470,7 +475,7 @@ class ProcessSelectedFaces:
self.module = None
self.radius = None
self.depthParams = None
- self.msgNoFaces = translate(self.module, 'Face selection is unavailable for Rotational scans. Ignoring selected faces.')
+ self.msgNoFaces = translate(self.module, 'Face selection is unavailable for Rotational scans. Ignoring selected faces.') + '\n'
self.JOB = JOB
self.obj = obj
self.profileEdges = 'None'
@@ -493,7 +498,7 @@ class ProcessSelectedFaces:
self.checkBase = True
if self.obj.ScanType == 'Rotational':
self.checkBase = False
- PathLog.warning(self.msgNoFaces)
+ FreeCAD.Console.PrintWarning(self.msgNoFaces)
def PathWaterline(self):
if self.obj.Base:
@@ -501,7 +506,7 @@ class ProcessSelectedFaces:
self.checkBase = True
if self.obj.Algorithm in ['OCL Dropcutter', 'Experimental']:
self.checkBase = False
- PathLog.warning(self.msgNoFaces)
+ FreeCAD.Console.PrintWarning(self.msgNoFaces)
# public class methods
def setShowDebugObjects(self, grpObj, val):
@@ -520,6 +525,7 @@ class ProcessSelectedFaces:
vShapes = list()
GRP = self.JOB.Model.Group
lenGRP = len(GRP)
+ proceed = False
# Crete place holders for each base model in Job
for m in range(0, lenGRP):
@@ -532,16 +538,20 @@ class ProcessSelectedFaces:
if self.checkBase:
PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.')
- # (FACES, VOIDS) = self._identifyFacesAndVoids(FACES, VOIDS)
- (F, V) = self._identifyFacesAndVoids(FACES, VOIDS)
+ (hasFace, hasVoid) = self._identifyFacesAndVoids(FACES, VOIDS) # modifies FACES and VOIDS
+ hasGeometry = True if hasFace or hasVoid else False
# Cycle through each base model, processing faces for each
for m in range(0, lenGRP):
base = GRP[m]
- (mFS, mVS, mPS) = self._preProcessFacesAndVoids(base, m, FACES, VOIDS)
+ (mFS, mVS, mPS) = self._preProcessFacesAndVoids(base, FACES[m], VOIDS[m])
fShapes[m] = mFS
vShapes[m] = mVS
self.profileShapes[m] = mPS
+ if mFS or mVS:
+ proceed = True
+ if hasGeometry and not proceed:
+ return False
else:
PathLog.debug(' -No obj.Base data.')
for m in range(0, lenGRP):
@@ -559,7 +569,7 @@ class ProcessSelectedFaces:
pPEB = self._preProcessEntireBase(base, m)
if pPEB is False:
- PathLog.error(' -Failed to pre-process base as a whole.')
+ FreeCAD.Console.PrintError(' -Failed to pre-process base as a whole.\n')
else:
(fcShp, prflShp) = pPEB
if fcShp is not False:
@@ -609,6 +619,8 @@ class ProcessSelectedFaces:
TUPS = list()
GRP = self.JOB.Model.Group
lenGRP = len(GRP)
+ hasFace = False
+ hasVoid = False
# Separate selected faces into (base, face) tuples and flag model(s) for STL creation
for (bs, SBS) in self.obj.Base:
@@ -634,19 +646,21 @@ class ProcessSelectedFaces:
if F[m] is False:
F[m] = list()
F[m].append((shape, faceIdx))
+ hasFace = True
else:
if V[m] is False:
V[m] = list()
V[m].append((shape, faceIdx))
- return (F, V)
+ hasVoid = True
+ return (hasFace, hasVoid)
- def _preProcessFacesAndVoids(self, base, m, FACES, VOIDS):
+ def _preProcessFacesAndVoids(self, base, FCS, VDS):
mFS = False
mVS = False
mPS = False
mIFS = list()
- if FACES[m] is not False:
+ if FCS:
isHole = False
if self.obj.HandleMultipleFeatures == 'Collectively':
cont = True
@@ -654,26 +668,18 @@ class ProcessSelectedFaces:
ifL = list() # avoid shape list
outFCS = list()
- # Get collective envelope slice of selected faces
- for (fcshp, fcIdx) in FACES[m]:
- fNum = fcIdx + 1
- fsL.append(fcshp)
- gFW = self._getFaceWires(base, fcshp, fcIdx)
- if gFW is False:
- PathLog.debug('Failed to get wires from Face{}'.format(fNum))
- elif gFW[0] is False:
- PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
- else:
- ((otrFace, raised), intWires) = gFW
- outFCS.append(otrFace)
- if self.obj.InternalFeaturesCut is False:
- if intWires is not False:
- for (iFace, rsd) in intWires:
- ifL.append(iFace)
+ # Use new face-unifying class
+ FUR = FindUnifiedRegions(FCS, self.JOB.GeometryTolerance.Value)
+ if self.showDebugObjects:
+ FUR.setTempGroup(self.tempGroup)
+ outFCS = FUR.getUnifiedRegions()
+ if not self.obj.InternalFeaturesCut:
+ ifL.extend(FUR.getInternalFeatures())
PathLog.debug('Attempting to get cross-section of collective faces.')
if len(outFCS) == 0:
- PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum))
+ msg = translate('PathSurfaceSupport', 'Cannot process selected faces. Check horizontal surface exposure.')
+ FreeCAD.Console.PrintError(msg + '\n')
cont = False
else:
cfsL = Part.makeCompound(outFCS)
@@ -688,7 +694,7 @@ class ProcessSelectedFaces:
mFS = True
cont = False
else:
- PathLog.error(' -Failed to create profile geometry for selected faces.')
+ # FreeCAD.Console.PrintError(' -Failed to create profile geometry for selected faces.\n')
cont = False
if cont:
@@ -701,7 +707,7 @@ class ProcessSelectedFaces:
ofstVal = self._calculateOffsetValue(isHole)
faceOfstShp = extractFaceOffset(cfsL, ofstVal, self.wpc)
if faceOfstShp is False:
- PathLog.error(' -Failed to create offset face.')
+ FreeCAD.Console.PrintError(' -Failed to create offset face.\n')
cont = False
if cont:
@@ -728,27 +734,19 @@ class ProcessSelectedFaces:
# Eif
elif self.obj.HandleMultipleFeatures == 'Individually':
- for (fcshp, fcIdx) in FACES[m]:
+ for (fcshp, fcIdx) in FCS:
cont = True
ifL = list() # avoid shape list
fNum = fcIdx + 1
outerFace = False
- gFW = self._getFaceWires(base, fcshp, fcIdx)
- if gFW is False:
- PathLog.debug('Failed to get wires from Face{}'.format(fNum))
- cont = False
- elif gFW[0] is False:
- PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
- cont = False
- outerFace = False
- else:
- ((otrFace, raised), intWires) = gFW
- outerFace = otrFace
- if self.obj.InternalFeaturesCut is False:
- if intWires is not False:
- for (iFace, rsd) in intWires:
- ifL.append(iFace)
+ # Use new face-unifying class
+ FUR = FindUnifiedRegions([(fcshp, fcIdx)], self.JOB.GeometryTolerance.Value)
+ if self.showDebugObjects:
+ FUR.setTempGroup(self.tempGroup)
+ outerFace = FUR.getUnifiedRegions()[0]
+ if not self.obj.InternalFeaturesCut:
+ ifL = FUR.getInternalFeatures()
if outerFace is not False:
PathLog.debug('Attempting to create offset face of Face{}'.format(fNum))
@@ -766,7 +764,7 @@ class ProcessSelectedFaces:
mFS.append(True)
cont = False
else:
- PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum))
+ # PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum))
cont = False
if cont:
@@ -799,26 +797,23 @@ class ProcessSelectedFaces:
for ifs in mIFS:
mVS.append(ifs)
- if VOIDS[m] is not False:
+ if VDS is not False:
PathLog.debug('Processing avoid faces.')
cont = True
isHole = False
outFCS = list()
intFEAT = list()
- for (fcshp, fcIdx) in VOIDS[m]:
+ for (fcshp, fcIdx) in VDS:
fNum = fcIdx + 1
- gFW = self._getFaceWires(base, fcshp, fcIdx)
- if gFW is False:
- PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum))
- cont = False
- else:
- ((otrFace, raised), intWires) = gFW
- outFCS.append(otrFace)
- if self.obj.AvoidLastX_InternalFeatures is False:
- if intWires is not False:
- for (iFace, rsd) in intWires:
- intFEAT.append(iFace)
+
+ # Use new face-unifying class
+ FUR = FindUnifiedRegions([(fcshp, fcIdx)], self.JOB.GeometryTolerance.Value)
+ if self.showDebugObjects:
+ FUR.setTempGroup(self.tempGroup)
+ outFCS.extend(FUR.getUnifiedRegions())
+ if not self.obj.InternalFeaturesCut:
+ intFEAT.extend(FUR.getInternalFeatures())
lenOtFcs = len(outFCS)
if lenOtFcs == 0:
@@ -846,7 +841,7 @@ class ProcessSelectedFaces:
ofstVal = self._calculateOffsetValue(isHole, isVoid=True)
avdOfstShp = extractFaceOffset(avoid, ofstVal, self.wpc)
if avdOfstShp is False:
- PathLog.error('Failed to create collective offset avoid face.')
+ FreeCAD.Console.PrintError('Failed to create collective offset avoid face.\n')
cont = False
if cont:
@@ -860,7 +855,7 @@ class ProcessSelectedFaces:
ofstVal = self._calculateOffsetValue(isHole=True)
ifOfstShp = extractFaceOffset(ifc, ofstVal, self.wpc)
if ifOfstShp is False:
- PathLog.error('Failed to create collective offset avoid internal features.')
+ FreeCAD.Console.PrintError('Failed to create collective offset avoid internal features.\n')
else:
avdShp = avdOfstShp.cut(ifOfstShp)
@@ -868,44 +863,8 @@ class ProcessSelectedFaces:
mVS = list()
mVS.append(avdShp)
-
return (mFS, mVS, mPS)
- def _getFaceWires(self, base, fcshp, fcIdx):
- outFace = False
- INTFCS = list()
- fNum = fcIdx + 1
- warnFinDep = translate(self.module, 'Final Depth might need to be lower. Internal features detected in Face')
-
- PathLog.debug('_getFaceWires() from Face{}'.format(fNum))
- WIRES = self._extractWiresFromFace(base, fcshp)
- if WIRES is False:
- PathLog.error('Failed to extract wires from Face{}'.format(fNum))
- return False
-
- # Process remaining internal features, adding to FCS list
- lenW = len(WIRES)
- for w in range(0, lenW):
- (wire, rsd) = WIRES[w]
- PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd))
- if wire.isClosed() is False:
- PathLog.debug(' -wire is not closed.')
- else:
- slc = self._flattenWireToFace(wire)
- if slc is False:
- PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum))
- else:
- if w == 0:
- outFace = (slc, rsd)
- else:
- # add to VOIDS so cutter avoids area.
- PathLog.warning(warnFinDep + str(fNum) + '.')
- INTFCS.append((slc, rsd))
- if len(INTFCS) == 0:
- return (outFace, False)
- else:
- return (outFace, INTFCS)
-
def _preProcessEntireBase(self, base, m):
cont = True
isHole = False
@@ -928,10 +887,8 @@ class ProcessSelectedFaces:
if cont:
csFaceShape = getShapeSlice(baseEnv)
if csFaceShape is False:
- PathLog.debug('getShapeSlice(baseEnv) failed')
csFaceShape = getCrossSection(baseEnv)
if csFaceShape is False:
- PathLog.debug('getCrossSection(baseEnv) failed')
csFaceShape = getSliceFromEnvelope(baseEnv)
if csFaceShape is False:
PathLog.error('Failed to slice baseEnv shape.')
@@ -946,103 +903,19 @@ class ProcessSelectedFaces:
return (True, psOfst)
prflShp = psOfst
else:
- PathLog.error(' -Failed to create profile geometry.')
+ # FreeCAD.Console.PrintError(' -Failed to create profile geometry.\n')
cont = False
if cont:
ofstVal = self._calculateOffsetValue(isHole)
faceOffsetShape = extractFaceOffset(csFaceShape, ofstVal, self.wpc)
if faceOffsetShape is False:
- PathLog.error('extractFaceOffset() failed.')
+ PathLog.error('extractFaceOffset() failed for entire base.')
else:
faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin))
return (faceOffsetShape, prflShp)
return False
- def _extractWiresFromFace(self, base, fc):
- '''_extractWiresFromFace(base, fc) ...
- Attempts to return all closed wires within a parent face, including the outer most wire of the parent.
- The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True).
- '''
- PathLog.debug('_extractWiresFromFace()')
-
- WIRES = list()
- lenWrs = len(fc.Wires)
- PathLog.debug(' -Wire count: {}'.format(lenWrs))
-
- def index0(tup):
- return tup[0]
-
- # Cycle through wires in face
- for w in range(0, lenWrs):
- PathLog.debug(' -Analyzing wire_{}'.format(w + 1))
- wire = fc.Wires[w]
- checkEdges = False
- cont = True
-
- # Check for closed edges (circles, ellipses, etc...)
- for E in wire.Edges:
- if E.isClosed() is True:
- checkEdges = True
- break
-
- if checkEdges is True:
- PathLog.debug(' -checkEdges is True')
- for e in range(0, len(wire.Edges)):
- edge = wire.Edges[e]
- if edge.isClosed() is True and edge.Mass > 0.01:
- PathLog.debug(' -Found closed edge')
- raised = False
- ip = self._isPocket(base, fc, edge)
- if ip is False:
- raised = True
- ebb = edge.BoundBox
- eArea = ebb.XLength * ebb.YLength
- F = Part.Face(Part.Wire([edge]))
- WIRES.append((eArea, F.Wires[0], raised))
- cont = False
-
- if cont:
- PathLog.debug(' -cont is True')
- # If only one wire and not checkEdges, return first wire
- if lenWrs == 1:
- return [(wire, False)]
-
- raised = False
- wbb = wire.BoundBox
- wArea = wbb.XLength * wbb.YLength
- if w > 0:
- ip = self._isPocket(base, fc, wire)
- if ip is False:
- raised = True
- WIRES.append((wArea, Part.Wire(wire.Edges), raised))
-
- nf = len(WIRES)
- if nf > 0:
- PathLog.debug(' -number of wires found is {}'.format(nf))
- if nf == 1:
- (area, W, raised) = WIRES[0]
- owLen = fc.OuterWire.Length
- wLen = W.Length
- if abs(owLen - wLen) > 0.0000001:
- OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges))
- return [(OW, False), (W, raised)]
- else:
- return [(W, raised)]
- else:
- sortedWIRES = sorted(WIRES, key=index0, reverse=True)
- WRS = [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size
- # Check if OuterWire is larger than largest in WRS list
- (W, raised) = WRS[0]
- owLen = fc.OuterWire.Length
- wLen = W.Length
- if abs(owLen - wLen) > 0.0000001:
- OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges))
- WRS.insert(0, (OW, False))
- return WRS
-
- return False
-
def _calculateOffsetValue(self, isHole, isVoid=False):
'''_calculateOffsetValue(self.obj, isHole, isVoid) ... internal function.
Calculate the offset for the Path.Area() function.'''
@@ -1066,75 +939,6 @@ class ProcessSelectedFaces:
return offset
- def _isPocket(self, b, f, w):
- '''_isPocket(b, f, w)...
- Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion.
- Returns True if pocket, False if raised protrusion.'''
- e = w.Edges[0]
- for fi in range(0, len(b.Shape.Faces)):
- face = b.Shape.Faces[fi]
- for ei in range(0, len(face.Edges)):
- edge = face.Edges[ei]
- if e.isSame(edge) is True:
- if f is face:
- # Alternative: run loop to see if all edges are same
- pass # same source face, look for another
- else:
- if face.CenterOfMass.z < f.CenterOfMass.z:
- return True
- return False
-
- def _flattenWireToFace(self, wire):
- PathLog.debug('_flattenWireToFace()')
- if wire.isClosed() is False:
- PathLog.debug(' -wire.isClosed() is False')
- return False
-
- # If wire is planar horizontal, convert to a face and return
- if wire.BoundBox.ZLength == 0.0:
- slc = Part.Face(wire)
- return slc
-
- # Attempt to create a new wire for manipulation, if not, use original
- newWire = Part.Wire(wire.Edges)
- if newWire.isClosed() is True:
- nWire = newWire
- else:
- PathLog.debug(' -newWire.isClosed() is False')
- nWire = wire
-
- # Attempt extrusion, and then try a manual slice and then cross-section
- ext = getExtrudedShape(nWire)
- if ext is False:
- PathLog.debug('getExtrudedShape() failed')
- else:
- slc = getShapeSlice(ext)
- if slc is not False:
- return slc
- cs = getCrossSection(ext, True)
- if cs is not False:
- return cs
-
- # Attempt creating an envelope, and then try a manual slice and then cross-section
- env = getShapeEnvelope(nWire)
- if env is False:
- PathLog.debug('getShapeEnvelope() failed')
- else:
- slc = getShapeSlice(env)
- if slc is not False:
- return slc
- cs = getCrossSection(env, True)
- if cs is not False:
- return cs
-
- # Attempt creating a projection
- slc = getProjectedFace(self.tempGroup, nWire)
- if slc is False:
- PathLog.debug('getProjectedFace() failed')
- else:
- return slc
-
- return False
# Eclass
@@ -1153,6 +957,7 @@ def getExtrudedShape(wire):
SHP = Part.makeSolid(shell)
return SHP
+
def getShapeSlice(shape):
PathLog.debug('getShapeSlice()')
@@ -1198,10 +1003,9 @@ def getShapeSlice(shape):
comp = Part.makeCompound(fL)
return comp
- # PathLog.debug(' -slcArea !< midArea')
- # PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges)))
return False
+
def getProjectedFace(tempGroup, wire):
import Draft
PathLog.debug('getProjectedFace()')
@@ -1226,7 +1030,8 @@ def getProjectedFace(tempGroup, wire):
slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
return slc
-def getCrossSection(shape, withExtrude=False):
+
+def getCrossSection(shape):
PathLog.debug('getCrossSection()')
wires = list()
bb = shape.BoundBox
@@ -1242,13 +1047,7 @@ def getCrossSection(shape, withExtrude=False):
if csWire.isClosed() is False:
PathLog.debug(' -comp.Wires[0] is not closed')
return False
- if withExtrude is True:
- ext = getExtrudedShape(csWire)
- CS = getShapeSlice(ext)
- if CS is False:
- return False
- else:
- CS = Part.Face(csWire)
+ CS = Part.Face(csWire)
CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin))
return CS
else:
@@ -1256,6 +1055,7 @@ def getCrossSection(shape, withExtrude=False):
return False
+
def getShapeEnvelope(shape):
PathLog.debug('getShapeEnvelope()')
@@ -1269,11 +1069,12 @@ def getShapeEnvelope(shape):
try:
env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape
except Exception as ee:
- PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee))
+ FreeCAD.Console.PrintError('try: PathUtils.getEnvelope() failed.\n' + str(ee) + '\n')
return False
else:
return env
+
def getSliceFromEnvelope(env):
PathLog.debug('getSliceFromEnvelope()')
eBB = env.BoundBox
@@ -1338,6 +1139,125 @@ def extractFaceOffset(fcShape, offset, wpc, makeComp=True):
return ofstFace # offsetShape
+# Functions for making model STLs
+def _prepareModelSTLs(self, JOB, obj, m, ocl):
+ PathLog.debug('_prepareModelSTLs()')
+ import MeshPart
+
+ if self.modelSTLs[m] is True:
+ M = JOB.Model.Group[m]
+
+ # PathLog.debug(f" -self.modelTypes[{m}] == 'M'")
+ if self.modelTypes[m] == 'M':
+ # TODO: test if this works
+ facets = M.Mesh.Facets.Points
+ else:
+ facets = Part.getFacets(M.Shape)
+ # mesh = MeshPart.meshFromShape(Shape=M.Shape,
+ # LinearDeflection=obj.LinearDeflection.Value,
+ # AngularDeflection=obj.AngularDeflection.Value,
+ # Relative=False)
+
+ stl = ocl.STLSurf()
+ for tri in facets:
+ t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
+ ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
+ ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
+ stl.addTriangle(t)
+ self.modelSTLs[m] = stl
+ return
+
+
+def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes, ocl):
+ '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)...
+ Creates and OCL.stl object with combined data with waste stock,
+ model, and avoided faces. Travel lines can be checked against this
+ STL object to determine minimum travel height to clear stock and model.'''
+ PathLog.debug('_makeSafeSTL()')
+ import MeshPart
+
+ fuseShapes = list()
+ Mdl = JOB.Model.Group[mdlIdx]
+ mBB = Mdl.Shape.BoundBox
+ sBB = JOB.Stock.Shape.BoundBox
+
+ # add Model shape to safeSTL shape
+ fuseShapes.append(Mdl.Shape)
+
+ if obj.BoundBox == 'BaseBoundBox':
+ cont = False
+ extFwd = (sBB.ZLength)
+ zmin = mBB.ZMin
+ zmax = mBB.ZMin + extFwd
+ stpDwn = (zmax - zmin) / 4.0
+ dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin)
+
+ try:
+ envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape
+ cont = True
+ except Exception as ee:
+ PathLog.error(str(ee))
+ shell = Mdl.Shape.Shells[0]
+ solid = Part.makeSolid(shell)
+ try:
+ envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape
+ cont = True
+ except Exception as eee:
+ PathLog.error(str(eee))
+
+ if cont:
+ stckWst = JOB.Stock.Shape.cut(envBB)
+ if obj.BoundaryAdjustment > 0.0:
+ cmpndFS = Part.makeCompound(faceShapes)
+ baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape
+ adjStckWst = stckWst.cut(baBB)
+ else:
+ adjStckWst = stckWst
+ fuseShapes.append(adjStckWst)
+ else:
+ PathLog.warning('Path transitions might not avoid the model. Verify paths.')
+ else:
+ # If boundbox is Job.Stock, add hidden pad under stock as base plate
+ toolDiam = self.cutter.getDiameter()
+ zMin = JOB.Stock.Shape.BoundBox.ZMin
+ xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam
+ yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam
+ bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam)
+ bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam)
+ bH = 1.0
+ crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0)
+ B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1))
+ fuseShapes.append(B)
+
+ if voidShapes is not False:
+ voidComp = Part.makeCompound(voidShapes)
+ voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape
+ fuseShapes.append(voidEnv)
+
+ fused = Part.makeCompound(fuseShapes)
+
+ if self.showDebugObjects:
+ T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape')
+ T.Shape = fused
+ T.purgeTouched()
+ self.tempGroup.addObject(T)
+
+ facets = Part.getFacets(fused)
+ # mesh = MeshPart.meshFromShape(Shape=fused,
+ # LinearDeflection=obj.LinearDeflection.Value,
+ # AngularDeflection=obj.AngularDeflection.Value,
+ # Relative=False)
+
+ stl = ocl.STLSurf()
+ for tri in facets:
+ t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
+ ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
+ ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
+ stl.addTriangle(t)
+
+ self.safeSTLs[mdlIdx] = stl
+
+
# Functions to convert path geometry into line/arc segments for OCL input or directly to g-code
def pathGeomToLinesPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps):
'''pathGeomToLinesPointSet(obj, compGeoShp)...
@@ -1404,6 +1324,7 @@ def pathGeomToLinesPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps
(vA, vB) = inLine.pop() # pop off previous line segment for combining with current
tup = (vA, tup[1])
closedGap = True
+ True if closedGap else False # used closedGap for LGTM
else:
# PathLog.debug('---- Gap: {} mm'.format(gap))
gap = round(gap, 6)
@@ -1411,6 +1332,7 @@ def pathGeomToLinesPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps
gaps.insert(0, gap)
gaps.pop()
inLine.append(tup)
+
# Efor
lnCnt += 1
if cutClimb is True:
@@ -1450,11 +1372,10 @@ def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gap
lnCnt = 0
chkGap = False
ec = len(compGeoShp.Edges)
+ dirFlg = 1
- if cutClimb is True:
+ if cutClimb:
dirFlg = -1
- else:
- dirFlg = 1
edg0 = compGeoShp.Edges[0]
p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
@@ -1476,9 +1397,8 @@ def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gap
cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment)
ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
- # iC = sp.isOnLineSegment(ep, cp)
iC = cp.isOnLineSegment(sp, ep)
- if iC is True:
+ if iC:
inLine.append('BRK')
chkGap = True
gap = abs(toolDiam - lst.sub(cp).Length)
@@ -1486,7 +1406,6 @@ def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gap
chkGap = False
if dirFlg == -1:
inLine.reverse()
- # LINES.append((dirFlg, inLine))
LINES.append(inLine)
lnCnt += 1
dirFlg = -1 * dirFlg # Change zig to zag
@@ -1499,7 +1418,7 @@ def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gap
else:
tup = (v2, v1)
- if chkGap is True:
+ if chkGap:
if gap < obj.GapThreshold.Value:
b = inLine.pop() # pop off 'BRK' marker
(vA, vB) = inLine.pop() # pop off previous line segment for combining with current
@@ -1524,8 +1443,8 @@ def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gap
else:
PathLog.debug('Line count is ODD.')
dirFlg = -1 * dirFlg
- if obj.CutPatternReversed is False:
- if cutClimb is True:
+ if not obj.CutPatternReversed:
+ if cutClimb:
dirFlg = -1 * dirFlg
if obj.CutPatternReversed:
@@ -1553,11 +1472,8 @@ def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gap
rev2.append((p2, p1))
rev2.reverse()
rev = rev2
-
- # LINES.append((dirFlg, rev))
LINES.append(rev)
else:
- # LINES.append((dirFlg, inLine))
LINES.append(inLine)
return LINES
@@ -1783,7 +1699,6 @@ def pathGeomToSpiralPointSet(obj, compGeoShp):
p2 = FreeCAD.Vector(edg1.Vertexes[1].X, edg1.Vertexes[1].Y, 0.0)
tup = ((p1.x, p1.y), (p2.x, p2.y))
inLine.append(tup)
- lst = p2
for ei in range(start, ec): # Skipped first edge, started with second edge above as edg1
edg = compGeoShp.Edges[ei] # Get edge for vertexes
@@ -1798,7 +1713,7 @@ def pathGeomToSpiralPointSet(obj, compGeoShp):
lnCnt += 1
inLine = list() # reset container
inLine.append(tup)
- p1 = sp
+ # p1 = sp
p2 = ep
# Efor
@@ -1852,4 +1767,520 @@ def pathGeomToOffsetPointSet(obj, compGeoShp):
LINES.append(tup)
# Efor
- return [LINES]
\ No newline at end of file
+ return [LINES]
+
+
+class FindUnifiedRegions:
+ '''FindUnifiedRegions() This class requires a list of face shapes.
+ It finds the unified horizontal unified regions, if they exist.'''
+
+ def __init__(self, facesList, geomToler):
+ self.FACES = facesList # format is tuple (faceShape, faceIndex_on_base)
+ self.geomToler = geomToler
+ self.tempGroup = None
+ self.topFaces = list()
+ self.edgeData = list()
+ self.circleData = list()
+ self.noSharedEdges = True
+ self.topWires = list()
+ self.REGIONS = list()
+ self.INTERNALS = False
+ self.idGroups = list()
+ self.sharedEdgeIdxs = list()
+ self.fusedFaces = None
+
+ if self.geomToler == 0.0:
+ self.geomToler = 0.00001
+
+ # Internal processing methods
+ def _showShape(self, shape, name):
+ if self.tempGroup:
+ S = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp' + name)
+ S.Shape = shape
+ S.purgeTouched()
+ self.tempGroup.addObject(S)
+
+ def _extractTopFaces(self):
+ for (F, fcIdx) in self.FACES: # format is tuple (faceShape, faceIndex_on_base)
+ cont = True
+ fNum = fcIdx + 1
+ # Extrude face
+ fBB = F.BoundBox
+ extFwd = math.floor(2.0 * fBB.ZLength) + 10.0
+ ef = F.extrude(FreeCAD.Vector(0.0, 0.0, extFwd))
+ ef = Part.makeSolid(ef)
+
+ # Cut top off of extrusion with Part.box
+ efBB = ef.BoundBox
+ ZLen = efBB.ZLength / 2.0
+ cutBox = Part.makeBox(efBB.XLength + 2.0, efBB.YLength + 2.0, ZLen)
+ zHght = efBB.ZMin + ZLen
+ cutBox.translate(FreeCAD.Vector(efBB.XMin - 1.0, efBB.YMin - 1.0, zHght))
+ base = ef.cut(cutBox)
+
+ # Identify top face of base
+ fIdx = 0
+ zMin = base.Faces[fIdx].BoundBox.ZMin
+ for bfi in range(0, len(base.Faces)):
+ fzmin = base.Faces[bfi].BoundBox.ZMin
+ if fzmin > zMin:
+ fIdx = bfi
+ zMin = fzmin
+
+ # Translate top face to Z=0.0 and save to topFaces list
+ topFace = base.Faces[fIdx]
+ # self._showShape(topFace, 'topFace_{}'.format(fNum))
+ tfBB = topFace.BoundBox
+ tfBB_Area = tfBB.XLength * tfBB.YLength
+ fBB_Area = fBB.XLength * fBB.YLength
+ if tfBB_Area < (fBB_Area * 0.9):
+ # attempt alternate methods
+ topFace = self._getCompleteCrossSection(ef)
+ tfBB = topFace.BoundBox
+ tfBB_Area = tfBB.XLength * tfBB.YLength
+ # self._showShape(topFace, 'topFaceAlt_1_{}'.format(fNum))
+ if tfBB_Area < (fBB_Area * 0.9):
+ topFace = getShapeSlice(ef)
+ tfBB = topFace.BoundBox
+ tfBB_Area = tfBB.XLength * tfBB.YLength
+ # self._showShape(topFace, 'topFaceAlt_2_{}'.format(fNum))
+ if tfBB_Area < (fBB_Area * 0.9):
+ FreeCAD.Console.PrintError('Faild to extract processing region for Face{}.\n'.format(fNum))
+ cont = False
+
+ if cont:
+ topFace.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - zMin))
+ self.topFaces.append((topFace, fcIdx))
+
+ def _fuseTopFaces(self):
+ (one, baseFcIdx) = self.topFaces.pop(0)
+ base = one
+ for (face, fcIdx) in self.topFaces:
+ base = base.fuse(face)
+ self.topFaces.insert(0, (one, baseFcIdx))
+ self.fusedFaces = base
+
+ def _getEdgesData(self):
+ topFaces = self.fusedFaces.Faces
+ tfLen = len(topFaces)
+ count = [0, 0]
+
+ # Get length and center of mass for each edge in all top faces
+ for fi in range(0, tfLen):
+ F = topFaces[fi]
+ edgCnt = len(F.Edges)
+ for ei in range(0, edgCnt):
+ E = F.Edges[ei]
+ tup = (E.Length, E.CenterOfMass, E, fi)
+ if len(E.Vertexes) == 1:
+ self.circleData.append(tup)
+ count[0] += 1
+ else:
+ self.edgeData.append(tup)
+ count[1] += 1
+
+ def _groupEdgesByLength(self):
+ cont = True
+ threshold = self.geomToler
+ grp = list()
+ processLast = False
+
+ def keyFirst(tup):
+ return tup[0]
+
+ # Sort edgeData data and prepare proxy indexes
+ self.edgeData.sort(key=keyFirst)
+ DATA = self.edgeData
+ lenDATA = len(DATA)
+ indexes = [i for i in range(0, lenDATA)]
+ idxCnt = len(indexes)
+
+ while idxCnt > 0:
+ processLast = True
+ # Pop off index for first edge
+ actvIdx = indexes.pop(0)
+ actvItem = DATA[actvIdx][0] # 0 index is length
+ grp.append(actvIdx)
+ idxCnt -= 1
+ noMatch = True
+
+ while idxCnt > 0:
+ tstIdx = indexes[0]
+ tstItem = DATA[tstIdx][0]
+
+ # test case(s) goes here
+ absLenDiff = abs(tstItem - actvItem)
+ if absLenDiff < threshold:
+ # Remove test index from indexes
+ indexes.pop(0)
+ idxCnt -= 1
+ grp.append(tstIdx)
+ noMatch = False
+ else:
+ if len(grp) > 1:
+ # grp.sort()
+ self.idGroups.append(grp)
+ grp = list()
+ break
+ # Ewhile
+ # Ewhile
+ if processLast:
+ if len(grp) > 1:
+ # grp.sort()
+ self.idGroups.append(grp)
+
+ def _identifySharedEdgesByLength(self, grp):
+ holds = list()
+ cont = True
+ specialIndexes = []
+ threshold = self.geomToler
+
+ def keyFirst(tup):
+ return tup[0]
+
+ # Sort edgeData data
+ self.edgeData.sort(key=keyFirst)
+ DATA = self.edgeData
+ lenDATA = len(DATA)
+ lenGrp = len(grp)
+
+ while lenGrp > 0:
+ # Pop off index for first edge
+ actvIdx = grp.pop(0)
+ actvItem = DATA[actvIdx][0] # 0 index is length
+ lenGrp -= 1
+ while lenGrp > 0:
+ isTrue = False
+ # Pop off index for test edge
+ tstIdx = grp.pop(0)
+ tstItem = DATA[tstIdx][0]
+ lenGrp -= 1
+
+ # test case(s) goes here
+ lenDiff = tstItem - actvItem
+ absLenDiff = abs(lenDiff)
+ if lenDiff > threshold:
+ break
+ if absLenDiff < threshold:
+ com1 = DATA[actvIdx][1]
+ com2 = DATA[tstIdx][1]
+ comDiff = com2.sub(com1).Length
+ if comDiff < threshold:
+ isTrue = True
+
+ # Action if test is true (finds special case)
+ if isTrue:
+ specialIndexes.append(actvIdx)
+ specialIndexes.append(tstIdx)
+ break
+ else:
+ holds.append(tstIdx)
+
+ # Put hold indexes back in search group
+ holds.extend(grp)
+ grp = holds
+ lenGrp = len(grp)
+ holds = list()
+
+ if len(specialIndexes) > 0:
+ # Remove shared edges from EDGES data
+ uniqueShared = list(set(specialIndexes))
+ self.sharedEdgeIdxs.extend(uniqueShared)
+ self.noSharedEdges = False
+
+ def _extractWiresFromEdges(self):
+ DATA = self.edgeData
+ holds = list()
+ lastEdge = None
+ lastIdx = None
+ firstEdge = None
+ isWire = False
+ cont = True
+ connectedEdges = []
+ connectedIndexes = []
+ connectedCnt = 0
+ LOOPS = list()
+
+ def faceIndex(tup):
+ return tup[3]
+
+ def faceArea(face):
+ return face.Area
+
+ # Sort by face index on original model base
+ DATA.sort(key=faceIndex)
+ lenDATA = len(DATA)
+ indexes = [i for i in range(0, lenDATA)]
+ idxCnt = len(indexes)
+
+ # Add circle edges into REGIONS list
+ if len(self.circleData) > 0:
+ for C in self.circleData:
+ face = Part.Face(Part.Wire(C[2]))
+ self.REGIONS.append(face)
+
+ actvIdx = indexes.pop(0)
+ actvEdge = DATA[actvIdx][2]
+ firstEdge = actvEdge # DATA[connectedIndexes[0]][2]
+ idxCnt -= 1
+ connectedIndexes.append(actvIdx)
+ connectedEdges.append(actvEdge)
+ connectedCnt = 1
+
+ safety = 750
+ while cont: # safety > 0
+ safety -= 1
+ notConnected = True
+ while idxCnt > 0:
+ isTrue = False
+ # Pop off index for test edge
+ tstIdx = indexes.pop(0)
+ tstEdge = DATA[tstIdx][2]
+ idxCnt -= 1
+ if self._edgesAreConnected(actvEdge, tstEdge):
+ isTrue = True
+
+ if isTrue:
+ notConnected = False
+ connectedIndexes.append(tstIdx)
+ connectedEdges.append(tstEdge)
+ connectedCnt += 1
+ actvIdx = tstIdx
+ actvEdge = tstEdge
+ break
+ else:
+ holds.append(tstIdx)
+ # Ewhile
+
+ if connectedCnt > 2:
+ if self._edgesAreConnected(actvEdge, firstEdge):
+ notConnected = False
+ # Save loop components
+ LOOPS.append(connectedEdges)
+ # reset connected variables and re-assess
+ connectedEdges = []
+ connectedIndexes = []
+ connectedCnt = 0
+ indexes.sort()
+ idxCnt = len(indexes)
+ if idxCnt > 0:
+ # Pop off index for first edge
+ actvIdx = indexes.pop(0)
+ actvEdge = DATA[actvIdx][2]
+ idxCnt -= 1
+ firstEdge = actvEdge
+ connectedIndexes.append(actvIdx)
+ connectedEdges.append(actvEdge)
+ connectedCnt = 1
+ # Eif
+
+ # Put holds indexes back in search stack
+ if notConnected:
+ holds.append(actvIdx)
+ if idxCnt == 0:
+ lastLoop = True
+ holds.extend(indexes)
+ indexes = holds
+ idxCnt = len(indexes)
+ holds = list()
+ if idxCnt == 0:
+ cont = False
+ # Ewhile
+
+ if len(LOOPS) > 0:
+ FACES = list()
+ for Edges in LOOPS:
+ wire = Part.Wire(Part.__sortEdges__(Edges))
+ if wire.isClosed():
+ face = Part.Face(wire)
+ self.REGIONS.append(face)
+ self.REGIONS.sort(key=faceArea, reverse=True)
+
+ def _identifyInternalFeatures(self):
+ remList = list()
+
+ for (top, fcIdx) in self.topFaces:
+ big = Part.Face(top.OuterWire)
+ for s in range(0, len(self.REGIONS)):
+ if s not in remList:
+ small = self.REGIONS[s]
+ if self._isInBoundBox(big, small):
+ cmn = big.common(small)
+ if cmn.Area > 0.0:
+ self.INTERNALS.append(small)
+ remList.append(s)
+ break
+ else:
+ FreeCAD.Console.PrintWarning(' - No common area.\n')
+
+ remList.sort(reverse=True)
+ for ri in remList:
+ self.REGIONS.pop(ri)
+
+ def _processNestedRegions(self):
+ cont = True
+ hold = list()
+ Ids = list()
+ remList = list()
+ for i in range(0, len(self.REGIONS)):
+ Ids.append(i)
+ idsCnt = len(Ids)
+
+ while cont:
+ while idsCnt > 0:
+ hi = Ids.pop(0)
+ high = self.REGIONS[hi]
+ idsCnt -= 1
+ while idsCnt > 0:
+ isTrue = False
+ li = Ids.pop(0)
+ idsCnt -= 1
+ low = self.REGIONS[li]
+ # Test case here
+ if self._isInBoundBox(high, low):
+ cmn = high.common(low)
+ if cmn.Area > 0.0:
+ isTrue = True
+ # if True action here
+ if isTrue:
+ self.REGIONS[hi] = high.cut(low)
+ # self.INTERNALS.append(low)
+ remList.append(li)
+ else:
+ hold.append(hi)
+ # Ewhile
+ hold.extend(Ids)
+ Ids = hold
+ hold = list()
+ if len(Ids) == 0:
+ cont = False
+ # Ewhile
+ # Ewhile
+ remList.sort(reverse=True)
+ for ri in remList:
+ self.REGIONS.pop(ri)
+
+ # Accessory methods
+ def _getCompleteCrossSection(self, shape):
+ PathLog.debug('_getCompleteCrossSection()')
+ wires = list()
+ bb = shape.BoundBox
+ mid = (bb.ZMin + bb.ZMax) / 2.0
+
+ for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid):
+ wires.append(i)
+
+ if len(wires) > 0:
+ comp = Part.Compound(wires) # produces correct cross-section wire !
+ CS = Part.Face(comp.Wires[0])
+ CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin))
+ return CS
+
+ PathLog.debug(' -No wires from .slice() method')
+ return False
+
+ def _edgesAreConnected(self, e1, e2):
+ # Assumes edges are flat and are at Z=0.0
+
+ def isSameVertex(v1, v2):
+ # Assumes vertexes at Z=0.0
+ if abs(v1.X - v2.X) < 0.000001:
+ if abs(v1.Y - v2.Y) < 0.000001:
+ return True
+ return False
+
+ if isSameVertex(e1.Vertexes[0], e2.Vertexes[0]):
+ return True
+ if isSameVertex(e1.Vertexes[0], e2.Vertexes[1]):
+ return True
+ if isSameVertex(e1.Vertexes[1], e2.Vertexes[0]):
+ return True
+ if isSameVertex(e1.Vertexes[1], e2.Vertexes[1]):
+ return True
+
+ return False
+
+ def _isInBoundBox(self, outShp, inShp):
+ obb = outShp.BoundBox
+ ibb = inShp.BoundBox
+
+ if obb.XMin < ibb.XMin:
+ if obb.XMax > ibb.XMax:
+ if obb.YMin < ibb.YMin:
+ if obb.YMax > ibb.YMax:
+ return True
+ return False
+
+ # Public methods
+ def setTempGroup(self, grpObj):
+ '''setTempGroup(grpObj)... For debugging, pass temporary object group.'''
+ self.tempGroup = grpObj
+
+ def getUnifiedRegions(self):
+ '''getUnifiedRegions()... Returns a list of unified regions from list
+ of tuples (faceShape, faceIndex) received at instantiation of the class object.'''
+ self.INTERNALS = list()
+ if len(self.FACES) == 0:
+ FreeCAD.Console.PrintError('No (faceShp, faceIdx) tuples received at instantiation of class.')
+ return []
+
+ self._extractTopFaces()
+ lenFaces = len(self.topFaces)
+ if lenFaces == 0:
+ return []
+
+ # if single topFace, return it
+ if lenFaces == 1:
+ topFace = self.topFaces[0][0]
+ # self._showShape(topFace, 'TopFace')
+ # prepare inner wires as faces for internal features
+ lenWrs = len(topFace.Wires)
+ if lenWrs > 1:
+ for w in range(1, lenWrs):
+ self.INTERNALS.append(Part.Face(topFace.Wires[w]))
+ # prepare outer wire as face for return value in list
+ if hasattr(topFace, 'OuterWire'):
+ ow = topFace.OuterWire
+ else:
+ ow = topFace.Wires[0]
+ face = Part.Face(ow)
+ return [face]
+
+ # process multiple top faces, unifying if possible
+ self._fuseTopFaces()
+ # for F in self.fusedFaces.Faces:
+ # self._showShape(F, 'TopFaceFused')
+
+ self._getEdgesData()
+ self._groupEdgesByLength()
+ for grp in self.idGroups:
+ self._identifySharedEdgesByLength(grp)
+
+ if self.noSharedEdges:
+ PathLog.debug('No shared edges by length detected.\n')
+ return [topFace for (topFace, fcIdx) in self.topFaces]
+ else:
+ # Delete shared edges from edgeData list
+ # FreeCAD.Console.PrintWarning('self.sharedEdgeIdxs: {}\n'.format(self.sharedEdgeIdxs))
+ self.sharedEdgeIdxs.sort(reverse=True)
+ for se in self.sharedEdgeIdxs:
+ # seShp = self.edgeData[se][2]
+ # self._showShape(seShp, 'SharedEdge')
+ self.edgeData.pop(se)
+
+ self._extractWiresFromEdges()
+ self._identifyInternalFeatures()
+ self._processNestedRegions()
+ for ri in range(0, len(self.REGIONS)):
+ self._showShape(self.REGIONS[ri], 'UnifiedRegion_{}'.format(ri))
+
+ return self.REGIONS
+
+ def getInternalFeatures(self):
+ '''getInternalFeatures()... Returns internal features identified
+ after calling getUnifiedRegions().'''
+ if self.INTERNALS:
+ return self.INTERNALS
+ FreeCAD.Console.PrintError('getUnifiedRegions() must be called before getInternalFeatures().\n')
+ return False
+# Eclass
\ No newline at end of file
diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py
index 1bf1c250df..e23b0202fc 100644
--- a/src/Mod/Path/PathScripts/PathWaterline.py
+++ b/src/Mod/Path/PathScripts/PathWaterline.py
@@ -54,7 +54,6 @@ import math
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
-MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart')
Part = LazyLoader('Part', globals(), 'Part')
if FreeCAD.GuiUp:
@@ -72,19 +71,18 @@ def translate(context, text, disambig=None):
class ObjectWaterline(PathOp.ObjectOp):
'''Proxy object for Surfacing operation.'''
- def baseObject(self):
- '''baseObject() ... returns super of receiver
- Used to call base implementation in overwritten functions.'''
- return super(self.__class__, self)
-
def opFeatures(self, obj):
- '''opFeatures(obj) ... return all standard features and edges based geomtries'''
- return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces
+ '''opFeatures(obj) ... return all standard features'''
+ return PathOp.FeatureTool | PathOp.FeatureDepths \
+ | PathOp.FeatureHeights | PathOp.FeatureStepDown \
+ | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces
def initOperation(self, obj):
- '''initPocketOp(obj) ...
- Initialize the operation - property creation and property editor status.'''
- self.initOpProperties(obj)
+ '''initOperation(obj) ... Initialize the operation by
+ managing property creation and property editor status.'''
+ self.propertiesReady = False
+
+ self.initOpProperties(obj) # Initialize operation-specific properties
# For debugging
if PathLog.getLevel(PathLog.thisModule()) != 4:
@@ -95,28 +93,30 @@ class ObjectWaterline(PathOp.ObjectOp):
def initOpProperties(self, obj, warn=False):
'''initOpProperties(obj) ... create operation specific properties'''
- missing = list()
+ self.addNewProps = list()
- for (prtyp, nm, grp, tt) in self.opProperties():
+ for (prtyp, nm, grp, tt) in self.opPropertyDefinitions():
if not hasattr(obj, nm):
obj.addProperty(prtyp, nm, grp, tt)
- missing.append(nm)
- if warn:
- newPropMsg = translate('PathWaterline', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. '
- newPropMsg += translate('PathWaterline', 'Check its default value.')
- PathLog.warning(newPropMsg)
+ self.addNewProps.append(nm)
# Set enumeration lists for enumeration properties
- if len(missing) > 0:
- ENUMS = self.propertyEnumerations()
+ if len(self.addNewProps) > 0:
+ ENUMS = self.opPropertyEnumerations()
for n in ENUMS:
- if n in missing:
+ if n in self.addNewProps:
setattr(obj, n, ENUMS[n])
- self.addedAllProperties = True
+ if warn:
+ newPropMsg = translate('PathWaterline', 'New property added to')
+ newPropMsg += ' "{}": {}'.format(obj.Label, self.addNewProps) + '. '
+ newPropMsg += translate('PathWaterline', 'Check default value(s).')
+ FreeCAD.Console.PrintWarning(newPropMsg + '\n')
- def opProperties(self):
- '''opProperties() ... return list of tuples containing operation specific properties'''
+ self.propertiesReady = True
+
+ def opPropertyDefinitions(self):
+ '''opPropertyDefinitions() ... return list of tuples containing operation specific properties'''
return [
("App::PropertyBool", "ShowTempObjects", "Debug",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")),
@@ -167,7 +167,7 @@ class ObjectWaterline(PathOp.ObjectOp):
QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the cut pattern.")),
("App::PropertyDistance", "SampleInterval", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")),
- ("App::PropertyPercent", "StepOver", "Clearing Options",
+ ("App::PropertyFloat", "StepOver", "Clearing Options",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")),
("App::PropertyBool", "OptimizeLinearPaths", "Optimization",
@@ -185,7 +185,7 @@ class ObjectWaterline(PathOp.ObjectOp):
QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"))
]
- def propertyEnumerations(self):
+ def opPropertyEnumerations(self):
# Enumeration lists for App::PropertyEnumeration properties
return {
'Algorithm': ['OCL Dropcutter', 'Experimental'],
@@ -198,6 +198,56 @@ class ObjectWaterline(PathOp.ObjectOp):
'LayerMode': ['Single-pass', 'Multi-pass'],
}
+ def opPropertyDefaults(self, obj, job):
+ '''opPropertyDefaults(obj, job) ... returns a dictionary
+ of default values for the operation's properties.'''
+ defaults = {
+ 'OptimizeLinearPaths': True,
+ 'InternalFeaturesCut': True,
+ 'OptimizeStepOverTransitions': False,
+ 'BoundaryEnforcement': True,
+ 'UseStartPoint': False,
+ 'AvoidLastX_InternalFeatures': True,
+ 'CutPatternReversed': False,
+ 'IgnoreOuterAbove': obj.StartDepth.Value + 0.00001,
+ 'StartPoint': FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value),
+ 'Algorithm': 'OCL Dropcutter',
+ 'LayerMode': 'Single-pass',
+ 'CutMode': 'Conventional',
+ 'CutPattern': 'None',
+ 'HandleMultipleFeatures': 'Collectively',
+ 'PatternCenterAt': 'CenterOfMass',
+ 'GapSizes': 'No gaps identified.',
+ 'ClearLastLayer': 'Off',
+ 'StepOver': 100.0,
+ 'CutPatternAngle': 0.0,
+ 'DepthOffset': 0.0,
+ 'SampleInterval': 1.0,
+ 'BoundaryAdjustment': 0.0,
+ 'InternalFeaturesAdjustment': 0.0,
+ 'AvoidLastX_Faces': 0,
+ 'PatternCenterCustom': FreeCAD.Vector(0.0, 0.0, 0.0),
+ 'GapThreshold': 0.005,
+ 'AngularDeflection': 0.25,
+ 'LinearDeflection': 0.0001,
+ # For debugging
+ 'ShowTempObjects': False
+ }
+
+ warn = True
+ if hasattr(job, 'GeometryTolerance'):
+ if job.GeometryTolerance.Value != 0.0:
+ warn = False
+ defaults['LinearDeflection'] = job.GeometryTolerance.Value
+ if warn:
+ msg = translate('PathWaterline',
+ 'The GeometryTolerance for this Job is 0.0.')
+ msg += translate('PathWaterline',
+ 'Initializing LinearDeflection to 0.0001 mm.')
+ FreeCAD.Console.PrintWarning(msg + '\n')
+
+ return defaults
+
def setEditorProperties(self, obj):
# Used to hide inputs in properties list
expMode = G = 0
@@ -251,21 +301,23 @@ class ObjectWaterline(PathOp.ObjectOp):
obj.setEditorMode('AngularDeflection', expMode)
def onChanged(self, obj, prop):
- if hasattr(self, 'addedAllProperties'):
- if self.addedAllProperties is True:
+ if hasattr(self, 'propertiesReady'):
+ if self.propertiesReady:
if prop in ['Algorithm', 'CutPattern']:
self.setEditorProperties(obj)
def opOnDocumentRestored(self, obj):
- self.initOpProperties(obj, warn=True)
+ self.propertiesReady = False
+ job = PathUtils.findParentJob(obj)
- if PathLog.getLevel(PathLog.thisModule()) != 4:
- obj.setEditorMode('ShowTempObjects', 2) # hide
- else:
- obj.setEditorMode('ShowTempObjects', 0) # show
+ self.initOpProperties(obj, warn=True)
+ self.opApplyPropertyDefaults(obj, job, self.addNewProps)
+
+ mode = 2 if PathLog.getLevel(PathLog.thisModule()) != 4 else 0
+ obj.setEditorMode('ShowTempObjects', mode)
# Repopulate enumerations in case of changes
- ENUMS = self.propertyEnumerations()
+ ENUMS = self.opPropertyEnumerations()
for n in ENUMS:
restore = False
if hasattr(obj, n):
@@ -277,40 +329,28 @@ class ObjectWaterline(PathOp.ObjectOp):
self.setEditorProperties(obj)
+ def opApplyPropertyDefaults(self, obj, job, propList):
+ # Set standard property defaults
+ PROP_DFLTS = self.opPropertyDefaults(obj, job)
+ for n in PROP_DFLTS:
+ if n in propList:
+ prop = getattr(obj, n)
+ val = PROP_DFLTS[n]
+ setVal = False
+ if hasattr(prop, 'Value'):
+ if isinstance(val, int) or isinstance(val, float):
+ setVal = True
+ if setVal:
+ propVal = getattr(prop, 'Value')
+ setattr(prop, 'Value', val)
+ else:
+ setattr(obj, n, val)
+
def opSetDefaultValues(self, obj, job):
'''opSetDefaultValues(obj, job) ... initialize defaults'''
job = PathUtils.findParentJob(obj)
- obj.OptimizeLinearPaths = True
- obj.InternalFeaturesCut = True
- obj.OptimizeStepOverTransitions = False
- obj.BoundaryEnforcement = True
- obj.UseStartPoint = False
- obj.AvoidLastX_InternalFeatures = True
- obj.CutPatternReversed = False
- obj.IgnoreOuterAbove = obj.StartDepth.Value + 0.00001
- obj.StartPoint = FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value)
- obj.Algorithm = 'OCL Dropcutter'
- obj.LayerMode = 'Single-pass'
- obj.CutMode = 'Conventional'
- obj.CutPattern = 'None'
- obj.HandleMultipleFeatures = 'Collectively' # 'Individually'
- obj.PatternCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom'
- obj.GapSizes = 'No gaps identified.'
- obj.ClearLastLayer = 'Off'
- obj.StepOver = 100
- obj.CutPatternAngle = 0.0
- obj.DepthOffset.Value = 0.0
- obj.SampleInterval.Value = 1.0
- obj.BoundaryAdjustment.Value = 0.0
- obj.InternalFeaturesAdjustment.Value = 0.0
- obj.AvoidLastX_Faces = 0
- obj.PatternCenterCustom = FreeCAD.Vector(0.0, 0.0, 0.0)
- obj.GapThreshold.Value = 0.005
- obj.LinearDeflection.Value = 0.0001
- obj.AngularDeflection.Value = 0.25
- # For debugging
- obj.ShowTempObjects = False
+ self.opApplyPropertyDefaults(obj, job, self.addNewProps)
# need to overwrite the default depth calculations for facing
d = None
@@ -353,10 +393,10 @@ class ObjectWaterline(PathOp.ObjectOp):
PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +- 360 degrees.'))
# Limit StepOver to natural number percentage
- if obj.StepOver > 100:
- obj.StepOver = 100
- if obj.StepOver < 1:
- obj.StepOver = 1
+ if obj.StepOver > 100.0:
+ obj.StepOver = 100.0
+ if obj.StepOver < 1.0:
+ obj.StepOver = 1.0
# Limit AvoidLastX_Faces to zero and positive values
if obj.AvoidLastX_Faces < 0:
@@ -536,13 +576,12 @@ class ObjectWaterline(PathOp.ObjectOp):
self.modelSTLs = PSF.modelSTLs
self.profileShapes = PSF.profileShapes
- # Create OCL.stl model objects
- if obj.Algorithm == 'OCL Dropcutter':
- self._prepareModelSTLs(JOB, obj)
- PathLog.debug('obj.LinearDeflection.Value: {}'.format(obj.LinearDeflection.Value))
- PathLog.debug('obj.AngularDeflection.Value: {}'.format(obj.AngularDeflection.Value))
for m in range(0, len(JOB.Model.Group)):
+ # Create OCL.stl model objects
+ if obj.Algorithm == 'OCL Dropcutter':
+ PathSurfaceSupport._prepareModelSTLs(self, JOB, obj, m, ocl)
+
Mdl = JOB.Model.Group[m]
if FACES[m] is False:
PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label))
@@ -554,7 +593,7 @@ class ObjectWaterline(PathOp.ObjectOp):
PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label))
# make stock-model-voidShapes STL model for avoidance detection on transitions
if obj.Algorithm == 'OCL Dropcutter':
- self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m])
+ PathSurfaceSupport._makeSafeSTL(self, JOB, obj, m, FACES[m], VOIDS[m], ocl)
# Process model/faces - OCL objects must be ready
CMDS.extend(self._processWaterlineAreas(JOB, obj, m, FACES[m], VOIDS[m]))
@@ -627,114 +666,7 @@ class ObjectWaterline(PathOp.ObjectOp):
return True
- # Methods for constructing the cut area
- def _prepareModelSTLs(self, JOB, obj):
- PathLog.debug('_prepareModelSTLs()')
- for m in range(0, len(JOB.Model.Group)):
- M = JOB.Model.Group[m]
-
- # PathLog.debug(f" -self.modelTypes[{m}] == 'M'")
- if self.modelTypes[m] == 'M':
- # TODO: test if this works
- facets = M.Mesh.Facets.Points
- else:
- facets = Part.getFacets(M.Shape)
-
- if self.modelSTLs[m] is True:
- stl = ocl.STLSurf()
-
- for tri in facets:
- t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
- ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
- ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
- stl.addTriangle(t)
- self.modelSTLs[m] = stl
- return
-
- def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes):
- '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)...
- Creates and OCL.stl object with combined data with waste stock,
- model, and avoided faces. Travel lines can be checked against this
- STL object to determine minimum travel height to clear stock and model.'''
- PathLog.debug('_makeSafeSTL()')
-
- fuseShapes = list()
- Mdl = JOB.Model.Group[mdlIdx]
- mBB = Mdl.Shape.BoundBox
- sBB = JOB.Stock.Shape.BoundBox
-
- # add Model shape to safeSTL shape
- fuseShapes.append(Mdl.Shape)
-
- if obj.BoundBox == 'BaseBoundBox':
- cont = False
- extFwd = (sBB.ZLength)
- zmin = mBB.ZMin
- zmax = mBB.ZMin + extFwd
- stpDwn = (zmax - zmin) / 4.0
- dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin)
-
- try:
- envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape
- cont = True
- except Exception as ee:
- PathLog.error(str(ee))
- shell = Mdl.Shape.Shells[0]
- solid = Part.makeSolid(shell)
- try:
- envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape
- cont = True
- except Exception as eee:
- PathLog.error(str(eee))
-
- if cont:
- stckWst = JOB.Stock.Shape.cut(envBB)
- if obj.BoundaryAdjustment > 0.0:
- cmpndFS = Part.makeCompound(faceShapes)
- baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape
- adjStckWst = stckWst.cut(baBB)
- else:
- adjStckWst = stckWst
- fuseShapes.append(adjStckWst)
- else:
- PathLog.warning('Path transitions might not avoid the model. Verify paths.')
- else:
- # If boundbox is Job.Stock, add hidden pad under stock as base plate
- toolDiam = self.cutter.getDiameter()
- zMin = JOB.Stock.Shape.BoundBox.ZMin
- xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam
- yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam
- bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam)
- bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam)
- bH = 1.0
- crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0)
- B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1))
- fuseShapes.append(B)
-
- if voidShapes is not False:
- voidComp = Part.makeCompound(voidShapes)
- voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape
- fuseShapes.append(voidEnv)
-
- fused = Part.makeCompound(fuseShapes)
-
- if self.showDebugObjects is True:
- T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape')
- T.Shape = fused
- T.purgeTouched()
- self.tempGroup.addObject(T)
-
- facets = Part.getFacets(fused)
-
- stl = ocl.STLSurf()
- for tri in facets:
- t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
- ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
- ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
- stl.addTriangle(t)
-
- self.safeSTLs[mdlIdx] = stl
-
+ # Methods for constructing the cut area and creating path geometry
def _processWaterlineAreas(self, JOB, obj, mdlIdx, FCS, VDS):
'''_processWaterlineAreas(JOB, obj, mdlIdx, FCS, VDS)...
This method applies any avoided faces or regions to the selected faces.
@@ -787,7 +719,6 @@ class ObjectWaterline(PathOp.ObjectOp):
return final
- # Methods for creating path geometry
def _getExperimentalWaterlinePaths(self, PNTSET, csHght, cutPattern):
'''_getExperimentalWaterlinePaths(PNTSET, csHght, cutPattern)...
Switching function for calling the appropriate path-geometry to OCL points conversion function
@@ -864,7 +795,6 @@ class ObjectWaterline(PathOp.ObjectOp):
height = first.z
elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z):
height = False # allow end of Zig to cut to beginning of Zag
-
# Create raise, shift, and optional lower commands
if height is not False:
@@ -979,7 +909,9 @@ class ObjectWaterline(PathOp.ObjectOp):
scanLines[L].append(oclScan[pi])
lenSL = len(scanLines)
pntsPerLine = len(scanLines[0])
- PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line")
+ msg = "--OCL scan: " + str(lenSL * pntsPerLine) + " points, with "
+ msg += str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line"
+ PathLog.debug(msg)
# Extract Wl layers per depthparams
lyr = 0
@@ -1694,7 +1626,7 @@ class ObjectWaterline(PathOp.ObjectOp):
return False
def _getModelCrossSection(self, shape, csHght):
- PathLog.debug('getCrossSection()')
+ PathLog.debug('_getModelCrossSection()')
wires = list()
def byArea(fc):
diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py
index 0616bbe6d2..ad4e06ba93 100644
--- a/src/Mod/Path/PathScripts/PathWaterlineGui.py
+++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py
@@ -107,8 +107,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
return signals
- def updateVisibility(self):
- '''updateVisibility()... Updates visibility of Tasks panel objects.'''
+ def updateVisibility(self, sentObj=None):
+ '''updateVisibility(sentObj=None)... Updates visibility of Tasks panel objects.'''
Algorithm = self.form.algorithmSelect.currentText()
self.form.optimizeEnabled.hide() # Has no independent QLabel object