From 1a9df4eb800c4212dc82bf9fd66828d0414a25e5 Mon Sep 17 00:00:00 2001 From: Dimitris75 Date: Sat, 16 Aug 2025 19:41:01 +0300 Subject: [PATCH 01/15] CAM: Waterline OCL Adaptive Adding OCL Adaptive Algorithm to Waterline Operation --- .../Resources/panels/PageOpWaterlineEdit.ui | 73 +++- src/Mod/CAM/Path/Op/Gui/Waterline.py | 57 ++- src/Mod/CAM/Path/Op/SurfaceSupport.py | 8 +- src/Mod/CAM/Path/Op/Waterline.py | 351 ++++++++++++++---- 4 files changed, 409 insertions(+), 80 deletions(-) diff --git a/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui b/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui index 1e7b8c6cf4..9b565219d3 100644 --- a/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui +++ b/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui @@ -26,14 +26,14 @@ - Tool controller + Tool Controller - The tool and its settings to be used for this operation + The tool and its settings to be used for this operation. @@ -43,7 +43,7 @@ - Coolant mode + Coolant Mode @@ -63,7 +63,7 @@ - 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). @@ -76,7 +76,7 @@ - Bounding box + Bounding Box @@ -88,14 +88,14 @@ - Select the overall boundary for the operation + Select the overall boundary for the operation. - Layer mode + Layer Mode @@ -107,14 +107,14 @@ - Complete the operation in a single pass at depth, or multiple passes to final depth + Complete the operation in a single pass at depth, or multiple passes to final depth. - Cut pattern + Cut Pattern @@ -126,7 +126,7 @@ - Set the geometric clearing pattern to use for the operation + Set the geometric clearing pattern to use for the operation. @@ -139,14 +139,14 @@ - Boundary adjustment + Boundary Adjustment - Set the Z-axis depth offset from the target surface + Set the Z-axis depth offset from the target surface. mm @@ -198,16 +198,60 @@ A step over of 100% results in no overlap between two different cycles. + + + + Min Sample interval + + + + + + Set the minimum sampling resolution. Smaller values quickly increase processing time. + + + mm + + + + Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-code output. - Optimize linear paths + Optimize Linear Paths + + + + Detect the interconnection of internal features or holes and raise the tool for transition. + + + Optimize Internal Features + + + + + + + Gap Detection Thershold + + + + + + + Minimum distance between the perimeter and any internal features. Lower values than Sample Interval will be ignored. + + + mm + + + @@ -242,7 +286,10 @@ A step over of 100% results in no overlap between two different cycles. cutPattern stepOver sampleInterval + minSampleInterval optimizeEnabled + optimizeInternal + gapDetectionThershold diff --git a/src/Mod/CAM/Path/Op/Gui/Waterline.py b/src/Mod/CAM/Path/Op/Gui/Waterline.py index dba1cd1533..641187ae50 100644 --- a/src/Mod/CAM/Path/Op/Gui/Waterline.py +++ b/src/Mod/CAM/Path/Op/Gui/Waterline.py @@ -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) diff --git a/src/Mod/CAM/Path/Op/SurfaceSupport.py b/src/Mod/CAM/Path/Op/SurfaceSupport.py index 55bce6c161..64e3b64427 100644 --- a/src/Mod/CAM/Path/Op/SurfaceSupport.py +++ b/src/Mod/CAM/Path/Op/SurfaceSupport.py @@ -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: diff --git a/src/Mod/CAM/Path/Op/Waterline.py b/src/Mod/CAM/Path/Op/Waterline.py index 5714565d85..f9c11e1706 100644 --- a/src/Mod/CAM/Path/Op/Waterline.py +++ b/src/Mod/CAM/Path/Op/Waterline.py @@ -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)) From 1e9295fe271e180de7f30ee420fe4fdc881dceca Mon Sep 17 00:00:00 2001 From: Dimitris75 Date: Sat, 16 Aug 2025 21:25:06 +0300 Subject: [PATCH 02/15] round BoundBox round xmin, xmax, ymin, ymax --- src/Mod/CAM/Path/Op/Waterline.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Mod/CAM/Path/Op/Waterline.py b/src/Mod/CAM/Path/Op/Waterline.py index f9c11e1706..ed1726890e 100644 --- a/src/Mod/CAM/Path/Op/Waterline.py +++ b/src/Mod/CAM/Path/Op/Waterline.py @@ -1279,10 +1279,10 @@ class ObjectWaterline(PathOp.ObjectOp): # 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) + xmin = round(abs(bb.XMin), 6) + xmax = round(abs(bb.XMax), 6) + ymin = round(abs(bb.YMin), 6) + ymax = round(abs(bb.YMax), 6) else: if subShp is None: From ce13cb01740df174b34568fe1b534c8987f91412 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 16 Aug 2025 19:03:12 +0000 Subject: [PATCH 03/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../Resources/panels/PageOpWaterlineEdit.ui | 10 +- src/Mod/CAM/Path/Op/Gui/Waterline.py | 38 ++-- src/Mod/CAM/Path/Op/SurfaceSupport.py | 4 +- src/Mod/CAM/Path/Op/Waterline.py | 179 +++++++++--------- 4 files changed, 117 insertions(+), 114 deletions(-) diff --git a/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui b/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui index 9b565219d3..204283833a 100644 --- a/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui +++ b/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui @@ -214,7 +214,7 @@ A step over of 100% results in no overlap between two different cycles. mm - + @@ -241,7 +241,7 @@ A step over of 100% results in no overlap between two different cycles. Gap Detection Thershold - + @@ -251,7 +251,7 @@ A step over of 100% results in no overlap between two different cycles. mm - + @@ -286,10 +286,10 @@ A step over of 100% results in no overlap between two different cycles. cutPattern stepOver sampleInterval - minSampleInterval + minSampleInterval optimizeEnabled optimizeInternal - gapDetectionThershold + gapDetectionThershold diff --git a/src/Mod/CAM/Path/Op/Gui/Waterline.py b/src/Mod/CAM/Path/Op/Gui/Waterline.py index 641187ae50..817786ec3a 100644 --- a/src/Mod/CAM/Path/Op/Gui/Waterline.py +++ b/src/Mod/CAM/Path/Op/Gui/Waterline.py @@ -88,11 +88,11 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): 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) + + PathGuiUtil.updateInputField(obj, "GapDetectionThershold", self.form.gapDetectionThershold) if obj.OptimizeLinearPaths != self.form.optimizeEnabled.isChecked(): obj.OptimizeLinearPaths = self.form.optimizeEnabled.isChecked() @@ -111,19 +111,19 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): 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) @@ -143,13 +143,13 @@ 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.minSampleInterval.editingFinished) signals.append(self.form.sampleInterval.editingFinished) - signals.append(self.form.gapDetectionThershold.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) + signals.append(self.form.optimizeInternal.stateChanged) if hasattr(self.form.optimizeEnabled, "checkStateChanged"): # Qt version >= 6.7.0 signals.append(self.form.optimizeEnabled.checkStateChanged) @@ -173,15 +173,15 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.form.stepOver.hide() self.form.stepOver_label.hide() self.form.minSampleInterval.hide() - self.form.minSampleInterval_label.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() + self.form.gapDetectionThershold_label.hide() elif Algorithm == "OCL Adaptive": self.form.boundBoxSelect.hide() - self.form.boundBoxSelect_label.hide() + self.form.boundBoxSelect_label.hide() self.form.cutPattern.hide() self.form.cutPattern_label.hide() self.form.boundaryAdjustment.hide() @@ -189,15 +189,15 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.form.stepOver.hide() self.form.stepOver_label.hide() self.form.minSampleInterval.show() - self.form.minSampleInterval_label.show() + self.form.minSampleInterval_label.show() self.form.sampleInterval.show() self.form.sampleInterval_label.show() - self.form.optimizeInternal.show() + self.form.optimizeInternal.show() self.form.gapDetectionThershold.show() - self.form.gapDetectionThershold_label.show() + self.form.gapDetectionThershold_label.show() elif Algorithm == "Experimental": self.form.boundBoxSelect.show() - self.form.boundBoxSelect_label.show() + self.form.boundBoxSelect_label.show() self.form.cutPattern.show() self.form.boundaryAdjustment.show() self.form.cutPattern_label.show() @@ -209,12 +209,12 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.form.stepOver.show() self.form.stepOver_label.show() self.form.minSampleInterval.hide() - self.form.minSampleInterval_label.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() + self.form.gapDetectionThershold_label.hide() def registerSignalHandlers(self, obj): self.form.algorithmSelect.currentIndexChanged.connect(self.updateVisibility) diff --git a/src/Mod/CAM/Path/Op/SurfaceSupport.py b/src/Mod/CAM/Path/Op/SurfaceSupport.py index 64e3b64427..895d01b3ed 100644 --- a/src/Mod/CAM/Path/Op/SurfaceSupport.py +++ b/src/Mod/CAM/Path/Op/SurfaceSupport.py @@ -36,7 +36,7 @@ import math # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader -MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') # tessellate bug Workaround +MeshPart = LazyLoader("MeshPart", globals(), "MeshPart") # tessellate bug Workaround Part = LazyLoader("Part", globals(), "Part") @@ -1260,7 +1260,7 @@ def _makeSTL(model, obj, ocl, model_type=None): shape = model.Shape else: shape = model - #vertices, facet_indices = shape.tessellate(obj.LinearDeflection.Value) # tessellate workaround + # 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] diff --git a/src/Mod/CAM/Path/Op/Waterline.py b/src/Mod/CAM/Path/Op/Waterline.py index ed1726890e..825afd571d 100644 --- a/src/Mod/CAM/Path/Op/Waterline.py +++ b/src/Mod/CAM/Path/Op/Waterline.py @@ -101,7 +101,7 @@ class ObjectWaterline(PathOp.ObjectOp): ], "BoundBox": [ (translate("path_waterline", "Stock"), "Stock"), - (translate("path_waterline", "BaseBoundBox"), "BaseBoundBox"), + (translate("path_waterline", "BaseBoundBox"), "BaseBoundBox"), ], "PatternCenterAt": [ (translate("path_waterline", "CenterOfMass"), "CenterOfMass"), @@ -420,7 +420,7 @@ class ObjectWaterline(PathOp.ObjectOp): "App::Property", "Minimum distance between the perimeter and any internal features. Lower values than Sample Interval will be ignored.", ), - ), + ), ( "App::PropertyFloat", "StepOver", @@ -549,13 +549,13 @@ class ObjectWaterline(PathOp.ObjectOp): obj.setEditorMode("OptimizeStepOverTransitions", hide) obj.setEditorMode("GapThreshold", hide) obj.setEditorMode("GapSizes", hide) - obj.setEditorMode("BoundaryAdjustment", hide) + obj.setEditorMode("BoundaryAdjustment", hide) if obj.Algorithm == "OCL Dropcutter": pass elif obj.Algorithm == "OCL Adaptive": D = 0 - expMode = 2 + expMode = 2 elif obj.Algorithm == "Experimental": A = B = C = 0 expMode = G = D = H = show = hide = 2 @@ -584,7 +584,7 @@ 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) @@ -685,7 +685,7 @@ 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 @@ -703,7 +703,7 @@ class ObjectWaterline(PathOp.ObjectOp): "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 @@ -720,7 +720,7 @@ class ObjectWaterline(PathOp.ObjectOp): "PathWaterline", "Gap Detection Thershold limits are 1.00 to 999.99 millimeters.", ) - ) + ) # Limit cut pattern angle if obj.CutPatternAngle < -360.0: @@ -1253,7 +1253,7 @@ 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 @@ -1274,17 +1274,17 @@ 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": + + if obj.Algorithm == "OCL Adaptive": # Get Stock boundbox for OCL Adaptive BS = JOB.Stock - bb = BS.Shape.BoundBox + bb = BS.Shape.BoundBox xmin = round(abs(bb.XMin), 6) xmax = round(abs(bb.XMax), 6) ymin = round(abs(bb.YMin), 6) - ymax = round(abs(bb.YMax), 6) - - else: + ymax = round(abs(bb.YMax), 6) + + else: if subShp is None: # Get correct boundbox if obj.BoundBox == "Stock": @@ -1302,10 +1302,10 @@ class ObjectWaterline(PathOp.ObjectOp): xmin = subShp.BoundBox.XMin xmax = subShp.BoundBox.XMax ymin = subShp.BoundBox.YMin - ymax = subShp.BoundBox.YMax + ymax = subShp.BoundBox.YMax smplInt = obj.SampleInterval.Value - minSmplInt = obj.MinSampleInterval.Value + minSmplInt = obj.MinSampleInterval.Value if minSmplInt > smplInt: minSmplInt = smplInt @@ -1323,7 +1323,7 @@ class ObjectWaterline(PathOp.ObjectOp): # Scan the piece to depth at smplInt oclScan = [] zheights = [] - scanLines = [] + 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) @@ -1331,24 +1331,27 @@ class ObjectWaterline(PathOp.ObjectOp): 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.") + 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) + # 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: + if obj.OptimizeInternalFeatures: GapDetec = float(obj.GapDetectionThershold) if smplInt >= GapDetec: - GapDetec = smplInt + 1 # We need smaller smplInt than GapDetec to identify Gaps + GapDetec = smplInt + 1 # We need smaller smplInt than GapDetec to identify Gaps optimize = self._optimizeAdaptive(scanLines, GapDetec) - scanLines = optimize - else: # Drop Cutter + 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) + + lenOS = len(oclScan) ptPrLn = int(lenOS / numScanLines) # Convert oclScan list of points to multi-dimensional list @@ -1373,8 +1376,8 @@ class ObjectWaterline(PathOp.ObjectOp): cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) commands.extend(cmds) if obj.Algorithm == "OCL Adaptive": - break # OCL_Adaptive processes all depths simultaneously (break loop) - lyr += 1 + 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 @@ -1400,98 +1403,98 @@ 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) - + 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) - + awl.setMinSampling(minSmplInt) + # Create Adaptive loops adapt_loops = [] acnt = 0 skippedZ = [] for zh in zheights: - #zh = round(zh, 3) + # zh = round(zh, 3) temp_loops = [] finalZ_loops = [] - skipZ = False - awl.setZ(zh) + 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) + 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) + 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 - + 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""" - + """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: + 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. + 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)] + 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 + 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] + 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) + 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 = [] @@ -1500,16 +1503,16 @@ class ObjectWaterline(PathOp.ObjectOp): self.topoMap = [] if obj.Algorithm == "OCL Adaptive": loopList = scanLines - else: + else: # Create topo map from scanLines (highs and lows) - self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine) + 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) + 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) @@ -1762,7 +1765,7 @@ class ObjectWaterline(PathOp.ObjectOp): + str(loopNum) + " loops." ) - + return loopList def _trackLoop(self, oclScan, lC, pC, L, P, loopNum): @@ -1847,16 +1850,16 @@ class ObjectWaterline(PathOp.ObjectOp): # Create (first and last) point if obj.Algorithm == "OCL Adaptive": if obj.CutMode == "Climb": - # Reverse loop for Climb Milling - loop.reverse() + # 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 + 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: + 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})) @@ -1875,12 +1878,12 @@ class ObjectWaterline(PathOp.ObjectOp): 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": + 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 @@ -1972,7 +1975,7 @@ class ObjectWaterline(PathOp.ObjectOp): if cont: # Identify solid areas in the offset data if obj.CutPattern == "Offset" or obj.CutPattern == "None": - if ofstArea: + if ofstArea: ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea) if ofstSolidFacesList: clearArea = Part.makeCompound(ofstSolidFacesList) From 99a5c03ba770502dd6ada8af18199d908f31de8d Mon Sep 17 00:00:00 2001 From: Dimitris75 Date: Sun, 17 Aug 2025 04:39:56 +0300 Subject: [PATCH 04/15] Correct problems found by Github bot Delete unused lists --- src/Mod/CAM/Path/Op/Waterline.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Mod/CAM/Path/Op/Waterline.py b/src/Mod/CAM/Path/Op/Waterline.py index ed1726890e..9fe0437453 100644 --- a/src/Mod/CAM/Path/Op/Waterline.py +++ b/src/Mod/CAM/Path/Op/Waterline.py @@ -558,7 +558,7 @@ class ObjectWaterline(PathOp.ObjectOp): expMode = 2 elif obj.Algorithm == "Experimental": A = B = C = 0 - expMode = G = D = H = show = hide = 2 + expMode = G = D = show = hide = 2 cutPattern = obj.CutPattern if obj.ClearLastLayer != "Off": @@ -1322,7 +1322,6 @@ class ObjectWaterline(PathOp.ObjectOp): # Scan the piece to depth at smplInt oclScan = [] - zheights = [] scanLines = [] if obj.Algorithm == "OCL Adaptive": # Check Stock's bounding box and Tool Path limits @@ -1402,8 +1401,7 @@ class ObjectWaterline(PathOp.ObjectOp): return pdc.getCLPoints() def _waterlineAdaptiveScan(self, stl, smplInt, minSmplInt, zheights, depOfst): - """Perform OCL Adaptive scan for waterline purpose.""" - aloops = [] + """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) @@ -1417,7 +1415,6 @@ class ObjectWaterline(PathOp.ObjectOp): # Create Adaptive loops adapt_loops = [] acnt = 0 - skippedZ = [] for zh in zheights: #zh = round(zh, 3) temp_loops = [] From c458e54e2c26eb680be305baf5236b1dc2e6cbe4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 17 Aug 2025 01:57:24 +0000 Subject: [PATCH 05/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/Mod/CAM/Path/Op/Waterline.py | 179 ++++++++++++++++--------------- 1 file changed, 91 insertions(+), 88 deletions(-) diff --git a/src/Mod/CAM/Path/Op/Waterline.py b/src/Mod/CAM/Path/Op/Waterline.py index 9fe0437453..ce568bcf71 100644 --- a/src/Mod/CAM/Path/Op/Waterline.py +++ b/src/Mod/CAM/Path/Op/Waterline.py @@ -101,7 +101,7 @@ class ObjectWaterline(PathOp.ObjectOp): ], "BoundBox": [ (translate("path_waterline", "Stock"), "Stock"), - (translate("path_waterline", "BaseBoundBox"), "BaseBoundBox"), + (translate("path_waterline", "BaseBoundBox"), "BaseBoundBox"), ], "PatternCenterAt": [ (translate("path_waterline", "CenterOfMass"), "CenterOfMass"), @@ -420,7 +420,7 @@ class ObjectWaterline(PathOp.ObjectOp): "App::Property", "Minimum distance between the perimeter and any internal features. Lower values than Sample Interval will be ignored.", ), - ), + ), ( "App::PropertyFloat", "StepOver", @@ -549,13 +549,13 @@ class ObjectWaterline(PathOp.ObjectOp): obj.setEditorMode("OptimizeStepOverTransitions", hide) obj.setEditorMode("GapThreshold", hide) obj.setEditorMode("GapSizes", hide) - obj.setEditorMode("BoundaryAdjustment", hide) + obj.setEditorMode("BoundaryAdjustment", hide) if obj.Algorithm == "OCL Dropcutter": pass elif obj.Algorithm == "OCL Adaptive": D = 0 - expMode = 2 + expMode = 2 elif obj.Algorithm == "Experimental": A = B = C = 0 expMode = G = D = show = hide = 2 @@ -584,7 +584,7 @@ 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) @@ -685,7 +685,7 @@ 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 @@ -703,7 +703,7 @@ class ObjectWaterline(PathOp.ObjectOp): "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 @@ -720,7 +720,7 @@ class ObjectWaterline(PathOp.ObjectOp): "PathWaterline", "Gap Detection Thershold limits are 1.00 to 999.99 millimeters.", ) - ) + ) # Limit cut pattern angle if obj.CutPatternAngle < -360.0: @@ -1253,7 +1253,7 @@ 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 @@ -1274,17 +1274,17 @@ 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": + + if obj.Algorithm == "OCL Adaptive": # Get Stock boundbox for OCL Adaptive BS = JOB.Stock - bb = BS.Shape.BoundBox + bb = BS.Shape.BoundBox xmin = round(abs(bb.XMin), 6) xmax = round(abs(bb.XMax), 6) ymin = round(abs(bb.YMin), 6) - ymax = round(abs(bb.YMax), 6) - - else: + ymax = round(abs(bb.YMax), 6) + + else: if subShp is None: # Get correct boundbox if obj.BoundBox == "Stock": @@ -1302,10 +1302,10 @@ class ObjectWaterline(PathOp.ObjectOp): xmin = subShp.BoundBox.XMin xmax = subShp.BoundBox.XMax ymin = subShp.BoundBox.YMin - ymax = subShp.BoundBox.YMax + ymax = subShp.BoundBox.YMax smplInt = obj.SampleInterval.Value - minSmplInt = obj.MinSampleInterval.Value + minSmplInt = obj.MinSampleInterval.Value if minSmplInt > smplInt: minSmplInt = smplInt @@ -1322,7 +1322,7 @@ class ObjectWaterline(PathOp.ObjectOp): # Scan the piece to depth at smplInt oclScan = [] - scanLines = [] + 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) @@ -1330,24 +1330,27 @@ class ObjectWaterline(PathOp.ObjectOp): 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.") + 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) + # 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: + if obj.OptimizeInternalFeatures: GapDetec = float(obj.GapDetectionThershold) if smplInt >= GapDetec: - GapDetec = smplInt + 1 # We need smaller smplInt than GapDetec to identify Gaps + GapDetec = smplInt + 1 # We need smaller smplInt than GapDetec to identify Gaps optimize = self._optimizeAdaptive(scanLines, GapDetec) - scanLines = optimize - else: # Drop Cutter + 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) + + lenOS = len(oclScan) ptPrLn = int(lenOS / numScanLines) # Convert oclScan list of points to multi-dimensional list @@ -1372,8 +1375,8 @@ class ObjectWaterline(PathOp.ObjectOp): cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) commands.extend(cmds) if obj.Algorithm == "OCL Adaptive": - break # OCL_Adaptive processes all depths simultaneously (break loop) - lyr += 1 + 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 @@ -1399,96 +1402,96 @@ 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.""" - - msg = translate("Waterline", ": Steps below the model's top Face will be the only ones processed.") - Path.Log.info("Waterline " + msg) - + """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) + awl = ocl.AdaptiveWaterline() awl.setSTL(stl) awl.setCutter(self.cutter) awl.setSampling(smplInt) - awl.setMinSampling(minSmplInt) - + awl.setMinSampling(minSmplInt) + # Create Adaptive loops adapt_loops = [] acnt = 0 for zh in zheights: - #zh = round(zh, 3) + # zh = round(zh, 3) temp_loops = [] finalZ_loops = [] - skipZ = False - awl.setZ(zh) + 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) + 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) + 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 - + 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""" - + """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: + 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. + 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)] + 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 + 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] + 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) + 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 = [] @@ -1497,16 +1500,16 @@ class ObjectWaterline(PathOp.ObjectOp): self.topoMap = [] if obj.Algorithm == "OCL Adaptive": loopList = scanLines - else: + else: # Create topo map from scanLines (highs and lows) - self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine) + 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) + 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) @@ -1759,7 +1762,7 @@ class ObjectWaterline(PathOp.ObjectOp): + str(loopNum) + " loops." ) - + return loopList def _trackLoop(self, oclScan, lC, pC, L, P, loopNum): @@ -1844,16 +1847,16 @@ class ObjectWaterline(PathOp.ObjectOp): # Create (first and last) point if obj.Algorithm == "OCL Adaptive": if obj.CutMode == "Climb": - # Reverse loop for Climb Milling - loop.reverse() + # 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 + 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: + 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})) @@ -1872,12 +1875,12 @@ class ObjectWaterline(PathOp.ObjectOp): 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": + 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 @@ -1969,7 +1972,7 @@ class ObjectWaterline(PathOp.ObjectOp): if cont: # Identify solid areas in the offset data if obj.CutPattern == "Offset" or obj.CutPattern == "None": - if ofstArea: + if ofstArea: ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea) if ofstSolidFacesList: clearArea = Part.makeCompound(ofstSolidFacesList) From 9c2176941051a3a2741d000d776c32d135e6ffac Mon Sep 17 00:00:00 2001 From: Dimitris75 Date: Tue, 19 Aug 2025 23:55:41 +0300 Subject: [PATCH 06/15] Update Waterline.py --- src/Mod/CAM/Path/Op/Waterline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Op/Waterline.py b/src/Mod/CAM/Path/Op/Waterline.py index 9fe0437453..2a4103e382 100644 --- a/src/Mod/CAM/Path/Op/Waterline.py +++ b/src/Mod/CAM/Path/Op/Waterline.py @@ -1292,7 +1292,7 @@ class ObjectWaterline(PathOp.ObjectOp): bb = BS.Shape.BoundBox elif obj.BoundBox == "BaseBoundBox": BS = base - bb = base.Shape.BoundBox + bb = BS.Shape.BoundBox xmin = bb.XMin xmax = bb.XMax From 96c23f19682cf39591a1c50392eb60e040433214 Mon Sep 17 00:00:00 2001 From: Dimitris75 <30848292+Dimitris75@users.noreply.github.com> Date: Tue, 16 Dec 2025 01:57:45 +0200 Subject: [PATCH 07/15] Simplify code and UI Simplify code and UI --- .../Resources/panels/PageOpWaterlineEdit.ui | 86 +++--- src/Mod/CAM/Path/Op/Gui/Waterline.py | 37 +-- src/Mod/CAM/Path/Op/Waterline.py | 268 ++++++------------ 3 files changed, 124 insertions(+), 267 deletions(-) diff --git a/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui b/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui index 204283833a..aef29eb173 100644 --- a/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui +++ b/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui @@ -22,18 +22,18 @@ QFrame::Raised - + - Tool Controller + Tool controller - The tool and its settings to be used for this operation. + The tool and its settings to be used for this operation @@ -43,7 +43,14 @@ - Coolant Mode + Coolant mode + + + + + + + Edit Tool Controller @@ -63,7 +70,7 @@ - Select the algorithm to use: OCL Dropcutter*, OCL Adaptive* or Experimental (Not OCL based). + Select the algorithm to use: 'OCL Dropcutter*', or 'Experimental' (not OCL based). @@ -76,7 +83,7 @@ - Bounding Box + Bounding box @@ -88,14 +95,14 @@ - Select the overall boundary for the operation. + Select the overall boundary for the operation - Layer Mode + Layer mode @@ -107,14 +114,14 @@ - Complete the operation in a single pass at depth, or multiple passes to final depth. + Complete the operation in a single pass at depth, or multiple passes to final depth - Cut Pattern + Cut pattern @@ -126,7 +133,7 @@ - Set the geometric clearing pattern to use for the operation. + Set the geometric clearing pattern to use for the operation @@ -139,14 +146,14 @@ - Boundary Adjustment + Boundary adjustment - Set the Z-axis depth offset from the target surface. + Set the Z-axis depth offset from the target surface mm @@ -205,50 +212,23 @@ A step over of 100% results in no overlap between two different cycles. - - - - Set the minimum sampling resolution. Smaller values quickly increase processing time. - - - mm - - - - + + + + Set the minimum sampling resolution. Smaller values quickly increase processing time. + + + mm + + + + Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-code output. - Optimize Linear Paths - - - - - - - Detect the interconnection of internal features or holes and raise the tool for transition. - - - Optimize Internal Features - - - - - - - Gap Detection Thershold - - - - - - - Minimum distance between the perimeter and any internal features. Lower values than Sample Interval will be ignored. - - - mm + Optimize linear paths @@ -288,8 +268,6 @@ A step over of 100% results in no overlap between two different cycles. sampleInterval minSampleInterval optimizeEnabled - optimizeInternal - gapDetectionThershold diff --git a/src/Mod/CAM/Path/Op/Gui/Waterline.py b/src/Mod/CAM/Path/Op/Gui/Waterline.py index 817786ec3a..29f1ec48c7 100644 --- a/src/Mod/CAM/Path/Op/Gui/Waterline.py +++ b/src/Mod/CAM/Path/Op/Gui/Waterline.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +# SPDX-License-Identifier: LGPL-2.1-or-later + # *************************************************************************** # * Copyright (c) 2020 sliptonic * # * Copyright (c) 2020 russ4262 * @@ -89,11 +90,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): 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() @@ -116,15 +112,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): 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) else: @@ -145,11 +132,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): 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) @@ -164,8 +146,6 @@ 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() @@ -176,12 +156,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): 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() @@ -192,12 +167,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): 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() @@ -212,9 +182,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): 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) diff --git a/src/Mod/CAM/Path/Op/Waterline.py b/src/Mod/CAM/Path/Op/Waterline.py index fdf796f4aa..b7502ecb0f 100644 --- a/src/Mod/CAM/Path/Op/Waterline.py +++ b/src/Mod/CAM/Path/Op/Waterline.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +# SPDX-License-Identifier: LGPL-2.1-or-later + # *************************************************************************** # * Copyright (c) 2019 Russell Johnson (russ4262) * # * Copyright (c) 2019 sliptonic * @@ -23,7 +24,6 @@ import FreeCAD - __title__ = "CAM Waterline Operation" __author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)" __url__ = "https://www.freecad.org" @@ -49,7 +49,6 @@ 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 @@ -100,8 +99,8 @@ class ObjectWaterline(PathOp.ObjectOp): (translate("path_waterline", "Experimental"), "Experimental"), ], "BoundBox": [ - (translate("path_waterline", "Stock"), "Stock"), (translate("path_waterline", "BaseBoundBox"), "BaseBoundBox"), + (translate("path_waterline", "Stock"), "Stock"), ], "PatternCenterAt": [ (translate("path_waterline", "CenterOfMass"), "CenterOfMass"), @@ -403,24 +402,6 @@ class ObjectWaterline(PathOp.ObjectOp): "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", @@ -509,8 +490,6 @@ class ObjectWaterline(PathOp.ObjectOp): "DepthOffset": 0.0, "SampleInterval": 1.0, "MinSampleInterval": 0.005, - "OptimizeInternalFeatures": False, - "GapDetectionThershold": 3.50, "BoundaryAdjustment": 0.0, "InternalFeaturesAdjustment": 0.0, "AvoidLastX_Faces": 0, @@ -544,12 +523,12 @@ class ObjectWaterline(PathOp.ObjectOp): 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 @@ -584,11 +563,7 @@ 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) @@ -598,6 +573,9 @@ class ObjectWaterline(PathOp.ObjectOp): 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) @@ -704,24 +682,6 @@ class ObjectWaterline(PathOp.ObjectOp): ) ) - # 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: obj.CutPatternAngle = 0.0 @@ -1273,8 +1233,19 @@ class ObjectWaterline(PathOp.ObjectOp): if self.layerEndPnt is None: self.layerEndPnt = FreeCAD.Vector(0.0, 0.0, 0.0) - # Set extra offset to diameter of cutter to allow cutter to move around perimeter of model + 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 boundbox for OCL Adaptive BS = JOB.Stock @@ -1284,7 +1255,31 @@ class ObjectWaterline(PathOp.ObjectOp): ymin = round(abs(bb.YMin), 6) ymax = round(abs(bb.YMax), 6) + # 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") + # 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": @@ -1303,81 +1298,43 @@ class ObjectWaterline(PathOp.ObjectOp): xmax = subShp.BoundBox.XMax ymin = subShp.BoundBox.YMin ymax = subShp.BoundBox.YMax - - smplInt = obj.SampleInterval.Value - minSmplInt = obj.MinSampleInterval.Value - if minSmplInt > smplInt: - minSmplInt = smplInt - - # Determine bounding box length for the OCL scan - bbLength = math.fabs(ymax - ymin) - numScanLines = int(math.ceil(bbLength / smplInt) + 1) # Number of lines - - # Compute number and size of stepdowns, and final depth - if obj.LayerMode == "Single-pass": - depthparams = [obj.FinalDepth.Value] - else: - depthparams = [dp for dp in self.depthParams] - lenDP = len(depthparams) - - # Scan the piece to depth at smplInt - oclScan = [] - 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 - ) + + # 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) - - # 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 " - msg += str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line" - Path.Log.debug(msg) - # Extract Wl layers per depthparams - lyr = 0 - cmds = [] - layTime = time.time() - self.topoMap = [] - for layDep in depthparams: - cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) - commands.extend(cmds) - 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") + # 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): @@ -1411,84 +1368,37 @@ class ObjectWaterline(PathOp.ObjectOp): ) Path.Log.info("Waterline " + msg) + # Setup OCL AdaptiveWaterline awl = ocl.AdaptiveWaterline() awl.setSTL(stl) awl.setCutter(self.cutter) awl.setSampling(smplInt) awl.setMinSampling(minSmplInt) - # Create Adaptive loops adapt_loops = [] - acnt = 0 + + # Iterate through each Z-depth for zh in zheights: - # zh = round(zh, 3) - temp_loops = [] - finalZ_loops = [] - skipZ = False 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: - # Skip if height is above model + # 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") - skipZ = True - acnt -= 1 - else: - for tmp in temp_loops: - finalZ_loops += tmp + continue - 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 + # 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 @@ -1514,7 +1424,6 @@ class ObjectWaterline(PathOp.ObjectOp): for loop in loopList: cmds = self._loopToGcode(obj, layDep, loop) commands.extend(cmds) - return commands def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine): @@ -1762,7 +1671,6 @@ class ObjectWaterline(PathOp.ObjectOp): + str(loopNum) + " loops." ) - return loopList def _trackLoop(self, oclScan, lC, pC, L, P, loopNum): @@ -1841,6 +1749,10 @@ class ObjectWaterline(PathOp.ObjectOp): """_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) @@ -1858,6 +1770,7 @@ class ObjectWaterline(PathOp.ObjectOp): 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})) @@ -1972,8 +1885,7 @@ class ObjectWaterline(PathOp.ObjectOp): if cont: # Identify solid areas in the offset data if obj.CutPattern == "Offset" or obj.CutPattern == "None": - if ofstArea: - ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea) + ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea) if ofstSolidFacesList: clearArea = Part.makeCompound(ofstSolidFacesList) self.showDebugObject(clearArea, "ClearArea_{}".format(caCnt)) From ccf0bdbce0c9db3beb08770d08e86696e305ac9a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:59:38 +0000 Subject: [PATCH 08/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/Mod/CAM/Path/Op/Waterline.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Mod/CAM/Path/Op/Waterline.py b/src/Mod/CAM/Path/Op/Waterline.py index b7502ecb0f..7734c32f01 100644 --- a/src/Mod/CAM/Path/Op/Waterline.py +++ b/src/Mod/CAM/Path/Op/Waterline.py @@ -1275,9 +1275,9 @@ class ObjectWaterline(PathOp.ObjectOp): # 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: @@ -1298,13 +1298,13 @@ class ObjectWaterline(PathOp.ObjectOp): 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] + 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] @@ -1381,7 +1381,7 @@ class ObjectWaterline(PathOp.ObjectOp): 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() @@ -1393,7 +1393,7 @@ class ObjectWaterline(PathOp.ObjectOp): FreeCAD.Console.PrintWarning(newPropMsg + "\n") continue - # Process each loop separately. + # 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 @@ -1749,10 +1749,10 @@ class ObjectWaterline(PathOp.ObjectOp): """_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 + return output nxt = FreeCAD.Vector(0.0, 0.0, 0.0) @@ -1770,7 +1770,7 @@ class ObjectWaterline(PathOp.ObjectOp): 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})) From a6bc504a2a065c6103b75ecdfb6520ab3d5a8fa7 Mon Sep 17 00:00:00 2001 From: Dimitris75 <30848292+Dimitris75@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:27:15 +0200 Subject: [PATCH 09/15] Add OCL Adaptive in Tooltip Add OCL Adaptive algorithm in UI Tooltip --- src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui b/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui index aef29eb173..4b1036a8df 100644 --- a/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui +++ b/src/Mod/CAM/Gui/Resources/panels/PageOpWaterlineEdit.ui @@ -70,7 +70,7 @@ - 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). From 67df5bd709e0ba313f966e808366b323982e21ad Mon Sep 17 00:00:00 2001 From: Dimitris75 <30848292+Dimitris75@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:37:04 +0200 Subject: [PATCH 10/15] Use dynamic LinearDeflection and AngularDeflection values Remove Temporary values from Mesh --- src/Mod/CAM/Path/Op/SurfaceSupport.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Op/SurfaceSupport.py b/src/Mod/CAM/Path/Op/SurfaceSupport.py index 895d01b3ed..3abb9a4cb4 100644 --- a/src/Mod/CAM/Path/Op/SurfaceSupport.py +++ b/src/Mod/CAM/Path/Op/SurfaceSupport.py @@ -1262,7 +1262,11 @@ def _makeSTL(model, obj, ocl, model_type=None): shape = model # 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) + mesh = MeshPart.meshFromShape( + Shape=shape, + LinearDeflection=obj.LinearDeflection.Value, + AngularDeflection=obj.AngularDeflection.Value + ) 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) From f9bb30217b469e2436b78ffd0bc805839b67273d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 15:38:56 +0000 Subject: [PATCH 11/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/Mod/CAM/Path/Op/SurfaceSupport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Op/SurfaceSupport.py b/src/Mod/CAM/Path/Op/SurfaceSupport.py index 3abb9a4cb4..01ae5357d8 100644 --- a/src/Mod/CAM/Path/Op/SurfaceSupport.py +++ b/src/Mod/CAM/Path/Op/SurfaceSupport.py @@ -1265,7 +1265,7 @@ def _makeSTL(model, obj, ocl, model_type=None): mesh = MeshPart.meshFromShape( Shape=shape, LinearDeflection=obj.LinearDeflection.Value, - AngularDeflection=obj.AngularDeflection.Value + AngularDeflection=obj.AngularDeflection.Value, ) vertices = [point.Vector for point in mesh.Points] facet_indices = [facet.PointIndices for facet in mesh.Facets] From 93410b264a9e1b9072998784398b7c6627f3cc67 Mon Sep 17 00:00:00 2001 From: Dimitris75 <30848292+Dimitris75@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:18:47 +0200 Subject: [PATCH 12/15] Override deflection values for OCL Adaptive algorithm Added logic to override deflection values for OCL Adaptive algorithm to improve topology stability. --- src/Mod/CAM/Path/Op/SurfaceSupport.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Mod/CAM/Path/Op/SurfaceSupport.py b/src/Mod/CAM/Path/Op/SurfaceSupport.py index 01ae5357d8..5609591ebc 100644 --- a/src/Mod/CAM/Path/Op/SurfaceSupport.py +++ b/src/Mod/CAM/Path/Op/SurfaceSupport.py @@ -1253,6 +1253,22 @@ def _makeSTL(model, obj, ocl, model_type=None): """Convert a mesh or shape into an OCL STL, using the tessellation tolerance specified in obj.LinearDeflection. Returns an ocl.STLSurf().""" + # Determine Deflection Values + lin_def = obj.LinearDeflection.Value + ang_def = obj.AngularDeflection.Value + + # Apply Overrides for Waterline OCL Adaptive + # OCL Adaptive is a Vector-based algorithm, not a Grid-based algorithm (like Dropcutter) + # This fundamental difference makes it sensitive to Topology (how points connect) rather than just density + # Models with internal features can cause the algorithm to be confused even with very high density values. + # The following values create the cleanest possible Topology for a vector-slicing algorithm + # Setting those values here rather than hacking the Obj values in Waterline.py is preferable. + algo = getattr(obj, "Algorithm", None) + if algo == "OCL Adaptive": + # Force the "Sweet Spot" values for topology stability (Good enough for 99% or more of operations) + lin_def = 0.001 + ang_def = 0.15 + if model_type == "M": facets = model.Mesh.Facets.Points else: @@ -1264,8 +1280,8 @@ def _makeSTL(model, obj, ocl, model_type=None): # Workaround for tessellate bug mesh = MeshPart.meshFromShape( Shape=shape, - LinearDeflection=obj.LinearDeflection.Value, - AngularDeflection=obj.AngularDeflection.Value, + LinearDeflection=obj.lin_def, + AngularDeflection=obj.ang_def, ) vertices = [point.Vector for point in mesh.Points] facet_indices = [facet.PointIndices for facet in mesh.Facets] From 5b0cdb2b6d7038788279162054885db3d6cdaa5b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 20:22:55 +0000 Subject: [PATCH 13/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/Mod/CAM/Path/Op/SurfaceSupport.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Mod/CAM/Path/Op/SurfaceSupport.py b/src/Mod/CAM/Path/Op/SurfaceSupport.py index 5609591ebc..13ba2d769e 100644 --- a/src/Mod/CAM/Path/Op/SurfaceSupport.py +++ b/src/Mod/CAM/Path/Op/SurfaceSupport.py @@ -1253,7 +1253,7 @@ def _makeSTL(model, obj, ocl, model_type=None): """Convert a mesh or shape into an OCL STL, using the tessellation tolerance specified in obj.LinearDeflection. Returns an ocl.STLSurf().""" - # Determine Deflection Values + # Determine Deflection Values lin_def = obj.LinearDeflection.Value ang_def = obj.AngularDeflection.Value @@ -1262,13 +1262,13 @@ def _makeSTL(model, obj, ocl, model_type=None): # This fundamental difference makes it sensitive to Topology (how points connect) rather than just density # Models with internal features can cause the algorithm to be confused even with very high density values. # The following values create the cleanest possible Topology for a vector-slicing algorithm - # Setting those values here rather than hacking the Obj values in Waterline.py is preferable. - algo = getattr(obj, "Algorithm", None) + # Setting those values here rather than hacking the Obj values in Waterline.py is preferable. + algo = getattr(obj, "Algorithm", None) if algo == "OCL Adaptive": # Force the "Sweet Spot" values for topology stability (Good enough for 99% or more of operations) lin_def = 0.001 ang_def = 0.15 - + if model_type == "M": facets = model.Mesh.Facets.Points else: From a840988ddba4dab8ee4d4fc36984f8a29d9594a0 Mon Sep 17 00:00:00 2001 From: Dimitris75 <30848292+Dimitris75@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:55:22 +0200 Subject: [PATCH 14/15] Refactor bounding box and tool path limit calculations Refactor bounding box calculations for OCL Adaptive algorithm to improve clarity and maintainability. --- src/Mod/CAM/Path/Op/Waterline.py | 34 +++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Mod/CAM/Path/Op/Waterline.py b/src/Mod/CAM/Path/Op/Waterline.py index 7734c32f01..bb1bd4cfc4 100644 --- a/src/Mod/CAM/Path/Op/Waterline.py +++ b/src/Mod/CAM/Path/Op/Waterline.py @@ -1247,25 +1247,35 @@ class ObjectWaterline(PathOp.ObjectOp): # Scan the piece to depth at smplInt if obj.Algorithm == "OCL Adaptive": - # Get Stock boundbox for OCL Adaptive + # Get Stock Bounding Box BS = JOB.Stock - bb = BS.Shape.BoundBox - xmin = round(abs(bb.XMin), 6) - xmax = round(abs(bb.XMax), 6) - ymin = round(abs(bb.YMin), 6) - ymax = round(abs(bb.YMax), 6) + 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): - # 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") + # Run the Scan (Processing ALL depths at once) scanLines = self._waterlineAdaptiveScan(stl, smplInt, minSmplInt, depthparams, depOfst) From be6c633d620db9b5999b8314b090e7088c138832 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 20:57:17 +0000 Subject: [PATCH 15/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/Mod/CAM/Path/Op/Waterline.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Mod/CAM/Path/Op/Waterline.py b/src/Mod/CAM/Path/Op/Waterline.py index bb1bd4cfc4..88d915d2a2 100644 --- a/src/Mod/CAM/Path/Op/Waterline.py +++ b/src/Mod/CAM/Path/Op/Waterline.py @@ -1257,18 +1257,20 @@ class ObjectWaterline(PathOp.ObjectOp): s_ymin = stock_bb.YMin s_ymax = stock_bb.YMax - # Calculate Tool Path Limits based on OCL STL + # 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): + 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",