Path: Area based unified projection implementation

Generalize the `extractFaceOffset` method to `getOffsetArea`, which can
handle both face offsetting and projection. Another difference is that
the new method exposes Area's ability to preserve internal holes,
defaulting to preserving. The method is moved to the PathUtils module,
reflecting its generality and fairly wide used across Path.

This method is then used to provide a drop-in alternative to
`FindUnifiedRegions` via a small wrapper in PathSurfaceSupport. The Area
implementation is generally quick, but can fail (throw) in some cases,
so the wrapper is trying the Area method as an optimization first, and
falls back to the full `FindUnifiedRegions` logic if that fails.
This commit is contained in:
Gabriel Wicke
2020-06-06 16:29:36 -07:00
parent c61a1d2dbf
commit 670f3b6098
5 changed files with 177 additions and 188 deletions

View File

@@ -189,7 +189,9 @@ class ObjectFace(PathPocketBase.ObjectPocket):
elif obj.BoundaryShape == 'Perimeter':
if obj.ClearEdges:
psZMin = planeshape.BoundBox.ZMin
ofstShape = PathSurfaceSupport.extractFaceOffset(planeshape, self.radius * 1.25, planeshape)
ofstShape = PathUtils.getOffsetArea(planeshape,
self.radius * 1.25,
plane=planeshape)
ofstShape.translate(FreeCAD.Vector(0.0, 0.0, psZMin - ofstShape.BoundBox.ZMin))
env = PathUtils.getEnvelope(partshape=ofstShape, depthparams=self.depthparams)
else:
@@ -198,7 +200,9 @@ class ObjectFace(PathPocketBase.ObjectPocket):
import PathScripts.PathSurfaceSupport as PathSurfaceSupport
baseShape = oneBase[0].Shape
psZMin = planeshape.BoundBox.ZMin
ofstShape = PathSurfaceSupport.extractFaceOffset(planeshape, self.tool.Diameter * 1.1, planeshape)
ofstShape = PathUtils.getOffsetArea(planeshape,
self.tool.Diameter * 1.1,
plane=planeshape)
ofstShape.translate(FreeCAD.Vector(0.0, 0.0, psZMin - ofstShape.BoundBox.ZMin))
custDepthparams = self._customDepthParams(obj, obj.StartDepth.Value + 0.1, obj.FinalDepth.Value - 0.1) # only an envelope

View File

@@ -1069,7 +1069,7 @@ class ObjectProfile(PathAreaOp.ObjectOp):
cent1 = FreeCAD.Vector(lstVrt.X, lstVrt.Y, fdv)
# Calculate offset shape, containing cut region
ofstShp = self._extractFaceOffset(obj, cutShp, False)
ofstShp = self._getOffsetArea(obj, cutShp, False)
# CHECK for ZERO area of offset shape
try:
@@ -1174,40 +1174,22 @@ class ObjectProfile(PathAreaOp.ObjectOp):
return rtnWIRES
def _extractFaceOffset(self, obj, fcShape, isHole):
'''_extractFaceOffset(obj, fcShape, isHole) ... internal function.
Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified.
Adjustments made based on notes by @sliptonic - https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
PathLog.debug('_extractFaceOffset()')
def _getOffsetArea(self, obj, fcShape, isHole):
'''Get an offset area for a shape. Wrapper around
PathUtils.getOffsetArea.'''
PathLog.debug('_getOffsetArea()')
areaParams = {}
# JOB = PathUtils.findParentJob(obj)
# tolrnc = JOB.GeometryTolerance.Value
# if self.useComp:
# offset = self.ofstRadius # + tolrnc
# else:
# offset = self.offsetExtra # + tolrnc
JOB = PathUtils.findParentJob(obj)
tolerance = JOB.GeometryTolerance.Value
offset = self.ofstRadius
if isHole is False:
offset = 0 - offset
areaParams['Offset'] = offset
areaParams['Fill'] = 1
areaParams['Coplanar'] = 0
areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
areaParams['Reorient'] = True
areaParams['OpenMode'] = 0
areaParams['MaxArcPoints'] = 400 # 400
areaParams['Project'] = True
# areaParams['JoinType'] = 1
area = Path.Area() # Create instance of Area() class object
area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane
area.add(fcShape) # obj.Shape to use for extracting offset
area.setParams(**areaParams) # set parameters
return area.getShape()
return PathUtils.getOffsetArea(fcShape,
offset,
plane=fcShape,
tolerance=tolerance)
def _findNearestVertex(self, shape, point):
PathLog.debug('_findNearestVertex()')
@@ -1373,7 +1355,7 @@ class ObjectProfile(PathAreaOp.ObjectOp):
return (wireIdxs[0], wireIdxs[1])
def _makeCrossSection(self, shape, sliceZ, zHghtTrgt=False):
'''_makeCrossSection(shape, sliceZ, zHghtTrgt=None)...
'''_makeCrossSection(shape, sliceZ, zHghtTrgt=None)...
Creates cross-section objectc from shape. Translates cross-section to zHghtTrgt if available.
Makes face shape from cross-section object. Returns face shape at zHghtTrgt.'''
PathLog.debug('_makeCrossSection()')
@@ -1502,7 +1484,7 @@ class ObjectProfile(PathAreaOp.ObjectOp):
# 2 6
# | |
# | ----5----|
# | 4
# | 4
# -----3-------|
# positive dist in _makePerp2DVector() is CCW rotation
p1 = E

View File

@@ -383,60 +383,18 @@ class PathGeometryGenerator:
def _extractOffsetFaces(self):
PathLog.debug('_extractOffsetFaces()')
wires = list()
faces = list()
ofst = 0.0 # - self.cutOut
shape = self.shape
cont = True
cnt = 0
while cont:
ofstArea = self._getFaceOffset(shape, ofst)
if not ofstArea:
cont = False
True if cont else False # cont used for LGTM
offset = 0.0 # Start right at the edge of cut area
while True:
offsetArea = PathUtils.getOffsetArea(shape, offset, plane=self.wpc)
if not offsetArea:
# Area fully consumed
break
for F in ofstArea.Faces:
faces.append(F)
for w in F.Wires:
for f in offsetArea.Faces:
for w in f.Wires:
wires.append(w)
shape = ofstArea
if cnt == 0:
ofst = 0.0 - self.cutOut
cnt += 1
offset -= self.cutOut
return wires
def _getFaceOffset(self, shape, offset):
'''_getFaceOffset(shape, offset) ... internal function.
Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified.
Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
areaParams = {}
areaParams['Offset'] = offset
areaParams['Fill'] = 1 # 1
areaParams['Coplanar'] = 0
areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
areaParams['Reorient'] = True
areaParams['OpenMode'] = 0
areaParams['MaxArcPoints'] = 400 # 400
areaParams['Project'] = True
area = Path.Area() # Create instance of Area() class object
area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1
area.add(shape)
area.setParams(**areaParams) # set parameters
offsetShape = area.getShape()
wCnt = len(offsetShape.Wires)
if wCnt == 0:
return False
elif wCnt == 1:
ofstFace = Part.Face(offsetShape.Wires[0])
else:
W = list()
for wr in offsetShape.Wires:
W.append(Part.Face(wr))
ofstFace = Part.makeCompound(W)
return ofstFace
# Eclass
@@ -654,24 +612,17 @@ class ProcessSelectedFaces:
isHole = False
if self.obj.HandleMultipleFeatures == 'Collectively':
cont = True
fsL = list() # face shape list
ifL = list() # avoid shape list
outFCS = list()
PathLog.debug(
'Attempting to get cross-section of collective faces.')
outFCS, ifL = self.findUnifiedRegions(FCS)
if self.obj.InternalFeaturesCut and ifL:
ifL = list() # clear avoid shape list
# 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:
gIF = FUR.getInternalFeatures()
if gIF:
ifL.extend(gIF)
PathLog.debug('Attempting to get cross-section of collective faces.')
if len(outFCS) == 0:
msg = translate('PathSurfaceSupport',
'Cannot process selected faces. Check horizontal surface exposure.')
msg = translate(
'PathSurfaceSupport',
'Cannot process selected faces. Check horizontal '
'surface exposure.')
FreeCAD.Console.PrintError(msg + '\n')
cont = False
else:
@@ -681,7 +632,9 @@ class ProcessSelectedFaces:
if cont and self.profileEdges != 'None':
PathLog.debug('.. include Profile Edge')
ofstVal = self._calculateOffsetValue(isHole)
psOfst = extractFaceOffset(cfsL, ofstVal, self.wpc)
psOfst = PathUtils.getOffsetArea(cfsL,
ofstVal,
plane=self.wpc)
if psOfst:
mPS = [psOfst]
if self.profileEdges == 'Only':
@@ -698,7 +651,8 @@ class ProcessSelectedFaces:
self.tempGroup.addObject(T)
ofstVal = self._calculateOffsetValue(isHole)
faceOfstShp = extractFaceOffset(cfsL, ofstVal, self.wpc)
faceOfstShp = PathUtils.getOffsetArea(
cfsL, ofstVal, plane=self.wpc)
if not faceOfstShp:
msg = translate('PathSurfaceSupport',
'Failed to create offset face.') + '\n'
@@ -721,7 +675,8 @@ class ProcessSelectedFaces:
C.purgeTouched()
self.tempGroup.addObject(C)
ofstVal = self._calculateOffsetValue(isHole=True)
intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc)
intOfstShp = PathUtils.getOffsetArea(
casL, ofstVal, plane=self.wpc)
mIFS.append(intOfstShp)
mFS = [faceOfstShp]
@@ -730,28 +685,22 @@ class ProcessSelectedFaces:
elif self.obj.HandleMultipleFeatures == 'Individually':
for (fcshp, fcIdx) in FCS:
cont = True
ifL = list() # avoid shape list
fNum = fcIdx + 1
outerFace = False
# Use new face-unifying class
FUR = FindUnifiedRegions([(fcshp, fcIdx)], self.JOB.GeometryTolerance.Value)
if self.showDebugObjects:
FUR.setTempGroup(self.tempGroup)
gUR = FUR.getUnifiedRegions()
gUR, ifL = self.findUnifiedRegions(FCS)
if len(gUR) > 0:
outerFace = gUR[0]
if not self.obj.InternalFeaturesCut:
gIF = FUR.getInternalFeatures()
if gIF:
ifL = gIF
if self.obj.InternalFeaturesCut:
ifL = list() # avoid shape list
if outerFace:
PathLog.debug('Attempting to create offset face of Face{}'.format(fNum))
if self.profileEdges != 'None':
ofstVal = self._calculateOffsetValue(isHole)
psOfst = extractFaceOffset(outerFace, ofstVal, self.wpc)
psOfst = PathUtils.getOffsetArea(
outerFace, ofstVal, plane=self.wpc)
if psOfst:
if mPS is False:
mPS = list()
@@ -766,7 +715,8 @@ class ProcessSelectedFaces:
if cont:
ofstVal = self._calculateOffsetValue(isHole)
faceOfstShp = extractFaceOffset(outerFace, ofstVal, self.wpc)
faceOfstShp = PathUtils.getOffsetArea(
outerFace, ofstVal, plane=self.wpc)
lenIfl = len(ifL)
if self.obj.InternalFeaturesCut is False and lenIfl > 0:
@@ -776,7 +726,8 @@ class ProcessSelectedFaces:
casL = Part.makeCompound(ifL)
ofstVal = self._calculateOffsetValue(isHole=True)
intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc)
intOfstShp = PathUtils.getOffsetArea(
casL, ofstVal, plane=self.wpc)
mIFS.append(intOfstShp)
# faceOfstShp = faceOfstShp.cut(intOfstShp)
@@ -801,20 +752,9 @@ class ProcessSelectedFaces:
outFCS = list()
intFEAT = list()
for (fcshp, fcIdx) in VDS:
fNum = fcIdx + 1
# Use new face-unifying class
FUR = FindUnifiedRegions([(fcshp, fcIdx)], self.JOB.GeometryTolerance.Value)
if self.showDebugObjects:
FUR.setTempGroup(self.tempGroup)
gUR = FUR.getUnifiedRegions()
if len(gUR) > 0:
outFCS.extend(gUR)
if not self.obj.InternalFeaturesCut:
gIF = FUR.getInternalFeatures()
if gIF:
intFEAT.extend(gIF)
outFCS, intFEAT = self.findUnifiedRegions(VDS)
if self.obj.InternalFeaturesCut:
intFEAT = list()
lenOtFcs = len(outFCS)
if lenOtFcs == 0:
@@ -838,7 +778,9 @@ class ProcessSelectedFaces:
P.purgeTouched()
self.tempGroup.addObject(P)
ofstVal = self._calculateOffsetValue(isHole, isVoid=True)
avdOfstShp = extractFaceOffset(avoid, ofstVal, self.wpc)
avdOfstShp = PathUtils.getOffsetArea(avoid,
ofstVal,
plane=self.wpc)
if avdOfstShp is False:
msg = translate('PathSurfaceSupport',
'Failed to create collective offset avoid face.')
@@ -854,7 +796,9 @@ class ProcessSelectedFaces:
else:
ifc = intFEAT[0]
ofstVal = self._calculateOffsetValue(isHole=True)
ifOfstShp = extractFaceOffset(ifc, ofstVal, self.wpc)
ifOfstShp = PathUtils.getOffsetArea(ifc,
ofstVal,
plane=self.wpc)
if ifOfstShp is False:
msg = translate('PathSurfaceSupport',
'Failed to create collective offset avoid internal features.') + '\n'
@@ -900,7 +844,9 @@ class ProcessSelectedFaces:
if cont and self.profileEdges != 'None':
PathLog.debug(' -Attempting profile geometry for model base.')
ofstVal = self._calculateOffsetValue(isHole)
psOfst = extractFaceOffset(csFaceShape, ofstVal, self.wpc)
psOfst = PathUtils.getOffsetArea(csFaceShape,
ofstVal,
plane=self.wpc)
if psOfst:
if self.profileEdges == 'Only':
return (True, psOfst)
@@ -910,9 +856,10 @@ class ProcessSelectedFaces:
if cont:
ofstVal = self._calculateOffsetValue(isHole)
faceOffsetShape = extractFaceOffset(csFaceShape, ofstVal, self.wpc)
faceOffsetShape = PathUtils.getOffsetArea(csFaceShape, ofstVal,
plane=self.wpc)
if faceOffsetShape is False:
PathLog.debug('extractFaceOffset() failed for entire base.')
PathLog.debug('getOffsetArea() failed for entire base.')
else:
faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin))
return (faceOffsetShape, prflShp)
@@ -944,6 +891,55 @@ class ProcessSelectedFaces:
offset += self.radius + tolrnc
return offset
def findUnifiedRegions(
self,
shapeAndIndexTuples,
useAreaImplementation=True):
"""Wrapper around area and wire based region unification
implementations."""
PathLog.debug('findUnifiedRegions()')
# Allow merging of faces within the LinearDeflection tolerance.
tolerance = self.obj.LinearDeflection.Value
# Default: normal to Z=1 (XY plane), at Z=0
try:
# Use Area based implementation
shapes = Part.makeCompound([t[0] for t in shapeAndIndexTuples])
outlineShape = PathUtils.getOffsetArea(
shapes,
# Make the outline very slightly smaller, to avoid creating
# small edges in the cut with the hole-preserving projection.
0.0 - tolerance / 10,
removeHoles=True, # Outline has holes filled in
tolerance=tolerance,
plane=self.wpc)
projectionShape = PathUtils.getOffsetArea(
shapes,
# Make the projection very slightly larger
tolerance / 10,
removeHoles=False, # Projection has holes preserved
tolerance=tolerance,
plane=self.wpc)
internalShape = outlineShape.cut(projectionShape)
# Filter out tiny faces, usually artifacts around the perimeter of
# the cut.
minArea = (10 * tolerance)**2
internalFaces = [
f for f in internalShape.Faces if f.Area > minArea
]
if internalFaces:
internalFaces = Part.makeCompound(internalFaces)
return ([outlineShape], [internalFaces])
except Exception as e:
PathLog.warning(
"getOffsetArea failed: {}; Using FindUnifiedRegions.".format(
e))
# Use face-unifying class
FUR = FindUnifiedRegions(shapeAndIndexTuples, tolerance)
if self.showDebugObjects:
FUR.setTempGroup(self.tempGroup)
return (FUR.getUnifiedRegions(), FUR.getInternalFeatures)
# Eclass
@@ -1097,50 +1093,6 @@ def getSliceFromEnvelope(env):
return tf
# Function to extract offset face from shape
def extractFaceOffset(fcShape, offset, wpc, makeComp=True):
'''extractFaceOffset(fcShape, offset) ... internal function.
Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified.
Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
PathLog.debug('extractFaceOffset()')
if fcShape.BoundBox.ZMin != 0.0:
fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin))
areaParams = {}
areaParams['Offset'] = offset
areaParams['Fill'] = 1 # 1
areaParams['Coplanar'] = 0
areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
areaParams['Reorient'] = True
areaParams['OpenMode'] = 0
areaParams['MaxArcPoints'] = 400 # 400
areaParams['Project'] = True
area = Path.Area() # Create instance of Area() class object
# area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane
area.setPlane(PathUtils.makeWorkplane(wpc)) # Set working plane to normal at Z=1
area.add(fcShape)
area.setParams(**areaParams) # set parameters
offsetShape = area.getShape()
wCnt = len(offsetShape.Wires)
if wCnt == 0:
return False
elif wCnt == 1:
ofstFace = Part.Face(offsetShape.Wires[0])
if not makeComp:
ofstFace = [ofstFace]
else:
W = list()
for wr in offsetShape.Wires:
W.append(Part.Face(wr))
if makeComp:
ofstFace = Part.makeCompound(W)
else:
ofstFace = W
return ofstFace # offsetShape
def _prepareModelSTLs(self, JOB, obj, m, ocl):
@@ -1159,7 +1111,6 @@ def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes, ocl):
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]
@@ -1728,7 +1679,8 @@ def pathGeomToOffsetPointSet(obj, compGeoShp):
optimize = obj.OptimizeLinearPaths
ofstCnt = len(compGeoShp)
# Cycle through offeset loops
# Cycle through offset loops
iPOL = False
for ei in range(0, ofstCnt):
OS = compGeoShp[ei]
lenOS = len(OS)
@@ -2279,7 +2231,7 @@ class FindUnifiedRegions:
face = Part.Face(wCS)
return [face]
else:
(faceShp, fcIdx) = self.FACES[0]
(faceShp, fcIdx) = self.FACES[0]
msg = translate('PathSurfaceSupport',
'Failed to identify a horizontal cross-section for Face')
msg += '{}.\n'.format(fcIdx + 1)
@@ -2535,8 +2487,8 @@ class OCL_Tool():
if self.flatRadius == 0.0:
self.flatRadius = self.diameter * 0.25
elif self.flatRadius > 0.0:
self.flatRadius = self.flatRadius * 1.25
self.flatRadius = self.flatRadius * 1.25
def _oclCylCutter(self):
# Standard End Mill, Slot cutter, or Fly cutter
# OCL -> CylCutter::CylCutter(diameter, length)
@@ -2680,3 +2632,5 @@ def makeExtendedBoundBox(wBB, bbBfr, zDep):
L4 = Part.makeLine(p4, p1)
return Part.Face(Part.Wire([L1, L2, L3, L4]))

View File

@@ -336,6 +336,49 @@ def getEnvelope(partshape, subshape=None, depthparams=None):
return envelopeshape
# Function to extract offset face from shape
def getOffsetArea(fcShape,
offset,
removeHoles=False,
# Default: XY plane
plane=Part.makeCircle(10),
tolerance=1e-4):
'''Make an offset area of a shape, projected onto a plane.
Positive offsets expand the area, negative offsets shrink it.
Inspired by _buildPathArea() from PathAreaOp.py module. Adjustments made
based on notes by @sliptonic at this webpage:
https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
PathLog.debug('getOffsetArea()')
areaParams = {}
areaParams['Offset'] = offset
areaParams['Fill'] = 1 # 1
areaParams['Outline'] = removeHoles
areaParams['Coplanar'] = 0
areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
areaParams['Reorient'] = True
areaParams['OpenMode'] = 0
areaParams['MaxArcPoints'] = 400 # 400
areaParams['Project'] = True
areaParams['FitArcs'] = False # Can be buggy & expensive
areaParams['Deflection'] = tolerance
areaParams['Accuracy'] = tolerance
areaParams['Tolerance'] = 1e-5 # Equal point tolerance
areaParams['Simplify'] = True
areaParams['CleanDistance'] = tolerance / 5
area = Path.Area() # Create instance of Area() class object
# Set working plane normal to Z=1
area.setPlane(makeWorkplane(plane))
area.add(fcShape)
area.setParams(**areaParams) # set parameters
offsetShape = area.getShape()
if not offsetShape.Faces:
return False
return offsetShape
def reverseEdge(e):
if DraftGeomUtils.geomType(e) == "Circle":
arcstpt = e.valueAt(e.FirstParameter)

View File

@@ -951,7 +951,7 @@ class ObjectWaterline(PathOp.ObjectOp):
return commands
def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines):
'''_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ...
'''_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ...
Perform OCL scan for waterline purpose.'''
pdc = ocl.PathDropCutter() # create a pdc
pdc.setSTL(stl)
@@ -1299,7 +1299,10 @@ class ObjectWaterline(PathOp.ObjectOp):
activeArea = area.cut(trimFace)
activeAreaWireCnt = len(activeArea.Wires) # first wire is boundFace wire
self.showDebugObject(activeArea, 'ActiveArea_{}'.format(caCnt))
ofstArea = PathSurfaceSupport.extractFaceOffset(activeArea, ofst, self.wpc, makeComp=False)
ofstArea = PathUtils.getOffsetArea(activeArea,
ofst,
self.wpc,
makeComp=False)
if not ofstArea:
data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString
PathLog.debug('No offset area returned for cut area depth at {}.'.format(data))
@@ -1359,7 +1362,7 @@ class ObjectWaterline(PathOp.ObjectOp):
CUTAREAS = list()
isFirst = True
lenDP = len(depthparams)
# Cycle through layer depths
for dp in range(0, lenDP):
csHght = depthparams[dp]
@@ -1472,7 +1475,10 @@ class ObjectWaterline(PathOp.ObjectOp):
cont = True
cnt = 0
while cont:
ofstArea = PathSurfaceSupport.extractFaceOffset(shape, ofst, self.wpc, makeComp=True)
ofstArea = PathUtils.getOffsetArea(shape,
ofst,
self.wpc,
makeComp=True)
if not ofstArea:
break
for F in ofstArea.Faces:
@@ -1584,7 +1590,7 @@ class ObjectWaterline(PathOp.ObjectOp):
li = fIds.pop()
low = csFaces[li] # senior face
pIds = self._idInternalFeature(csFaces, fIds, pIds, li, low)
for af in range(lenCsF - 1, -1, -1): # cycle from last item toward first
prnt = pIds[af]
if prnt == -1: