Files
create/src/Mod/Path/PathScripts/PathSurfaceSupport.py
Gabriel Wicke 84113fbd17 Path: More ambitious step-over and break optimizations
Extend the work from #3496 to allow the safe optimization of more
complex step transitions.

- Use the actual safePDC path for short step transitions and breaks,
  currently up to 2 cutter diameters. This value is chosen to cover
  basically all typical end step-overs, including those with heavy skew.
  Extending this much further (up to the break even point for a retract &
  rapid) will need some careful thinking for multi pass paths.
- Coordinate offset tolerances with per-operation tessellation
  tolerances, to avoid tessellation artifacts messing up paths by
  causing false retracts.  Such retracts can cause entire steps near
  vertical areas to be falsely skipped, which would cause a major
  deviation from the target model.  By considering per-job tolerances, we
  allow users to safely save computational resources by computing roughing
  operations with lower precision, or selectively increase precision for
  finish passes.
- Refine the default tessellation tolerance to GeometryTolerance / 4.
  This makes sure that the job GeometryTolerance is respected by
  operation defaults.
2020-06-05 18:21:24 -07:00

2321 lines
85 KiB
Python

# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2020 russ4262 <russ4262@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
from __future__ import print_function
__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."
__contributors__ = ""
import FreeCAD
from PySide import QtCore
import Path
import PathScripts.PathLog as PathLog
import PathScripts.PathUtils as PathUtils
import math
# 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())
# PathLog.trackModule(PathLog.thisModule())
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
class PathGeometryGenerator:
'''Creates a path geometry shape from an assigned pattern for conversion to tool paths.
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.
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
# Create a corresponding processing method below. Precede the name with an underscore(_)
patterns = ('Circular', 'CircularZigZag', 'Line', 'Offset', 'Spiral', 'ZigZag')
def __init__(self, obj, shape, pattern):
'''__init__(obj, shape, pattern)... Instantiate PathGeometryGenerator class.
Required arguments are the operation object, horizontal planar shape, and pattern name.'''
self.debugObjectsGroup = False
self.pattern = 'None'
self.shape = None
self.pathGeometry = None
self.rawGeoList = None
self.centerOfMass = None
self.centerofPattern = None
self.deltaX = None
self.deltaY = None
self.deltaC = None
self.halfDiag = None
self.halfPasses = None
self.obj = obj
self.toolDiam = float(obj.ToolController.Tool.Diameter)
self.cutOut = self.toolDiam * (float(obj.StepOver) / 100.0)
self.wpc = Part.makeCircle(2.0) # make circle for workplane
# validate requested pattern
if pattern in self.patterns:
if hasattr(self, '_' + pattern):
self.pattern = pattern
if shape.BoundBox.ZMin != 0.0:
shape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - shape.BoundBox.ZMin))
if shape.BoundBox.ZLength > 1.0e-8:
msg = translate('PathSurfaceSupport',
'Shape appears to not be horizontal planar.')
msg += ' ZMax == {} mm.\n'.format(shape.BoundBox.ZMax)
FreeCAD.Console.PrintWarning(msg)
else:
self.shape = shape
self._prepareConstants()
def _prepareConstants(self):
# Compute weighted center of mass of all faces combined
if self.pattern in ['Circular', 'CircularZigZag', 'Spiral']:
if self.obj.PatternCenterAt == 'CenterOfMass':
fCnt = 0
totArea = 0.0
zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0)
for F in self.shape.Faces:
comF = F.CenterOfMass
areaF = F.Area
totArea += areaF
fCnt += 1
zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF))
if fCnt == 0:
msg = translate('PathSurfaceSupport',
'Cannot calculate the Center Of Mass.')
msg += ' ' + translate('PathSurfaceSupport',
'Using Center of Boundbox instead.') + '\n'
FreeCAD.Console.PrintError(msg)
bbC = self.shape.BoundBox.Center
zeroCOM = FreeCAD.Vector(bbC.x, bbC.y, 0.0)
else:
avgArea = totArea / fCnt
zeroCOM.multiply(1 / fCnt)
zeroCOM.multiply(1 / avgArea)
self.centerOfMass = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0)
self.centerOfPattern = self._getPatternCenter()
else:
bbC = self.shape.BoundBox.Center
self.centerOfPattern = FreeCAD.Vector(bbC.x, bbC.y, 0.0)
# get X, Y, Z spans; Compute center of rotation
self.deltaX = self.shape.BoundBox.XLength
self.deltaY = self.shape.BoundBox.YLength
self.deltaC = self.shape.BoundBox.DiagonalLength # math.sqrt(self.deltaX**2 + self.deltaY**2)
lineLen = self.deltaC + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end
self.halfDiag = math.ceil(lineLen / 2.0)
cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal
self.halfPasses = math.ceil(cutPasses / 2.0)
# Public methods
def setDebugObjectsGroup(self, tmpGrpObject):
'''setDebugObjectsGroup(tmpGrpObject)...
Pass the temporary object group to show temporary construction objects'''
self.debugObjectsGroup = tmpGrpObject
def getCenterOfPattern(self):
'''getCenterOfPattern()...
Returns the Center Of Mass for the current class instance.'''
return self.centerOfPattern
def generatePathGeometry(self):
'''generatePathGeometry()...
Call this function to obtain the path geometry shape, generated by this class.'''
if self.pattern == 'None':
return False
if self.shape is None:
return False
cmd = 'self._' + self.pattern + '()'
exec(cmd)
if self.obj.CutPatternReversed is True:
self.rawGeoList.reverse()
# Create compound object to bind all lines in Lineset
geomShape = Part.makeCompound(self.rawGeoList)
# Position and rotate the Line and ZigZag geometry
if self.pattern in ['Line', 'ZigZag']:
if self.obj.CutPatternAngle != 0.0:
geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), self.obj.CutPatternAngle)
bbC = self.shape.BoundBox.Center
geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin)
if self.debugObjectsGroup:
F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpGeometrySet')
F.Shape = geomShape
F.purgeTouched()
self.debugObjectsGroup.addObject(F)
if self.pattern == 'Offset':
return geomShape
# Identify intersection of cross-section face and lineset
cmnShape = self.shape.common(geomShape)
if self.debugObjectsGroup:
F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpPathGeometry')
F.Shape = cmnShape
F.purgeTouched()
self.debugObjectsGroup.addObject(F)
return cmnShape
# Cut pattern methods
def _Circular(self):
GeoSet = list()
radialPasses = self._getRadialPasses()
minRad = self.toolDiam * 0.45
siX3 = 3 * self.obj.SampleInterval.Value
minRadSI = (siX3 / 2.0) / math.pi
if minRad < minRadSI:
minRad = minRadSI
PathLog.debug(' -centerOfPattern: {}'.format(self.centerOfPattern))
# Make small center circle to start pattern
if self.obj.StepOver > 50:
circle = Part.makeCircle(minRad, self.centerOfPattern)
GeoSet.append(circle)
for lc in range(1, radialPasses + 1):
rad = (lc * self.cutOut)
if rad >= minRad:
circle = Part.makeCircle(rad, self.centerOfPattern)
GeoSet.append(circle)
# Efor
self.rawGeoList = GeoSet
def _CircularZigZag(self):
self._Circular() # Use _Circular generator
def _Line(self):
GeoSet = list()
centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model
cAng = math.atan(self.deltaX / self.deltaY) # BoundaryBox angle
# Create end points for set of lines to intersect with cross-section face
pntTuples = list()
for lc in range((-1 * (self.halfPasses - 1)), self.halfPasses + 1):
x1 = centRot.x - self.halfDiag
x2 = centRot.x + self.halfDiag
y1 = centRot.y + (lc * self.cutOut)
# y2 = y1
p1 = FreeCAD.Vector(x1, y1, 0.0)
p2 = FreeCAD.Vector(x2, y1, 0.0)
pntTuples.append((p1, p2))
# Convert end points to lines
for (p1, p2) in pntTuples:
line = Part.makeLine(p1, p2)
GeoSet.append(line)
self.rawGeoList = GeoSet
def _Offset(self):
self.rawGeoList = self._extractOffsetFaces()
def _Spiral(self):
GeoSet = list()
SEGS = list()
draw = True
loopRadians = 0.0 # Used to keep track of complete loops/cycles
sumRadians = 0.0
loopCnt = 0
segCnt = 0
twoPi = 2.0 * math.pi
maxDist = math.ceil(self.cutOut * self._getRadialPasses()) # self.halfDiag
move = self.centerOfPattern # Use to translate the center of the spiral
lastPoint = FreeCAD.Vector(0.0, 0.0, 0.0)
# Set tool properties and calculate cutout
cutOut = self.cutOut / twoPi
segLen = self.obj.SampleInterval.Value # CutterDiameter / 10.0 # SampleInterval.Value
stepAng = segLen / ((loopCnt + 1) * self.cutOut) # math.pi / 18.0 # 10 degrees
stopRadians = maxDist / cutOut
if self.obj.CutPatternReversed:
if self.obj.CutMode == 'Conventional':
getPoint = self._makeOppSpiralPnt
else:
getPoint = self._makeRegSpiralPnt
while draw:
radAng = sumRadians + stepAng
p1 = lastPoint
p2 = getPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng
sumRadians += stepAng # Increment sumRadians
loopRadians += stepAng # Increment loopRadians
if loopRadians > twoPi:
loopCnt += 1
loopRadians -= twoPi
stepAng = segLen / ((loopCnt + 1) * self.cutOut) # adjust stepAng with each loop/cycle
segCnt += 1
lastPoint = p2
if sumRadians > stopRadians:
draw = False
# Create line and show in Object tree
lineSeg = Part.makeLine(p2, p1)
SEGS.append(lineSeg)
# Ewhile
SEGS.reverse()
else:
if self.obj.CutMode == 'Climb':
getPoint = self._makeOppSpiralPnt
else:
getPoint = self._makeRegSpiralPnt
while draw:
radAng = sumRadians + stepAng
p1 = lastPoint
p2 = getPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng
sumRadians += stepAng # Increment sumRadians
loopRadians += stepAng # Increment loopRadians
if loopRadians > twoPi:
loopCnt += 1
loopRadians -= twoPi
stepAng = segLen / ((loopCnt + 1) * self.cutOut) # adjust stepAng with each loop/cycle
segCnt += 1
lastPoint = p2
if sumRadians > stopRadians:
draw = False
# Create line and show in Object tree
lineSeg = Part.makeLine(p1, p2)
SEGS.append(lineSeg)
# Ewhile
# Eif
spiral = Part.Wire([ls.Edges[0] for ls in SEGS])
GeoSet.append(spiral)
self.rawGeoList = GeoSet
def _ZigZag(self):
self._Line() # Use _Line generator
# Support methods
def _getPatternCenter(self):
centerAt = self.obj.PatternCenterAt
if centerAt == 'CenterOfMass':
cntrPnt = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, 0.0)
elif centerAt == 'CenterOfBoundBox':
cent = self.shape.BoundBox.Center
cntrPnt = FreeCAD.Vector(cent.x, cent.y, 0.0)
elif centerAt == 'XminYmin':
cntrPnt = FreeCAD.Vector(self.shape.BoundBox.XMin, self.shape.BoundBox.YMin, 0.0)
elif centerAt == 'Custom':
cntrPnt = FreeCAD.Vector(self.obj.PatternCenterCustom.x, self.obj.PatternCenterCustom.y, 0.0)
# Update centerOfPattern point
if centerAt != 'Custom':
self.obj.PatternCenterCustom = cntrPnt
self.centerOfPattern = cntrPnt
return cntrPnt
def _getRadialPasses(self):
# recalculate number of passes, if need be
radialPasses = self.halfPasses
if self.obj.PatternCenterAt != 'CenterOfBoundBox':
# make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center
EBB = self.shape.BoundBox
CORNERS = [
FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0),
FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0),
FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0),
FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0),
]
dMax = 0.0
for c in range(0, 4):
dist = CORNERS[c].sub(self.centerOfPattern).Length
if dist > dMax:
dMax = dist
diag = dMax + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end
radialPasses = math.ceil(diag / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal
return radialPasses
def _makeRegSpiralPnt(self, move, b, radAng):
x = b * radAng * math.cos(radAng)
y = b * radAng * math.sin(radAng)
return FreeCAD.Vector(x, y, 0.0).add(move)
def _makeOppSpiralPnt(self, move, b, radAng):
x = b * radAng * math.cos(radAng)
y = b * radAng * math.sin(radAng)
return FreeCAD.Vector(-1 * x, y, 0.0).add(move)
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
break
for F in ofstArea.Faces:
faces.append(F)
for w in F.Wires:
wires.append(w)
shape = ofstArea
if cnt == 0:
ofst = 0.0 - self.cutOut
cnt += 1
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
class ProcessSelectedFaces:
"""ProcessSelectedFaces(JOB, obj) class.
This class processes the `obj.Base` object for selected geometery.
Calling the preProcessModel(module) method returns
two compound objects as a tuple: (FACES, VOIDS) or False."""
def __init__(self, JOB, obj):
self.modelSTLs = list()
self.profileShapes = list()
self.tempGroup = False
self.showDebugObjects = False
self.checkBase = False
self.module = None
self.radius = None
self.depthParams = None
self.msgNoFaces = translate('PathSurfaceSupport',
'Face selection is unavailable for Rotational scans.') + '\n'
self.msgNoFaces += ' ' + translate('PathSurfaceSupport',
'Ignoring selected faces.') + '\n'
self.JOB = JOB
self.obj = obj
self.profileEdges = 'None'
if hasattr(obj, 'ProfileEdges'):
self.profileEdges = obj.ProfileEdges
# Setup STL, model type, and bound box containers for each model in Job
for m in range(0, len(JOB.Model.Group)):
M = JOB.Model.Group[m]
self.modelSTLs.append(False)
self.profileShapes.append(False)
# make circle for workplane
self.wpc = Part.makeCircle(2.0)
def PathSurface(self):
if self.obj.Base:
if len(self.obj.Base) > 0:
self.checkBase = True
if self.obj.ScanType == 'Rotational':
self.checkBase = False
FreeCAD.Console.PrintWarning(self.msgNoFaces)
def PathWaterline(self):
if self.obj.Base:
if len(self.obj.Base) > 0:
self.checkBase = True
if self.obj.Algorithm in ['OCL Dropcutter', 'Experimental']:
self.checkBase = False
FreeCAD.Console.PrintWarning(self.msgNoFaces)
# public class methods
def setShowDebugObjects(self, grpObj, val):
self.tempGroup = grpObj
self.showDebugObjects = val
def preProcessModel(self, module):
PathLog.debug('preProcessModel()')
if not self._isReady(module):
return False
FACES = list()
VOIDS = list()
fShapes = list()
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):
FACES.append(False)
VOIDS.append(False)
fShapes.append(False)
vShapes.append(False)
# The user has selected subobjects from the base. Pre-Process each.
if self.checkBase:
PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.')
(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, 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):
self.modelSTLs[m] = True
# Process each model base, as a whole, as needed
for m in range(0, lenGRP):
if self.modelSTLs[m] and not fShapes[m]:
PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label))
if self.obj.BoundBox == 'BaseBoundBox':
base = GRP[m]
elif self.obj.BoundBox == 'Stock':
base = self.JOB.Stock
pPEB = self._preProcessEntireBase(base, m)
if pPEB is False:
msg = translate('PathSurfaceSupport',
'Failed to pre-process base as a whole.') + '\n'
FreeCAD.Console.PrintError(msg)
else:
(fcShp, prflShp) = pPEB
if fcShp:
if fcShp is True:
PathLog.debug(' -fcShp is True.')
fShapes[m] = True
else:
fShapes[m] = [fcShp]
if prflShp:
if fcShp:
PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m]))
if vShapes[m]:
PathLog.debug(' -Cutting void from base profile shape.')
adjPS = prflShp.cut(vShapes[m][0])
self.profileShapes[m] = [adjPS]
else:
PathLog.debug(' -vShapes[m] is False.')
self.profileShapes[m] = [prflShp]
else:
PathLog.debug(' -Saving base profile shape.')
self.profileShapes[m] = [prflShp]
PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m]))
# Efor
return (fShapes, vShapes)
# private class methods
def _isReady(self, module):
'''_isReady(module)... Internal method.
Checks if required attributes are available for processing obj.Base (the Base Geometry).'''
if hasattr(self, module):
self.module = module
modMethod = getattr(self, module) # gets the attribute only
modMethod() # executes as method
else:
return False
if not self.radius:
return False
if not self.depthParams:
return False
return True
def _identifyFacesAndVoids(self, F, V):
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:
for sb in SBS:
# Flag model for STL creation
mdlIdx = None
for m in range(0, lenGRP):
if bs is GRP[m]:
self.modelSTLs[m] = True
mdlIdx = m
break
TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub)
# Apply `AvoidXFaces` value
faceCnt = len(TUPS)
add = faceCnt - self.obj.AvoidLastX_Faces
for bst in range(0, faceCnt):
(m, base, sub) = TUPS[bst]
shape = getattr(base.Shape, sub)
if isinstance(shape, Part.Face):
faceIdx = int(sub[4:]) - 1
if bst < add:
if F[m] is False:
F[m] = list()
F[m].append((shape, faceIdx))
PathLog.debug('.. Cutting {}'.format(sub))
hasFace = True
else:
if V[m] is False:
V[m] = list()
V[m].append((shape, faceIdx))
PathLog.debug('.. Avoiding {}'.format(sub))
hasVoid = True
return (hasFace, hasVoid)
def _preProcessFacesAndVoids(self, base, FCS, VDS):
mFS = False
mVS = False
mPS = False
mIFS = list()
if FCS:
isHole = False
if self.obj.HandleMultipleFeatures == 'Collectively':
cont = True
fsL = list() # face shape list
ifL = list() # avoid shape list
outFCS = 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:
ifL.extend(FUR.getInternalFeatures())
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.')
FreeCAD.Console.PrintError(msg + '\n')
cont = False
else:
cfsL = Part.makeCompound(outFCS)
# Handle profile edges request
if cont and self.profileEdges != 'None':
PathLog.debug('.. include Profile Edge')
ofstVal = self._calculateOffsetValue(isHole)
psOfst = extractFaceOffset(cfsL, ofstVal, self.wpc)
if psOfst:
mPS = [psOfst]
if self.profileEdges == 'Only':
mFS = True
cont = False
else:
cont = False
if cont:
if self.showDebugObjects:
T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape')
T.Shape = cfsL
T.purgeTouched()
self.tempGroup.addObject(T)
ofstVal = self._calculateOffsetValue(isHole)
faceOfstShp = extractFaceOffset(cfsL, ofstVal, self.wpc)
if not faceOfstShp:
msg = translate('PathSurfaceSupport',
'Failed to create offset face.') + '\n'
FreeCAD.Console.PrintError(msg)
cont = False
if cont:
lenIfL = len(ifL)
if not self.obj.InternalFeaturesCut:
if lenIfL == 0:
PathLog.debug(' -No internal features saved.')
else:
if lenIfL == 1:
casL = ifL[0]
else:
casL = Part.makeCompound(ifL)
if self.showDebugObjects:
C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat')
C.Shape = casL
C.purgeTouched()
self.tempGroup.addObject(C)
ofstVal = self._calculateOffsetValue(isHole=True)
intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc)
mIFS.append(intOfstShp)
mFS = [faceOfstShp]
# Eif
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()
if len(gUR) > 0:
outerFace = gUR[0]
if not self.obj.InternalFeaturesCut:
ifL = FUR.getInternalFeatures()
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)
if psOfst:
if mPS is False:
mPS = list()
mPS.append(psOfst)
if self.profileEdges == 'Only':
if mFS is False:
mFS = list()
mFS.append(True)
cont = False
else:
cont = False
if cont:
ofstVal = self._calculateOffsetValue(isHole)
faceOfstShp = extractFaceOffset(outerFace, ofstVal, self.wpc)
lenIfl = len(ifL)
if self.obj.InternalFeaturesCut is False and lenIfl > 0:
if lenIfl == 1:
casL = ifL[0]
else:
casL = Part.makeCompound(ifL)
ofstVal = self._calculateOffsetValue(isHole=True)
intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc)
mIFS.append(intOfstShp)
# faceOfstShp = faceOfstShp.cut(intOfstShp)
if mFS is False:
mFS = list()
mFS.append(faceOfstShp)
# Eif
# Efor
# Eif
# Eif
if len(mIFS) > 0:
if mVS is False:
mVS = list()
for ifs in mIFS:
mVS.append(ifs)
if VDS:
PathLog.debug('Processing avoid faces.')
cont = True
isHole = False
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)
outFCS.extend(FUR.getUnifiedRegions())
if not self.obj.InternalFeaturesCut:
intFEAT.extend(FUR.getInternalFeatures())
lenOtFcs = len(outFCS)
if lenOtFcs == 0:
cont = False
else:
if lenOtFcs == 1:
avoid = outFCS[0]
else:
avoid = Part.makeCompound(outFCS)
if self.showDebugObjects:
P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope')
P.Shape = avoid
P.purgeTouched()
self.tempGroup.addObject(P)
if cont:
if self.showDebugObjects:
P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound')
P.Shape = avoid
P.purgeTouched()
self.tempGroup.addObject(P)
ofstVal = self._calculateOffsetValue(isHole, isVoid=True)
avdOfstShp = extractFaceOffset(avoid, ofstVal, self.wpc)
if avdOfstShp is False:
msg = translate('PathSurfaceSupport',
'Failed to create collective offset avoid face.')
FreeCAD.Console.PrintError(msg + '\n')
cont = False
if cont:
avdShp = avdOfstShp
if not self.obj.AvoidLastX_InternalFeatures and len(intFEAT) > 0:
if len(intFEAT) > 1:
ifc = Part.makeCompound(intFEAT)
else:
ifc = intFEAT[0]
ofstVal = self._calculateOffsetValue(isHole=True)
ifOfstShp = extractFaceOffset(ifc, ofstVal, self.wpc)
if ifOfstShp is False:
msg = translate('PathSurfaceSupport',
'Failed to create collective offset avoid internal features.') + '\n'
FreeCAD.Console.PrintError(msg)
else:
avdShp = avdOfstShp.cut(ifOfstShp)
if mVS is False:
mVS = list()
mVS.append(avdShp)
return (mFS, mVS, mPS)
def _preProcessEntireBase(self, base, m):
cont = True
isHole = False
prflShp = False
# Create envelope, extract cross-section and make offset co-planar shape
# baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams)
try:
baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape
except Exception as ee:
PathLog.error(str(ee))
shell = base.Shape.Shells[0]
solid = Part.makeSolid(shell)
try:
baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape
except Exception as eee:
PathLog.error(str(eee))
cont = False
if cont:
csFaceShape = getShapeSlice(baseEnv)
if csFaceShape is False:
csFaceShape = getCrossSection(baseEnv)
if csFaceShape is False:
csFaceShape = getSliceFromEnvelope(baseEnv)
if csFaceShape is False:
PathLog.debug('Failed to slice baseEnv shape.')
cont = False
if cont and self.profileEdges != 'None':
PathLog.debug(' -Attempting profile geometry for model base.')
ofstVal = self._calculateOffsetValue(isHole)
psOfst = extractFaceOffset(csFaceShape, ofstVal, self.wpc)
if psOfst:
if self.profileEdges == 'Only':
return (True, psOfst)
prflShp = psOfst
else:
cont = False
if cont:
ofstVal = self._calculateOffsetValue(isHole)
faceOffsetShape = extractFaceOffset(csFaceShape, ofstVal, self.wpc)
if faceOffsetShape is False:
PathLog.debug('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 _calculateOffsetValue(self, isHole, isVoid=False):
'''_calculateOffsetValue(self.obj, isHole, isVoid) ... internal function.
Calculate the offset for the Path.Area() function.'''
self.JOB = PathUtils.findParentJob(self.obj)
# We need to offset by at least our linear tessellation deflection
# (default GeometryTolerance / 4) to avoid false retracts at the
# boundaries.
tolrnc = max(self.JOB.GeometryTolerance.Value / 10.0,
self.obj.LinearDeflection.Value)
if isVoid is False:
if isHole is True:
offset = -1 * self.obj.InternalFeaturesAdjustment.Value
offset += self.radius + tolrnc
else:
offset = -1 * self.obj.BoundaryAdjustment.Value
if self.obj.BoundaryEnforcement is True:
offset += self.radius + tolrnc
else:
offset -= self.radius + tolrnc
offset = 0.0 - offset
else:
offset = -1 * self.obj.BoundaryAdjustment.Value
offset += self.radius + tolrnc
return offset
# Eclass
# Functions for getting a shape envelope and cross-section
def getExtrudedShape(wire):
PathLog.debug('getExtrudedShape()')
wBB = wire.BoundBox
extFwd = math.floor(2.0 * wBB.ZLength) + 10.0
try:
shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd))
except Exception as ee:
PathLog.error(' -extrude wire failed: \n{}'.format(ee))
return False
SHP = Part.makeSolid(shell)
return SHP
def getShapeSlice(shape):
PathLog.debug('getShapeSlice()')
bb = shape.BoundBox
mid = (bb.ZMin + bb.ZMax) / 2.0
xmin = bb.XMin - 1.0
xmax = bb.XMax + 1.0
ymin = bb.YMin - 1.0
ymax = bb.YMax + 1.0
p1 = FreeCAD.Vector(xmin, ymin, mid)
p2 = FreeCAD.Vector(xmax, ymin, mid)
p3 = FreeCAD.Vector(xmax, ymax, mid)
p4 = FreeCAD.Vector(xmin, ymax, mid)
e1 = Part.makeLine(p1, p2)
e2 = Part.makeLine(p2, p3)
e3 = Part.makeLine(p3, p4)
e4 = Part.makeLine(p4, p1)
face = Part.Face(Part.Wire([e1, e2, e3, e4]))
fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area
sArea = shape.BoundBox.XLength * shape.BoundBox.YLength
midArea = (fArea + sArea) / 2.0
slcShp = shape.common(face)
slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength
if slcArea < midArea:
for W in slcShp.Wires:
if W.isClosed() is False:
PathLog.debug(' -wire.isClosed() is False')
return False
if len(slcShp.Wires) == 1:
wire = slcShp.Wires[0]
slc = Part.Face(wire)
slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
return slc
else:
fL = list()
for W in slcShp.Wires:
slc = Part.Face(W)
slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
fL.append(slc)
comp = Part.makeCompound(fL)
return comp
return False
def getProjectedFace(tempGroup, wire):
import Draft
PathLog.debug('getProjectedFace()')
F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire')
F.Shape = wire
F.purgeTouched()
tempGroup.addObject(F)
try:
prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1))
prj.recompute()
prj.purgeTouched()
tempGroup.addObject(prj)
except Exception as ee:
PathLog.error(str(ee))
return False
else:
pWire = Part.Wire(prj.Shape.Edges)
if pWire.isClosed() is False:
return False
slc = Part.Face(pWire)
slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
return slc
def getCrossSection(shape):
PathLog.debug('getCrossSection()')
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 !
comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin))
csWire = comp.Wires[0]
if csWire.isClosed() is False:
PathLog.debug(' -comp.Wires[0] is not closed')
return False
CS = Part.Face(csWire)
CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin))
return CS
else:
PathLog.debug(' -No wires from .slice() method')
return False
def getShapeEnvelope(shape):
PathLog.debug('getShapeEnvelope()')
wBB = shape.BoundBox
extFwd = wBB.ZLength + 10.0
minz = wBB.ZMin
maxz = wBB.ZMin + extFwd
stpDwn = (maxz - minz) / 4.0
dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz)
try:
env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape
except Exception as ee:
FreeCAD.Console.PrintError('PathUtils.getEnvelope() failed.\n' + str(ee) + '\n')
return False
else:
return env
def getSliceFromEnvelope(env):
PathLog.debug('getSliceFromEnvelope()')
eBB = env.BoundBox
extFwd = eBB.ZLength + 10.0
maxz = eBB.ZMin + extFwd
emax = math.floor(maxz - 1.0)
E = list()
for e in range(0, len(env.Edges)):
emin = env.Edges[e].BoundBox.ZMin
if emin > emax:
E.append(env.Edges[e])
tf = Part.Face(Part.Wire(Part.__sortEdges__(E)))
tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin))
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):
"""Tessellate model shapes or copy existing meshes into ocl.STLSurf
objects"""
PathLog.debug('_prepareModelSTLs()')
if self.modelSTLs[m] is True:
model = JOB.Model.Group[m]
if self.modelSTLs[m] is True:
self.modelSTLs[m] = _makeSTL(model, obj, ocl, self.modelTypes[m])
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:
msg = translate('PathSurfaceSupport',
'Path transitions might not avoid the model. Verify paths.')
FreeCAD.Console.PrintWarning(msg + '\n')
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:
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)
self.safeSTLs[mdlIdx] = _makeSTL(fused, obj, ocl)
def _makeSTL(model, obj, ocl, model_type=None):
"""Convert a mesh or shape into an OCL STL, using the tessellation
tolerance specified in obj.LinearDeflection.
Returns an ocl.STLSurf()."""
if model_type == 'M':
facets = model.Mesh.Facets.Points
else:
if hasattr(model, 'Shape'):
shape = model.Shape
else:
shape = model
vertices, facet_indices = shape.tessellate(
obj.LinearDeflection.Value)
facets = ((vertices[f[0]], vertices[f[1]], vertices[f[2]])
for f in facet_indices)
stl = ocl.STLSurf()
for tri in facets:
v1, v2, v3 = tri
t = ocl.Triangle(ocl.Point(v1[0], v1[1], v1[2]),
ocl.Point(v2[0], v2[1], v2[2]),
ocl.Point(v3[0], v3[1], v3[2]))
stl.addTriangle(t)
return 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)...
Convert a compound set of sequential line segments to directionally-oriented collinear groupings.'''
PathLog.debug('pathGeomToLinesPointSet()')
# Extract intersection line segments for return value as list()
LINES = list()
inLine = list()
chkGap = False
lnCnt = 0
ec = len(compGeoShp.Edges)
cpa = obj.CutPatternAngle
edg0 = compGeoShp.Edges[0]
p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
if cutClimb is True:
tup = (p2, p1)
lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
else:
tup = (p1, p2)
lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
inLine.append(tup)
sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
for ei in range(1, ec):
chkGap = False
edg = compGeoShp.Edges[ei] # Get edge for vertexes
v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0
v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1
ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point)
# iC = sp.isOnLineSegment(ep, cp)
iC = cp.isOnLineSegment(sp, ep)
if iC is True:
inLine.append('BRK')
chkGap = True
else:
if cutClimb is True:
inLine.reverse()
LINES.append(inLine) # Save inLine segments
lnCnt += 1
inLine = list() # reset collinear container
if cutClimb is True:
sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
else:
sp = ep
if cutClimb is True:
tup = (v2, v1)
if chkGap is True:
gap = abs(toolDiam - lst.sub(ep).Length)
lst = cp
else:
tup = (v1, v2)
if chkGap is True:
gap = abs(toolDiam - lst.sub(cp).Length)
lst = ep
if chkGap is True:
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
tup = (vA, tup[1])
closedGap = True
True if closedGap else False # used closedGap for LGTM
else:
gap = round(gap, 6)
if gap < gaps[0]:
gaps.insert(0, gap)
gaps.pop()
inLine.append(tup)
# Efor
lnCnt += 1
if cutClimb is True:
inLine.reverse()
LINES.append(inLine) # Save inLine segments
# Handle last inLine set, reversing it.
if obj.CutPatternReversed is True:
if cpa != 0.0 and cpa % 90.0 == 0.0:
F = LINES.pop(0)
rev = list()
for iL in F:
if iL == 'BRK':
rev.append(iL)
else:
(p1, p2) = iL
rev.append((p2, p1))
rev.reverse()
LINES.insert(0, rev)
isEven = lnCnt % 2
if isEven == 0:
PathLog.debug('Line count is ODD: {}.'.format(lnCnt))
else:
PathLog.debug('Line count is even: {}.'.format(lnCnt))
return LINES
def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps):
'''_pathGeomToZigzagPointSet(obj, compGeoShp)...
Convert a compound set of sequential line segments to directionally-oriented collinear groupings
with a ZigZag directional indicator included for each collinear group.'''
PathLog.debug('_pathGeomToZigzagPointSet()')
# Extract intersection line segments for return value as list()
LINES = list()
inLine = list()
lnCnt = 0
chkGap = False
ec = len(compGeoShp.Edges)
dirFlg = 1
if cutClimb:
dirFlg = -1
edg0 = compGeoShp.Edges[0]
p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
if dirFlg == 1:
tup = (p1, p2)
lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
else:
tup = (p2, p1)
lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point
inLine.append(tup)
for ei in range(1, ec):
edg = compGeoShp.Edges[ei]
v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y)
v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y)
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 = cp.isOnLineSegment(sp, ep)
if iC:
inLine.append('BRK')
chkGap = True
gap = abs(toolDiam - lst.sub(cp).Length)
else:
chkGap = False
if dirFlg == -1:
inLine.reverse()
LINES.append(inLine)
lnCnt += 1
dirFlg = -1 * dirFlg # Change zig to zag
inLine = list() # reset collinear container
sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
lst = ep
if dirFlg == 1:
tup = (v1, v2)
else:
tup = (v2, v1)
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
if dirFlg == 1:
tup = (vA, tup[1])
else:
tup = (tup[0], vB)
closedGap = True
else:
gap = round(gap, 6)
if gap < gaps[0]:
gaps.insert(0, gap)
gaps.pop()
inLine.append(tup)
# Efor
lnCnt += 1
# Fix directional issue with LAST line when line count is even
isEven = lnCnt % 2
if isEven == 0: # Changed to != with 90 degree CutPatternAngle
PathLog.debug('Line count is even: {}.'.format(lnCnt))
else:
PathLog.debug('Line count is ODD: {}.'.format(lnCnt))
dirFlg = -1 * dirFlg
if not obj.CutPatternReversed:
if cutClimb:
dirFlg = -1 * dirFlg
if obj.CutPatternReversed:
dirFlg = -1 * dirFlg
# Handle last inLine list
if dirFlg == 1:
rev = list()
for iL in inLine:
if iL == 'BRK':
rev.append(iL)
else:
(p1, p2) = iL
rev.append((p2, p1))
if not obj.CutPatternReversed:
rev.reverse()
else:
rev2 = list()
for iL in rev:
if iL == 'BRK':
rev2.append(iL)
else:
(p1, p2) = iL
rev2.append((p2, p1))
rev2.reverse()
rev = rev2
LINES.append(rev)
else:
LINES.append(inLine)
return LINES
def pathGeomToCircularPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps, COM):
'''pathGeomToCircularPointSet(obj, compGeoShp)...
Convert a compound set of arcs/circles to a set of directionally-oriented arc end points
and the corresponding center point.'''
# Extract intersection line segments for return value as list()
PathLog.debug('pathGeomToCircularPointSet()')
ARCS = list()
stpOvrEI = list()
segEI = list()
isSame = False
sameRad = None
ec = len(compGeoShp.Edges)
def gapDist(sp, ep):
X = (ep[0] - sp[0])**2
Y = (ep[1] - sp[1])**2
return math.sqrt(X + Y) # the 'z' value is zero in both points
# Separate arc data into Loops and Arcs
for ei in range(0, ec):
edg = compGeoShp.Edges[ei]
if edg.Closed is True:
stpOvrEI.append(('L', ei, False))
else:
if isSame is False:
segEI.append(ei)
isSame = True
pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
sameRad = pnt.sub(COM).Length
else:
# Check if arc is co-radial to current SEGS
pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
if abs(sameRad - pnt.sub(COM).Length) > 0.00001:
isSame = False
if isSame is True:
segEI.append(ei)
else:
# Move co-radial arc segments
stpOvrEI.append(['A', segEI, False])
# Start new list of arc segments
segEI = [ei]
isSame = True
pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
sameRad = pnt.sub(COM).Length
# Process trailing `segEI` data, if available
if isSame is True:
stpOvrEI.append(['A', segEI, False])
# Identify adjacent arcs with y=0 start/end points that connect
for so in range(0, len(stpOvrEI)):
SO = stpOvrEI[so]
if SO[0] == 'A':
startOnAxis = list()
endOnAxis = list()
EI = SO[1] # list of corresponding compGeoShp.Edges indexes
# Identify startOnAxis and endOnAxis arcs
for i in range(0, len(EI)):
ei = EI[i] # edge index
E = compGeoShp.Edges[ei] # edge object
if abs(COM.y - E.Vertexes[0].Y) < 0.00001:
startOnAxis.append((i, ei, E.Vertexes[0]))
elif abs(COM.y - E.Vertexes[1].Y) < 0.00001:
endOnAxis.append((i, ei, E.Vertexes[1]))
# Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected
lenSOA = len(startOnAxis)
lenEOA = len(endOnAxis)
if lenSOA > 0 and lenEOA > 0:
for soa in range(0, lenSOA):
(iS, eiS, vS) = startOnAxis[soa]
for eoa in range(0, len(endOnAxis)):
(iE, eiE, vE) = endOnAxis[eoa]
dist = vE.X - vS.X
if abs(dist) < 0.00001: # They connect on axis at same radius
SO[2] = (eiE, eiS)
break
elif dist > 0:
break # stop searching
# Eif
# Eif
# Efor
# Construct arc data tuples for OCL
dirFlg = 1
if not cutClimb: # True yields Climb when set to Conventional
dirFlg = -1
# Cycle through stepOver data
for so in range(0, len(stpOvrEI)):
SO = stpOvrEI[so]
if SO[0] == 'L': # L = Loop/Ring/Circle
# PathLog.debug("SO[0] == 'Loop'")
lei = SO[1] # loop Edges index
v1 = compGeoShp.Edges[lei].Vertexes[0]
# space = obj.SampleInterval.Value / 10.0
# space = 0.000001
space = toolDiam * 0.005 # If too small, OCL will fail to scan the loop
# p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z)
p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) # z=0.0 for waterline; z=v1.Z for 3D Surface
rad = p1.sub(COM).Length
spcRadRatio = space/rad
if spcRadRatio < 1.0:
tolrncAng = math.asin(spcRadRatio)
else:
tolrncAng = 0.99999998 * math.pi
EX = COM.x + (rad * math.cos(tolrncAng))
EY = v1.Y - space # rad * math.sin(tolrncAng)
sp = (v1.X, v1.Y, 0.0)
ep = (EX, EY, 0.0)
cp = (COM.x, COM.y, 0.0)
if dirFlg == 1:
arc = (sp, ep, cp)
else:
arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
ARCS.append(('L', dirFlg, [arc]))
else: # SO[0] == 'A' A = Arc
# PathLog.debug("SO[0] == 'Arc'")
PRTS = list()
EI = SO[1] # list of corresponding Edges indexes
CONN = SO[2] # list of corresponding connected edges tuples (iE, iS)
chkGap = False
lst = None
if CONN:
(iE, iS) = CONN
v1 = compGeoShp.Edges[iE].Vertexes[0]
v2 = compGeoShp.Edges[iS].Vertexes[1]
sp = (v1.X, v1.Y, 0.0)
ep = (v2.X, v2.Y, 0.0)
cp = (COM.x, COM.y, 0.0)
if dirFlg == 1:
arc = (sp, ep, cp)
lst = ep
else:
arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
lst = sp
PRTS.append(arc)
# Pop connected edge index values from arc segments index list
iEi = EI.index(iE)
iSi = EI.index(iS)
if iEi > iSi:
EI.pop(iEi)
EI.pop(iSi)
else:
EI.pop(iSi)
EI.pop(iEi)
if len(EI) > 0:
PRTS.append('BRK')
chkGap = True
cnt = 0
for ei in EI:
if cnt > 0:
PRTS.append('BRK')
chkGap = True
v1 = compGeoShp.Edges[ei].Vertexes[0]
v2 = compGeoShp.Edges[ei].Vertexes[1]
sp = (v1.X, v1.Y, 0.0)
ep = (v2.X, v2.Y, 0.0)
cp = (COM.x, COM.y, 0.0)
if dirFlg == 1:
arc = (sp, ep, cp)
if chkGap is True:
gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length)
lst = ep
else:
arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
if chkGap is True:
gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length)
lst = sp
if chkGap is True:
if gap < obj.GapThreshold.Value:
PRTS.pop() # pop off 'BRK' marker
(vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current
arc = (vA, arc[1], vC)
closedGap = True
else:
gap = round(gap, 6)
if gap < gaps[0]:
gaps.insert(0, gap)
gaps.pop()
PRTS.append(arc)
cnt += 1
if dirFlg == -1:
PRTS.reverse()
ARCS.append(('A', dirFlg, PRTS))
# Eif
if obj.CutPattern == 'CircularZigZag':
dirFlg = -1 * dirFlg
# Efor
return ARCS
def pathGeomToSpiralPointSet(obj, compGeoShp):
'''_pathGeomToSpiralPointSet(obj, compGeoShp)...
Convert a compound set of sequential line segments to directional, connected groupings.'''
PathLog.debug('_pathGeomToSpiralPointSet()')
# Extract intersection line segments for return value as list()
LINES = list()
inLine = list()
lnCnt = 0
ec = len(compGeoShp.Edges)
start = 2
if obj.CutPatternReversed:
edg1 = compGeoShp.Edges[0] # Skip first edge, as it is the closing edge: center to outer tail
ec -= 1
start = 1
else:
edg1 = compGeoShp.Edges[1] # Skip first edge, as it is the closing edge: center to outer tail
p1 = FreeCAD.Vector(edg1.Vertexes[0].X, edg1.Vertexes[0].Y, 0.0)
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)
for ei in range(start, ec): # Skipped first edge, started with second edge above as edg1
edg = compGeoShp.Edges[ei] # Get edge for vertexes
sp = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) # check point (first / middle point)
ep = FreeCAD.Vector(edg.Vertexes[1].X, edg.Vertexes[1].Y, 0.0) # end point
tup = ((sp.x, sp.y), (ep.x, ep.y))
if sp.sub(p2).Length < 0.000001:
inLine.append(tup)
else:
LINES.append(inLine) # Save inLine segments
lnCnt += 1
inLine = list() # reset container
inLine.append(tup)
# p1 = sp
p2 = ep
# Efor
lnCnt += 1
LINES.append(inLine) # Save inLine segments
return LINES
def pathGeomToOffsetPointSet(obj, compGeoShp):
'''pathGeomToOffsetPointSet(obj, compGeoShp)...
Convert a compound set of 3D profile segmented wires to 2D segments, applying linear optimization.'''
PathLog.debug('pathGeomToOffsetPointSet()')
LINES = list()
optimize = obj.OptimizeLinearPaths
ofstCnt = len(compGeoShp)
# Cycle through offeset loops
for ei in range(0, ofstCnt):
OS = compGeoShp[ei]
lenOS = len(OS)
if ei > 0:
LINES.append('BRK')
fp = FreeCAD.Vector(OS[0].x, OS[0].y, OS[0].z)
OS.append(fp)
# Cycle through points in each loop
prev = OS[0]
pnt = OS[1]
for v in range(1, lenOS):
nxt = OS[v + 1]
if optimize:
# iPOL = prev.isOnLineSegment(nxt, pnt)
iPOL = pnt.isOnLineSegment(prev, nxt)
if iPOL:
pnt = nxt
else:
tup = ((prev.x, prev.y), (pnt.x, pnt.y))
LINES.append(tup)
prev = pnt
pnt = nxt
else:
tup = ((prev.x, prev.y), (pnt.x, pnt.y))
LINES.append(tup)
prev = pnt
pnt = nxt
if iPOL:
tup = ((prev.x, prev.y), (pnt.x, pnt.y))
LINES.append(tup)
# Efor
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):
msg = translate('PathSurfaceSupport',
'Faild to extract processing region for Face')
FreeCAD.Console.PrintError(msg + '{}.\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):
PathLog.debug('_groupEdgesByLength()')
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):
PathLog.debug('_identifySharedEdgesByLength()')
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):
PathLog.debug('_extractWiresFromEdges()')
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
numLoops = len(LOOPS)
PathLog.debug(' -numLoops: {}.'.format(numLoops))
if numLoops > 0:
FACES = list()
for li in range(0, numLoops):
Edges = LOOPS[li]
#for e in Edges:
# self._showShape(e, 'Loop_{}_Edge'.format(li))
wire = Part.Wire(Part.__sortEdges__(Edges))
if wire.isClosed():
# This simple Part.Face() method fails to catch
# wires with tangent closed wires, or an external
# wire with one or more internal tangent wires.
# face = Part.Face(wire)
# This method works with the complex tangent
# closed wires mentioned above.
extWire = wire.extrude(FreeCAD.Vector(0.0, 0.0, 2.0))
wireSolid = Part.makeSolid(extWire)
extdBBFace1 = makeExtendedBoundBox(wireSolid.BoundBox, 5.0, wireSolid.BoundBox.ZMin + 1.0)
extdBBFace2 = makeExtendedBoundBox(wireSolid.BoundBox, 5.0, wireSolid.BoundBox.ZMin + 1.0)
inverse = extdBBFace1.cut(wireSolid)
face = extdBBFace2.cut(inverse)
self.REGIONS.append(face)
self.REGIONS.sort(key=faceArea, reverse=True)
def _identifyInternalFeatures(self):
PathLog.debug('_identifyInternalFeatures()')
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:
PathLog.debug(' - No common area.\n')
remList.sort(reverse=True)
for ri in remList:
self.REGIONS.pop(ri)
def _processNestedRegions(self):
PathLog.debug('_processNestedRegions()')
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.'''
PathLog.debug('getUnifiedRegions()')
self.INTERNALS = list()
if len(self.FACES) == 0:
msg = translate('PathSurfaceSupport',
'No FACE data tuples received at instantiation of class.')
FreeCAD.Console.PrintError(msg + '\n')
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):
wr = topFace.Wires[w]
self.INTERNALS.append(Part.Face(wr))
# Flatten face and extract outer wire, then convert to face
extWire = getExtrudedShape(topFace)
wCS = getCrossSection(extWire)
if wCS:
face = Part.Face(wCS)
return [face]
else:
(faceShp, fcIdx) = self.FACES[0]
msg = translate('PathSurfaceSupport',
'Failed to identify a horizontal cross-section for Face')
msg += '{}.\n'.format(fcIdx + 1)
FreeCAD.Console.PrintWarning(msg)
return []
# 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.')
return [topFace for (topFace, fcIdx) in self.topFaces]
else:
# Delete shared edges from edgeData list
self.sharedEdgeIdxs.sort(reverse=True)
for se in self.sharedEdgeIdxs:
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
msg = translate('PathSurfaceSupport',
'getUnifiedRegions() must be called before getInternalFeatures().')
FreeCAD.Console.PrintError(msg + '\n')
return False
# Eclass
# Support functions
def makeExtendedBoundBox(wBB, bbBfr, zDep):
PathLog.debug('makeExtendedBoundBox()')
p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep)
p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep)
p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep)
p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep)
L1 = Part.makeLine(p1, p2)
L2 = Part.makeLine(p2, p3)
L3 = Part.makeLine(p3, p4)
L4 = Part.makeLine(p4, p1)
return Part.Face(Part.Wire([L1, L2, L3, L4]))