1855 lines
70 KiB
Python
1855 lines
70 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."
|
|
# __name__ = "PathSurfaceSupport"
|
|
__contributors__ = ""
|
|
|
|
import FreeCAD
|
|
from PySide import QtCore
|
|
import Path
|
|
import PathScripts.PathLog as PathLog
|
|
import PathScripts.PathUtils as PathUtils
|
|
import math
|
|
import 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 == 0.0:
|
|
self.shape = shape
|
|
else:
|
|
PathLog.warning('Shape appears to not be horizontal planar. ZMax is {}.'.format(shape.BoundBox.ZMax))
|
|
|
|
self._prepareConstants()
|
|
|
|
def _prepareConstants(self):
|
|
# Apply drop cutter extra offset and set the max and min XY area of the operation
|
|
xmin = self.shape.BoundBox.XMin
|
|
xmax = self.shape.BoundBox.XMax
|
|
ymin = self.shape.BoundBox.YMin
|
|
ymax = self.shape.BoundBox.YMax
|
|
|
|
# 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:
|
|
PathLog.error(translate(self.module, 'Cannot calculate the Center Of Mass. Using Center of Boundbox instead.'))
|
|
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':
|
|
PathLog.warning('PGG: No pattern set.')
|
|
return False
|
|
|
|
if self.shape is None:
|
|
PathLog.warning('PGG: No shape set.')
|
|
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
|
|
|
|
# Determine end points and create top lines
|
|
x1 = centRot.x - self.halfDiag
|
|
x2 = centRot.x + self.halfDiag
|
|
diag = None
|
|
if self.obj.CutPatternAngle == 0 or self.obj.CutPatternAngle == 180:
|
|
diag = self.deltaY
|
|
elif self.obj.CutPatternAngle == 90 or self.obj.CutPatternAngle == 270:
|
|
diag = self.deltaX
|
|
else:
|
|
perpDist = math.cos(cAng - math.radians(self.obj.CutPatternAngle)) * self.deltaC
|
|
diag = perpDist
|
|
y1 = centRot.y + diag
|
|
# y2 = y1
|
|
|
|
# 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:
|
|
PathLog.warning('PGG: No offset clearing area returned.')
|
|
cont = False
|
|
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.'''
|
|
PathLog.debug('_getFaceOffset()')
|
|
|
|
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(shape)) # Set working plane
|
|
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(self.module, 'Face selection is unavailable for Rotational scans. Ignoring selected faces.')
|
|
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
|
|
PathLog.warning(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
|
|
PathLog.warning(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)
|
|
|
|
# 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.')
|
|
|
|
# (FACES, VOIDS) = self._identifyFacesAndVoids(FACES, VOIDS)
|
|
(F, V) = self._identifyFacesAndVoids(FACES, VOIDS)
|
|
|
|
# Cycle through each base model, processing faces for each
|
|
for m in range(0, lenGRP):
|
|
base = GRP[m]
|
|
(mFS, mVS, mPS) = self._preProcessFacesAndVoids(base, m, FACES, VOIDS)
|
|
fShapes[m] = mFS
|
|
vShapes[m] = mVS
|
|
self.profileShapes[m] = mPS
|
|
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
|
|
# PathLog.debug(' -Pre-processing all models in Job.')
|
|
for m in range(0, lenGRP):
|
|
if fShapes[m] is False:
|
|
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:
|
|
PathLog.error(' -Failed to pre-process base as a whole.')
|
|
else:
|
|
(fcShp, prflShp) = pPEB
|
|
if fcShp is not False:
|
|
if fcShp is True:
|
|
PathLog.debug(' -fcShp is True.')
|
|
fShapes[m] = True
|
|
else:
|
|
fShapes[m] = [fcShp]
|
|
if prflShp is not False:
|
|
if fcShp is not False:
|
|
PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m]))
|
|
if vShapes[m] is not False:
|
|
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)
|
|
|
|
# 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))
|
|
else:
|
|
if V[m] is False:
|
|
V[m] = list()
|
|
V[m].append((shape, faceIdx))
|
|
return (F, V)
|
|
|
|
def _preProcessFacesAndVoids(self, base, m, FACES, VOIDS):
|
|
mFS = False
|
|
mVS = False
|
|
mPS = False
|
|
mIFS = list()
|
|
|
|
if FACES[m] is not False:
|
|
isHole = False
|
|
if self.obj.HandleMultipleFeatures == 'Collectively':
|
|
cont = True
|
|
fsL = list() # face shape list
|
|
ifL = list() # avoid shape list
|
|
outFCS = list()
|
|
|
|
# Get collective envelope slice of selected faces
|
|
for (fcshp, fcIdx) in FACES[m]:
|
|
fNum = fcIdx + 1
|
|
fsL.append(fcshp)
|
|
gFW = self._getFaceWires(base, fcshp, fcIdx)
|
|
if gFW is False:
|
|
PathLog.debug('Failed to get wires from Face{}'.format(fNum))
|
|
elif gFW[0] is False:
|
|
PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
|
|
else:
|
|
((otrFace, raised), intWires) = gFW
|
|
outFCS.append(otrFace)
|
|
if self.obj.InternalFeaturesCut is False:
|
|
if intWires is not False:
|
|
for (iFace, rsd) in intWires:
|
|
ifL.append(iFace)
|
|
|
|
PathLog.debug('Attempting to get cross-section of collective faces.')
|
|
if len(outFCS) == 0:
|
|
PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum))
|
|
cont = False
|
|
else:
|
|
cfsL = Part.makeCompound(outFCS)
|
|
|
|
# Handle profile edges request
|
|
if cont is True and self.profileEdges != 'None':
|
|
ofstVal = self._calculateOffsetValue(isHole)
|
|
psOfst = extractFaceOffset(cfsL, ofstVal, self.wpc)
|
|
if psOfst is not False:
|
|
mPS = [psOfst]
|
|
if self.profileEdges == 'Only':
|
|
mFS = True
|
|
cont = False
|
|
else:
|
|
PathLog.error(' -Failed to create profile geometry for selected faces.')
|
|
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 faceOfstShp is False:
|
|
PathLog.error(' -Failed to create offset face.')
|
|
cont = False
|
|
|
|
if cont:
|
|
lenIfL = len(ifL)
|
|
if self.obj.InternalFeaturesCut is False:
|
|
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)
|
|
# faceOfstShp = faceOfstShp.cut(intOfstShp)
|
|
|
|
mFS = [faceOfstShp]
|
|
# Eif
|
|
|
|
elif self.obj.HandleMultipleFeatures == 'Individually':
|
|
for (fcshp, fcIdx) in FACES[m]:
|
|
cont = True
|
|
ifL = list() # avoid shape list
|
|
fNum = fcIdx + 1
|
|
outerFace = False
|
|
|
|
gFW = self._getFaceWires(base, fcshp, fcIdx)
|
|
if gFW is False:
|
|
PathLog.debug('Failed to get wires from Face{}'.format(fNum))
|
|
cont = False
|
|
elif gFW[0] is False:
|
|
PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
|
|
cont = False
|
|
outerFace = False
|
|
else:
|
|
((otrFace, raised), intWires) = gFW
|
|
outerFace = otrFace
|
|
if self.obj.InternalFeaturesCut is False:
|
|
if intWires is not False:
|
|
for (iFace, rsd) in intWires:
|
|
ifL.append(iFace)
|
|
|
|
if outerFace is not False:
|
|
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 is not False:
|
|
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:
|
|
PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum))
|
|
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 VOIDS[m] is not False:
|
|
PathLog.debug('Processing avoid faces.')
|
|
cont = True
|
|
isHole = False
|
|
outFCS = list()
|
|
intFEAT = list()
|
|
|
|
for (fcshp, fcIdx) in VOIDS[m]:
|
|
fNum = fcIdx + 1
|
|
gFW = self._getFaceWires(base, fcshp, fcIdx)
|
|
if gFW is False:
|
|
PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum))
|
|
cont = False
|
|
else:
|
|
((otrFace, raised), intWires) = gFW
|
|
outFCS.append(otrFace)
|
|
if self.obj.AvoidLastX_InternalFeatures is False:
|
|
if intWires is not False:
|
|
for (iFace, rsd) in intWires:
|
|
intFEAT.append(iFace)
|
|
|
|
lenOtFcs = len(outFCS)
|
|
if lenOtFcs == 0:
|
|
cont = False
|
|
else:
|
|
if lenOtFcs == 1:
|
|
avoid = outFCS[0]
|
|
else:
|
|
avoid = Part.makeCompound(outFCS)
|
|
|
|
if self.showDebugObjects:
|
|
PathLog.debug('*** tmpAvoidArea')
|
|
P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope')
|
|
P.Shape = avoid
|
|
P.purgeTouched()
|
|
self.tempGroup.addObject(P)
|
|
|
|
if cont:
|
|
if self.showDebugObjects:
|
|
PathLog.debug('*** tmpVoidCompound')
|
|
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:
|
|
PathLog.error('Failed to create collective offset avoid face.')
|
|
cont = False
|
|
|
|
if cont:
|
|
avdShp = avdOfstShp
|
|
|
|
if self.obj.AvoidLastX_InternalFeatures is False 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:
|
|
PathLog.error('Failed to create collective offset avoid internal features.')
|
|
else:
|
|
avdShp = avdOfstShp.cut(ifOfstShp)
|
|
|
|
if mVS is False:
|
|
mVS = list()
|
|
mVS.append(avdShp)
|
|
|
|
|
|
return (mFS, mVS, mPS)
|
|
|
|
def _getFaceWires(self, base, fcshp, fcIdx):
|
|
outFace = False
|
|
INTFCS = list()
|
|
fNum = fcIdx + 1
|
|
warnFinDep = translate(self.module, 'Final Depth might need to be lower. Internal features detected in Face')
|
|
|
|
PathLog.debug('_getFaceWires() from Face{}'.format(fNum))
|
|
WIRES = self._extractWiresFromFace(base, fcshp)
|
|
if WIRES is False:
|
|
PathLog.error('Failed to extract wires from Face{}'.format(fNum))
|
|
return False
|
|
|
|
# Process remaining internal features, adding to FCS list
|
|
lenW = len(WIRES)
|
|
for w in range(0, lenW):
|
|
(wire, rsd) = WIRES[w]
|
|
PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd))
|
|
if wire.isClosed() is False:
|
|
PathLog.debug(' -wire is not closed.')
|
|
else:
|
|
slc = self._flattenWireToFace(wire)
|
|
if slc is False:
|
|
PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum))
|
|
else:
|
|
if w == 0:
|
|
outFace = (slc, rsd)
|
|
else:
|
|
# add to VOIDS so cutter avoids area.
|
|
PathLog.warning(warnFinDep + str(fNum) + '.')
|
|
INTFCS.append((slc, rsd))
|
|
if len(INTFCS) == 0:
|
|
return (outFace, False)
|
|
else:
|
|
return (outFace, INTFCS)
|
|
|
|
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:
|
|
PathLog.debug('getShapeSlice(baseEnv) failed')
|
|
csFaceShape = getCrossSection(baseEnv)
|
|
if csFaceShape is False:
|
|
PathLog.debug('getCrossSection(baseEnv) failed')
|
|
csFaceShape = getSliceFromEnvelope(baseEnv)
|
|
if csFaceShape is False:
|
|
PathLog.error('Failed to slice baseEnv shape.')
|
|
cont = False
|
|
|
|
if cont is True and self.profileEdges != 'None':
|
|
PathLog.debug(' -Attempting profile geometry for model base.')
|
|
ofstVal = self._calculateOffsetValue(isHole)
|
|
psOfst = extractFaceOffset(csFaceShape, ofstVal, self.wpc)
|
|
if psOfst is not False:
|
|
if self.profileEdges == 'Only':
|
|
return (True, psOfst)
|
|
prflShp = psOfst
|
|
else:
|
|
PathLog.error(' -Failed to create profile geometry.')
|
|
cont = False
|
|
|
|
if cont:
|
|
ofstVal = self._calculateOffsetValue(isHole)
|
|
faceOffsetShape = extractFaceOffset(csFaceShape, ofstVal, self.wpc)
|
|
if faceOffsetShape is False:
|
|
PathLog.error('extractFaceOffset() failed.')
|
|
else:
|
|
faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin))
|
|
return (faceOffsetShape, prflShp)
|
|
return False
|
|
|
|
def _extractWiresFromFace(self, base, fc):
|
|
'''_extractWiresFromFace(base, fc) ...
|
|
Attempts to return all closed wires within a parent face, including the outer most wire of the parent.
|
|
The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True).
|
|
'''
|
|
PathLog.debug('_extractWiresFromFace()')
|
|
|
|
WIRES = list()
|
|
lenWrs = len(fc.Wires)
|
|
PathLog.debug(' -Wire count: {}'.format(lenWrs))
|
|
|
|
def index0(tup):
|
|
return tup[0]
|
|
|
|
# Cycle through wires in face
|
|
for w in range(0, lenWrs):
|
|
PathLog.debug(' -Analyzing wire_{}'.format(w + 1))
|
|
wire = fc.Wires[w]
|
|
checkEdges = False
|
|
cont = True
|
|
|
|
# Check for closed edges (circles, ellipses, etc...)
|
|
for E in wire.Edges:
|
|
if E.isClosed() is True:
|
|
checkEdges = True
|
|
break
|
|
|
|
if checkEdges is True:
|
|
PathLog.debug(' -checkEdges is True')
|
|
for e in range(0, len(wire.Edges)):
|
|
edge = wire.Edges[e]
|
|
if edge.isClosed() is True and edge.Mass > 0.01:
|
|
PathLog.debug(' -Found closed edge')
|
|
raised = False
|
|
ip = self._isPocket(base, fc, edge)
|
|
if ip is False:
|
|
raised = True
|
|
ebb = edge.BoundBox
|
|
eArea = ebb.XLength * ebb.YLength
|
|
F = Part.Face(Part.Wire([edge]))
|
|
WIRES.append((eArea, F.Wires[0], raised))
|
|
cont = False
|
|
|
|
if cont:
|
|
PathLog.debug(' -cont is True')
|
|
# If only one wire and not checkEdges, return first wire
|
|
if lenWrs == 1:
|
|
return [(wire, False)]
|
|
|
|
raised = False
|
|
wbb = wire.BoundBox
|
|
wArea = wbb.XLength * wbb.YLength
|
|
if w > 0:
|
|
ip = self._isPocket(base, fc, wire)
|
|
if ip is False:
|
|
raised = True
|
|
WIRES.append((wArea, Part.Wire(wire.Edges), raised))
|
|
|
|
nf = len(WIRES)
|
|
if nf > 0:
|
|
PathLog.debug(' -number of wires found is {}'.format(nf))
|
|
if nf == 1:
|
|
(area, W, raised) = WIRES[0]
|
|
owLen = fc.OuterWire.Length
|
|
wLen = W.Length
|
|
if abs(owLen - wLen) > 0.0000001:
|
|
OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges))
|
|
return [(OW, False), (W, raised)]
|
|
else:
|
|
return [(W, raised)]
|
|
else:
|
|
sortedWIRES = sorted(WIRES, key=index0, reverse=True)
|
|
WRS = [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size
|
|
# Check if OuterWire is larger than largest in WRS list
|
|
(W, raised) = WRS[0]
|
|
owLen = fc.OuterWire.Length
|
|
wLen = W.Length
|
|
if abs(owLen - wLen) > 0.0000001:
|
|
OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges))
|
|
WRS.insert(0, (OW, False))
|
|
return WRS
|
|
|
|
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)
|
|
tolrnc = self.JOB.GeometryTolerance.Value
|
|
|
|
if isVoid is False:
|
|
if isHole is True:
|
|
offset = -1 * self.obj.InternalFeaturesAdjustment.Value
|
|
offset += self.radius + (tolrnc / 10.0)
|
|
else:
|
|
offset = -1 * self.obj.BoundaryAdjustment.Value
|
|
if self.obj.BoundaryEnforcement is True:
|
|
offset += self.radius + (tolrnc / 10.0)
|
|
else:
|
|
offset -= self.radius + (tolrnc / 10.0)
|
|
offset = 0.0 - offset
|
|
else:
|
|
offset = -1 * self.obj.BoundaryAdjustment.Value
|
|
offset += self.radius + (tolrnc / 10.0)
|
|
|
|
return offset
|
|
|
|
def _isPocket(self, b, f, w):
|
|
'''_isPocket(b, f, w)...
|
|
Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion.
|
|
Returns True if pocket, False if raised protrusion.'''
|
|
e = w.Edges[0]
|
|
for fi in range(0, len(b.Shape.Faces)):
|
|
face = b.Shape.Faces[fi]
|
|
for ei in range(0, len(face.Edges)):
|
|
edge = face.Edges[ei]
|
|
if e.isSame(edge) is True:
|
|
if f is face:
|
|
# Alternative: run loop to see if all edges are same
|
|
pass # same source face, look for another
|
|
else:
|
|
if face.CenterOfMass.z < f.CenterOfMass.z:
|
|
return True
|
|
return False
|
|
|
|
def _flattenWireToFace(self, wire):
|
|
PathLog.debug('_flattenWireToFace()')
|
|
if wire.isClosed() is False:
|
|
PathLog.debug(' -wire.isClosed() is False')
|
|
return False
|
|
|
|
# If wire is planar horizontal, convert to a face and return
|
|
if wire.BoundBox.ZLength == 0.0:
|
|
slc = Part.Face(wire)
|
|
return slc
|
|
|
|
# Attempt to create a new wire for manipulation, if not, use original
|
|
newWire = Part.Wire(wire.Edges)
|
|
if newWire.isClosed() is True:
|
|
nWire = newWire
|
|
else:
|
|
PathLog.debug(' -newWire.isClosed() is False')
|
|
nWire = wire
|
|
|
|
# Attempt extrusion, and then try a manual slice and then cross-section
|
|
ext = getExtrudedShape(nWire)
|
|
if ext is False:
|
|
PathLog.debug('getExtrudedShape() failed')
|
|
else:
|
|
slc = getShapeSlice(ext)
|
|
if slc is not False:
|
|
return slc
|
|
cs = getCrossSection(ext, True)
|
|
if cs is not False:
|
|
return cs
|
|
|
|
# Attempt creating an envelope, and then try a manual slice and then cross-section
|
|
env = getShapeEnvelope(nWire)
|
|
if env is False:
|
|
PathLog.debug('getShapeEnvelope() failed')
|
|
else:
|
|
slc = getShapeSlice(env)
|
|
if slc is not False:
|
|
return slc
|
|
cs = getCrossSection(env, True)
|
|
if cs is not False:
|
|
return cs
|
|
|
|
# Attempt creating a projection
|
|
slc = getProjectedFace(self.tempGroup, nWire)
|
|
if slc is False:
|
|
PathLog.debug('getProjectedFace() failed')
|
|
else:
|
|
return slc
|
|
|
|
return False
|
|
# 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
|
|
|
|
# PathLog.debug(' -slcArea !< midArea')
|
|
# PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges)))
|
|
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:
|
|
# PathLog.debug(' -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, withExtrude=False):
|
|
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
|
|
if withExtrude is True:
|
|
ext = getExtrudedShape(csWire)
|
|
CS = getShapeSlice(ext)
|
|
if CS is False:
|
|
return False
|
|
else:
|
|
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:
|
|
PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee))
|
|
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
|
|
|
|
|
|
# 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
|
|
else:
|
|
# PathLog.debug('---- Gap: {} mm'.format(gap))
|
|
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.')
|
|
else:
|
|
PathLog.debug('Line count is even.')
|
|
|
|
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)
|
|
|
|
if cutClimb is True:
|
|
dirFlg = -1
|
|
else:
|
|
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 = sp.isOnLineSegment(ep, cp)
|
|
iC = cp.isOnLineSegment(sp, ep)
|
|
if iC is True:
|
|
inLine.append('BRK')
|
|
chkGap = True
|
|
gap = abs(toolDiam - lst.sub(cp).Length)
|
|
else:
|
|
chkGap = False
|
|
if dirFlg == -1:
|
|
inLine.reverse()
|
|
# LINES.append((dirFlg, inLine))
|
|
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 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
|
|
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.')
|
|
else:
|
|
PathLog.debug('Line count is ODD.')
|
|
dirFlg = -1 * dirFlg
|
|
if obj.CutPatternReversed is False:
|
|
if cutClimb is True:
|
|
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((dirFlg, rev))
|
|
LINES.append(rev)
|
|
else:
|
|
# LINES.append((dirFlg, inLine))
|
|
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 is not False:
|
|
(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:
|
|
# PathLog.debug('---- Gap: {} mm'.format(gap))
|
|
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)
|
|
lst = p2
|
|
|
|
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] |