From d4fafcc34c92fa2d2914ac60024b28492e7f592a Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 28 Apr 2020 23:40:54 -0500 Subject: [PATCH] Path: new FindUnifiedRegions class Improve `HandleMultipleFeatures` processing when set to `Collectively` by implementing new class to refine the processing area, attempting to remove common edges between connected face regions. --- .../Path/PathScripts/PathSurfaceSupport.py | 4017 +++++++++-------- 1 file changed, 2162 insertions(+), 1855 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathSurfaceSupport.py b/src/Mod/Path/PathScripts/PathSurfaceSupport.py index 0e0ca7cdfc..ea0dc5a2d2 100644 --- a/src/Mod/Path/PathScripts/PathSurfaceSupport.py +++ b/src/Mod/Path/PathScripts/PathSurfaceSupport.py @@ -1,1855 +1,2162 @@ -# -*- 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] \ No newline at end of file +# -*- 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." +__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) + proceed = False + + # Crete place holders for each base model in Job + for m in range(0, lenGRP): + FACES.append(False) + VOIDS.append(False) + fShapes.append(False) + vShapes.append(False) + + # The user has selected subobjects from the base. Pre-Process each. + if self.checkBase: + PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') + + (hasFace, hasVoid) = self._identifyFacesAndVoids(FACES, VOIDS) # modifies FACES and VOIDS + hasGeometry = True if hasFace or hasVoid else False + + # Cycle through each base model, processing faces for each + for m in range(0, lenGRP): + base = GRP[m] + (mFS, mVS, mPS) = self._preProcessFacesAndVoids(base, FACES[m], VOIDS[m]) + fShapes[m] = mFS + vShapes[m] = mVS + self.profileShapes[m] = mPS + if mFS or mVS: + proceed = True + if hasGeometry and not proceed: + return False + else: + PathLog.debug(' -No obj.Base data.') + for m in range(0, lenGRP): + self.modelSTLs[m] = True + + # Process each model base, as a whole, as needed + # 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) + hasFace = False + hasVoid = False + + # Separate selected faces into (base, face) tuples and flag model(s) for STL creation + for (bs, SBS) in self.obj.Base: + for sb in SBS: + # Flag model for STL creation + mdlIdx = None + for m in range(0, lenGRP): + if bs is GRP[m]: + self.modelSTLs[m] = True + mdlIdx = m + break + TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) + + # Apply `AvoidXFaces` value + faceCnt = len(TUPS) + add = faceCnt - self.obj.AvoidLastX_Faces + for bst in range(0, faceCnt): + (m, base, sub) = TUPS[bst] + shape = getattr(base.Shape, sub) + if isinstance(shape, Part.Face): + faceIdx = int(sub[4:]) - 1 + if bst < add: + if F[m] is False: + F[m] = list() + F[m].append((shape, faceIdx)) + hasFace = True + else: + if V[m] is False: + V[m] = list() + V[m].append((shape, faceIdx)) + hasVoid = True + return (hasFace, hasVoid) + + def _preProcessFacesAndVoids(self, base, FCS, VDS): + mFS = False + mVS = False + mPS = False + mIFS = list() + + if FCS: + isHole = False + if self.obj.HandleMultipleFeatures == 'Collectively': + cont = True + fsL = list() # face shape list + ifL = list() # avoid shape list + outFCS = list() + + # Use new face-unifying class + FUR = FindUnifiedRegions(FCS, self.JOB.GeometryTolerance.Value) + if self.showDebugObjects: + FUR.setTempGroup(self.tempGroup) + outFCS = FUR.getUnifiedRegions() + if not self.obj.InternalFeaturesCut: + ifL.extend(FUR.getInternalFeatures()) + + PathLog.debug('Attempting to get cross-section of collective faces.') + if len(outFCS) == 0: + # FreeCAD.Console.PrintError(translate('PathSurfaceSupport', 'Cannot process selected faces. Check horizontal surface exposure.\n')) + 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 FCS: + cont = True + ifL = list() # avoid shape list + fNum = fcIdx + 1 + outerFace = False + + # Use new face-unifying class + FUR = FindUnifiedRegions([(fcshp, fcIdx)], self.JOB.GeometryTolerance.Value) + if self.showDebugObjects: + FUR.setTempGroup(self.tempGroup) + outerFace = FUR.getUnifiedRegions()[0] + if not self.obj.InternalFeaturesCut: + ifL = FUR.getInternalFeatures() + + 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 VDS is not False: + PathLog.debug('Processing avoid faces.') + cont = True + isHole = False + outFCS = list() + intFEAT = list() + + for (fcshp, fcIdx) in VDS: + fNum = fcIdx + 1 + + # Use new face-unifying class + FUR = FindUnifiedRegions([(fcshp, fcIdx)], self.JOB.GeometryTolerance.Value) + if self.showDebugObjects: + FUR.setTempGroup(self.tempGroup) + outFCS.extend(FUR.getUnifiedRegions()) + if not self.obj.InternalFeaturesCut: + intFEAT.extend(FUR.getInternalFeatures()) + + lenOtFcs = len(outFCS) + if lenOtFcs == 0: + cont = False + else: + if lenOtFcs == 1: + avoid = outFCS[0] + else: + avoid = Part.makeCompound(outFCS) + + if self.showDebugObjects: + 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 _preProcessEntireBase(self, base, m): + cont = True + isHole = False + prflShp = False + # Create envelope, extract cross-section and make offset co-planar shape + # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) + + try: + baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as ee: + PathLog.error(str(ee)) + shell = base.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as eee: + PathLog.error(str(eee)) + cont = False + + if cont: + csFaceShape = getShapeSlice(baseEnv) + if csFaceShape is False: + csFaceShape = getCrossSection(baseEnv) + if csFaceShape is False: + csFaceShape = getSliceFromEnvelope(baseEnv) + if csFaceShape is False: + PathLog.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 _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 + +# Eclass + + +# Functions for getting a shape envelope and cross-section +def getExtrudedShape(wire): + PathLog.debug('getExtrudedShape()') + wBB = wire.BoundBox + extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 + + try: + shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) + except Exception as ee: + PathLog.error(' -extrude wire failed: \n{}'.format(ee)) + return False + + SHP = Part.makeSolid(shell) + return SHP + +def getShapeSlice(shape): + PathLog.debug('getShapeSlice()') + + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + xmin = bb.XMin - 1.0 + xmax = bb.XMax + 1.0 + ymin = bb.YMin - 1.0 + ymax = bb.YMax + 1.0 + p1 = FreeCAD.Vector(xmin, ymin, mid) + p2 = FreeCAD.Vector(xmax, ymin, mid) + p3 = FreeCAD.Vector(xmax, ymax, mid) + p4 = FreeCAD.Vector(xmin, ymax, mid) + + e1 = Part.makeLine(p1, p2) + e2 = Part.makeLine(p2, p3) + e3 = Part.makeLine(p3, p4) + e4 = Part.makeLine(p4, p1) + face = Part.Face(Part.Wire([e1, e2, e3, e4])) + fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area + sArea = shape.BoundBox.XLength * shape.BoundBox.YLength + midArea = (fArea + sArea) / 2.0 + + slcShp = shape.common(face) + slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength + + if slcArea < midArea: + for W in slcShp.Wires: + if W.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + if len(slcShp.Wires) == 1: + wire = slcShp.Wires[0] + slc = Part.Face(wire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + else: + fL = list() + for W in slcShp.Wires: + slc = Part.Face(W) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + fL.append(slc) + comp = Part.makeCompound(fL) + return comp + + return False + +def getProjectedFace(tempGroup, wire): + import Draft + PathLog.debug('getProjectedFace()') + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') + F.Shape = wire + F.purgeTouched() + tempGroup.addObject(F) + try: + prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) + prj.recompute() + prj.purgeTouched() + tempGroup.addObject(prj) + except Exception as ee: + PathLog.error(str(ee)) + return False + else: + pWire = Part.Wire(prj.Shape.Edges) + if pWire.isClosed() is False: + # 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): + PathLog.debug('getCrossSection()') + wires = list() + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + + for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): + wires.append(i) + + if len(wires) > 0: + comp = Part.Compound(wires) # produces correct cross-section wire ! + comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) + csWire = comp.Wires[0] + if csWire.isClosed() is False: + PathLog.debug(' -comp.Wires[0] is not closed') + return False + CS = Part.Face(csWire) + CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) + return CS + else: + PathLog.debug(' -No wires from .slice() method') + + return False + +def getShapeEnvelope(shape): + PathLog.debug('getShapeEnvelope()') + + wBB = shape.BoundBox + extFwd = wBB.ZLength + 10.0 + minz = wBB.ZMin + maxz = wBB.ZMin + extFwd + stpDwn = (maxz - minz) / 4.0 + dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) + + try: + env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape + except Exception as ee: + 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] + + +class FindUnifiedRegions: + '''FindUnifiedRegions() This class requires a list of face shapes. + It finds the unified horizontal unified regions, if they exist.''' + + def __init__(self, facesList, geomToler): + self.FACES = facesList # format is tuple (faceShape, faceIndex_on_base) + self.geomToler = geomToler + self.tempGroup = None + self.topFaces = list() + self.edgeData = list() + self.circleData = list() + self.noSharedEdges = True + self.topWires = list() + self.REGIONS = list() + self.INTERNALS = False + self.idGroups = list() + self.sharedEdgeIdxs = list() + self.fusedFaces = None + + if self.geomToler == 0.0: + self.geomToler = 0.00001 + + # Internal processing methods + def _showShape(self, shape, name): + if self.tempGroup: + S = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp' + name) + S.Shape = shape + S.purgeTouched() + self.tempGroup.addObject(S) + + def _extractTopFaces(self): + for (F, fcIdx) in self.FACES: # format is tuple (faceShape, faceIndex_on_base) + cont = True + fNum = fcIdx + 1 + # Extrude face + fBB = F.BoundBox + extFwd = math.floor(2.0 * fBB.ZLength) + 10.0 + ef = F.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) + ef = Part.makeSolid(ef) + + # Cut top off of extrusion with Part.box + efBB = ef.BoundBox + ZLen = efBB.ZLength / 2.0 + cutBox = Part.makeBox(efBB.XLength + 2.0, efBB.YLength + 2.0, ZLen) + zHght = efBB.ZMin + ZLen + cutBox.translate(FreeCAD.Vector(efBB.XMin - 1.0, efBB.YMin - 1.0, zHght)) + base = ef.cut(cutBox) + + # Identify top face of base + fIdx = 0 + zMin = base.Faces[fIdx].BoundBox.ZMin + for bfi in range(0, len(base.Faces)): + fzmin = base.Faces[bfi].BoundBox.ZMin + if fzmin > zMin: + fIdx = bfi + zMin = fzmin + + # Translate top face to Z=0.0 and save to topFaces list + topFace = base.Faces[fIdx] + # self._showShape(topFace, 'topFace_{}'.format(fNum)) + tfBB = topFace.BoundBox + tfBB_Area = tfBB.XLength * tfBB.YLength + fBB_Area = fBB.XLength * fBB.YLength + if tfBB_Area < (fBB_Area * 0.9): + # attempt alternate methods + topFace = self._getCompleteCrossSection(ef) + tfBB = topFace.BoundBox + tfBB_Area = tfBB.XLength * tfBB.YLength + # self._showShape(topFace, 'topFaceAlt_1_{}'.format(fNum)) + if tfBB_Area < (fBB_Area * 0.9): + topFace = getShapeSlice(ef) + tfBB = topFace.BoundBox + tfBB_Area = tfBB.XLength * tfBB.YLength + # self._showShape(topFace, 'topFaceAlt_2_{}'.format(fNum)) + if tfBB_Area < (fBB_Area * 0.9): + FreeCAD.Console.PrintError('Faild to extract processing region for Face{}.\n'.format(fNum)) + cont = False + + if cont: + topFace.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - zMin)) + self.topFaces.append((topFace, fcIdx)) + + def _fuseTopFaces(self): + (one, baseFcIdx) = self.topFaces.pop(0) + base = one + for (face, fcIdx) in self.topFaces: + base = base.fuse(face) + self.topFaces.insert(0, (one, baseFcIdx)) + self.fusedFaces = base + + def _getEdgesData(self): + topFaces = self.fusedFaces.Faces + tfLen = len(topFaces) + count = [0, 0] + + # Get length and center of mass for each edge in all top faces + for fi in range(0, tfLen): + F = topFaces[fi] + edgCnt = len(F.Edges) + for ei in range(0, edgCnt): + E = F.Edges[ei] + tup = (E.Length, E.CenterOfMass, E, fi) + if len(E.Vertexes) == 1: + self.circleData.append(tup) + count[0] += 1 + else: + self.edgeData.append(tup) + count[1] += 1 + + def _groupEdgesByLength(self): + cont = True + threshold = self.geomToler + grp = list() + processLast = False + + def keyFirst(tup): + return tup[0] + + # Sort edgeData data and prepare proxy indexes + self.edgeData.sort(key=keyFirst) + DATA = self.edgeData + lenDATA = len(DATA) + indexes = [i for i in range(0, lenDATA)] + idxCnt = len(indexes) + # FreeCAD.Console.PrintWarning('indexes:\n{}\n'.format(indexes)) + + while idxCnt > 0: + processLast = True + # Pop off index for first edge + actvIdx = indexes.pop(0) + actvItem = DATA[actvIdx][0] # 0 index is length + grp.append(actvIdx) + idxCnt -= 1 + noMatch = True + + while idxCnt > 0: + tstIdx = indexes[0] + tstItem = DATA[tstIdx][0] + + # test case(s) goes here + absLenDiff = abs(tstItem - actvItem) + if absLenDiff < threshold: + # Remove test index from indexes + indexes.pop(0) + idxCnt -= 1 + grp.append(tstIdx) + noMatch = False + else: + if len(grp) > 1: + # grp.sort() + self.idGroups.append(grp) + grp = list() + break + # Ewhile + # Ewhile + if processLast: + if len(grp) > 1: + # grp.sort() + self.idGroups.append(grp) + + def _identifySharedEdgesByLength(self, grp): + holds = list() + cont = True + specialIndexes = [] + threshold = self.geomToler + + def keyFirst(tup): + return tup[0] + + # Sort edgeData data + self.edgeData.sort(key=keyFirst) + DATA = self.edgeData + lenDATA = len(DATA) + lenGrp = len(grp) + + while lenGrp > 0: + # Pop off index for first edge + actvIdx = grp.pop(0) + actvItem = DATA[actvIdx][0] # 0 index is length + lenGrp -= 1 + while lenGrp > 0: + isTrue = False + # Pop off index for test edge + tstIdx = grp.pop(0) + tstItem = DATA[tstIdx][0] + lenGrp -= 1 + + # test case(s) goes here + lenDiff = tstItem - actvItem + absLenDiff = abs(lenDiff) + if lenDiff > threshold: + break + if absLenDiff < threshold: + com1 = DATA[actvIdx][1] + com2 = DATA[tstIdx][1] + comDiff = com2.sub(com1).Length + if comDiff < threshold: + isTrue = True + + # Action if test is true (finds special case) + if isTrue: + specialIndexes.append(actvIdx) + specialIndexes.append(tstIdx) + break + else: + holds.append(tstIdx) + + # Put hold indexes back in search group + holds.extend(grp) + grp = holds + lenGrp = len(grp) + holds = list() + + if len(specialIndexes) > 0: + # Remove shared edges from EDGES data + uniqueShared = list(set(specialIndexes)) + self.sharedEdgeIdxs.extend(uniqueShared) + self.noSharedEdges = False + + def _extractWiresFromEdges(self): + DATA = self.edgeData + holds = list() + lastEdge = None + lastIdx = None + firstEdge = None + isWire = False + cont = True + connectedEdges = [] + connectedIndexes = [] + connectedCnt = 0 + LOOPS = list() + + def faceIndex(tup): + return tup[3] + + def faceArea(face): + return face.Area + + # Sort by face index on original model base + DATA.sort(key=faceIndex) + lenDATA = len(DATA) + indexes = [i for i in range(0, lenDATA)] + idxCnt = len(indexes) + + # Add circle edges into REGIONS list + if len(self.circleData) > 0: + for C in self.circleData: + face = Part.Face(Part.Wire(C[2])) + self.REGIONS.append(face) + + actvIdx = indexes.pop(0) + actvEdge = DATA[actvIdx][2] + firstEdge = actvEdge # DATA[connectedIndexes[0]][2] + idxCnt -= 1 + connectedIndexes.append(actvIdx) + connectedEdges.append(actvEdge) + connectedCnt = 1 + + safety = 750 + while cont: # safety > 0 + safety -= 1 + notConnected = True + while idxCnt > 0: + isTrue = False + # Pop off index for test edge + tstIdx = indexes.pop(0) + tstEdge = DATA[tstIdx][2] + idxCnt -= 1 + if self._edgesAreConnected(actvEdge, tstEdge): + isTrue = True + + if isTrue: + notConnected = False + connectedIndexes.append(tstIdx) + connectedEdges.append(tstEdge) + connectedCnt += 1 + actvIdx = tstIdx + actvEdge = tstEdge + break + else: + holds.append(tstIdx) + # Ewhile + + if connectedCnt > 2: + if self._edgesAreConnected(actvEdge, firstEdge): + notConnected = False + # Save loop components + LOOPS.append(connectedEdges) + # reset connected variables and re-assess + connectedEdges = [] + connectedIndexes = [] + connectedCnt = 0 + indexes.sort() + idxCnt = len(indexes) + if idxCnt > 0: + # Pop off index for first edge + actvIdx = indexes.pop(0) + actvEdge = DATA[actvIdx][2] + idxCnt -= 1 + firstEdge = actvEdge + connectedIndexes.append(actvIdx) + connectedEdges.append(actvEdge) + connectedCnt = 1 + # Eif + + # Put holds indexes back in search stack + if notConnected: + holds.append(actvIdx) + if idxCnt == 0: + lastLoop = True + holds.extend(indexes) + indexes = holds + idxCnt = len(indexes) + holds = list() + if idxCnt == 0: + cont = False + # Ewhile + + if len(LOOPS) > 0: + FACES = list() + for Edges in LOOPS: + wire = Part.Wire(Part.__sortEdges__(Edges)) + if wire.isClosed(): + face = Part.Face(wire) + self.REGIONS.append(face) + self.REGIONS.sort(key=faceArea, reverse=True) + + def _identifyInternalFeatures(self): + remList = list() + + for (top, fcIdx) in self.topFaces: + big = Part.Face(top.OuterWire) + for s in range(0, len(self.REGIONS)): + if s not in remList: + small = self.REGIONS[s] + if self._isInBoundBox(big, small): + cmn = big.common(small) + if cmn.Area > 0.0: + self.INTERNALS.append(small) + remList.append(s) + break + else: + FreeCAD.Console.PrintWarning(' - No common area.\n') + + remList.sort(reverse=True) + for ri in remList: + self.REGIONS.pop(ri) + + def _processNestedRegions(self): + cont = True + hold = list() + Ids = list() + remList = list() + for i in range(0, len(self.REGIONS)): + Ids.append(i) + idsCnt = len(Ids) + # FreeCAD.Console.PrintWarning('_processNestedRegions() Ids: {}\n'.format(Ids)) + + while cont: + while idsCnt > 0: + hi = Ids.pop(0) + high = self.REGIONS[hi] + idsCnt -= 1 + while idsCnt > 0: + isTrue = False + li = Ids.pop(0) + idsCnt -= 1 + low = self.REGIONS[li] + # Test case here + if self._isInBoundBox(high, low): + cmn = high.common(low) + if cmn.Area > 0.0: + isTrue = True + # if True action here + if isTrue: + self.REGIONS[hi] = high.cut(low) + # self.INTERNALS.append(low) + remList.append(li) + else: + hold.append(hi) + # Ewhile + hold.extend(Ids) + Ids = hold + hold = list() + if len(Ids) == 0: + cont = False + # Ewhile + # Ewhile + remList.sort(reverse=True) + for ri in remList: + self.REGIONS.pop(ri) + + # Accessory methods + def _getCompleteCrossSection(self, shape): + PathLog.debug('_getCompleteCrossSection()') + wires = list() + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + + for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): + wires.append(i) + + if len(wires) > 0: + comp = Part.Compound(wires) # produces correct cross-section wire ! + CS = Part.Face(comp.Wires[0]) + CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) + return CS + + PathLog.debug(' -No wires from .slice() method') + return False + + def _edgesAreConnected(self, e1, e2): + # Assumes edges are flat and are at Z=0.0 + + def isSameVertex(v1, v2): + # Assumes vertexes at Z=0.0 + if abs(v1.X - v2.X) < 0.000001: + if abs(v1.Y - v2.Y) < 0.000001: + return True + return False + + if isSameVertex(e1.Vertexes[0], e2.Vertexes[0]): + return True + if isSameVertex(e1.Vertexes[0], e2.Vertexes[1]): + return True + if isSameVertex(e1.Vertexes[1], e2.Vertexes[0]): + return True + if isSameVertex(e1.Vertexes[1], e2.Vertexes[1]): + return True + + return False + + def _isInBoundBox(self, outShp, inShp): + obb = outShp.BoundBox + ibb = inShp.BoundBox + + if obb.XMin < ibb.XMin: + if obb.XMax > ibb.XMax: + if obb.YMin < ibb.YMin: + if obb.YMax > ibb.YMax: + return True + return False + + # Public methods + def setTempGroup(self, grpObj): + '''setTempGroup(grpObj)... For debugging, pass temporary object group.''' + self.tempGroup = grpObj + + def getUnifiedRegions(self): + '''getUnifiedRegions()... Returns a list of unified regions from list + of tuples (faceShape, faceIndex) received at instantiation of the class object.''' + self.INTERNALS = list() + if len(self.FACES) == 0: + FreeCAD.Console.PrintError('No (faceShp, faceIdx) tuples received at instantiation of class.') + return [] + + self._extractTopFaces() + lenFaces = len(self.topFaces) + if lenFaces == 0: + return [] + + # if single topFace, return it + if lenFaces == 1: + topFace = self.topFaces[0][0] + # self._showShape(topFace, 'TopFace') + # prepare inner wires as faces for internal features + lenWrs = len(topFace.Wires) + if lenWrs > 1: + for w in range(1, lenWrs): + self.INTERNALS.append(Part.Face(topFace.Wires[w])) + # prepare outer wire as face for return value in list + if hasattr(topFace, 'OuterWire'): + ow = topFace.OuterWire + else: + ow = topFace.Wires[0] + face = Part.Face(ow) + return [face] + + # process multiple top faces, unifying if possible + self._fuseTopFaces() + # for F in self.fusedFaces.Faces: + # self._showShape(F, 'TopFaceFused') + + self._getEdgesData() + self._groupEdgesByLength() + for grp in self.idGroups: + self._identifySharedEdgesByLength(grp) + + if self.noSharedEdges: + PathLog.debug('No shared edges by length detected.\n') + return [topFace for (topFace, fcIdx) in self.topFaces] + else: + # Delete shared edges from edgeData list + # FreeCAD.Console.PrintWarning('self.sharedEdgeIdxs: {}\n'.format(self.sharedEdgeIdxs)) + self.sharedEdgeIdxs.sort(reverse=True) + for se in self.sharedEdgeIdxs: + # seShp = self.edgeData[se][2] + # self._showShape(seShp, 'SharedEdge') + self.edgeData.pop(se) + + self._extractWiresFromEdges() + self._identifyInternalFeatures() + self._processNestedRegions() + for ri in range(0, len(self.REGIONS)): + self._showShape(self.REGIONS[ri], 'UnifiedRegion_{}'.format(ri)) + + return self.REGIONS + + def getInternalFeatures(self): + '''getInternalFeatures()... Returns internal features identified + after calling getUnifiedRegions().''' + if self.INTERNALS: + return self.INTERNALS + FreeCAD.Console.PrintError('getUnifiedRegions() must be called before getInternalFeatures().') + return False +# Eclass \ No newline at end of file