diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt
index ded7c91a93..78415e1e32 100644
--- a/src/Mod/Path/CMakeLists.txt
+++ b/src/Mod/Path/CMakeLists.txt
@@ -107,6 +107,7 @@ SET(PathScripts_SRCS
PathScripts/PathStop.py
PathScripts/PathSurface.py
PathScripts/PathSurfaceGui.py
+ PathScripts/PathSurfaceSupport.py
PathScripts/PathToolBit.py
PathScripts/PathToolBitCmd.py
PathScripts/PathToolBitEdit.py
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
index 5e0edef1c9..82533fe061 100644
--- a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
@@ -50,6 +50,9 @@
8
+
+ <html><head/><body><p>Select the overall boundary for the operation.</p></body></html>
+
-
Stock
@@ -70,6 +73,9 @@
0
+
+ <html><head/><body><p>Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.</p></body></html>
+
-
@@ -79,6 +85,9 @@
8
+
+ <html><head/><body><p>Complete the operation in a single pass at depth, or mulitiple passes to final depth.</p></body></html>
+
-
Single-pass
@@ -93,6 +102,9 @@
-
+
+ <html><head/><body><p>Select the algorithm to use: OCL Dropcutter*, or Experimental (Not OCL based).</p></body></html>
+
-
OCL Dropcutter
@@ -107,6 +119,9 @@
-
+
+ <html><head/><body><p>Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.</p></body></html>
+
Optimize Linear Paths
@@ -132,6 +147,9 @@
8
+
+ <html><head/><body><p>Set the geometric clearing pattern to use for the operation.</p></body></html>
+
-
None
@@ -213,6 +231,9 @@
-
+
+ <html><head/><body><p>Set the sampling resolution. Smaller values quickly increase processing time.</p></body></html>
+
mm
@@ -260,8 +281,8 @@
Gui::InputField
- QWidget
-
+ QLineEdit
+
diff --git a/src/Mod/Path/PathScripts/PathSurfaceSupport.py b/src/Mod/Path/PathScripts/PathSurfaceSupport.py
new file mode 100644
index 0000000000..e991b28163
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathSurfaceSupport.py
@@ -0,0 +1,1855 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2020 russ4262 *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+from __future__ import print_function
+
+__title__ = "Path Surface Support Module"
+__author__ = "russ4262 (Russell Johnson)"
+__url__ = "http://www.freecadweb.org"
+__doc__ = "Support functions and classes for 3D Surface and Waterline operations."
+# __name__ = "PathSurfaceSupport"
+__contributors__ = ""
+
+import FreeCAD
+from PySide import QtCore
+import Path
+import PathScripts.PathLog as PathLog
+import PathScripts.PathUtils as PathUtils
+import math
+import Part
+
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+# PathLog.trackModule(PathLog.thisModule())
+
+
+# Qt translation handling
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+
+class PathGeometryGenerator:
+ '''Creates a path geometry shape from an assigned pattern for conversion to tool paths.
+ PathGeometryGenerator(obj, shape, pattern)
+ `obj` is the operation object, `shape` is the horizontal planar shape object,
+ and `pattern` is the name of the geometric pattern to apply.
+ Frist, call the getCenterOfPattern() method for the CenterOfMass for patterns allowing a custom center.
+ Next, call the generatePathGeometry() method to request the path geometry shape.'''
+
+ # Register valid patterns here by name
+ # Create a corresponding processing method below. Precede the name with an underscore(_)
+ patterns = ('Circular', 'CircularZigZag', 'Line', 'Offset', 'Spiral', 'ZigZag')
+
+ def __init__(self, obj, shape, pattern):
+ '''__init__(obj, shape, pattern)... Instantiate PathGeometryGenerator class.
+ Required arguments are the operation object, horizontal planar shape, and pattern name.'''
+ self.debugObjectsGroup = False
+ self.pattern = 'None'
+ self.shape = None
+ self.pathGeometry = None
+ self.rawGeoList = None
+ self.centerOfMass = None
+ self.centerofPattern = None
+ self.deltaX = None
+ self.deltaY = None
+ self.deltaC = None
+ self.halfDiag = None
+ self.halfPasses = None
+ self.obj = obj
+ self.toolDiam = float(obj.ToolController.Tool.Diameter)
+ self.cutOut = self.toolDiam * (float(obj.StepOver) / 100.0)
+ self.wpc = Part.makeCircle(2.0) # make circle for workplane
+
+ # validate requested pattern
+ if pattern in self.patterns:
+ if hasattr(self, '_' + pattern):
+ self.pattern = pattern
+
+ if shape.BoundBox.ZMin != 0.0:
+ shape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - shape.BoundBox.ZMin))
+ if shape.BoundBox.ZLength == 0.0:
+ self.shape = shape
+ else:
+ PathLog.warning('Shape appears to not be horizontal planar. ZMax is {}.'.format(shape.BoundBox.ZMax))
+
+ self._prepareConstants()
+
+ def _prepareConstants(self):
+ # Apply drop cutter extra offset and set the max and min XY area of the operation
+ xmin = self.shape.BoundBox.XMin
+ xmax = self.shape.BoundBox.XMax
+ ymin = self.shape.BoundBox.YMin
+ ymax = self.shape.BoundBox.YMax
+
+ # Compute weighted center of mass of all faces combined
+ if self.pattern in ['Circular', 'CircularZigZag', 'Spiral']:
+ if self.obj.PatternCenterAt == 'CenterOfMass':
+ fCnt = 0
+ totArea = 0.0
+ zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0)
+ for F in self.shape.Faces:
+ comF = F.CenterOfMass
+ areaF = F.Area
+ totArea += areaF
+ fCnt += 1
+ zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF))
+ if fCnt == 0:
+ PathLog.error(translate(self.module, 'Cannot calculate the Center Of Mass. Using Center of Boundbox instead.'))
+ bbC = self.shape.BoundBox.Center
+ zeroCOM = FreeCAD.Vector(bbC.x, bbC.y, 0.0)
+ else:
+ avgArea = totArea / fCnt
+ zeroCOM.multiply(1 / fCnt)
+ zeroCOM.multiply(1 / avgArea)
+ self.centerOfMass = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0)
+ self.centerOfPattern = self._getPatternCenter()
+ else:
+ bbC = self.shape.BoundBox.Center
+ self.centerOfPattern = FreeCAD.Vector(bbC.x, bbC.y, 0.0)
+
+ # get X, Y, Z spans; Compute center of rotation
+ self.deltaX = self.shape.BoundBox.XLength
+ self.deltaY = self.shape.BoundBox.YLength
+ self.deltaC = self.shape.BoundBox.DiagonalLength # math.sqrt(self.deltaX**2 + self.deltaY**2)
+ lineLen = self.deltaC + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end
+ self.halfDiag = math.ceil(lineLen / 2.0)
+ cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal
+ self.halfPasses = math.ceil(cutPasses / 2.0)
+
+ # Public methods
+ def setDebugObjectsGroup(self, tmpGrpObject):
+ '''setDebugObjectsGroup(tmpGrpObject)...
+ Pass the temporary object group to show temporary construction objects'''
+ self.debugObjectsGroup = tmpGrpObject
+
+ def getCenterOfPattern(self):
+ '''getCenterOfPattern()...
+ Returns the Center Of Mass for the current class instance.'''
+ return self.centerOfPattern
+
+ def generatePathGeometry(self):
+ '''generatePathGeometry()...
+ Call this function to obtain the path geometry shape, generated by this class.'''
+ if self.pattern == 'None':
+ PathLog.warning('PGG: No pattern set.')
+ return False
+
+ if self.shape is None:
+ PathLog.warning('PGG: No shape set.')
+ return False
+
+ cmd = 'self._' + self.pattern + '()'
+ exec(cmd)
+
+ if self.obj.CutPatternReversed is True:
+ self.rawGeoList.reverse()
+
+ # Create compound object to bind all lines in Lineset
+ geomShape = Part.makeCompound(self.rawGeoList)
+
+ # Position and rotate the Line and ZigZag geometry
+ if self.pattern in ['Line', 'ZigZag']:
+ if self.obj.CutPatternAngle != 0.0:
+ geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), self.obj.CutPatternAngle)
+ bbC = self.shape.BoundBox.Center
+ geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin)
+
+ if self.debugObjectsGroup:
+ F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpGeometrySet')
+ F.Shape = geomShape
+ F.purgeTouched()
+ self.debugObjectsGroup.addObject(F)
+
+ if self.pattern == 'Offset':
+ return geomShape
+
+ # Identify intersection of cross-section face and lineset
+ cmnShape = self.shape.common(geomShape)
+
+ if self.debugObjectsGroup:
+ F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpPathGeometry')
+ F.Shape = cmnShape
+ F.purgeTouched()
+ self.debugObjectsGroup.addObject(F)
+
+ return cmnShape
+
+ # Cut pattern methods
+ def _Circular(self):
+ GeoSet = list()
+ radialPasses = self._getRadialPasses()
+ minRad = self.toolDiam * 0.45
+ siX3 = 3 * self.obj.SampleInterval.Value
+ minRadSI = (siX3 / 2.0) / math.pi
+
+ if minRad < minRadSI:
+ minRad = minRadSI
+
+ PathLog.debug(' -centerOfPattern: {}'.format(self.centerOfPattern))
+ # Make small center circle to start pattern
+ if self.obj.StepOver > 50:
+ circle = Part.makeCircle(minRad, self.centerOfPattern)
+ GeoSet.append(circle)
+
+ for lc in range(1, radialPasses + 1):
+ rad = (lc * self.cutOut)
+ if rad >= minRad:
+ circle = Part.makeCircle(rad, self.centerOfPattern)
+ GeoSet.append(circle)
+ # Efor
+ self.rawGeoList = GeoSet
+
+ def _CircularZigZag(self):
+ self._Circular() # Use _Circular generator
+
+ def _Line(self):
+ GeoSet = list()
+ centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model
+ cAng = math.atan(self.deltaX / self.deltaY) # BoundaryBox angle
+
+ # Determine end points and create top lines
+ x1 = centRot.x - self.halfDiag
+ x2 = centRot.x + self.halfDiag
+ diag = None
+ if self.obj.CutPatternAngle == 0 or self.obj.CutPatternAngle == 180:
+ diag = self.deltaY
+ elif self.obj.CutPatternAngle == 90 or self.obj.CutPatternAngle == 270:
+ diag = self.deltaX
+ else:
+ perpDist = math.cos(cAng - math.radians(self.obj.CutPatternAngle)) * self.deltaC
+ diag = perpDist
+ y1 = centRot.y + diag
+ # y2 = y1
+
+ # Create end points for set of lines to intersect with cross-section face
+ pntTuples = list()
+ for lc in range((-1 * (self.halfPasses - 1)), self.halfPasses + 1):
+ x1 = centRot.x - self.halfDiag
+ x2 = centRot.x + self.halfDiag
+ y1 = centRot.y + (lc * self.cutOut)
+ # y2 = y1
+ p1 = FreeCAD.Vector(x1, y1, 0.0)
+ p2 = FreeCAD.Vector(x2, y1, 0.0)
+ pntTuples.append((p1, p2))
+
+ # Convert end points to lines
+ for (p1, p2) in pntTuples:
+ line = Part.makeLine(p1, p2)
+ GeoSet.append(line)
+
+ self.rawGeoList = GeoSet
+
+ def _Offset(self):
+ self.rawGeoList = self._extractOffsetFaces()
+
+ def _Spiral(self):
+ GeoSet = list()
+ SEGS = list()
+ draw = True
+ loopRadians = 0.0 # Used to keep track of complete loops/cycles
+ sumRadians = 0.0
+ loopCnt = 0
+ segCnt = 0
+ twoPi = 2.0 * math.pi
+ maxDist = math.ceil(self.cutOut * self._getRadialPasses()) # self.halfDiag
+ move = self.centerOfPattern # Use to translate the center of the spiral
+ lastPoint = FreeCAD.Vector(0.0, 0.0, 0.0)
+
+ # Set tool properties and calculate cutout
+ cutOut = self.cutOut / twoPi
+ segLen = self.obj.SampleInterval.Value # CutterDiameter / 10.0 # SampleInterval.Value
+ stepAng = segLen / ((loopCnt + 1) * self.cutOut) # math.pi / 18.0 # 10 degrees
+ stopRadians = maxDist / cutOut
+
+ if self.obj.CutPatternReversed:
+ if self.obj.CutMode == 'Conventional':
+ getPoint = self._makeOppSpiralPnt
+ else:
+ getPoint = self._makeRegSpiralPnt
+
+ while draw:
+ radAng = sumRadians + stepAng
+ p1 = lastPoint
+ p2 = getPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng
+ sumRadians += stepAng # Increment sumRadians
+ loopRadians += stepAng # Increment loopRadians
+ if loopRadians > twoPi:
+ loopCnt += 1
+ loopRadians -= twoPi
+ stepAng = segLen / ((loopCnt + 1) * self.cutOut) # adjust stepAng with each loop/cycle
+ segCnt += 1
+ lastPoint = p2
+ if sumRadians > stopRadians:
+ draw = False
+ # Create line and show in Object tree
+ lineSeg = Part.makeLine(p2, p1)
+ SEGS.append(lineSeg)
+ # Ewhile
+ SEGS.reverse()
+ else:
+ if self.obj.CutMode == 'Climb':
+ getPoint = self._makeOppSpiralPnt
+ else:
+ getPoint = self._makeRegSpiralPnt
+
+ while draw:
+ radAng = sumRadians + stepAng
+ p1 = lastPoint
+ p2 = getPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng
+ sumRadians += stepAng # Increment sumRadians
+ loopRadians += stepAng # Increment loopRadians
+ if loopRadians > twoPi:
+ loopCnt += 1
+ loopRadians -= twoPi
+ stepAng = segLen / ((loopCnt + 1) * self.cutOut) # adjust stepAng with each loop/cycle
+ segCnt += 1
+ lastPoint = p2
+ if sumRadians > stopRadians:
+ draw = False
+ # Create line and show in Object tree
+ lineSeg = Part.makeLine(p1, p2)
+ SEGS.append(lineSeg)
+ # Ewhile
+ # Eif
+ spiral = Part.Wire([ls.Edges[0] for ls in SEGS])
+ GeoSet.append(spiral)
+
+ self.rawGeoList = GeoSet
+
+ def _ZigZag(self):
+ self._Line() # Use _Line generator
+
+ # Support methods
+ def _getPatternCenter(self):
+ centerAt = self.obj.PatternCenterAt
+
+ if centerAt == 'CenterOfMass':
+ cntrPnt = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, 0.0)
+ elif centerAt == 'CenterOfBoundBox':
+ cent = self.shape.BoundBox.Center
+ cntrPnt = FreeCAD.Vector(cent.x, cent.y, 0.0)
+ elif centerAt == 'XminYmin':
+ cntrPnt = FreeCAD.Vector(self.shape.BoundBox.XMin, self.shape.BoundBox.YMin, 0.0)
+ elif centerAt == 'Custom':
+ cntrPnt = FreeCAD.Vector(self.obj.PatternCenterCustom.x, self.obj.PatternCenterCustom.y, 0.0)
+
+ # Update centerOfPattern point
+ if centerAt != 'Custom':
+ self.obj.PatternCenterCustom = cntrPnt
+ self.centerOfPattern = cntrPnt
+
+ return cntrPnt
+
+ def _getRadialPasses(self):
+ # recalculate number of passes, if need be
+ radialPasses = self.halfPasses
+ if self.obj.PatternCenterAt != 'CenterOfBoundBox':
+ # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center
+ EBB = self.shape.BoundBox
+ CORNERS = [
+ FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0),
+ FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0),
+ FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0),
+ FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0),
+ ]
+ dMax = 0.0
+ for c in range(0, 4):
+ dist = CORNERS[c].sub(self.centerOfPattern).Length
+ if dist > dMax:
+ dMax = dist
+ diag = dMax + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end
+ radialPasses = math.ceil(diag / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal
+
+ return radialPasses
+
+ def _makeRegSpiralPnt(self, move, b, radAng):
+ x = b * radAng * math.cos(radAng)
+ y = b * radAng * math.sin(radAng)
+ return FreeCAD.Vector(x, y, 0.0).add(move)
+
+ def _makeOppSpiralPnt(self, move, b, radAng):
+ x = b * radAng * math.cos(radAng)
+ y = b * radAng * math.sin(radAng)
+ return FreeCAD.Vector(-1 * x, y, 0.0).add(move)
+
+ def _extractOffsetFaces(self):
+ PathLog.debug('_extractOffsetFaces()')
+ wires = list()
+ faces = list()
+ ofst = 0.0 # - self.cutOut
+ shape = self.shape
+ cont = True
+ cnt = 0
+ while cont:
+ ofstArea = self._getFaceOffset(shape, ofst)
+ if not ofstArea:
+ PathLog.warning('PGG: No offset clearing area returned.')
+ cont = False
+ break
+ for F in ofstArea.Faces:
+ faces.append(F)
+ for w in F.Wires:
+ wires.append(w)
+ shape = ofstArea
+ if cnt == 0:
+ ofst = 0.0 - self.cutOut
+ cnt += 1
+ return wires
+
+ def _getFaceOffset(self, shape, offset):
+ '''_getFaceOffset(shape, offset) ... internal function.
+ Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified.
+ Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
+ PathLog.debug('_getFaceOffset()')
+
+ areaParams = {}
+ areaParams['Offset'] = offset
+ areaParams['Fill'] = 1 # 1
+ areaParams['Coplanar'] = 0
+ areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
+ areaParams['Reorient'] = True
+ areaParams['OpenMode'] = 0
+ areaParams['MaxArcPoints'] = 400 # 400
+ areaParams['Project'] = True
+
+ area = Path.Area() # Create instance of Area() class object
+ # area.setPlane(PathUtils.makeWorkplane(shape)) # Set working plane
+ area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1
+ area.add(shape)
+ area.setParams(**areaParams) # set parameters
+
+ offsetShape = area.getShape()
+ wCnt = len(offsetShape.Wires)
+ if wCnt == 0:
+ return False
+ elif wCnt == 1:
+ ofstFace = Part.Face(offsetShape.Wires[0])
+ else:
+ W = list()
+ for wr in offsetShape.Wires:
+ W.append(Part.Face(wr))
+ ofstFace = Part.makeCompound(W)
+
+ return ofstFace
+# Eclass
+
+
+class ProcessSelectedFaces:
+ """ProcessSelectedFaces(JOB, obj) class.
+ This class processes the `obj.Base` object for selected geometery.
+ Calling the preProcessModel(module) method returns
+ two compound objects as a tuple: (FACES, VOIDS) or False."""
+
+ def __init__(self, JOB, obj):
+ self.modelSTLs = list()
+ self.profileShapes = list()
+ self.tempGroup = False
+ self.showDebugObjects = False
+ self.checkBase = False
+ self.module = None
+ self.radius = None
+ self.depthParams = None
+ self.msgNoFaces = translate(self.module, 'Face selection is unavailable for Rotational scans. Ignoring selected faces.')
+ self.JOB = JOB
+ self.obj = obj
+ self.profileEdges = 'None'
+
+ if hasattr(obj, 'ProfileEdges'):
+ self.profileEdges = obj.ProfileEdges
+
+ # Setup STL, model type, and bound box containers for each model in Job
+ for m in range(0, len(JOB.Model.Group)):
+ M = JOB.Model.Group[m]
+ self.modelSTLs.append(False)
+ self.profileShapes.append(False)
+
+ # make circle for workplane
+ self.wpc = Part.makeCircle(2.0)
+
+ def PathSurface(self):
+ if self.obj.Base:
+ if len(self.obj.Base) > 0:
+ self.checkBase = True
+ if self.obj.ScanType == 'Rotational':
+ self.checkBase = False
+ PathLog.warning(self.msgNoFaces)
+
+ def PathWaterline(self):
+ if self.obj.Base:
+ if len(self.obj.Base) > 0:
+ self.checkBase = True
+ if self.obj.Algorithm in ['OCL Dropcutter', 'Experimental']:
+ self.checkBase = False
+ PathLog.warning(self.msgNoFaces)
+
+ # public class methods
+ def setShowDebugObjects(self, grpObj, val):
+ self.tempGroup = grpObj
+ self.showDebugObjects = val
+
+ def preProcessModel(self, module):
+ PathLog.debug('preProcessModel()')
+
+ if not self._isReady(module):
+ return False
+
+ FACES = list()
+ VOIDS = list()
+ fShapes = list()
+ vShapes = list()
+ GRP = self.JOB.Model.Group
+ lenGRP = len(GRP)
+
+ # Crete place holders for each base model in Job
+ for m in range(0, lenGRP):
+ FACES.append(False)
+ VOIDS.append(False)
+ fShapes.append(False)
+ vShapes.append(False)
+
+ # The user has selected subobjects from the base. Pre-Process each.
+ if self.checkBase:
+ PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.')
+
+ # (FACES, VOIDS) = self._identifyFacesAndVoids(FACES, VOIDS)
+ (F, V) = self._identifyFacesAndVoids(FACES, VOIDS)
+
+ # Cycle through each base model, processing faces for each
+ for m in range(0, lenGRP):
+ base = GRP[m]
+ (mFS, mVS, mPS) = self._preProcessFacesAndVoids(base, m, FACES, VOIDS)
+ fShapes[m] = mFS
+ vShapes[m] = mVS
+ self.profileShapes[m] = mPS
+ else:
+ PathLog.debug(' -No obj.Base data.')
+ for m in range(0, lenGRP):
+ self.modelSTLs[m] = True
+
+ # Process each model base, as a whole, as needed
+ # PathLog.debug(' -Pre-processing all models in Job.')
+ for m in range(0, lenGRP):
+ if fShapes[m] is False:
+ PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label))
+ if self.obj.BoundBox == 'BaseBoundBox':
+ base = GRP[m]
+ elif self.obj.BoundBox == 'Stock':
+ base = self.JOB.Stock
+
+ pPEB = self._preProcessEntireBase(base, m)
+ if pPEB is False:
+ PathLog.error(' -Failed to pre-process base as a whole.')
+ else:
+ (fcShp, prflShp) = pPEB
+ if fcShp is not False:
+ if fcShp is True:
+ PathLog.debug(' -fcShp is True.')
+ fShapes[m] = True
+ else:
+ fShapes[m] = [fcShp]
+ if prflShp is not False:
+ if fcShp is not False:
+ PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m]))
+ if vShapes[m] is not False:
+ PathLog.debug(' -Cutting void from base profile shape.')
+ adjPS = prflShp.cut(vShapes[m][0])
+ self.profileShapes[m] = [adjPS]
+ else:
+ PathLog.debug(' -vShapes[m] is False.')
+ self.profileShapes[m] = [prflShp]
+ else:
+ PathLog.debug(' -Saving base profile shape.')
+ self.profileShapes[m] = [prflShp]
+ PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m]))
+ # Efor
+
+ return (fShapes, vShapes)
+
+ # private class methods
+ def _isReady(self, module):
+ '''_isReady(module)... Internal method.
+ Checks if required attributes are available for processing obj.Base (the Base Geometry).'''
+ if hasattr(self, module):
+ self.module = module
+ modMethod = getattr(self, module) # gets the attribute only
+ modMethod() # executes as method
+ else:
+ return False
+
+ if not self.radius:
+ return False
+
+ if not self.depthParams:
+ return False
+
+ return True
+
+ def _identifyFacesAndVoids(self, F, V):
+ TUPS = list()
+ GRP = self.JOB.Model.Group
+ lenGRP = len(GRP)
+
+ # Separate selected faces into (base, face) tuples and flag model(s) for STL creation
+ for (bs, SBS) in self.obj.Base:
+ for sb in SBS:
+ # Flag model for STL creation
+ mdlIdx = None
+ for m in range(0, lenGRP):
+ if bs is GRP[m]:
+ self.modelSTLs[m] = True
+ mdlIdx = m
+ break
+ TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub)
+
+ # Apply `AvoidXFaces` value
+ faceCnt = len(TUPS)
+ add = faceCnt - self.obj.AvoidLastX_Faces
+ for bst in range(0, faceCnt):
+ (m, base, sub) = TUPS[bst]
+ shape = getattr(base.Shape, sub)
+ if isinstance(shape, Part.Face):
+ faceIdx = int(sub[4:]) - 1
+ if bst < add:
+ if F[m] is False:
+ F[m] = list()
+ F[m].append((shape, faceIdx))
+ else:
+ if V[m] is False:
+ V[m] = list()
+ V[m].append((shape, faceIdx))
+ return (F, V)
+
+ def _preProcessFacesAndVoids(self, base, m, FACES, VOIDS):
+ mFS = False
+ mVS = False
+ mPS = False
+ mIFS = list()
+
+ if FACES[m] is not False:
+ isHole = False
+ if self.obj.HandleMultipleFeatures == 'Collectively':
+ cont = True
+ fsL = list() # face shape list
+ ifL = list() # avoid shape list
+ outFCS = list()
+
+ # Get collective envelope slice of selected faces
+ for (fcshp, fcIdx) in FACES[m]:
+ fNum = fcIdx + 1
+ fsL.append(fcshp)
+ gFW = self._getFaceWires(base, fcshp, fcIdx)
+ if gFW is False:
+ PathLog.debug('Failed to get wires from Face{}'.format(fNum))
+ elif gFW[0] is False:
+ PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
+ else:
+ ((otrFace, raised), intWires) = gFW
+ outFCS.append(otrFace)
+ if self.obj.InternalFeaturesCut is False:
+ if intWires is not False:
+ for (iFace, rsd) in intWires:
+ ifL.append(iFace)
+
+ PathLog.debug('Attempting to get cross-section of collective faces.')
+ if len(outFCS) == 0:
+ PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum))
+ cont = False
+ else:
+ cfsL = Part.makeCompound(outFCS)
+
+ # Handle profile edges request
+ if cont is True and self.profileEdges != 'None':
+ ofstVal = self._calculateOffsetValue(isHole)
+ psOfst = extractFaceOffset(cfsL, ofstVal, self.wpc)
+ if psOfst is not False:
+ mPS = [psOfst]
+ if self.profileEdges == 'Only':
+ mFS = True
+ cont = False
+ else:
+ PathLog.error(' -Failed to create profile geometry for selected faces.')
+ cont = False
+
+ if cont:
+ if self.showDebugObjects:
+ T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape')
+ T.Shape = cfsL
+ T.purgeTouched()
+ self.tempGroup.addObject(T)
+
+ ofstVal = self._calculateOffsetValue(isHole)
+ faceOfstShp = extractFaceOffset(cfsL, ofstVal, self.wpc)
+ if faceOfstShp is False:
+ PathLog.error(' -Failed to create offset face.')
+ cont = False
+
+ if cont:
+ lenIfL = len(ifL)
+ if self.obj.InternalFeaturesCut is False:
+ if lenIfL == 0:
+ PathLog.debug(' -No internal features saved.')
+ else:
+ if lenIfL == 1:
+ casL = ifL[0]
+ else:
+ casL = Part.makeCompound(ifL)
+ if self.showDebugObjects:
+ C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat')
+ C.Shape = casL
+ C.purgeTouched()
+ self.tempGroup.addObject(C)
+ ofstVal = self._calculateOffsetValue(isHole=True)
+ intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc)
+ mIFS.append(intOfstShp)
+ # faceOfstShp = faceOfstShp.cut(intOfstShp)
+
+ mFS = [faceOfstShp]
+ # Eif
+
+ elif self.obj.HandleMultipleFeatures == 'Individually':
+ for (fcshp, fcIdx) in FACES[m]:
+ cont = True
+ ifL = list() # avoid shape list
+ fNum = fcIdx + 1
+ outerFace = False
+
+ gFW = self._getFaceWires(base, fcshp, fcIdx)
+ if gFW is False:
+ PathLog.debug('Failed to get wires from Face{}'.format(fNum))
+ cont = False
+ elif gFW[0] is False:
+ PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
+ cont = False
+ outerFace = False
+ else:
+ ((otrFace, raised), intWires) = gFW
+ outerFace = otrFace
+ if self.obj.InternalFeaturesCut is False:
+ if intWires is not False:
+ for (iFace, rsd) in intWires:
+ ifL.append(iFace)
+
+ if outerFace is not False:
+ PathLog.debug('Attempting to create offset face of Face{}'.format(fNum))
+
+ if self.profileEdges != 'None':
+ ofstVal = self._calculateOffsetValue(isHole)
+ psOfst = extractFaceOffset(outerFace, ofstVal, self.wpc)
+ if psOfst is not False:
+ if mPS is False:
+ mPS = list()
+ mPS.append(psOfst)
+ if self.profileEdges == 'Only':
+ if mFS is False:
+ mFS = list()
+ mFS.append(True)
+ cont = False
+ else:
+ PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum))
+ cont = False
+
+ if cont:
+ ofstVal = self._calculateOffsetValue(isHole)
+ faceOfstShp = extractFaceOffset(outerFace, ofstVal, self.wpc)
+
+ lenIfl = len(ifL)
+ if self.obj.InternalFeaturesCut is False and lenIfl > 0:
+ if lenIfl == 1:
+ casL = ifL[0]
+ else:
+ casL = Part.makeCompound(ifL)
+
+ ofstVal = self._calculateOffsetValue(isHole=True)
+ intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc)
+ mIFS.append(intOfstShp)
+ # faceOfstShp = faceOfstShp.cut(intOfstShp)
+
+ if mFS is False:
+ mFS = list()
+ mFS.append(faceOfstShp)
+ # Eif
+ # Efor
+ # Eif
+ # Eif
+
+ if len(mIFS) > 0:
+ if mVS is False:
+ mVS = list()
+ for ifs in mIFS:
+ mVS.append(ifs)
+
+ if VOIDS[m] is not False:
+ PathLog.debug('Processing avoid faces.')
+ cont = True
+ isHole = False
+ outFCS = list()
+ intFEAT = list()
+
+ for (fcshp, fcIdx) in VOIDS[m]:
+ fNum = fcIdx + 1
+ gFW = self._getFaceWires(base, fcshp, fcIdx)
+ if gFW is False:
+ PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum))
+ cont = False
+ else:
+ ((otrFace, raised), intWires) = gFW
+ outFCS.append(otrFace)
+ if self.obj.AvoidLastX_InternalFeatures is False:
+ if intWires is not False:
+ for (iFace, rsd) in intWires:
+ intFEAT.append(iFace)
+
+ lenOtFcs = len(outFCS)
+ if lenOtFcs == 0:
+ cont = False
+ else:
+ if lenOtFcs == 1:
+ avoid = outFCS[0]
+ else:
+ avoid = Part.makeCompound(outFCS)
+
+ if self.showDebugObjects:
+ PathLog.debug('*** tmpAvoidArea')
+ P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope')
+ P.Shape = avoid
+ P.purgeTouched()
+ self.tempGroup.addObject(P)
+
+ if cont:
+ if self.showDebugObjects:
+ PathLog.debug('*** tmpVoidCompound')
+ P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound')
+ P.Shape = avoid
+ P.purgeTouched()
+ self.tempGroup.addObject(P)
+ ofstVal = self._calculateOffsetValue(isHole, isVoid=True)
+ avdOfstShp = extractFaceOffset(avoid, ofstVal, self.wpc)
+ if avdOfstShp is False:
+ PathLog.error('Failed to create collective offset avoid face.')
+ cont = False
+
+ if cont:
+ avdShp = avdOfstShp
+
+ if self.obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0:
+ if len(intFEAT) > 1:
+ ifc = Part.makeCompound(intFEAT)
+ else:
+ ifc = intFEAT[0]
+ ofstVal = self._calculateOffsetValue(isHole=True)
+ ifOfstShp = extractFaceOffset(ifc, ofstVal, self.wpc)
+ if ifOfstShp is False:
+ PathLog.error('Failed to create collective offset avoid internal features.')
+ else:
+ avdShp = avdOfstShp.cut(ifOfstShp)
+
+ if mVS is False:
+ mVS = list()
+ mVS.append(avdShp)
+
+
+ return (mFS, mVS, mPS)
+
+ def _getFaceWires(self, base, fcshp, fcIdx):
+ outFace = False
+ INTFCS = list()
+ fNum = fcIdx + 1
+ warnFinDep = translate(self.module, 'Final Depth might need to be lower. Internal features detected in Face')
+
+ PathLog.debug('_getFaceWires() from Face{}'.format(fNum))
+ WIRES = self._extractWiresFromFace(base, fcshp)
+ if WIRES is False:
+ PathLog.error('Failed to extract wires from Face{}'.format(fNum))
+ return False
+
+ # Process remaining internal features, adding to FCS list
+ lenW = len(WIRES)
+ for w in range(0, lenW):
+ (wire, rsd) = WIRES[w]
+ PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd))
+ if wire.isClosed() is False:
+ PathLog.debug(' -wire is not closed.')
+ else:
+ slc = self._flattenWireToFace(wire)
+ if slc is False:
+ PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum))
+ else:
+ if w == 0:
+ outFace = (slc, rsd)
+ else:
+ # add to VOIDS so cutter avoids area.
+ PathLog.warning(warnFinDep + str(fNum) + '.')
+ INTFCS.append((slc, rsd))
+ if len(INTFCS) == 0:
+ return (outFace, False)
+ else:
+ return (outFace, INTFCS)
+
+ def _preProcessEntireBase(self, base, m):
+ cont = True
+ isHole = False
+ prflShp = False
+ # Create envelope, extract cross-section and make offset co-planar shape
+ # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams)
+
+ try:
+ baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape
+ except Exception as ee:
+ PathLog.error(str(ee))
+ shell = base.Shape.Shells[0]
+ solid = Part.makeSolid(shell)
+ try:
+ baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape
+ except Exception as eee:
+ PathLog.error(str(eee))
+ cont = False
+
+ if cont:
+ csFaceShape = getShapeSlice(baseEnv)
+ if csFaceShape is False:
+ PathLog.debug('getShapeSlice(baseEnv) failed')
+ csFaceShape = getCrossSection(baseEnv)
+ if csFaceShape is False:
+ PathLog.debug('getCrossSection(baseEnv) failed')
+ csFaceShape = getSliceFromEnvelope(baseEnv)
+ if csFaceShape is False:
+ PathLog.error('Failed to slice baseEnv shape.')
+ cont = False
+
+ if cont is True and self.profileEdges != 'None':
+ PathLog.debug(' -Attempting profile geometry for model base.')
+ ofstVal = self._calculateOffsetValue(isHole)
+ psOfst = extractFaceOffset(csFaceShape, ofstVal, self.wpc)
+ if psOfst is not False:
+ if self.profileEdges == 'Only':
+ return (True, psOfst)
+ prflShp = psOfst
+ else:
+ PathLog.error(' -Failed to create profile geometry.')
+ cont = False
+
+ if cont:
+ ofstVal = self._calculateOffsetValue(isHole)
+ faceOffsetShape = extractFaceOffset(csFaceShape, ofstVal, self.wpc)
+ if faceOffsetShape is False:
+ PathLog.error('extractFaceOffset() failed.')
+ else:
+ faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin))
+ return (faceOffsetShape, prflShp)
+ return False
+
+ def _extractWiresFromFace(self, base, fc):
+ '''_extractWiresFromFace(base, fc) ...
+ Attempts to return all closed wires within a parent face, including the outer most wire of the parent.
+ The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True).
+ '''
+ PathLog.debug('_extractWiresFromFace()')
+
+ WIRES = list()
+ lenWrs = len(fc.Wires)
+ PathLog.debug(' -Wire count: {}'.format(lenWrs))
+
+ def index0(tup):
+ return tup[0]
+
+ # Cycle through wires in face
+ for w in range(0, lenWrs):
+ PathLog.debug(' -Analyzing wire_{}'.format(w + 1))
+ wire = fc.Wires[w]
+ checkEdges = False
+ cont = True
+
+ # Check for closed edges (circles, ellipses, etc...)
+ for E in wire.Edges:
+ if E.isClosed() is True:
+ checkEdges = True
+ break
+
+ if checkEdges is True:
+ PathLog.debug(' -checkEdges is True')
+ for e in range(0, len(wire.Edges)):
+ edge = wire.Edges[e]
+ if edge.isClosed() is True and edge.Mass > 0.01:
+ PathLog.debug(' -Found closed edge')
+ raised = False
+ ip = self._isPocket(base, fc, edge)
+ if ip is False:
+ raised = True
+ ebb = edge.BoundBox
+ eArea = ebb.XLength * ebb.YLength
+ F = Part.Face(Part.Wire([edge]))
+ WIRES.append((eArea, F.Wires[0], raised))
+ cont = False
+
+ if cont:
+ PathLog.debug(' -cont is True')
+ # If only one wire and not checkEdges, return first wire
+ if lenWrs == 1:
+ return [(wire, False)]
+
+ raised = False
+ wbb = wire.BoundBox
+ wArea = wbb.XLength * wbb.YLength
+ if w > 0:
+ ip = self._isPocket(base, fc, wire)
+ if ip is False:
+ raised = True
+ WIRES.append((wArea, Part.Wire(wire.Edges), raised))
+
+ nf = len(WIRES)
+ if nf > 0:
+ PathLog.debug(' -number of wires found is {}'.format(nf))
+ if nf == 1:
+ (area, W, raised) = WIRES[0]
+ owLen = fc.OuterWire.Length
+ wLen = W.Length
+ if abs(owLen - wLen) > 0.0000001:
+ OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges))
+ return [(OW, False), (W, raised)]
+ else:
+ return [(W, raised)]
+ else:
+ sortedWIRES = sorted(WIRES, key=index0, reverse=True)
+ WRS = [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size
+ # Check if OuterWire is larger than largest in WRS list
+ (W, raised) = WRS[0]
+ owLen = fc.OuterWire.Length
+ wLen = W.Length
+ if abs(owLen - wLen) > 0.0000001:
+ OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges))
+ WRS.insert(0, (OW, False))
+ return WRS
+
+ return False
+
+ def _calculateOffsetValue(self, isHole, isVoid=False):
+ '''_calculateOffsetValue(self.obj, isHole, isVoid) ... internal function.
+ Calculate the offset for the Path.Area() function.'''
+ self.JOB = PathUtils.findParentJob(self.obj)
+ tolrnc = self.JOB.GeometryTolerance.Value
+
+ if isVoid is False:
+ if isHole is True:
+ offset = -1 * self.obj.InternalFeaturesAdjustment.Value
+ offset += self.radius + (tolrnc / 10.0)
+ else:
+ offset = -1 * self.obj.BoundaryAdjustment.Value
+ if self.obj.BoundaryEnforcement is True:
+ offset += self.radius + (tolrnc / 10.0)
+ else:
+ offset -= self.radius + (tolrnc / 10.0)
+ offset = 0.0 - offset
+ else:
+ offset = -1 * self.obj.BoundaryAdjustment.Value
+ offset += self.radius + (tolrnc / 10.0)
+
+ return offset
+
+ def _isPocket(self, b, f, w):
+ '''_isPocket(b, f, w)...
+ Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion.
+ Returns True if pocket, False if raised protrusion.'''
+ e = w.Edges[0]
+ for fi in range(0, len(b.Shape.Faces)):
+ face = b.Shape.Faces[fi]
+ for ei in range(0, len(face.Edges)):
+ edge = face.Edges[ei]
+ if e.isSame(edge) is True:
+ if f is face:
+ # Alternative: run loop to see if all edges are same
+ pass # same source face, look for another
+ else:
+ if face.CenterOfMass.z < f.CenterOfMass.z:
+ return True
+ return False
+
+ def _flattenWireToFace(self, wire):
+ PathLog.debug('_flattenWireToFace()')
+ if wire.isClosed() is False:
+ PathLog.debug(' -wire.isClosed() is False')
+ return False
+
+ # If wire is planar horizontal, convert to a face and return
+ if wire.BoundBox.ZLength == 0.0:
+ slc = Part.Face(wire)
+ return slc
+
+ # Attempt to create a new wire for manipulation, if not, use original
+ newWire = Part.Wire(wire.Edges)
+ if newWire.isClosed() is True:
+ nWire = newWire
+ else:
+ PathLog.debug(' -newWire.isClosed() is False')
+ nWire = wire
+
+ # Attempt extrusion, and then try a manual slice and then cross-section
+ ext = getExtrudedShape(nWire)
+ if ext is False:
+ PathLog.debug('getExtrudedShape() failed')
+ else:
+ slc = getShapeSlice(ext)
+ if slc is not False:
+ return slc
+ cs = getCrossSection(ext, True)
+ if cs is not False:
+ return cs
+
+ # Attempt creating an envelope, and then try a manual slice and then cross-section
+ env = getShapeEnvelope(nWire)
+ if env is False:
+ PathLog.debug('getShapeEnvelope() failed')
+ else:
+ slc = getShapeSlice(env)
+ if slc is not False:
+ return slc
+ cs = getCrossSection(env, True)
+ if cs is not False:
+ return cs
+
+ # Attempt creating a projection
+ slc = getProjectedFace(self.tempGroup, nWire)
+ if slc is False:
+ PathLog.debug('getProjectedFace() failed')
+ else:
+ return slc
+
+ return False
+# Eclass
+
+
+# Functions for getting a shape envelope and cross-section
+def getExtrudedShape(wire):
+ PathLog.debug('getExtrudedShape()')
+ wBB = wire.BoundBox
+ extFwd = math.floor(2.0 * wBB.ZLength) + 10.0
+
+ try:
+ shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd))
+ except Exception as ee:
+ PathLog.error(' -extrude wire failed: \n{}'.format(ee))
+ return False
+
+ SHP = Part.makeSolid(shell)
+ return SHP
+
+def getShapeSlice(shape):
+ PathLog.debug('getShapeSlice()')
+
+ bb = shape.BoundBox
+ mid = (bb.ZMin + bb.ZMax) / 2.0
+ xmin = bb.XMin - 1.0
+ xmax = bb.XMax + 1.0
+ ymin = bb.YMin - 1.0
+ ymax = bb.YMax + 1.0
+ p1 = FreeCAD.Vector(xmin, ymin, mid)
+ p2 = FreeCAD.Vector(xmax, ymin, mid)
+ p3 = FreeCAD.Vector(xmax, ymax, mid)
+ p4 = FreeCAD.Vector(xmin, ymax, mid)
+
+ e1 = Part.makeLine(p1, p2)
+ e2 = Part.makeLine(p2, p3)
+ e3 = Part.makeLine(p3, p4)
+ e4 = Part.makeLine(p4, p1)
+ face = Part.Face(Part.Wire([e1, e2, e3, e4]))
+ fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area
+ sArea = shape.BoundBox.XLength * shape.BoundBox.YLength
+ midArea = (fArea + sArea) / 2.0
+
+ slcShp = shape.common(face)
+ slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength
+
+ if slcArea < midArea:
+ for W in slcShp.Wires:
+ if W.isClosed() is False:
+ PathLog.debug(' -wire.isClosed() is False')
+ return False
+ if len(slcShp.Wires) == 1:
+ wire = slcShp.Wires[0]
+ slc = Part.Face(wire)
+ slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
+ return slc
+ else:
+ fL = list()
+ for W in slcShp.Wires:
+ slc = Part.Face(W)
+ slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
+ fL.append(slc)
+ comp = Part.makeCompound(fL)
+ return comp
+
+ # PathLog.debug(' -slcArea !< midArea')
+ # PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges)))
+ return False
+
+def getProjectedFace(tempGroup, wire):
+ import Draft
+ PathLog.debug('getProjectedFace()')
+ F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire')
+ F.Shape = wire
+ F.purgeTouched()
+ tempGroup.addObject(F)
+ try:
+ prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1))
+ prj.recompute()
+ prj.purgeTouched()
+ tempGroup.addObject(prj)
+ except Exception as ee:
+ PathLog.error(str(ee))
+ return False
+ else:
+ pWire = Part.Wire(prj.Shape.Edges)
+ if pWire.isClosed() is False:
+ # PathLog.debug(' -pWire.isClosed() is False')
+ return False
+ slc = Part.Face(pWire)
+ slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
+ return slc
+
+def getCrossSection(shape, withExtrude=False):
+ PathLog.debug('getCrossSection()')
+ wires = list()
+ bb = shape.BoundBox
+ mid = (bb.ZMin + bb.ZMax) / 2.0
+
+ for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid):
+ wires.append(i)
+
+ if len(wires) > 0:
+ comp = Part.Compound(wires) # produces correct cross-section wire !
+ comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin))
+ csWire = comp.Wires[0]
+ if csWire.isClosed() is False:
+ PathLog.debug(' -comp.Wires[0] is not closed')
+ return False
+ if withExtrude is True:
+ ext = getExtrudedShape(csWire)
+ CS = getShapeSlice(ext)
+ if CS is False:
+ return False
+ else:
+ CS = Part.Face(csWire)
+ CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin))
+ return CS
+ else:
+ PathLog.debug(' -No wires from .slice() method')
+
+ return False
+
+def getShapeEnvelope(shape):
+ PathLog.debug('getShapeEnvelope()')
+
+ wBB = shape.BoundBox
+ extFwd = wBB.ZLength + 10.0
+ minz = wBB.ZMin
+ maxz = wBB.ZMin + extFwd
+ stpDwn = (maxz - minz) / 4.0
+ dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz)
+
+ try:
+ env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape
+ except Exception as ee:
+ PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee))
+ return False
+ else:
+ return env
+
+def getSliceFromEnvelope(env):
+ PathLog.debug('getSliceFromEnvelope()')
+ eBB = env.BoundBox
+ extFwd = eBB.ZLength + 10.0
+ maxz = eBB.ZMin + extFwd
+
+ emax = math.floor(maxz - 1.0)
+ E = list()
+ for e in range(0, len(env.Edges)):
+ emin = env.Edges[e].BoundBox.ZMin
+ if emin > emax:
+ E.append(env.Edges[e])
+ tf = Part.Face(Part.Wire(Part.__sortEdges__(E)))
+ tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin))
+
+ return tf
+
+
+# Function to extract offset face from shape
+def extractFaceOffset(fcShape, offset, wpc, makeComp=True):
+ '''extractFaceOffset(fcShape, offset) ... internal function.
+ Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified.
+ Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
+ PathLog.debug('extractFaceOffset()')
+
+ if fcShape.BoundBox.ZMin != 0.0:
+ fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin))
+
+ areaParams = {}
+ areaParams['Offset'] = offset
+ areaParams['Fill'] = 1 # 1
+ areaParams['Coplanar'] = 0
+ areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
+ areaParams['Reorient'] = True
+ areaParams['OpenMode'] = 0
+ areaParams['MaxArcPoints'] = 400 # 400
+ areaParams['Project'] = True
+
+ area = Path.Area() # Create instance of Area() class object
+ # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane
+ area.setPlane(PathUtils.makeWorkplane(wpc)) # Set working plane to normal at Z=1
+ area.add(fcShape)
+ area.setParams(**areaParams) # set parameters
+
+ offsetShape = area.getShape()
+ wCnt = len(offsetShape.Wires)
+ if wCnt == 0:
+ return False
+ elif wCnt == 1:
+ ofstFace = Part.Face(offsetShape.Wires[0])
+ if not makeComp:
+ ofstFace = [ofstFace]
+ else:
+ W = list()
+ for wr in offsetShape.Wires:
+ W.append(Part.Face(wr))
+ if makeComp:
+ ofstFace = Part.makeCompound(W)
+ else:
+ ofstFace = W
+
+ return ofstFace # offsetShape
+
+
+# Functions to convert path geometry into line/arc segments for OCL input or directly to g-code
+def pathGeomToLinesPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps):
+ '''pathGeomToLinesPointSet(obj, compGeoShp)...
+ Convert a compound set of sequential line segments to directionally-oriented collinear groupings.'''
+ PathLog.debug('pathGeomToLinesPointSet()')
+ # Extract intersection line segments for return value as list()
+ LINES = list()
+ inLine = list()
+ chkGap = False
+ lnCnt = 0
+ ec = len(compGeoShp.Edges)
+ cpa = obj.CutPatternAngle
+
+ edg0 = compGeoShp.Edges[0]
+ p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
+ p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
+ if cutClimb is True:
+ tup = (p2, p1)
+ lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
+ else:
+ tup = (p1, p2)
+ lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
+ inLine.append(tup)
+ sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
+
+ for ei in range(1, ec):
+ chkGap = False
+ edg = compGeoShp.Edges[ei] # Get edge for vertexes
+ v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0
+ v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1
+
+ ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
+ cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point)
+ # iC = sp.isOnLineSegment(ep, cp)
+ iC = cp.isOnLineSegment(sp, ep)
+ if iC is True:
+ inLine.append('BRK')
+ chkGap = True
+ else:
+ if cutClimb is True:
+ inLine.reverse()
+ LINES.append(inLine) # Save inLine segments
+ lnCnt += 1
+ inLine = list() # reset collinear container
+ if cutClimb is True:
+ sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
+ else:
+ sp = ep
+
+ if cutClimb is True:
+ tup = (v2, v1)
+ if chkGap is True:
+ gap = abs(toolDiam - lst.sub(ep).Length)
+ lst = cp
+ else:
+ tup = (v1, v2)
+ if chkGap is True:
+ gap = abs(toolDiam - lst.sub(cp).Length)
+ lst = ep
+
+ if chkGap is True:
+ if gap < obj.GapThreshold.Value:
+ b = inLine.pop() # pop off 'BRK' marker
+ (vA, vB) = inLine.pop() # pop off previous line segment for combining with current
+ tup = (vA, tup[1])
+ closedGap = True
+ else:
+ # PathLog.debug('---- Gap: {} mm'.format(gap))
+ gap = round(gap, 6)
+ if gap < gaps[0]:
+ gaps.insert(0, gap)
+ gaps.pop()
+ inLine.append(tup)
+ # Efor
+ lnCnt += 1
+ if cutClimb is True:
+ inLine.reverse()
+ LINES.append(inLine) # Save inLine segments
+
+ # Handle last inLine set, reversing it.
+ if obj.CutPatternReversed is True:
+ if cpa != 0.0 and cpa % 90.0 == 0.0:
+ F = LINES.pop(0)
+ rev = list()
+ for iL in F:
+ if iL == 'BRK':
+ rev.append(iL)
+ else:
+ (p1, p2) = iL
+ rev.append((p2, p1))
+ rev.reverse()
+ LINES.insert(0, rev)
+
+ isEven = lnCnt % 2
+ if isEven == 0:
+ PathLog.debug('Line count is ODD.')
+ else:
+ PathLog.debug('Line count is even.')
+
+ return LINES
+
+def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps):
+ '''_pathGeomToZigzagPointSet(obj, compGeoShp)...
+ Convert a compound set of sequential line segments to directionally-oriented collinear groupings
+ with a ZigZag directional indicator included for each collinear group.'''
+ PathLog.debug('_pathGeomToZigzagPointSet()')
+ # Extract intersection line segments for return value as list()
+ LINES = list()
+ inLine = list()
+ lnCnt = 0
+ chkGap = False
+ ec = len(compGeoShp.Edges)
+
+ if cutClimb is True:
+ dirFlg = -1
+ else:
+ dirFlg = 1
+
+ edg0 = compGeoShp.Edges[0]
+ p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
+ p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
+ if dirFlg == 1:
+ tup = (p1, p2)
+ lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
+ sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
+ else:
+ tup = (p2, p1)
+ lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
+ sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point
+ inLine.append(tup)
+
+ for ei in range(1, ec):
+ edg = compGeoShp.Edges[ei]
+ v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y)
+ v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y)
+
+ cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment)
+ ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
+ # iC = sp.isOnLineSegment(ep, cp)
+ iC = cp.isOnLineSegment(sp, ep)
+ if iC is True:
+ inLine.append('BRK')
+ chkGap = True
+ gap = abs(toolDiam - lst.sub(cp).Length)
+ else:
+ chkGap = False
+ if dirFlg == -1:
+ inLine.reverse()
+ # LINES.append((dirFlg, inLine))
+ LINES.append(inLine)
+ lnCnt += 1
+ dirFlg = -1 * dirFlg # Change zig to zag
+ inLine = list() # reset collinear container
+ sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
+
+ lst = ep
+ if dirFlg == 1:
+ tup = (v1, v2)
+ else:
+ tup = (v2, v1)
+
+ if chkGap is True:
+ if gap < obj.GapThreshold.Value:
+ b = inLine.pop() # pop off 'BRK' marker
+ (vA, vB) = inLine.pop() # pop off previous line segment for combining with current
+ if dirFlg == 1:
+ tup = (vA, tup[1])
+ else:
+ tup = (tup[0], vB)
+ closedGap = True
+ else:
+ gap = round(gap, 6)
+ if gap < gaps[0]:
+ gaps.insert(0, gap)
+ gaps.pop()
+ inLine.append(tup)
+ # Efor
+ lnCnt += 1
+
+ # Fix directional issue with LAST line when line count is even
+ isEven = lnCnt % 2
+ if isEven == 0: # Changed to != with 90 degree CutPatternAngle
+ PathLog.debug('Line count is even.')
+ else:
+ PathLog.debug('Line count is ODD.')
+ dirFlg = -1 * dirFlg
+ if obj.CutPatternReversed is False:
+ if cutClimb is True:
+ dirFlg = -1 * dirFlg
+
+ if obj.CutPatternReversed:
+ dirFlg = -1 * dirFlg
+
+ # Handle last inLine list
+ if dirFlg == 1:
+ rev = list()
+ for iL in inLine:
+ if iL == 'BRK':
+ rev.append(iL)
+ else:
+ (p1, p2) = iL
+ rev.append((p2, p1))
+
+ if not obj.CutPatternReversed:
+ rev.reverse()
+ else:
+ rev2 = list()
+ for iL in rev:
+ if iL == 'BRK':
+ rev2.append(iL)
+ else:
+ (p1, p2) = iL
+ rev2.append((p2, p1))
+ rev2.reverse()
+ rev = rev2
+
+ # LINES.append((dirFlg, rev))
+ LINES.append(rev)
+ else:
+ # LINES.append((dirFlg, inLine))
+ LINES.append(inLine)
+
+ return LINES
+
+def pathGeomToCircularPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps, COM):
+ '''pathGeomToCircularPointSet(obj, compGeoShp)...
+ Convert a compound set of arcs/circles to a set of directionally-oriented arc end points
+ and the corresponding center point.'''
+ # Extract intersection line segments for return value as list()
+ PathLog.debug('pathGeomToCircularPointSet()')
+ ARCS = list()
+ stpOvrEI = list()
+ segEI = list()
+ isSame = False
+ sameRad = None
+ ec = len(compGeoShp.Edges)
+
+ def gapDist(sp, ep):
+ X = (ep[0] - sp[0])**2
+ Y = (ep[1] - sp[1])**2
+ return math.sqrt(X + Y) # the 'z' value is zero in both points
+
+ # Separate arc data into Loops and Arcs
+ for ei in range(0, ec):
+ edg = compGeoShp.Edges[ei]
+ if edg.Closed is True:
+ stpOvrEI.append(('L', ei, False))
+ else:
+ if isSame is False:
+ segEI.append(ei)
+ isSame = True
+ pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
+ sameRad = pnt.sub(COM).Length
+ else:
+ # Check if arc is co-radial to current SEGS
+ pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
+ if abs(sameRad - pnt.sub(COM).Length) > 0.00001:
+ isSame = False
+
+ if isSame is True:
+ segEI.append(ei)
+ else:
+ # Move co-radial arc segments
+ stpOvrEI.append(['A', segEI, False])
+ # Start new list of arc segments
+ segEI = [ei]
+ isSame = True
+ pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
+ sameRad = pnt.sub(COM).Length
+ # Process trailing `segEI` data, if available
+ if isSame is True:
+ stpOvrEI.append(['A', segEI, False])
+
+ # Identify adjacent arcs with y=0 start/end points that connect
+ for so in range(0, len(stpOvrEI)):
+ SO = stpOvrEI[so]
+ if SO[0] == 'A':
+ startOnAxis = list()
+ endOnAxis = list()
+ EI = SO[1] # list of corresponding compGeoShp.Edges indexes
+
+ # Identify startOnAxis and endOnAxis arcs
+ for i in range(0, len(EI)):
+ ei = EI[i] # edge index
+ E = compGeoShp.Edges[ei] # edge object
+ if abs(COM.y - E.Vertexes[0].Y) < 0.00001:
+ startOnAxis.append((i, ei, E.Vertexes[0]))
+ elif abs(COM.y - E.Vertexes[1].Y) < 0.00001:
+ endOnAxis.append((i, ei, E.Vertexes[1]))
+
+ # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected
+ lenSOA = len(startOnAxis)
+ lenEOA = len(endOnAxis)
+ if lenSOA > 0 and lenEOA > 0:
+ for soa in range(0, lenSOA):
+ (iS, eiS, vS) = startOnAxis[soa]
+ for eoa in range(0, len(endOnAxis)):
+ (iE, eiE, vE) = endOnAxis[eoa]
+ dist = vE.X - vS.X
+ if abs(dist) < 0.00001: # They connect on axis at same radius
+ SO[2] = (eiE, eiS)
+ break
+ elif dist > 0:
+ break # stop searching
+ # Eif
+ # Eif
+ # Efor
+
+ # Construct arc data tuples for OCL
+ dirFlg = 1
+ if not cutClimb: # True yields Climb when set to Conventional
+ dirFlg = -1
+
+ # Cycle through stepOver data
+ for so in range(0, len(stpOvrEI)):
+ SO = stpOvrEI[so]
+ if SO[0] == 'L': # L = Loop/Ring/Circle
+ # PathLog.debug("SO[0] == 'Loop'")
+ lei = SO[1] # loop Edges index
+ v1 = compGeoShp.Edges[lei].Vertexes[0]
+
+ # space = obj.SampleInterval.Value / 10.0
+ # space = 0.000001
+ space = toolDiam * 0.005 # If too small, OCL will fail to scan the loop
+
+ # p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z)
+ p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) # z=0.0 for waterline; z=v1.Z for 3D Surface
+ rad = p1.sub(COM).Length
+ spcRadRatio = space/rad
+ if spcRadRatio < 1.0:
+ tolrncAng = math.asin(spcRadRatio)
+ else:
+ tolrncAng = 0.99999998 * math.pi
+ EX = COM.x + (rad * math.cos(tolrncAng))
+ EY = v1.Y - space # rad * math.sin(tolrncAng)
+
+ sp = (v1.X, v1.Y, 0.0)
+ ep = (EX, EY, 0.0)
+ cp = (COM.x, COM.y, 0.0)
+ if dirFlg == 1:
+ arc = (sp, ep, cp)
+ else:
+ arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
+ ARCS.append(('L', dirFlg, [arc]))
+ else: # SO[0] == 'A' A = Arc
+ # PathLog.debug("SO[0] == 'Arc'")
+ PRTS = list()
+ EI = SO[1] # list of corresponding Edges indexes
+ CONN = SO[2] # list of corresponding connected edges tuples (iE, iS)
+ chkGap = False
+ lst = None
+
+ if CONN is not False:
+ (iE, iS) = CONN
+ v1 = compGeoShp.Edges[iE].Vertexes[0]
+ v2 = compGeoShp.Edges[iS].Vertexes[1]
+ sp = (v1.X, v1.Y, 0.0)
+ ep = (v2.X, v2.Y, 0.0)
+ cp = (COM.x, COM.y, 0.0)
+ if dirFlg == 1:
+ arc = (sp, ep, cp)
+ lst = ep
+ else:
+ arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
+ lst = sp
+ PRTS.append(arc)
+ # Pop connected edge index values from arc segments index list
+ iEi = EI.index(iE)
+ iSi = EI.index(iS)
+ if iEi > iSi:
+ EI.pop(iEi)
+ EI.pop(iSi)
+ else:
+ EI.pop(iSi)
+ EI.pop(iEi)
+ if len(EI) > 0:
+ PRTS.append('BRK')
+ chkGap = True
+ cnt = 0
+ for ei in EI:
+ if cnt > 0:
+ PRTS.append('BRK')
+ chkGap = True
+ v1 = compGeoShp.Edges[ei].Vertexes[0]
+ v2 = compGeoShp.Edges[ei].Vertexes[1]
+ sp = (v1.X, v1.Y, 0.0)
+ ep = (v2.X, v2.Y, 0.0)
+ cp = (COM.x, COM.y, 0.0)
+ if dirFlg == 1:
+ arc = (sp, ep, cp)
+ if chkGap is True:
+ gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length)
+ lst = ep
+ else:
+ arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
+ if chkGap is True:
+ gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length)
+ lst = sp
+ if chkGap is True:
+ if gap < obj.GapThreshold.Value:
+ PRTS.pop() # pop off 'BRK' marker
+ (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current
+ arc = (vA, arc[1], vC)
+ closedGap = True
+ else:
+ # PathLog.debug('---- Gap: {} mm'.format(gap))
+ gap = round(gap, 6)
+ if gap < gaps[0]:
+ gaps.insert(0, gap)
+ gaps.pop()
+ PRTS.append(arc)
+ cnt += 1
+
+ if dirFlg == -1:
+ PRTS.reverse()
+
+ ARCS.append(('A', dirFlg, PRTS))
+ # Eif
+ if obj.CutPattern == 'CircularZigZag':
+ dirFlg = -1 * dirFlg
+ # Efor
+
+ return ARCS
+
+def pathGeomToSpiralPointSet(obj, compGeoShp):
+ '''_pathGeomToSpiralPointSet(obj, compGeoShp)...
+ Convert a compound set of sequential line segments to directional, connected groupings.'''
+ PathLog.debug('_pathGeomToSpiralPointSet()')
+ # Extract intersection line segments for return value as list()
+ LINES = list()
+ inLine = list()
+ lnCnt = 0
+ ec = len(compGeoShp.Edges)
+ start = 2
+
+ if obj.CutPatternReversed:
+ edg1 = compGeoShp.Edges[0] # Skip first edge, as it is the closing edge: center to outer tail
+ ec -= 1
+ start = 1
+ else:
+ edg1 = compGeoShp.Edges[1] # Skip first edge, as it is the closing edge: center to outer tail
+ p1 = FreeCAD.Vector(edg1.Vertexes[0].X, edg1.Vertexes[0].Y, 0.0)
+ p2 = FreeCAD.Vector(edg1.Vertexes[1].X, edg1.Vertexes[1].Y, 0.0)
+ tup = ((p1.x, p1.y), (p2.x, p2.y))
+ inLine.append(tup)
+ lst = p2
+
+ for ei in range(start, ec): # Skipped first edge, started with second edge above as edg1
+ edg = compGeoShp.Edges[ei] # Get edge for vertexes
+ sp = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) # check point (first / middle point)
+ ep = FreeCAD.Vector(edg.Vertexes[1].X, edg.Vertexes[1].Y, 0.0) # end point
+ tup = ((sp.x, sp.y), (ep.x, ep.y))
+
+ if sp.sub(p2).Length < 0.000001:
+ inLine.append(tup)
+ else:
+ LINES.append(inLine) # Save inLine segments
+ lnCnt += 1
+ inLine = list() # reset container
+ inLine.append(tup)
+ p1 = sp
+ p2 = ep
+ # Efor
+
+ lnCnt += 1
+ LINES.append(inLine) # Save inLine segments
+
+ return LINES
+
+def pathGeomToOffsetPointSet(obj, compGeoShp):
+ '''pathGeomToOffsetPointSet(obj, compGeoShp)...
+ Convert a compound set of 3D profile segmented wires to 2D segments, applying linear optimization.'''
+ PathLog.debug('pathGeomToOffsetPointSet()')
+
+ LINES = list()
+ optimize = obj.OptimizeLinearPaths
+ ofstCnt = len(compGeoShp)
+
+ # Cycle through offeset loops
+ for ei in range(0, ofstCnt):
+ OS = compGeoShp[ei]
+ lenOS = len(OS)
+
+ if ei > 0:
+ LINES.append('BRK')
+
+ fp = FreeCAD.Vector(OS[0].x, OS[0].y, OS[0].z)
+ OS.append(fp)
+
+ # Cycle through points in each loop
+ prev = OS[0]
+ pnt = OS[1]
+ for v in range(1, lenOS):
+ nxt = OS[v + 1]
+ if optimize:
+ # iPOL = prev.isOnLineSegment(nxt, pnt)
+ iPOL = pnt.isOnLineSegment(prev, nxt)
+ if iPOL:
+ pnt = nxt
+ else:
+ tup = ((prev.x, prev.y), (pnt.x, pnt.y))
+ LINES.append(tup)
+ prev = pnt
+ pnt = nxt
+ else:
+ tup = ((prev.x, prev.y), (pnt.x, pnt.y))
+ LINES.append(tup)
+ prev = pnt
+ pnt = nxt
+ if iPOL:
+ tup = ((prev.x, prev.y), (pnt.x, pnt.y))
+ LINES.append(tup)
+ # Efor
+
+ return [LINES]
\ No newline at end of file
diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py
index c1c8b66cb6..ba930db881 100644
--- a/src/Mod/Path/PathScripts/PathWaterline.py
+++ b/src/Mod/Path/PathScripts/PathWaterline.py
@@ -1,3484 +1,1971 @@
-# -*- coding: utf-8 -*-
-
-# ***************************************************************************
-# * *
-# * Copyright (c) 2019 Russell Johnson (russ4262) *
-# * Copyright (c) 2019 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 *
-# * *
-# ***************************************************************************
-
-from __future__ import print_function
-
-import FreeCAD
-import Path
-import PathScripts.PathLog as PathLog
-import PathScripts.PathUtils as PathUtils
-import PathScripts.PathOp as PathOp
-
-from PySide import QtCore
-import time
-import math
-
-# lazily loaded modules
-from lazy_loader.lazy_loader import LazyLoader
-MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart')
-Draft = LazyLoader('Draft', globals(), 'Draft')
-Part = LazyLoader('Part', globals(), 'Part')
-
-if FreeCAD.GuiUp:
- import FreeCADGui
-
-__title__ = "Path Waterline Operation"
-__author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)"
-__url__ = "http://www.freecadweb.org"
-__doc__ = "Class and implementation of Mill Facing operation."
-
-PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
-# PathLog.trackModule(PathLog.thisModule())
-
-
-# Qt translation handling
-def translate(context, text, disambig=None):
- return QtCore.QCoreApplication.translate(context, text, disambig)
-
-
-# OCL must be installed
-try:
- import ocl
-except ImportError:
- FreeCAD.Console.PrintError(
- translate("Path_Waterline", "This operation requires OpenCamLib to be installed.") + "\n")
- import sys
- sys.exit(translate("Path_Waterline", "This operation requires OpenCamLib to be installed."))
-
-
-class ObjectWaterline(PathOp.ObjectOp):
- '''Proxy object for Surfacing operation.'''
-
- def baseObject(self):
- '''baseObject() ... returns super of receiver
- Used to call base implementation in overwritten functions.'''
- return super(self.__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 | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces
-
- def initOperation(self, obj):
- '''initPocketOp(obj) ...
- Initialize the operation - property creation and property editor status.'''
- self.initOpProperties(obj)
-
- # For debugging
- if PathLog.getLevel(PathLog.thisModule()) != 4:
- obj.setEditorMode('ShowTempObjects', 2) # hide
-
- if not hasattr(obj, 'DoNotSetDefaultValues'):
- self.setEditorProperties(obj)
-
- def initOpProperties(self, obj):
- '''initOpProperties(obj) ... create operation specific properties'''
- PROPS = [
- ("App::PropertyBool", "ShowTempObjects", "Debug",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")),
-
- ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")),
- ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")),
-
- ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")),
- ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")),
- ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")),
- ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")),
- ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")),
- ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")),
- ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")),
-
- ("App::PropertyEnumeration", "Algorithm", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the algorithm to use: OCL Dropcutter*, or Experimental.")),
- ("App::PropertyEnumeration", "BoundBox", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")),
- ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")),
- ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the circular pattern.")),
- ("App::PropertyEnumeration", "ClearLastLayer", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set to clear last layer in a `Multi-pass` operation.")),
- ("App::PropertyEnumeration", "CutMode", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")),
- ("App::PropertyEnumeration", "CutPattern", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")),
- ("App::PropertyFloat", "CutPatternAngle", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")),
- ("App::PropertyBool", "CutPatternReversed", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")),
- ("App::PropertyDistance", "DepthOffset", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")),
- ("App::PropertyEnumeration", "LayerMode", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")),
- ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")),
- ("App::PropertyDistance", "SampleInterval", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")),
- ("App::PropertyPercent", "StepOver", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")),
-
- ("App::PropertyBool", "OptimizeLinearPaths", "Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")),
- ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")),
- ("App::PropertyDistance", "GapThreshold", "Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")),
- ("App::PropertyString", "GapSizes", "Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")),
-
- ("App::PropertyVectorDistance", "StartPoint", "Start Point",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")),
- ("App::PropertyBool", "UseStartPoint", "Start Point",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"))
- ]
-
- missing = list()
- for (prtyp, nm, grp, tt) in PROPS:
- if not hasattr(obj, nm):
- obj.addProperty(prtyp, nm, grp, tt)
- missing.append(nm)
-
- # Set enumeration lists for enumeration properties
- if len(missing) > 0:
- ENUMS = self._propertyEnumerations()
- for n in ENUMS:
- if n in missing:
- cmdStr = 'obj.{}={}'.format(n, ENUMS[n])
- exec(cmdStr)
-
- self.addedAllProperties = True
-
- def _propertyEnumerations(self):
- # Enumeration lists for App::PropertyEnumeration properties
- return {
- 'Algorithm': ['OCL Dropcutter', 'Experimental'],
- 'BoundBox': ['BaseBoundBox', 'Stock'],
- 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'],
- 'ClearLastLayer': ['Off', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'],
- 'CutMode': ['Conventional', 'Climb'],
- 'CutPattern': ['None', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle']
- 'HandleMultipleFeatures': ['Collectively', 'Individually'],
- 'LayerMode': ['Single-pass', 'Multi-pass'],
- 'ProfileEdges': ['None', 'Only', 'First', 'Last'],
- }
-
- def setEditorProperties(self, obj):
- # Used to hide inputs in properties list
- show = 0
- hide = 2
- cpShow = 0
- expMode = 0
- obj.setEditorMode('BoundaryEnforcement', hide)
- obj.setEditorMode('ProfileEdges', hide)
- obj.setEditorMode('InternalFeaturesAdjustment', hide)
- obj.setEditorMode('InternalFeaturesCut', hide)
- obj.setEditorMode('GapSizes', hide)
- obj.setEditorMode('GapThreshold', hide)
- obj.setEditorMode('AvoidLastX_Faces', hide)
- obj.setEditorMode('AvoidLastX_InternalFeatures', hide)
- obj.setEditorMode('BoundaryAdjustment', hide)
- obj.setEditorMode('HandleMultipleFeatures', hide)
- if hasattr(obj, 'EnableRotation'):
- obj.setEditorMode('EnableRotation', hide)
- if obj.CutPattern == 'None':
- show = 2
- hide = 2
- cpShow = 2
- # elif obj.CutPattern in ['Line', 'ZigZag']:
- # show = 0
- # hide = 2
- elif obj.CutPattern in ['Circular', 'CircularZigZag']:
- show = 2 # hide
- hide = 0 # show
- # obj.setEditorMode('StepOver', cpShow)
- obj.setEditorMode('CutPatternAngle', show)
- obj.setEditorMode('CircularCenterAt', hide)
- obj.setEditorMode('CircularCenterCustom', hide)
- if obj.Algorithm == 'Experimental':
- expMode = 2
- obj.setEditorMode('SampleInterval', expMode)
- obj.setEditorMode('LinearDeflection', expMode)
- obj.setEditorMode('AngularDeflection', expMode)
-
- def onChanged(self, obj, prop):
- if hasattr(self, 'addedAllProperties'):
- if self.addedAllProperties is True:
- if prop in ['Algorithm', 'CutPattern']:
- self.setEditorProperties(obj)
-
- def opOnDocumentRestored(self, obj):
- self.initOpProperties(obj)
-
- if PathLog.getLevel(PathLog.thisModule()) != 4:
- obj.setEditorMode('ShowTempObjects', 2) # hide
- else:
- obj.setEditorMode('ShowTempObjects', 0) # show
-
- self.setEditorProperties(obj)
-
- def opSetDefaultValues(self, obj, job):
- '''opSetDefaultValues(obj, job) ... initialize defaults'''
- job = PathUtils.findParentJob(obj)
-
- obj.OptimizeLinearPaths = True
- obj.InternalFeaturesCut = True
- obj.OptimizeStepOverTransitions = False
- obj.BoundaryEnforcement = True
- obj.UseStartPoint = False
- obj.AvoidLastX_InternalFeatures = True
- obj.CutPatternReversed = False
- obj.StartPoint.x = 0.0
- obj.StartPoint.y = 0.0
- obj.StartPoint.z = obj.ClearanceHeight.Value
- obj.Algorithm = 'OCL Dropcutter'
- obj.ProfileEdges = 'None'
- obj.LayerMode = 'Single-pass'
- obj.CutMode = 'Conventional'
- obj.CutPattern = 'None'
- obj.HandleMultipleFeatures = 'Collectively' # 'Individually'
- obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom'
- obj.GapSizes = 'No gaps identified.'
- obj.ClearLastLayer = 'Off'
- obj.StepOver = 100
- obj.CutPatternAngle = 0.0
- obj.DepthOffset.Value = 0.0
- obj.SampleInterval.Value = 1.0
- obj.BoundaryAdjustment.Value = 0.0
- obj.InternalFeaturesAdjustment.Value = 0.0
- obj.AvoidLastX_Faces = 0
- obj.CircularCenterCustom.x = 0.0
- obj.CircularCenterCustom.y = 0.0
- obj.CircularCenterCustom.z = 0.0
- obj.GapThreshold.Value = 0.005
- obj.LinearDeflection.Value = 0.0001
- obj.AngularDeflection.Value = 0.25
- # For debugging
- obj.ShowTempObjects = False
-
- # need to overwrite the default depth calculations for facing
- d = None
- 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 d is not None:
- obj.OpFinalDepth.Value = d.final_depth
- obj.OpStartDepth.Value = d.start_depth
- else:
- obj.OpFinalDepth.Value = -10
- obj.OpStartDepth.Value = 10
-
- PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value))
- PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value))
-
- def opApplyPropertyLimits(self, obj):
- '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.'''
- # Limit sample interval
- if obj.SampleInterval.Value < 0.001:
- obj.SampleInterval.Value = 0.001
- PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.001 to 25.4 millimeters.'))
- if obj.SampleInterval.Value > 25.4:
- obj.SampleInterval.Value = 25.4
- PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.001 to 25.4 millimeters.'))
-
- # Limit cut pattern angle
- if obj.CutPatternAngle < -360.0:
- obj.CutPatternAngle = 0.0
- PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +-360 degrees.'))
- if obj.CutPatternAngle >= 360.0:
- obj.CutPatternAngle = 0.0
- PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +- 360 degrees.'))
-
- # Limit StepOver to natural number percentage
- if obj.StepOver > 100:
- obj.StepOver = 100
- if obj.StepOver < 1:
- obj.StepOver = 1
-
- # Limit AvoidLastX_Faces to zero and positive values
- if obj.AvoidLastX_Faces < 0:
- obj.AvoidLastX_Faces = 0
- PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Only zero or positive values permitted.'))
- if obj.AvoidLastX_Faces > 100:
- obj.AvoidLastX_Faces = 100
- PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.'))
-
- def opExecute(self, obj):
- '''opExecute(obj) ... process surface operation'''
- PathLog.track()
-
- self.modelSTLs = list()
- self.safeSTLs = list()
- self.modelTypes = list()
- self.boundBoxes = list()
- self.profileShapes = list()
- self.collectiveShapes = list()
- self.individualShapes = list()
- self.avoidShapes = list()
- self.geoTlrnc = None
- self.tempGroup = None
- self.CutClimb = False
- self.closedGap = False
- self.gaps = [0.1, 0.2, 0.3]
- CMDS = list()
- modelVisibility = list()
- FCAD = FreeCAD.ActiveDocument
-
- # Set debugging behavior
- self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects
- self.showDebugObjects = obj.ShowTempObjects
- deleteTempsFlag = True # Set to False for debugging
- if PathLog.getLevel(PathLog.thisModule()) == 4:
- deleteTempsFlag = False
- else:
- self.showDebugObjects = False
-
- # mark beginning of operation and identify parent Job
- PathLog.info('\nBegin Waterline operation...')
- startTime = time.time()
-
- # Identify parent Job
- JOB = PathUtils.findParentJob(obj)
- if JOB is None:
- PathLog.error(translate('PathWaterline', "No JOB"))
- return
- self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin
-
- # set cut mode; reverse as needed
- if obj.CutMode == 'Climb':
- self.CutClimb = True
- if obj.CutPatternReversed is True:
- if self.CutClimb is True:
- self.CutClimb = False
- else:
- self.CutClimb = True
-
- # Begin GCode for operation with basic information
- # ... and move cutter to clearance height and startpoint
- output = ''
- if obj.Comment != '':
- self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {}))
- self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {}))
- self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {}))
- self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {}))
- self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {}))
- self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {}))
- self.commandlist.append(Path.Command('N ({})'.format(output), {}))
- self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
- if obj.UseStartPoint is True:
- self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid}))
-
- # Instantiate additional class operation variables
- self.resetOpVariables()
-
- # Impose property limits
- self.opApplyPropertyLimits(obj)
-
- # Create temporary group for temporary objects, removing existing
- # if self.showDebugObjects is True:
- tempGroupName = 'tempPathWaterlineGroup'
- if FCAD.getObject(tempGroupName):
- for to in FCAD.getObject(tempGroupName).Group:
- FCAD.removeObject(to.Name)
- FCAD.removeObject(tempGroupName) # remove temp directory if already exists
- if FCAD.getObject(tempGroupName + '001'):
- for to in FCAD.getObject(tempGroupName + '001').Group:
- FCAD.removeObject(to.Name)
- FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists
- tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName)
- tempGroupName = tempGroup.Name
- self.tempGroup = tempGroup
- tempGroup.purgeTouched()
- # Add temp object to temp group folder with following code:
- # ... self.tempGroup.addObject(OBJ)
-
- # Setup cutter for OCL and cutout value for operation - based on tool controller properties
- self.cutter = self.setOclCutter(obj)
- self.safeCutter = self.setOclCutter(obj, safe=True)
- if self.cutter is False or self.safeCutter is False:
- PathLog.error(translate('PathWaterline', "Canceling Waterline operation. Error creating OCL cutter."))
- return
- toolDiam = self.cutter.getDiameter()
- self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0))
- self.radius = toolDiam / 2.0
- self.gaps = [toolDiam, toolDiam, toolDiam]
-
- # Get height offset values for later use
- self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value
- self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value
-
- # Set deflection values for mesh generation
- useDGT = False
- try: # try/except is for Path Jobs created before GeometryTolerance
- self.geoTlrnc = JOB.GeometryTolerance.Value
- if self.geoTlrnc == 0.0:
- useDGT = True
- except AttributeError as ee:
- PathLog.warning('{}\nPlease set Job.GeometryTolerance to an acceptable value. Using PathPreferences.defaultGeometryTolerance().'.format(ee))
- useDGT = True
- if useDGT:
- import PathScripts.PathPreferences as PathPreferences
- self.geoTlrnc = PathPreferences.defaultGeometryTolerance()
-
- # Calculate default depthparams for operation
- self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value)
- self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0
-
- # make circle for workplane
- self.wpc = Part.makeCircle(2.0)
-
- # Save model visibilities for restoration
- if FreeCAD.GuiUp:
- for m in range(0, len(JOB.Model.Group)):
- mNm = JOB.Model.Group[m].Name
- modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility)
-
- # Setup STL, model type, and bound box containers for each model in Job
- for m in range(0, len(JOB.Model.Group)):
- M = JOB.Model.Group[m]
- self.modelSTLs.append(False)
- self.safeSTLs.append(False)
- self.profileShapes.append(False)
- # Set bound box
- if obj.BoundBox == 'BaseBoundBox':
- if M.TypeId.startswith('Mesh'):
- self.modelTypes.append('M') # Mesh
- self.boundBoxes.append(M.Mesh.BoundBox)
- else:
- self.modelTypes.append('S') # Solid
- self.boundBoxes.append(M.Shape.BoundBox)
- elif obj.BoundBox == 'Stock':
- self.modelTypes.append('S') # Solid
- self.boundBoxes.append(JOB.Stock.Shape.BoundBox)
-
- # ###### MAIN COMMANDS FOR OPERATION ######
-
- # Begin processing obj.Base data and creating GCode
- # Process selected faces, if available
- pPM = self._preProcessModel(JOB, obj)
- if pPM is False:
- PathLog.error('Unable to pre-process obj.Base.')
- else:
- (FACES, VOIDS) = pPM
-
- # Create OCL.stl model objects
- if obj.Algorithm == 'OCL Dropcutter':
- self._prepareModelSTLs(JOB, obj)
- PathLog.debug('obj.LinearDeflection.Value: {}'.format(obj.LinearDeflection.Value))
- PathLog.debug('obj.AngularDeflection.Value: {}'.format(obj.AngularDeflection.Value))
-
- for m in range(0, len(JOB.Model.Group)):
- Mdl = JOB.Model.Group[m]
- if FACES[m] is False:
- PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label))
- else:
- if m > 0:
- # Raise to clearance between models
- CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label)))
- CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
- PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label))
- # make stock-model-voidShapes STL model for avoidance detection on transitions
- if obj.Algorithm == 'OCL Dropcutter':
- self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m])
- # time.sleep(0.2)
- # Process model/faces - OCL objects must be ready
- CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m]))
-
- # Save gcode produced
- self.commandlist.extend(CMDS)
-
- # ###### CLOSING COMMANDS FOR OPERATION ######
-
- # Delete temporary objects
- # Restore model visibilities for restoration
- if FreeCAD.GuiUp:
- FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False
- for m in range(0, len(JOB.Model.Group)):
- M = JOB.Model.Group[m]
- M.Visibility = modelVisibility[m]
-
- if deleteTempsFlag is True:
- for to in tempGroup.Group:
- if hasattr(to, 'Group'):
- for go in to.Group:
- FCAD.removeObject(go.Name)
- FCAD.removeObject(to.Name)
- FCAD.removeObject(tempGroupName)
- else:
- if len(tempGroup.Group) == 0:
- FCAD.removeObject(tempGroupName)
- else:
- tempGroup.purgeTouched()
-
- # Provide user feedback for gap sizes
- gaps = list()
- for g in self.gaps:
- if g != toolDiam:
- gaps.append(g)
- if len(gaps) > 0:
- obj.GapSizes = '{} mm'.format(gaps)
- else:
- if self.closedGap is True:
- obj.GapSizes = 'Closed gaps < Gap Threshold.'
- else:
- obj.GapSizes = 'No gaps identified.'
-
- # clean up class variables
- self.resetOpVariables()
- self.deleteOpVariables()
-
- self.modelSTLs = None
- self.safeSTLs = None
- self.modelTypes = None
- self.boundBoxes = None
- self.gaps = None
- self.closedGap = None
- self.SafeHeightOffset = None
- self.ClearHeightOffset = None
- self.depthParams = None
- self.midDep = None
- self.wpc = None
- del self.modelSTLs
- del self.safeSTLs
- del self.modelTypes
- del self.boundBoxes
- del self.gaps
- del self.closedGap
- del self.SafeHeightOffset
- del self.ClearHeightOffset
- del self.depthParams
- del self.midDep
- del self.wpc
-
- execTime = time.time() - startTime
- PathLog.info('Operation time: {} sec.'.format(execTime))
-
- return True
-
- # Methods for constructing the cut area
- def _preProcessModel(self, JOB, obj):
- PathLog.debug('_preProcessModel()')
-
- FACES = list()
- VOIDS = list()
- fShapes = list()
- vShapes = list()
- preProcEr = translate('PathWaterline', 'Error pre-processing Face')
- warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face')
- GRP = JOB.Model.Group
- lenGRP = len(GRP)
-
- # Crete place holders for each base model in Job
- for m in range(0, lenGRP):
- FACES.append(False)
- VOIDS.append(False)
- fShapes.append(False)
- vShapes.append(False)
-
- # The user has selected subobjects from the base. Pre-Process each.
- if obj.Base and len(obj.Base) > 0:
- PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.')
-
- (FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS)
-
- # Cycle through each base model, processing faces for each
- for m in range(0, lenGRP):
- base = GRP[m]
- (mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS)
- fShapes[m] = mFS
- vShapes[m] = mVS
- self.profileShapes[m] = mPS
- else:
- PathLog.debug(' -No obj.Base data.')
- for m in range(0, lenGRP):
- self.modelSTLs[m] = True
-
- # Process each model base, as a whole, as needed
- # PathLog.debug(' -Pre-processing all models in Job.')
- for m in range(0, lenGRP):
- if fShapes[m] is False:
- PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label))
- if obj.BoundBox == 'BaseBoundBox':
- base = GRP[m]
- elif obj.BoundBox == 'Stock':
- base = JOB.Stock
-
- pPEB = self._preProcessEntireBase(obj, base, m)
- if pPEB is False:
- PathLog.error(' -Failed to pre-process base as a whole.')
- else:
- (fcShp, prflShp) = pPEB
- if fcShp is not False:
- if fcShp is True:
- PathLog.debug(' -fcShp is True.')
- fShapes[m] = True
- else:
- fShapes[m] = [fcShp]
- if prflShp is not False:
- if fcShp is not False:
- PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m]))
- if vShapes[m] is not False:
- PathLog.debug(' -Cutting void from base profile shape.')
- adjPS = prflShp.cut(vShapes[m][0])
- self.profileShapes[m] = [adjPS]
- else:
- PathLog.debug(' -vShapes[m] is False.')
- self.profileShapes[m] = [prflShp]
- else:
- PathLog.debug(' -Saving base profile shape.')
- self.profileShapes[m] = [prflShp]
- PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m]))
- # Efor
-
- return (fShapes, vShapes)
-
- def _identifyFacesAndVoids(self, JOB, obj, F, V):
- TUPS = list()
- GRP = JOB.Model.Group
- lenGRP = len(GRP)
-
- # Separate selected faces into (base, face) tuples and flag model(s) for STL creation
- for (bs, SBS) in obj.Base:
- for sb in SBS:
- # Flag model for STL creation
- mdlIdx = None
- for m in range(0, lenGRP):
- if bs is GRP[m]:
- self.modelSTLs[m] = True
- mdlIdx = m
- break
- TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub)
-
- # Apply `AvoidXFaces` value
- faceCnt = len(TUPS)
- add = faceCnt - obj.AvoidLastX_Faces
- for bst in range(0, faceCnt):
- (m, base, sub) = TUPS[bst]
- shape = getattr(base.Shape, sub)
- if isinstance(shape, Part.Face):
- faceIdx = int(sub[4:]) - 1
- if bst < add:
- if F[m] is False:
- F[m] = list()
- F[m].append((shape, faceIdx))
- else:
- if V[m] is False:
- V[m] = list()
- V[m].append((shape, faceIdx))
- return (F, V)
-
- def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS):
- mFS = False
- mVS = False
- mPS = False
- mIFS = list()
- BB = base.Shape.BoundBox
-
- if FACES[m] is not False:
- isHole = False
- if obj.HandleMultipleFeatures == 'Collectively':
- cont = True
- fsL = list() # face shape list
- ifL = list() # avoid shape list
- outFCS = list()
-
- # Get collective envelope slice of selected faces
- for (fcshp, fcIdx) in FACES[m]:
- fNum = fcIdx + 1
- fsL.append(fcshp)
- gFW = self._getFaceWires(base, fcshp, fcIdx)
- if gFW is False:
- PathLog.debug('Failed to get wires from Face{}'.format(fNum))
- elif gFW[0] is False:
- PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
- else:
- ((otrFace, raised), intWires) = gFW
- outFCS.append(otrFace)
- if obj.InternalFeaturesCut is False:
- if intWires is not False:
- for (iFace, rsd) in intWires:
- ifL.append(iFace)
-
- PathLog.debug('Attempting to get cross-section of collective faces.')
- if len(outFCS) == 0:
- PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum))
- cont = False
- else:
- cfsL = Part.makeCompound(outFCS)
-
- # Handle profile edges request
- if cont is True and obj.ProfileEdges != 'None':
- ofstVal = self._calculateOffsetValue(obj, isHole)
- psOfst = self._extractFaceOffset(obj, cfsL, ofstVal)
- if psOfst is not False:
- mPS = [psOfst]
- if obj.ProfileEdges == 'Only':
- mFS = True
- cont = False
- else:
- PathLog.error(' -Failed to create profile geometry for selected faces.')
- cont = False
-
- if cont is True:
- if self.showDebugObjects is True:
- T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape')
- T.Shape = cfsL
- T.purgeTouched()
- self.tempGroup.addObject(T)
-
- ofstVal = self._calculateOffsetValue(obj, isHole)
- faceOfstShp = self._extractFaceOffset(obj, cfsL, ofstVal)
- if faceOfstShp is False:
- PathLog.error(' -Failed to create offset face.')
- cont = False
-
- if cont is True:
- lenIfL = len(ifL)
- if obj.InternalFeaturesCut is False:
- if lenIfL == 0:
- PathLog.debug(' -No internal features saved.')
- else:
- if lenIfL == 1:
- casL = ifL[0]
- else:
- casL = Part.makeCompound(ifL)
- if self.showDebugObjects is True:
- C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat')
- C.Shape = casL
- C.purgeTouched()
- self.tempGroup.addObject(C)
- ofstVal = self._calculateOffsetValue(obj, isHole=True)
- intOfstShp = self._extractFaceOffset(obj, casL, ofstVal)
- mIFS.append(intOfstShp)
- # faceOfstShp = faceOfstShp.cut(intOfstShp)
-
- mFS = [faceOfstShp]
- # Eif
-
- elif obj.HandleMultipleFeatures == 'Individually':
- for (fcshp, fcIdx) in FACES[m]:
- cont = True
- fsL = list() # face shape list
- ifL = list() # avoid shape list
- fNum = fcIdx + 1
- outerFace = False
-
- gFW = self._getFaceWires(base, fcshp, fcIdx)
- if gFW is False:
- PathLog.debug('Failed to get wires from Face{}'.format(fNum))
- cont = False
- elif gFW[0] is False:
- PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
- cont = False
- outerFace = False
- else:
- ((otrFace, raised), intWires) = gFW
- outerFace = otrFace
- if obj.InternalFeaturesCut is False:
- if intWires is not False:
- for (iFace, rsd) in intWires:
- ifL.append(iFace)
-
- if outerFace is not False:
- PathLog.debug('Attempting to create offset face of Face{}'.format(fNum))
-
- if obj.ProfileEdges != 'None':
- ofstVal = self._calculateOffsetValue(obj, isHole)
- psOfst = self._extractFaceOffset(obj, outerFace, ofstVal)
- if psOfst is not False:
- if mPS is False:
- mPS = list()
- mPS.append(psOfst)
- if obj.ProfileEdges == 'Only':
- if mFS is False:
- mFS = list()
- mFS.append(True)
- cont = False
- else:
- PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum))
- cont = False
-
- if cont is True:
- ofstVal = self._calculateOffsetValue(obj, isHole)
- faceOfstShp = self._extractFaceOffset(obj, slc, ofstVal)
-
- lenIfl = len(ifL)
- if obj.InternalFeaturesCut is False and lenIfl > 0:
- if lenIfl == 1:
- casL = ifL[0]
- else:
- casL = Part.makeCompound(ifL)
-
- ofstVal = self._calculateOffsetValue(obj, isHole=True)
- intOfstShp = self._extractFaceOffset(obj, casL, ofstVal)
- mIFS.append(intOfstShp)
- # faceOfstShp = faceOfstShp.cut(intOfstShp)
-
- if mFS is False:
- mFS = list()
- mFS.append(faceOfstShp)
- # Eif
- # Efor
- # Eif
- # Eif
-
- if len(mIFS) > 0:
- if mVS is False:
- mVS = list()
- for ifs in mIFS:
- mVS.append(ifs)
-
- if VOIDS[m] is not False:
- PathLog.debug('Processing avoid faces.')
- cont = True
- isHole = False
- outFCS = list()
- intFEAT = list()
-
- for (fcshp, fcIdx) in VOIDS[m]:
- fNum = fcIdx + 1
- gFW = self._getFaceWires(base, fcshp, fcIdx)
- if gFW is False:
- PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum))
- cont = False
- else:
- ((otrFace, raised), intWires) = gFW
- outFCS.append(otrFace)
- if obj.AvoidLastX_InternalFeatures is False:
- if intWires is not False:
- for (iFace, rsd) in intWires:
- intFEAT.append(iFace)
-
- lenOtFcs = len(outFCS)
- if lenOtFcs == 0:
- cont = False
- else:
- if lenOtFcs == 1:
- avoid = outFCS[0]
- else:
- avoid = Part.makeCompound(outFCS)
-
- if self.showDebugObjects is True:
- PathLog.debug('*** tmpAvoidArea')
- P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope')
- P.Shape = avoid
- # P.recompute()
- P.purgeTouched()
- self.tempGroup.addObject(P)
-
- if cont is True:
- if self.showDebugObjects is True:
- PathLog.debug('*** tmpVoidCompound')
- P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound')
- P.Shape = avoid
- # P.recompute()
- P.purgeTouched()
- self.tempGroup.addObject(P)
- ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True)
- avdOfstShp = self._extractFaceOffset(obj, avoid, ofstVal)
- if avdOfstShp is False:
- PathLog.error('Failed to create collective offset avoid face.')
- cont = False
-
- if cont is True:
- avdShp = avdOfstShp
-
- if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0:
- if len(intFEAT) > 1:
- ifc = Part.makeCompound(intFEAT)
- else:
- ifc = intFEAT[0]
- ofstVal = self._calculateOffsetValue(obj, isHole=True)
- ifOfstShp = self._extractFaceOffset(obj, ifc, ofstVal)
- if ifOfstShp is False:
- PathLog.error('Failed to create collective offset avoid internal features.')
- else:
- avdShp = avdOfstShp.cut(ifOfstShp)
-
- if mVS is False:
- mVS = list()
- mVS.append(avdShp)
-
-
- return (mFS, mVS, mPS)
-
- def _getFaceWires(self, base, fcshp, fcIdx):
- outFace = False
- INTFCS = list()
- fNum = fcIdx + 1
- # preProcEr = translate('PathWaterline', 'Error pre-processing Face')
- warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face')
-
- PathLog.debug('_getFaceWires() from Face{}'.format(fNum))
- WIRES = self._extractWiresFromFace(base, fcshp)
- if WIRES is False:
- PathLog.error('Failed to extract wires from Face{}'.format(fNum))
- return False
-
- # Process remaining internal features, adding to FCS list
- lenW = len(WIRES)
- for w in range(0, lenW):
- (wire, rsd) = WIRES[w]
- PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd))
- if wire.isClosed() is False:
- PathLog.debug(' -wire is not closed.')
- else:
- slc = self._flattenWireToFace(wire)
- if slc is False:
- PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum))
- else:
- if w == 0:
- outFace = (slc, rsd)
- else:
- # add to VOIDS so cutter avoids area.
- PathLog.warning(warnFinDep + str(fNum) + '.')
- INTFCS.append((slc, rsd))
- if len(INTFCS) == 0:
- return (outFace, False)
- else:
- return (outFace, INTFCS)
-
- def _preProcessEntireBase(self, obj, base, m):
- cont = True
- isHole = False
- prflShp = False
- # Create envelope, extract cross-section and make offset co-planar shape
- # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams)
-
- try:
- baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape
- except Exception as ee:
- PathLog.error(str(ee))
- shell = base.Shape.Shells[0]
- solid = Part.makeSolid(shell)
- try:
- baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape
- except Exception as eee:
- PathLog.error(str(eee))
- cont = False
- # time.sleep(0.2)
-
- if cont is True:
- csFaceShape = self._getShapeSlice(baseEnv)
- if csFaceShape is False:
- PathLog.debug('_getShapeSlice(baseEnv) failed')
- csFaceShape = self._getCrossSection(baseEnv)
- if csFaceShape is False:
- PathLog.debug('_getCrossSection(baseEnv) failed')
- csFaceShape = self._getSliceFromEnvelope(baseEnv)
- if csFaceShape is False:
- PathLog.error('Failed to slice baseEnv shape.')
- cont = False
-
- if cont is True and obj.ProfileEdges != 'None':
- PathLog.debug(' -Attempting profile geometry for model base.')
- ofstVal = self._calculateOffsetValue(obj, isHole)
- psOfst = self._extractFaceOffset(obj, csFaceShape, ofstVal)
- if psOfst is not False:
- if obj.ProfileEdges == 'Only':
- return (True, psOfst)
- prflShp = psOfst
- else:
- PathLog.error(' -Failed to create profile geometry.')
- cont = False
-
- if cont is True:
- ofstVal = self._calculateOffsetValue(obj, isHole)
- faceOffsetShape = self._extractFaceOffset(obj, csFaceShape, ofstVal)
- if faceOffsetShape is False:
- PathLog.error('_extractFaceOffset() failed.')
- else:
- faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin))
- return (faceOffsetShape, prflShp)
- return False
-
- def _extractWiresFromFace(self, base, fc):
- '''_extractWiresFromFace(base, fc) ...
- Attempts to return all closed wires within a parent face, including the outer most wire of the parent.
- The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True).
- '''
- PathLog.debug('_extractWiresFromFace()')
-
- WIRES = list()
- lenWrs = len(fc.Wires)
- PathLog.debug(' -Wire count: {}'.format(lenWrs))
-
- def index0(tup):
- return tup[0]
-
- # Cycle through wires in face
- for w in range(0, lenWrs):
- PathLog.debug(' -Analyzing wire_{}'.format(w + 1))
- wire = fc.Wires[w]
- checkEdges = False
- cont = True
-
- # Check for closed edges (circles, ellipses, etc...)
- for E in wire.Edges:
- if E.isClosed() is True:
- checkEdges = True
- break
-
- if checkEdges is True:
- PathLog.debug(' -checkEdges is True')
- for e in range(0, len(wire.Edges)):
- edge = wire.Edges[e]
- if edge.isClosed() is True and edge.Mass > 0.01:
- PathLog.debug(' -Found closed edge')
- raised = False
- ip = self._isPocket(base, fc, edge)
- if ip is False:
- raised = True
- ebb = edge.BoundBox
- eArea = ebb.XLength * ebb.YLength
- F = Part.Face(Part.Wire([edge]))
- WIRES.append((eArea, F.Wires[0], raised))
- cont = False
-
- if cont is True:
- PathLog.debug(' -cont is True')
- # If only one wire and not checkEdges, return first wire
- if lenWrs == 1:
- return [(wire, False)]
-
- raised = False
- wbb = wire.BoundBox
- wArea = wbb.XLength * wbb.YLength
- if w > 0:
- ip = self._isPocket(base, fc, wire)
- if ip is False:
- raised = True
- WIRES.append((wArea, Part.Wire(wire.Edges), raised))
-
- nf = len(WIRES)
- if nf > 0:
- PathLog.debug(' -number of wires found is {}'.format(nf))
- if nf == 1:
- (area, W, raised) = WIRES[0]
- return [(W, raised)]
- else:
- sortedWIRES = sorted(WIRES, key=index0, reverse=True)
- return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size
-
- return False
-
- def _calculateOffsetValue(self, obj, isHole, isVoid=False):
- '''_calculateOffsetValue(obj, isHole, isVoid) ... internal function.
- Calculate the offset for the Path.Area() function.'''
- JOB = PathUtils.findParentJob(obj)
- tolrnc = JOB.GeometryTolerance.Value
-
- if isVoid is False:
- if isHole is True:
- offset = -1 * obj.InternalFeaturesAdjustment.Value
- offset += self.radius # (self.radius + (tolrnc / 10.0))
- else:
- offset = -1 * obj.BoundaryAdjustment.Value
- if obj.BoundaryEnforcement is True:
- offset += self.radius # (self.radius + (tolrnc / 10.0))
- else:
- offset -= self.radius # (self.radius + (tolrnc / 10.0))
- offset = 0.0 - offset
- else:
- offset = -1 * obj.BoundaryAdjustment.Value
- offset += self.radius # (self.radius + (tolrnc / 10.0))
-
- return offset
-
- def _extractFaceOffset(self, obj, fcShape, offset, makeComp=True):
- '''_extractFaceOffset(fcShape, offset) ... internal function.
- Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified.
- Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
- PathLog.debug('_extractFaceOffset()')
-
- if fcShape.BoundBox.ZMin != 0.0:
- fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin))
-
- areaParams = {}
- areaParams['Offset'] = offset
- areaParams['Fill'] = 1 # 1
- areaParams['Coplanar'] = 0
- areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
- areaParams['Reorient'] = True
- areaParams['OpenMode'] = 0
- areaParams['MaxArcPoints'] = 400 # 400
- areaParams['Project'] = True
- # areaParams['Tolerance'] = 0.001
-
- area = Path.Area() # Create instance of Area() class object
- # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane
- area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1
- area.add(fcShape)
- area.setParams(**areaParams) # set parameters
-
- # Save parameters for debugging
- # obj.AreaParams = str(area.getParams())
- # PathLog.debug("Area with params: {}".format(area.getParams()))
-
- offsetShape = area.getShape()
- wCnt = len(offsetShape.Wires)
- if wCnt == 0:
- return False
- elif wCnt == 1:
- ofstFace = Part.Face(offsetShape.Wires[0])
- if not makeComp:
- ofstFace = [ofstFace]
- else:
- W = list()
- for wr in offsetShape.Wires:
- W.append(Part.Face(wr))
- if makeComp:
- ofstFace = Part.makeCompound(W)
- else:
- ofstFace = W
-
- return ofstFace # offsetShape
-
- def _isPocket(self, b, f, w):
- '''_isPocket(b, f, w)...
- Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion.
- Returns True if pocket, False if raised protrusion.'''
- e = w.Edges[0]
- for fi in range(0, len(b.Shape.Faces)):
- face = b.Shape.Faces[fi]
- for ei in range(0, len(face.Edges)):
- edge = face.Edges[ei]
- if e.isSame(edge) is True:
- if f is face:
- # Alternative: run loop to see if all edges are same
- pass # same source face, look for another
- else:
- if face.CenterOfMass.z < f.CenterOfMass.z:
- return True
- return False
-
- def _flattenWireToFace(self, wire):
- PathLog.debug('_flattenWireToFace()')
- if wire.isClosed() is False:
- PathLog.debug(' -wire.isClosed() is False')
- return False
-
- # If wire is planar horizontal, convert to a face and return
- if wire.BoundBox.ZLength == 0.0:
- slc = Part.Face(wire)
- return slc
-
- # Attempt to create a new wire for manipulation, if not, use original
- newWire = Part.Wire(wire.Edges)
- if newWire.isClosed() is True:
- nWire = newWire
- else:
- PathLog.debug(' -newWire.isClosed() is False')
- nWire = wire
-
- # Attempt extrusion, and then try a manual slice and then cross-section
- ext = self._getExtrudedShape(nWire)
- if ext is False:
- PathLog.debug('_getExtrudedShape() failed')
- else:
- slc = self._getShapeSlice(ext)
- if slc is not False:
- return slc
- cs = self._getCrossSection(ext, True)
- if cs is not False:
- return cs
-
- # Attempt creating an envelope, and then try a manual slice and then cross-section
- env = self._getShapeEnvelope(nWire)
- if env is False:
- PathLog.debug('_getShapeEnvelope() failed')
- else:
- slc = self._getShapeSlice(env)
- if slc is not False:
- return slc
- cs = self._getCrossSection(env, True)
- if cs is not False:
- return cs
-
- # Attempt creating a projection
- slc = self._getProjectedFace(nWire)
- if slc is False:
- PathLog.debug('_getProjectedFace() failed')
- else:
- return slc
-
- return False
-
- def _getExtrudedShape(self, wire):
- PathLog.debug('_getExtrudedShape()')
- wBB = wire.BoundBox
- extFwd = math.floor(2.0 * wBB.ZLength) + 10.0
-
- try:
- # slower, but renders collective faces correctly. Method 5 in TESTING
- shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd))
- except Exception as ee:
- PathLog.error(' -extrude wire failed: \n{}'.format(ee))
- return False
-
- SHP = Part.makeSolid(shell)
- return SHP
-
- def _getShapeSlice(self, shape):
- PathLog.debug('_getShapeSlice()')
-
- bb = shape.BoundBox
- mid = (bb.ZMin + bb.ZMax) / 2.0
- xmin = bb.XMin - 1.0
- xmax = bb.XMax + 1.0
- ymin = bb.YMin - 1.0
- ymax = bb.YMax + 1.0
- p1 = FreeCAD.Vector(xmin, ymin, mid)
- p2 = FreeCAD.Vector(xmax, ymin, mid)
- p3 = FreeCAD.Vector(xmax, ymax, mid)
- p4 = FreeCAD.Vector(xmin, ymax, mid)
-
- e1 = Part.makeLine(p1, p2)
- e2 = Part.makeLine(p2, p3)
- e3 = Part.makeLine(p3, p4)
- e4 = Part.makeLine(p4, p1)
- face = Part.Face(Part.Wire([e1, e2, e3, e4]))
- fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area
- sArea = shape.BoundBox.XLength * shape.BoundBox.YLength
- midArea = (fArea + sArea) / 2.0
-
- slcShp = shape.common(face)
- slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength
-
- if slcArea < midArea:
- for W in slcShp.Wires:
- if W.isClosed() is False:
- PathLog.debug(' -wire.isClosed() is False')
- return False
- if len(slcShp.Wires) == 1:
- wire = slcShp.Wires[0]
- slc = Part.Face(wire)
- slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
- return slc
- else:
- fL = list()
- for W in slcShp.Wires:
- slc = Part.Face(W)
- slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
- fL.append(slc)
- comp = Part.makeCompound(fL)
- if self.showDebugObjects is True:
- PathLog.debug('*** tmpSliceCompound')
- P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound')
- P.Shape = comp
- # P.recompute()
- P.purgeTouched()
- self.tempGroup.addObject(P)
- return comp
-
- PathLog.debug(' -slcArea !< midArea')
- PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges)))
- return False
-
- def _getProjectedFace(self, wire):
- PathLog.debug('_getProjectedFace()')
- F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire')
- F.Shape = wire
- F.purgeTouched()
- self.tempGroup.addObject(F)
- try:
- prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1))
- prj.recompute()
- prj.purgeTouched()
- self.tempGroup.addObject(prj)
- except Exception as ee:
- PathLog.error(str(ee))
- return False
- else:
- pWire = Part.Wire(prj.Shape.Edges)
- if pWire.isClosed() is False:
- # PathLog.debug(' -pWire.isClosed() is False')
- return False
- slc = Part.Face(pWire)
- slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
- return slc
- return False
-
- def _getCrossSection(self, shape, withExtrude=False):
- PathLog.debug('_getCrossSection()')
- wires = list()
- bb = shape.BoundBox
- mid = (bb.ZMin + bb.ZMax) / 2.0
-
- for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid):
- wires.append(i)
-
- if len(wires) > 0:
- comp = Part.Compound(wires) # produces correct cross-section wire !
- comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin))
- csWire = comp.Wires[0]
- if csWire.isClosed() is False:
- PathLog.debug(' -comp.Wires[0] is not closed')
- return False
- if withExtrude is True:
- ext = self._getExtrudedShape(csWire)
- CS = self._getShapeSlice(ext)
- else:
- CS = Part.Face(csWire)
- CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin))
- return CS
- else:
- PathLog.debug(' -No wires from .slice() method')
-
- return False
-
- def _getShapeEnvelope(self, shape):
- PathLog.debug('_getShapeEnvelope()')
-
- wBB = shape.BoundBox
- extFwd = wBB.ZLength + 10.0
- minz = wBB.ZMin
- maxz = wBB.ZMin + extFwd
- stpDwn = (maxz - minz) / 4.0
- dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz)
-
- try:
- env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape
- except Exception as ee:
- PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee))
- return False
- else:
- return env
-
- return False
-
- def _getSliceFromEnvelope(self, env):
- PathLog.debug('_getSliceFromEnvelope()')
- eBB = env.BoundBox
- extFwd = eBB.ZLength + 10.0
- maxz = eBB.ZMin + extFwd
-
- maxMax = env.Edges[0].BoundBox.ZMin
- emax = math.floor(maxz - 1.0)
- E = list()
- for e in range(0, len(env.Edges)):
- emin = env.Edges[e].BoundBox.ZMin
- if emin > emax:
- E.append(env.Edges[e])
- tf = Part.Face(Part.Wire(Part.__sortEdges__(E)))
- tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin))
-
- return tf
-
- def _prepareModelSTLs(self, JOB, obj):
- PathLog.debug('_prepareModelSTLs()')
- for m in range(0, len(JOB.Model.Group)):
- M = JOB.Model.Group[m]
-
- # PathLog.debug(f" -self.modelTypes[{m}] == 'M'")
- if self.modelTypes[m] == 'M':
- #TODO: test if this works
- facets = M.Mesh.Facets.Points
- else:
- facets = Path.getFacets(M.Shape)
-
- if self.modelSTLs[m] is True:
- stl = ocl.STLSurf()
-
- for tri in facets:
- t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2] + obj.DepthOffset.Value),
- ocl.Point(tri[1][0], tri[1][1], tri[1][2] + obj.DepthOffset.Value),
- ocl.Point(tri[2][0], tri[2][1], tri[2][2] + obj.DepthOffset.Value))
- stl.addTriangle(t)
- self.modelSTLs[m] = stl
- return
-
- def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes):
- '''_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.'''
- PathLog.debug('_makeSafeSTL()')
-
- fuseShapes = list()
- Mdl = JOB.Model.Group[mdlIdx]
- FCAD = FreeCAD.ActiveDocument
- mBB = Mdl.Shape.BoundBox
- sBB = JOB.Stock.Shape.BoundBox
-
- # add Model shape to safeSTL shape
- fuseShapes.append(Mdl.Shape)
-
- if obj.BoundBox == 'BaseBoundBox':
- cont = False
- extFwd = (sBB.ZLength)
- zmin = mBB.ZMin
- zmax = mBB.ZMin + extFwd
- stpDwn = (zmax - zmin) / 4.0
- dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin)
-
- try:
- envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape
- cont = True
- except Exception as ee:
- PathLog.error(str(ee))
- shell = Mdl.Shape.Shells[0]
- solid = Part.makeSolid(shell)
- try:
- envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape
- cont = True
- except Exception as eee:
- PathLog.error(str(eee))
-
- if cont is True:
- stckWst = JOB.Stock.Shape.cut(envBB)
- if obj.BoundaryAdjustment > 0.0:
- cmpndFS = Part.makeCompound(faceShapes)
- baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape
- adjStckWst = stckWst.cut(baBB)
- else:
- adjStckWst = stckWst
- fuseShapes.append(adjStckWst)
- else:
- PathLog.warning('Path transitions might not avoid the model. Verify paths.')
- # time.sleep(0.3)
-
- else:
- # If boundbox is Job.Stock, add hidden pad under stock as base plate
- toolDiam = self.cutter.getDiameter()
- zMin = JOB.Stock.Shape.BoundBox.ZMin
- xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam
- yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam
- bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam)
- bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam)
- bH = 1.0
- crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0)
- B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1))
- fuseShapes.append(B)
-
- if voidShapes is not False:
- voidComp = Part.makeCompound(voidShapes)
- voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape
- fuseShapes.append(voidEnv)
-
- fused = Part.makeCompound(fuseShapes)
-
- if self.showDebugObjects is True:
- T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape')
- T.Shape = fused
- T.purgeTouched()
- self.tempGroup.addObject(T)
-
- facets = Path.getFacets(fused)
-
- stl = ocl.STLSurf()
- for tri in facets:
- t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
- ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
- ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
- stl.addTriangle(t)
-
- self.safeSTLs[mdlIdx] = stl
-
- def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS):
- '''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)...
- This method applies any avoided faces or regions to the selected faces.
- It then calls the correct method.'''
- PathLog.debug('_processCutAreas()')
-
- final = list()
- base = JOB.Model.Group[mdlIdx]
-
- # Process faces Collectively or Individually
- if obj.HandleMultipleFeatures == 'Collectively':
- if FCS is True:
- COMP = False
- else:
- ADD = Part.makeCompound(FCS)
- if VDS is not False:
- DEL = Part.makeCompound(VDS)
- COMP = ADD.cut(DEL)
- else:
- COMP = ADD
-
- final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
- if obj.Algorithm == 'OCL Dropcutter':
- final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
- else:
- final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
-
- elif obj.HandleMultipleFeatures == 'Individually':
- for fsi in range(0, len(FCS)):
- fShp = FCS[fsi]
- # self.deleteOpVariables(all=False)
- self.resetOpVariables(all=False)
-
- if fShp is True:
- COMP = False
- else:
- ADD = Part.makeCompound([fShp])
- if VDS is not False:
- DEL = Part.makeCompound(VDS)
- COMP = ADD.cut(DEL)
- else:
- COMP = ADD
-
- final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
- if obj.Algorithm == 'OCL Dropcutter':
- final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
- else:
- final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
- COMP = None
- # Eif
-
- return final
-
- # Methods for creating path geometry
- def _planarMakePathGeom(self, obj, faceShp):
- '''_planarMakePathGeom(obj, faceShp)...
- Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp.
- The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.'''
- PathLog.debug('_planarMakePathGeom()')
- GeoSet = list()
-
- # Apply drop cutter extra offset and set the max and min XY area of the operation
- xmin = faceShp.BoundBox.XMin
- xmax = faceShp.BoundBox.XMax
- ymin = faceShp.BoundBox.YMin
- ymax = faceShp.BoundBox.YMax
- zmin = faceShp.BoundBox.ZMin
- zmax = faceShp.BoundBox.ZMax
-
- # Compute weighted center of mass of all faces combined
- fCnt = 0
- totArea = 0.0
- zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0)
- for F in faceShp.Faces:
- comF = F.CenterOfMass
- areaF = F.Area
- totArea += areaF
- fCnt += 1
- zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF))
- if fCnt == 0:
- PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.'))
- zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0)
- else:
- avgArea = totArea / fCnt
- zeroCOM.multiply(1 / fCnt)
- zeroCOM.multiply(1 / avgArea)
- COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0)
-
- # get X, Y, Z spans; Compute center of rotation
- deltaX = abs(xmax-xmin)
- deltaY = abs(ymax-ymin)
- deltaZ = abs(zmax-zmin)
- deltaC = math.sqrt(deltaX**2 + deltaY**2)
- lineLen = deltaC + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end
- halfLL = math.ceil(lineLen / 2.0)
- cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen
- halfPasses = math.ceil(cutPasses / 2.0)
- bbC = faceShp.BoundBox.Center
-
- # Generate the Draft line/circle sets to be intersected with the cut-face-area
- if obj.CutPattern in ['ZigZag', 'Line']:
- MaxLC = -1
- centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model
- cAng = math.atan(deltaX / deltaY) # BoundaryBox angle
-
- # Determine end points and create top lines
- x1 = centRot.x - halfLL
- x2 = centRot.x + halfLL
- diag = None
- if obj.CutPatternAngle == 0 or obj.CutPatternAngle == 180:
- MaxLC = math.floor(deltaY / self.cutOut)
- diag = deltaY
- elif obj.CutPatternAngle == 90 or obj.CutPatternAngle == 270:
- MaxLC = math.floor(deltaX / self.cutOut)
- diag = deltaX
- else:
- perpDist = math.cos(cAng - math.radians(obj.CutPatternAngle)) * deltaC
- MaxLC = math.floor(perpDist / self.cutOut)
- diag = perpDist
- y1 = centRot.y + diag
- # y2 = y1
-
- p1 = FreeCAD.Vector(x1, y1, 0.0)
- p2 = FreeCAD.Vector(x2, y1, 0.0)
- topLineTuple = (p1, p2)
- ny1 = centRot.y - diag
- n1 = FreeCAD.Vector(x1, ny1, 0.0)
- n2 = FreeCAD.Vector(x2, ny1, 0.0)
- negTopLineTuple = (n1, n2)
-
- # Create end points for set of lines to intersect with cross-section face
- pntTuples = list()
- for lc in range((-1 * (halfPasses - 1)), halfPasses + 1):
- # if lc == (cutPasses - MaxLC - 1):
- # pntTuples.append(negTopLineTuple)
- # if lc == (MaxLC + 1):
- # pntTuples.append(topLineTuple)
- x1 = centRot.x - halfLL
- x2 = centRot.x + halfLL
- y1 = centRot.y + (lc * self.cutOut)
- # y2 = y1
- p1 = FreeCAD.Vector(x1, y1, 0.0)
- p2 = FreeCAD.Vector(x2, y1, 0.0)
- pntTuples.append( (p1, p2) )
-
- # Convert end points to lines
- for (p1, p2) in pntTuples:
- line = Part.makeLine(p1, p2)
- GeoSet.append(line)
- elif obj.CutPattern in ['Circular', 'CircularZigZag']:
- zTgt = faceShp.BoundBox.ZMin
- axisRot = FreeCAD.Vector(0.0, 0.0, 1.0)
- cntr = FreeCAD.Placement()
- cntr.Rotation = FreeCAD.Rotation(axisRot, 0.0)
-
- if obj.CircularCenterAt == 'CenterOfMass':
- cntr.Base = FreeCAD.Vector(COM.x, COM.y, zTgt) # COM # Use center of Mass
- elif obj.CircularCenterAt == 'CenterOfBoundBox':
- cent = faceShp.BoundBox.Center
- cntr.Base = FreeCAD.Vector(cent.x, cent.y, zTgt)
- elif obj.CircularCenterAt == 'XminYmin':
- cntr.Base = FreeCAD.Vector(faceShp.BoundBox.XMin, faceShp.BoundBox.YMin, zTgt)
- elif obj.CircularCenterAt == 'Custom':
- newCent = FreeCAD.Vector(obj.CircularCenterCustom.x, obj.CircularCenterCustom.y, zTgt)
- cntr.Base = newCent
-
- # recalculate cutPasses value, if need be
- radialPasses = halfPasses
- if obj.CircularCenterAt != 'CenterOfBoundBox':
- # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center
- EBB = faceShp.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(cntr.Base).Length
- if dist > dMax:
- dMax = dist
- lineLen = dMax + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end
- radialPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen
-
- # Update COM point and current CircularCenter
- if obj.CircularCenterAt != 'Custom':
- obj.CircularCenterCustom = cntr.Base
-
- minRad = self.cutter.getDiameter() * 0.45
- siX3 = 3 * obj.SampleInterval.Value
- minRadSI = (siX3 / 2.0) / math.pi
- if minRad < minRadSI:
- minRad = minRadSI
-
- # Make small center circle to start pattern
- if obj.StepOver > 50:
- circle = Part.makeCircle(minRad, cntr.Base)
- GeoSet.append(circle)
-
- for lc in range(1, radialPasses + 1):
- rad = (lc * self.cutOut)
- if rad >= minRad:
- circle = Part.makeCircle(rad, cntr.Base)
- GeoSet.append(circle)
- # Efor
- COM = cntr.Base
- # Eif
-
- if obj.CutPatternReversed is True:
- GeoSet.reverse()
-
- if faceShp.BoundBox.ZMin != 0.0:
- faceShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceShp.BoundBox.ZMin))
-
- # Create compound object to bind all lines in Lineset
- geomShape = Part.makeCompound(GeoSet)
-
- # Position and rotate the Line and ZigZag geometry
- if obj.CutPattern in ['Line', 'ZigZag']:
- if obj.CutPatternAngle != 0.0:
- geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), obj.CutPatternAngle)
- geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin)
-
- if self.showDebugObjects is True:
- F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet')
- F.Shape = geomShape
- F.purgeTouched()
- self.tempGroup.addObject(F)
-
- # Identify intersection of cross-section face and lineset
- cmnShape = faceShp.common(geomShape)
-
- if self.showDebugObjects is True:
- F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry')
- F.Shape = cmnShape
- F.purgeTouched()
- self.tempGroup.addObject(F)
-
- self.tmpCOM = FreeCAD.Vector(COM.x, COM.y, faceShp.BoundBox.ZMin)
- return cmnShape
-
- def _pathGeomToLinesPointSet(self, obj, compGeoShp):
- '''_pathGeomToLinesPointSet(obj, compGeoShp)...
- Convert a compound set of sequential line segments to directionally-oriented collinear groupings.'''
- PathLog.debug('_pathGeomToLinesPointSet()')
- # Extract intersection line segments for return value as list()
- LINES = list()
- inLine = list()
- chkGap = False
- lnCnt = 0
- ec = len(compGeoShp.Edges)
- cutClimb = self.CutClimb
- toolDiam = 2.0 * self.radius
- cpa = obj.CutPatternAngle
-
- edg0 = compGeoShp.Edges[0]
- p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
- p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
- if cutClimb is True:
- tup = (p2, p1)
- lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
- else:
- tup = (p1, p2)
- lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
- inLine.append(tup)
- sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
-
- for ei in range(1, ec):
- chkGap = False
- edg = compGeoShp.Edges[ei] # Get edge for vertexes
- v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0
- v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1
-
- ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
- cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point)
- iC = sp.isOnLineSegment(ep, cp)
- if iC is True:
- inLine.append('BRK')
- chkGap = True
- else:
- if cutClimb is True:
- inLine.reverse()
- LINES.append(inLine) # Save inLine segments
- lnCnt += 1
- inLine = list() # reset collinear container
- if cutClimb is True:
- sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
- else:
- sp = ep
-
- if cutClimb is True:
- tup = (v2, v1)
- if chkGap is True:
- gap = abs(toolDiam - lst.sub(ep).Length)
- lst = cp
- else:
- tup = (v1, v2)
- if chkGap is True:
- gap = abs(toolDiam - lst.sub(cp).Length)
- lst = ep
-
- if chkGap is True:
- if gap < obj.GapThreshold.Value:
- b = inLine.pop() # pop off 'BRK' marker
- (vA, vB) = inLine.pop() # pop off previous line segment for combining with current
- tup = (vA, tup[1])
- self.closedGap = True
- else:
- # PathLog.debug('---- Gap: {} mm'.format(gap))
- gap = round(gap, 6)
- if gap < self.gaps[0]:
- self.gaps.insert(0, gap)
- self.gaps.pop()
- inLine.append(tup)
- # Efor
- lnCnt += 1
- if cutClimb is True:
- inLine.reverse()
- LINES.append(inLine) # Save inLine segments
-
- # Handle last inLine set, reversing it.
- if obj.CutPatternReversed is True:
- if cpa != 0.0 and cpa % 90.0 == 0.0:
- F = LINES.pop(0)
- rev = list()
- for iL in F:
- if iL == 'BRK':
- rev.append(iL)
- else:
- (p1, p2) = iL
- rev.append((p2, p1))
- rev.reverse()
- LINES.insert(0, rev)
-
- isEven = lnCnt % 2
- if isEven == 0:
- PathLog.debug('Line count is ODD.')
- else:
- PathLog.debug('Line count is even.')
-
- return LINES
-
- def _pathGeomToZigzagPointSet(self, obj, compGeoShp):
- '''_pathGeomToZigzagPointSet(obj, compGeoShp)...
- Convert a compound set of sequential line segments to directionally-oriented collinear groupings
- with a ZigZag directional indicator included for each collinear group.'''
- PathLog.debug('_pathGeomToZigzagPointSet()')
- # Extract intersection line segments for return value as list()
- LINES = list()
- inLine = list()
- lnCnt = 0
- chkGap = False
- ec = len(compGeoShp.Edges)
- toolDiam = 2.0 * self.radius
-
- if self.CutClimb is True:
- dirFlg = -1
- else:
- dirFlg = 1
-
- edg0 = compGeoShp.Edges[0]
- p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
- p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
- if dirFlg == 1:
- tup = (p1, p2)
- lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
- sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
- else:
- tup = (p2, p1)
- lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
- sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point
- inLine.append(tup)
- otr = lst
-
- for ei in range(1, ec):
- edg = compGeoShp.Edges[ei]
- v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y)
- v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y)
-
- cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment)
- ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
- iC = sp.isOnLineSegment(ep, cp)
- if iC is True:
- inLine.append('BRK')
- chkGap = True
- gap = abs(toolDiam - lst.sub(cp).Length)
- else:
- chkGap = False
- if dirFlg == -1:
- inLine.reverse()
- LINES.append((dirFlg, inLine))
- lnCnt += 1
- dirFlg = -1 * dirFlg # Change zig to zag
- inLine = list() # reset collinear container
- sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
- otr = ep
-
- lst = ep
- if dirFlg == 1:
- tup = (v1, v2)
- else:
- tup = (v2, v1)
-
- if chkGap is True:
- if gap < obj.GapThreshold.Value:
- b = inLine.pop() # pop off 'BRK' marker
- (vA, vB) = inLine.pop() # pop off previous line segment for combining with current
- if dirFlg == 1:
- tup = (vA, tup[1])
- else:
- #tup = (vA, tup[1])
- #tup = (tup[1], vA)
- tup = (tup[0], vB)
- self.closedGap = True
- else:
- gap = round(gap, 6)
- if gap < self.gaps[0]:
- self.gaps.insert(0, gap)
- self.gaps.pop()
- inLine.append(tup)
- # Efor
- lnCnt += 1
-
- # Fix directional issue with LAST line when line count is even
- isEven = lnCnt % 2
- if isEven == 0: # Changed to != with 90 degree CutPatternAngle
- PathLog.debug('Line count is even.')
- else:
- PathLog.debug('Line count is ODD.')
- dirFlg = -1 * dirFlg
- if obj.CutPatternReversed is False:
- if self.CutClimb is True:
- dirFlg = -1 * dirFlg
-
- if obj.CutPatternReversed is True:
- dirFlg = -1 * dirFlg
-
- # Handle last inLine list
- if dirFlg == 1:
- rev = list()
- for iL in inLine:
- if iL == 'BRK':
- rev.append(iL)
- else:
- (p1, p2) = iL
- rev.append((p2, p1))
-
- if obj.CutPatternReversed is False:
- rev.reverse()
- else:
- rev2 = list()
- for iL in rev:
- if iL == 'BRK':
- rev2.append(iL)
- else:
- (p1, p2) = iL
- rev2.append((p2, p1))
- rev2.reverse()
- rev = rev2
-
- LINES.append((dirFlg, rev))
- else:
- LINES.append((dirFlg, inLine))
-
- return LINES
-
- def _pathGeomToArcPointSet(self, obj, compGeoShp):
- '''_pathGeomToArcPointSet(obj, compGeoShp)...
- Convert a compound set of arcs/circles to a set of directionally-oriented arc end points
- and the corresponding center point.'''
- # Extract intersection line segments for return value as list()
- PathLog.debug('_pathGeomToArcPointSet()')
- ARCS = list()
- stpOvrEI = list()
- segEI = list()
- isSame = False
- sameRad = None
- COM = self.tmpCOM
- toolDiam = 2.0 * self.radius
- ec = len(compGeoShp.Edges)
-
- def gapDist(sp, ep):
- X = (ep[0] - sp[0])**2
- Y = (ep[1] - sp[1])**2
- Z = (ep[2] - sp[2])**2
- # return math.sqrt(X + Y + Z)
- return math.sqrt(X + Y) # the 'z' value is zero in both points
-
- # Separate arc data into Loops and Arcs
- for ei in range(0, ec):
- edg = compGeoShp.Edges[ei]
- if edg.Closed is True:
- stpOvrEI.append(('L', ei, False))
- else:
- if isSame is False:
- segEI.append(ei)
- isSame = True
- pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
- sameRad = pnt.sub(COM).Length
- else:
- # Check if arc is co-radial to current SEGS
- pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
- if abs(sameRad - pnt.sub(COM).Length) > 0.00001:
- isSame = False
-
- if isSame is True:
- segEI.append(ei)
- else:
- # Move co-radial arc segments
- stpOvrEI.append(['A', segEI, False])
- # Start new list of arc segments
- segEI = [ei]
- isSame = True
- pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
- sameRad = pnt.sub(COM).Length
- # Process trailing `segEI` data, if available
- if isSame is True:
- stpOvrEI.append(['A', segEI, False])
-
- # Identify adjacent arcs with y=0 start/end points that connect
- for so in range(0, len(stpOvrEI)):
- SO = stpOvrEI[so]
- if SO[0] == 'A':
- startOnAxis = list()
- endOnAxis = list()
- EI = SO[1] # list of corresponding compGeoShp.Edges indexes
-
- # Identify startOnAxis and endOnAxis arcs
- for i in range(0, len(EI)):
- ei = EI[i] # edge index
- E = compGeoShp.Edges[ei] # edge object
- if abs(COM.y - E.Vertexes[0].Y) < 0.00001:
- startOnAxis.append((i, ei, E.Vertexes[0]))
- elif abs(COM.y - E.Vertexes[1].Y) < 0.00001:
- endOnAxis.append((i, ei, E.Vertexes[1]))
-
- # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected
- lenSOA = len(startOnAxis)
- lenEOA = len(endOnAxis)
- if lenSOA > 0 and lenEOA > 0:
- delIdxs = list()
- lstFindIdx = 0
- for soa in range(0, lenSOA):
- (iS, eiS, vS) = startOnAxis[soa]
- for eoa in range(0, len(endOnAxis)):
- (iE, eiE, vE) = endOnAxis[eoa]
- dist = vE.X - vS.X
- if abs(dist) < 0.00001: # They connect on axis at same radius
- SO[2] = (eiE, eiS)
- break
- elif dist > 0:
- break # stop searching
- # Eif
- # Eif
- # Efor
-
- # Construct arc data tuples for OCL
- dirFlg = 1
- # cutPat = obj.CutPattern
- if self.CutClimb is False: # True yields Climb when set to Conventional
- dirFlg = -1
-
- # Cycle through stepOver data
- for so in range(0, len(stpOvrEI)):
- SO = stpOvrEI[so]
- if SO[0] == 'L': # L = Loop/Ring/Circle
- # PathLog.debug("SO[0] == 'Loop'")
- lei = SO[1] # loop Edges index
- v1 = compGeoShp.Edges[lei].Vertexes[0]
-
- # space = obj.SampleInterval.Value / 2.0
- space = 0.0000001
-
- # p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z)
- p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0)
- rad = p1.sub(COM).Length
- spcRadRatio = space/rad
- if spcRadRatio < 1.0:
- tolrncAng = math.asin(spcRadRatio)
- else:
- tolrncAng = 0.9999998 * math.pi
- EX = COM.x + (rad * math.cos(tolrncAng))
- EY = v1.Y - space # rad * math.sin(tolrncAng)
-
- sp = (v1.X, v1.Y, 0.0)
- ep = (EX, EY, 0.0)
- cp = (COM.x, COM.y, 0.0)
- if dirFlg == 1:
- arc = (sp, ep, cp)
- else:
- arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
- ARCS.append(('L', dirFlg, [arc]))
- else: # SO[0] == 'A' A = Arc
- # PathLog.debug("SO[0] == 'Arc'")
- PRTS = list()
- EI = SO[1] # list of corresponding Edges indexes
- CONN = SO[2] # list of corresponding connected edges tuples (iE, iS)
- chkGap = False
- lst = None
-
- if CONN is not False:
- (iE, iS) = CONN
- v1 = compGeoShp.Edges[iE].Vertexes[0]
- v2 = compGeoShp.Edges[iS].Vertexes[1]
- sp = (v1.X, v1.Y, 0.0)
- ep = (v2.X, v2.Y, 0.0)
- cp = (COM.x, COM.y, 0.0)
- if dirFlg == 1:
- arc = (sp, ep, cp)
- lst = ep
- else:
- arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
- lst = sp
- PRTS.append(arc)
- # Pop connected edge index values from arc segments index list
- iEi = EI.index(iE)
- iSi = EI.index(iS)
- if iEi > iSi:
- EI.pop(iEi)
- EI.pop(iSi)
- else:
- EI.pop(iSi)
- EI.pop(iEi)
- if len(EI) > 0:
- PRTS.append('BRK')
- chkGap = True
- cnt = 0
- for ei in EI:
- if cnt > 0:
- PRTS.append('BRK')
- chkGap = True
- v1 = compGeoShp.Edges[ei].Vertexes[0]
- v2 = compGeoShp.Edges[ei].Vertexes[1]
- sp = (v1.X, v1.Y, 0.0)
- ep = (v2.X, v2.Y, 0.0)
- cp = (COM.x, COM.y, 0.0)
- if dirFlg == 1:
- arc = (sp, ep, cp)
- if chkGap is True:
- gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length)
- lst = ep
- else:
- arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
- if chkGap is True:
- gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length)
- lst = sp
- if chkGap is True:
- if gap < obj.GapThreshold.Value:
- b = PRTS.pop() # pop off 'BRK' marker
- (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current
- arc = (vA, arc[1], vC)
- self.closedGap = True
- else:
- # PathLog.debug('---- Gap: {} mm'.format(gap))
- gap = round(gap, 6)
- if gap < self.gaps[0]:
- self.gaps.insert(0, gap)
- self.gaps.pop()
- PRTS.append(arc)
- cnt += 1
-
- if dirFlg == -1:
- PRTS.reverse()
-
- ARCS.append(('A', dirFlg, PRTS))
- # Eif
- if obj.CutPattern == 'CircularZigZag':
- dirFlg = -1 * dirFlg
- # Efor
-
- return ARCS
-
- def _getExperimentalWaterlinePaths(self, obj, PNTSET, csHght):
- '''_getExperimentalWaterlinePaths(obj, PNTSET, csHght)...
- Switching function for calling the appropriate path-geometry to OCL points conversion function
- for the various cut patterns.'''
- PathLog.debug('_getExperimentalWaterlinePaths()')
- SCANS = list()
-
- if obj.CutPattern == 'Line':
- stpOvr = list()
- for D in PNTSET:
- for SEG in D:
- if SEG == 'BRK':
- stpOvr.append(SEG)
- else:
- # D format is ((p1, p2), (p3, p4))
- (A, B) = SEG
- P1 = FreeCAD.Vector(A[0], A[1], csHght)
- P2 = FreeCAD.Vector(B[0], B[1], csHght)
- stpOvr.append((P1, P2))
- SCANS.append(stpOvr)
- stpOvr = list()
- elif obj.CutPattern == 'ZigZag':
- stpOvr = list()
- for (dirFlg, LNS) in PNTSET:
- for SEG in LNS:
- if SEG == 'BRK':
- stpOvr.append(SEG)
- else:
- # D format is ((p1, p2), (p3, p4))
- (A, B) = SEG
- P1 = FreeCAD.Vector(A[0], A[1], csHght)
- P2 = FreeCAD.Vector(B[0], B[1], csHght)
- stpOvr.append((P1, P2))
- SCANS.append(stpOvr)
- stpOvr = list()
- elif obj.CutPattern in ['Circular', 'CircularZigZag']:
- # PNTSET is list, by stepover.
- # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp)
- for so in range(0, len(PNTSET)):
- stpOvr = list()
- erFlg = False
- (aTyp, dirFlg, ARCS) = PNTSET[so]
-
- if dirFlg == 1: # 1
- cMode = True # Climb mode
- else:
- cMode = False
-
- for a in range(0, len(ARCS)):
- Arc = ARCS[a]
- if Arc == 'BRK':
- stpOvr.append('BRK')
- else:
- (sp, ep, cp) = Arc
- S = FreeCAD.Vector(sp[0], sp[1], csHght)
- E = FreeCAD.Vector(ep[0], ep[1], csHght)
- C = FreeCAD.Vector(cp[0], cp[1], csHght)
- scan = (S, E, C, cMode)
- if scan is False:
- erFlg = True
- else:
- ##if aTyp == 'L':
- ## stpOvr.append(FreeCAD.Vector(scan[0][0].x, scan[0][0].y, scan[0][0].z))
- stpOvr.append(scan)
- if erFlg is False:
- SCANS.append(stpOvr)
-
- return SCANS
-
- # Main planar scan functions
- def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc):
- cmds = list()
- rtpd = False
- horizGC = 'G0'
- hSpeed = self.horizRapid
- height = obj.SafeHeight.Value
-
- if obj.CutPattern in ['Line', 'Circular']:
- if obj.OptimizeStepOverTransitions is True:
- height = minSTH + 2.0
- # if obj.LayerMode == 'Multi-pass':
- # rtpd = minSTH
- elif obj.CutPattern in ['ZigZag', 'CircularZigZag']:
- if obj.OptimizeStepOverTransitions is True:
- zChng = first.z - lstPnt.z
- # PathLog.debug('first.z: {}'.format(first.z))
- # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z))
- # PathLog.debug('zChng: {}'.format(zChng))
- # PathLog.debug('minSTH: {}'.format(minSTH))
- if abs(zChng) < tolrnc: # transitions to same Z height
- # PathLog.debug('abs(zChng) < tolrnc')
- if (minSTH - first.z) > tolrnc:
- # PathLog.debug('(minSTH - first.z) > tolrnc')
- height = minSTH + 2.0
- else:
- # PathLog.debug('ELSE (minSTH - first.z) > tolrnc')
- horizGC = 'G1'
- height = first.z
- elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z):
- height = False # allow end of Zig to cut to beginning of Zag
-
-
- # Create raise, shift, and optional lower commands
- if height is not False:
- cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
- cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
- if rtpd is not False: # ReturnToPreviousDepth
- cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
-
- return cmds
-
- def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc):
- cmds = list()
- rtpd = False
- horizGC = 'G0'
- hSpeed = self.horizRapid
- height = obj.SafeHeight.Value
-
- if obj.CutPattern in ['Line', 'Circular']:
- if obj.OptimizeStepOverTransitions is True:
- height = minSTH + 2.0
- elif obj.CutPattern in ['ZigZag', 'CircularZigZag']:
- if obj.OptimizeStepOverTransitions is True:
- zChng = first.z - lstPnt.z
- if abs(zChng) < tolrnc: # transitions to same Z height
- if (minSTH - first.z) > tolrnc:
- height = minSTH + 2.0
- else:
- height = first.z + 2.0 # first.z
-
- cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
- cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
- if rtpd is not False: # ReturnToPreviousDepth
- cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
-
- return cmds
-
- def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False):
- pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object
- pdc.setSTL(stl) # add stl model
- if useSafeCutter is True:
- pdc.setCutter(self.safeCutter) # add safeCutter
- else:
- pdc.setCutter(self.cutter) # add cutter
- pdc.setZ(finalDep) # set minimumZ (final / target depth value)
- pdc.setSampling(SampleInterval) # set sampling size
- return pdc
-
- # Main waterline functions
- def _oclWaterlineOp(self, JOB, obj, mdlIdx, subShp=None):
- '''_oclWaterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.'''
- commands = []
-
- t_begin = time.time()
- # JOB = PathUtils.findParentJob(obj)
- base = JOB.Model.Group[mdlIdx]
- bb = self.boundBoxes[mdlIdx]
- stl = self.modelSTLs[mdlIdx]
-
- # 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
- toolDiam = self.cutter.getDiameter()
- cdeoX = 0.6 * toolDiam
- cdeoY = 0.6 * toolDiam
-
- if subShp is None:
- # Get correct boundbox
- if obj.BoundBox == 'Stock':
- BS = JOB.Stock
- bb = BS.Shape.BoundBox
- elif obj.BoundBox == 'BaseBoundBox':
- BS = base
- bb = base.Shape.BoundBox
-
- env = PathUtils.getEnvelope(partshape=BS.Shape, depthparams=self.depthParams) # Produces .Shape
-
- xmin = bb.XMin
- xmax = bb.XMax
- ymin = bb.YMin
- ymax = bb.YMax
- zmin = bb.ZMin
- zmax = bb.ZMax
- else:
- xmin = subShp.BoundBox.XMin
- xmax = subShp.BoundBox.XMax
- ymin = subShp.BoundBox.YMin
- ymax = subShp.BoundBox.YMax
- zmin = subShp.BoundBox.ZMin
- zmax = subShp.BoundBox.ZMax
-
- smplInt = obj.SampleInterval.Value
- 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:
- depthparams = [dp for dp in self.depthParams]
- lenDP = len(depthparams)
-
- # Prepare PathDropCutter objects with STL data
- safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx],
- depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False)
-
- # Scan the piece to depth at smplInt
- oclScan = []
- oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines)
- # oclScan = SCANS
- 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])
- PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line")
-
- # 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
- PathLog.debug("--All layer scans combined took " + str(time.time() - layTime) + " s")
- return commands
-
- def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines):
- '''_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ...
- Perform OCL scan for waterline purpose.'''
- 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
- path.append(ocl.Line(p1, p2))
- # 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):
- '''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.'''
- 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
- self._bufferTopoMap(lenSL, pntsPerLine)
- # Identify layer waterline from OCL scan
- self._highlightWaterline(4, 9)
- # Extract waterline and convert to gcode
- loopList = self._extractWaterlines(obj, scanLines, lyr, layDep)
- # save commands
- for loop in loopList:
- cmds = self._loopToGcode(obj, layDep, loop)
- commands.extend(cmds)
- return commands
-
- def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine):
- '''_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.'''
- 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):
- '''_bufferTopoMap(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):
- '''_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.'''
- TM = self.topoMap
- lastPnt = len(TM[1]) - 1
- lastLn = len(TM) - 1
- highFlag = 0
-
- # ("--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
-
- # ("--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")
- 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
- 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
-
- return True
-
- def _extractWaterlines(self, obj, oclScan, lyr, layDep):
- '''_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.'''
- srch = True
- lastPnt = len(self.topoMap[0]) - 1
- lastLn = len(self.topoMap) - 1
- maxSrchs = 5
- srchCnt = 1
- loopList = []
- loop = []
- loopNum = 0
-
- if self.CutClimb is True:
- 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:
- PathLog.debug("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
- PathLog.debug("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.")
- return loopList
-
- def _trackLoop(self, oclScan, lC, pC, L, P, loopNum):
- '''_trackLoop(oclScan, lC, pC, L, P, loopNum) ... Track the loop direction.'''
- 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:
- PathLog.debug("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):
- '''_findNextWlPoint(lC, pC, cl, cp, pl, pp) ...
- Find the next waterline point in the point cloud layer provided.'''
- 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:
- # ("_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]
-
- # ("_findNext: No next pnt found")
- return [cl, cp, num]
-
- def _loopToGcode(self, obj, layDep, loop):
- '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.'''
- # generate the path commands
- output = []
- optimize = obj.OptimizeLinearPaths
-
- 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 first 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 FreeCAD.Vector(prev.x, prev.y, prev.z).isOnLineSegment(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
-
- # 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
-
- # Main waterline functions
- def _experimentalWaterlineOp(self, JOB, obj, mdlIdx, subShp=None):
- '''_waterlineOp(JOB, obj, mdlIdx, subShp=None) ...
- Main waterline function to perform waterline extraction from model.'''
- PathLog.debug('_experimentalWaterlineOp()')
-
- msg = translate('PathWaterline', 'Experimental Waterline does not currently support selected faces.')
- PathLog.info('\n..... ' + msg)
-
- commands = []
- t_begin = time.time()
- base = JOB.Model.Group[mdlIdx]
- bb = self.boundBoxes[mdlIdx]
- stl = self.modelSTLs[mdlIdx]
- safeSTL = self.safeSTLs[mdlIdx]
- self.endVector = None
-
- finDep = obj.FinalDepth.Value + (self.geoTlrnc / 10.0)
- depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, finDep)
-
- # Compute number and size of stepdowns, and final depth
- if obj.LayerMode == 'Single-pass':
- depthparams = [finDep]
- else:
- depthparams = [dp for dp in depthParams]
- lenDP = len(depthparams)
- PathLog.debug('Experimental Waterline depthparams:\n{}'.format(depthparams))
-
- # Prepare PathDropCutter objects with STL data
- # safePDC = self._planarGetPDC(safeSTL, depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False)
-
- buffer = self.cutter.getDiameter() * 2.0
- borderFace = Part.Face(self._makeExtendedBoundBox(JOB.Stock.Shape.BoundBox, buffer, 0.0))
-
- # Get correct boundbox
- if obj.BoundBox == 'Stock':
- stockEnv = self._getShapeEnvelope(JOB.Stock.Shape)
- bbFace = self._getCrossSection(stockEnv) # returned at Z=0.0
- elif obj.BoundBox == 'BaseBoundBox':
- baseEnv = self._getShapeEnvelope(base.Shape)
- bbFace = self._getCrossSection(baseEnv) # returned at Z=0.0
-
- trimFace = borderFace.cut(bbFace)
- if self.showDebugObjects is True:
- TF = FreeCAD.ActiveDocument.addObject('Part::Feature', 'trimFace')
- TF.Shape = trimFace
- TF.purgeTouched()
- self.tempGroup.addObject(TF)
-
- # Cycle through layer depths
- CUTAREAS = self._getCutAreas(base.Shape, depthparams, bbFace, trimFace, borderFace)
- if not CUTAREAS:
- PathLog.error('No cross-section cut areas identified.')
- return commands
-
- caCnt = 0
- ofst = obj.BoundaryAdjustment.Value
- ofst -= self.radius # (self.radius + (tolrnc / 10.0))
- caLen = len(CUTAREAS)
- lastCA = caLen - 1
- lastClearArea = None
- lastCsHght = None
- clearLastLayer = True
- for ca in range(0, caLen):
- area = CUTAREAS[ca]
- csHght = area.BoundBox.ZMin
- csHght += obj.DepthOffset.Value
- cont = False
- caCnt += 1
- if area.Area > 0.0:
- cont = True
- caWireCnt = len(area.Wires) - 1 # first wire is boundFace wire
- PathLog.debug('cutAreaWireCnt: {}'.format(caWireCnt))
- if self.showDebugObjects is True:
- CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'cutArea_{}'.format(caCnt))
- CA.Shape = area
- CA.purgeTouched()
- self.tempGroup.addObject(CA)
- else:
- PathLog.error('Cut area at {} is zero.'.format(round(csHght, 4)))
-
- # get offset wire(s) based upon cross-section cut area
- if cont:
- area.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - area.BoundBox.ZMin))
- activeArea = area.cut(trimFace)
- activeAreaWireCnt = len(activeArea.Wires) # first wire is boundFace wire
- PathLog.debug('activeAreaWireCnt: {}'.format(activeAreaWireCnt))
- if self.showDebugObjects is True:
- CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'activeArea_{}'.format(caCnt))
- CA.Shape = activeArea
- CA.purgeTouched()
- self.tempGroup.addObject(CA)
- ofstArea = self._extractFaceOffset(obj, activeArea, ofst, makeComp=False)
- if not ofstArea:
- PathLog.error('No offset area returned for cut area depth: {}'.format(csHght))
- cont = False
-
- if cont:
- # Identify solid areas in the offset data
- ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea)
- if ofstSolidFacesList:
- clearArea = Part.makeCompound(ofstSolidFacesList)
- if self.showDebugObjects is True:
- CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearArea_{}'.format(caCnt))
- CA.Shape = clearArea
- CA.purgeTouched()
- self.tempGroup.addObject(CA)
- else:
- cont = False
- PathLog.error('ofstSolids is False.')
-
- if cont:
- # Make waterline path for current CUTAREA depth (csHght)
- commands.extend(self._wiresToWaterlinePath(obj, clearArea, csHght))
- clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clearArea.BoundBox.ZMin))
- lastClearArea = clearArea
- lastCsHght = csHght
-
- # Clear layer as needed
- (useOfst, usePat, clearLastLayer) = self._clearLayer(obj, ca, lastCA, clearLastLayer)
- ##if self.showDebugObjects is True and (usePat or useOfst):
- ## OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearPatternArea_{}'.format(round(csHght, 2)))
- ## OA.Shape = clearArea
- ## OA.purgeTouched()
- ## self.tempGroup.addObject(OA)
- if usePat:
- commands.extend(self._makeCutPatternLayerPaths(JOB, obj, clearArea, csHght))
- if useOfst:
- commands.extend(self._makeOffsetLayerPaths(JOB, obj, clearArea, csHght))
- # Efor
-
- if clearLastLayer:
- (useOfst, usePat, cLL) = self._clearLayer(obj, 1, 1, False)
- clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - lastClearArea.BoundBox.ZMin))
- if usePat:
- commands.extend(self._makeCutPatternLayerPaths(JOB, obj, lastClearArea, lastCsHght))
-
- if useOfst:
- commands.extend(self._makeOffsetLayerPaths(JOB, obj, lastClearArea, lastCsHght))
-
- PathLog.info("Waterline: All layer scans combined took " + str(time.time() - t_begin) + " s")
- return commands
-
- def _getCutAreas(self, shape, depthparams, bbFace, trimFace, borderFace):
- '''_getCutAreas(JOB, shape, depthparams, bbFace, borderFace) ...
- Takes shape, depthparams and base-envelope-cross-section, and
- returns a list of cut areas - one for each depth.'''
- PathLog.debug('_getCutAreas()')
-
- CUTAREAS = list()
- lastLayComp = None
- isFirst = True
- lenDP = len(depthparams)
-
- # Cycle through layer depths
- for dp in range(0, lenDP):
- csHght = depthparams[dp]
- PathLog.debug('Depth {} is {}'.format(dp + 1, csHght))
-
- # Get slice at depth of shape
- csFaces = self._getModelCrossSection(shape, csHght) # returned at Z=0.0
- if not csFaces:
- PathLog.error('No cross-section wires at {}'.format(csHght))
- else:
- PathLog.debug('cross-section face count {}'.format(len(csFaces)))
- if len(csFaces) > 0:
- useFaces = self._getSolidAreasFromPlanarFaces(csFaces)
- else:
- useFaces = False
-
- if useFaces:
- PathLog.debug('useFacesCnt: {}'.format(len(useFaces)))
- compAdjFaces = Part.makeCompound(useFaces)
-
- if self.showDebugObjects is True:
- CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSolids_{}'.format(dp + 1))
- CA.Shape = compAdjFaces
- CA.purgeTouched()
- self.tempGroup.addObject(CA)
-
- if isFirst:
- allPrevComp = compAdjFaces
- cutArea = borderFace.cut(compAdjFaces)
- else:
- preCutArea = borderFace.cut(compAdjFaces)
- cutArea = preCutArea.cut(allPrevComp) # cut out higher layers to avoid cutting recessed areas
- allPrevComp = allPrevComp.fuse(compAdjFaces)
- cutArea.translate(FreeCAD.Vector(0.0, 0.0, csHght - cutArea.BoundBox.ZMin))
- CUTAREAS.append(cutArea)
- isFirst = False
- else:
- PathLog.error('No waterline at depth: {} mm.'.format(csHght))
- # Efor
-
- if len(CUTAREAS) > 0:
- return CUTAREAS
-
- return False
-
- def _wiresToWaterlinePath(self, obj, ofstPlnrShp, csHght):
- PathLog.debug('_wiresToWaterlinePath()')
- commands = list()
-
- # Translate path geometry to layer height
- ofstPlnrShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - ofstPlnrShp.BoundBox.ZMin))
- if self.showDebugObjects is True:
- OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'waterlinePathArea_{}'.format(round(csHght, 2)))
- OA.Shape = ofstPlnrShp
- OA.purgeTouched()
- self.tempGroup.addObject(OA)
-
- commands.append(Path.Command('N (Cut Area {}.)'.format(round(csHght, 2))))
- for w in range(0, len(ofstPlnrShp.Wires)):
- wire = ofstPlnrShp.Wires[w]
- V = wire.Vertexes
- if obj.CutMode == 'Climb':
- lv = len(V) - 1
- startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z)
- else:
- startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z)
-
- commands.append(Path.Command('N (Wire {}.)'.format(w)))
- (cmds, endVect) = self._wireToPath(obj, wire, startVect)
- commands.extend(cmds)
- commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
-
- return commands
-
- def _makeCutPatternLayerPaths(self, JOB, obj, clrAreaShp, csHght):
- PathLog.debug('_makeCutPatternLayerPaths()')
- commands = []
-
- clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clrAreaShp.BoundBox.ZMin))
- pathGeom = self._planarMakePathGeom(obj, clrAreaShp)
- pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin))
- # clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin))
-
- if self.showDebugObjects is True:
- OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'pathGeom_{}'.format(round(csHght, 2)))
- OA.Shape = pathGeom
- OA.purgeTouched()
- self.tempGroup.addObject(OA)
-
- # Convert pathGeom to gcode more efficiently
- if True:
- if obj.CutPattern == 'Offset':
- commands.extend(self._makeOffsetLayerPaths(JOB, obj, clrAreaShp, csHght))
- else:
- clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin))
- if obj.CutPattern == 'Line':
- pntSet = self._pathGeomToLinesPointSet(obj, pathGeom)
- elif obj.CutPattern == 'ZigZag':
- pntSet = self._pathGeomToZigzagPointSet(obj, pathGeom)
- elif obj.CutPattern in ['Circular', 'CircularZigZag']:
- pntSet = self._pathGeomToArcPointSet(obj, pathGeom)
- stpOVRS = self._getExperimentalWaterlinePaths(obj, pntSet, csHght)
- # PathLog.debug('stpOVRS:\n{}'.format(stpOVRS))
- safePDC = False
- cmds = self._clearGeomToPaths(JOB, obj, safePDC, stpOVRS, csHght)
- commands.extend(cmds)
- else:
- # Use Path.fromShape() to convert edges to paths
- for w in range(0, len(pathGeom.Edges)):
- wire = pathGeom.Edges[w]
- V = wire.Vertexes
- if obj.CutMode == 'Climb':
- lv = len(V) - 1
- startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z)
- else:
- startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z)
-
- commands.append(Path.Command('N (Wire {}.)'.format(w)))
- (cmds, endVect) = self._wireToPath(obj, wire, startVect)
- commands.extend(cmds)
- commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
-
- return commands
-
- def _makeOffsetLayerPaths(self, JOB, obj, clrAreaShp, csHght):
- PathLog.debug('_makeOffsetLayerPaths()')
- PathLog.warning('Using `Offset` for clearing bottom layer.')
- cmds = list()
- # ofst = obj.BoundaryAdjustment.Value
- ofst = 0.0 - self.cutOut # - self.cutter.getDiameter() # (self.radius + (tolrnc / 10.0))
- shape = clrAreaShp
- cont = True
- cnt = 0
- while cont:
- ofstArea = self._extractFaceOffset(obj, shape, ofst, makeComp=True)
- if not ofstArea:
- PathLog.warning('No offset clearing area returned.')
- break
- for F in ofstArea.Faces:
- cmds.extend(self._wiresToWaterlinePath(obj, F, csHght))
- shape = ofstArea
- if cnt == 0:
- ofst = 0.0 - self.cutOut # self.cutter.Diameter()
- cnt += 1
- return cmds
-
- def _clearGeomToPaths(self, JOB, obj, safePDC, SCANDATA, csHght):
- PathLog.debug('_clearGeomToPaths()')
-
- GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})]
- tolrnc = JOB.GeometryTolerance.Value
- prevDepth = obj.SafeHeight.Value
- lenSCANDATA = len(SCANDATA)
- gDIR = ['G3', 'G2']
-
- if self.CutClimb is True:
- gDIR = ['G2', 'G3']
-
- # Send cutter to x,y position of first point on first line
- first = SCANDATA[0][0][0] # [step][item][point]
- GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid}))
-
- # Cycle through step-over sections (line segments or arcs)
- odd = True
- lstStpEnd = None
- prevDepth = obj.SafeHeight.Value # Not used for Single-pass
- for so in range(0, lenSCANDATA):
- cmds = list()
- PRTS = SCANDATA[so]
- lenPRTS = len(PRTS)
- first = PRTS[0][0] # first point of arc/line stepover group
- start = PRTS[0][0] # will change with each line/arc segment
- last = None
- cmds.append(Path.Command('N (Begin step {}.)'.format(so), {}))
-
- if so > 0:
- if obj.CutPattern == 'CircularZigZag':
- if odd is True:
- odd = False
- else:
- odd = True
- # minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL
- minTrnsHght = obj.SafeHeight.Value
- # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {}))
- cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc))
-
- # Cycle through current step-over parts
- for i in range(0, lenPRTS):
- prt = PRTS[i]
- lenPrt = len(prt)
- # PathLog.debug('prt: {}'.format(prt))
- if prt == 'BRK':
- nxtStart = PRTS[i + 1][0]
- # minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL
- minSTH = obj.SafeHeight.Value
- cmds.append(Path.Command('N (Break)', {}))
- cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc))
- else:
- cmds.append(Path.Command('N (part {}.)'.format(i + 1), {}))
- if obj.CutPattern in ['Line', 'ZigZag']:
- start, last = prt
- cmds.append(Path.Command('G1', {'X': start.x, 'Y': start.y, 'Z': start.z, 'F': self.horizFeed}))
- cmds.append(Path.Command('G1', {'X': last.x, 'Y': last.y, 'F': self.horizFeed}))
- elif obj.CutPattern in ['Circular', 'CircularZigZag']:
- start, last, centPnt, cMode = prt
- gcode = self._makeGcodeArc(start, last, odd, gDIR, tolrnc)
- cmds.extend(gcode)
- cmds.append(Path.Command('N (End of step {}.)'.format(so), {}))
- GCODE.extend(cmds) # save line commands
- lstStpEnd = last
- # Efor
-
- return GCODE
-
- def _getSolidAreasFromPlanarFaces(self, csFaces):
- PathLog.debug('_getSolidAreasFromPlanarFaces()')
- holds = list()
- cutFaces = list()
- useFaces = list()
- lenCsF = len(csFaces)
- PathLog.debug('lenCsF: {}'.format(lenCsF))
-
- if lenCsF == 1:
- useFaces = csFaces
- else:
- fIds = list()
- aIds = list()
- pIds = list()
- cIds = list()
-
- for af in range(0, lenCsF):
- fIds.append(af) # face ids
- aIds.append(af) # face ids
- pIds.append(-1) # parent ids
- cIds.append(False) # cut ids
- holds.append(False)
-
- while len(fIds) > 0:
- li = fIds.pop()
- low = csFaces[li] # senior face
- pIds = self._idInternalFeature(csFaces, fIds, pIds, li, low)
- # Ewhile
- ##PathLog.info('fIds: {}'.format(fIds))
- ##PathLog.info('pIds: {}'.format(pIds))
-
- for af in range(lenCsF - 1, -1, -1): # cycle from last item toward first
- ##PathLog.info('af: {}'.format(af))
- prnt = pIds[af]
- ##PathLog.info('prnt: {}'.format(prnt))
- if prnt == -1:
- stack = -1
- else:
- stack = [af]
- # get_face_ids_to_parent
- stack.insert(0, prnt)
- nxtPrnt = pIds[prnt]
- # find af value for nxtPrnt
- while nxtPrnt != -1:
- stack.insert(0, nxtPrnt)
- nxtPrnt = pIds[nxtPrnt]
- cIds[af] = stack
- # PathLog.debug('cIds: {}\n'.format(cIds))
-
- for af in range(0, lenCsF):
- # PathLog.debug('af is {}'.format(af))
- pFc = cIds[af]
- if pFc == -1:
- # Simple, independent region
- holds[af] = csFaces[af] # place face in hold
- # PathLog.debug('pFc == -1')
- else:
- # Compound region
- # PathLog.debug('pFc is not -1')
- cnt = len(pFc)
- if cnt % 2.0 == 0.0:
- # even is donut cut
- # PathLog.debug('cnt is even')
- inr = pFc[cnt - 1]
- otr = pFc[cnt - 2]
- # PathLog.debug('inr / otr: {} / {}'.format(inr, otr))
- holds[otr] = holds[otr].cut(csFaces[inr])
- else:
- # odd is floating solid
- # PathLog.debug('cnt is ODD')
- holds[af] = csFaces[af]
- # Efor
-
- for af in range(0, lenCsF):
- if holds[af]:
- useFaces.append(holds[af]) # save independent solid
-
- # Eif
-
- if len(useFaces) > 0:
- return useFaces
-
- return False
-
- def _getModelCrossSection(self, shape, csHght):
- PathLog.debug('_getCrossSection()')
- wires = list()
-
- def byArea(fc):
- return fc.Area
-
- for i in shape.slice(FreeCAD.Vector(0, 0, 1), csHght):
- wires.append(i)
-
- if len(wires) > 0:
- for w in wires:
- if w.isClosed() is False:
- return False
- FCS = list()
- for w in wires:
- w.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - w.BoundBox.ZMin))
- FCS.append(Part.Face(w))
- FCS.sort(key=byArea, reverse=True)
- return FCS
- else:
- PathLog.debug(' -No wires from .slice() method')
-
- 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 _idInternalFeature(self, csFaces, fIds, pIds, li, low):
- Ids = list()
- for i in fIds:
- Ids.append(i)
- while len(Ids) > 0:
- hi = Ids.pop()
- high = csFaces[hi]
- if self._isInBoundBox(high, low):
- cmn = high.common(low)
- if cmn.Area > 0.0:
- pIds[li] = hi
- break
- # Ewhile
- return pIds
-
- def _wireToPath(self, obj, wire, startVect):
- '''_wireToPath(obj, wire, startVect) ... wire to path.'''
- PathLog.track()
-
- paths = []
- pathParams = {} # pylint: disable=assignment-from-no-return
- V = wire.Vertexes
-
- pathParams['shapes'] = [wire]
- pathParams['feedrate'] = self.horizFeed
- pathParams['feedrate_v'] = self.vertFeed
- pathParams['verbose'] = True
- pathParams['resume_height'] = obj.SafeHeight.Value
- pathParams['retraction'] = obj.ClearanceHeight.Value
- pathParams['return_end'] = True
- # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers
- pathParams['preamble'] = False
- pathParams['start'] = startVect
-
- (pp, end_vector) = Path.fromShapes(**pathParams)
- paths.extend(pp.Commands)
- # PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector))
-
- self.endVector = end_vector # pylint: disable=attribute-defined-outside-init
-
- return (paths, end_vector)
-
- def _makeExtendedBoundBox(self, wBB, bbBfr, zDep):
- pl = FreeCAD.Placement()
- pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0)
- pl.Base = FreeCAD.Vector(0, 0, 0)
-
- 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)
- bb = Part.makePolygon([p1, p2, p3, p4, p1])
-
- return bb
-
- def _makeGcodeArc(self, strtPnt, endPnt, odd, gDIR, tolrnc):
- cmds = list()
- isCircle = False
- inrPnt = None
- gdi = 0
- if odd is True:
- gdi = 1
-
- # Test if pnt set is circle
- if abs(strtPnt.x - endPnt.x) < tolrnc:
- if abs(strtPnt.y - endPnt.y) < tolrnc:
- isCircle = True
- isCircle = False
-
- if isCircle is True:
- # convert LN to G2/G3 arc, consolidating GCode
- # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc
- # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/
- # Dividing circle into two arcs allows for G2/G3 on inclined surfaces
-
- # ijk = self.tmpCOM - strtPnt # vector from start to center
- ijk = self.tmpCOM - strtPnt # vector from start to center
- xyz = self.tmpCOM.add(ijk) # end point
- cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
- cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z,
- 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
- 'F': self.horizFeed}))
- cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed}))
- ijk = self.tmpCOM - xyz # vector from start to center
- rst = strtPnt # end point
- cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z,
- 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
- 'F': self.horizFeed}))
- cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
- else:
- # ijk = self.tmpCOM - strtPnt
- ijk = self.tmpCOM.sub(strtPnt) # vector from start to center
- xyz = endPnt
- cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
- cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z,
- 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
- 'F': self.horizFeed}))
- cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed}))
-
- return cmds
-
- def _clearLayer(self, obj, ca, lastCA, clearLastLayer):
- PathLog.debug('_clearLayer()')
- usePat = False
- useOfst = False
-
- if obj.ClearLastLayer == 'Off':
- if obj.CutPattern != 'None':
- usePat = True
- else:
- if ca == lastCA:
- PathLog.debug('... Clearing bottom layer.')
- if obj.ClearLastLayer == 'Offset':
- obj.CutPattern = 'None'
- useOfst = True
- else:
- obj.CutPattern = obj.ClearLastLayer
- usePat = True
- clearLastLayer = False
-
- return (useOfst, usePat, clearLastLayer)
-
- def resetOpVariables(self, all=True):
- '''resetOpVariables() ... Reset class variables used for instance of operation.'''
- self.holdPoint = None
- self.layerEndPnt = None
- self.onHold = False
- self.SafeHeightOffset = 2.0
- self.ClearHeightOffset = 4.0
- self.layerEndzMax = 0.0
- self.resetTolerance = 0.0
- self.holdPntCnt = 0
- self.bbRadius = 0.0
- self.axialFeed = 0.0
- self.axialRapid = 0.0
- self.FinalDepth = 0.0
- self.clearHeight = 0.0
- self.safeHeight = 0.0
- self.faceZMax = -999999999999.0
- if all is True:
- self.cutter = None
- self.stl = None
- self.fullSTL = None
- self.cutOut = 0.0
- self.radius = 0.0
- self.useTiltCutter = False
- return True
-
- def deleteOpVariables(self, all=True):
- '''deleteOpVariables() ... Reset class variables used for instance of operation.'''
- del self.holdPoint
- del self.layerEndPnt
- del self.onHold
- del self.SafeHeightOffset
- del self.ClearHeightOffset
- del self.layerEndzMax
- del self.resetTolerance
- del self.holdPntCnt
- del self.bbRadius
- del self.axialFeed
- del self.axialRapid
- del self.FinalDepth
- del self.clearHeight
- del self.safeHeight
- del self.faceZMax
- if all is True:
- del self.cutter
- del self.stl
- del self.fullSTL
- del self.cutOut
- del self.radius
- del self.useTiltCutter
- return True
-
- def setOclCutter(self, obj, safe=False):
- ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. '''
- # Set cutter details
- # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details
- diam_1 = float(obj.ToolController.Tool.Diameter)
- lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0
- FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0
- CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0
- CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0
-
- # Make safeCutter with 2 mm buffer around physical cutter
- if safe is True:
- diam_1 += 4.0
- if FR != 0.0:
- FR += 2.0
-
- PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType))
- if obj.ToolController.Tool.ToolType == 'EndMill':
- # Standard End Mill
- return 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.useTiltCutter = True
- return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst))
-
- 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)
- return 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)
- return ocl.ConeCutter(diam_1, (CEA / 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)
- return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
- else:
- # Default to standard end mill
- PathLog.warning("Defaulting cutter to standard end mill.")
- return ocl.CylCutter(diam_1, (CEH + lenOfst))
-
- # http://www.carbidecutter.net/products/carbide-burr-cone-shape-sm.html
- '''
- # Available FreeCAD cutter types - some still need translation to available OCL cutter classes.
- Drill, CenterDrill, CounterSink, CounterBore, FlyCutter, Reamer, Tap,
- EndMill, SlotCutter, BallEndMill, ChamferMill, CornerRound, Engraver
- '''
- # Adittional problem is with new ToolBit user-defined cutter shapes.
- # Some sort of translation/conversion will have to be defined to make compatible with OCL.
- PathLog.error('Unable to set OCL cutter.')
- return False
-
-
-def SetupProperties():
- ''' SetupProperties() ... Return list of properties required for operation.'''
- setup = []
- setup.append('Algorithm')
- setup.append('AngularDeflection')
- setup.append('AvoidLastX_Faces')
- setup.append('AvoidLastX_InternalFeatures')
- setup.append('BoundBox')
- setup.append('BoundaryAdjustment')
- setup.append('CircularCenterAt')
- setup.append('CircularCenterCustom')
- setup.append('ClearLastLayer')
- setup.append('CutMode')
- setup.append('CutPattern')
- setup.append('CutPatternAngle')
- setup.append('CutPatternReversed')
- setup.append('DepthOffset')
- setup.append('GapSizes')
- setup.append('GapThreshold')
- setup.append('HandleMultipleFeatures')
- setup.append('InternalFeaturesCut')
- setup.append('InternalFeaturesAdjustment')
- setup.append('LayerMode')
- setup.append('LinearDeflection')
- setup.append('OptimizeStepOverTransitions')
- setup.append('ProfileEdges')
- setup.append('BoundaryEnforcement')
- setup.append('SampleInterval')
- setup.append('StartPoint')
- setup.append('StepOver')
- setup.append('UseStartPoint')
- # For debugging
- setup.append('ShowTempObjects')
- return setup
-
-
-def Create(name, obj=None):
- '''Create(name) ... Creates and returns a Waterline operation.'''
- if obj is None:
- obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
- obj.Proxy = ObjectWaterline(obj, name)
- return obj
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2019 Russell Johnson (russ4262) *
+# * Copyright (c) 2019 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 *
+# * *
+# ***************************************************************************
+
+from __future__ import print_function
+
+__title__ = "Path Waterline Operation"
+__author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)"
+__url__ = "http://www.freecadweb.org"
+__doc__ = "Class and implementation of Waterline operation."
+__contributors__ = ""
+
+import FreeCAD
+from PySide import QtCore
+
+# OCL must be installed
+try:
+ import ocl
+except ImportError:
+ msg = QtCore.QCoreApplication.translate("PathWaterline", "This operation requires OpenCamLib to be installed.")
+ FreeCAD.Console.PrintError(msg + "\n")
+ raise ImportError
+ # import sys
+ # sys.exit(msg)
+
+import Path
+import PathScripts.PathLog as PathLog
+import PathScripts.PathUtils as PathUtils
+import PathScripts.PathOp as PathOp
+import PathScripts.PathSurfaceSupport as PathSurfaceSupport
+import time
+import math
+
+# lazily loaded modules
+from lazy_loader.lazy_loader import LazyLoader
+MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart')
+Part = LazyLoader('Part', globals(), 'Part')
+
+if FreeCAD.GuiUp:
+ import FreeCADGui
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+# PathLog.trackModule(PathLog.thisModule())
+
+
+# Qt translation handling
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+
+class ObjectWaterline(PathOp.ObjectOp):
+ '''Proxy object for Surfacing operation.'''
+
+ def baseObject(self):
+ '''baseObject() ... returns super of receiver
+ Used to call base implementation in overwritten functions.'''
+ return super(self.__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 | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces
+
+ def initOperation(self, obj):
+ '''initPocketOp(obj) ...
+ Initialize the operation - property creation and property editor status.'''
+ self.initOpProperties(obj)
+
+ # For debugging
+ if PathLog.getLevel(PathLog.thisModule()) != 4:
+ obj.setEditorMode('ShowTempObjects', 2) # hide
+
+ if not hasattr(obj, 'DoNotSetDefaultValues'):
+ self.setEditorProperties(obj)
+
+ def initOpProperties(self, obj, warn=False):
+ '''initOpProperties(obj) ... create operation specific properties'''
+ missing = list()
+
+ for (prtyp, nm, grp, tt) in self.opProperties():
+ if not hasattr(obj, nm):
+ obj.addProperty(prtyp, nm, grp, tt)
+ missing.append(nm)
+ if warn:
+ newPropMsg = translate('PathWaterline', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. '
+ newPropMsg += translate('PathWaterline', 'Check its default value.')
+ PathLog.warning(newPropMsg)
+
+ # Set enumeration lists for enumeration properties
+ if len(missing) > 0:
+ ENUMS = self.propertyEnumerations()
+ for n in ENUMS:
+ if n in missing:
+ setattr(obj, n, ENUMS[n])
+
+ self.addedAllProperties = True
+
+ def opProperties(self):
+ '''opProperties() ... return list of tuples containing operation specific properties'''
+ return [
+ ("App::PropertyBool", "ShowTempObjects", "Debug",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")),
+
+ ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")),
+ ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")),
+
+ ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")),
+ ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")),
+ ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")),
+ ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")),
+ ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")),
+ ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")),
+ ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")),
+
+ ("App::PropertyEnumeration", "Algorithm", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the algorithm to use: OCL Dropcutter*, or Experimental (Not OCL based).")),
+ ("App::PropertyEnumeration", "BoundBox", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation.")),
+ ("App::PropertyEnumeration", "ClearLastLayer", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set to clear last layer in a `Multi-pass` operation.")),
+ ("App::PropertyEnumeration", "CutMode", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")),
+ ("App::PropertyEnumeration", "CutPattern", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")),
+ ("App::PropertyFloat", "CutPatternAngle", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")),
+ ("App::PropertyBool", "CutPatternReversed", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")),
+ ("App::PropertyDistance", "DepthOffset", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")),
+ ("App::PropertyDistance", "IgnoreOuterAbove", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore outer waterlines above this height.")),
+ ("App::PropertyEnumeration", "LayerMode", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")),
+ ("App::PropertyVectorDistance", "PatternCenterCustom", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for the cut pattern.")),
+ ("App::PropertyEnumeration", "PatternCenterAt", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the cut pattern.")),
+ ("App::PropertyDistance", "SampleInterval", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")),
+ ("App::PropertyPercent", "StepOver", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")),
+
+ ("App::PropertyBool", "OptimizeLinearPaths", "Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")),
+ ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")),
+ ("App::PropertyDistance", "GapThreshold", "Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")),
+ ("App::PropertyString", "GapSizes", "Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")),
+
+ ("App::PropertyVectorDistance", "StartPoint", "Start Point",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")),
+ ("App::PropertyBool", "UseStartPoint", "Start Point",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"))
+ ]
+
+ def propertyEnumerations(self):
+ # Enumeration lists for App::PropertyEnumeration properties
+ return {
+ 'Algorithm': ['OCL Dropcutter', 'Experimental'],
+ 'BoundBox': ['BaseBoundBox', 'Stock'],
+ 'PatternCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'],
+ 'ClearLastLayer': ['Off', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'],
+ 'CutMode': ['Conventional', 'Climb'],
+ 'CutPattern': ['None', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle']
+ 'HandleMultipleFeatures': ['Collectively', 'Individually'],
+ 'LayerMode': ['Single-pass', 'Multi-pass'],
+ }
+
+ def setEditorProperties(self, obj):
+ # Used to hide inputs in properties list
+ expMode = G = 0
+ show = hide = A = B = C = 2
+ if hasattr(obj, 'EnableRotation'):
+ obj.setEditorMode('EnableRotation', hide)
+
+ obj.setEditorMode('BoundaryEnforcement', hide)
+ obj.setEditorMode('InternalFeaturesAdjustment', hide)
+ obj.setEditorMode('InternalFeaturesCut', hide)
+ obj.setEditorMode('AvoidLastX_Faces', hide)
+ obj.setEditorMode('AvoidLastX_InternalFeatures', hide)
+ obj.setEditorMode('BoundaryAdjustment', hide)
+ obj.setEditorMode('HandleMultipleFeatures', hide)
+ obj.setEditorMode('OptimizeLinearPaths', hide)
+ obj.setEditorMode('OptimizeStepOverTransitions', hide)
+ obj.setEditorMode('GapThreshold', hide)
+ obj.setEditorMode('GapSizes', hide)
+
+ if obj.Algorithm == 'OCL Dropcutter':
+ pass
+ elif obj.Algorithm == 'Experimental':
+ A = B = C = 0
+ expMode = G = show = hide = 2
+
+ cutPattern = obj.CutPattern
+ if obj.ClearLastLayer != 'Off':
+ cutPattern = obj.ClearLastLayer
+
+ if cutPattern == 'None':
+ show = hide = A = 2
+ elif cutPattern in ['Line', 'ZigZag']:
+ show = 0
+ elif cutPattern in ['Circular', 'CircularZigZag']:
+ show = 2 # hide
+ hide = 0 # show
+ elif cutPattern == 'Spiral':
+ G = hide = 0
+
+ obj.setEditorMode('CutPatternAngle', show)
+ obj.setEditorMode('PatternCenterAt', hide)
+ obj.setEditorMode('PatternCenterCustom', hide)
+ obj.setEditorMode('CutPatternReversed', A)
+
+ obj.setEditorMode('ClearLastLayer', C)
+ obj.setEditorMode('StepOver', B)
+ obj.setEditorMode('IgnoreOuterAbove', B)
+ obj.setEditorMode('CutPattern', C)
+ obj.setEditorMode('SampleInterval', G)
+ obj.setEditorMode('LinearDeflection', expMode)
+ obj.setEditorMode('AngularDeflection', expMode)
+
+ def onChanged(self, obj, prop):
+ if hasattr(self, 'addedAllProperties'):
+ if self.addedAllProperties is True:
+ if prop in ['Algorithm', 'CutPattern']:
+ self.setEditorProperties(obj)
+
+ def opOnDocumentRestored(self, obj):
+ self.initOpProperties(obj, warn=True)
+
+ if PathLog.getLevel(PathLog.thisModule()) != 4:
+ obj.setEditorMode('ShowTempObjects', 2) # hide
+ else:
+ obj.setEditorMode('ShowTempObjects', 0) # show
+
+ # Repopulate enumerations in case of changes
+ ENUMS = self.propertyEnumerations()
+ for n in ENUMS:
+ restore = False
+ if hasattr(obj, n):
+ val = obj.getPropertyByName(n)
+ restore = True
+ setattr(obj, n, ENUMS[n])
+ if restore:
+ setattr(obj, n, val)
+
+ self.setEditorProperties(obj)
+
+ def opSetDefaultValues(self, obj, job):
+ '''opSetDefaultValues(obj, job) ... initialize defaults'''
+ job = PathUtils.findParentJob(obj)
+
+ obj.OptimizeLinearPaths = True
+ obj.InternalFeaturesCut = True
+ obj.OptimizeStepOverTransitions = False
+ obj.BoundaryEnforcement = True
+ obj.UseStartPoint = False
+ obj.AvoidLastX_InternalFeatures = True
+ obj.CutPatternReversed = False
+ obj.IgnoreOuterAbove = obj.StartDepth.Value + 0.00001
+ obj.StartPoint = FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value)
+ obj.Algorithm = 'OCL Dropcutter'
+ obj.LayerMode = 'Single-pass'
+ obj.CutMode = 'Conventional'
+ obj.CutPattern = 'None'
+ obj.HandleMultipleFeatures = 'Collectively' # 'Individually'
+ obj.PatternCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom'
+ obj.GapSizes = 'No gaps identified.'
+ obj.ClearLastLayer = 'Off'
+ obj.StepOver = 100
+ obj.CutPatternAngle = 0.0
+ obj.DepthOffset.Value = 0.0
+ obj.SampleInterval.Value = 1.0
+ obj.BoundaryAdjustment.Value = 0.0
+ obj.InternalFeaturesAdjustment.Value = 0.0
+ obj.AvoidLastX_Faces = 0
+ obj.PatternCenterCustom = FreeCAD.Vector(0.0, 0.0, 0.0)
+ obj.GapThreshold.Value = 0.005
+ obj.LinearDeflection.Value = 0.0001
+ obj.AngularDeflection.Value = 0.25
+ # For debugging
+ obj.ShowTempObjects = False
+
+ # need to overwrite the default depth calculations for facing
+ d = None
+ if job:
+ if job.Stock:
+ d = PathUtils.guessDepths(job.Stock.Shape, None)
+ obj.IgnoreOuterAbove = job.Stock.Shape.BoundBox.ZMax + 0.000001
+ PathLog.debug("job.Stock exists")
+ else:
+ PathLog.debug("job.Stock NOT exist")
+ else:
+ PathLog.debug("job NOT exist")
+
+ if d is not None:
+ obj.OpFinalDepth.Value = d.final_depth
+ obj.OpStartDepth.Value = d.start_depth
+ else:
+ obj.OpFinalDepth.Value = -10
+ obj.OpStartDepth.Value = 10
+
+ PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value))
+ PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value))
+
+ def opApplyPropertyLimits(self, obj):
+ '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.'''
+ # Limit sample interval
+ if obj.SampleInterval.Value < 0.0001:
+ obj.SampleInterval.Value = 0.0001
+ PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.0001 to 25.4 millimeters.'))
+ if obj.SampleInterval.Value > 25.4:
+ obj.SampleInterval.Value = 25.4
+ PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.0001 to 25.4 millimeters.'))
+
+ # Limit cut pattern angle
+ if obj.CutPatternAngle < -360.0:
+ obj.CutPatternAngle = 0.0
+ PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +-360 degrees.'))
+ if obj.CutPatternAngle >= 360.0:
+ obj.CutPatternAngle = 0.0
+ PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +- 360 degrees.'))
+
+ # Limit StepOver to natural number percentage
+ if obj.StepOver > 100:
+ obj.StepOver = 100
+ if obj.StepOver < 1:
+ obj.StepOver = 1
+
+ # Limit AvoidLastX_Faces to zero and positive values
+ if obj.AvoidLastX_Faces < 0:
+ obj.AvoidLastX_Faces = 0
+ PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Only zero or positive values permitted.'))
+ if obj.AvoidLastX_Faces > 100:
+ obj.AvoidLastX_Faces = 100
+ PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.'))
+
+ def opExecute(self, obj):
+ '''opExecute(obj) ... process surface operation'''
+ PathLog.track()
+
+ self.modelSTLs = list()
+ self.safeSTLs = list()
+ self.modelTypes = list()
+ self.boundBoxes = list()
+ self.profileShapes = list()
+ self.collectiveShapes = list()
+ self.individualShapes = list()
+ self.avoidShapes = list()
+ self.geoTlrnc = None
+ self.tempGroup = None
+ self.CutClimb = False
+ self.closedGap = False
+ self.tmpCOM = None
+ self.gaps = [0.1, 0.2, 0.3]
+ CMDS = list()
+ modelVisibility = list()
+ FCAD = FreeCAD.ActiveDocument
+
+ try:
+ dotIdx = __name__.index('.') + 1
+ except Exception:
+ dotIdx = 0
+ self.module = __name__[dotIdx:]
+
+ # make circle for workplane
+ self.wpc = Part.makeCircle(2.0)
+
+ # Set debugging behavior
+ self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects
+ self.showDebugObjects = obj.ShowTempObjects
+ deleteTempsFlag = True # Set to False for debugging
+ if PathLog.getLevel(PathLog.thisModule()) == 4:
+ deleteTempsFlag = False
+ else:
+ self.showDebugObjects = False
+
+ # mark beginning of operation and identify parent Job
+ PathLog.info('\nBegin Waterline operation...')
+ startTime = time.time()
+
+ # Identify parent Job
+ JOB = PathUtils.findParentJob(obj)
+ if JOB is None:
+ PathLog.error(translate('PathWaterline', "No JOB"))
+ return
+ self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin
+
+ # set cut mode; reverse as needed
+ if obj.CutMode == 'Climb':
+ self.CutClimb = True
+ if obj.CutPatternReversed is True:
+ if self.CutClimb is True:
+ self.CutClimb = False
+ else:
+ self.CutClimb = True
+
+ # Begin GCode for operation with basic information
+ # ... and move cutter to clearance height and startpoint
+ output = ''
+ if obj.Comment != '':
+ self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {}))
+ self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {}))
+ self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {}))
+ self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {}))
+ self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {}))
+ self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {}))
+ self.commandlist.append(Path.Command('N ({})'.format(output), {}))
+ self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
+ if obj.UseStartPoint:
+ self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid}))
+
+ # Instantiate additional class operation variables
+ self.resetOpVariables()
+
+ # Impose property limits
+ self.opApplyPropertyLimits(obj)
+
+ # Create temporary group for temporary objects, removing existing
+ # if self.showDebugObjects is True:
+ tempGroupName = 'tempPathWaterlineGroup'
+ if FCAD.getObject(tempGroupName):
+ for to in FCAD.getObject(tempGroupName).Group:
+ FCAD.removeObject(to.Name)
+ FCAD.removeObject(tempGroupName) # remove temp directory if already exists
+ if FCAD.getObject(tempGroupName + '001'):
+ for to in FCAD.getObject(tempGroupName + '001').Group:
+ FCAD.removeObject(to.Name)
+ FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists
+ tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName)
+ tempGroupName = tempGroup.Name
+ self.tempGroup = tempGroup
+ tempGroup.purgeTouched()
+ # Add temp object to temp group folder with following code:
+ # ... self.tempGroup.addObject(OBJ)
+
+ # Setup cutter for OCL and cutout value for operation - based on tool controller properties
+ self.cutter = self.setOclCutter(obj)
+ if self.cutter is False:
+ PathLog.error(translate('PathWaterline', "Canceling Waterline operation. Error creating OCL cutter."))
+ return
+ self.toolDiam = self.cutter.getDiameter()
+ self.radius = self.toolDiam / 2.0
+ self.cutOut = (self.toolDiam * (float(obj.StepOver) / 100.0))
+ self.gaps = [self.toolDiam, self.toolDiam, self.toolDiam]
+
+ # Get height offset values for later use
+ self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value
+ self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value
+
+ # Set deflection values for mesh generation
+ useDGT = False
+ try: # try/except is for Path Jobs created before GeometryTolerance
+ self.geoTlrnc = JOB.GeometryTolerance.Value
+ if self.geoTlrnc == 0.0:
+ useDGT = True
+ except AttributeError as ee:
+ PathLog.warning('{}\nPlease set Job.GeometryTolerance to an acceptable value. Using PathPreferences.defaultGeometryTolerance().'.format(ee))
+ useDGT = True
+ if useDGT:
+ import PathScripts.PathPreferences as PathPreferences
+ self.geoTlrnc = PathPreferences.defaultGeometryTolerance()
+
+ # Calculate default depthparams for operation
+ self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value)
+ self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0
+
+ # Save model visibilities for restoration
+ if FreeCAD.GuiUp:
+ for m in range(0, len(JOB.Model.Group)):
+ mNm = JOB.Model.Group[m].Name
+ modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility)
+
+ # Setup STL, model type, and bound box containers for each model in Job
+ for m in range(0, len(JOB.Model.Group)):
+ M = JOB.Model.Group[m]
+ self.modelSTLs.append(False)
+ self.safeSTLs.append(False)
+ self.profileShapes.append(False)
+ # Set bound box
+ if obj.BoundBox == 'BaseBoundBox':
+ if M.TypeId.startswith('Mesh'):
+ self.modelTypes.append('M') # Mesh
+ self.boundBoxes.append(M.Mesh.BoundBox)
+ else:
+ self.modelTypes.append('S') # Solid
+ self.boundBoxes.append(M.Shape.BoundBox)
+ elif obj.BoundBox == 'Stock':
+ self.modelTypes.append('S') # Solid
+ self.boundBoxes.append(JOB.Stock.Shape.BoundBox)
+
+ # ###### MAIN COMMANDS FOR OPERATION ######
+
+ # Begin processing obj.Base data and creating GCode
+ PSF = PathSurfaceSupport.ProcessSelectedFaces(JOB, obj)
+ PSF.setShowDebugObjects(tempGroup, self.showDebugObjects)
+ PSF.radius = self.radius
+ PSF.depthParams = self.depthParams
+ pPM = PSF.preProcessModel(self.module)
+ # Process selected faces, if available
+ if pPM is False:
+ PathLog.error('Unable to pre-process obj.Base.')
+ else:
+ (FACES, VOIDS) = pPM
+ self.modelSTLs = PSF.modelSTLs
+ self.profileShapes = PSF.profileShapes
+
+ # Create OCL.stl model objects
+ if obj.Algorithm == 'OCL Dropcutter':
+ self._prepareModelSTLs(JOB, obj)
+ PathLog.debug('obj.LinearDeflection.Value: {}'.format(obj.LinearDeflection.Value))
+ PathLog.debug('obj.AngularDeflection.Value: {}'.format(obj.AngularDeflection.Value))
+
+ for m in range(0, len(JOB.Model.Group)):
+ Mdl = JOB.Model.Group[m]
+ if FACES[m] is False:
+ PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label))
+ else:
+ if m > 0:
+ # Raise to clearance between models
+ CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label)))
+ CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
+ PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label))
+ # make stock-model-voidShapes STL model for avoidance detection on transitions
+ if obj.Algorithm == 'OCL Dropcutter':
+ self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m])
+ # Process model/faces - OCL objects must be ready
+ CMDS.extend(self._processWaterlineAreas(JOB, obj, m, FACES[m], VOIDS[m]))
+
+ # Save gcode produced
+ self.commandlist.extend(CMDS)
+
+ # ###### CLOSING COMMANDS FOR OPERATION ######
+
+ # Delete temporary objects
+ # Restore model visibilities for restoration
+ if FreeCAD.GuiUp:
+ FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False
+ for m in range(0, len(JOB.Model.Group)):
+ M = JOB.Model.Group[m]
+ M.Visibility = modelVisibility[m]
+
+ if deleteTempsFlag is True:
+ for to in tempGroup.Group:
+ if hasattr(to, 'Group'):
+ for go in to.Group:
+ FCAD.removeObject(go.Name)
+ FCAD.removeObject(to.Name)
+ FCAD.removeObject(tempGroupName)
+ else:
+ if len(tempGroup.Group) == 0:
+ FCAD.removeObject(tempGroupName)
+ else:
+ tempGroup.purgeTouched()
+
+ # Provide user feedback for gap sizes
+ gaps = list()
+ for g in self.gaps:
+ if g != self.toolDiam:
+ gaps.append(g)
+ if len(gaps) > 0:
+ obj.GapSizes = '{} mm'.format(gaps)
+ else:
+ if self.closedGap is True:
+ obj.GapSizes = 'Closed gaps < Gap Threshold.'
+ else:
+ obj.GapSizes = 'No gaps identified.'
+
+ # clean up class variables
+ self.resetOpVariables()
+ self.deleteOpVariables()
+
+ self.modelSTLs = None
+ self.safeSTLs = None
+ self.modelTypes = None
+ self.boundBoxes = None
+ self.gaps = None
+ self.closedGap = None
+ self.SafeHeightOffset = None
+ self.ClearHeightOffset = None
+ self.depthParams = None
+ self.midDep = None
+ del self.modelSTLs
+ del self.safeSTLs
+ del self.modelTypes
+ del self.boundBoxes
+ del self.gaps
+ del self.closedGap
+ del self.SafeHeightOffset
+ del self.ClearHeightOffset
+ del self.depthParams
+ del self.midDep
+
+ execTime = time.time() - startTime
+ PathLog.info('Operation time: {} sec.'.format(execTime))
+
+ return True
+
+ # Methods for constructing the cut area
+ def _prepareModelSTLs(self, JOB, obj):
+ PathLog.debug('_prepareModelSTLs()')
+ for m in range(0, len(JOB.Model.Group)):
+ M = JOB.Model.Group[m]
+
+ # PathLog.debug(f" -self.modelTypes[{m}] == 'M'")
+ if self.modelTypes[m] == 'M':
+ # TODO: test if this works
+ facets = M.Mesh.Facets.Points
+ else:
+ facets = Part.getFacets(M.Shape)
+
+ if self.modelSTLs[m] is True:
+ stl = ocl.STLSurf()
+
+ for tri in facets:
+ t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
+ ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
+ ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
+ stl.addTriangle(t)
+ self.modelSTLs[m] = stl
+ return
+
+ def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes):
+ '''_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.'''
+ PathLog.debug('_makeSafeSTL()')
+
+ fuseShapes = list()
+ Mdl = JOB.Model.Group[mdlIdx]
+ mBB = Mdl.Shape.BoundBox
+ sBB = JOB.Stock.Shape.BoundBox
+
+ # add Model shape to safeSTL shape
+ fuseShapes.append(Mdl.Shape)
+
+ if obj.BoundBox == 'BaseBoundBox':
+ cont = False
+ extFwd = (sBB.ZLength)
+ zmin = mBB.ZMin
+ zmax = mBB.ZMin + extFwd
+ stpDwn = (zmax - zmin) / 4.0
+ dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin)
+
+ try:
+ envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape
+ cont = True
+ except Exception as ee:
+ PathLog.error(str(ee))
+ shell = Mdl.Shape.Shells[0]
+ solid = Part.makeSolid(shell)
+ try:
+ envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape
+ cont = True
+ except Exception as eee:
+ PathLog.error(str(eee))
+
+ if cont:
+ stckWst = JOB.Stock.Shape.cut(envBB)
+ if obj.BoundaryAdjustment > 0.0:
+ cmpndFS = Part.makeCompound(faceShapes)
+ baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape
+ adjStckWst = stckWst.cut(baBB)
+ else:
+ adjStckWst = stckWst
+ fuseShapes.append(adjStckWst)
+ else:
+ PathLog.warning('Path transitions might not avoid the model. Verify paths.')
+ else:
+ # If boundbox is Job.Stock, add hidden pad under stock as base plate
+ toolDiam = self.cutter.getDiameter()
+ zMin = JOB.Stock.Shape.BoundBox.ZMin
+ xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam
+ yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam
+ bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam)
+ bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam)
+ bH = 1.0
+ crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0)
+ B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1))
+ fuseShapes.append(B)
+
+ if voidShapes is not False:
+ voidComp = Part.makeCompound(voidShapes)
+ voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape
+ fuseShapes.append(voidEnv)
+
+ fused = Part.makeCompound(fuseShapes)
+
+ if self.showDebugObjects is True:
+ T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape')
+ T.Shape = fused
+ T.purgeTouched()
+ self.tempGroup.addObject(T)
+
+ facets = Part.getFacets(fused)
+
+ stl = ocl.STLSurf()
+ for tri in facets:
+ t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
+ ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
+ ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
+ stl.addTriangle(t)
+
+ self.safeSTLs[mdlIdx] = stl
+
+ def _processWaterlineAreas(self, JOB, obj, mdlIdx, FCS, VDS):
+ '''_processWaterlineAreas(JOB, obj, mdlIdx, FCS, VDS)...
+ This method applies any avoided faces or regions to the selected faces.
+ It then calls the correct method.'''
+ PathLog.debug('_processWaterlineAreas()')
+
+ final = list()
+
+ # Process faces Collectively or Individually
+ if obj.HandleMultipleFeatures == 'Collectively':
+ if FCS is True:
+ COMP = False
+ else:
+ ADD = Part.makeCompound(FCS)
+ if VDS is not False:
+ DEL = Part.makeCompound(VDS)
+ COMP = ADD.cut(DEL)
+ else:
+ COMP = ADD
+
+ final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+ if obj.Algorithm == 'OCL Dropcutter':
+ final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
+ else:
+ final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
+
+ elif obj.HandleMultipleFeatures == 'Individually':
+ for fsi in range(0, len(FCS)):
+ fShp = FCS[fsi]
+ # self.deleteOpVariables(all=False)
+ self.resetOpVariables(all=False)
+
+ if fShp is True:
+ COMP = False
+ else:
+ ADD = Part.makeCompound([fShp])
+ if VDS is not False:
+ DEL = Part.makeCompound(VDS)
+ COMP = ADD.cut(DEL)
+ else:
+ COMP = ADD
+
+ final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+ if obj.Algorithm == 'OCL Dropcutter':
+ final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
+ else:
+ final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
+ COMP = None
+ # Eif
+
+ return final
+
+ # Methods for creating path geometry
+ def _getExperimentalWaterlinePaths(self, PNTSET, csHght, cutPattern):
+ '''_getExperimentalWaterlinePaths(PNTSET, csHght, cutPattern)...
+ Switching function for calling the appropriate path-geometry to OCL points conversion function
+ for the various cut patterns.'''
+ PathLog.debug('_getExperimentalWaterlinePaths()')
+ SCANS = list()
+
+ if cutPattern in ['Line', 'Spiral']:
+ stpOvr = list()
+ for D in PNTSET:
+ for SEG in D:
+ if SEG == 'BRK':
+ stpOvr.append(SEG)
+ else:
+ # D format is ((p1, p2), (p3, p4))
+ (A, B) = SEG
+ P1 = FreeCAD.Vector(A[0], A[1], csHght)
+ P2 = FreeCAD.Vector(B[0], B[1], csHght)
+ stpOvr.append((P1, P2))
+ SCANS.append(stpOvr)
+ stpOvr = list()
+ elif cutPattern == 'ZigZag':
+ stpOvr = list()
+ for (dirFlg, LNS) in PNTSET:
+ for SEG in LNS:
+ if SEG == 'BRK':
+ stpOvr.append(SEG)
+ else:
+ # D format is ((p1, p2), (p3, p4))
+ (A, B) = SEG
+ P1 = FreeCAD.Vector(A[0], A[1], csHght)
+ P2 = FreeCAD.Vector(B[0], B[1], csHght)
+ stpOvr.append((P1, P2))
+ SCANS.append(stpOvr)
+ stpOvr = list()
+ elif cutPattern in ['Circular', 'CircularZigZag']:
+ # PNTSET is list, by stepover.
+ # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp)
+ for so in range(0, len(PNTSET)):
+ stpOvr = list()
+ erFlg = False
+ (aTyp, dirFlg, ARCS) = PNTSET[so]
+
+ if dirFlg == 1: # 1
+ cMode = True # Climb mode
+ else:
+ cMode = False
+
+ for a in range(0, len(ARCS)):
+ Arc = ARCS[a]
+ if Arc == 'BRK':
+ stpOvr.append('BRK')
+ else:
+ (sp, ep, cp) = Arc
+ S = FreeCAD.Vector(sp[0], sp[1], csHght)
+ E = FreeCAD.Vector(ep[0], ep[1], csHght)
+ C = FreeCAD.Vector(cp[0], cp[1], csHght)
+ scan = (S, E, C, cMode)
+ if scan is False:
+ erFlg = True
+ else:
+ ##if aTyp == 'L':
+ ## stpOvr.append(FreeCAD.Vector(scan[0][0].x, scan[0][0].y, scan[0][0].z))
+ stpOvr.append(scan)
+ if erFlg is False:
+ SCANS.append(stpOvr)
+
+ return SCANS
+
+ # Main planar scan functions
+ def _stepTransitionCmds(self, obj, cutPattern, lstPnt, first, minSTH, tolrnc):
+ cmds = list()
+ rtpd = False
+ horizGC = 'G0'
+ hSpeed = self.horizRapid
+ height = obj.SafeHeight.Value
+
+ if cutPattern in ['Line', 'Circular', 'Spiral']:
+ if obj.OptimizeStepOverTransitions is True:
+ height = minSTH + 2.0
+ # if obj.LayerMode == 'Multi-pass':
+ # rtpd = minSTH
+ elif cutPattern in ['ZigZag', 'CircularZigZag']:
+ if obj.OptimizeStepOverTransitions is True:
+ zChng = first.z - lstPnt.z
+ # PathLog.debug('first.z: {}'.format(first.z))
+ # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z))
+ # PathLog.debug('zChng: {}'.format(zChng))
+ # PathLog.debug('minSTH: {}'.format(minSTH))
+ if abs(zChng) < tolrnc: # transitions to same Z height
+ # PathLog.debug('abs(zChng) < tolrnc')
+ if (minSTH - first.z) > tolrnc:
+ # PathLog.debug('(minSTH - first.z) > tolrnc')
+ height = minSTH + 2.0
+ else:
+ # PathLog.debug('ELSE (minSTH - first.z) > tolrnc')
+ horizGC = 'G1'
+ height = first.z
+ elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z):
+ height = False # allow end of Zig to cut to beginning of Zag
+
+
+ # Create raise, shift, and optional lower commands
+ if height is not False:
+ cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
+ cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
+ if rtpd is not False: # ReturnToPreviousDepth
+ cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
+
+ return cmds
+
+ def _breakCmds(self, obj, cutPattern, lstPnt, first, minSTH, tolrnc):
+ cmds = list()
+ rtpd = False
+ horizGC = 'G0'
+ hSpeed = self.horizRapid
+ height = obj.SafeHeight.Value
+
+ if cutPattern in ['Line', 'Circular', 'Spiral']:
+ if obj.OptimizeStepOverTransitions is True:
+ height = minSTH + 2.0
+ elif cutPattern in ['ZigZag', 'CircularZigZag']:
+ if obj.OptimizeStepOverTransitions is True:
+ zChng = first.z - lstPnt.z
+ if abs(zChng) < tolrnc: # transitions to same Z height
+ if (minSTH - first.z) > tolrnc:
+ height = minSTH + 2.0
+ else:
+ height = first.z + 2.0 # first.z
+
+ cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
+ cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
+ if rtpd is not False: # ReturnToPreviousDepth
+ cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
+
+ return cmds
+
+ def _planarGetPDC(self, stl, finalDep, SampleInterval, cutter):
+ pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object
+ pdc.setSTL(stl) # add stl model
+ pdc.setCutter(cutter) # add cutter
+ pdc.setZ(finalDep) # set minimumZ (final / target depth value)
+ pdc.setSampling(SampleInterval) # set sampling size
+ return pdc
+
+ # OCL Dropcutter waterline functions
+ def _oclWaterlineOp(self, JOB, obj, mdlIdx, subShp=None):
+ '''_oclWaterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.'''
+ commands = []
+
+ base = JOB.Model.Group[mdlIdx]
+ bb = self.boundBoxes[mdlIdx]
+ stl = self.modelSTLs[mdlIdx]
+ depOfst = obj.DepthOffset.Value
+
+ # Prepare global holdpoint and layerEndPnt containers
+ if self.holdPoint is None:
+ self.holdPoint = FreeCAD.Vector(0.0, 0.0, 0.0)
+ if self.layerEndPnt is None:
+ self.layerEndPnt = FreeCAD.Vector(0.0, 0.0, 0.0)
+
+ # Set extra offset to diameter of cutter to allow cutter to move around perimeter of model
+ toolDiam = self.cutter.getDiameter()
+
+ if subShp is None:
+ # Get correct boundbox
+ if obj.BoundBox == 'Stock':
+ BS = JOB.Stock
+ bb = BS.Shape.BoundBox
+ elif obj.BoundBox == 'BaseBoundBox':
+ BS = base
+ bb = base.Shape.BoundBox
+
+ xmin = bb.XMin
+ xmax = bb.XMax
+ ymin = bb.YMin
+ ymax = bb.YMax
+ else:
+ xmin = subShp.BoundBox.XMin
+ xmax = subShp.BoundBox.XMax
+ ymin = subShp.BoundBox.YMin
+ ymax = subShp.BoundBox.YMax
+
+ smplInt = obj.SampleInterval.Value
+ 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:
+ depthparams = [dp for dp in self.depthParams]
+ lenDP = len(depthparams)
+
+ # Scan the piece to depth at smplInt
+ oclScan = []
+ oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines)
+ oclScan = [FreeCAD.Vector(P.x, P.y, P.z + depOfst) for P in oclScan]
+ 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])
+ PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line")
+
+ # 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
+ PathLog.debug("--All layer scans combined took " + str(time.time() - layTime) + " s")
+ return commands
+
+ def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines):
+ '''_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ...
+ Perform OCL scan for waterline purpose.'''
+ 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
+ path.append(ocl.Line(p1, p2))
+ # path.append(l) # add the line to the path
+ pdc.setPath(path)
+ pdc.run() # run drop-cutter on the path
+
+ # return the list of points
+ return pdc.getCLPoints()
+
+ def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine):
+ '''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.'''
+ 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
+ self._bufferTopoMap(lenSL, pntsPerLine)
+ # Identify layer waterline from OCL scan
+ self._highlightWaterline(4, 9)
+ # Extract waterline and convert to gcode
+ loopList = self._extractWaterlines(obj, scanLines, lyr, layDep)
+ # save commands
+ for loop in loopList:
+ cmds = self._loopToGcode(obj, layDep, loop)
+ commands.extend(cmds)
+ return commands
+
+ def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine):
+ '''_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.'''
+ 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):
+ '''_bufferTopoMap(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):
+ '''_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.'''
+ TM = self.topoMap
+ lastPnt = len(TM[1]) - 1
+ lastLn = len(TM) - 1
+ highFlag = 0
+
+ # ("--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
+
+ # ("--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")
+ 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
+ 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
+
+ return True
+
+ def _extractWaterlines(self, obj, oclScan, lyr, layDep):
+ '''_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.'''
+ srch = True
+ lastPnt = len(self.topoMap[0]) - 1
+ lastLn = len(self.topoMap) - 1
+ maxSrchs = 5
+ srchCnt = 1
+ loopList = []
+ loop = []
+ loopNum = 0
+
+ if self.CutClimb is True:
+ 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:
+ PathLog.debug("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
+ PathLog.debug("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.")
+ return loopList
+
+ def _trackLoop(self, oclScan, lC, pC, L, P, loopNum):
+ '''_trackLoop(oclScan, lC, pC, L, P, loopNum) ... Track the loop direction.'''
+ 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:
+ PathLog.debug("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):
+ '''_findNextWlPoint(lC, pC, cl, cp, pl, pp) ...
+ Find the next waterline point in the point cloud layer provided.'''
+ 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:
+ # ("_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]
+
+ # ("_findNext: No next pnt found")
+ return [cl, cp, num]
+
+ def _loopToGcode(self, obj, layDep, loop):
+ '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.'''
+ # generate the path commands
+ output = []
+
+ prev = FreeCAD.Vector(2135984513.165, -58351896873.17455, 13838638431.861)
+ nxt = FreeCAD.Vector(0.0, 0.0, 0.0)
+
+ # Create first point
+ pnt = FreeCAD.Vector(loop[0].x, loop[0].y, 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
+
+ output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizFeed}))
+
+ # Rotate point data
+ prev = pnt
+ pnt = nxt
+
+ # Save layer end point for use in transitioning to next layer
+ self.layerEndPnt = pnt
+
+ return output
+
+ # Experimental waterline functions
+ def _experimentalWaterlineOp(self, JOB, obj, mdlIdx, subShp=None):
+ '''_waterlineOp(JOB, obj, mdlIdx, subShp=None) ...
+ Main waterline function to perform waterline extraction from model.'''
+ PathLog.debug('_experimentalWaterlineOp()')
+
+ commands = []
+ t_begin = time.time()
+ base = JOB.Model.Group[mdlIdx]
+ # bb = self.boundBoxes[mdlIdx]
+ # stl = self.modelSTLs[mdlIdx]
+ # safeSTL = self.safeSTLs[mdlIdx]
+ self.endVector = None
+
+ finDep = obj.FinalDepth.Value + (self.geoTlrnc / 10.0)
+ depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, finDep)
+
+ # Compute number and size of stepdowns, and final depth
+ if obj.LayerMode == 'Single-pass':
+ depthparams = [finDep]
+ else:
+ depthparams = [dp for dp in depthParams]
+ PathLog.debug('Experimental Waterline depthparams:\n{}'.format(depthparams))
+
+ # Prepare PathDropCutter objects with STL data
+ # safePDC = self._planarGetPDC(safeSTL, depthparams[lenDP - 1], obj.SampleInterval.Value, self.cutter)
+
+ buffer = self.cutter.getDiameter() * 10.0
+ borderFace = Part.Face(self._makeExtendedBoundBox(JOB.Stock.Shape.BoundBox, buffer, 0.0))
+
+ # Get correct boundbox
+ if obj.BoundBox == 'Stock':
+ stockEnv = PathSurfaceSupport.getShapeEnvelope(JOB.Stock.Shape)
+ bbFace = PathSurfaceSupport.getCrossSection(stockEnv) # returned at Z=0.0
+ elif obj.BoundBox == 'BaseBoundBox':
+ baseEnv = PathSurfaceSupport.getShapeEnvelope(base.Shape)
+ bbFace = PathSurfaceSupport.getCrossSection(baseEnv) # returned at Z=0.0
+
+ trimFace = borderFace.cut(bbFace)
+ if self.showDebugObjects is True:
+ TF = FreeCAD.ActiveDocument.addObject('Part::Feature', 'trimFace')
+ TF.Shape = trimFace
+ TF.purgeTouched()
+ self.tempGroup.addObject(TF)
+
+ # Cycle through layer depths
+ CUTAREAS = self._getCutAreas(base.Shape, depthparams, bbFace, trimFace, borderFace)
+ if not CUTAREAS:
+ PathLog.error('No cross-section cut areas identified.')
+ return commands
+
+ caCnt = 0
+ ofst = obj.BoundaryAdjustment.Value
+ ofst -= self.radius # (self.radius + (tolrnc / 10.0))
+ caLen = len(CUTAREAS)
+ lastCA = caLen - 1
+ lastClearArea = None
+ lastCsHght = None
+ clearLastLayer = True
+ for ca in range(0, caLen):
+ area = CUTAREAS[ca]
+ csHght = area.BoundBox.ZMin
+ csHght += obj.DepthOffset.Value
+ cont = False
+ caCnt += 1
+ if area.Area > 0.0:
+ cont = True
+ caWireCnt = len(area.Wires) - 1 # first wire is boundFace wire
+ if self.showDebugObjects:
+ CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'cutArea_{}'.format(caCnt))
+ CA.Shape = area
+ CA.purgeTouched()
+ self.tempGroup.addObject(CA)
+ else:
+ data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString
+ PathLog.debug('Cut area at {} is zero.'.format(data))
+
+ # get offset wire(s) based upon cross-section cut area
+ if cont:
+ area.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - area.BoundBox.ZMin))
+ activeArea = area.cut(trimFace)
+ activeAreaWireCnt = len(activeArea.Wires) # first wire is boundFace wire
+ if self.showDebugObjects:
+ CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'activeArea_{}'.format(caCnt))
+ CA.Shape = activeArea
+ CA.purgeTouched()
+ self.tempGroup.addObject(CA)
+ ofstArea = PathSurfaceSupport.extractFaceOffset(activeArea, ofst, self.wpc, makeComp=False)
+ if not ofstArea:
+ data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString
+ PathLog.debug('No offset area returned for cut area depth at {}.'.format(data))
+ cont = False
+
+ if cont:
+ # Identify solid areas in the offset data
+ ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea)
+ if ofstSolidFacesList:
+ clearArea = Part.makeCompound(ofstSolidFacesList)
+ if self.showDebugObjects is True:
+ CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearArea_{}'.format(caCnt))
+ CA.Shape = clearArea
+ CA.purgeTouched()
+ self.tempGroup.addObject(CA)
+ else:
+ cont = False
+ data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString
+ PathLog.error('Could not determine solid faces at {}.'.format(data))
+
+ if cont:
+ # Make waterline path for current CUTAREA depth (csHght)
+ commands.extend(self._wiresToWaterlinePath(obj, clearArea, csHght))
+ clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clearArea.BoundBox.ZMin))
+ lastClearArea = clearArea
+ lastCsHght = csHght
+
+ # Clear layer as needed
+ (clrLyr, clearLastLayer) = self._clearLayer(obj, ca, lastCA, clearLastLayer)
+ if clrLyr == 'Offset':
+ commands.extend(self._makeOffsetLayerPaths(obj, clearArea, csHght))
+ elif clrLyr:
+ cutPattern = obj.CutPattern
+ if clearLastLayer is False:
+ cutPattern = obj.ClearLastLayer
+ commands.extend(self._makeCutPatternLayerPaths(JOB, obj, clearArea, csHght, cutPattern))
+ # Efor
+
+ if clearLastLayer:
+ (clrLyr, cLL) = self._clearLayer(obj, 1, 1, False)
+ lastClearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - lastClearArea.BoundBox.ZMin))
+ if clrLyr == 'Offset':
+ commands.extend(self._makeOffsetLayerPaths(obj, lastClearArea, lastCsHght))
+ elif clrLyr:
+ commands.extend(self._makeCutPatternLayerPaths(JOB, obj, lastClearArea, lastCsHght, obj.ClearLastLayer))
+
+ PathLog.info("Waterline: All layer scans combined took " + str(time.time() - t_begin) + " s")
+ return commands
+
+ def _getCutAreas(self, shape, depthparams, bbFace, trimFace, borderFace):
+ '''_getCutAreas(JOB, shape, depthparams, bbFace, borderFace) ...
+ Takes shape, depthparams and base-envelope-cross-section, and
+ returns a list of cut areas - one for each depth.'''
+ PathLog.debug('_getCutAreas()')
+
+ CUTAREAS = list()
+ isFirst = True
+ lenDP = len(depthparams)
+
+ # Cycle through layer depths
+ for dp in range(0, lenDP):
+ csHght = depthparams[dp]
+ # PathLog.debug('Depth {} is {}'.format(dp + 1, csHght))
+
+ # Get slice at depth of shape
+ csFaces = self._getModelCrossSection(shape, csHght) # returned at Z=0.0
+ if not csFaces:
+ data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString
+ else:
+ if len(csFaces) > 0:
+ useFaces = self._getSolidAreasFromPlanarFaces(csFaces)
+ else:
+ useFaces = False
+
+ if useFaces:
+ compAdjFaces = Part.makeCompound(useFaces)
+
+ if self.showDebugObjects is True:
+ CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSolids_{}'.format(dp + 1))
+ CA.Shape = compAdjFaces
+ CA.purgeTouched()
+ self.tempGroup.addObject(CA)
+
+ if isFirst:
+ allPrevComp = compAdjFaces
+ cutArea = borderFace.cut(compAdjFaces)
+ else:
+ preCutArea = borderFace.cut(compAdjFaces)
+ cutArea = preCutArea.cut(allPrevComp) # cut out higher layers to avoid cutting recessed areas
+ allPrevComp = allPrevComp.fuse(compAdjFaces)
+ cutArea.translate(FreeCAD.Vector(0.0, 0.0, csHght - cutArea.BoundBox.ZMin))
+ CUTAREAS.append(cutArea)
+ isFirst = False
+ else:
+ PathLog.error('No waterline at depth: {} mm.'.format(csHght))
+ # Efor
+
+ if len(CUTAREAS) > 0:
+ return CUTAREAS
+
+ return False
+
+ def _wiresToWaterlinePath(self, obj, ofstPlnrShp, csHght):
+ PathLog.debug('_wiresToWaterlinePath()')
+ commands = list()
+
+ # Translate path geometry to layer height
+ ofstPlnrShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - ofstPlnrShp.BoundBox.ZMin))
+ if self.showDebugObjects is True:
+ OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'waterlinePathArea_{}'.format(round(csHght, 2)))
+ OA.Shape = ofstPlnrShp
+ OA.purgeTouched()
+ self.tempGroup.addObject(OA)
+
+ commands.append(Path.Command('N (Cut Area {}.)'.format(round(csHght, 2))))
+ start = 1
+ if csHght < obj.IgnoreOuterAbove:
+ start = 0
+ for w in range(start, len(ofstPlnrShp.Wires)):
+ wire = ofstPlnrShp.Wires[w]
+ V = wire.Vertexes
+ if obj.CutMode == 'Climb':
+ lv = len(V) - 1
+ startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z)
+ else:
+ startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z)
+
+ commands.append(Path.Command('N (Wire {}.)'.format(w)))
+ (cmds, endVect) = self._wireToPath(obj, wire, startVect)
+ commands.extend(cmds)
+ commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+
+ return commands
+
+ def _makeCutPatternLayerPaths(self, JOB, obj, clrAreaShp, csHght, cutPattern):
+ PathLog.debug('_makeCutPatternLayerPaths()')
+ commands = []
+
+ clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clrAreaShp.BoundBox.ZMin))
+
+ # Convert pathGeom to gcode more efficiently
+ if cutPattern == 'Offset':
+ commands.extend(self._makeOffsetLayerPaths(obj, clrAreaShp, csHght))
+ else:
+ # Request path geometry from external support class
+ PGG = PathSurfaceSupport.PathGeometryGenerator(obj, clrAreaShp, cutPattern)
+ if self.showDebugObjects:
+ PGG.setDebugObjectsGroup(self.tempGroup)
+ self.tmpCOM = PGG.getCenterOfPattern()
+ pathGeom = PGG.generatePathGeometry()
+ if not pathGeom:
+ PathLog.warning('No path geometry generated.')
+ return commands
+ pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin))
+
+ if self.showDebugObjects is True:
+ OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'pathGeom_{}'.format(round(csHght, 2)))
+ OA.Shape = pathGeom
+ OA.purgeTouched()
+ self.tempGroup.addObject(OA)
+
+ if cutPattern == 'Line':
+ pntSet = PathSurfaceSupport.pathGeomToLinesPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps)
+ elif cutPattern == 'ZigZag':
+ pntSet = PathSurfaceSupport.pathGeomToZigzagPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps)
+ elif cutPattern in ['Circular', 'CircularZigZag']:
+ pntSet = PathSurfaceSupport.pathGeomToCircularPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps, self.tmpCOM)
+ elif cutPattern == 'Spiral':
+ pntSet = PathSurfaceSupport.pathGeomToSpiralPointSet(obj, pathGeom)
+
+ stpOVRS = self._getExperimentalWaterlinePaths(pntSet, csHght, cutPattern)
+ safePDC = False
+ cmds = self._clearGeomToPaths(JOB, obj, safePDC, stpOVRS, cutPattern)
+ commands.extend(cmds)
+
+ return commands
+
+ def _makeOffsetLayerPaths(self, obj, clrAreaShp, csHght):
+ PathLog.debug('_makeOffsetLayerPaths()')
+ cmds = list()
+ ofst = 0.0 - self.cutOut
+ shape = clrAreaShp
+ cont = True
+ cnt = 0
+ while cont:
+ ofstArea = PathSurfaceSupport.extractFaceOffset(shape, ofst, self.wpc, makeComp=True)
+ if not ofstArea:
+ break
+ for F in ofstArea.Faces:
+ cmds.extend(self._wiresToWaterlinePath(obj, F, csHght))
+ shape = ofstArea
+ if cnt == 0:
+ ofst = 0.0 - self.cutOut
+ cnt += 1
+ return cmds
+
+ def _clearGeomToPaths(self, JOB, obj, safePDC, stpOVRS, cutPattern):
+ PathLog.debug('_clearGeomToPaths()')
+
+ GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})]
+ tolrnc = JOB.GeometryTolerance.Value
+ lenstpOVRS = len(stpOVRS)
+ lstSO = lenstpOVRS - 1
+ lstStpOvr = False
+ gDIR = ['G3', 'G2']
+
+ if self.CutClimb is True:
+ gDIR = ['G2', 'G3']
+
+ # Send cutter to x,y position of first point on first line
+ first = stpOVRS[0][0][0] # [step][item][point]
+ GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid}))
+
+ # Cycle through step-over sections (line segments or arcs)
+ odd = True
+ lstStpEnd = None
+ for so in range(0, lenstpOVRS):
+ cmds = list()
+ PRTS = stpOVRS[so]
+ lenPRTS = len(PRTS)
+ first = PRTS[0][0] # first point of arc/line stepover group
+ last = None
+ cmds.append(Path.Command('N (Begin step {}.)'.format(so), {}))
+ if so == lstSO:
+ lstStpOvr = True
+
+ if so > 0:
+ if cutPattern == 'CircularZigZag':
+ if odd:
+ odd = False
+ else:
+ odd = True
+ # minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL
+ minTrnsHght = obj.SafeHeight.Value
+ # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {}))
+ cmds.extend(self._stepTransitionCmds(obj, cutPattern, lstStpEnd, first, minTrnsHght, tolrnc))
+
+ # Cycle through current step-over parts
+ for i in range(0, lenPRTS):
+ prt = PRTS[i]
+ # PathLog.debug('prt: {}'.format(prt))
+ if prt == 'BRK':
+ nxtStart = PRTS[i + 1][0]
+ # minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL
+ minSTH = obj.SafeHeight.Value
+ cmds.append(Path.Command('N (Break)', {}))
+ cmds.extend(self._breakCmds(obj, cutPattern, last, nxtStart, minSTH, tolrnc))
+ else:
+ cmds.append(Path.Command('N (part {}.)'.format(i + 1), {}))
+ if cutPattern in ['Line', 'ZigZag', 'Spiral']:
+ start, last = prt
+ cmds.append(Path.Command('G1', {'X': start.x, 'Y': start.y, 'Z': start.z, 'F': self.horizFeed}))
+ cmds.append(Path.Command('G1', {'X': last.x, 'Y': last.y, 'F': self.horizFeed}))
+ elif cutPattern in ['Circular', 'CircularZigZag']:
+ # isCircle = True if lenPRTS == 1 else False
+ isZigZag = True if cutPattern == 'CircularZigZag' else False
+ PathLog.debug('so, isZigZag, odd, cMode: {}, {}, {}, {}'.format(so, isZigZag, odd, prt[3]))
+ gcode = self._makeGcodeArc(prt, gDIR, odd, isZigZag)
+ cmds.extend(gcode)
+ cmds.append(Path.Command('N (End of step {}.)'.format(so), {}))
+ GCODE.extend(cmds) # save line commands
+ lstStpEnd = last
+ # Efor
+
+ # Raise to safe height after clearing
+ GCODE.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+
+ return GCODE
+
+ def _getSolidAreasFromPlanarFaces(self, csFaces):
+ PathLog.debug('_getSolidAreasFromPlanarFaces()')
+ holds = list()
+ useFaces = list()
+ lenCsF = len(csFaces)
+ PathLog.debug('lenCsF: {}'.format(lenCsF))
+
+ if lenCsF == 1:
+ useFaces = csFaces
+ else:
+ fIds = list()
+ aIds = list()
+ pIds = list()
+ cIds = list()
+
+ for af in range(0, lenCsF):
+ fIds.append(af) # face ids
+ aIds.append(af) # face ids
+ pIds.append(-1) # parent ids
+ cIds.append(False) # cut ids
+ holds.append(False)
+
+ while len(fIds) > 0:
+ li = fIds.pop()
+ low = csFaces[li] # senior face
+ pIds = self._idInternalFeature(csFaces, fIds, pIds, li, low)
+
+ for af in range(lenCsF - 1, -1, -1): # cycle from last item toward first
+ prnt = pIds[af]
+ if prnt == -1:
+ stack = -1
+ else:
+ stack = [af]
+ # get_face_ids_to_parent
+ stack.insert(0, prnt)
+ nxtPrnt = pIds[prnt]
+ # find af value for nxtPrnt
+ while nxtPrnt != -1:
+ stack.insert(0, nxtPrnt)
+ nxtPrnt = pIds[nxtPrnt]
+ cIds[af] = stack
+
+ for af in range(0, lenCsF):
+ pFc = cIds[af]
+ if pFc == -1:
+ # Simple, independent region
+ holds[af] = csFaces[af] # place face in hold
+ else:
+ # Compound region
+ cnt = len(pFc)
+ if cnt % 2.0 == 0.0:
+ # even is donut cut
+ inr = pFc[cnt - 1]
+ otr = pFc[cnt - 2]
+ holds[otr] = holds[otr].cut(csFaces[inr])
+ else:
+ # odd is floating solid
+ holds[af] = csFaces[af]
+
+ for af in range(0, lenCsF):
+ if holds[af]:
+ useFaces.append(holds[af]) # save independent solid
+ # Eif
+
+ if len(useFaces) > 0:
+ return useFaces
+
+ return False
+
+ def _getModelCrossSection(self, shape, csHght):
+ PathLog.debug('getCrossSection()')
+ wires = list()
+
+ def byArea(fc):
+ return fc.Area
+
+ for i in shape.slice(FreeCAD.Vector(0, 0, 1), csHght):
+ wires.append(i)
+
+ if len(wires) > 0:
+ for w in wires:
+ if w.isClosed() is False:
+ return False
+ FCS = list()
+ for w in wires:
+ w.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - w.BoundBox.ZMin))
+ FCS.append(Part.Face(w))
+ FCS.sort(key=byArea, reverse=True)
+ return FCS
+ else:
+ PathLog.debug(' -No wires from .slice() method')
+
+ 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 _idInternalFeature(self, csFaces, fIds, pIds, li, low):
+ Ids = list()
+ for i in fIds:
+ Ids.append(i)
+ while len(Ids) > 0:
+ hi = Ids.pop()
+ high = csFaces[hi]
+ if self._isInBoundBox(high, low):
+ cmn = high.common(low)
+ if cmn.Area > 0.0:
+ pIds[li] = hi
+ break
+
+ return pIds
+
+ def _wireToPath(self, obj, wire, startVect):
+ '''_wireToPath(obj, wire, startVect) ... wire to path.'''
+ PathLog.track()
+
+ paths = []
+ pathParams = {} # pylint: disable=assignment-from-no-return
+
+ pathParams['shapes'] = [wire]
+ pathParams['feedrate'] = self.horizFeed
+ pathParams['feedrate_v'] = self.vertFeed
+ pathParams['verbose'] = True
+ pathParams['resume_height'] = obj.SafeHeight.Value
+ pathParams['retraction'] = obj.ClearanceHeight.Value
+ pathParams['return_end'] = True
+ # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers
+ pathParams['preamble'] = False
+ pathParams['start'] = startVect
+
+ (pp, end_vector) = Path.fromShapes(**pathParams)
+ paths.extend(pp.Commands)
+
+ self.endVector = end_vector # pylint: disable=attribute-defined-outside-init
+
+ return (paths, end_vector)
+
+ def _makeExtendedBoundBox(self, wBB, bbBfr, zDep):
+ pl = FreeCAD.Placement()
+ pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0)
+ pl.Base = FreeCAD.Vector(0, 0, 0)
+
+ 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)
+ bb = Part.makePolygon([p1, p2, p3, p4, p1])
+
+ return bb
+
+ def _makeGcodeArc(self, prt, gDIR, odd, isZigZag):
+ cmds = list()
+ strtPnt, endPnt, cntrPnt, cMode = prt
+ gdi = 0
+ if odd:
+ gdi = 1
+ else:
+ if not cMode and isZigZag:
+ gdi = 1
+ gCmd = gDIR[gdi]
+
+ # ijk = self.tmpCOM - strtPnt
+ # ijk = self.tmpCOM.sub(strtPnt) # vector from start to center
+ ijk = cntrPnt.sub(strtPnt) # vector from start to center
+ xyz = endPnt
+ cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
+ cmds.append(Path.Command(gCmd, {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z,
+ 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
+ 'F': self.horizFeed}))
+ cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed}))
+
+ return cmds
+
+ def _clearLayer(self, obj, ca, lastCA, clearLastLayer):
+ PathLog.debug('_clearLayer()')
+ clrLyr = False
+
+ if obj.ClearLastLayer == 'Off':
+ if obj.CutPattern != 'None':
+ clrLyr = obj.CutPattern
+ else:
+ obj.CutPattern = 'None'
+ if ca == lastCA: # if current iteration is last layer
+ PathLog.debug('... Clearing bottom layer.')
+ clrLyr = obj.ClearLastLayer
+ clearLastLayer = False
+
+ return (clrLyr, clearLastLayer)
+
+ # Support methods
+ def resetOpVariables(self, all=True):
+ '''resetOpVariables() ... Reset class variables used for instance of operation.'''
+ self.holdPoint = None
+ self.layerEndPnt = None
+ self.onHold = False
+ self.SafeHeightOffset = 2.0
+ self.ClearHeightOffset = 4.0
+ self.layerEndzMax = 0.0
+ self.resetTolerance = 0.0
+ self.holdPntCnt = 0
+ self.bbRadius = 0.0
+ self.axialFeed = 0.0
+ self.axialRapid = 0.0
+ self.FinalDepth = 0.0
+ self.clearHeight = 0.0
+ self.safeHeight = 0.0
+ self.faceZMax = -999999999999.0
+ if all is True:
+ self.cutter = None
+ self.stl = None
+ self.fullSTL = None
+ self.cutOut = 0.0
+ self.radius = 0.0
+ self.useTiltCutter = False
+ return True
+
+ def deleteOpVariables(self, all=True):
+ '''deleteOpVariables() ... Reset class variables used for instance of operation.'''
+ del self.holdPoint
+ del self.layerEndPnt
+ del self.onHold
+ del self.SafeHeightOffset
+ del self.ClearHeightOffset
+ del self.layerEndzMax
+ del self.resetTolerance
+ del self.holdPntCnt
+ del self.bbRadius
+ del self.axialFeed
+ del self.axialRapid
+ del self.FinalDepth
+ del self.clearHeight
+ del self.safeHeight
+ del self.faceZMax
+ if all is True:
+ del self.cutter
+ del self.stl
+ del self.fullSTL
+ del self.cutOut
+ del self.radius
+ del self.useTiltCutter
+ return True
+
+ def setOclCutter(self, obj, safe=False):
+ ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. '''
+ # Set cutter details
+ # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details
+ diam_1 = float(obj.ToolController.Tool.Diameter)
+ lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0
+ FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0
+ CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0
+ CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0
+
+ # Make safeCutter with 2 mm buffer around physical cutter
+ if safe is True:
+ diam_1 += 4.0
+ if FR != 0.0:
+ FR += 2.0
+
+ PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType))
+ if obj.ToolController.Tool.ToolType == 'EndMill':
+ # Standard End Mill
+ return 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.useTiltCutter = True
+ return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst))
+
+ 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)
+ return 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)
+ return ocl.ConeCutter(diam_1, (CEA / 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)
+ return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
+ else:
+ # Default to standard end mill
+ PathLog.warning("Defaulting cutter to standard end mill.")
+ return ocl.CylCutter(diam_1, (CEH + lenOfst))
+
+
+def SetupProperties():
+ ''' SetupProperties() ... Return list of properties required for operation.'''
+ setup = ['Algorithm', 'AvoidLastX_Faces', 'AvoidLastX_InternalFeatures', 'BoundBox']
+ setup.extend(['BoundaryAdjustment', 'PatternCenterAt', 'PatternCenterCustom'])
+ setup.extend(['ClearLastLayer', 'InternalFeaturesCut', 'InternalFeaturesAdjustment'])
+ setup.extend(['CutMode', 'CutPattern', 'CutPatternAngle', 'CutPatternReversed'])
+ setup.extend(['DepthOffset', 'GapSizes', 'GapThreshold', 'StepOver'])
+ setup.extend(['HandleMultipleFeatures', 'LayerMode', 'OptimizeStepOverTransitions'])
+ setup.extend(['BoundaryEnforcement', 'SampleInterval', 'StartPoint', 'IgnoreOuterAbove'])
+ setup.extend(['UseStartPoint', 'AngularDeflection', 'LinearDeflection', 'ShowTempObjects'])
+ return setup
+
+
+def Create(name, obj=None):
+ '''Create(name) ... Creates and returns a Waterline operation.'''
+ if obj is None:
+ obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
+ obj.Proxy = ObjectWaterline(obj, name)
+ return obj
diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py
index eed15fc3d3..0616bbe6d2 100644
--- a/src/Mod/Path/PathScripts/PathWaterlineGui.py
+++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py
@@ -90,6 +90,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
else:
self.form.optimizeEnabled.setCheckState(QtCore.Qt.Unchecked)
+ self.updateVisibility()
+
def getSignalsForUpdate(self, obj):
'''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
signals = []
@@ -106,21 +108,32 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
return signals
def updateVisibility(self):
- if self.form.algorithmSelect.currentText() == 'OCL Dropcutter':
- self.form.cutPattern.setEnabled(False)
- self.form.boundaryAdjustment.setEnabled(False)
- self.form.stepOver.setEnabled(False)
- self.form.sampleInterval.setEnabled(True)
- self.form.optimizeEnabled.setEnabled(True)
- else:
- self.form.cutPattern.setEnabled(True)
- self.form.boundaryAdjustment.setEnabled(True)
+ '''updateVisibility()... Updates visibility of Tasks panel objects.'''
+ Algorithm = self.form.algorithmSelect.currentText()
+ self.form.optimizeEnabled.hide() # Has no independent QLabel object
+
+ if Algorithm == 'OCL Dropcutter':
+ self.form.cutPattern.hide()
+ self.form.cutPattern_label.hide()
+ self.form.boundaryAdjustment.hide()
+ self.form.boundaryAdjustment_label.hide()
+ self.form.stepOver.hide()
+ self.form.stepOver_label.hide()
+ self.form.sampleInterval.show()
+ self.form.sampleInterval_label.show()
+ elif Algorithm == 'Experimental':
+ self.form.cutPattern.show()
+ self.form.boundaryAdjustment.show()
+ self.form.cutPattern_label.show()
+ self.form.boundaryAdjustment_label.show()
if self.form.cutPattern.currentText() == 'None':
- self.form.stepOver.setEnabled(False)
+ self.form.stepOver.hide()
+ self.form.stepOver_label.hide()
else:
- self.form.stepOver.setEnabled(True)
- self.form.sampleInterval.setEnabled(False)
- self.form.optimizeEnabled.setEnabled(False)
+ self.form.stepOver.show()
+ self.form.stepOver_label.show()
+ self.form.sampleInterval.hide()
+ self.form.sampleInterval_label.hide()
def registerSignalHandlers(self, obj):
self.form.algorithmSelect.currentIndexChanged.connect(self.updateVisibility)