# -*- coding: utf-8 -*- # *************************************************************************** # * * # * Copyright (c) 2020 russ4262 * # * * # * 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. First, 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]