Download src/Mod/CAM/Path/Op/SurfaceSupport.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 101 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/SurfaceSupport.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/CAM/Path/Op/SurfaceSupport.py
-
curl -L -o SurfaceSupport.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/SurfaceSupport.py
101 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| # *************************************************************************** | |
| # * Copyright (c) 2020 russ4262 <russ4262@gmail.com> * | |
| # * * | |
| # * This program is free software; you can redistribute it and/or modify * | |
| # * it under the terms of the GNU Lesser General Public License (LGPL) * | |
| # * as published by the Free Software Foundation; either version 2 of * | |
| # * the License, or (at your option) any later version. * | |
| # * for detail see the LICENCE text file. * | |
| # * * | |
| # * This program is distributed in the hope that it will be useful, * | |
| # * but WITHOUT ANY WARRANTY; without even the implied warranty of * | |
| # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * | |
| # * GNU Library General Public License for more details. * | |
| # * * | |
| # * You should have received a copy of the GNU Library General Public * | |
| # * License along with this program; if not, write to the Free Software * | |
| # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * | |
| # * USA * | |
| # * * | |
| # *************************************************************************** | |
| __title__ = "CAM Surface Support Module" | |
| __author__ = "russ4262 (Russell Johnson)" | |
| __url__ = "https://www.freecad.org" | |
| __doc__ = "Support functions and classes for 3D Surface and Waterline operations." | |
| __contributors__ = "" | |
| import FreeCAD | |
| import Path | |
| import Path.Op.Util as PathOpUtil | |
| import PathScripts.PathUtils as PathUtils | |
| import math | |
| # lazily loaded modules | |
| from lazy_loader.lazy_loader import LazyLoader | |
| MeshPart = LazyLoader("MeshPart", globals(), "MeshPart") # tessellate bug Workaround | |
| Part = LazyLoader("Part", globals(), "Part") | |
| if False: | |
| Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) | |
| Path.Log.trackModule(Path.Log.thisModule()) | |
| else: | |
| Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) | |
| translate = FreeCAD.Qt.translate | |
| 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 > 1.0e-8: | |
| msg = translate("PathSurfaceSupport", "Shape appears to not be horizontal planar.") | |
| msg += " ZMax == {} mm.\n".format(shape.BoundBox.ZMax) | |
| FreeCAD.Console.PrintWarning(msg) | |
| else: | |
| self.shape = shape | |
| self._prepareConstants() | |
| def _prepareConstants(self): | |
| # Compute weighted center of mass of all faces combined | |
| if self.pattern in ["Circular", "CircularZigZag", "Spiral"]: | |
| if self.obj.PatternCenterAt == "CenterOfMass": | |
| fCnt = 0 | |
| totArea = 0.0 | |
| zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) | |
| for F in self.shape.Faces: | |
| comF = F.CenterOfMass | |
| areaF = F.Area | |
| totArea += areaF | |
| fCnt += 1 | |
| zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) | |
| if fCnt == 0: | |
| msg = translate("PathSurfaceSupport", "Cannot calculate the Center Of Mass.") | |
| msg += ( | |
| " " | |
| + translate("PathSurfaceSupport", "Using Center of Boundbox instead.") | |
| + "\n" | |
| ) | |
| FreeCAD.Console.PrintError(msg) | |
| bbC = self.shape.BoundBox.Center | |
| zeroCOM = FreeCAD.Vector(bbC.x, bbC.y, 0.0) | |
| else: | |
| avgArea = totArea / fCnt | |
| zeroCOM.multiply(1 / fCnt) | |
| zeroCOM.multiply(1 / avgArea) | |
| self.centerOfMass = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) | |
| self.centerOfPattern = self._getPatternCenter() | |
| else: | |
| bbC = self.shape.BoundBox.Center | |
| self.centerOfPattern = FreeCAD.Vector(bbC.x, bbC.y, 0.0) | |
| # get X, Y, Z spans; Compute center of rotation | |
| self.deltaX = self.shape.BoundBox.XLength | |
| self.deltaY = self.shape.BoundBox.YLength | |
| self.deltaC = ( | |
| self.shape.BoundBox.DiagonalLength | |
| ) # math.sqrt(self.deltaX**2 + self.deltaY**2) | |
| lineLen = self.deltaC + ( | |
| 2.0 * self.toolDiam | |
| ) # Line length to span boundbox diag with 2x cutter diameter extra on each end | |
| self.halfDiag = math.ceil(lineLen / 2.0) | |
| cutPasses = ( | |
| math.ceil(lineLen / self.cutOut) + 1 | |
| ) # Number of lines(passes) required to cover boundbox diagonal | |
| self.halfPasses = math.ceil(cutPasses / 2.0) | |
| # Public methods | |
| def setDebugObjectsGroup(self, tmpGrpObject): | |
| """setDebugObjectsGroup(tmpGrpObject)... | |
| Pass the temporary object group to show temporary construction objects""" | |
| self.debugObjectsGroup = tmpGrpObject | |
| def getCenterOfPattern(self): | |
| """getCenterOfPattern()... | |
| Returns the Center Of Mass for the current class instance.""" | |
| return self.centerOfPattern | |
| def generatePathGeometry(self): | |
| """generatePathGeometry()... | |
| Call this function to obtain the path geometry shape, generated by this class.""" | |
| if self.pattern == "None": | |
| return False | |
| if self.shape is None: | |
| return False | |
| cmd = "self._" + self.pattern + "()" | |
| exec(cmd) | |
| if self.obj.CutPatternReversed is True: | |
| self.rawGeoList.reverse() | |
| # Create compound object to bind all lines in Lineset | |
| geomShape = Part.makeCompound(self.rawGeoList) | |
| # Position and rotate the Line and ZigZag geometry | |
| if self.pattern in ["Line", "ZigZag"]: | |
| if self.obj.CutPatternAngle != 0.0: | |
| geomShape.Placement.Rotation = FreeCAD.Rotation( | |
| FreeCAD.Vector(0, 0, 1), self.obj.CutPatternAngle | |
| ) | |
| bbC = self.shape.BoundBox.Center | |
| geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) | |
| if self.debugObjectsGroup: | |
| F = FreeCAD.ActiveDocument.addObject("Part::Feature", "tmpGeometrySet") | |
| F.Shape = geomShape | |
| F.purgeTouched() | |
| self.debugObjectsGroup.addObject(F) | |
| if self.pattern == "Offset": | |
| return geomShape | |
| # Identify intersection of cross-section face and lineset | |
| cmnShape = self.shape.common(geomShape) | |
| if self.debugObjectsGroup: | |
| F = FreeCAD.ActiveDocument.addObject("Part::Feature", "tmpPathGeometry") | |
| F.Shape = cmnShape | |
| F.purgeTouched() | |
| self.debugObjectsGroup.addObject(F) | |
| return cmnShape | |
| # Cut pattern methods | |
| def _Circular(self): | |
| GeoSet = [] | |
| 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 | |
| Path.Log.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 = [] | |
| centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model | |
| # Create end points for set of lines to intersect with cross-section face | |
| pntTuples = [] | |
| 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 = [] | |
| SEGS = [] | |
| 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): | |
| Path.Log.debug("_extractOffsetFaces()") | |
| wires = [] | |
| shape = self.shape | |
| offset = 0.0 # Start right at the edge of cut area | |
| direction = 0 | |
| loop_cnt = 0 | |
| def _get_direction(w): | |
| if PathOpUtil._isWireClockwise(w): | |
| return 1 | |
| return -1 | |
| def _reverse_wire(w): | |
| rev_list = [] | |
| for e in w.Edges: | |
| rev_list.append(PathUtils.reverseEdge(e)) | |
| rev_list.reverse() | |
| # return Part.Wire(Part.__sortEdges__(rev_list)) | |
| return Part.Wire(rev_list) | |
| while True: | |
| offsetArea = PathUtils.getOffsetArea(shape, offset, plane=self.wpc) | |
| if not offsetArea: | |
| # Area fully consumed | |
| break | |
| # set initial cut direction | |
| if direction == 0: | |
| first_face_wire = offsetArea.Faces[0].Wires[0] | |
| direction = _get_direction(first_face_wire) | |
| if self.obj.CutMode == "Climb": | |
| if direction == 1: | |
| direction = -1 | |
| else: | |
| if direction == -1: | |
| direction = 1 | |
| # Correct cut direction for `Conventional` cuts | |
| if self.obj.CutMode == "Conventional": | |
| if loop_cnt == 1: | |
| direction = direction * -1 | |
| # process each wire within face | |
| for f in offsetArea.Faces: | |
| wire_cnt = 0 | |
| for w in f.Wires: | |
| use_direction = direction | |
| if wire_cnt > 0: | |
| # swap direction for internal features | |
| use_direction = direction * -1 | |
| wire_direction = _get_direction(w) | |
| # Process wire | |
| if wire_direction == use_direction: | |
| # direction is correct | |
| wires.append(w) | |
| else: | |
| # incorrect direction, so reverse wire | |
| rw = _reverse_wire(w) | |
| wires.append(rw) | |
| offset -= self.cutOut | |
| loop_cnt += 1 | |
| return wires | |
| # 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 = [] | |
| self.profileShapes = [] | |
| self.tempGroup = False | |
| self.showDebugObjects = False | |
| self.checkBase = False | |
| self.module = None | |
| self.radius = None | |
| self.depthParams = None | |
| self.msgNoFaces = ( | |
| translate( | |
| "PathSurfaceSupport", | |
| "Face selection is unavailable for Rotational scans.", | |
| ) | |
| + "\n" | |
| ) | |
| self.msgNoFaces += " " + translate("PathSurfaceSupport", "Ignoring selected faces.") + "\n" | |
| self.JOB = JOB | |
| self.obj = obj | |
| self.profileEdges = "None" | |
| if hasattr(obj, "ProfileEdges"): | |
| self.profileEdges = obj.ProfileEdges | |
| # Setup STL, model type, and bound box containers for each model in Job | |
| for m in range(0, len(JOB.Model.Group)): | |
| self.modelSTLs.append(False) | |
| self.profileShapes.append(False) | |
| # make circle for workplane | |
| self.wpc = Part.makeCircle(2.0) | |
| def PathSurface(self): | |
| if self.obj.Base: | |
| if len(self.obj.Base) > 0: | |
| self.checkBase = True | |
| if self.obj.ScanType == "Rotational": | |
| self.checkBase = False | |
| FreeCAD.Console.PrintWarning(self.msgNoFaces) | |
| def PathWaterline(self): | |
| if self.obj.Base: | |
| if len(self.obj.Base) > 0: | |
| self.checkBase = True | |
| if self.obj.Algorithm in ["OCL Dropcutter", "Experimental"]: | |
| self.checkBase = False | |
| FreeCAD.Console.PrintWarning(self.msgNoFaces) | |
| # public class methods | |
| def setShowDebugObjects(self, grpObj, val): | |
| self.tempGroup = grpObj | |
| self.showDebugObjects = val | |
| def preProcessModel(self, module): | |
| Path.Log.debug("preProcessModel()") | |
| if not self._isReady(module): | |
| return False | |
| FACES = [] | |
| VOIDS = [] | |
| fShapes = [] | |
| vShapes = [] | |
| 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: | |
| Path.Log.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: | |
| Path.Log.debug(" -No obj.Base data.") | |
| for m in range(0, lenGRP): | |
| self.modelSTLs[m] = True | |
| # Process each model base, as a whole, as needed | |
| for m in range(0, lenGRP): | |
| if self.modelSTLs[m] and not fShapes[m]: | |
| Path.Log.debug(" -Pre-processing {} as a whole.".format(GRP[m].Label)) | |
| if self.obj.BoundBox == "BaseBoundBox": | |
| base = GRP[m] | |
| elif self.obj.BoundBox == "Stock": | |
| base = self.JOB.Stock | |
| pPEB = self._preProcessEntireBase(base, m) | |
| if pPEB is False: | |
| msg = ( | |
| translate( | |
| "PathSurfaceSupport", | |
| "Failed to pre-process base as a whole.", | |
| ) | |
| + "\n" | |
| ) | |
| FreeCAD.Console.PrintError(msg) | |
| else: | |
| (fcShp, prflShp) = pPEB | |
| if fcShp: | |
| if fcShp is True: | |
| Path.Log.debug(" -fcShp is True.") | |
| fShapes[m] = True | |
| else: | |
| fShapes[m] = [fcShp] | |
| if prflShp: | |
| if fcShp: | |
| Path.Log.debug("vShapes[{}]: {}".format(m, vShapes[m])) | |
| if vShapes[m]: | |
| Path.Log.debug(" -Cutting void from base profile shape.") | |
| adjPS = prflShp.cut(vShapes[m][0]) | |
| self.profileShapes[m] = [adjPS] | |
| else: | |
| Path.Log.debug(" -vShapes[m] is False.") | |
| self.profileShapes[m] = [prflShp] | |
| else: | |
| Path.Log.debug(" -Saving base profile shape.") | |
| self.profileShapes[m] = [prflShp] | |
| Path.Log.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).""" | |
| Path.Log.debug("ProcessSelectedFaces _isReady({})".format(module)) | |
| modMethodName = module.replace("Op.", "Path") | |
| if hasattr(self, modMethodName): | |
| self.module = module | |
| modMethod = getattr(self, modMethodName) # gets the attribute only | |
| modMethod() # executes as method | |
| else: | |
| Path.Log.error('PSF._isReady() no "{}" method.'.format(module)) | |
| return False | |
| if not self.radius: | |
| Path.Log.error("PSF._isReady() no cutter radius available.") | |
| return False | |
| if not self.depthParams: | |
| Path.Log.error("PSF._isReady() no depth params available.") | |
| return False | |
| return True | |
| def _identifyFacesAndVoids(self, F, V): | |
| TUPS = [] | |
| 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] = [] | |
| F[m].append((shape, faceIdx)) | |
| Path.Log.debug(".. Cutting {}".format(sub)) | |
| hasFace = True | |
| else: | |
| if V[m] is False: | |
| V[m] = [] | |
| V[m].append((shape, faceIdx)) | |
| Path.Log.debug(".. Avoiding {}".format(sub)) | |
| hasVoid = True | |
| return (hasFace, hasVoid) | |
| def _preProcessFacesAndVoids(self, base, FCS, VDS): | |
| mFS = False | |
| mVS = False | |
| mPS = False | |
| mIFS = [] | |
| if FCS: | |
| isHole = False | |
| if self.obj.HandleMultipleFeatures == "Collectively": | |
| cont = True | |
| Path.Log.debug("Attempting to get cross-section of collective faces.") | |
| outFCS, ifL = self.findUnifiedRegions(FCS) | |
| if self.obj.InternalFeaturesCut and ifL: | |
| ifL = [] # clear avoid shape list | |
| if len(outFCS) == 0: | |
| msg = "PathSurfaceSupport \n Cannot process selected faces. Check horizontal \n surface exposure.\n" | |
| FreeCAD.Console.PrintError(msg) | |
| cont = False | |
| else: | |
| cfsL = Part.makeCompound(outFCS) | |
| # Handle profile edges request | |
| if cont and self.profileEdges != "None": | |
| Path.Log.debug(".. include Profile Edge") | |
| ofstVal = self._calculateOffsetValue(isHole) | |
| psOfst = PathUtils.getOffsetArea(cfsL, ofstVal, plane=self.wpc) | |
| if psOfst: | |
| mPS = [psOfst] | |
| if self.profileEdges == "Only": | |
| mFS = True | |
| cont = False | |
| else: | |
| cont = False | |
| if cont: | |
| if self.showDebugObjects: | |
| T = FreeCAD.ActiveDocument.addObject("Part::Feature", "tmpCollectiveShape") | |
| T.Shape = cfsL | |
| T.purgeTouched() | |
| self.tempGroup.addObject(T) | |
| ofstVal = self._calculateOffsetValue(isHole) | |
| faceOfstShp = PathUtils.getOffsetArea(cfsL, ofstVal, plane=self.wpc) | |
| if not faceOfstShp: | |
| msg = "Failed to create offset face." | |
| FreeCAD.Console.PrintError(msg) | |
| cont = False | |
| if cont: | |
| lenIfL = len(ifL) | |
| if not self.obj.InternalFeaturesCut: | |
| if lenIfL == 0: | |
| Path.Log.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 = PathUtils.getOffsetArea(casL, ofstVal, plane=self.wpc) | |
| mIFS.append(intOfstShp) | |
| mFS = [faceOfstShp] | |
| # Eif | |
| elif self.obj.HandleMultipleFeatures == "Individually": | |
| for fcshp, fcIdx in FCS: | |
| cont = True | |
| fNum = fcIdx + 1 | |
| outerFace = False | |
| gUR, ifL = self.findUnifiedRegions(FCS) | |
| if len(gUR) > 0: | |
| outerFace = gUR[0] | |
| if self.obj.InternalFeaturesCut: | |
| ifL = [] # avoid shape list | |
| if outerFace: | |
| Path.Log.debug("Attempting to create offset face of Face{}".format(fNum)) | |
| if self.profileEdges != "None": | |
| ofstVal = self._calculateOffsetValue(isHole) | |
| psOfst = PathUtils.getOffsetArea(outerFace, ofstVal, plane=self.wpc) | |
| if psOfst: | |
| if mPS is False: | |
| mPS = [] | |
| mPS.append(psOfst) | |
| if self.profileEdges == "Only": | |
| if mFS is False: | |
| mFS = [] | |
| mFS.append(True) | |
| cont = False | |
| else: | |
| cont = False | |
| if cont: | |
| ofstVal = self._calculateOffsetValue(isHole) | |
| faceOfstShp = PathUtils.getOffsetArea( | |
| outerFace, ofstVal, plane=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 = PathUtils.getOffsetArea(casL, ofstVal, plane=self.wpc) | |
| mIFS.append(intOfstShp) | |
| # faceOfstShp = faceOfstShp.cut(intOfstShp) | |
| if mFS is False: | |
| mFS = [] | |
| mFS.append(faceOfstShp) | |
| # Eif | |
| # Efor | |
| # Eif | |
| # Eif | |
| if len(mIFS) > 0: | |
| if mVS is False: | |
| mVS = [] | |
| for ifs in mIFS: | |
| mVS.append(ifs) | |
| if VDS: | |
| Path.Log.debug("Processing avoid faces.") | |
| cont = True | |
| isHole = False | |
| outFCS, intFEAT = self.findUnifiedRegions(VDS) | |
| if self.obj.InternalFeaturesCut: | |
| intFEAT = [] | |
| lenOtFcs = len(outFCS) | |
| if lenOtFcs == 0: | |
| cont = False | |
| else: | |
| if lenOtFcs == 1: | |
| avoid = outFCS[0] | |
| else: | |
| avoid = Part.makeCompound(outFCS) | |
| if self.showDebugObjects: | |
| P = FreeCAD.ActiveDocument.addObject("Part::Feature", "tmpVoidEnvelope") | |
| P.Shape = avoid | |
| P.purgeTouched() | |
| self.tempGroup.addObject(P) | |
| if cont: | |
| if self.showDebugObjects: | |
| P = FreeCAD.ActiveDocument.addObject("Part::Feature", "tmpVoidCompound") | |
| P.Shape = avoid | |
| P.purgeTouched() | |
| self.tempGroup.addObject(P) | |
| ofstVal = self._calculateOffsetValue(isHole, isVoid=True) | |
| avdOfstShp = PathUtils.getOffsetArea(avoid, ofstVal, plane=self.wpc) | |
| if avdOfstShp is False: | |
| msg = "Failed to create collective offset avoid face.\n" | |
| FreeCAD.Console.PrintError(msg) | |
| cont = False | |
| if cont: | |
| avdShp = avdOfstShp | |
| if not self.obj.AvoidLastX_InternalFeatures and len(intFEAT) > 0: | |
| if len(intFEAT) > 1: | |
| ifc = Part.makeCompound(intFEAT) | |
| else: | |
| ifc = intFEAT[0] | |
| ofstVal = self._calculateOffsetValue(isHole=True) | |
| ifOfstShp = PathUtils.getOffsetArea(ifc, ofstVal, plane=self.wpc) | |
| if ifOfstShp is False: | |
| msg = "Failed to create collective offset avoid internal features.\n" | |
| FreeCAD.Console.PrintError(msg) | |
| else: | |
| avdShp = avdOfstShp.cut(ifOfstShp) | |
| if mVS is False: | |
| mVS = [] | |
| 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: | |
| Path.Log.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: | |
| Path.Log.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: | |
| Path.Log.debug("Failed to slice baseEnv shape.") | |
| cont = False | |
| if cont and self.profileEdges != "None": | |
| Path.Log.debug(" -Attempting profile geometry for model base.") | |
| ofstVal = self._calculateOffsetValue(isHole) | |
| psOfst = PathUtils.getOffsetArea(csFaceShape, ofstVal, plane=self.wpc) | |
| if psOfst: | |
| if self.profileEdges == "Only": | |
| return (True, psOfst) | |
| prflShp = psOfst | |
| else: | |
| cont = False | |
| if cont: | |
| ofstVal = self._calculateOffsetValue(isHole) | |
| faceOffsetShape = PathUtils.getOffsetArea(csFaceShape, ofstVal, plane=self.wpc) | |
| if faceOffsetShape is False: | |
| Path.Log.debug("getOffsetArea() failed for entire base.") | |
| else: | |
| faceOffsetShape.translate( | |
| FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin) | |
| ) | |
| return (faceOffsetShape, prflShp) | |
| return False | |
| def _calculateOffsetValue(self, isHole, isVoid=False): | |
| """_calculateOffsetValue(self.obj, isHole, isVoid) ... internal function. | |
| Calculate the offset for the Path.Area() function.""" | |
| self.JOB = PathUtils.findParentJob(self.obj) | |
| # We need to offset by at least our linear tessellation deflection | |
| # (default GeometryTolerance / 4) to avoid false retracts at the | |
| # boundaries. | |
| tolrnc = max(self.JOB.GeometryTolerance.Value / 10.0, self.obj.LinearDeflection.Value) | |
| if isVoid is False: | |
| if isHole is True: | |
| offset = -1 * self.obj.InternalFeaturesAdjustment.Value | |
| offset += self.radius + tolrnc | |
| else: | |
| offset = -1 * self.obj.BoundaryAdjustment.Value | |
| if self.obj.BoundaryEnforcement is True: | |
| offset += self.radius + tolrnc | |
| else: | |
| offset -= self.radius + tolrnc | |
| offset = 0.0 - offset | |
| else: | |
| offset = -1 * self.obj.BoundaryAdjustment.Value | |
| offset += self.radius + tolrnc | |
| return offset | |
| def findUnifiedRegions(self, shapeAndIndexTuples, useAreaImplementation=True): | |
| """Wrapper around area and wire based region unification | |
| implementations.""" | |
| Path.Log.debug("findUnifiedRegions()") | |
| # Allow merging of faces within the LinearDeflection tolerance. | |
| tolerance = self.obj.LinearDeflection.Value | |
| # Default: normal to Z=1 (XY plane), at Z=0 | |
| try: | |
| # Use Area based implementation | |
| shapes = Part.makeCompound([t[0] for t in shapeAndIndexTuples]) | |
| outlineShape = PathUtils.getOffsetArea( | |
| shapes, | |
| # Make the outline very slightly smaller, to avoid creating | |
| # small edges in the cut with the hole-preserving projection. | |
| 0.0 - tolerance / 10, | |
| removeHoles=True, # Outline has holes filled in | |
| tolerance=tolerance, | |
| plane=self.wpc, | |
| ) | |
| projectionShape = PathUtils.getOffsetArea( | |
| shapes, | |
| # Make the projection very slightly larger | |
| tolerance / 10, | |
| removeHoles=False, # Projection has holes preserved | |
| tolerance=tolerance, | |
| plane=self.wpc, | |
| ) | |
| internalShape = outlineShape.cut(projectionShape) | |
| # Filter out tiny faces, usually artifacts around the perimeter of | |
| # the cut. | |
| minArea = (10 * tolerance) ** 2 | |
| internalFaces = [f for f in internalShape.Faces if f.Area > minArea] | |
| if internalFaces: | |
| internalFaces = Part.makeCompound(internalFaces) | |
| return ([outlineShape], [internalFaces]) | |
| except Exception as e: | |
| Path.Log.warning("getOffsetArea failed: {}; Using FindUnifiedRegions.".format(e)) | |
| # Use face-unifying class | |
| FUR = FindUnifiedRegions(shapeAndIndexTuples, tolerance) | |
| if self.showDebugObjects: | |
| FUR.setTempGroup(self.tempGroup) | |
| return (FUR.getUnifiedRegions(), FUR.getInternalFeatures) | |
| # Eclass | |
| # Functions for getting a shape envelope and cross-section | |
| def getExtrudedShape(wire): | |
| Path.Log.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: | |
| Path.Log.error(" -extrude wire failed: \n{}".format(ee)) | |
| return False | |
| SHP = Part.makeSolid(shell) | |
| return SHP | |
| def getShapeSlice(shape): | |
| Path.Log.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: | |
| Path.Log.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 = [] | |
| 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 | |
| Path.Log.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: | |
| Path.Log.error(str(ee)) | |
| return False | |
| else: | |
| pWire = Part.Wire(prj.Shape.Edges) | |
| if pWire.isClosed() is False: | |
| return False | |
| slc = Part.Face(pWire) | |
| slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) | |
| return slc | |
| def getCrossSection(shape): | |
| Path.Log.debug("getCrossSection()") | |
| wires = [] | |
| 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: | |
| Path.Log.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: | |
| Path.Log.debug(" -No wires from .slice() method") | |
| return False | |
| def getShapeEnvelope(shape): | |
| Path.Log.debug("getShapeEnvelope()") | |
| wBB = shape.BoundBox | |
| extFwd = wBB.ZLength + 10.0 | |
| minz = wBB.ZMin | |
| maxz = wBB.ZMin + extFwd | |
| stpDwn = (maxz - minz) / 4.0 | |
| dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) | |
| try: | |
| env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape | |
| except Exception as ee: | |
| FreeCAD.Console.PrintError("PathUtils.getEnvelope() failed.\n" + str(ee) + "\n") | |
| return False | |
| else: | |
| return env | |
| def getSliceFromEnvelope(env): | |
| Path.Log.debug("getSliceFromEnvelope()") | |
| eBB = env.BoundBox | |
| extFwd = eBB.ZLength + 10.0 | |
| maxz = eBB.ZMin + extFwd | |
| emax = math.floor(maxz - 1.0) | |
| E = [] | |
| 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 | |
| def _prepareModelSTLs(self, JOB, obj, m, ocl): | |
| """Tessellate model shapes or copy existing meshes into ocl.STLSurf | |
| objects""" | |
| if self.modelSTLs[m] is True: | |
| model = JOB.Model.Group[m] | |
| self.modelSTLs[m] = _makeSTL(model, obj, ocl, self.modelTypes[m]) | |
| def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes, ocl): | |
| """_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... | |
| Creates and OCL.stl object with combined data with waste stock, | |
| model, and avoided faces. Travel lines can be checked against this | |
| STL object to determine minimum travel height to clear stock and model.""" | |
| Path.Log.debug("_makeSafeSTL()") | |
| fuseShapes = [] | |
| Mdl = JOB.Model.Group[mdlIdx] | |
| mBB = Mdl.Shape.BoundBox | |
| sBB = JOB.Stock.Shape.BoundBox | |
| # add Model shape to safeSTL shape | |
| fuseShapes.append(Mdl.Shape) | |
| if obj.BoundBox == "BaseBoundBox": | |
| cont = False | |
| extFwd = sBB.ZLength | |
| zmin = mBB.ZMin | |
| zmax = mBB.ZMin + extFwd | |
| stpDwn = (zmax - zmin) / 4.0 | |
| dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) | |
| try: | |
| envBB = PathUtils.getEnvelope( | |
| partshape=Mdl.Shape, depthparams=dep_par | |
| ) # Produces .Shape | |
| cont = True | |
| except Exception as ee: | |
| Path.Log.error(str(ee)) | |
| shell = Mdl.Shape.Shells[0] | |
| solid = Part.makeSolid(shell) | |
| try: | |
| envBB = PathUtils.getEnvelope( | |
| partshape=solid, depthparams=dep_par | |
| ) # Produces .Shape | |
| cont = True | |
| except Exception as eee: | |
| Path.Log.error(str(eee)) | |
| if cont: | |
| stckWst = JOB.Stock.Shape.cut(envBB) | |
| if obj.BoundaryAdjustment > 0.0: | |
| cmpndFS = Part.makeCompound(faceShapes) | |
| baBB = PathUtils.getEnvelope( | |
| partshape=cmpndFS, depthparams=self.depthParams | |
| ) # Produces .Shape | |
| adjStckWst = stckWst.cut(baBB) | |
| else: | |
| adjStckWst = stckWst | |
| fuseShapes.append(adjStckWst) | |
| else: | |
| msg = "Path transitions might not avoid the model. Verify paths.\n" | |
| FreeCAD.Console.PrintWarning(msg) | |
| else: | |
| # If boundbox is Job.Stock, add hidden pad under stock as base plate | |
| toolDiam = self.cutter.getDiameter() | |
| zMin = JOB.Stock.Shape.BoundBox.ZMin | |
| xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam | |
| yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam | |
| bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) | |
| bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) | |
| bH = 1.0 | |
| crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) | |
| B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) | |
| fuseShapes.append(B) | |
| if voidShapes: | |
| voidComp = Part.makeCompound(voidShapes) | |
| voidEnv = PathUtils.getEnvelope( | |
| partshape=voidComp, depthparams=self.depthParams | |
| ) # Produces .Shape | |
| fuseShapes.append(voidEnv) | |
| fused = Part.makeCompound(fuseShapes) | |
| if self.showDebugObjects: | |
| T = FreeCAD.ActiveDocument.addObject("Part::Feature", "safeSTLShape") | |
| T.Shape = fused | |
| T.purgeTouched() | |
| self.tempGroup.addObject(T) | |
| self.safeSTLs[mdlIdx] = _makeSTL(fused, obj, ocl) | |
| def _makeSTL(model, obj, ocl, model_type=None): | |
| """Convert a mesh or shape into an OCL STL, using the tessellation | |
| tolerance specified in obj.LinearDeflection. | |
| Returns an ocl.STLSurf().""" | |
| # Determine Deflection Values | |
| lin_def = obj.LinearDeflection.Value | |
| ang_def = obj.AngularDeflection.Value | |
| # Apply Overrides for Waterline OCL Adaptive | |
| # OCL Adaptive is a Vector-based algorithm, not a Grid-based algorithm (like Dropcutter) | |
| # This fundamental difference makes it sensitive to Topology (how points connect) rather than just density | |
| # Models with internal features can cause the algorithm to be confused even with very high density values. | |
| # The following values create the cleanest possible Topology for a vector-slicing algorithm | |
| # Setting those values here rather than hacking the Obj values in Waterline.py is preferable. | |
| algo = getattr(obj, "Algorithm", None) | |
| if algo == "OCL Adaptive": | |
| # Force the "Sweet Spot" values for topology stability (Good enough for 99% or more of operations) | |
| lin_def = 0.001 | |
| ang_def = 0.15 | |
| if model_type == "M": | |
| facets = model.Mesh.Facets.Points | |
| else: | |
| if hasattr(model, "Shape"): | |
| shape = model.Shape | |
| else: | |
| shape = model | |
| # vertices, facet_indices = shape.tessellate(obj.LinearDeflection.Value) # tessellate workaround | |
| # Workaround for tessellate bug | |
| mesh = MeshPart.meshFromShape( | |
| Shape=shape, | |
| LinearDeflection=lin_def, | |
| AngularDeflection=ang_def, | |
| ) | |
| vertices = [point.Vector for point in mesh.Points] | |
| facet_indices = [facet.PointIndices for facet in mesh.Facets] | |
| facets = ((vertices[f[0]], vertices[f[1]], vertices[f[2]]) for f in facet_indices) | |
| stl = ocl.STLSurf() | |
| for tri in facets: | |
| v1, v2, v3 = tri | |
| t = ocl.Triangle( | |
| ocl.Point(v1[0], v1[1], v1[2]), | |
| ocl.Point(v2[0], v2[1], v2[2]), | |
| ocl.Point(v3[0], v3[1], v3[2]), | |
| ) | |
| stl.addTriangle(t) | |
| return stl | |
| # Functions to convert path geometry into line/arc segments for OCL input or directly to g-code | |
| def pathGeomToLinesPointSet(self, obj, compGeoShp): | |
| """pathGeomToLinesPointSet(self, obj, compGeoShp)... | |
| Convert a compound set of sequential line segments to directionally-oriented collinear groupings. | |
| """ | |
| Path.Log.debug("pathGeomToLinesPointSet()") | |
| # Extract intersection line segments for return value as [] | |
| LINES = [] | |
| inLine = [] | |
| 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 self.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 self.CutClimb is True: | |
| inLine.reverse() | |
| LINES.append(inLine) # Save inLine segments | |
| lnCnt += 1 | |
| inLine = [] # reset collinear container | |
| if self.CutClimb is True: | |
| sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) | |
| else: | |
| sp = ep | |
| if self.CutClimb is True: | |
| tup = (v2, v1) | |
| if chkGap: | |
| gap = abs(self.toolDiam - lst.sub(ep).Length) | |
| lst = cp | |
| else: | |
| tup = (v1, v2) | |
| if chkGap: | |
| gap = abs(self.toolDiam - lst.sub(cp).Length) | |
| lst = ep | |
| if chkGap: | |
| if gap < obj.GapThreshold.Value: | |
| inLine.pop() # pop off 'BRK' marker | |
| ( | |
| vA, | |
| vB, | |
| ) = inLine.pop() # pop off previous line segment for combining with current | |
| tup = (vA, tup[1]) | |
| self.closedGap = True | |
| else: | |
| gap = round(gap, 6) | |
| if gap < self.gaps[0]: | |
| self.gaps.insert(0, gap) | |
| self.gaps.pop() | |
| inLine.append(tup) | |
| # Efor | |
| lnCnt += 1 | |
| if self.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 = [] | |
| 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: | |
| Path.Log.debug("Line count is ODD: {}.".format(lnCnt)) | |
| else: | |
| Path.Log.debug("Line count is even: {}.".format(lnCnt)) | |
| return LINES | |
| def pathGeomToZigzagPointSet(self, obj, compGeoShp): | |
| """_pathGeomToZigzagPointSet(self, 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.""" | |
| Path.Log.debug("_pathGeomToZigzagPointSet()") | |
| # Extract intersection line segments for return value as [] | |
| LINES = [] | |
| inLine = [] | |
| lnCnt = 0 | |
| chkGap = False | |
| ec = len(compGeoShp.Edges) | |
| dirFlg = 1 | |
| if self.CutClimb: | |
| dirFlg = -1 | |
| edg0 = compGeoShp.Edges[0] | |
| p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) | |
| p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) | |
| if dirFlg == 1: | |
| tup = (p1, p2) | |
| lst = FreeCAD.Vector(p2[0], p2[1], 0.0) | |
| sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point | |
| else: | |
| tup = (p2, p1) | |
| lst = FreeCAD.Vector(p1[0], p1[1], 0.0) | |
| sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point | |
| inLine.append(tup) | |
| for ei in range(1, ec): | |
| edg = compGeoShp.Edges[ei] | |
| v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) | |
| v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) | |
| cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) | |
| ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point | |
| iC = cp.isOnLineSegment(sp, ep) | |
| if iC: | |
| inLine.append("BRK") | |
| chkGap = True | |
| gap = abs(self.toolDiam - lst.sub(cp).Length) | |
| else: | |
| chkGap = False | |
| if dirFlg == -1: | |
| inLine.reverse() | |
| LINES.append(inLine) | |
| lnCnt += 1 | |
| dirFlg = -1 * dirFlg # Change zig to zag | |
| inLine = [] # reset collinear container | |
| sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) | |
| lst = ep | |
| if dirFlg == 1: | |
| tup = (v1, v2) | |
| else: | |
| tup = (v2, v1) | |
| if chkGap: | |
| if gap < obj.GapThreshold.Value: | |
| 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) | |
| self.closedGap = True | |
| else: | |
| gap = round(gap, 6) | |
| if gap < self.gaps[0]: | |
| self.gaps.insert(0, gap) | |
| self.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 | |
| Path.Log.debug("Line count is even: {}.".format(lnCnt)) | |
| else: | |
| Path.Log.debug("Line count is ODD: {}.".format(lnCnt)) | |
| dirFlg = -1 * dirFlg | |
| if not obj.CutPatternReversed: | |
| if self.CutClimb: | |
| dirFlg = -1 * dirFlg | |
| if obj.CutPatternReversed: | |
| dirFlg = -1 * dirFlg | |
| # Handle last inLine list | |
| if dirFlg == 1: | |
| rev = [] | |
| 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 = [] | |
| for iL in rev: | |
| if iL == "BRK": | |
| rev2.append(iL) | |
| else: | |
| (p1, p2) = iL | |
| rev2.append((p2, p1)) | |
| rev2.reverse() | |
| rev = rev2 | |
| LINES.append(rev) | |
| else: | |
| LINES.append(inLine) | |
| return LINES | |
| def pathGeomToCircularPointSet(self, obj, compGeoShp): | |
| """pathGeomToCircularPointSet(self, 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 [] | |
| Path.Log.debug("pathGeomToCircularPointSet()") | |
| ARCS = [] | |
| stpOvrEI = [] | |
| segEI = [] | |
| 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 | |
| def dist_to_cent(item): | |
| # Sort incoming arcs by distance to center | |
| # item: edge type, direction flag, parts tuple | |
| # parts: start tuple, end tuple, center tuple | |
| s = item[2][0][0] | |
| p1 = FreeCAD.Vector(s[0], s[1], 0.0) | |
| e = item[2][0][2] | |
| p2 = FreeCAD.Vector(e[0], e[1], 0.0) | |
| return p1.sub(p2).Length | |
| if obj.CutPatternReversed: | |
| if self.CutClimb: | |
| self.CutClimb = False | |
| else: | |
| self.CutClimb = True | |
| # 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(self.tmpCOM).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(self.tmpCOM).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(self.tmpCOM).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 = [] | |
| endOnAxis = [] | |
| 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(self.tmpCOM.y - E.Vertexes[0].Y) < 0.00001: | |
| startOnAxis.append((i, ei, E.Vertexes[0])) | |
| elif abs(self.tmpCOM.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 self.CutClimb: # True yields Climb when set to Conventional | |
| dirFlg = -1 | |
| # Declare center point of circle pattern | |
| cp = (self.tmpCOM.x, self.tmpCOM.y, 0.0) | |
| # Cycle through stepOver data | |
| for so in range(0, len(stpOvrEI)): | |
| SO = stpOvrEI[so] | |
| if SO[0] == "L": # L = Loop/Ring/Circle | |
| # Path.Log.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 = self.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(self.tmpCOM).Length | |
| spcRadRatio = space / rad | |
| if spcRadRatio < 1.0: | |
| tolrncAng = math.asin(spcRadRatio) | |
| else: | |
| tolrncAng = 0.99999998 * math.pi | |
| EX = self.tmpCOM.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) | |
| 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])) | |
| elif SO[0] == "A": # A = Arc | |
| # Path.Log.debug("SO[0] == 'Arc'") | |
| PRTS = [] | |
| 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: # Connected edges(arcs) | |
| (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) | |
| 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) | |
| if dirFlg == 1: | |
| arc = (sp, ep, cp) | |
| if chkGap: | |
| gap = abs( | |
| self.toolDiam - gapDist(lst, sp) | |
| ) # abs(self.toolDiam - lst.sub(sp).Length) | |
| lst = ep | |
| else: | |
| arc = ( | |
| ep, | |
| sp, | |
| cp, | |
| ) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) | |
| if chkGap: | |
| gap = abs( | |
| self.toolDiam - gapDist(lst, ep) | |
| ) # abs(self.toolDiam - lst.sub(ep).Length) | |
| lst = sp | |
| if chkGap: | |
| 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) | |
| self.closedGap = True | |
| else: | |
| gap = round(gap, 6) | |
| if gap < self.gaps[0]: | |
| self.gaps.insert(0, gap) | |
| self.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 | |
| ARCS.sort(key=dist_to_cent, reverse=obj.CutPatternReversed) | |
| return ARCS | |
| def pathGeomToSpiralPointSet(obj, compGeoShp): | |
| """_pathGeomToSpiralPointSet(obj, compGeoShp)... | |
| Convert a compound set of sequential line segments to directional, connected groupings.""" | |
| Path.Log.debug("_pathGeomToSpiralPointSet()") | |
| # Extract intersection line segments for return value as [] | |
| LINES = [] | |
| inLine = [] | |
| lnCnt = 0 | |
| ec = len(compGeoShp.Edges) | |
| start = 2 | |
| if obj.CutPatternReversed: | |
| edg1 = compGeoShp.Edges[ | |
| 0 | |
| ] # Skip first edge, as it is the closing edge: center to outer tail | |
| ec -= 1 | |
| start = 1 | |
| else: | |
| edg1 = compGeoShp.Edges[ | |
| 1 | |
| ] # Skip first edge, as it is the closing edge: center to outer tail | |
| p1 = FreeCAD.Vector(edg1.Vertexes[0].X, edg1.Vertexes[0].Y, 0.0) | |
| p2 = FreeCAD.Vector(edg1.Vertexes[1].X, edg1.Vertexes[1].Y, 0.0) | |
| tup = ((p1.x, p1.y), (p2.x, p2.y)) | |
| inLine.append(tup) | |
| for ei in range(start, ec): # Skipped first edge, started with second edge above as edg1 | |
| edg = compGeoShp.Edges[ei] # Get edge for vertexes | |
| sp = FreeCAD.Vector( | |
| edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0 | |
| ) # check point (first / middle point) | |
| ep = FreeCAD.Vector(edg.Vertexes[1].X, edg.Vertexes[1].Y, 0.0) # end point | |
| tup = ((sp.x, sp.y), (ep.x, ep.y)) | |
| if sp.sub(p2).Length < 0.000001: | |
| inLine.append(tup) | |
| else: | |
| LINES.append(inLine) # Save inLine segments | |
| lnCnt += 1 | |
| inLine = [] # 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. | |
| """ | |
| Path.Log.debug("pathGeomToOffsetPointSet()") | |
| LINES = [] | |
| optimize = obj.OptimizeLinearPaths | |
| ofstCnt = len(compGeoShp) | |
| # Cycle through offset loops | |
| iPOL = False | |
| 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 = [] | |
| self.edgeData = [] | |
| self.circleData = [] | |
| self.noSharedEdges = True | |
| self.topWires = [] | |
| self.REGIONS = [] | |
| self.INTERNALS = [] | |
| self.idGroups = [] | |
| self.sharedEdgeIdxs = [] | |
| self.fusedFaces = None | |
| self.internalsReady = False | |
| 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) | |
| if base.Volume == 0: | |
| Path.Log.debug( | |
| "Ignoring Face{}. It is likely vertical with no horizontal exposure.".format( | |
| fcIdx | |
| ) | |
| ) | |
| cont = False | |
| if cont: | |
| # Identify top face of base | |
| fIdx = 0 | |
| zMin = base.Faces[fIdx].BoundBox.ZMin | |
| for bfi in range(0, len(base.Faces)): | |
| fzmin = base.Faces[bfi].BoundBox.ZMin | |
| if fzmin > zMin: | |
| fIdx = bfi | |
| zMin = fzmin | |
| # Translate top face to Z=0.0 and save to topFaces list | |
| topFace = base.Faces[fIdx] | |
| # self._showShape(topFace, 'topFace_{}'.format(fNum)) | |
| tfBB = topFace.BoundBox | |
| tfBB_Area = tfBB.XLength * tfBB.YLength | |
| fBB_Area = fBB.XLength * fBB.YLength | |
| if tfBB_Area < (fBB_Area * 0.9): | |
| # attempt alternate methods | |
| topFace = self._getCompleteCrossSection(ef) | |
| tfBB = topFace.BoundBox | |
| tfBB_Area = tfBB.XLength * tfBB.YLength | |
| # self._showShape(topFace, 'topFaceAlt_1_{}'.format(fNum)) | |
| if tfBB_Area < (fBB_Area * 0.9): | |
| topFace = getShapeSlice(ef) | |
| tfBB = topFace.BoundBox | |
| tfBB_Area = tfBB.XLength * tfBB.YLength | |
| # self._showShape(topFace, 'topFaceAlt_2_{}'.format(fNum)) | |
| if tfBB_Area < (fBB_Area * 0.9): | |
| msg = "Failed to extract processing region for Face {}\n".format(fNum) | |
| FreeCAD.Console.PrintError(msg) | |
| cont = False | |
| # Eif | |
| 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): | |
| Path.Log.debug("_groupEdgesByLength()") | |
| threshold = self.geomToler | |
| grp = [] | |
| processLast = False | |
| def keyFirst(tup): | |
| return tup[0] | |
| # Sort edgeData data and prepare proxy indexes | |
| self.edgeData.sort(key=keyFirst) | |
| DATA = self.edgeData | |
| lenDATA = len(DATA) | |
| indexes = [i for i in range(0, lenDATA)] | |
| idxCnt = len(indexes) | |
| while idxCnt > 0: | |
| processLast = True | |
| # Pop off index for first edge | |
| actvIdx = indexes.pop(0) | |
| actvItem = DATA[actvIdx][0] # 0 index is length | |
| grp.append(actvIdx) | |
| idxCnt -= 1 | |
| 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) | |
| else: | |
| if len(grp) > 1: | |
| # grp.sort() | |
| self.idGroups.append(grp) | |
| grp = [] | |
| break | |
| # Ewhile | |
| # Ewhile | |
| if processLast: | |
| if len(grp) > 1: | |
| # grp.sort() | |
| self.idGroups.append(grp) | |
| def _identifySharedEdgesByLength(self, grp): | |
| Path.Log.debug("_identifySharedEdgesByLength()") | |
| holds = [] | |
| specialIndexes = [] | |
| threshold = self.geomToler | |
| def keyFirst(tup): | |
| return tup[0] | |
| # Sort edgeData data | |
| self.edgeData.sort(key=keyFirst) | |
| DATA = self.edgeData | |
| 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 = [] | |
| if len(specialIndexes) > 0: | |
| # Remove shared edges from EDGES data | |
| uniqueShared = list(set(specialIndexes)) | |
| self.sharedEdgeIdxs.extend(uniqueShared) | |
| self.noSharedEdges = False | |
| def _extractWiresFromEdges(self): | |
| Path.Log.debug("_extractWiresFromEdges()") | |
| DATA = self.edgeData | |
| holds = [] | |
| firstEdge = None | |
| cont = True | |
| connectedEdges = [] | |
| connectedIndexes = [] | |
| connectedCnt = 0 | |
| LOOPS = [] | |
| 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) | |
| holds.extend(indexes) | |
| indexes = holds | |
| idxCnt = len(indexes) | |
| holds = [] | |
| if idxCnt == 0: | |
| cont = False | |
| if safety == 0: | |
| cont = False | |
| # Ewhile | |
| numLoops = len(LOOPS) | |
| Path.Log.debug(" -numLoops: {}.".format(numLoops)) | |
| if numLoops > 0: | |
| for li in range(0, numLoops): | |
| Edges = LOOPS[li] | |
| # for e in Edges: | |
| # self._showShape(e, 'Loop_{}_Edge'.format(li)) | |
| wire = Part.Wire(Part.__sortEdges__(Edges)) | |
| if wire.isClosed(): | |
| # This simple Part.Face() method fails to catch | |
| # wires with tangent closed wires, or an external | |
| # wire with one or more internal tangent wires. | |
| # face = Part.Face(wire) | |
| # This method works with the complex tangent | |
| # closed wires mentioned above. | |
| extWire = wire.extrude(FreeCAD.Vector(0.0, 0.0, 2.0)) | |
| wireSolid = Part.makeSolid(extWire) | |
| extdBBFace1 = makeExtendedBoundBox( | |
| wireSolid.BoundBox, 5.0, wireSolid.BoundBox.ZMin + 1.0 | |
| ) | |
| extdBBFace2 = makeExtendedBoundBox( | |
| wireSolid.BoundBox, 5.0, wireSolid.BoundBox.ZMin + 1.0 | |
| ) | |
| inverse = extdBBFace1.cut(wireSolid) | |
| face = extdBBFace2.cut(inverse) | |
| self.REGIONS.append(face) | |
| self.REGIONS.sort(key=faceArea, reverse=True) | |
| def _identifyInternalFeatures(self): | |
| Path.Log.debug("_identifyInternalFeatures()") | |
| remList = [] | |
| 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: | |
| Path.Log.debug(" - No common area.\n") | |
| remList.sort(reverse=True) | |
| for ri in remList: | |
| self.REGIONS.pop(ri) | |
| def _processNestedRegions(self): | |
| Path.Log.debug("_processNestedRegions()") | |
| cont = True | |
| hold = [] | |
| Ids = [] | |
| remList = [] | |
| for i in range(0, len(self.REGIONS)): | |
| Ids.append(i) | |
| idsCnt = len(Ids) | |
| while cont: | |
| while idsCnt > 0: | |
| hi = Ids.pop(0) | |
| high = self.REGIONS[hi] | |
| idsCnt -= 1 | |
| while idsCnt > 0: | |
| isTrue = False | |
| li = Ids.pop(0) | |
| idsCnt -= 1 | |
| low = self.REGIONS[li] | |
| # Test case here | |
| if self._isInBoundBox(high, low): | |
| cmn = high.common(low) | |
| if cmn.Area > 0.0: | |
| isTrue = True | |
| # if True action here | |
| if isTrue: | |
| self.REGIONS[hi] = high.cut(low) | |
| remList.append(li) | |
| else: | |
| hold.append(hi) | |
| # Ewhile | |
| hold.extend(Ids) | |
| Ids = hold | |
| hold = [] | |
| idsCnt = len(Ids) | |
| 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): | |
| Path.Log.debug("_getCompleteCrossSection()") | |
| wires = [] | |
| 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 | |
| Path.Log.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.""" | |
| Path.Log.debug("getUnifiedRegions()") | |
| if len(self.FACES) == 0: | |
| msg = "No FACE data tuples received at instantiation of class.\n" | |
| FreeCAD.Console.PrintError(msg) | |
| return [] | |
| self._extractTopFaces() | |
| lenFaces = len(self.topFaces) | |
| if lenFaces == 0: | |
| return [] | |
| # if single topFace, return it | |
| if lenFaces == 1: | |
| topFace = self.topFaces[0][0] | |
| self._showShape(topFace, "TopFace") | |
| # prepare inner wires as faces for internal features | |
| lenWrs = len(topFace.Wires) | |
| if lenWrs > 1: | |
| for w in range(1, lenWrs): | |
| wr = topFace.Wires[w] | |
| self.INTERNALS.append(Part.Face(wr)) | |
| self.internalsReady = True | |
| # Flatten face and extract outer wire, then convert to face | |
| extWire = getExtrudedShape(topFace) | |
| wCS = getCrossSection(extWire) | |
| if wCS: | |
| face = Part.Face(wCS) | |
| return [face] | |
| else: | |
| (faceShp, fcIdx) = self.FACES[0] | |
| msg = translate( | |
| "PathSurfaceSupport", | |
| "Failed to identify a horizontal cross-section for Face", | |
| ) | |
| msg += "{}.\n".format(fcIdx + 1) | |
| FreeCAD.Console.PrintWarning(msg) | |
| return [] | |
| # process multiple top faces, unifying if possible | |
| self._fuseTopFaces() | |
| for F in self.fusedFaces.Faces: | |
| self._showShape(F, "TopFaceFused") | |
| self._getEdgesData() | |
| self._groupEdgesByLength() | |
| for grp in self.idGroups: | |
| self._identifySharedEdgesByLength(grp) | |
| if self.noSharedEdges: | |
| Path.Log.debug("No shared edges by length detected.") | |
| allTopFaces = [] | |
| for topFace, fcIdx in self.topFaces: | |
| allTopFaces.append(topFace) | |
| # Identify internal features | |
| lenWrs = len(topFace.Wires) | |
| if lenWrs > 1: | |
| for w in range(1, lenWrs): | |
| wr = topFace.Wires[w] | |
| self.INTERNALS.append(Part.Face(wr)) | |
| self.internalsReady = True | |
| return allTopFaces | |
| else: | |
| # Delete shared edges from edgeData list | |
| self.sharedEdgeIdxs.sort(reverse=True) | |
| for se in self.sharedEdgeIdxs: | |
| self.edgeData.pop(se) | |
| self._extractWiresFromEdges() | |
| self._identifyInternalFeatures() | |
| self._processNestedRegions() | |
| # for ri in range(0, len(self.REGIONS)): | |
| # self._showShape(self.REGIONS[ri], 'UnifiedRegion_{}'.format(ri)) | |
| self.internalsReady = True | |
| return self.REGIONS | |
| def getInternalFeatures(self): | |
| """getInternalFeatures()... Returns internal features identified | |
| after calling getUnifiedRegions().""" | |
| if self.internalsReady: | |
| if len(self.INTERNALS) > 0: | |
| return self.INTERNALS | |
| else: | |
| return False | |
| msg = "getUnifiedRegions() must be called before getInternalFeatures().\n" | |
| FreeCAD.Console.PrintError(msg) | |
| return False | |
| class OCL_Tool: | |
| """The OCL_Tool class is designed to translate a FreeCAD standard ToolBit shape | |
| in the active Tool Controller, into an OCL tool type.""" | |
| def __init__(self, ocl, obj, safe=False): | |
| self.ocl = ocl | |
| self.obj = obj | |
| self.tool = None | |
| self.tiltCutter = False | |
| self.safe = safe | |
| self.oclTool = None | |
| self.toolType = None | |
| self.toolMode = None | |
| self.toolMethod = None | |
| self.diameter = -1.0 | |
| self.cornerRadius = -1.0 | |
| self.flatRadius = -1.0 | |
| self.cutEdgeHeight = -1.0 | |
| self.cutEdgeAngle = -1.0 | |
| # Default to zero. ToolBit likely is without. | |
| self.lengthOffset = 0.0 | |
| if hasattr(obj, "ToolController"): | |
| if hasattr(obj.ToolController, "Tool"): | |
| self.tool = obj.ToolController.Tool | |
| if hasattr(self.tool, "ShapeType"): | |
| self.toolType = self.tool.ShapeType.lower() | |
| self.toolMode = "ToolBit" | |
| elif hasattr(self.tool, "ShapeName"): # backward compatibility | |
| self.toolType = self.tool.ShapeName # Indicates ToolBit tool | |
| self.toolMode = "ToolBit" | |
| if self.toolType: | |
| Path.Log.debug("OCL_Tool tool mode, type: {}, {}".format(self.toolMode, self.toolType)) | |
| """ | |
| #### FreeCAD Legacy tool shape properties per tool type | |
| shape = EndMill | |
| Diameter | |
| CuttingEdgeHeight | |
| LengthOffset | |
| shape = Drill | |
| Diameter | |
| CuttingEdgeAngle # TipAngle from above, center shaft. 180 = flat tip (endmill) | |
| CuttingEdgeHeight | |
| LengthOffset | |
| shape = CenterDrill | |
| Diameter | |
| FlatRadius | |
| CornerRadius | |
| CuttingEdgeAngle # TipAngle from above, center shaft. 180 = flat tip (endmill) | |
| CuttingEdgeHeight | |
| LengthOffset | |
| shape = CounterSink | |
| Diameter | |
| FlatRadius | |
| CornerRadius | |
| CuttingEdgeAngle # TipAngle from above, center shaft. 180 = flat tip (endmill) | |
| CuttingEdgeHeight | |
| LengthOffset | |
| shape = CounterBore | |
| Diameter | |
| FlatRadius | |
| CornerRadius | |
| CuttingEdgeAngle # TipAngle from above, center shaft. 180 = flat tip (endmill) | |
| CuttingEdgeHeight | |
| LengthOffset | |
| shape = FlyCutter | |
| Diameter | |
| FlatRadius | |
| CornerRadius | |
| CuttingEdgeAngle # TipAngle from above, center shaft. 180 = flat tip (endmill) | |
| CuttingEdgeHeight | |
| LengthOffset | |
| shape = Reamer | |
| Diameter | |
| FlatRadius | |
| CornerRadius | |
| CuttingEdgeAngle # TipAngle from above, center shaft. 180 = flat tip (endmill) | |
| CuttingEdgeHeight | |
| LengthOffset | |
| shape = Tap | |
| Diameter | |
| FlatRadius | |
| CornerRadius | |
| CuttingEdgeAngle # TipAngle from above, center shaft. 180 = flat tip (endmill) | |
| CuttingEdgeHeight | |
| LengthOffset | |
| shape = SlotCutter | |
| Diameter | |
| FlatRadius | |
| CornerRadius | |
| CuttingEdgeAngle # TipAngle from above, center shaft. 180 = flat tip (endmill) | |
| CuttingEdgeHeight | |
| LengthOffset | |
| shape = BallEndMill | |
| Diameter | |
| FlatRadius | |
| CornerRadius | |
| CuttingEdgeAngle # TipAngle from above, center shaft. 180 = flat tip (endmill) | |
| CuttingEdgeHeight | |
| LengthOffset | |
| shape = ChamferMill | |
| Diameter | |
| FlatRadius | |
| CornerRadius | |
| CuttingEdgeAngle # TipAngle from above, center shaft. 180 = flat tip (endmill) | |
| CuttingEdgeHeight | |
| LengthOffset | |
| shape = CornerRound | |
| Diameter | |
| FlatRadius | |
| CornerRadius | |
| CuttingEdgeAngle # TipAngle from above, center shaft. 180 = flat tip (endmill) | |
| CuttingEdgeHeight | |
| LengthOffset | |
| shape = Engraver | |
| Diameter | |
| CuttingEdgeAngle # TipAngle from above, center shaft. 180 = flat tip (endmill) | |
| CuttingEdgeHeight | |
| LengthOffset | |
| #### FreeCAD packaged ToolBit named constraints per shape files | |
| shape = endmill | |
| Diameter; Endmill diameter | |
| Length; Overall length of the endmill | |
| ShankDiameter; diameter of the shank | |
| CuttingEdgeHeight | |
| shape = ballend | |
| Diameter; Endmill diameter | |
| Length; Overall length of the endmill | |
| ShankDiameter; diameter of the shank | |
| CuttingEdgeHeight | |
| shape = bullnose | |
| Diameter; Endmill diameter | |
| Length; Overall length of the endmill | |
| ShankDiameter; diameter of the shank | |
| FlatRadius;Radius of the bottom flat part. | |
| CuttingEdgeHeight | |
| shape = drill | |
| TipAngle; Full angle of the drill tip | |
| Diameter; Drill bit diameter | |
| Length; Overall length of the drillbit | |
| shape = v-bit | |
| Diameter; Overall diameter of the V-bit | |
| CuttingEdgeAngle;Full angle of the v-bit | |
| Length; Overall bit length | |
| ShankDiameter | |
| FlatHeight;Height of the flat extension of the v-bit | |
| FlatRadius; Diameter of the flat end of the tip | |
| """ | |
| # Private methods | |
| def _setDimensions(self): | |
| """_setDimensions() ... Set values for possible dimensions.""" | |
| if hasattr(self.tool, "Diameter"): | |
| self.diameter = float(self.tool.Diameter) | |
| else: | |
| msg = translate("PathSurfaceSupport", "Diameter dimension missing from ToolBit shape.") | |
| FreeCAD.Console.PrintError(msg + "\n") | |
| return False | |
| if hasattr(self.tool, "LengthOffset"): | |
| self.lengthOffset = float(self.tool.LengthOffset) | |
| # Derive flatRadius from diameter and cornerRadius if both are present | |
| if hasattr(self.tool, "FlatRadius"): | |
| self.flatRadius = float(self.tool.FlatRadius) | |
| if hasattr(self.tool, "CornerRadius") and hasattr(self.tool, "Diameter"): | |
| self.cornerRadius = float(self.tool.CornerRadius) | |
| self.flatRadius = (self.diameter / 2.0) - self.cornerRadius | |
| if hasattr(self.tool, "CuttingEdgeHeight"): | |
| self.cutEdgeHeight = float(self.tool.CuttingEdgeHeight) | |
| if hasattr(self.tool, "CuttingEdgeAngle"): | |
| self.cutEdgeAngle = float(self.tool.CuttingEdgeAngle) | |
| return True | |
| def _makeSafeCutter(self): | |
| # Make safeCutter with 25% buffer around physical cutter | |
| if self.safe: | |
| self.diameter = self.diameter * 1.25 | |
| if self.flatRadius == 0.0: | |
| self.flatRadius = self.diameter * 0.25 | |
| elif self.flatRadius > 0.0: | |
| self.flatRadius = self.flatRadius * 1.25 | |
| def _oclCylCutter(self): | |
| # Standard End Mill, Slot cutter, or Fly cutter | |
| # OCL -> CylCutter::CylCutter(diameter, length) | |
| if self.diameter == -1.0 or self.cutEdgeHeight == -1.0: | |
| return | |
| self.oclTool = self.ocl.CylCutter(self.diameter, self.cutEdgeHeight + self.lengthOffset) | |
| def _oclBallCutter(self): | |
| # Standard Ball End Mill | |
| # OCL -> BallCutter::BallCutter(diameter, length) | |
| if self.diameter == -1.0 or self.cutEdgeHeight == -1.0: | |
| return | |
| self.tiltCutter = True | |
| if self.cutEdgeHeight == 0: | |
| self.cutEdgeHeight = self.diameter / 2 | |
| self.oclTool = self.ocl.BallCutter(self.diameter, self.cutEdgeHeight + self.lengthOffset) | |
| def _oclBullCutter(self): | |
| # Standard Bull Nose cutter | |
| # Reference: https://www.fine-tools.com/halbstabfraeser.html | |
| # OCL -> BullCutter::BullCutter(diameter, minor radius, length) | |
| if self.diameter == -1.0 or self.flatRadius == -1.0 or self.cutEdgeHeight == -1.0: | |
| return | |
| self.oclTool = self.ocl.BullCutter( | |
| self.diameter, | |
| self.diameter / 2 - self.flatRadius, | |
| self.cutEdgeHeight + self.lengthOffset, | |
| ) | |
| def _oclConeCutter(self): | |
| # Engraver or V-bit cutter | |
| # OCL -> ConeCutter::ConeCutter(diameter, angle, length) | |
| if self.diameter == -1.0 or self.cutEdgeAngle == -1.0 or self.cutEdgeHeight == -1.0: | |
| return | |
| self.oclTool = self.ocl.ConeCutter(self.diameter, self.cutEdgeAngle / 2, self.lengthOffset) | |
| def _setToolMethod(self): | |
| toolMap = dict() | |
| if self.toolMode == "ToolBit": | |
| toolMap = { | |
| "endmill": "CylCutter", | |
| "ballend": "BallCutter", | |
| "bullnose": "BullCutter", | |
| "drill": "ConeCutter", | |
| "engraver": "ConeCutter", | |
| "v_bit": "ConeCutter", | |
| "v-bit": "ConeCutter", | |
| "vbit": "ConeCutter", | |
| "chamfer": "None", | |
| } | |
| self.toolMethod = "None" | |
| if self.toolType in toolMap: | |
| self.toolMethod = toolMap[self.toolType] | |
| # Public methods | |
| def getOclTool(self): | |
| """getOclTool()... Call this method after class instantiation | |
| to return OCL tool object.""" | |
| # Check for tool controller and tool object | |
| if not self.tool or not self.toolMode: | |
| msg = translate("PathSurface", "Failed to identify tool for operation.") | |
| FreeCAD.Console.PrintError(msg + "\n") | |
| return False | |
| if not self._setDimensions(): | |
| return False | |
| self._setToolMethod() | |
| if self.toolMethod == "None": | |
| err = translate("PathSurface", "Failed to map selected tool to an OCL tool type.") | |
| FreeCAD.Console.PrintError(err + "\n") | |
| return False | |
| else: | |
| Path.Log.debug("OCL_Tool tool method: {}".format(self.toolMethod)) | |
| oclToolMethod = getattr(self, "_ocl" + self.toolMethod) | |
| oclToolMethod() | |
| if self.oclTool: | |
| return self.oclTool | |
| # Set error messages | |
| err = translate("PathSurface", "Failed to translate active tool to OCL tool type.") | |
| FreeCAD.Console.PrintError(err + "\n") | |
| return False | |
| def useTiltCutter(self): | |
| """useTiltCutter()... Call this method after getOclTool() method | |
| to return status of cutter tilt availability - generally this | |
| is for a ball end mill.""" | |
| if not self.tool or not self.oclTool: | |
| err = translate( | |
| "PathSurface", | |
| "OCL tool not available. Cannot determine is cutter has tilt available.", | |
| ) | |
| FreeCAD.Console.PrintError(err + "\n") | |
| return False | |
| return self.tiltCutter | |
| # Eclass | |
| # Support functions | |
| def makeExtendedBoundBox(wBB, bbBfr, zDep): | |
| Path.Log.debug("makeExtendedBoundBox()") | |
| p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep) | |
| p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep) | |
| p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep) | |
| p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep) | |
| L1 = Part.makeLine(p1, p2) | |
| L2 = Part.makeLine(p2, p3) | |
| L3 = Part.makeLine(p3, p4) | |
| L4 = Part.makeLine(p4, p1) | |
| return Part.Face(Part.Wire([L1, L2, L3, L4])) | |