Merge pull request #3585 from gwicke/unified_region_experiments

Path: Area based unified projection implementation
This commit is contained in:
sliptonic
2020-07-28 11:48:56 -05:00
committed by GitHub
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: