FreeCAD / src /Mod /CAM /Path /Op /Waterline.py
AbdulElahGwaith's picture
Upload folder using huggingface_hub
985c397 verified
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2019 Russell Johnson (russ4262) <russ4262@gmail.com> *
# * Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
__title__ = "CAM Waterline Operation"
__author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)"
__url__ = "https://www.freecad.org"
__doc__ = "Class and implementation of Waterline operation."
__contributors__ = ""
translate = FreeCAD.Qt.translate
# OCL must be installed
try:
try:
import ocl
except ImportError:
import opencamlib as ocl
except ImportError:
msg = translate("path_waterline", "This operation requires OpenCamLib to be installed.")
FreeCAD.Console.PrintError(msg + "\n")
raise ImportError
import Path
import Path.Op.Base as PathOp
import Path.Op.SurfaceSupport as PathSurfaceSupport
import PathScripts.PathUtils as PathUtils
import math
import time
from PySide.QtCore import QT_TRANSLATE_NOOP
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
Part = LazyLoader("Part", globals(), "Part")
if FreeCAD.GuiUp:
import FreeCADGui
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
class ObjectWaterline(PathOp.ObjectOp):
"""Proxy object for Surfacing operation."""
def opFeatures(self, obj):
"""opFeatures(obj) ... return all standard features"""
return (
PathOp.FeatureTool
| PathOp.FeatureDepths
| PathOp.FeatureHeights
| PathOp.FeatureStepDown
| PathOp.FeatureCoolant
| PathOp.FeatureBaseFaces
)
@classmethod
def propertyEnumerations(self, dataType="data"):
"""propertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
Args:
dataType = 'data', 'raw', 'translated'
Notes:
'data' is list of internal string literals used in code
'raw' is list of (translated_text, data_string) tuples
'translated' is list of translated string literals
"""
# Enumeration lists for App::PropertyEnumeration properties
enums = {
"Algorithm": [
(translate("path_waterline", "OCL Dropcutter"), "OCL Dropcutter"),
(translate("path_waterline", "OCL Adaptive"), "OCL Adaptive"),
(translate("path_waterline", "Experimental"), "Experimental"),
],
"BoundBox": [
(translate("path_waterline", "BaseBoundBox"), "BaseBoundBox"),
(translate("path_waterline", "Stock"), "Stock"),
],
"PatternCenterAt": [
(translate("path_waterline", "CenterOfMass"), "CenterOfMass"),
(translate("path_waterline", "CenterOfBoundBox"), "CenterOfBoundBox"),
(translate("path_waterline", "XminYmin"), "XminYmin"),
(translate("path_waterline", "Custom"), "Custom"),
],
"ClearLastLayer": [
(translate("path_waterline", "Off"), "Off"),
(translate("path_waterline", "Circular"), "Circular"),
(translate("path_waterline", "CircularZigZag"), "CircularZigZag"),
(translate("path_waterline", "Line"), "Line"),
(translate("path_waterline", "Offset"), "Offset"),
(translate("path_waterline", "Spiral"), "Spiral"),
(translate("path_waterline", "ZigZag"), "ZigZag"),
],
"CutMode": [
(translate("path_waterline", "Conventional"), "Conventional"),
(translate("path_waterline", "Climb"), "Climb"),
],
"CutPattern": [
(translate("path_waterline", "None"), "None"),
(translate("path_waterline", "Circular"), "Circular"),
(translate("path_waterline", "CircularZigZag"), "CircularZigZag"),
(translate("path_waterline", "Line"), "Line"),
(translate("path_waterline", "Offset"), "Offset"),
(translate("path_waterline", "Spiral"), "Spiral"),
(translate("path_waterline", "ZigZag"), "ZigZag"),
],
"HandleMultipleFeatures": [
(translate("path_waterline", "Collectively"), "Collectively"),
(translate("path_waterline", "Individually"), "Individually"),
],
"LayerMode": [
(translate("path_waterline", "Single-pass"), "Single-pass"),
(translate("path_waterline", "Multi-pass"), "Multi-pass"),
],
}
if dataType == "raw":
return enums
data = list()
idx = 0 if dataType == "translated" else 1
Path.Log.debug(enums)
for k, v in enumerate(enums):
data.append((v, [tup[idx] for tup in enums[v]]))
Path.Log.debug(data)
return data
def initOperation(self, obj):
"""initOperation(obj) ... Initialize the operation by
managing property creation and property editor status."""
self.propertiesReady = False
self.initOpProperties(obj) # Initialize operation-specific properties
# For debugging
if Path.Log.getLevel(Path.Log.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"""
self.addNewProps = list()
for prtyp, nm, grp, tt in self.opPropertyDefinitions():
if not hasattr(obj, nm):
obj.addProperty(prtyp, nm, grp, tt)
self.addNewProps.append(nm)
# Set enumeration lists for enumeration properties
if len(self.addNewProps) > 0:
ENUMS = self.propertyEnumerations()
for n in ENUMS:
if n[0] in self.addNewProps:
setattr(obj, n[0], n[1])
if warn:
newPropMsg = translate("PathWaterline", "New property added to")
newPropMsg += ' "{}": {}'.format(obj.Label, self.addNewProps) + ". "
newPropMsg += translate("PathWaterline", "Check default value(s).")
FreeCAD.Console.PrintWarning(newPropMsg + "\n")
self.propertiesReady = True
def opPropertyDefinitions(self):
"""opPropertyDefinitions() ... return list of tuples containing operation specific properties"""
return [
(
"App::PropertyBool",
"ShowTempObjects",
"Debug",
QT_TRANSLATE_NOOP(
"App::Property",
"Show the temporary path construction objects when module is in DEBUG mode.",
),
),
(
"App::PropertyDistance",
"AngularDeflection",
"Mesh Conversion",
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",
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",
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",
QT_TRANSLATE_NOOP(
"App::Property", "Do not cut internal features on avoided faces."
),
),
(
"App::PropertyDistance",
"BoundaryAdjustment",
"Selected Geometry Settings",
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",
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",
QT_TRANSLATE_NOOP(
"App::Property",
"Choose how to process multiple Base Geometry features.",
),
),
(
"App::PropertyDistance",
"InternalFeaturesAdjustment",
"Selected Geometry Settings",
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",
QT_TRANSLATE_NOOP(
"App::Property",
"Cut internal feature areas within a larger selected face.",
),
),
(
"App::PropertyEnumeration",
"Algorithm",
"Clearing Options",
QT_TRANSLATE_NOOP(
"App::Property",
"Select the algorithm to use: OCL Dropcutter*, OCL Adaptive or Experimental (Not OCL based).",
),
),
(
"App::PropertyEnumeration",
"BoundBox",
"Clearing Options",
QT_TRANSLATE_NOOP(
"App::Property", "Select the overall boundary for the operation."
),
),
(
"App::PropertyEnumeration",
"ClearLastLayer",
"Clearing Options",
QT_TRANSLATE_NOOP(
"App::Property",
"Set to clear last layer in a `Multi-pass` operation.",
),
),
(
"App::PropertyEnumeration",
"CutMode",
"Clearing Options",
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",
QT_TRANSLATE_NOOP(
"App::Property",
"Set the geometric clearing pattern to use for the operation.",
),
),
(
"App::PropertyFloat",
"CutPatternAngle",
"Clearing Options",
QT_TRANSLATE_NOOP(
"App::Property", "The yaw angle used for certain clearing patterns"
),
),
(
"App::PropertyBool",
"CutPatternReversed",
"Clearing Options",
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",
QT_TRANSLATE_NOOP(
"App::Property",
"Set the Z-axis depth offset from the target surface.",
),
),
(
"App::PropertyDistance",
"IgnoreOuterAbove",
"Clearing Options",
QT_TRANSLATE_NOOP("App::Property", "Ignore outer waterlines above this height."),
),
(
"App::PropertyEnumeration",
"LayerMode",
"Clearing Options",
QT_TRANSLATE_NOOP(
"App::Property",
"Complete the operation in a single pass at depth, or multiple passes to final depth.",
),
),
(
"App::PropertyVectorDistance",
"PatternCenterCustom",
"Clearing Options",
QT_TRANSLATE_NOOP("App::Property", "Set the start point for the cut pattern."),
),
(
"App::PropertyEnumeration",
"PatternCenterAt",
"Clearing Options",
QT_TRANSLATE_NOOP(
"App::Property",
"Choose location of the center point for starting the cut pattern.",
),
),
(
"App::PropertyDistance",
"SampleInterval",
"Clearing Options",
QT_TRANSLATE_NOOP(
"App::Property",
"Set the sampling resolution. Smaller values quickly increase processing time.",
),
),
(
"App::PropertyDistance",
"MinSampleInterval",
"Clearing Options",
QT_TRANSLATE_NOOP(
"App::Property",
"Set the minimum sampling resolution. Smaller values quickly increase processing time.",
),
),
(
"App::PropertyFloat",
"StepOver",
"Clearing Options",
QT_TRANSLATE_NOOP(
"App::Property",
"Set the stepover percentage, based on the tool's diameter.",
),
),
(
"App::PropertyBool",
"OptimizeLinearPaths",
"Optimization",
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",
QT_TRANSLATE_NOOP(
"App::Property",
"Enable separate optimization of transitions between, and breaks within, each step over path.",
),
),
(
"App::PropertyDistance",
"GapThreshold",
"Optimization",
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",
QT_TRANSLATE_NOOP(
"App::Property",
"Feedback: three smallest gaps identified in the path geometry.",
),
),
(
"App::PropertyVectorDistance",
"StartPoint",
"Start Point",
QT_TRANSLATE_NOOP(
"App::Property",
"The custom start point for the path of this operation",
),
),
(
"App::PropertyBool",
"UseStartPoint",
"Start Point",
QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"),
),
]
def opPropertyDefaults(self, obj, job):
"""opPropertyDefaults(obj, job) ... returns a dictionary
of default values for the operation's properties."""
defaults = {
"OptimizeLinearPaths": True,
"InternalFeaturesCut": True,
"OptimizeStepOverTransitions": False,
"BoundaryEnforcement": True,
"UseStartPoint": False,
"AvoidLastX_InternalFeatures": True,
"CutPatternReversed": False,
"IgnoreOuterAbove": obj.StartDepth.Value + 0.00001,
"StartPoint": FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value),
"Algorithm": "OCL Dropcutter",
"LayerMode": "Single-pass",
"CutMode": "Conventional",
"CutPattern": "None",
"HandleMultipleFeatures": "Collectively",
"PatternCenterAt": "CenterOfMass",
"GapSizes": "No gaps identified.",
"ClearLastLayer": "Off",
"StepOver": 100.0,
"CutPatternAngle": 0.0,
"DepthOffset": 0.0,
"SampleInterval": 1.0,
"MinSampleInterval": 0.005,
"BoundaryAdjustment": 0.0,
"InternalFeaturesAdjustment": 0.0,
"AvoidLastX_Faces": 0,
"PatternCenterCustom": FreeCAD.Vector(0.0, 0.0, 0.0),
"GapThreshold": 0.005,
"AngularDeflection": 0.25,
"LinearDeflection": 0.0001,
# For debugging
"ShowTempObjects": False,
}
warn = True
if hasattr(job, "GeometryTolerance"):
if job.GeometryTolerance.Value != 0.0:
warn = False
defaults["LinearDeflection"] = job.GeometryTolerance.Value
if warn:
msg = translate("PathWaterline", "The GeometryTolerance for this Job is 0.0.")
msg += translate("PathWaterline", "Initializing LinearDeflection to 0.0001 mm.")
FreeCAD.Console.PrintWarning(msg + "\n")
return defaults
def setEditorProperties(self, obj):
# Used to hide inputs in properties list
expMode = G = 0
show = hide = A = B = C = D = 2
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 == "OCL Adaptive":
D = 0
expMode = 2
elif obj.Algorithm == "Experimental":
A = B = C = 0
expMode = G = D = 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("MinSampleInterval", D)
obj.setEditorMode("LinearDeflection", expMode)
obj.setEditorMode("AngularDeflection", expMode)
def onChanged(self, obj, prop):
if hasattr(self, "propertiesReady"):
if self.propertiesReady:
if prop in ["Algorithm", "CutPattern"]:
self.setEditorProperties(obj)
if prop == "Active" and obj.ViewObject:
obj.ViewObject.signalChangeIcon()
def opOnDocumentRestored(self, obj):
self.propertiesReady = False
job = PathUtils.findParentJob(obj)
self.initOpProperties(obj, warn=True)
self.opApplyPropertyDefaults(obj, job, self.addNewProps)
mode = 2 if Path.Log.getLevel(Path.Log.thisModule()) != 4 else 0
obj.setEditorMode("ShowTempObjects", mode)
# Repopulate enumerations in case of changes
ENUMS = self.propertyEnumerations()
for n in ENUMS:
restore = False
if hasattr(obj, n[0]):
val = obj.getPropertyByName(n[0])
restore = True
setattr(obj, n[0], n[1])
if restore:
setattr(obj, n[0], val)
self.setEditorProperties(obj)
def opApplyPropertyDefaults(self, obj, job, propList):
# Set standard property defaults
PROP_DFLTS = self.opPropertyDefaults(obj, job)
for n in PROP_DFLTS:
if n in propList:
prop = getattr(obj, n)
val = PROP_DFLTS[n]
setVal = False
if hasattr(prop, "Value"):
if isinstance(val, int) or isinstance(val, float):
setVal = True
if setVal:
setattr(prop, "Value", val)
else:
setattr(obj, n, val)
def opSetDefaultValues(self, obj, job):
"""opSetDefaultValues(obj, job) ... initialize defaults"""
job = PathUtils.findParentJob(obj)
self.opApplyPropertyDefaults(obj, job, self.addNewProps)
# 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
Path.Log.debug("job.Stock exists")
else:
Path.Log.debug("job.Stock NOT exist")
else:
Path.Log.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
Path.Log.debug("Default OpFinalDepth: {}".format(obj.OpFinalDepth.Value))
Path.Log.debug("Default 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
Path.Log.error(
translate(
"PathWaterline",
"Sample interval limits are 0.0001 to 25.4 millimeters.",
)
)
if obj.SampleInterval.Value > 25.4:
obj.SampleInterval.Value = 25.4
Path.Log.error(
translate(
"PathWaterline",
"Sample interval limits are 0.0001 to 25.4 millimeters.",
)
)
# Limit min sample interval
if obj.MinSampleInterval.Value < 0.0001:
obj.MinSampleInterval.Value = 0.0001
Path.Log.error(
translate(
"PathWaterline",
"Min Sample interval limits are 0.0001 to 25.4 millimeters.",
)
)
if obj.MinSampleInterval.Value > 25.4:
obj.MinSampleInterval.Value = 25.4
Path.Log.error(
translate(
"PathWaterline",
"Min Sample interval limits are 0.0001 to 25.4 millimeters.",
)
)
# Limit cut pattern angle
if obj.CutPatternAngle < -360.0:
obj.CutPatternAngle = 0.0
Path.Log.error(
translate("PathWaterline", "Cut pattern angle limits are +-360 degrees.")
)
if obj.CutPatternAngle >= 360.0:
obj.CutPatternAngle = 0.0
Path.Log.error(
translate("PathWaterline", "Cut pattern angle limits are +- 360 degrees.")
)
# Limit StepOver to natural number percentage
if obj.StepOver > 100.0:
obj.StepOver = 100.0
if obj.StepOver < 1.0:
obj.StepOver = 1.0
# Limit AvoidLastX_Faces to zero and positive values
if obj.AvoidLastX_Faces < 0:
obj.AvoidLastX_Faces = 0
Path.Log.error(
translate(
"PathWaterline",
"AvoidLastX_Faces: Only zero or positive values permitted.",
)
)
if obj.AvoidLastX_Faces > 100:
obj.AvoidLastX_Faces = 100
Path.Log.error(
translate(
"PathWaterline",
"AvoidLastX_Faces: Avoid last X faces count limited to 100.",
)
)
def opUpdateDepths(self, obj):
if hasattr(obj, "Base") and obj.Base:
base, sublist = obj.Base[0]
fbb = base.Shape.getElement(sublist[0]).BoundBox
zmin = fbb.ZMax
for base, sublist in obj.Base:
for sub in sublist:
try:
fbb = base.Shape.getElement(sub).BoundBox
zmin = min(zmin, fbb.ZMin)
except Part.OCCError as e:
Path.Log.error(e)
obj.OpFinalDepth = zmin
elif self.job:
if hasattr(obj, "BoundBox"):
if obj.BoundBox == "BaseBoundBox":
models = self.job.Model.Group
zmin = models[0].Shape.BoundBox.ZMin
for M in models:
zmin = min(zmin, M.Shape.BoundBox.ZMin)
obj.OpFinalDepth = zmin
if obj.BoundBox == "Stock":
models = self.job.Stock
obj.OpFinalDepth = self.job.Stock.Shape.BoundBox.ZMin
def opExecute(self, obj):
"""opExecute(obj) ... process surface operation"""
Path.Log.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 Path.Log.getLevel(Path.Log.thisModule()) == 4:
deleteTempsFlag = False
else:
self.showDebugObjects = False
# mark beginning of operation and identify parent Job
Path.Log.info("\nBegin Waterline operation...")
startTime = time.time()
# Identify parent Job
JOB = PathUtils.findParentJob(obj)
if JOB is None:
Path.Log.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
# Instantiate additional class operation variables
self.resetOpVariables()
# Setup cutter for OCL and cutout value for operation - based on tool controller properties
oclTool = PathSurfaceSupport.OCL_Tool(ocl, obj)
self.cutter = oclTool.getOclTool()
if not self.cutter:
Path.Log.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]
# 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(oclTool.toolType), {}))
self.commandlist.append(
Path.Command("N (Compensated Tool Path. Diameter: {})".format(oclTool.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,
},
)
)
# Impose property limits
self.opApplyPropertyLimits(obj)
# Create temporary group for temporary objects, removing existing
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)
# 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:
Path.Log.warning(
"{}\nPlease set Job.GeometryTolerance to an acceptable value. Using Path.Preferences.defaultGeometryTolerance().".format(
ee
)
)
useDGT = True
if useDGT:
self.geoTlrnc = Path.Preferences.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:
Path.Log.error("Unable to pre-process obj.Base.")
else:
(FACES, VOIDS) = pPM
self.modelSTLs = PSF.modelSTLs
self.profileShapes = PSF.profileShapes
for m in range(0, len(JOB.Model.Group)):
# Create OCL.stl model objects
if obj.Algorithm == "OCL Dropcutter" or obj.Algorithm == "OCL Adaptive":
PathSurfaceSupport._prepareModelSTLs(self, JOB, obj, m, ocl)
Mdl = JOB.Model.Group[m]
if FACES[m] is False:
Path.Log.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},
)
)
Path.Log.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" or obj.Algorithm == "OCL Adaptive":
PathSurfaceSupport._makeSafeSTL(self, JOB, obj, m, FACES[m], VOIDS[m], ocl)
# 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
msg = translate("PathWaterline", "operation time is")
Path.Log.info("Waterline " + msg + " {} sec.".format(execTime))
return True
# Methods for constructing the cut area and creating path geometry
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."""
Path.Log.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" or obj.Algorithm == "OCL Adaptive":
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" or obj.Algorithm == "OCL Adaptive":
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
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."""
Path.Log.debug("_getExperimentalWaterlinePaths()")
SCANS = list()
# PNTSET is list, by stepover.
if cutPattern in ["Line", "Spiral", "ZigZag"]:
stpOvr = list()
for STEP in PNTSET:
for SEG in STEP:
if SEG == "BRK":
stpOvr.append(SEG)
else:
(A, B) = SEG # format is ((p1, p2), (p3, p4))
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"]:
# 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:
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
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:
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 - OCL Adaptive 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)
smplInt = obj.SampleInterval.Value
minSmplInt = obj.MinSampleInterval.Value
if minSmplInt > smplInt:
minSmplInt = smplInt
# 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
if obj.Algorithm == "OCL Adaptive":
# Get Stock Bounding Box
BS = JOB.Stock
stock_bb = BS.Shape.BoundBox
# Stock Limits
s_xmin = stock_bb.XMin
s_xmax = stock_bb.XMax
s_ymin = stock_bb.YMin
s_ymax = stock_bb.YMax
# Calculate Tool Path Limits based on OCL STL
path_min_x = stl.bb.minpt.x - self.radius
path_min_y = stl.bb.minpt.y - self.radius
path_max_x = stl.bb.maxpt.x + self.radius
path_max_y = stl.bb.maxpt.y + self.radius
# Compare with a tiny tolerance
tol = 0.001
if (
(path_min_x < s_xmin - tol)
or (path_min_y < s_ymin - tol)
or (path_max_x > s_xmax + tol)
or (path_max_y > s_ymax + tol)
):
newPropMsg = translate(
"PathWaterline",
"The toolpath has exceeded the stock bounding box limits. Consider using a Boundary Dressup.",
)
FreeCAD.Console.PrintWarning(newPropMsg + "\n")
# Run the Scan (Processing ALL depths at once)
scanLines = self._waterlineAdaptiveScan(stl, smplInt, minSmplInt, depthparams, depOfst)
# Generate G-Code
layTime = time.time()
for loop in scanLines:
# We pass '0.0' as layDep because Adaptive loops have their own Z embedded
cmds = self._loopToGcode(obj, 0.0, loop)
commands.extend(cmds)
Path.Log.debug("--Adaptive generation took " + str(time.time() - layTime) + " s")
else:
# Setup BoundBox for Dropcutter grid
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 = BS.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
# Determine bounding box length for the OCL scan
bbLength = math.fabs(ymax - ymin)
numScanLines = int(math.ceil(bbLength / smplInt) + 1)
# Run Scan (Grid based)
fd = depthparams[-1]
oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines)
oclScan = [FreeCAD.Vector(P.x, P.y, P.z + depOfst) for P in oclScan]
# Convert point list to grid (scanLines)
lenOS = len(oclScan)
ptPrLn = int(lenOS / numScanLines)
scanLines = []
for L in range(0, numScanLines):
scanLines.append([])
for P in range(0, ptPrLn):
pi = L * ptPrLn + P
scanLines[L].append(oclScan[pi])
# Extract Waterline Layers Iteratively
lenSL = len(scanLines)
pntsPerLine = len(scanLines[0])
msg = "--OCL scan: " + str(lenSL * pntsPerLine) + " points, with "
msg += str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line"
Path.Log.debug(msg)
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
Path.Log.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 _waterlineAdaptiveScan(self, stl, smplInt, minSmplInt, zheights, depOfst):
"""Perform OCL Adaptive scan for waterline purpose."""
msg = translate(
"Waterline", ": Steps below the model's top Face will be the only ones processed."
)
Path.Log.info("Waterline " + msg)
# Setup OCL AdaptiveWaterline
awl = ocl.AdaptiveWaterline()
awl.setSTL(stl)
awl.setCutter(self.cutter)
awl.setSampling(smplInt)
awl.setMinSampling(minSmplInt)
adapt_loops = []
# Iterate through each Z-depth
for zh in zheights:
awl.setZ(zh)
awl.run()
# OCL returns a list of separate loops (list of lists of Points)
# Example: [[PerimeterPoints], [HolePoints]]
temp_loops = awl.getLoops()
if not temp_loops:
# Warn if the step is outside the model bounds
newPropMsg = translate("PathWaterline", "Step Down above model. Skipping height : ")
newPropMsg += "{} mm".format(zh)
FreeCAD.Console.PrintWarning(newPropMsg + "\n")
continue
# Process each loop separately.
# This ensures that islands (holes) remain distinct from perimeters.
for loop in temp_loops:
# Convert OCL Points to FreeCAD Vectors and apply Z offset
fc_loop = [FreeCAD.Vector(P.x, P.y, P.z + depOfst) for P in loop]
adapt_loops.append(fc_loop)
return adapt_loops
def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine):
"""_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline."""
commands = []
cmds = []
loopList = []
self.topoMap = []
if obj.Algorithm == "OCL Adaptive":
loopList = scanLines
else:
# 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 i in range(0, lenSL):
self.topoMap[i].insert(0, 0)
self.topoMap[i].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:
Path.Log.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
Path.Log.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:
Path.Log.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 = []
# Safety check for empty loops
if not loop:
return output
nxt = FreeCAD.Vector(0.0, 0.0, 0.0)
# Create (first and last) point
if obj.Algorithm == "OCL Adaptive":
if obj.CutMode == "Climb":
# Reverse loop for Climb Milling
loop.reverse()
pnt = pnt1 = FreeCAD.Vector(loop[0].x, loop[0].y, loop[0].z)
else:
pnt = FreeCAD.Vector(loop[0].x, loop[0].y, layDep)
# Position cutter to begin loop
if self.layerEndPnt.x == 0 and self.layerEndPnt.y == 0: # First to Clearance Height
output.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}))
else:
output.append(Path.Command("G0", {"Z": obj.SafeHeight.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
if obj.Algorithm == "OCL Adaptive":
nxt.z = loop[i + 1].z
else:
nxt.z = layDep
output.append(Path.Command("G1", {"X": pnt.x, "Y": pnt.y, "F": self.horizFeed}))
# Rotate point data
pnt = nxt
# Connect first and last points for Adaptive
if obj.Algorithm == "OCL Adaptive":
output.append(Path.Command("G1", {"X": pnt1.x, "Y": pnt1.y, "F": self.horizFeed}))
# 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."""
Path.Log.debug("_experimentalWaterlineOp()")
commands = []
base = JOB.Model.Group[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]
Path.Log.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)
self.showDebugObject(trimFace, "TrimFace")
# Cycle through layer depths
CUTAREAS = self._getCutAreas(base.Shape, depthparams, bbFace, trimFace, borderFace)
if not CUTAREAS:
Path.Log.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
self.showDebugObject(area, "CutArea_{}".format(caCnt))
else:
data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString
Path.Log.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)
self.showDebugObject(activeArea, "ActiveArea_{}".format(caCnt))
ofstArea = PathUtils.getOffsetArea(activeArea, ofst, self.wpc)
if not ofstArea:
data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString
Path.Log.debug("No offset area returned for cut area depth at {}.".format(data))
cont = False
if cont:
# Identify solid areas in the offset data
if obj.CutPattern == "Offset" or obj.CutPattern == "None":
ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea.Faces)
if ofstSolidFacesList:
clearArea = Part.makeCompound(ofstSolidFacesList)
self.showDebugObject(clearArea, "ClearArea_{}".format(caCnt))
else:
cont = False
data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString
Path.Log.error("Could not determine solid faces at {}.".format(data))
else:
clearArea = activeArea
if cont:
data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString
Path.Log.debug("... Clearning area at {}.".format(data))
# 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 and obj.ClearLastLayer != "Off":
Path.Log.debug("... Clearning last layer")
(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
)
)
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."""
Path.Log.debug("_getCutAreas()")
CUTAREAS = list()
isFirst = True
lenDP = len(depthparams)
# Cycle through layer depths
for dp in range(0, lenDP):
csHght = depthparams[dp]
# Path.Log.debug('Depth {} is {}'.format(dp + 1, csHght))
# Get slice at depth of shape
csFaces = self._getModelCrossSection(shape, csHght) # returned at Z=0.0
if csFaces:
if len(csFaces) > 0:
useFaces = self._getSolidAreasFromPlanarFaces(csFaces)
else:
useFaces = False
if useFaces:
compAdjFaces = Part.makeCompound(useFaces)
self.showDebugObject(compAdjFaces, "Solids_{}".format(dp + 1))
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:
Path.Log.error("No waterline at depth: {} mm.".format(csHght))
# Efor
if len(CUTAREAS) > 0:
return CUTAREAS
return False
def _wiresToWaterlinePath(self, obj, ofstPlnrShp, csHght):
Path.Log.debug("_wiresToWaterlinePath()")
commands = list()
# Translate path geometry to layer height
ofstPlnrShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - ofstPlnrShp.BoundBox.ZMin))
self.showDebugObject(ofstPlnrShp, "WaterlinePathArea_{}".format(round(csHght, 2)))
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)))
# This ensures the tool is directly above the entry point before plunging,
# preventing diagonal moves through the material.
commands.append(
Path.Command("G0", {"X": startVect.x, "Y": startVect.y, "F": self.horizRapid})
)
(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):
Path.Log.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:
Path.Log.warning("No path geometry generated.")
return commands
pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin))
self.showDebugObject(pathGeom, "PathGeom_{}".format(round(csHght, 2)))
if cutPattern == "Line":
# pntSet = PathSurfaceSupport.pathGeomToLinesPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps)
pntSet = PathSurfaceSupport.pathGeomToLinesPointSet(self, obj, pathGeom)
elif cutPattern == "ZigZag":
# pntSet = PathSurfaceSupport.pathGeomToZigzagPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps)
pntSet = PathSurfaceSupport.pathGeomToZigzagPointSet(self, obj, pathGeom)
elif cutPattern in ["Circular", "CircularZigZag"]:
# pntSet = PathSurfaceSupport.pathGeomToCircularPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps, self.tmpCOM)
pntSet = PathSurfaceSupport.pathGeomToCircularPointSet(self, obj, pathGeom)
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):
Path.Log.debug("_makeOffsetLayerPaths()")
cmds = list()
ofst = 0.0 - self.cutOut
shape = clrAreaShp
cont = True
cnt = 0
while cont:
ofstArea = PathUtils.getOffsetArea(shape, ofst, self.wpc)
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
Path.Log.debug(" -Offset path count: {} at height: {}".format(cnt, round(csHght, 2)))
return cmds
def _clearGeomToPaths(self, JOB, obj, safePDC, stpOVRS, cutPattern):
Path.Log.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 > 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]
# Path.Log.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
Path.Log.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):
Path.Log.debug("_getSolidAreasFromPlanarFaces()")
holds = list()
useFaces = list()
lenCsF = len(csFaces)
Path.Log.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):
Path.Log.debug("_getModelCrossSection()")
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:
Path.Log.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."""
Path.Log.track()
paths = []
pathParams = {}
pathParams["shapes"] = [wire]
pathParams["feedrate"] = self.horizFeed
pathParams["feedrate_v"] = self.vertFeed
pathParams["verbose"] = True
pathParams["retraction"] = obj.SafeHeight.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
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):
Path.Log.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
Path.Log.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.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 showDebugObject(self, objShape, objName):
if self.showDebugObjects:
do = FreeCAD.ActiveDocument.addObject("Part::Feature", "tmp_" + objName)
do.Shape = objShape
do.purgeTouched()
self.tempGroup.addObject(do)
def SetupProperties():
"""SetupProperties() ... Return list of properties required for operation."""
return [tup[1] for tup in ObjectWaterline.opPropertyDefinitions(False)]
def Create(name, obj=None, parentJob=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, parentJob)
return obj