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:
@@ -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]))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user