CAM: Waterline OCL Adaptive

Adding OCL Adaptive Algorithm to Waterline Operation
This commit is contained in:
Dimitris75
2025-08-16 19:41:01 +03:00
parent 1a4ff21bdd
commit 1a9df4eb80
4 changed files with 409 additions and 80 deletions

View File

@@ -26,14 +26,14 @@
<item row="0" column="0">
<widget class="QLabel" name="toolController_label">
<property name="text">
<string>Tool controller</string>
<string>Tool Controller</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="toolController">
<property name="toolTip">
<string>The tool and its settings to be used for this operation</string>
<string>The tool and its settings to be used for this operation.</string>
</property>
</widget>
</item>
@@ -43,7 +43,7 @@
<item row="1" column="0">
<widget class="QLabel" name="coolantController_label">
<property name="text">
<string>Coolant mode</string>
<string>Coolant Mode</string>
</property>
</widget>
</item>
@@ -63,7 +63,7 @@
<item row="0" column="1">
<widget class="QComboBox" name="algorithmSelect">
<property name="toolTip">
<string>Select the algorithm to use: 'OCL Dropcutter*', or 'Experimental' (not OCL based).</string>
<string>Select the algorithm to use: OCL Dropcutter*, OCL Adaptive* or Experimental (Not OCL based).</string>
</property>
</widget>
</item>
@@ -76,7 +76,7 @@
</sizepolicy>
</property>
<property name="text">
<string>Bounding box</string>
<string>Bounding Box</string>
</property>
</widget>
</item>
@@ -88,14 +88,14 @@
</font>
</property>
<property name="toolTip">
<string>Select the overall boundary for the operation</string>
<string>Select the overall boundary for the operation.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="layerMode_label">
<property name="text">
<string>Layer mode</string>
<string>Layer Mode</string>
</property>
</widget>
</item>
@@ -107,14 +107,14 @@
</font>
</property>
<property name="toolTip">
<string>Complete the operation in a single pass at depth, or multiple passes to final depth</string>
<string>Complete the operation in a single pass at depth, or multiple passes to final depth.</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="cutPattern_label">
<property name="text">
<string>Cut pattern</string>
<string>Cut Pattern</string>
</property>
</widget>
</item>
@@ -126,7 +126,7 @@
</font>
</property>
<property name="toolTip">
<string>Set the geometric clearing pattern to use for the operation</string>
<string>Set the geometric clearing pattern to use for the operation.</string>
</property>
</widget>
</item>
@@ -139,14 +139,14 @@
</sizepolicy>
</property>
<property name="text">
<string>Boundary adjustment</string>
<string>Boundary Adjustment</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="Gui::InputField" name="boundaryAdjustment">
<property name="toolTip">
<string>Set the Z-axis depth offset from the target surface</string>
<string>Set the Z-axis depth offset from the target surface.</string>
</property>
<property name="unit" stdset="0">
<string notr="true">mm</string>
@@ -198,16 +198,60 @@ A step over of 100% results in no overlap between two different cycles.</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="minSampleInterval_label">
<property name="text">
<string>Min Sample interval</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="Gui::InputField" name="minSampleInterval">
<property name="toolTip">
<string>Set the minimum sampling resolution. Smaller values quickly increase processing time.</string>
</property>
<property name="unit" stdset="0">
<string notr="true">mm</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QCheckBox" name="optimizeEnabled">
<property name="toolTip">
<string>Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-code output.</string>
</property>
<property name="text">
<string>Optimize linear paths</string>
<string>Optimize Linear Paths</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QCheckBox" name="optimizeInternal">
<property name="toolTip">
<string>Detect the interconnection of internal features or holes and raise the tool for transition.</string>
</property>
<property name="text">
<string>Optimize Internal Features</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="gapDetectionThershold_label">
<property name="text">
<string>Gap Detection Thershold</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="Gui::InputField" name="gapDetectionThershold">
<property name="toolTip">
<string>Minimum distance between the perimeter and any internal features. Lower values than Sample Interval will be ignored.</string>
</property>
<property name="unit" stdset="0">
<string notr="true">mm</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -242,7 +286,10 @@ A step over of 100% results in no overlap between two different cycles.</string>
<tabstop>cutPattern</tabstop>
<tabstop>stepOver</tabstop>
<tabstop>sampleInterval</tabstop>
<tabstop>minSampleInterval</tabstop>
<tabstop>optimizeEnabled</tabstop>
<tabstop>optimizeInternal</tabstop>
<tabstop>gapDetectionThershold</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@@ -86,7 +86,13 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
if obj.StepOver != self.form.stepOver.value():
obj.StepOver = self.form.stepOver.value()
PathGuiUtil.updateInputField(obj, "MinSampleInterval", self.form.minSampleInterval)
PathGuiUtil.updateInputField(obj, "SampleInterval", self.form.sampleInterval)
if obj.OptimizeInternalFeatures != self.form.optimizeInternal.isChecked():
obj.OptimizeInternalFeatures = self.form.optimizeInternal.isChecked()
PathGuiUtil.updateInputField(obj, "GapDetectionThershold", self.form.gapDetectionThershold)
if obj.OptimizeLinearPaths != self.form.optimizeEnabled.isChecked():
obj.OptimizeLinearPaths = self.form.optimizeEnabled.isChecked()
@@ -103,9 +109,21 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
FreeCAD.Units.Quantity(obj.BoundaryAdjustment.Value, FreeCAD.Units.Length).UserString
)
self.form.stepOver.setValue(obj.StepOver)
self.form.minSampleInterval.setText(
FreeCAD.Units.Quantity(obj.MinSampleInterval.Value, FreeCAD.Units.Length).UserString
)
self.form.sampleInterval.setText(
FreeCAD.Units.Quantity(obj.SampleInterval.Value, FreeCAD.Units.Length).UserString
)
)
if obj.OptimizeInternalFeatures:
self.form.optimizeInternal.setCheckState(QtCore.Qt.Checked)
else:
self.form.optimizeInternal.setCheckState(QtCore.Qt.Unchecked)
self.form.gapDetectionThershold.setText(
FreeCAD.Units.Quantity(obj.GapDetectionThershold.Value, FreeCAD.Units.Length).UserString
)
if obj.OptimizeLinearPaths:
self.form.optimizeEnabled.setCheckState(QtCore.Qt.Checked)
@@ -125,7 +143,14 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
signals.append(self.form.cutPattern.currentIndexChanged)
signals.append(self.form.boundaryAdjustment.editingFinished)
signals.append(self.form.stepOver.editingFinished)
signals.append(self.form.minSampleInterval.editingFinished)
signals.append(self.form.sampleInterval.editingFinished)
signals.append(self.form.gapDetectionThershold.editingFinished)
if hasattr(self.form.optimizeInternal, "checkStateChanged"): # Qt version >= 6.7.0
signals.append(self.form.optimizeInternal.checkStateChanged)
else: # Qt version < 6.7.0
signals.append(self.form.optimizeInternal.stateChanged)
if hasattr(self.form.optimizeEnabled, "checkStateChanged"): # Qt version >= 6.7.0
signals.append(self.form.optimizeEnabled.checkStateChanged)
else: # Qt version < 6.7.0
@@ -139,15 +164,40 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
self.form.optimizeEnabled.hide() # Has no independent QLabel object
if Algorithm == "OCL Dropcutter":
self.form.boundBoxSelect.show()
self.form.boundBoxSelect_label.show()
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.minSampleInterval.hide()
self.form.minSampleInterval_label.hide()
self.form.sampleInterval.show()
self.form.sampleInterval_label.show()
self.form.optimizeInternal.hide()
self.form.gapDetectionThershold.hide()
self.form.gapDetectionThershold_label.hide()
elif Algorithm == "OCL Adaptive":
self.form.boundBoxSelect.hide()
self.form.boundBoxSelect_label.hide()
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.minSampleInterval.show()
self.form.minSampleInterval_label.show()
self.form.sampleInterval.show()
self.form.sampleInterval_label.show()
self.form.optimizeInternal.show()
self.form.gapDetectionThershold.show()
self.form.gapDetectionThershold_label.show()
elif Algorithm == "Experimental":
self.form.boundBoxSelect.show()
self.form.boundBoxSelect_label.show()
self.form.cutPattern.show()
self.form.boundaryAdjustment.show()
self.form.cutPattern_label.show()
@@ -158,8 +208,13 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
else:
self.form.stepOver.show()
self.form.stepOver_label.show()
self.form.minSampleInterval.hide()
self.form.minSampleInterval_label.hide()
self.form.sampleInterval.hide()
self.form.sampleInterval_label.hide()
self.form.optimizeInternal.hide()
self.form.gapDetectionThershold.hide()
self.form.gapDetectionThershold_label.hide()
def registerSignalHandlers(self, obj):
self.form.algorithmSelect.currentIndexChanged.connect(self.updateVisibility)

View File

@@ -36,7 +36,7 @@ import math
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
# MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart')
MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') # tessellate bug Workaround
Part = LazyLoader("Part", globals(), "Part")
@@ -1260,7 +1260,11 @@ def _makeSTL(model, obj, ocl, model_type=None):
shape = model.Shape
else:
shape = model
vertices, facet_indices = shape.tessellate(obj.LinearDeflection.Value)
#vertices, facet_indices = shape.tessellate(obj.LinearDeflection.Value) # tessellate workaround
# Workaround for tessellate bug
mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=0.001, AngularDeflection=0.25)
vertices = [point.Vector for point in mesh.Points]
facet_indices = [facet.PointIndices for facet in mesh.Facets]
facets = ((vertices[f[0]], vertices[f[1]], vertices[f[2]]) for f in facet_indices)
stl = ocl.STLSurf()
for tri in facets:

View File

@@ -23,6 +23,7 @@
import FreeCAD
__title__ = "CAM Waterline Operation"
__author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)"
__url__ = "https://www.freecad.org"
@@ -48,6 +49,7 @@ 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
@@ -94,11 +96,12 @@ class ObjectWaterline(PathOp.ObjectOp):
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"),
(translate("path_waterline", "BaseBoundBox"), "BaseBoundBox"),
],
"PatternCenterAt": [
(translate("path_waterline", "CenterOfMass"), "CenterOfMass"),
@@ -288,7 +291,7 @@ class ObjectWaterline(PathOp.ObjectOp):
"Clearing Options",
QT_TRANSLATE_NOOP(
"App::Property",
"Select the algorithm to use: OCL Dropcutter*, or Experimental (Not OCL based).",
"Select the algorithm to use: OCL Dropcutter*, OCL Adaptive or Experimental (Not OCL based).",
),
),
(
@@ -391,6 +394,33 @@ class ObjectWaterline(PathOp.ObjectOp):
"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::PropertyBool",
"OptimizeInternalFeatures",
"Optimization",
QT_TRANSLATE_NOOP(
"App::Property",
"Detect the interconnection of internal features or holes and raise the tool for transition.",
),
),
(
"App::PropertyDistance",
"GapDetectionThershold",
"Optimization",
QT_TRANSLATE_NOOP(
"App::Property",
"Minimum distance between the perimeter and any internal features. Lower values than Sample Interval will be ignored.",
),
),
(
"App::PropertyFloat",
"StepOver",
@@ -478,6 +508,9 @@ class ObjectWaterline(PathOp.ObjectOp):
"CutPatternAngle": 0.0,
"DepthOffset": 0.0,
"SampleInterval": 1.0,
"MinSampleInterval": 0.005,
"OptimizeInternalFeatures": False,
"GapDetectionThershold": 3.50,
"BoundaryAdjustment": 0.0,
"InternalFeaturesAdjustment": 0.0,
"AvoidLastX_Faces": 0,
@@ -504,25 +537,28 @@ class ObjectWaterline(PathOp.ObjectOp):
def setEditorProperties(self, obj):
# Used to hide inputs in properties list
expMode = G = 0
show = hide = A = B = C = 2
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)
obj.setEditorMode("BoundaryAdjustment", 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 = show = hide = 2
expMode = G = D = H = show = hide = 2
cutPattern = obj.CutPattern
if obj.ClearLastLayer != "Off":
@@ -548,6 +584,11 @@ class ObjectWaterline(PathOp.ObjectOp):
obj.setEditorMode("IgnoreOuterAbove", B)
obj.setEditorMode("CutPattern", C)
obj.setEditorMode("SampleInterval", G)
obj.setEditorMode("MinSampleInterval", D)
obj.setEditorMode("OptimizeInternalFeatures", D)
obj.setEditorMode("GapDetectionThershold", D)
obj.setEditorMode("LinearDeflection", expMode)
obj.setEditorMode("AngularDeflection", expMode)
@@ -644,6 +685,42 @@ class ObjectWaterline(PathOp.ObjectOp):
"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 Gap Detection Threshold Adaptive
if obj.GapDetectionThershold.Value < 1.00:
obj.GapDetectionThershold.Value = 1.00
Path.Log.error(
translate(
"PathWaterline",
"Gap Detection Thershold limits are 1.00 to 999.99 millimeters.",
)
)
if obj.GapDetectionThershold.Value > 999.99:
obj.GapDetectionThershold.Value = 999.99
Path.Log.error(
translate(
"PathWaterline",
"Gap Detection Thershold limits are 1.00 to 999.99 millimeters.",
)
)
# Limit cut pattern angle
if obj.CutPatternAngle < -360.0:
@@ -908,7 +985,7 @@ class ObjectWaterline(PathOp.ObjectOp):
for m in range(0, len(JOB.Model.Group)):
# Create OCL.stl model objects
if obj.Algorithm == "OCL Dropcutter":
if obj.Algorithm == "OCL Dropcutter" or obj.Algorithm == "OCL Adaptive":
PathSurfaceSupport._prepareModelSTLs(self, JOB, obj, m, ocl)
Mdl = JOB.Model.Group[m]
@@ -926,7 +1003,7 @@ class ObjectWaterline(PathOp.ObjectOp):
)
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":
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]))
@@ -1023,7 +1100,7 @@ class ObjectWaterline(PathOp.ObjectOp):
COMP = ADD
final.append(Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid}))
if obj.Algorithm == "OCL Dropcutter":
if obj.Algorithm == "OCL Dropcutter" or obj.Algorithm == "OCL Adaptive":
final.extend(
self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)
) # independent method set for Waterline
@@ -1049,7 +1126,7 @@ class ObjectWaterline(PathOp.ObjectOp):
COMP = ADD
final.append(Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid}))
if obj.Algorithm == "OCL Dropcutter":
if obj.Algorithm == "OCL Dropcutter" or obj.Algorithm == "OCL Adaptive":
final.extend(
self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)
) # independent method set for Waterline
@@ -1176,11 +1253,11 @@ class ObjectWaterline(PathOp.ObjectOp):
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.setZ(finalDep) # set minimumZ (final / target depth value)
pdc.setSampling(SampleInterval) # set sampling size
return pdc
# OCL Dropcutter waterline functions
# 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 = []
@@ -1197,30 +1274,40 @@ class ObjectWaterline(PathOp.ObjectOp):
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
if obj.Algorithm == "OCL Adaptive":
# Get Stock boundbox for OCL Adaptive
BS = JOB.Stock
bb = BS.Shape.BoundBox
xmin = abs(bb.XMin)
xmax = abs(bb.XMax)
ymin = abs(bb.YMin)
ymax = abs(bb.YMax)
else:
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
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
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
minSmplInt = obj.MinSampleInterval.Value
if minSmplInt > smplInt:
minSmplInt = smplInt
# Determine bounding box length for the OCL scan
bbLength = math.fabs(ymax - ymin)
@@ -1235,20 +1322,42 @@ class ObjectWaterline(PathOp.ObjectOp):
# 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)
zheights = []
scanLines = []
if obj.Algorithm == "OCL Adaptive":
# Check Stock's bounding box and Tool Path limits
MinX = round(abs(stl.bb.minpt.x) + self.toolDiam, 6)
MinY = round(abs(stl.bb.minpt.y) + self.toolDiam, 6)
MaxX = round(abs(stl.bb.maxpt.x) + self.toolDiam, 6)
MaxY = round(abs(stl.bb.maxpt.y) + self.toolDiam, 6)
if MinX < xmin or MinY < ymin or MaxX > xmax or MaxY > ymax:
newPropMsg = translate("PathWaterline", "The toolpath has exceeded the stock bounding box limits. Consider using a Boundary Dressup.")
FreeCAD.Console.PrintWarning(newPropMsg + "\n")
# Scan the piece
scanLines = self._waterlineAdaptiveScan(stl, smplInt, minSmplInt, depthparams, depOfst)
# Optimize loop. Separate the connected Path of the perimeter and internal features.
if obj.OptimizeInternalFeatures:
GapDetec = float(obj.GapDetectionThershold)
if smplInt >= GapDetec:
GapDetec = smplInt + 1 # We need smaller smplInt than GapDetec to identify Gaps
optimize = self._optimizeAdaptive(scanLines, GapDetec)
scanLines = optimize
else: # Drop Cutter
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])
# 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])
msg = "--OCL scan: " + str(lenSL * pntsPerLine) + " points, with "
@@ -1263,7 +1372,9 @@ class ObjectWaterline(PathOp.ObjectOp):
for layDep in depthparams:
cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine)
commands.extend(cmds)
lyr += 1
if obj.Algorithm == "OCL Adaptive":
break # OCL_Adaptive processes all depths simultaneously (break loop)
lyr += 1
Path.Log.debug("--All layer scans combined took " + str(time.time() - layTime) + " s")
return commands
@@ -1289,25 +1400,121 @@ class ObjectWaterline(PathOp.ObjectOp):
# return the list of points
return pdc.getCLPoints()
def _waterlineAdaptiveScan(self, stl, smplInt, minSmplInt, zheights, depOfst):
"""Perform OCL Adaptive scan for waterline purpose."""
aloops = []
msg = translate("Waterline", ": Steps below the model's top Face will be the only ones processed.")
Path.Log.info("Waterline " + msg)
awl = ocl.AdaptiveWaterline()
awl.setSTL(stl)
awl.setCutter(self.cutter)
awl.setSampling(smplInt)
awl.setMinSampling(minSmplInt)
# Create Adaptive loops
adapt_loops = []
acnt = 0
skippedZ = []
for zh in zheights:
#zh = round(zh, 3)
temp_loops = []
finalZ_loops = []
skipZ = False
awl.setZ(zh)
awl.run()
temp_loops = awl.getLoops()
if not temp_loops:
# Skip if height is above model
newPropMsg = translate("PathWaterline", "Step Down above model. Skipping height : ")
newPropMsg += '{} mm'.format(zh)
FreeCAD.Console.PrintWarning(newPropMsg + "\n")
skipZ = True
acnt -= 1
else:
for tmp in temp_loops:
finalZ_loops += tmp
if not skipZ:
adapt_loops.append(acnt)
adapt_loops[acnt] = [FreeCAD.Vector(P.x, P.y, P.z + depOfst) for P in finalZ_loops]
acnt += 1
# return the list of loops
return adapt_loops
def _optimizeAdaptive(self, adapt_loops, GapDetec):
"""Attempt to repair holes and internal features on model"""
# Search for the gaps that caused by Internal features.
new_adapt = []
for adapt in adapt_loops:
fz = adapt
firstX = fz[0].x
firstY = fz[0].y
secLastX = fz[-1].x
secLastY = fz[-1].y
# First and last points in loop should not be in greater distance than Gap Detection Threshold.
if not (
secLastX - GapDetec) <= firstX <= (secLastX + GapDetec) or not (
secLastY - GapDetec) <= firstY <= (secLastY + GapDetec
):
# List with internal features found. Points in greater distance than GapDetec.
fz.reverse()
start_cut = 0
# First point is known. Search for next points to break loop.
for r in range(len(fz)):
# This is the last Step of loop. Close what has been left.
if r == (len(fz)-1):
r_fz = fz[(start_cut+2):len(fz)]
r_fz.reverse()
if len(r_fz) != 0: # check if anything left to append after cut
new_adapt.append(r_fz)
break
if not (
fz[r].x - GapDetec) <= fz[r+1].x <= (fz[r].x + GapDetec) or not (
fz[r].y - GapDetec) <= fz[r+1].y <= (fz[r].y + GapDetec
):
# Next point found.
r_fz = fz[(start_cut+2):r]
r_fz.reverse()
if len(r_fz) != 0: # check if anything left to append after cut
new_adapt.append(r_fz)
start_cut = r
# List without Gap, add as is.
else:
new_adapt.append(adapt)
adapt_loops = new_adapt
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 = []
# 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)
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):
@@ -1555,6 +1762,7 @@ class ObjectWaterline(PathOp.ObjectOp):
+ str(loopNum)
+ " loops."
)
return loopList
def _trackLoop(self, oclScan, lC, pC, L, P, loopNum):
@@ -1634,14 +1842,22 @@ class ObjectWaterline(PathOp.ObjectOp):
# 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)
# 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
output.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}))
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}))
@@ -1652,13 +1868,19 @@ class ObjectWaterline(PathOp.ObjectOp):
if i < lastIdx:
nxt.x = loop[i + 1].x
nxt.y = loop[i + 1].y
nxt.z = layDep
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
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
@@ -1750,7 +1972,8 @@ class ObjectWaterline(PathOp.ObjectOp):
if cont:
# Identify solid areas in the offset data
if obj.CutPattern == "Offset" or obj.CutPattern == "None":
ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea)
if ofstArea:
ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea)
if ofstSolidFacesList:
clearArea = Part.makeCompound(ofstSolidFacesList)
self.showDebugObject(clearArea, "ClearArea_{}".format(caCnt))