# -*- coding: utf-8 -*- # *************************************************************************** # * * # * Copyright (c) 2016 sliptonic * # * * # * 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 * # * * # *************************************************************************** # * * # * Additional modifications and contributions beginning 2019 * # * by Russell Johnson * # * Version: Rev. 3t Usable * # * * # *************************************************************************** # Revision Notes # - Continue implementation of cut patterns: zigzag, line (line is used to force cut modes: climb, conventional) # - Continue implementation of ignore waste feature for planar scans # - Planar Op: move scan result(self.CLP) to local scope(CLP) # - Implemented @sliptonic's meshFromShape() parameter improvements for better, faster mesh creation # RELEASE NOTES # Only G0 and G1 gcode commands are used throughout. # CutMode: Climb, Conventional is only functional for some operations, like waterline and rotational scans # CutPattern: only functional for some operations # IgnoreWaste: not implemented yet - target op is for planar dropcutter # High density/resolution, mult-layer scans require significantly more processing time. # Rotational scans are very processor intensive. # Rotational scans take a longer time to complete. # Multi-pass rotational scans require a very long time to complete, # even at larger SampleInterval values - ex: 1mm, 0.5mm # Remember, the larger the model, the more time to complete an operation! # Rotational require even more time. Multi-pass require much, much more time. # This release is NOT bug-free. # After changing an operational value in the Properties list, press the ENTER key. # Change all desired values, one at a time, pressing ENTER key after each change. # When all property changes complete, click the blue recompute icon under user menus. # If you have an existing 3D Surface Op in your Job(s), you will need to delete it and recreate # with this updated script installed because it may contain additional properties not # created with the original version. from __future__ import print_function import FreeCAD import MeshPart # import Part import Path import PathScripts.PathLog as PathLog # import PathScripts.PathPocketBase as PathPocketBase import PathScripts.PathUtils as PathUtils import PathScripts.PathOp as PathOp from PySide import QtCore import time import math __title__ = "Path Surface Operation" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" __doc__ = "Class and implementation of Mill Facing operation." __contributors__ = "roivai[FreeCAD], russ4262 (Russell Johnson)" __created__ = "2016" __scriptVersion__ = "3t Usable" __lastModified__ = "2019-05-10 10:37 CST" if False: PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) PathLog.trackModule(PathLog.thisModule()) else: PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) # Qt tanslation handling def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) # OCL must be installed try: import ocl except: FreeCAD.Console.PrintError( translate("Path_Surface", "This operation requires OpenCamLib to be installed.") + "\n") import sys sys.exit(translate("Path_Surface", "This operation requires OpenCamLib to be installed.")) class ObjectSurface(PathOp.ObjectOp): '''Proxy object for Surfacing operation.''' # These are static while document is open, if it contains a 3D Surface Op initFinalDepth = None initOpFinalDepth = None initOpStartDepth = None docRestored = False def baseObject(self): '''baseObject() ... returns super of receiver Used to call base implementation in overwritten functions.''' return super(__class__, self) def opFeatures(self, obj): '''opFeatures(obj) ... return all standard features and edges based geomtries''' return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown def initOperation(self, obj): '''initPocketOp(obj) ... create facing specific properties''' obj.addProperty("App::PropertyEnumeration", "Algorithm", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The library to use to generate the path")) obj.addProperty("App::PropertyEnumeration", "DropCutterDir", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction along which dropcutter lines are created")) obj.addProperty("App::PropertyEnumeration", "BoundBox", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "Should the operation be limited by the stock object or by the bounding box of the base object")) obj.addProperty("App::PropertyVectorDistance", "DropCutterExtraOffset", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box")) obj.addProperty("App::PropertyEnumeration", "ScanType", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.")) obj.addProperty("App::PropertyEnumeration", "LayerMode", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The completion mode for the operation: single or multi-pass")) obj.addProperty("App::PropertyEnumeration", "CutMode", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part: Climb(ClockWise) or Conventional(CounterClockWise)")) obj.addProperty("App::PropertyEnumeration", "CutPattern", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Clearing pattern to use")) obj.addProperty("App::PropertyEnumeration", "RotationAxis", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "The model will be rotated around this axis.")) obj.addProperty("App::PropertyFloat", "StartIndex", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Start index(angle) for rotational scan")) obj.addProperty("App::PropertyFloat", "StopIndex", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")) obj.addProperty("App::PropertyFloat", "CutterTilt", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")) obj.addProperty("App::PropertyPercent", "StepOver", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Step over percentage of the drop cutter path")) obj.addProperty("App::PropertyDistance", "DepthOffset", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Z-axis offset from the surface of the object")) # obj.addProperty("App::PropertyFloatConstraint", "SampleInterval", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times")) obj.addProperty("App::PropertyFloat", "SampleInterval", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times")) obj.addProperty("App::PropertyBool", "Optimize", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization which removes unnecessary points from G-Code output")) obj.addProperty("App::PropertyBool", "IgnoreWaste", "Waste", QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore areas that proceed below specified depth.")) obj.addProperty("App::PropertyFloat", "IgnoreWasteDepth", "Waste", QtCore.QT_TRANSLATE_NOOP("App::Property", "Depth used to identify waste areas to ignore.")) obj.addProperty("App::PropertyBool", "ReleaseFromWaste", "Waste", QtCore.QT_TRANSLATE_NOOP("App::Property", "Cut through waste to depth at model edge, releasing the model.")) obj.CutMode = ['Conventional', 'Climb'] obj.BoundBox = ['BaseBoundBox', 'Stock'] obj.DropCutterDir = ['X', 'Y'] obj.Algorithm = ['OCL Dropcutter', 'OCL Waterline'] # obj.SampleInterval = (0.04, 0.01, 1.0, 0.01) obj.LayerMode = ['Single-pass', 'Multi-pass'] obj.ScanType = ['Planar', 'Rotational'] obj.RotationAxis = ['X', 'Y'] # obj.CutPattern = ['ZigZag', 'Offset', 'Spiral', 'ZigZagOffset', 'Line', 'Grid', 'Triangle'] obj.CutPattern = ['ZigZag', 'Line'] if not hasattr(obj, 'DoNotSetDefaultValues'): self.setEditorProperties(obj) def setEditorProperties(self, obj): # Used to hide inputs in properties list if obj.Algorithm == 'OCL Dropcutter': obj.setEditorMode('DropCutterDir', 0) # obj.setEditorMode('DropCutterExtraOffset', 0) else: obj.setEditorMode('DropCutterDir', 2) # obj.setEditorMode('DropCutterExtraOffset', 2) def onChanged(self, obj, prop): if prop == "Algorithm": self.setEditorProperties(obj) def opOnDocumentRestored(self, obj): self.setEditorProperties(obj) # Import FinalDepth from existing operation for use in recompute() operations self.initFinalDepth = obj.FinalDepth.Value self.initOpFinalDepth = obj.OpFinalDepth.Value self.docRestored = True PathLog.debug("Imported existing OpFinalDepth of " + str(self.initOpFinalDepth) + " for recompute() purposes.") PathLog.debug("Imported existing FinalDepth of " + str(self.initFinalDepth) + " for recompute() purposes.") def opExecute(self, obj): '''opExecute(obj) ... process surface operation''' PathLog.track() initIdx = 0.0 # Instantiate additional class operation variables rtn = self.resetOpVariables() # mark beginning of operation self.startTime = time.time() # Set cutter for OCL based on tool controller properties rtn = self.setOclCutter(obj) self.reportThis("\n-----\n-----\nBegin 3D surface operation") self.reportThis("Script version: " + __scriptVersion__ + " Lm: " + __lastModified__) output = "" if obj.Comment != "": output += '(' + str(obj.Comment) + ')\n' output += "(" + obj.Label + ")" output += "(Compensated Tool Path. Diameter: " + str(obj.ToolController.Tool.Diameter) + ")" parentJob = PathUtils.findParentJob(obj) if parentJob is None: self.reportThis("No parentJob") return self.SafeHeightOffset = parentJob.SetupSheet.SafeHeightOffset.Value self.ClearHeightOffset = parentJob.SetupSheet.ClearanceHeightOffset.Value # Import OpFinalDepth from pre-existing operation for recompute() scenarios if obj.OpFinalDepth.Value != self.initOpFinalDepth: if obj.OpFinalDepth.Value == obj.FinalDepth.Value: obj.FinalDepth.Value = self.initOpFinalDepth obj.OpFinalDepth.Value = self.initOpFinalDepth if self.initOpFinalDepth is not None: obj.OpFinalDepth.Value = self.initOpFinalDepth # Limit start index if obj.StartIndex < 0.0: obj.StartIndex = 0.0 if obj.StartIndex > 360.0: obj.StartIndex = 360.0 # Limit stop index if obj.StopIndex > 360.0: obj.StopIndex = 360.0 if obj.StopIndex < 0.0: obj.StopIndex = 0.0 # Limit cutter tilt if obj.CutterTilt < -90.0: obj.CutterTilt = -90.0 if obj.CutterTilt > 90.0: obj.CutterTilt = 90.0 # Limit sample interval if obj.SampleInterval < 0.001: obj.SampleInterval = 0.001 if obj.SampleInterval > 25.4: obj.SampleInterval = 25.4 # Limit StepOver to natural number percentage if obj.Algorithm == 'OCL Dropcutter': if obj.StepOver > 100: obj.StepOver = 100 if obj.StepOver < 1: obj.StepOver = 1 self.cutOut = (self.cutter.getDiameter() * (float(obj.StepOver) / 100.0)) self.reportThis("Cut out: " + str(self.cutOut) + " mm") # Cycle through parts of model for base in self.model: self.reportThis("BASE object: " + str(base.Name)) # Rotate model to initial index if obj.ScanType == 'Rotational': initIdx = obj.CutterTilt + obj.StartIndex if initIdx != 0.0: self.basePlacement = FreeCAD.ActiveDocument.getObject(base.Name).Placement if obj.RotationAxis == 'X': # FreeCAD.ActiveDocument.getObject(base.Name).Placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0),FreeCAD.Rotation(FreeCAD.Vector(1,0,0), initIdx)) base.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), initIdx)) else: # FreeCAD.ActiveDocument.getObject(base.Name).Placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0),FreeCAD.Rotation(FreeCAD.Vector(0,1,0), initIdx)) base.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), initIdx)) if base.TypeId.startswith('Mesh'): mesh = base.Mesh else: # try/except is for Path Jobs created before GeometryTolerance try: deflection = parentJob.GeometryTolerance except AttributeError: import PathScripts.PathPreferences as PathPreferences deflection = PathPreferences.defaultGeometryTolerance() # base.Shape.tessellate(0.05) # 0.5 original value # mesh = MeshPart.meshFromShape(base.Shape, Deflection=deflection) mesh = MeshPart.meshFromShape(Shape=base.Shape, LinearDeflection=deflection, AngularDeflection=0.5, Relative=False) # Set bound box if obj.BoundBox == "BaseBoundBox": bb = mesh.BoundBox else: bb = parentJob.Stock.Shape.BoundBox # Objective is to remove material from surface in StepDown layers rather than one pass to FinalDepth final = [] if obj.Algorithm == 'OCL Waterline': self.reportThis("--CutMode: " + str(obj.CutMode)) stl = ocl.STLSurf() for f in mesh.Facets: p = f.Points[0] q = f.Points[1] r = f.Points[2] t = ocl.Triangle(ocl.Point(p[0], p[1], p[2] + obj.DepthOffset.Value), ocl.Point(q[0], q[1], q[2] + obj.DepthOffset.Value), ocl.Point(r[0], r[1], r[2] + obj.DepthOffset.Value)) aT = stl.addTriangle(t) final = self._waterlineOp(obj, stl, bb) elif obj.Algorithm == 'OCL Dropcutter': # Create stl object via OCL stl = ocl.STLSurf() for f in mesh.Facets: p = f.Points[0] q = f.Points[1] r = f.Points[2] t = ocl.Triangle(ocl.Point(p[0], p[1], p[2]), ocl.Point(q[0], q[1], q[2]), ocl.Point(r[0], r[1], r[2])) aT = stl.addTriangle(t) # Rotate model back to original index if obj.ScanType == 'Rotational': if initIdx != 0.0: initIdx = 0.0 base.Placement = self.basePlacement # if obj.RotationAxis == 'X': # FreeCAD.ActiveDocument.getObject(base.Name).Placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0),FreeCAD.Rotation(FreeCAD.Vector(1,0,0), initIdx)) # else: # FreeCAD.ActiveDocument.getObject(base.Name).Placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0),FreeCAD.Rotation(FreeCAD.Vector(0,1,0), initIdx)) # Prepare global holdpoint container if self.holdPoint is None: self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) if self.layerEndPnt is None: self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf")) if obj.ScanType == 'Rotational': # Remove extended material from Stock and re-assign bb if hasattr(parentJob.Stock, 'ExtXneg'): parentJob.Stock.ExtXneg = 0 parentJob.Stock.ExtXpos = 0 parentJob.Stock.ExtYneg = 0 parentJob.Stock.ExtYpos = 0 parentJob.Stock.ExtZneg = 0 parentJob.Stock.ExtZpos = 0 # Avoid division by zero in rotational scan calculations if obj.FinalDepth.Value <= 0.0: zero = obj.SampleInterval # 0.00001 self.FinalDepth = zero obj.FinalDepth.Value = 0.0 else: self.FinalDepth = obj.FinalDepth.Value # Determine boundbox radius based upon xzy limits data if math.fabs(bb.ZMin) > math.fabs(bb.ZMax): vlim = bb.ZMin else: vlim = bb.ZMax if obj.RotationAxis == 'X': # Rotation is around X-axis, cutter moves along same axis if math.fabs(bb.YMin) > math.fabs(bb.YMax): hlim = bb.YMin else: hlim = bb.YMax else: # Rotation is around Y-axis, cutter moves along same axis if math.fabs(bb.XMin) > math.fabs(bb.XMax): hlim = bb.XMin else: hlim = bb.XMax # Compute max radius of stock, as it rotates, and rotational clearance & safe heights self.bbRadius = math.sqrt(hlim**2 + vlim**2) self.clearHeight = self.bbRadius + parentJob.SetupSheet.ClearanceHeightOffset.Value self.safeHeight = self.bbRadius + parentJob.SetupSheet.ClearanceHeightOffset.Value final = self._rotationalDropCutterOp(obj, stl, bb) elif obj.ScanType == 'Planar': final = self._planarDropCutOp(obj, stl, bb) # End IF # Send final list of commands to operation object self.commandlist.extend(final) # End IF self.endTime = time.time() self.reportThis("OPERATION time: " + str(self.endTime - self.startTime) + " sec.") print(self.opReport) def _planarDropCutOp(self, obj, stl, bb): # t_before = time.time() pntsPerLine = 0 ignoreWasteFlag = obj.IgnoreWaste ignoreMap = [1] def createTopoMap(scanCLP, ignoreDepth): topoMap = [] for pt in scanCLP: if pt.z < ignoreDepth: topoMap.append(0) else: topoMap.append(2) return topoMap # Convert linear list of points to multi-dimensional list def listToMultiDimensional(points, nL, pPL): multiDim = [] for L in range(0, nL): multiDim.append([]) for P in range(0, pPL): pi = L * pPL + P multiDim[L].append(points[pi]) return multiDim # De-buffer multi dimensional list def debufferMultiDimenList(multi): multi.pop(0) multi.pop() for i in range(0, len(multi)): multi[i].pop(0) multi[i].pop() return multi # Convert multi-dimensional list to linear list of points def multiDimensionalToList(multi): points = [] for L in multi: points.extend(L) return points # Prepare global holdpoint container if self.holdPoint is None: self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) # Set crop cutter extra offset cdeoX = obj.DropCutterExtraOffset.x cdeoY = obj.DropCutterExtraOffset.y # the max and min XY area of the operation xmin = bb.XMin - cdeoX xmax = bb.XMax + cdeoX ymin = bb.YMin - cdeoY ymax = bb.YMax + cdeoY # Compute number and size of stepdowns, and final depth if obj.LayerMode == 'Single-pass': depthparams = [obj.FinalDepth.Value] else: dep_par = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) depthparams = [i for i in dep_par] # self.reportThis("--depthparams:" + str(depthparams)) lenDP = len(depthparams) prevDepth = depthparams[0] # Determine bounding box length bbLength = bb.YLength if obj.DropCutterDir == 'Y': bbLength = bb.XLength # Determine number of lines for OCL scan if obj.DropCutterDir == 'X': exOff = obj.DropCutterExtraOffset.y else: exOff = obj.DropCutterExtraOffset.x numLines = int(math.ceil((bbLength + (2 * exOff)) / self.cutOut)) # Number of lines # Scan the piece to depth scanCLP = self._planarDropCutScan(obj, stl, bbLength, xmin, ymin, xmax, ymax, depthparams[lenDP - 1], numLines, self.cutOut) # Apply depth offset if obj.DepthOffset.Value != 0: self.reportThis("--Applying DepthOffset") for pt in range(0, len(scanCLP)): scanCLP[pt].z += obj.DepthOffset.Value numPts = len(scanCLP) pntsPerLine = numPts / numLines # self.reportThis("--points: " + str(numPts)) # self.reportThis("--lines: " + str(numLines)) # self.reportThis("--pntsPerLine: " + str(pntsPerLine)) if math.ceil(pntsPerLine) != math.floor(pntsPerLine): pntsPerLine = None # Create topo map for ignoring waste material if ignoreWasteFlag is True: topoMap = createTopoMap(scanCLP, obj.IgnoreWasteDepth) self.topoMap = listToMultiDimensional(topoMap, numLines, pntsPerLine) rtnA = self._bufferTopoMap(numLines, pntsPerLine) rtnB = self._highlightWaterline(4, 1) self.topoMap = debufferMultiDimenList(self.topoMap) ignoreMap = multiDimensionalToList(self.topoMap) # Extract layers per depthparams for lyr in range(0, lenDP): # Convert current layer data to gcode self._planarScanToGcode(obj, lyr, prevDepth, depthparams[lyr], scanCLP, pntsPerLine, ignoreMap) prevDepth = depthparams[lyr] commands = self._processPlanarHolds(obj, scanCLP) # self.reportThis("--Elapsed time after processing gcode holds is " + str(time.time() - t_before) + " s") # self.keepTime return commands def _planarDropCutScan(self, obj, stl, bbLength, xmin, ymin, xmax, ymax, fd, Nl, cOut): t_before = time.time() def cutPatternLine(obj, n, p1, p2): if obj.CutPattern == 'ZigZag': if (n % 2 == 0.0): # even lo = ocl.Line(p1, p2) # line-object else: # odd lo = ocl.Line(p2, p1) # line-object elif obj.CutPattern == 'Line': if obj.CutMode == 'Conventional': lo = ocl.Line(p1, p2) # line-object else: # odd lo = ocl.Line(p2, p1) # line-object return lo pdc = ocl.PathDropCutter() # create a pdc pdc.setSTL(stl) pdc.setCutter(self.cutter) pdc.setZ(fd) # set minimumZ (final / target depth value) pdc.setSampling(obj.SampleInterval) path = ocl.Path() # create an empty path object if obj.DropCutterDir == 'X': # add Line objects to the path in this loop for n in range(0, Nl): if n == Nl - 1: if obj.StepOver > 50: cOut = (self.cutter.getDiameter() / 2) y = ymax - cOut else: y = ymin - (self.cutter.getDiameter() / 2) + ((n + 1) * cOut) # all lines are offest by 1/2 cutter diameter p1 = ocl.Point(xmin, y, 0) # start-point of line p2 = ocl.Point(xmax, y, 0) # end-point of line lo = cutPatternLine(obj, n, p1, p2) path.append(lo) # add the line to the path else: # add Line objects to the path in this loop for n in range(0, Nl): if n == Nl - 1: if obj.StepOver > 50: cOut = (self.cutter.getDiameter() / 2) x = xmax - cOut else: x = xmin - (self.cutter.getDiameter() / 2) + ((n + 1) * cOut) # all lines are offest by 1/2 cutter diameter p1 = ocl.Point(x, ymin, 0) # start-point of line p2 = ocl.Point(x, ymax, 0) # end-point of line lo = cutPatternLine(obj, n, p1, p2) path.append(lo) # add the line to the path pdc.setPath(path) # run drop-cutter on the path pdc.run() self.reportThis("--OCL scan took " + str(time.time() - t_before) + " s") clp = pdc.getCLPoints() # return the list the points return clp def _planarScanToGcode(self, obj, lc, prvDep, layDep, CLP, pntsPerLine, ignoreMap): output = [] optimize = obj.Optimize ignWF = obj.IgnoreWaste lenCLP = len(CLP) lastCLP = len(CLP) - 1 holdStart = False holdStop = False holdCount = 0 holdLine = 0 onLine = False lineOfTravel = "X" pointsOnLine = 0 zMin = prvDep zMax = prvDep prcs = False prcsCnt = 0 pntCount = 0 rowCount = 1 begLayCmds = ["aaa", "bbb"] minIgnVal = 1 def makePnt(pnt): p = ocl.Point(float("inf"), float("inf"), float("inf")) p.x = pnt.x p.y = pnt.y p.z = pnt.z return p def beginLayerCommand(pnt, lc): # Send cutter to starting position(first point) begcmd = [] begcmd.append(Path.Command('N (Beginning of layer ' + str(lc) + ')', {})) begcmd.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) begcmd.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) begcmd.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) begcmd.reverse() return begcmd # Create containers for x,y,z points prev = ocl.Point(float("inf"), float("inf"), float("inf")) nxt = ocl.Point(float("inf"), float("inf"), float("inf")) pnt = ocl.Point(float("inf"), float("inf"), float("inf")) travVect = ocl.Point(float("inf"), float("inf"), float("inf")) # Determine if releasing model from ignore waste areas if obj.ReleaseFromWaste == True: minIgnVal = 0 # Set values for first gcode point in layer pnt.x = CLP[0].x pnt.y = CLP[0].y pnt.z = CLP[0].z if CLP[0].z < layDep: pnt.z = layDep # generate the path commands # Begin processing ocl points list into gcode for i in range(0, lenCLP): # Calculate next point for consideration of next point if i < lastCLP: nxt.x = CLP[i + 1].x nxt.y = CLP[i + 1].y if CLP[i + 1].z < layDep: nxt.z = layDep else: nxt.z = CLP[i + 1].z else: optimize = False pntCount += 1 if pntCount == 1: # PathLog.debug("--Start row: " + str(rowCount)) nn = 1 elif pntCount == pntsPerLine: # PathLog.debug("--End row: " + str(rowCount)) pntCount = 0 rowCount += 1 # Add rise to clear height before beginning next row in CutPattern: Line # if obj.CutPattern == 'Line': # output.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) # determine vector direction for each axis if nxt.x == pnt.x: travVect.x = 0 elif nxt.x < pnt.x: travVect.x = -1 else: travVect.x = 1 if nxt.y == pnt.y: travVect.y = 0 elif nxt.y < pnt.y: travVect.y = -1 else: travVect.y = 1 # Determine cutter line of travel if travVect.x == 0 and travVect.y != 0: lineOfTravel = "Y" elif travVect.y == 0 and travVect.x != 0: lineOfTravel = "X" else: lineOfTravel = "O" # used for turns # determine if lineOfTravel is same as obj.DropCutterDir line if onLine is False: if lineOfTravel == obj.DropCutterDir: onLine = True self.lineCNT += 1 # increment line count pointsOnLine += 1 else: if lineOfTravel != obj.DropCutterDir: if self.onHold is False: zMax = prvDep onLine = False pointsOnLine = 0 else: pointsOnLine += 1 # Ignore waste operation triggers if ignWF is False: prcs = True else: # ignWF is TRUE if obj.LayerMode == 'Single-pass': if ignoreMap[i] > minIgnVal: if ignoreMap[i] == 1: pnt.z = obj.IgnoreWasteDepth if prcs is False: # Move cutter to current xy position output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) prcs = True else: if prcs is True: # Raise cutter to safe height output.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) prcs = False else: # Multi-pass mode if ignoreMap[i] > minIgnVal: if prcs is False: # Move cutter to current xy position output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) prcs = True else: prcs = False # End of ignWF if prcs is True: prcsCnt += 1 if prcsCnt == 1: begLayCmds = beginLayerCommand(pnt, lc) # start layer at this point if obj.LayerMode == 'Multi-pass': # if z travels above previous layer, start/continue hold high cycle if pnt.z > prvDep: if self.onHold is False: holdStart = True self.onHold = True # Update zMin and zMax values if pnt.z < zMin: zMin = pnt.z if pnt.z > zMax: zMax = pnt.z if self.onHold is True: if holdStart is True: # go to current coordinate output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) # Save holdStart coordinate and prvDep values self.holdPoint.x = pnt.x self.holdPoint.y = pnt.y self.holdPoint.z = pnt.z holdCount += 1 # Increment hold count holdLine = self.lineCNT # remember holdLine self.holdStartPnts.append(makePnt(pnt)) self.holdPrevLayerVals.append(prvDep) holdStart = False # cancel holdStart # hold cutter high until Z value drops below prvDep if pnt.z <= prvDep: holdStop = True # End of onHold if holdStop is True: # Send hold and current points to if holdLine == self.lineCNT: # if start and stop points on same line, process has simple hold self.holdStartPnts.pop() self.holdPrevLayerVals.pop() zMax += 2.0 for cmd in self.holdStopCmds(obj, zMax, prvDep, pnt, "Hold Stop: in-line"): output.append(cmd) else: # if start and stop points on different lines, process has complex hold self.holdStopPnts.append(makePnt(pnt)) self.holdZMaxVals.append(zMax) self.holdStopTypes.append("Mid") output.append("HD") # add placeholder for processing of hold self.holdPntCnt += 1 # reset necessary hold related settings zMax = prvDep holdStop = False self.onHold = False self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) # End of holdStop if self.onHold is False: if not optimize or not self.isPointOnLine(FreeCAD.Vector(prev.x, prev.y, prev.z), FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)): output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) elif i == lastCLP: output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) # Rotate point data prev.x = pnt.x prev.y = pnt.y prev.z = pnt.z # End of prcs pnt.x = nxt.x pnt.y = nxt.y pnt.z = nxt.z # save current layer end point if self.onHold is True: if holdLine == self.lineCNT: self.holdStartPnts.pop() self.holdPrevLayerVals.pop() for cmd in self.holdStopCmds(obj, obj.SafeHeight.Value, obj.SafeHeight.Value, pnt, "Hold Stop: Layer endpoint online"): # zMax as prvDep removes drop to prvDep output.append(cmd) else: self.holdStopPnts.append(makePnt(pnt)) self.holdZMaxVals.append(zMax) output.append("HD") # add placeholder for processing of hold self.holdStopTypes.append("End") # tag hold type self.holdPntCnt += 1 self.onHold = False # save last point for insertion into next layer CLP as start point endPnt = pnt endPnt.z = obj.OpStockZMax.Value + obj.DepthOffset.Value self.layerEndPnt = endPnt # self.reportThis("----Points after linear optimization: " + str(len(output))) # append layer commands to operation command list for cmd in begLayCmds: output.insert(0, cmd) for o in output: self.gcodeCmds.append(o) self.gcodeCmds.append(Path.Command('N (End of layer ' + str(lc) + ')', {})) # return output def _processPlanarHolds(self, obj, scanCLP): # Process all HOLDs in gcode command list self.keepTime = time.time() hldcnt = 0 hpCmds = [] lenHP = len(self.holdStartPnts) commands = [Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})] self.reportThis("--Processing " + str(lenHP) + " HOLD optimizations---") # cycle throug hold points if lenHP > 0: hdCnt = 0 lenGC = len(self.gcodeCmds) for idx in range(0, lenGC): if self.gcodeCmds[idx] == "HD": hdCnt += 1 # process hold here p1 = self.holdStartPnts.pop(0) p2 = self.holdStopPnts.pop(0) pd = self.holdPrevLayerVals.pop(0) hscType = self.holdStopTypes.pop(0) if hscType == "End": N = None # Create gcode commands to connect p1 and p2 hpCmds = self.holdStopEndCmds(obj, p2, "Hold Stop: End point") elif hscType == "Mid": # Set the max and min XY boundaries of the HOLD connection operation cutterClearance = self.cutter.getDiameter() / 1.25 if p1.x < p2.x: xmin = p1.x - cutterClearance xmax = p2.x + cutterClearance else: xmin = p2.x - cutterClearance xmax = p1.x + cutterClearance if p1.y < p2.y: ymin = p1.y - cutterClearance ymax = p2.y + cutterClearance else: ymin = p2.y - cutterClearance ymax = p1.y + cutterClearance # get focused list of points based on bound box with p1 and p2 as corners, with cutter diam. as additional buffer subCLP = self.subsectionCLP(scanCLP, xmin, ymin, xmax, ymax) # Determine max z height for clearance between p1 and p2 zMax = self.getMaxHeight(self.targetDepth, p1, p2, self.cutter.getDiameter(), subCLP) # Create gcode commands to connect p1 and p2 hpCmds = self.holdStopCmds(obj, zMax, pd, p2, "Hold Stop: Group processed") # Add commands to list for cmd in hpCmds: commands.append(cmd) hldcnt += 1 else: commands.append(self.gcodeCmds[idx]) else: commands = self.gcodeCmds return commands def _rotationalDropCutterOp(self, obj, stl, bb): eT = time.time() self.resetTolerance = 0.0000001 # degrees self.layerEndzMax = 0.0 commands = [] scanLines = [] advances = [] iSTG = [] rSTG = [] rings = [] lCnt = 0 rNum = 0 stepDeg = 1.1 layCircum = 1.1 begIdx = 0.0 endIdx = 0.0 arc = 0.0 sumAdv = 0.0 bbRad = self.bbRadius def invertAdvances(advances): idxs = [1.1] for adv in advances: idxs.append(-1 * adv) idxs.pop(0) return idxs def linesToPointRings(scanLines): rngs = [] numPnts = len(scanLines[0]) # Number of points per line along axis, at obj.SampleInterval spacing for line in scanLines: # extract circular set(ring) of points from scan lines if len(line) != numPnts: PathLog.debug("Error: line lengths not equal") return rngs for num in range(0, numPnts): rngs.append([1.1]) # Initiate new ring for line in scanLines: # extract circular set(ring) of points from scan lines rngs[num].append(line[num]) nn = rngs[num].pop(0) return rngs def indexAdvances(arc, stepDeg): indexes = [0.0] numSteps = int(math.floor(arc / stepDeg)) for ns in range(0, numSteps): indexes.append(stepDeg) travel = sum(indexes) if arc == 360.0: indexes.insert(0, 0.0) else: indexes.append(arc - travel) return indexes # Compute number and size of stepdowns, and final depth if obj.LayerMode == 'Single-pass': depthparams = [self.FinalDepth] else: dep_par = PathUtils.depth_params(self.clearHeight, self.safeHeight, self.bbRadius, obj.StepDown.Value, 0.0, self.FinalDepth) depthparams = [i for i in dep_par] prevDepth = depthparams[0] lenDP = len(depthparams) # Set drop cutter extra offset cdeoX = obj.DropCutterExtraOffset.x cdeoY = obj.DropCutterExtraOffset.y # Set updated bound box values and redefine the new min/mas XY area of the operation based on greatest point radius of model bb.ZMin = -1 * bbRad bb.ZMax = bbRad if obj.RotationAxis == 'X': bb.YMin = -1 * bbRad bb.YMax = bbRad ymin = 0.0 ymax = 0.0 xmin = bb.XMin - cdeoX xmax = bb.XMax + cdeoX else: bb.XMin = -1 * bbRad bb.XMax = bbRad ymin = bb.YMin - cdeoY ymax = bb.YMax + cdeoY xmin = 0.0 xmax = 0.0 # Calculate arc begIdx = obj.StartIndex endIdx = obj.StopIndex if endIdx < begIdx: begIdx -= 360.0 arc = endIdx - begIdx # Begin gcode operation with raising cutter to safe height commands.append(Path.Command('G0', {'Z': self.safeHeight, 'F': self.vertRapid})) # Complete rotational scans at layer and translate into gcode for layDep in depthparams: t_before = time.time() self.reportThis("--layDep " + str(layDep)) # Compute circumference and step angles for current layer layCircum = 2 * math.pi * layDep if lenDP == 1: layCircum = 2 * math.pi * bbRad # Set axial feed rates self.axialFeed = 360 / layCircum * self.horizFeed self.axialRapid = 360 / layCircum * self.horizRapid # Determine step angle. if obj.RotationAxis == obj.DropCutterDir: # Same == indexed stepDeg = (self.cutOut / layCircum) * 360.0 else: stepDeg = (obj.SampleInterval / layCircum) * 360.0 # Limit step angle and determine rotational index angles [indexes]. if stepDeg > 120.0: stepDeg = 120.0 advances = indexAdvances(arc, stepDeg) # Reset for each step down layer # Perform rotational indexed scans to layer depth if obj.RotationAxis == obj.DropCutterDir: # Same == indexed OR parallel sample = obj.SampleInterval else: sample = self.cutOut scanLines = self._indexedDropCutScan(obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample) # Complete rotation if necessary if arc == 360.0: advances.append(360.0 - sum(advances)) advances.pop(0) zero = scanLines.pop(0) scanLines.append(zero) # Translate OCL scans into gcode if obj.RotationAxis == obj.DropCutterDir: # Same == indexed (cutter runs parallel to axis) # Invert advances if RotationAxis == Y if obj.RotationAxis == 'Y': advances = invertAdvances(advances) # Translate scan to gcode # sumAdv = 0.0 sumAdv = begIdx for sl in range(0, len(scanLines)): sumAdv += advances[sl] # Translate scan to gcode iSTG = self._indexedScanToGcode(obj, sl, scanLines[sl], sumAdv, prevDepth, layDep, lenDP) commands.extend(iSTG) # Add rise to clear height before beginning next index in CutPattern: Line # if obj.CutPattern == 'Line': # commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) # Raise cutter to safe height after each index cut commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) # Eol else: if obj.CutMode == 'Conventional': advances = invertAdvances(advances) rt = advances.reverse() rtn = scanLines.reverse() # Invert advances if RotationAxis == Y if obj.RotationAxis == 'Y': advances = invertAdvances(advances) # Begin gcode operation with raising cutter to safe height commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) # Convert rotational scans into gcode rings = linesToPointRings(scanLines) rNum = 0 for rng in rings: rSTG = self._rotationalScanToGcode(obj, rng, rNum, prevDepth, layDep, advances) commands.extend(rSTG) if arc != 360.0: clrZ = self.layerEndzMax + self.SafeHeightOffset commands.append(Path.Command('G0', {'Z': clrZ, 'F': self.vertRapid})) rNum += 1 # Eol # Add rise to clear height before beginning next index in CutPattern: Line # if obj.CutPattern == 'Line': # commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) prevDepth = layDep lCnt += 1 # increment layer count self.reportThis("--Layer " + str(lCnt) + ": " + str(len(advances)) + " OCL scans and gcode in " + str(time.time() - t_before) + " s") time.sleep(0.2) # Eol return commands def _indexedDropCutScan(self, obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample): cutterOfst = 0.0 radsRot = 0.0 reset = 0.0 iCnt = 0 Lines = [] result = None pdc = ocl.PathDropCutter() # create a pdc pdc.setCutter(self.cutter) pdc.setZ(layDep) # set minimumZ (final / ta9rget depth value) pdc.setSampling(sample) # if self.useTiltCutter == True: if obj.CutterTilt != 0.0: cutterOfst = layDep * math.sin(obj.CutterTilt * math.pi / 180.0) self.reportThis("CutterTilt: cutterOfst is " + str(cutterOfst)) sumAdv = 0.0 for adv in advances: sumAdv += adv if adv > 0.0: # Rotate STL object using OCL method radsRot = math.radians(adv) if obj.RotationAxis == 'X': rStl = stl.rotate(radsRot, 0.0, 0.0) else: rStl = stl.rotate(0.0, radsRot, 0.0) # Set STL after rotation is made pdc.setSTL(stl) # add Line objects to the path in this loop if obj.RotationAxis == 'X': p1 = ocl.Point(xmin, cutterOfst, 0.0) # start-point of line p2 = ocl.Point(xmax, cutterOfst, 0.0) # end-point of line else: p1 = ocl.Point(cutterOfst, ymin, 0.0) # start-point of line p2 = ocl.Point(cutterOfst, ymax, 0.0) # end-point of line # Create line object if obj.RotationAxis == obj.DropCutterDir: # parallel cut if obj.CutPattern == 'ZigZag': if (iCnt % 2 == 0.0): # even lo = ocl.Line(p1, p2) else: # odd lo = ocl.Line(p2, p1) elif obj.CutPattern == 'Line': if obj.CutMode == 'Conventional': lo = ocl.Line(p1, p2) else: lo = ocl.Line(p2, p1) else: lo = ocl.Line(p1, p2) # line-object path = ocl.Path() # create an empty path object path.append(lo) # add the line to the path pdc.setPath(path) # set path pdc.run() # run drop-cutter on the path result = pdc.getCLPoints() Lines.append(result) # request the list of points iCnt += 1 # End loop # Rotate STL object back to original position using OCL method reset = -1 * math.radians(sumAdv - self.resetTolerance) if obj.RotationAxis == 'X': rStl = stl.rotate(reset, 0.0, 0.0) else: rStl = stl.rotate(0.0, reset, 0.0) self.resetTolerance = 0.0 return Lines def _indexedScanToGcode(self, obj, li, CLP, idxAng, prvDep, layerDepth, numDeps): # generate the path commands output = [] optimize = obj.Optimize holdCount = 0 holdStart = False holdStop = False zMax = prvDep lenCLP = len(CLP) lastCLP = lenCLP - 1 prev = ocl.Point(float("inf"), float("inf"), float("inf")) nxt = ocl.Point(float("inf"), float("inf"), float("inf")) pnt = ocl.Point(float("inf"), float("inf"), float("inf")) # Create frist point pnt.x = CLP[0].x pnt.y = CLP[0].y pnt.z = CLP[0].z + float(obj.DepthOffset.Value) # Rotate to correct index location if obj.RotationAxis == 'X': output.append(Path.Command('G0', {'A': idxAng, 'F': self.axialFeed})) else: output.append(Path.Command('G0', {'B': idxAng, 'F': self.axialFeed})) if li > 0: if pnt.z > self.layerEndPnt.z: clrZ = pnt.z + 2.0 output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid})) else: output.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) for i in range(0, lenCLP): if i < lastCLP: nxt.x = CLP[i + 1].x nxt.y = CLP[i + 1].y nxt.z = CLP[i + 1].z + float(obj.DepthOffset.Value) else: optimize = False # Update zMax values if pnt.z > zMax: zMax = pnt.z if obj.LayerMode == 'Multi-pass': # if z travels above previous layer, start/continue hold high cycle if pnt.z > prvDep and optimize is True: if self.onHold is False: holdStart = True self.onHold = True if self.onHold is True: if holdStart is True: # go to current coordinate output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) # Save holdStart coordinate and prvDep values self.holdPoint.x = pnt.x self.holdPoint.y = pnt.y self.holdPoint.z = pnt.z holdCount += 1 # Increment hold count holdStart = False # cancel holdStart # hold cutter high until Z value drops below prvDep if pnt.z <= prvDep: holdStop = True if holdStop is True: # Send hold and current points to zMax += 2.0 for cmd in self.holdStopCmds(obj, zMax, prvDep, pnt, "Hold Stop: in-line"): output.append(cmd) # reset necessary hold related settings zMax = prvDep holdStop = False self.onHold = False self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) if self.onHold is False: if not optimize or not self.isPointOnLine(FreeCAD.Vector(prev.x, prev.y, prev.z), FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)): output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) elif i == lastCLP: output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) # Rotate point data prev.x = pnt.x prev.y = pnt.y prev.z = pnt.z pnt.x = nxt.x pnt.y = nxt.y pnt.z = nxt.z # self.reportThis("points after optimization: " + str(len(output))) output.append(Path.Command('N (End index angle ' + str(round(idxAng, 4)) + ')', {})) # Save layer end point for use in transitioning to next layer self.layerEndPnt.x = pnt.x self.layerEndPnt.y = pnt.y self.layerEndPnt.z = pnt.z return output def _rotationalScanToGcode(self, obj, RNG, rN, prvDep, layDep, advances): # generate the path commands output = [] nxtAng = 0 zMax = 0.0 prev = ocl.Point(float("inf"), float("inf"), float("inf")) nxt = ocl.Point(float("inf"), float("inf"), float("inf")) pnt = ocl.Point(float("inf"), float("inf"), float("inf")) begIdx = obj.StartIndex endIdx = obj.StopIndex if endIdx < begIdx: begIdx -= 360.0 # Rotate to correct index location axisOfRot = 'A' if obj.RotationAxis == 'Y': axisOfRot = 'B' # Create frist point ang = 0.0 + obj.CutterTilt pnt.x = RNG[0].x pnt.y = RNG[0].y pnt.z = RNG[0].z + float(obj.DepthOffset.Value) # Adjust feed rate based on radius/circumferance of cutter. # Original feed rate based on travel at circumferance. if rN > 0: # if pnt.z > self.layerEndPnt.z: if pnt.z >= self.layerEndzMax: clrZ = pnt.z + 5.0 output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid})) else: output.append(Path.Command('G1', {'Z': self.clearHeight, 'F': self.vertRapid})) output.append(Path.Command('G0', {axisOfRot: ang, 'F': self.axialFeed})) output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.axialFeed})) output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.axialFeed})) lenRNG = len(RNG) lastIdx = lenRNG - 1 for i in range(0, lenRNG): if i < lastIdx: nxtAng = ang + advances[i + 1] nxt.x = RNG[i + 1].x nxt.y = RNG[i + 1].y nxt.z = RNG[i + 1].z + float(obj.DepthOffset.Value) if pnt.z > zMax: zMax = pnt.z output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, axisOfRot: ang, 'F': self.axialFeed})) pnt.x = nxt.x pnt.y = nxt.y pnt.z = nxt.z ang = nxtAng # self.reportThis("points after optimization: " + str(len(output))) # Save layer end point for use in transitioning to next layer self.layerEndPnt.x = RNG[0].x self.layerEndPnt.y = RNG[0].y self.layerEndPnt.z = RNG[0].z self.layerEndIdx = ang self.layerEndzMax = zMax # Move cutter to final point # output.append(Path.Command('G1', {'X': self.layerEndPnt.x, 'Y': self.layerEndPnt.y, 'Z': self.layerEndPnt.z, axisOfRot: endang, 'F': self.axialFeed})) return output def _waterlineOp(self, obj, stl, bb): t_begin = time.time() # self.keepTime = time.time() commands = [] # Prepare global holdpoint and layerEndPnt containers if self.holdPoint is None: self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) if self.layerEndPnt is None: self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf")) # Set extra offset to diameter of cutter to allow cutter to move around perimeter of model # Need to make DropCutterExtraOffset available for waterline algorithm # cdeoX = obj.DropCutterExtraOffset.x # cdeoY = obj.DropCutterExtraOffset.y cdeoX = 0.6 * self.cutter.getDiameter() cdeoY = 0.6 * self.cutter.getDiameter() # the max and min XY area of the operation xmin = bb.XMin - cdeoX xmax = bb.XMax + cdeoX ymin = bb.YMin - cdeoY ymax = bb.YMax + cdeoY smplInt = obj.SampleInterval minSampInt = 0.001 # value is mm if smplInt < minSampInt: smplInt = minSampInt # Determine bounding box length for the OCL scan bbLength = math.fabs(ymax - ymin) numScanLines = int(math.ceil(bbLength / smplInt) + 1) # Number of lines # Compute number and size of stepdowns, and final depth if obj.LayerMode == 'Single-pass': depthparams = [obj.FinalDepth.Value] else: dep_par = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) depthparams = [dp for dp in dep_par] lenDP = len(depthparams) # Scan the piece to depth at smplInt oclScan = [] oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines) lenOS = len(oclScan) ptPrLn = int(lenOS / numScanLines) # Convert oclScan list of points to multi-dimensional list scanLines = [] for L in range(0, numScanLines): scanLines.append([]) for P in range(0, ptPrLn): pi = L * ptPrLn + P scanLines[L].append(oclScan[pi]) lenSL = len(scanLines) pntsPerLine = len(scanLines[0]) self.reportThis("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line") self.reportThis("--Setup, OCL scan, and scan conversion to multi-dimen. list took " + str(time.time() - t_begin) + " s") # Extract Wl layers per depthparams lyr = 0 cmds = [] layTime = time.time() self.topoMap = [] for layDep in depthparams: cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) commands.extend(cmds) lyr += 1 self.reportThis("--All layer scans combined took " + str(time.time() - layTime) + " s") return commands def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines): pdc = ocl.PathDropCutter() # create a pdc pdc.setSTL(stl) pdc.setCutter(self.cutter) pdc.setZ(fd) # set minimumZ (final / target depth value) pdc.setSampling(smplInt) # Create line object as path path = ocl.Path() # create an empty path object for nSL in range(0, numScanLines): yVal = ymin + (nSL * smplInt) p1 = ocl.Point(xmin, yVal, fd) # start-point of line p2 = ocl.Point(xmax, yVal, fd) # end-point of line l = ocl.Line(p1, p2) # line-object path.append(l) # add the line to the path pdc.setPath(path) pdc.run() # run drop-cutter on the path # return the list the points return pdc.getCLPoints() def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine): commands = [] cmds = [] loopList = [] self.topoMap = [] # Create topo map from scanLines (highs and lows) self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine) # Add buffer lines and columns to topo map rtn = self._bufferTopoMap(lenSL, pntsPerLine) # Identify layer waterline from OCL scan rtn = self._highlightWaterline(4, 9) # Extract waterline and convert to gcode loopList = self._extractWaterlines(obj, scanLines, lyr, layDep) time.sleep(0.1) # save commands for loop in loopList: cmds = self._loopToGcode(obj, layDep, loop) commands.extend(cmds) return commands def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine): topoMap = [] for L in range(0, lenSL): topoMap.append([]) for P in range(0, pntsPerLine): if scanLines[L][P].z > layDep: topoMap[L].append(2) else: topoMap[L].append(0) return topoMap def _bufferTopoMap(self, lenSL, pntsPerLine): # add buffer boarder of zeros to all sides to topoMap data pre = [0, 0] post = [0, 0] for p in range(0, pntsPerLine): pre.append(0) post.append(0) for l in range(0, lenSL): self.topoMap[l].insert(0, 0) self.topoMap[l].append(0) self.topoMap.insert(0, pre) self.topoMap.append(post) return True def _highlightWaterline(self, extraMaterial, insCorn): TM = self.topoMap lastPnt = len(TM[1]) - 1 lastLn = len(TM) - 1 highFlag = 0 # self.reportThis("--Convert parallel data to ridges") for lin in range(1, lastLn): for pt in range(1, lastPnt): # Ignore first and last points if TM[lin][pt] == 0: if TM[lin][pt + 1] == 2: # step up TM[lin][pt] = 1 if TM[lin][pt - 1] == 2: # step down TM[lin][pt] = 1 # self.reportThis("--Convert perpendicular data to ridges and highlight ridges") for pt in range(1, lastPnt): # Ignore first and last points for lin in range(1, lastLn): if TM[lin][pt] == 0: highFlag = 0 if TM[lin + 1][pt] == 2: # step up TM[lin][pt] = 1 if TM[lin - 1][pt] == 2: # step down TM[lin][pt] = 1 elif TM[lin][pt] == 2: highFlag += 1 if highFlag == 3: if TM[lin - 1][pt - 1] < 2 or TM[lin - 1][pt + 1] < 2: highFlag = 2 else: TM[lin - 1][pt] = extraMaterial highFlag = 2 # Square corners # self.reportThis("--Square corners") for pt in range(1, lastPnt): for lin in range(1, lastLn): if TM[lin][pt] == 1: # point == 1 cont = True if TM[lin + 1][pt] == 0: # forward == 0 if TM[lin + 1][pt - 1] == 1: # forward left == 1 if TM[lin][pt - 1] == 2: # left == 2 TM[lin + 1][pt] = 1 # square the corner cont = False if cont is True and TM[lin + 1][pt + 1] == 1: # forward right == 1 if TM[lin][pt + 1] == 2: # right == 2 TM[lin + 1][pt] = 1 # square the corner cont = True if TM[lin - 1][pt] == 0: # back == 0 if TM[lin - 1][pt - 1] == 1: # back left == 1 if TM[lin][pt - 1] == 2: # left == 2 TM[lin - 1][pt] = 1 # square the corner cont = False if cont is True and TM[lin - 1][pt + 1] == 1: # back right == 1 if TM[lin][pt + 1] == 2: # right == 2 TM[lin - 1][pt] = 1 # square the corner # remove inside corners # self.reportThis("--Remove inside corners") for pt in range(1, lastPnt): for lin in range(1, lastLn): if TM[lin][pt] == 1: # point == 1 if TM[lin][pt + 1] == 1: if TM[lin - 1][pt + 1] == 1 or TM[lin + 1][pt + 1] == 1: TM[lin][pt + 1] = insCorn elif TM[lin][pt - 1] == 1: if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1: TM[lin][pt - 1] = insCorn # PathLog.debug("\n-------------") # for li in TM: # PathLog.debug("Line: " + str(li)) return True def _extractWaterlines(self, obj, oclScan, lyr, layDep): srch = True lastPnt = len(self.topoMap[0]) - 1 lastLn = len(self.topoMap) - 1 maxSrchs = 5 srchCnt = 1 loopList = [] loop = [] loopNum = 0 if obj.CutMode == 'Conventional': lC = [1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0] pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] else: lC = [-1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0] pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] while srch is True: srch = False if srchCnt > maxSrchs: self.reportThis("Max search scans, " + str(maxSrchs) + " reached\nPossible incomplete waterline result!") break for L in range(1, lastLn): for P in range(1, lastPnt): if self.topoMap[L][P] == 1: # start loop follow srch = True loopNum += 1 loop = self._trackLoop(oclScan, lC, pC, L, P, loopNum) self.topoMap[L][P] = 0 # Mute the starting point loopList.append(loop) srchCnt += 1 # self.reportThis("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.") return loopList def _trackLoop(self, oclScan, lC, pC, L, P, loopNum): loop = [oclScan[L - 1][P - 1]] # Start loop point list cur = [L, P, 1] prv = [L, P - 1, 1] nxt = [L, P + 1, 1] follow = True ptc = 0 ptLmt = 200000 while follow is True: ptc += 1 if ptc > ptLmt: self.reportThis("Loop number " + str(loopNum) + " at [" + str(nxt[0]) + ", " + str(nxt[1]) + "] pnt count exceeds, " + str(ptLmt) + ". Stopped following loop.") break nxt = self._findNextWlPoint(lC, pC, cur[0], cur[1], prv[0], prv[1]) # get next point loop.append(oclScan[nxt[0] - 1][nxt[1] - 1]) # add it to loop point list self.topoMap[nxt[0]][nxt[1]] = nxt[2] # Mute the point, if not Y stem if nxt[0] == L and nxt[1] == P: # check if loop complete follow = False elif nxt[0] == cur[0] and nxt[1] == cur[1]: # check if line cannot be detected follow = False prv = cur cur = nxt return loop def _findNextWlPoint(self, lC, pC, cl, cp, pl, pp): dl = cl - pl dp = cp - pp num = 0 i = 3 s = 0 mtch = 0 found = False while mtch < 8: # check all 8 points around current point if lC[i] == dl: if pC[i] == dp: s = i - 3 found = True # Check for y branch where current point is connection between branches for y in range(1, mtch): if lC[i + y] == dl: if pC[i + y] == dp: num = 1 break break i += 1 mtch += 1 if found is False: # self.reportThis("_findNext: No start point found.") return [cl, cp, num] for r in range(0, 8): l = cl + lC[s + r] p = cp + pC[s + r] if self.topoMap[l][p] == 1: return [l, p, num] # self.reportThis("_findNext: No next pnt found") return [cl, cp, num] def _loopToGcode(self, obj, layDep, loop): # generate the path commands output = [] optimize = obj.Optimize prev = ocl.Point(float("inf"), float("inf"), float("inf")) nxt = ocl.Point(float("inf"), float("inf"), float("inf")) pnt = ocl.Point(float("inf"), float("inf"), float("inf")) # Create frist point pnt.x = loop[0].x pnt.y = loop[0].y pnt.z = layDep # Position cutter to begin loop output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) lenCLP = len(loop) lastIdx = lenCLP - 1 # Cycle through each point on loop for i in range(0, lenCLP): if i < lastIdx: nxt.x = loop[i + 1].x nxt.y = loop[i + 1].y nxt.z = layDep else: optimize = False if not optimize or not self.isPointOnLine(FreeCAD.Vector(prev.x, prev.y, prev.z), FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)): output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizFeed})) # Rotate point data prev.x = pnt.x prev.y = pnt.y prev.z = pnt.z pnt.x = nxt.x pnt.y = nxt.y pnt.z = nxt.z # self.reportThis("points after optimization: " + str(len(output))) # Save layer end point for use in transitioning to next layer self.layerEndPnt.x = pnt.x self.layerEndPnt.y = pnt.y self.layerEndPnt.z = pnt.z return output def isPointOnLine(self, lineA, lineB, pointP): tolerance = 1e-6 vectorAB = lineB - lineA vectorAC = pointP - lineA crossproduct = vectorAB.cross(vectorAC) dotproduct = vectorAB.dot(vectorAC) if crossproduct.Length > tolerance: return False if dotproduct < 0: return False if dotproduct > vectorAB.Length * vectorAB.Length: return False return True def holdStopCmds(self, obj, zMax, pd, p2, txt): cmds = [] msg = 'N (' + txt + ')' cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel cmds.append(Path.Command('G0', {'Z': zMax, 'F': self.vertRapid})) # Raise cutter rapid to zMax in line of travel cmds.append(Path.Command('G0', {'X': p2.x, 'Y': p2.y, 'F': self.horizRapid})) # horizontal rapid to current XY coordinate if zMax != pd: cmds.append(Path.Command('G0', {'Z': pd, 'F': self.vertRapid})) # drop cutter down rapidly to prevDepth depth cmds.append(Path.Command('G0', {'Z': p2.z, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed return cmds def holdStopEndCmds(self, obj, p2, txt): cmds = [] msg = 'N (' + txt + ')' cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel cmds.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) # Raise cutter rapid to zMax in line of travel # cmds.append(Path.Command('G0', {'X': p2.x, 'Y': p2.y, 'F': self.horizRapid})) # horizontal rapid to current XY coordinate return cmds def subsectionCLP(self, CLP, xmin, ymin, xmax, ymax): # This function returns a subsection of the CLP scan, limited to the min/max values supplied section = [] lenCLP = len(CLP) for i in range(0, lenCLP): if CLP[i].x < xmax: if CLP[i].y < ymax: if CLP[i].x > xmin: if CLP[i].y > ymin: section.append(CLP[i]) return section def getMaxHeight(self, finalDepth, p1, p2, cutter, CLP): # This function connects two HOLD points with line # Each point within the subsection point list is tested to determinie if it is under cutter # Points determined to be under the cutter on line are tested for z height # The highest z point is the requirement for clearance between p1 and p2, and returned as zMax with 2 mm extra dx = (p2.x - p1.x) if dx == 0.0: dx = 0.00001 m = (p2.y - p1.y) / dx b = p1.y - (m * p1.x) avoidTool = round(cutter * 0.75, 1) # 1/2 diam. of cutter is theoretically safe, but 3/4 diam is used for extra clearance zMax = finalDepth lenCLP = len(CLP) for i in range(0, lenCLP): mSqrd = m**2 if mSqrd < 0.0000001: mSqrd = 0.0000001 perpDist = math.sqrt((CLP[i].y - (m * CLP[i].x) - b)**2 / (1 + 1 / (mSqrd))) if perpDist < avoidTool: # if point within cutter reach on line of travel, test z height and update as needed if CLP[i].z > zMax: zMax = CLP[i].z return zMax + 2.0 def holdStopPerpCmds(self, obj, zMax, pd, p2, aor, ang, txt): cmds = [] msg = 'N (' + txt + ')' cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel cmds.append(Path.Command('G0', {'Z': zMax, 'F': self.vertRapid})) # Raise cutter rapid to zMax in line of travel cmds.append(Path.Command('G0', {'X': p2.x, 'Y': p2.y, 'F': self.horizRapid})) # horizontal rapid to current XY coordinate if zMax != pd: cmds.append(Path.Command('G0', {'Z': pd, 'F': self.vertRapid})) # drop cutter down rapidly to prevDepth depth cmds.append(Path.Command('G0', {'Z': p2.z, aor: ang, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed return cmds def reportThis(self, txt): self.opReport += "\n" + txt def resetOpVariables(self): # reset operation variables self.opReport = "" self.cutter = None self.holdPoint = None self.stl = None self.layerEndPnt = None self.onHold = False self.useTiltCutter = False self.holdStartPnts = [] self.holdStopPnts = [] self.holdStopTypes = [] self.holdZMaxVals = [] self.holdPrevLayerVals = [] self.gcodeCmds = [] self.CLP = [] self.SafeHeightOffset = 2.0 self.ClearHeightOffset = 4.0 self.layerEndIdx = 0.0 self.layerEndzMax = 0.0 self.resetTolerance = 0.0 self.holdPntCnt = 0 self.startTime = 0.0 self.endTime = 0.0 self.lineCNT = 0 self.keepTime = 0.0 self.lineScanTime = 0.0 self.bbRadius = 0.0 self.targetDepth = 0.0 self.stepDeg = 0.0 self.stepRads = 0.0 self.axialFeed = 0.0 self.axialRapid = 0.0 self.FinalDepth = 0.0 self.cutOut = 0.0 self.clearHeight = 0.0 self.safeHeight = 0.0 return True def setOclCutter(self, obj): # Set cutter details # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details diam_1 = obj.ToolController.Tool.Diameter lenOfst = obj.ToolController.Tool.LengthOffset FR = obj.ToolController.Tool.FlatRadius CEH = obj.ToolController.Tool.CuttingEdgeHeight if obj.ToolController.Tool.ToolType == 'EndMill': # Standard End Mill self.cutter = ocl.CylCutter(diam_1, (CEH + lenOfst)) elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0: # Standard Ball End Mill # OCL -> BallCutter::BallCutter(diameter, length) self.cutter = ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst)) self.useTiltCutter = True elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0: # Bull Nose or Corner Radius cutter # Reference: https://www.fine-tools.com/halbstabfraeser.html # OCL -> BallCutter::BallCutter(diameter, length) self.cutter = ocl.BullCutter(diam_1, FR, (CEH + lenOfst)) elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0: # Bull Nose or Corner Radius cutter # Reference: https://www.fine-tools.com/halbstabfraeser.html # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) self.cutter = ocl.ConeCutter(diam_1, (obj.ToolController.Tool.CuttingEdgeAngle / 2), lenOfst) elif obj.ToolController.Tool.ToolType == 'ChamferMill': # Bull Nose or Corner Radius cutter # Reference: https://www.fine-tools.com/halbstabfraeser.html # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) self.cutter = ocl.ConeCutter(diam_1, (obj.ToolController.Tool.CuttingEdgeAngle / 2), lenOfst) else: # Default to standard end mill self.cutter = ocl.CylCutter(diam_1, (CEH + lenOfst)) PathLog.info("Defaulting cutter to standard end mill.") # http://www.carbidecutter.net/products/carbide-burr-cone-shape-sm.html ''' return "Drill"; return "CenterDrill"; return "CounterSink"; return "CounterBore"; return "FlyCutter"; return "Reamer"; return "Tap"; return "EndMill"; return "SlotCutter"; return "BallEndMill"; return "ChamferMill"; return "CornerRound"; return "Engraver"; return "Undefined"; ''' return True def pocketInvertExtraOffset(self): return True def opSetDefaultValues(self, obj, job): '''opSetDefaultValues(obj, job) ... initialize defaults''' obj.StepOver = 100 obj.Optimize = True obj.IgnoreWaste = False obj.ReleaseFromWaste = False obj.LayerMode = 'Single-pass' obj.ScanType = 'Planar' obj.RotationAxis = 'X' obj.CutMode = 'Conventional' obj.CutPattern = 'ZigZag' obj.CutterTilt = 0.0 obj.StartIndex = 0.0 obj.StopIndex = 360.0 obj.SampleInterval = 1.0 # need to overwrite the default depth calculations for facing job = PathUtils.findParentJob(obj) if job: if job.Stock: d = PathUtils.guessDepths(job.Stock.Shape, None) PathLog.debug("job.Stock exists") else: PathLog.debug("job.Stock NOT exist") else: PathLog.debug("job NOT exist") if self.docRestored is True: # This op is NOT the first in the Operations list PathLog.debug("doc restored") obj.FinalDepth.Value = obj.OpFinalDepth.Value else: PathLog.debug("new operation") obj.OpFinalDepth.Value = d.final_depth obj.OpStartDepth.Value = d.start_depth if self.initOpFinalDepth is None and self.initFinalDepth is None: self.initFinalDepth = d.final_depth self.initOpFinalDepth = d.final_depth else: PathLog.debug("-initFinalDepth" + str(self.initFinalDepth)) PathLog.debug("-initOpFinalDepth" + str(self.initOpFinalDepth)) obj.IgnoreWasteDepth = obj.FinalDepth.Value + 0.001 def SetupProperties(): setup = [] setup.append("Algorithm") setup.append("DropCutterDir") setup.append("BoundBox") setup.append("StepOver") setup.append("DepthOffset") setup.append("LayerMode") setup.append("ScanType") setup.append("RotationAxis") setup.append("CutMode") setup.append("SampleInterval") setup.append("StartIndex") setup.append("StopIndex") setup.append("CutterTilt") setup.append("CutPattern") setup.append("IgnoreWasteDepth") setup.append("IgnoreWaste") setup.append("ReleaseFromWaste") return setup def Create(name, obj=None): '''Create(name) ... Creates and returns a Surface operation.''' if obj is None: obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) proxy = ObjectSurface(obj, name) return obj