From 1a9df4eb800c4212dc82bf9fd66828d0414a25e5 Mon Sep 17 00:00:00 2001 From: Dimitris75 Date: Sat, 16 Aug 2025 19:41:01 +0300 Subject: [PATCH] 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))