| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | __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 |
| |
|
| | |
| | from lazy_loader.lazy_loader import LazyLoader |
| |
|
| | MeshPart = LazyLoader("MeshPart", globals(), "MeshPart") |
| | 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.""" |
| |
|
| | |
| | |
| | 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) |
| |
|
| | |
| | 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): |
| | |
| | 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) |
| |
|
| | |
| | self.deltaX = self.shape.BoundBox.XLength |
| | self.deltaY = self.shape.BoundBox.YLength |
| | self.deltaC = ( |
| | self.shape.BoundBox.DiagonalLength |
| | ) |
| | lineLen = self.deltaC + ( |
| | 2.0 * self.toolDiam |
| | ) |
| | self.halfDiag = math.ceil(lineLen / 2.0) |
| | cutPasses = ( |
| | math.ceil(lineLen / self.cutOut) + 1 |
| | ) |
| | self.halfPasses = math.ceil(cutPasses / 2.0) |
| |
|
| | |
| | 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() |
| |
|
| | |
| | geomShape = Part.makeCompound(self.rawGeoList) |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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)) |
| | |
| | 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) |
| | |
| | self.rawGeoList = GeoSet |
| |
|
| | def _CircularZigZag(self): |
| | self._Circular() |
| |
|
| | def _Line(self): |
| | GeoSet = [] |
| | centRot = FreeCAD.Vector(0.0, 0.0, 0.0) |
| |
|
| | |
| | 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) |
| | |
| | p1 = FreeCAD.Vector(x1, y1, 0.0) |
| | p2 = FreeCAD.Vector(x2, y1, 0.0) |
| | pntTuples.append((p1, p2)) |
| |
|
| | |
| | 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 |
| | sumRadians = 0.0 |
| | loopCnt = 0 |
| | segCnt = 0 |
| | twoPi = 2.0 * math.pi |
| | maxDist = math.ceil(self.cutOut * self._getRadialPasses()) |
| | move = self.centerOfPattern |
| | lastPoint = FreeCAD.Vector(0.0, 0.0, 0.0) |
| |
|
| | |
| | cutOut = self.cutOut / twoPi |
| | segLen = self.obj.SampleInterval.Value |
| | stepAng = segLen / ((loopCnt + 1) * self.cutOut) |
| | 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) |
| | sumRadians += stepAng |
| | loopRadians += stepAng |
| | if loopRadians > twoPi: |
| | loopCnt += 1 |
| | loopRadians -= twoPi |
| | stepAng = segLen / ( |
| | (loopCnt + 1) * self.cutOut |
| | ) |
| | segCnt += 1 |
| | lastPoint = p2 |
| | if sumRadians > stopRadians: |
| | draw = False |
| | |
| | lineSeg = Part.makeLine(p2, p1) |
| | SEGS.append(lineSeg) |
| | |
| | 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) |
| | sumRadians += stepAng |
| | loopRadians += stepAng |
| | if loopRadians > twoPi: |
| | loopCnt += 1 |
| | loopRadians -= twoPi |
| | stepAng = segLen / ( |
| | (loopCnt + 1) * self.cutOut |
| | ) |
| | segCnt += 1 |
| | lastPoint = p2 |
| | if sumRadians > stopRadians: |
| | draw = False |
| | |
| | lineSeg = Part.makeLine(p1, p2) |
| | SEGS.append(lineSeg) |
| | |
| | |
| | spiral = Part.Wire([ls.Edges[0] for ls in SEGS]) |
| | GeoSet.append(spiral) |
| |
|
| | self.rawGeoList = GeoSet |
| |
|
| | def _ZigZag(self): |
| | self._Line() |
| |
|
| | |
| | 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 |
| | ) |
| |
|
| | |
| | if centerAt != "Custom": |
| | self.obj.PatternCenterCustom = cntrPnt |
| | self.centerOfPattern = cntrPnt |
| |
|
| | return cntrPnt |
| |
|
| | def _getRadialPasses(self): |
| | |
| | radialPasses = self.halfPasses |
| | if self.obj.PatternCenterAt != "CenterOfBoundBox": |
| | |
| | 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 |
| | ) |
| | radialPasses = ( |
| | math.ceil(diag / self.cutOut) + 1 |
| | ) |
| |
|
| | 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 |
| | 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(rev_list) |
| |
|
| | while True: |
| | offsetArea = PathUtils.getOffsetArea(shape, offset, plane=self.wpc) |
| | if not offsetArea: |
| | |
| | break |
| |
|
| | |
| | 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 |
| |
|
| | |
| | if self.obj.CutMode == "Conventional": |
| | if loop_cnt == 1: |
| | direction = direction * -1 |
| |
|
| | |
| | for f in offsetArea.Faces: |
| | wire_cnt = 0 |
| | for w in f.Wires: |
| | use_direction = direction |
| | if wire_cnt > 0: |
| | |
| | use_direction = direction * -1 |
| | wire_direction = _get_direction(w) |
| | |
| | if wire_direction == use_direction: |
| | |
| | wires.append(w) |
| | else: |
| | |
| | rw = _reverse_wire(w) |
| | wires.append(rw) |
| |
|
| | offset -= self.cutOut |
| | loop_cnt += 1 |
| | return wires |
| |
|
| |
|
| | |
| |
|
| |
|
| | 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 |
| |
|
| | |
| | for m in range(0, len(JOB.Model.Group)): |
| | self.modelSTLs.append(False) |
| | self.profileShapes.append(False) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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 |
| |
|
| | |
| | for m in range(0, lenGRP): |
| | FACES.append(False) |
| | VOIDS.append(False) |
| | fShapes.append(False) |
| | vShapes.append(False) |
| |
|
| | |
| | if self.checkBase: |
| | Path.Log.debug(" -obj.Base exists. Pre-processing for selected faces.") |
| |
|
| | (hasFace, hasVoid) = self._identifyFacesAndVoids( |
| | FACES, VOIDS |
| | ) |
| | hasGeometry = True if hasFace or hasVoid else False |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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]) |
| | ) |
| | |
| |
|
| | return (fShapes, vShapes) |
| |
|
| | |
| | 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) |
| | modMethod() |
| | 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 |
| |
|
| | |
| | for bs, SBS in self.obj.Base: |
| | for sb in SBS: |
| | |
| | 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)) |
| |
|
| | |
| | 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 = [] |
| |
|
| | 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) |
| |
|
| | |
| | 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] |
| | |
| |
|
| | 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 = [] |
| |
|
| | 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) |
| | |
| |
|
| | if mFS is False: |
| | mFS = [] |
| | mFS.append(faceOfstShp) |
| | |
| | |
| | |
| | |
| |
|
| | 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 |
| | |
| | |
| |
|
| | try: |
| | baseEnv = PathUtils.getEnvelope( |
| | partshape=base.Shape, subshape=None, depthparams=self.depthParams |
| | ) |
| | 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 |
| | ) |
| | 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) |
| | |
| | |
| | |
| | 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()") |
| | |
| | tolerance = self.obj.LinearDeflection.Value |
| | |
| | try: |
| | |
| | shapes = Part.makeCompound([t[0] for t in shapeAndIndexTuples]) |
| | outlineShape = PathUtils.getOffsetArea( |
| | shapes, |
| | |
| | |
| | 0.0 - tolerance / 10, |
| | removeHoles=True, |
| | tolerance=tolerance, |
| | plane=self.wpc, |
| | ) |
| | projectionShape = PathUtils.getOffsetArea( |
| | shapes, |
| | |
| | tolerance / 10, |
| | removeHoles=False, |
| | tolerance=tolerance, |
| | plane=self.wpc, |
| | ) |
| | internalShape = outlineShape.cut(projectionShape) |
| | |
| | |
| | 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)) |
| | |
| | FUR = FindUnifiedRegions(shapeAndIndexTuples, tolerance) |
| | if self.showDebugObjects: |
| | FUR.setTempGroup(self.tempGroup) |
| | return (FUR.getUnifiedRegions(), FUR.getInternalFeatures) |
| |
|
| |
|
| | |
| |
|
| |
|
| | |
| | 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 |
| | 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) |
| | 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) |
| | 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 |
| |
|
| | |
| | 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 |
| | ) |
| | 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 |
| | ) |
| | 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 |
| | ) |
| | 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: |
| | |
| | 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 |
| | ) |
| | 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().""" |
| | |
| | lin_def = obj.LinearDeflection.Value |
| | ang_def = obj.AngularDeflection.Value |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | algo = getattr(obj, "Algorithm", None) |
| | if algo == "OCL Adaptive": |
| | |
| | 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 |
| | |
| | |
| | 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 |
| |
|
| |
|
| | |
| | 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()") |
| | |
| | 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) |
| |
|
| | for ei in range(1, ec): |
| | chkGap = False |
| | edg = compGeoShp.Edges[ei] |
| | v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) |
| | v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) |
| |
|
| | ep = FreeCAD.Vector(v2[0], v2[1], 0.0) |
| | cp = FreeCAD.Vector(v1[0], v1[1], 0.0) |
| | |
| | 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) |
| | lnCnt += 1 |
| | inLine = [] |
| | if self.CutClimb is True: |
| | sp = cp |
| | 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() |
| | ( |
| | vA, |
| | vB, |
| | ) = inLine.pop() |
| | 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) |
| |
|
| | |
| | lnCnt += 1 |
| | if self.CutClimb is True: |
| | inLine.reverse() |
| | LINES.append(inLine) |
| |
|
| | |
| | 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()") |
| | |
| | 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) |
| | else: |
| | tup = (p2, p1) |
| | lst = FreeCAD.Vector(p1[0], p1[1], 0.0) |
| | sp = FreeCAD.Vector(p2[0], p2[1], 0.0) |
| | 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) |
| | ep = FreeCAD.Vector(v2[0], v2[1], 0.0) |
| | 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 |
| | inLine = [] |
| | sp = cp |
| |
|
| | lst = ep |
| | if dirFlg == 1: |
| | tup = (v1, v2) |
| | else: |
| | tup = (v2, v1) |
| |
|
| | if chkGap: |
| | if gap < obj.GapThreshold.Value: |
| | inLine.pop() |
| | ( |
| | vA, |
| | vB, |
| | ) = inLine.pop() |
| | 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) |
| | |
| | lnCnt += 1 |
| |
|
| | |
| | isEven = lnCnt % 2 |
| | if isEven == 0: |
| | 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 |
| |
|
| | |
| | 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.""" |
| | |
| | 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) |
| |
|
| | def dist_to_cent(item): |
| | |
| | |
| | |
| | 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 |
| |
|
| | |
| | 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: |
| | |
| | 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: |
| | |
| | stpOvrEI.append(["A", segEI, False]) |
| | |
| | segEI = [ei] |
| | isSame = True |
| | pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) |
| | sameRad = pnt.sub(self.tmpCOM).Length |
| | |
| | if isSame is True: |
| | stpOvrEI.append(["A", segEI, False]) |
| |
|
| | |
| | for so in range(0, len(stpOvrEI)): |
| | SO = stpOvrEI[so] |
| | if SO[0] == "A": |
| | startOnAxis = [] |
| | endOnAxis = [] |
| | EI = SO[1] |
| |
|
| | |
| | for i in range(0, len(EI)): |
| | ei = EI[i] |
| | E = compGeoShp.Edges[ei] |
| | 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])) |
| |
|
| | |
| | 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: |
| | SO[2] = (eiE, eiS) |
| | break |
| | elif dist > 0: |
| | break |
| | |
| | |
| | |
| |
|
| | |
| | dirFlg = 1 |
| | if not self.CutClimb: |
| | dirFlg = -1 |
| |
|
| | |
| | cp = (self.tmpCOM.x, self.tmpCOM.y, 0.0) |
| |
|
| | |
| | for so in range(0, len(stpOvrEI)): |
| | SO = stpOvrEI[so] |
| | if SO[0] == "L": |
| | |
| | lei = SO[1] |
| | v1 = compGeoShp.Edges[lei].Vertexes[0] |
| |
|
| | |
| | |
| | space = self.toolDiam * 0.005 |
| |
|
| | |
| | p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) |
| | 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 |
| |
|
| | sp = (v1.X, v1.Y, 0.0) |
| | ep = (EX, EY, 0.0) |
| | if dirFlg == 1: |
| | arc = (sp, ep, cp) |
| | else: |
| | arc = ( |
| | ep, |
| | sp, |
| | cp, |
| | ) |
| | ARCS.append(("L", dirFlg, [arc])) |
| | elif SO[0] == "A": |
| | |
| | PRTS = [] |
| | EI = SO[1] |
| | CONN = SO[2] |
| | chkGap = False |
| | lst = None |
| |
|
| | if CONN: |
| | (iE, iS) = CONN |
| | v1 = compGeoShp.Edges[iE].Vertexes[0] |
| | v2 = compGeoShp.Edges[iS].Vertexes[1] |
| | sp = (v1.X, v1.Y, 0.0) |
| | ep = (v2.X, v2.Y, 0.0) |
| | if dirFlg == 1: |
| | arc = (sp, ep, cp) |
| | lst = ep |
| | else: |
| | arc = ( |
| | ep, |
| | sp, |
| | cp, |
| | ) |
| | lst = sp |
| | PRTS.append(arc) |
| | |
| | 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) |
| | ) |
| | lst = ep |
| | else: |
| | arc = ( |
| | ep, |
| | sp, |
| | cp, |
| | ) |
| | if chkGap: |
| | gap = abs( |
| | self.toolDiam - gapDist(lst, ep) |
| | ) |
| | lst = sp |
| | if chkGap: |
| | if gap < obj.GapThreshold.Value: |
| | PRTS.pop() |
| | ( |
| | vA, |
| | vB, |
| | vC, |
| | ) = PRTS.pop() |
| | 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)) |
| | |
| | if obj.CutPattern == "CircularZigZag": |
| | dirFlg = -1 * dirFlg |
| | |
| |
|
| | 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()") |
| | |
| | LINES = [] |
| | inLine = [] |
| | lnCnt = 0 |
| | ec = len(compGeoShp.Edges) |
| | start = 2 |
| |
|
| | if obj.CutPatternReversed: |
| | edg1 = compGeoShp.Edges[ |
| | 0 |
| | ] |
| | ec -= 1 |
| | start = 1 |
| | else: |
| | edg1 = compGeoShp.Edges[ |
| | 1 |
| | ] |
| | 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): |
| | edg = compGeoShp.Edges[ei] |
| | sp = FreeCAD.Vector( |
| | edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0 |
| | ) |
| | ep = FreeCAD.Vector(edg.Vertexes[1].X, edg.Vertexes[1].Y, 0.0) |
| | tup = ((sp.x, sp.y), (ep.x, ep.y)) |
| |
|
| | if sp.sub(p2).Length < 0.000001: |
| | inLine.append(tup) |
| | else: |
| | LINES.append(inLine) |
| | lnCnt += 1 |
| | inLine = [] |
| | inLine.append(tup) |
| | |
| | p2 = ep |
| | |
| |
|
| | lnCnt += 1 |
| | LINES.append(inLine) |
| |
|
| | 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) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | prev = OS[0] |
| | pnt = OS[1] |
| | for v in range(1, lenOS): |
| | nxt = OS[v + 1] |
| | if optimize: |
| | |
| | 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) |
| | |
| |
|
| | 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 |
| | 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 |
| |
|
| | |
| | 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: |
| | cont = True |
| | fNum = fcIdx + 1 |
| | |
| | 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) |
| |
|
| | |
| | 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: |
| | |
| | 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 |
| |
|
| | |
| | topFace = base.Faces[fIdx] |
| | |
| | tfBB = topFace.BoundBox |
| | tfBB_Area = tfBB.XLength * tfBB.YLength |
| | fBB_Area = fBB.XLength * fBB.YLength |
| | if tfBB_Area < (fBB_Area * 0.9): |
| | |
| | topFace = self._getCompleteCrossSection(ef) |
| | tfBB = topFace.BoundBox |
| | tfBB_Area = tfBB.XLength * tfBB.YLength |
| | |
| | if tfBB_Area < (fBB_Area * 0.9): |
| | topFace = getShapeSlice(ef) |
| | tfBB = topFace.BoundBox |
| | tfBB_Area = tfBB.XLength * tfBB.YLength |
| | |
| | if tfBB_Area < (fBB_Area * 0.9): |
| | msg = "Failed to extract processing region for Face {}\n".format(fNum) |
| | FreeCAD.Console.PrintError(msg) |
| | 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] |
| |
|
| | |
| | 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] |
| |
|
| | |
| | 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 |
| | |
| | actvIdx = indexes.pop(0) |
| | actvItem = DATA[actvIdx][0] |
| | grp.append(actvIdx) |
| | idxCnt -= 1 |
| |
|
| | while idxCnt > 0: |
| | tstIdx = indexes[0] |
| | tstItem = DATA[tstIdx][0] |
| |
|
| | |
| | absLenDiff = abs(tstItem - actvItem) |
| | if absLenDiff < threshold: |
| | |
| | indexes.pop(0) |
| | idxCnt -= 1 |
| | grp.append(tstIdx) |
| | else: |
| | if len(grp) > 1: |
| | |
| | self.idGroups.append(grp) |
| | grp = [] |
| | break |
| | |
| | |
| | if processLast: |
| | if len(grp) > 1: |
| | |
| | self.idGroups.append(grp) |
| |
|
| | def _identifySharedEdgesByLength(self, grp): |
| | Path.Log.debug("_identifySharedEdgesByLength()") |
| | holds = [] |
| | specialIndexes = [] |
| | threshold = self.geomToler |
| |
|
| | def keyFirst(tup): |
| | return tup[0] |
| |
|
| | |
| | self.edgeData.sort(key=keyFirst) |
| | DATA = self.edgeData |
| | lenGrp = len(grp) |
| |
|
| | while lenGrp > 0: |
| | |
| | actvIdx = grp.pop(0) |
| | actvItem = DATA[actvIdx][0] |
| | lenGrp -= 1 |
| | while lenGrp > 0: |
| | isTrue = False |
| | |
| | tstIdx = grp.pop(0) |
| | tstItem = DATA[tstIdx][0] |
| | lenGrp -= 1 |
| |
|
| | |
| | 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 |
| |
|
| | |
| | if isTrue: |
| | specialIndexes.append(actvIdx) |
| | specialIndexes.append(tstIdx) |
| | break |
| | else: |
| | holds.append(tstIdx) |
| |
|
| | |
| | holds.extend(grp) |
| | grp = holds |
| | lenGrp = len(grp) |
| | holds = [] |
| |
|
| | if len(specialIndexes) > 0: |
| | |
| | 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 |
| |
|
| | |
| | DATA.sort(key=faceIndex) |
| | lenDATA = len(DATA) |
| | indexes = [i for i in range(0, lenDATA)] |
| | idxCnt = len(indexes) |
| |
|
| | |
| | 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 |
| | idxCnt -= 1 |
| | connectedIndexes.append(actvIdx) |
| | connectedEdges.append(actvEdge) |
| | connectedCnt = 1 |
| |
|
| | safety = 750 |
| | while cont: |
| | safety -= 1 |
| | notConnected = True |
| | while idxCnt > 0: |
| | isTrue = False |
| | |
| | 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) |
| | |
| |
|
| | if connectedCnt > 2: |
| | if self._edgesAreConnected(actvEdge, firstEdge): |
| | notConnected = False |
| | |
| | LOOPS.append(connectedEdges) |
| | |
| | connectedEdges = [] |
| | connectedIndexes = [] |
| | connectedCnt = 0 |
| | indexes.sort() |
| | idxCnt = len(indexes) |
| | if idxCnt > 0: |
| | |
| | actvIdx = indexes.pop(0) |
| | actvEdge = DATA[actvIdx][2] |
| | idxCnt -= 1 |
| | firstEdge = actvEdge |
| | connectedIndexes.append(actvIdx) |
| | connectedEdges.append(actvEdge) |
| | connectedCnt = 1 |
| | |
| |
|
| | |
| | if notConnected: |
| | holds.append(actvIdx) |
| | holds.extend(indexes) |
| | indexes = holds |
| | idxCnt = len(indexes) |
| | holds = [] |
| | if idxCnt == 0: |
| | cont = False |
| | if safety == 0: |
| | cont = False |
| | |
| |
|
| | numLoops = len(LOOPS) |
| | Path.Log.debug(" -numLoops: {}.".format(numLoops)) |
| | if numLoops > 0: |
| | for li in range(0, numLoops): |
| | Edges = LOOPS[li] |
| | |
| | |
| | wire = Part.Wire(Part.__sortEdges__(Edges)) |
| | if wire.isClosed(): |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | 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] |
| | |
| | if self._isInBoundBox(high, low): |
| | cmn = high.common(low) |
| | if cmn.Area > 0.0: |
| | isTrue = True |
| | |
| | if isTrue: |
| | self.REGIONS[hi] = high.cut(low) |
| | remList.append(li) |
| | else: |
| | hold.append(hi) |
| | |
| | hold.extend(Ids) |
| | Ids = hold |
| | hold = [] |
| | idsCnt = len(Ids) |
| | if len(Ids) == 0: |
| | cont = False |
| | |
| | |
| | remList.sort(reverse=True) |
| | for ri in remList: |
| | self.REGIONS.pop(ri) |
| |
|
| | |
| | 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) |
| | 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): |
| | |
| |
|
| | def isSameVertex(v1, v2): |
| | |
| | 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 |
| |
|
| | |
| | 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 lenFaces == 1: |
| | topFace = self.topFaces[0][0] |
| | self._showShape(topFace, "TopFace") |
| | |
| | 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 |
| | |
| | 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 [] |
| |
|
| | |
| | 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) |
| | |
| | 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: |
| | |
| | self.sharedEdgeIdxs.sort(reverse=True) |
| | for se in self.sharedEdgeIdxs: |
| | self.edgeData.pop(se) |
| |
|
| | self._extractWiresFromEdges() |
| | self._identifyInternalFeatures() |
| | self._processNestedRegions() |
| | |
| | |
| |
|
| | 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 |
| | |
| | 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"): |
| | self.toolType = self.tool.ShapeName |
| | 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 |
| | """ |
| |
|
| | |
| | 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) |
| | |
| | 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): |
| | |
| | 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): |
| | |
| | |
| | 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): |
| | |
| | |
| | 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): |
| | |
| | |
| | |
| | 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): |
| | |
| | |
| | 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] |
| |
|
| | |
| | def getOclTool(self): |
| | """getOclTool()... Call this method after class instantiation |
| | to return OCL 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 |
| |
|
| | |
| | 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 |
| |
|
| |
|
| | |
| |
|
| |
|
| | |
| | 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])) |
| |
|