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