diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 1a51ebc872..ded7c91a93 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -25,6 +25,8 @@ INSTALL( SET(PathScripts_SRCS PathCommands.py + PathScripts/PathAdaptive.py + PathScripts/PathAdaptiveGui.py PathScripts/PathAreaOp.py PathScripts/PathArray.py PathScripts/PathCircularHoleBase.py @@ -100,6 +102,7 @@ SET(PathScripts_SRCS PathScripts/PathSetupSheetOpPrototype.py PathScripts/PathSetupSheetOpPrototypeGui.py PathScripts/PathSimpleCopy.py + PathScripts/PathSimulatorGui.py PathScripts/PathStock.py PathScripts/PathStop.py PathScripts/PathSurface.py @@ -113,15 +116,14 @@ SET(PathScripts_SRCS PathScripts/PathToolController.py PathScripts/PathToolControllerGui.py PathScripts/PathToolEdit.py - PathScripts/PathToolLibraryManager.py PathScripts/PathToolLibraryEditor.py + PathScripts/PathToolLibraryManager.py PathScripts/PathUtil.py PathScripts/PathUtils.py PathScripts/PathUtilsGui.py - PathScripts/PathSimulatorGui.py + PathScripts/PathWaterline.py + PathScripts/PathWaterlineGui.py PathScripts/PostUtils.py - PathScripts/PathAdaptiveGui.py - PathScripts/PathAdaptive.py PathScripts/__init__.py ) @@ -192,8 +194,8 @@ SET(PathTests_SRCS PathTests/boxtest.fcstd PathTests/test_centroid_00.ngc PathTests/test_geomop.fcstd - PathTests/test_linuxcnc_00.ngc PathTests/test_holes00.fcstd + PathTests/test_linuxcnc_00.ngc ) SET(PathImages_Ops diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index ca09e140b8..4471e2eb6e 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -1,9 +1,19 @@ + icons/Path-Adaptive.svg + icons/Path-ToolDuplicate.svg icons/Path-3DPocket.svg icons/Path-3DSurface.svg + icons/Path-Area-View.svg + icons/Path-Area-Workplane.svg + icons/Path-Area.svg icons/Path-Array.svg icons/Path-Axis.svg + icons/Path-BFastForward.svg + icons/Path-BPause.svg + icons/Path-BPlay.svg + icons/Path-BStep.svg + icons/Path-BStop.svg icons/Path-BaseGeometry.svg icons/Path-Comment.svg icons/Path-Compound.svg @@ -17,9 +27,9 @@ icons/Path-Drilling.svg icons/Path-Engrave.svg icons/Path-ExportTemplate.svg + icons/Path-Face.svg icons/Path-FacePocket.svg icons/Path-FaceProfile.svg - icons/Path-Face.svg icons/Path-Heights.svg icons/Path-Helix.svg icons/Path-Hop.svg @@ -27,13 +37,13 @@ icons/Path-Job.svg icons/Path-Kurve.svg icons/Path-LengthOffset.svg + icons/Path-Machine.svg icons/Path-MachineLathe.svg icons/Path-MachineMill.svg - icons/Path-Machine.svg icons/Path-OpActive.svg + icons/Path-OpCopy.svg icons/Path-OperationA.svg icons/Path-OperationB.svg - icons/Path-OpCopy.svg icons/Path-Plane.svg icons/Path-Pocket.svg icons/Path-Post.svg @@ -46,49 +56,40 @@ icons/Path-SetupSheet.svg icons/Path-Shape.svg icons/Path-SimpleCopy.svg + icons/Path-Simulator.svg icons/Path-Speed.svg icons/Path-Stock.svg icons/Path-Stop.svg icons/Path-ToolBit.svg icons/Path-ToolChange.svg icons/Path-ToolController.svg - icons/Path-ToolDuplicate.svg icons/Path-Toolpath.svg icons/Path-ToolTable.svg - icons/Path-Area.svg - icons/Path-Area-View.svg - icons/Path-Area-Workplane.svg - icons/Path-Simulator.svg - icons/Path-BFastForward.svg - icons/Path-BPause.svg - icons/Path-BPlay.svg - icons/Path-BStep.svg - icons/Path-BStop.svg + icons/Path-Waterline.svg icons/arrow-ccw.svg icons/arrow-cw.svg icons/arrow-down.svg - icons/arrow-left.svg icons/arrow-left-down.svg icons/arrow-left-up.svg - icons/arrow-right.svg + icons/arrow-left.svg icons/arrow-right-down.svg icons/arrow-right-up.svg + icons/arrow-right.svg icons/arrow-up.svg - icons/edge-join-miter.svg icons/edge-join-miter-not.svg - icons/edge-join-round.svg + icons/edge-join-miter.svg icons/edge-join-round-not.svg + icons/edge-join-round.svg icons/preferences-path.svg - icons/Path-Adaptive.svg panels/DlgJobChooser.ui panels/DlgJobCreate.ui panels/DlgJobModelSelect.ui panels/DlgJobTemplateExport.ui panels/DlgSelectPostProcessor.ui + panels/DlgTCChooser.ui panels/DlgToolControllerEdit.ui panels/DlgToolCopy.ui panels/DlgToolEdit.ui - panels/DlgTCChooser.ui panels/DogboneEdit.ui panels/DressupPathBoundary.ui panels/HoldingTagsEdit.ui @@ -106,6 +107,7 @@ panels/PageOpProbeEdit.ui panels/PageOpProfileFullEdit.ui panels/PageOpSurfaceEdit.ui + panels/PageOpWaterlineEdit.ui panels/PathEdit.ui panels/PointEdit.ui panels/SetupGlobal.ui @@ -120,16 +122,25 @@ preferences/PathDressupHoldingTags.ui preferences/PathJob.ui translations/Path_af.qm + translations/Path_ar.qm + translations/Path_ca.qm translations/Path_cs.qm translations/Path_de.qm translations/Path_el.qm translations/Path_es-ES.qm + translations/Path_eu.qm translations/Path_fi.qm + translations/Path_fil.qm translations/Path_fr.qm + translations/Path_gl.qm translations/Path_hr.qm translations/Path_hu.qm + translations/Path_id.qm translations/Path_it.qm translations/Path_ja.qm + translations/Path_kab.qm + translations/Path_ko.qm + translations/Path_lt.qm translations/Path_nl.qm translations/Path_no.qm translations/Path_pl.qm @@ -143,18 +154,9 @@ translations/Path_sv-SE.qm translations/Path_tr.qm translations/Path_uk.qm + translations/Path_val-ES.qm + translations/Path_vi.qm translations/Path_zh-CN.qm translations/Path_zh-TW.qm - translations/Path_eu.qm - translations/Path_ca.qm - translations/Path_gl.qm - translations/Path_kab.qm - translations/Path_ko.qm - translations/Path_fil.qm - translations/Path_id.qm - translations/Path_lt.qm - translations/Path_val-ES.qm - translations/Path_ar.qm - translations/Path_vi.qm diff --git a/src/Mod/Path/Gui/Resources/icons/Path-Waterline.svg b/src/Mod/Path/Gui/Resources/icons/Path-Waterline.svg new file mode 100644 index 0000000000..86c07f4c62 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/icons/Path-Waterline.svg @@ -0,0 +1,281 @@ + + + Path_Waterline + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Path_Waterline + Path-Waterline + 2019-05-19 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Waterline.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [russ4262] Russell Johnson + + + + + [russ4262] Russell Johnson + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui index d019e3fe67..e4aaf19e5e 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui @@ -6,8 +6,8 @@ 0 0 - 357 - 427 + 350 + 400 @@ -24,7 +24,7 @@ - + ToolController @@ -38,7 +38,7 @@ - + Coolant Mode @@ -57,126 +57,35 @@ - - - - Algorithm - - - - - - - - OCL Dropcutter - - - - - OCL Waterline - - - - - - - - BoundBox - - - - + - Stock + Planar - BaseBoundBox + Rotational - - - - BoundBox extra offset X, Y - - - - - - - mm - - - - - - - mm - - - - - - - Drop Cutter Direction - - - - - + + - X + Single-pass - Y + Multi-pass - - - - Depth offset - - - - - - - mm - - - - - - - Sample interval - - - - - - - mm - - - - - - - Step over - - - - + <html><head/><body><p>The amount by which the tool is laterally displaced on each cycle of the pattern, specified in percent of the tool diameter.</p><p>A step over of 100% results in no overlap between two different cycles.</p></body></html> @@ -195,20 +104,180 @@ - - + + - Optimize output + Step over + + + + + + + Sample interval + + + + + + + Layer Mode + + + + + + + Optimize Linear Paths + + + + + + + Drop Cutter Direction + + + + + + + BoundBox extra offset X, Y + + + + + + + Use Start Point + + + + + + + Scan Type + + + + + + + BoundBox - - - Enabled + + + mm + + + + + + + 0 + 0 + + + + mm + + + + + + + mm + + + + + + + + + mm + + + + + + + Depth offset + + + + + + + + X + + + + + Y + + + + + + + + + Stock + + + + + BaseBoundBox + + + + + + + + Optimize StepOver Transitions + + + + + + + Cut Pattern + + + + + + + + Line + + + + + ZigZag + + + + + Circular + + + + + CircularZigZag + + + + diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui new file mode 100644 index 0000000000..5e0edef1c9 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui @@ -0,0 +1,269 @@ + + + Form + + + + 0 + 0 + 350 + 400 + + + + Form + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + ToolController + + + + + + + <html><head/><body><p>The tool and its settings to be used for this operation.</p></body></html> + + + + + + + + + + + + + + 8 + + + + + Stock + + + + + BaseBoundBox + + + + + + + + + 0 + 0 + + + + + + + + + 8 + + + + + Single-pass + + + + + Multi-pass + + + + + + + + + OCL Dropcutter + + + + + Experimental + + + + + + + + Optimize Linear Paths + + + + + + + + 0 + 0 + + + + Boundary Adjustment + + + + + + + + 8 + + + + + None + + + + + Line + + + + + ZigZag + + + + + Circular + + + + + CircularZigZag + + + + + + + + + 0 + 0 + + + + <html><head/><body><p>The amount by which the tool is laterally displaced on each cycle of the pattern, specified in percent of the tool diameter.</p><p>A step over of 100% results in no overlap between two different cycles.</p></body></html> + + + 1 + + + 100 + + + 10 + + + 100 + + + + + + + Layer Mode + + + + + + + + 0 + 0 + + + + BoundBox + + + + + + + Step over + + + + + + + mm + + + + + + + Cut Pattern + + + + + + + Sample interval + + + + + + + Algorithm + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Gui::InputField + QWidget +
gui::inputfield.h
+
+
+ + +
diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index dc638a9299..6fabab05f0 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -21,8 +21,9 @@ # * * # ***************************************************************************/ + class PathCommandGroup: - def __init__(self, cmdlist, menu, tooltip = None): + def __init__(self, cmdlist, menu, tooltip=None): self.cmdlist = cmdlist self.menu = menu if tooltip is None: @@ -34,7 +35,7 @@ class PathCommandGroup: return tuple(self.cmdlist) def GetResources(self): - return { 'MenuText': self.menu, 'ToolTip': self.tooltip } + return {'MenuText': self.menu, 'ToolTip': self.tooltip} def IsActive(self): if FreeCAD.ActiveDocument is not None: @@ -43,6 +44,7 @@ class PathCommandGroup: return True return False + class PathWorkbench (Workbench): "Path workbench" @@ -88,14 +90,14 @@ class PathWorkbench (Workbench): projcmdlist = ["Path_Job", "Path_Post"] toolcmdlist = ["Path_Inspect", "Path_Simulator", "Path_ToolLibraryEdit", "Path_SelectLoop", "Path_OpActiveToggle"] prepcmdlist = ["Path_Fixture", "Path_Comment", "Path_Stop", "Path_Custom", "Path_Probe"] - twodopcmdlist = ["Path_Contour", "Path_Profile_Faces", "Path_Profile_Edges", "Path_Pocket_Shape", "Path_Drilling", "Path_MillFace", "Path_Helix", "Path_Adaptive" ] + twodopcmdlist = ["Path_Contour", "Path_Profile_Faces", "Path_Profile_Edges", "Path_Pocket_Shape", "Path_Drilling", "Path_MillFace", "Path_Helix", "Path_Adaptive"] threedopcmdlist = ["Path_Pocket_3D"] engravecmdlist = ["Path_Engrave", "Path_Deburr"] - modcmdlist = ["Path_OperationCopy", "Path_Array", "Path_SimpleCopy" ] + modcmdlist = ["Path_OperationCopy", "Path_Array", "Path_SimpleCopy"] dressupcmdlist = ["Path_DressupAxisMap", "Path_DressupPathBoundary", "Path_DressupDogbone", "Path_DressupDragKnife", "Path_DressupLeadInOut", "Path_DressupRampEntry", "Path_DressupTag", "Path_DressupZCorrect"] extracmdlist = [] - #modcmdmore = ["Path_Hop",] - #remotecmdlist = ["Path_Remote"] + # modcmdmore = ["Path_Hop",] + # remotecmdlist = ["Path_Remote"] engravecmdgroup = ['Path_EngraveTools'] FreeCADGui.addCommand('Path_EngraveTools', PathCommandGroup(engravecmdlist, QtCore.QT_TRANSLATE_NOOP("Path", 'Engraving Operations'))) @@ -107,11 +109,12 @@ class PathWorkbench (Workbench): extracmdlist.extend(["Path_Area", "Path_Area_Workplane"]) try: - import ocl # pylint: disable=unused-variable + import ocl # pylint: disable=unused-variable from PathScripts import PathSurfaceGui - threedopcmdlist.append("Path_Surface") + from PathScripts import PathWaterlineGui + threedopcmdlist.extend(["Path_Surface", "Path_Waterline"]) threedcmdgroup = ['Path_3dTools'] - FreeCADGui.addCommand('Path_3dTools', PathCommandGroup(threedopcmdlist, QtCore.QT_TRANSLATE_NOOP("Path",'3D Operations'))) + FreeCADGui.addCommand('Path_3dTools', PathCommandGroup(threedopcmdlist, QtCore.QT_TRANSLATE_NOOP("Path", '3D Operations'))) except ImportError: FreeCAD.Console.PrintError("OpenCamLib is not working!\n") @@ -122,7 +125,9 @@ class PathWorkbench (Workbench): if extracmdlist: self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist) - self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + toolbitcmdlist + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist + ["Path_ExportTemplate", "Separator"] + + toolbitcmdlist + toolcmdlist + ["Separator"] + twodopcmdlist + engravecmdlist + ["Separator"] + + threedopcmdlist + ["Separator"]) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( "Path", "Path Dressup")], dressupcmdlist) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( @@ -136,7 +141,7 @@ class PathWorkbench (Workbench): curveAccuracy = PathPreferences.defaultLibAreaCurveAccuracy() if curveAccuracy: - Path.Area.setDefaultParams(Accuracy = curveAccuracy) + Path.Area.setDefaultParams(Accuracy=curveAccuracy) Log('Loading Path workbench... done\n') @@ -171,8 +176,8 @@ class PathWorkbench (Workbench): if obj.isDerivedFrom("Path::Feature"): if "Profile" in selectedName or "Contour" in selectedName or "Dressup" in selectedName: self.appendContextMenu("", "Separator") - #self.appendContextMenu("", ["Set_StartPoint"]) - #self.appendContextMenu("", ["Set_EndPoint"]) + # self.appendContextMenu("", ["Set_StartPoint"]) + # self.appendContextMenu("", ["Set_EndPoint"]) for cmd in self.dressupcmds: self.appendContextMenu("", [cmd]) menuAppended = True @@ -182,10 +187,10 @@ class PathWorkbench (Workbench): if menuAppended: self.appendContextMenu("", "Separator") + Gui.addWorkbench(PathWorkbench()) FreeCAD.addImportType( "GCode (*.nc *.gc *.ncc *.ngc *.cnc *.tap *.gcode)", "PathGui") # FreeCAD.addExportType( # "GCode (*.nc *.gc *.ncc *.ngc *.cnc *.tap *.gcode)", "PathGui") - diff --git a/src/Mod/Path/PathScripts/PathGuiInit.py b/src/Mod/Path/PathScripts/PathGuiInit.py index 29272c0382..52116e608b 100644 --- a/src/Mod/Path/PathScripts/PathGuiInit.py +++ b/src/Mod/Path/PathScripts/PathGuiInit.py @@ -35,6 +35,7 @@ else: Processed = False + def Startup(): global Processed # pylint: disable=global-statement if not Processed: @@ -71,12 +72,13 @@ def Startup(): from PathScripts import PathSimpleCopy from PathScripts import PathSimulatorGui from PathScripts import PathStop + # from PathScripts import PathSurfaceGui # Added in initGui.py due to OCL dependency from PathScripts import PathToolController from PathScripts import PathToolControllerGui from PathScripts import PathToolLibraryManager from PathScripts import PathToolLibraryEditor from PathScripts import PathUtilsGui + # from PathScripts import PathWaterlineGui # Added in initGui.py due to OCL dependency Processed = True else: PathLog.debug('Skipping PathGui initialisation') - diff --git a/src/Mod/Path/PathScripts/PathSelection.py b/src/Mod/Path/PathScripts/PathSelection.py index 386ff1c29c..0cccde13b3 100644 --- a/src/Mod/Path/PathScripts/PathSelection.py +++ b/src/Mod/Path/PathScripts/PathSelection.py @@ -30,12 +30,14 @@ import PathScripts.PathUtils as PathUtils import math PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -#PathLog.trackModule(PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) + class PathBaseGate(object): # pylint: disable=no-init pass + class EGate(PathBaseGate): def allow(self, doc, obj, sub): # pylint: disable=unused-argument return sub and sub[0:4] == 'Edge' @@ -66,6 +68,7 @@ class ENGRAVEGate(PathBaseGate): return False + class CHAMFERGate(PathBaseGate): def allow(self, doc, obj, sub): # pylint: disable=unused-argument try: @@ -94,7 +97,7 @@ class DRILLGate(PathBaseGate): if hasattr(obj, "Shape") and sub: shape = obj.Shape subobj = shape.getElement(sub) - return PathUtils.isDrillable(shape, subobj, includePartials = True) + return PathUtils.isDrillable(shape, subobj, includePartials=True) else: return False @@ -159,6 +162,7 @@ class POCKETGate(PathBaseGate): return pocketable + class ADAPTIVEGate(PathBaseGate): def allow(self, doc, obj, sub): # pylint: disable=unused-argument @@ -170,6 +174,7 @@ class ADAPTIVEGate(PathBaseGate): return adaptive + class CONTOURGate(PathBaseGate): def allow(self, doc, obj, sub): # pylint: disable=unused-argument pass @@ -182,34 +187,42 @@ def contourselect(): FreeCADGui.Selection.addSelectionGate(CONTOURGate()) FreeCAD.Console.PrintWarning("Contour Select Mode\n") + def eselect(): FreeCADGui.Selection.addSelectionGate(EGate()) FreeCAD.Console.PrintWarning("Edge Select Mode\n") + def drillselect(): FreeCADGui.Selection.addSelectionGate(DRILLGate()) FreeCAD.Console.PrintWarning("Drilling Select Mode\n") + def engraveselect(): FreeCADGui.Selection.addSelectionGate(ENGRAVEGate()) FreeCAD.Console.PrintWarning("Engraving Select Mode\n") + def chamferselect(): FreeCADGui.Selection.addSelectionGate(CHAMFERGate()) FreeCAD.Console.PrintWarning("Deburr Select Mode\n") + def profileselect(): FreeCADGui.Selection.addSelectionGate(PROFILEGate()) FreeCAD.Console.PrintWarning("Profiling Select Mode\n") + def pocketselect(): FreeCADGui.Selection.addSelectionGate(POCKETGate()) FreeCAD.Console.PrintWarning("Pocketing Select Mode\n") + def adaptiveselect(): FreeCADGui.Selection.addSelectionGate(ADAPTIVEGate()) FreeCAD.Console.PrintWarning("Adaptive Select Mode\n") + def surfaceselect(): if(MESHGate() is True or PROFILEGate() is True): FreeCADGui.Selection.addSelectionGate(True) @@ -237,10 +250,12 @@ def select(op): opsel['Profile Edges'] = eselect opsel['Profile Faces'] = profileselect opsel['Surface'] = surfaceselect + opsel['Waterline'] = surfaceselect opsel['Adaptive'] = adaptiveselect opsel['Probe'] = probeselect return opsel[op] + def clear(): FreeCADGui.Selection.removeSelectionGate() FreeCAD.Console.PrintWarning("Free Select\n") diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index 0c83884bc7..19af63586d 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -82,138 +82,171 @@ class ObjectSurface(PathOp.ObjectOp): return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces def initOperation(self, obj): - '''initPocketOp(obj) ... create facing specific properties''' - obj.addProperty("App::PropertyEnumeration", "Algorithm", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The library to use to generate the path")) - obj.addProperty("App::PropertyEnumeration", "BoundBox", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "Should the operation be limited by the stock object or by the bounding box of the base object")) - obj.addProperty("App::PropertyEnumeration", "DropCutterDir", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction along which dropcutter lines are created")) - obj.addProperty("App::PropertyVectorDistance", "DropCutterExtraOffset", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box")) - obj.addProperty("App::PropertyEnumeration", "LayerMode", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The completion mode for the operation: single or multi-pass")) - obj.addProperty("App::PropertyEnumeration", "ScanType", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.")) - - obj.addProperty("App::PropertyDistance", "AngularDeflection", "Mesh Conversion", QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")) - obj.addProperty("App::PropertyDistance", "LinearDeflection", "Mesh Conversion", QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")) - - obj.addProperty("App::PropertyFloat", "CutterTilt", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")) - obj.addProperty("App::PropertyEnumeration", "RotationAxis", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "The model will be rotated around this axis.")) - obj.addProperty("App::PropertyFloat", "StartIndex", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Start index(angle) for rotational scan")) - obj.addProperty("App::PropertyFloat", "StopIndex", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")) - - obj.addProperty("App::PropertyInteger", "AvoidLastX_Faces", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")) - obj.addProperty("App::PropertyBool", "AvoidLastX_InternalFeatures", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")) - obj.addProperty("App::PropertyDistance", "BoundaryAdjustment", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")) - obj.addProperty("App::PropertyBool", "BoundaryEnforcement", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")) - obj.addProperty("App::PropertyDistance", "DepthOffset", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Z-axis offset from the surface of the object")) - obj.addProperty("App::PropertyEnumeration", "HandleMultipleFeatures", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")) - obj.addProperty("App::PropertyDistance", "InternalFeaturesAdjustment", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")) - obj.addProperty("App::PropertyBool", "InternalFeaturesCut", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")) - obj.addProperty("App::PropertyEnumeration", "ProfileEdges", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")) - obj.addProperty("App::PropertyDistance", "SampleInterval", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times")) - obj.addProperty("App::PropertyPercent", "StepOver", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Step over percentage of the drop cutter path")) - - obj.addProperty("App::PropertyVectorDistance", "CircularCenterCustom", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("PathOp", "The start point of this path")) - obj.addProperty("App::PropertyEnumeration", "CircularCenterAt", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("PathOp", "Choose what point to start the circular pattern: Center Of Mass, Center Of Boundbox, Xmin Ymin of boundbox, Custom.")) - obj.addProperty("App::PropertyEnumeration", "CutMode", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part: Climb(ClockWise) or Conventional(CounterClockWise)")) - obj.addProperty("App::PropertyEnumeration", "CutPattern", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Clearing pattern to use")) - obj.addProperty("App::PropertyFloat", "CutPatternAngle", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Yaw angle for certain clearing patterns")) - obj.addProperty("App::PropertyBool", "CutPatternReversed", "Surface Cut Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the order of the step-overs will be reversed; the operation will begin cutting the outer most line/arc, and work toward the inner most line/arc.")) - - obj.addProperty("App::PropertyBool", "OptimizeLinearPaths", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")) - obj.addProperty("App::PropertyBool", "OptimizeStepOverTransitions", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")) - obj.addProperty("App::PropertyBool", "CircularUseG2G3", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Convert co-planar arcs to G2/G3 gcode commands for `Circular` and `CircularZigZag` cut patterns.")) - obj.addProperty("App::PropertyDistance", "GapThreshold", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")) - obj.addProperty("App::PropertyString", "GapSizes", "Surface Optimization", QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")) - - obj.addProperty("App::PropertyBool", "IgnoreWaste", "Waste", QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore areas that proceed below specified depth.")) - obj.addProperty("App::PropertyFloat", "IgnoreWasteDepth", "Waste", QtCore.QT_TRANSLATE_NOOP("App::Property", "Depth used to identify waste areas to ignore.")) - obj.addProperty("App::PropertyBool", "ReleaseFromWaste", "Waste", QtCore.QT_TRANSLATE_NOOP("App::Property", "Cut through waste to depth at model edge, releasing the model.")) - - obj.addProperty("App::PropertyVectorDistance", "StartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("PathOp", "The start point of this path")) - obj.addProperty("App::PropertyBool", "UseStartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("PathOp", "Make True, if specifying a Start Point")) + '''initPocketOp(obj) ... create operation specific properties''' + self.initOpProperties(obj) # For debugging - obj.addProperty('App::PropertyString', 'AreaParams', 'Debugging') - obj.setEditorMode('AreaParams', 2) # hide - obj.addProperty("App::PropertyBool", "ShowTempObjects", "Debug", QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the temporary path construction objects will be shown.")) if PathLog.getLevel(PathLog.thisModule()) != 4: obj.setEditorMode('ShowTempObjects', 2) # hide - obj.Algorithm = ['OCL Dropcutter', 'OCL Waterline'] - obj.BoundBox = ['BaseBoundBox', 'Stock'] - obj.CircularCenterAt = ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'] - obj.CutMode = ['Conventional', 'Climb'] - obj.CutPattern = ['Line', 'ZigZag', 'Circular', 'CircularZigZag'] # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] - obj.DropCutterDir = ['X', 'Y'] - obj.HandleMultipleFeatures = ['Collectively', 'Individually'] - obj.LayerMode = ['Single-pass', 'Multi-pass'] - obj.ProfileEdges = ['None', 'Only', 'First', 'Last'] - obj.RotationAxis = ['X', 'Y'] - obj.ScanType = ['Planar', 'Rotational'] - if not hasattr(obj, 'DoNotSetDefaultValues'): self.setEditorProperties(obj) + def initOpProperties(self, obj): + '''initOpProperties(obj) ... create operation specific properties''' + + PROPS = [ + ("App::PropertyBool", "ShowTempObjects", "Debug", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")), + + ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values increase processing time a lot.")), + ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values do not increase processing time much.")), + + ("App::PropertyFloat", "CutterTilt", "Rotational", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")), + ("App::PropertyEnumeration", "DropCutterDir", "Rotational", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction along which dropcutter lines are created")), + ("App::PropertyVectorDistance", "DropCutterExtraOffset", "Rotational", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box")), + ("App::PropertyEnumeration", "RotationAxis", "Rotational", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The model will be rotated around this axis.")), + ("App::PropertyFloat", "StartIndex", "Rotational", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Start index(angle) for rotational scan")), + ("App::PropertyFloat", "StopIndex", "Rotational", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")), + + ("App::PropertyEnumeration", "ScanType", "Surface", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.")), + + ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")), + ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")), + ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")), + ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")), + ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")), + ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")), + ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")), + + ("App::PropertyEnumeration", "BoundBox", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")), + ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")), + ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the ciruclar pattern.")), + ("App::PropertyEnumeration", "CutMode", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")), + ("App::PropertyEnumeration", "CutPattern", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")), + ("App::PropertyFloat", "CutPatternAngle", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")), + ("App::PropertyBool", "CutPatternReversed", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")), + ("App::PropertyDistance", "DepthOffset", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")), + ("App::PropertyEnumeration", "LayerMode", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")), + ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")), + ("App::PropertyDistance", "SampleInterval", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")), + ("App::PropertyPercent", "StepOver", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), + + ("App::PropertyBool", "OptimizeLinearPaths", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")), + ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")), + ("App::PropertyBool", "CircularUseG2G3", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Convert co-planar arcs to G2/G3 gcode commands for `Circular` and `CircularZigZag` cut patterns.")), + ("App::PropertyDistance", "GapThreshold", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")), + ("App::PropertyString", "GapSizes", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")), + + ("App::PropertyVectorDistance", "StartPoint", "Start Point", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")), + ("App::PropertyBool", "UseStartPoint", "Start Point", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) + ] + + missing = list() + for (prtyp, nm, grp, tt) in PROPS: + if not hasattr(obj, nm): + obj.addProperty(prtyp, nm, grp, tt) + missing.append(nm) + + # Set enumeration lists for enumeration properties + if len(missing) > 0: + ENUMS = self._propertyEnumerations() + for n in ENUMS: + if n in missing: + cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) + exec(cmdStr) + self.addedAllProperties = True + def _propertyEnumerations(self): + # Enumeration lists for App::PropertyEnumeration properties + return { + 'BoundBox': ['BaseBoundBox', 'Stock'], + 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], + 'CutMode': ['Conventional', 'Climb'], + 'CutPattern': ['Line', 'Circular', 'CircularZigZag', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] + 'DropCutterDir': ['X', 'Y'], + 'HandleMultipleFeatures': ['Collectively', 'Individually'], + 'LayerMode': ['Single-pass', 'Multi-pass'], + 'ProfileEdges': ['None', 'Only', 'First', 'Last'], + 'RotationAxis': ['X', 'Y'], + 'ScanType': ['Planar', 'Rotational'] + } + def setEditorProperties(self, obj): # Used to hide inputs in properties list - if obj.Algorithm == 'OCL Dropcutter': - obj.setEditorMode('CutPattern', 0) - obj.setEditorMode('HandleMultipleFeatures', 0) - obj.setEditorMode('CircularCenterAt', 0) - obj.setEditorMode('CircularCenterCustom', 0) - obj.setEditorMode('CutPatternAngle', 0) - # obj.setEditorMode('BoundaryEnforcement', 0) - - if obj.ScanType == 'Planar': - obj.setEditorMode('DropCutterDir', 2) - obj.setEditorMode('DropCutterExtraOffset', 2) - obj.setEditorMode('RotationAxis', 2) # 2=hidden - obj.setEditorMode('StartIndex', 2) - obj.setEditorMode('StopIndex', 2) - obj.setEditorMode('CutterTilt', 2) - if obj.CutPattern == 'Circular' or obj.CutPattern == 'CircularZigZag': - obj.setEditorMode('CutPatternAngle', 2) - else: # if obj.CutPattern == 'Line' or obj.CutPattern == 'ZigZag': - obj.setEditorMode('CircularCenterAt', 2) - obj.setEditorMode('CircularCenterCustom', 2) - elif obj.ScanType == 'Rotational': - obj.setEditorMode('DropCutterDir', 0) - obj.setEditorMode('DropCutterExtraOffset', 0) - obj.setEditorMode('RotationAxis', 0) # 0=show & editable - obj.setEditorMode('StartIndex', 0) - obj.setEditorMode('StopIndex', 0) - obj.setEditorMode('CutterTilt', 0) - - elif obj.Algorithm == 'OCL Waterline': - obj.setEditorMode('DropCutterExtraOffset', 2) - obj.setEditorMode('DropCutterDir', 2) - obj.setEditorMode('HandleMultipleFeatures', 2) - obj.setEditorMode('CutPattern', 2) - obj.setEditorMode('CutPatternAngle', 2) - # obj.setEditorMode('BoundaryEnforcement', 2) - - # Disable IgnoreWaste feature - obj.setEditorMode('IgnoreWaste', 2) - obj.setEditorMode('IgnoreWasteDepth', 2) - obj.setEditorMode('ReleaseFromWaste', 2) + mode = 2 # 2=hidden + if obj.ScanType == 'Planar': + show = 0 + hide = 2 + # if obj.CutPattern in ['Line', 'ZigZag']: + if obj.CutPattern in ['Circular', 'CircularZigZag']: + show = 2 # hide + hide = 0 # show + obj.setEditorMode('CutPatternAngle', show) + obj.setEditorMode('CircularCenterAt', hide) + obj.setEditorMode('CircularCenterCustom', hide) + elif obj.ScanType == 'Rotational': + mode = 0 # show and editable + obj.setEditorMode('DropCutterDir', mode) + obj.setEditorMode('DropCutterExtraOffset', mode) + obj.setEditorMode('RotationAxis', mode) + obj.setEditorMode('StartIndex', mode) + obj.setEditorMode('StopIndex', mode) + obj.setEditorMode('CutterTilt', mode) def onChanged(self, obj, prop): if hasattr(self, 'addedAllProperties'): if self.addedAllProperties is True: - if prop == 'Algorithm': - self.setEditorProperties(obj) if prop == 'ScanType': self.setEditorProperties(obj) if prop == 'CutPattern': self.setEditorProperties(obj) def opOnDocumentRestored(self, obj): + self.initOpProperties(obj) + if PathLog.getLevel(PathLog.thisModule()) != 4: obj.setEditorMode('ShowTempObjects', 2) # hide else: obj.setEditorMode('ShowTempObjects', 0) # show - self.addedAllProperties = True + self.setEditorProperties(obj) def opSetDefaultValues(self, obj, job): @@ -221,8 +254,6 @@ class ObjectSurface(PathOp.ObjectOp): job = PathUtils.findParentJob(obj) obj.OptimizeLinearPaths = True - obj.IgnoreWaste = False - obj.ReleaseFromWaste = False obj.InternalFeaturesCut = True obj.OptimizeStepOverTransitions = False obj.CircularUseG2G3 = False @@ -241,7 +272,6 @@ class ObjectSurface(PathOp.ObjectOp): obj.CutPattern = 'Line' obj.HandleMultipleFeatures = 'Collectively' # 'Individually' obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' - obj.AreaParams = '' obj.GapSizes = 'No gaps identified.' obj.StepOver = 100 obj.CutPatternAngle = 0.0 @@ -303,8 +333,8 @@ class ObjectSurface(PathOp.ObjectOp): obj.CutterTilt = 90.0 # Limit sample interval - if obj.SampleInterval.Value < 0.001: - obj.SampleInterval.Value = 0.001 + if obj.SampleInterval.Value < 0.0001: + obj.SampleInterval.Value = 0.0001 PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.')) if obj.SampleInterval.Value > 25.4: obj.SampleInterval.Value = 25.4 @@ -366,9 +396,6 @@ class ObjectSurface(PathOp.ObjectOp): PathLog.info('\nBegin 3D Surface operation...') startTime = time.time() - # Disable(ignore) ReleaseFromWaste option(input) - obj.ReleaseFromWaste = False - # Identify parent Job JOB = PathUtils.findParentJob(obj) if JOB is None: @@ -389,12 +416,12 @@ class ObjectSurface(PathOp.ObjectOp): # ... and move cutter to clearance height and startpoint output = '' if obj.Comment != '': - output += '(' + str(obj.Comment) + ')\n' - output += '(' + obj.Label + ')\n' - output += '(Tool type: ' + str(obj.ToolController.Tool.ToolType) + ')\n' - output += '(Compensated Tool Path. Diameter: ' + str(obj.ToolController.Tool.Diameter) + ')\n' - output += '(Sample interval: ' + str(obj.SampleInterval.Value) + ')\n' - output += '(Step over %: ' + str(obj.StepOver) + ')\n' + self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {})) + self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {})) + self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {})) + self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {})) + self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {})) + self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {})) self.commandlist.append(Path.Command('N ({})'.format(output), {})) self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) if obj.UseStartPoint is True: @@ -480,16 +507,6 @@ class ObjectSurface(PathOp.ObjectOp): # ###### MAIN COMMANDS FOR OPERATION ###### - # If algorithm is `Waterline`, force certain property values - # Save initial value for restoration later. - if obj.Algorithm == 'OCL Waterline': - preCP = obj.CutPattern - preCPA = obj.CutPatternAngle - preRB = obj.BoundaryEnforcement - obj.CutPattern = 'Line' - obj.CutPatternAngle = 0.0 - obj.BoundaryEnforcement = False - # Begin processing obj.Base data and creating GCode # Process selected faces, if available pPM = self._preProcessModel(JOB, obj) @@ -520,12 +537,6 @@ class ObjectSurface(PathOp.ObjectOp): # Save gcode produced self.commandlist.extend(CMDS) - # If algorithm is `Waterline`, restore initial property values - if obj.Algorithm == 'OCL Waterline': - obj.CutPattern = preCP - obj.CutPatternAngle = preCPA - obj.BoundaryEnforcement = preRB - # ###### CLOSING COMMANDS FOR OPERATION ###### # Delete temporary objects @@ -750,7 +761,7 @@ class ObjectSurface(PathOp.ObjectOp): # Handle profile edges request if cont is True and obj.ProfileEdges != 'None': ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(obj, cfsL, ofstVal) + psOfst = self._extractFaceOffset(cfsL, ofstVal) if psOfst is not False: mPS = [psOfst] if obj.ProfileEdges == 'Only': @@ -760,7 +771,7 @@ class ObjectSurface(PathOp.ObjectOp): PathLog.error(' -Failed to create profile geometry for selected faces.') cont = False - if cont is True: + if cont: if self.showDebugObjects is True: T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') T.Shape = cfsL @@ -768,12 +779,12 @@ class ObjectSurface(PathOp.ObjectOp): self.tempGroup.addObject(T) ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(obj, cfsL, ofstVal) + faceOfstShp = self._extractFaceOffset(cfsL, ofstVal) if faceOfstShp is False: PathLog.error(' -Failed to create offset face.') cont = False - if cont is True: + if cont: lenIfL = len(ifL) if obj.InternalFeaturesCut is False: if lenIfL == 0: @@ -789,7 +800,7 @@ class ObjectSurface(PathOp.ObjectOp): C.purgeTouched() self.tempGroup.addObject(C) ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(obj, casL, ofstVal) + intOfstShp = self._extractFaceOffset(casL, ofstVal) mIFS.append(intOfstShp) # faceOfstShp = faceOfstShp.cut(intOfstShp) @@ -825,7 +836,7 @@ class ObjectSurface(PathOp.ObjectOp): if obj.ProfileEdges != 'None': ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(obj, outerFace, ofstVal) + psOfst = self._extractFaceOffset(outerFace, ofstVal) if psOfst is not False: if mPS is False: mPS = list() @@ -839,9 +850,9 @@ class ObjectSurface(PathOp.ObjectOp): PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) cont = False - if cont is True: + if cont: ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(obj, outerFace, ofstVal) + faceOfstShp = self._extractFaceOffset(outerFace, ofstVal) lenIfl = len(ifL) if obj.InternalFeaturesCut is False and lenIfl > 0: @@ -851,7 +862,7 @@ class ObjectSurface(PathOp.ObjectOp): casL = Part.makeCompound(ifL) ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(obj, casL, ofstVal) + intOfstShp = self._extractFaceOffset(casL, ofstVal) mIFS.append(intOfstShp) # faceOfstShp = faceOfstShp.cut(intOfstShp) @@ -907,7 +918,7 @@ class ObjectSurface(PathOp.ObjectOp): P.purgeTouched() self.tempGroup.addObject(P) - if cont is True: + if cont: if self.showDebugObjects is True: PathLog.debug('*** tmpVoidCompound') P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') @@ -916,12 +927,12 @@ class ObjectSurface(PathOp.ObjectOp): P.purgeTouched() self.tempGroup.addObject(P) ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True) - avdOfstShp = self._extractFaceOffset(obj, avoid, ofstVal) + avdOfstShp = self._extractFaceOffset(avoid, ofstVal) if avdOfstShp is False: PathLog.error('Failed to create collective offset avoid face.') cont = False - if cont is True: + if cont: avdShp = avdOfstShp if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: @@ -930,7 +941,7 @@ class ObjectSurface(PathOp.ObjectOp): else: ifc = intFEAT[0] ofstVal = self._calculateOffsetValue(obj, isHole=True) - ifOfstShp = self._extractFaceOffset(obj, ifc, ofstVal) + ifOfstShp = self._extractFaceOffset(ifc, ofstVal) if ifOfstShp is False: PathLog.error('Failed to create collective offset avoid internal features.') else: @@ -999,7 +1010,7 @@ class ObjectSurface(PathOp.ObjectOp): cont = False time.sleep(0.2) - if cont is True: + if cont: csFaceShape = self._getShapeSlice(baseEnv) if csFaceShape is False: PathLog.debug('_getShapeSlice(baseEnv) failed') @@ -1014,7 +1025,7 @@ class ObjectSurface(PathOp.ObjectOp): if cont is True and obj.ProfileEdges != 'None': PathLog.debug(' -Attempting profile geometry for model base.') ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(obj, csFaceShape, ofstVal) + psOfst = self._extractFaceOffset(csFaceShape, ofstVal) if psOfst is not False: if obj.ProfileEdges == 'Only': return (True, psOfst) @@ -1023,9 +1034,9 @@ class ObjectSurface(PathOp.ObjectOp): PathLog.error(' -Failed to create profile geometry.') cont = False - if cont is True: + if cont: ofstVal = self._calculateOffsetValue(obj, isHole) - faceOffsetShape = self._extractFaceOffset(obj, csFaceShape, ofstVal) + faceOffsetShape = self._extractFaceOffset(csFaceShape, ofstVal) if faceOffsetShape is False: PathLog.error('_extractFaceOffset() failed.') else: @@ -1076,7 +1087,7 @@ class ObjectSurface(PathOp.ObjectOp): WIRES.append((eArea, F.Wires[0], raised)) cont = False - if cont is True: + if cont: PathLog.debug(' -cont is True') # If only one wire and not checkEdges, return first wire if lenWrs == 1: @@ -1112,21 +1123,21 @@ class ObjectSurface(PathOp.ObjectOp): if isVoid is False: if isHole is True: offset = -1 * obj.InternalFeaturesAdjustment.Value - offset += self.radius # (self.radius + (tolrnc / 10.0)) + offset += self.radius + (tolrnc / 10.0) else: offset = -1 * obj.BoundaryAdjustment.Value if obj.BoundaryEnforcement is True: - offset += self.radius # (self.radius + (tolrnc / 10.0)) + offset += self.radius + (tolrnc / 10.0) else: - offset -= self.radius # (self.radius + (tolrnc / 10.0)) + offset -= self.radius + (tolrnc / 10.0) offset = 0.0 - offset else: offset = -1 * obj.BoundaryAdjustment.Value - offset += self.radius # (self.radius + (tolrnc / 10.0)) + offset += self.radius + (tolrnc / 10.0) return offset - def _extractFaceOffset(self, obj, fcShape, offset): + def _extractFaceOffset(self, fcShape, offset): '''_extractFaceOffset(fcShape, offset) ... internal function. Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' @@ -1151,10 +1162,6 @@ class ObjectSurface(PathOp.ObjectOp): area.add(fcShape) area.setParams(**areaParams) # set parameters - # Save parameters for debugging - # obj.AreaParams = str(area.getParams()) - # PathLog.debug("Area with params: {}".format(area.getParams())) - offsetShape = area.getShape() wCnt = len(offsetShape.Wires) if wCnt == 0: @@ -1420,26 +1427,15 @@ class ObjectSurface(PathOp.ObjectOp): if self.modelSTLs[m] is True: stl = ocl.STLSurf() - if obj.Algorithm == 'OCL Dropcutter': - for f in mesh.Facets: - p = f.Points[0] - q = f.Points[1] - r = f.Points[2] - t = ocl.Triangle(ocl.Point(p[0], p[1], p[2]), - ocl.Point(q[0], q[1], q[2]), - ocl.Point(r[0], r[1], r[2])) - stl.addTriangle(t) - self.modelSTLs[m] = stl - elif obj.Algorithm == 'OCL Waterline': - for f in mesh.Facets: - p = f.Points[0] - q = f.Points[1] - r = f.Points[2] - t = ocl.Triangle(ocl.Point(p[0], p[1], p[2] + obj.DepthOffset.Value), - ocl.Point(q[0], q[1], q[2] + obj.DepthOffset.Value), - ocl.Point(r[0], r[1], r[2] + obj.DepthOffset.Value)) - stl.addTriangle(t) - self.modelSTLs[m] = stl + for f in mesh.Facets: + p = f.Points[0] + q = f.Points[1] + r = f.Points[2] + t = ocl.Triangle(ocl.Point(p[0], p[1], p[2]), + ocl.Point(q[0], q[1], q[2]), + ocl.Point(r[0], r[1], r[2])) + stl.addTriangle(t) + self.modelSTLs[m] = stl return def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): @@ -1479,7 +1475,7 @@ class ObjectSurface(PathOp.ObjectOp): except Exception as eee: PathLog.error(str(eee)) - if cont is True: + if cont: stckWst = JOB.Stock.Shape.cut(envBB) if obj.BoundaryAdjustment > 0.0: cmpndFS = Part.makeCompound(faceShapes) @@ -1543,7 +1539,7 @@ class ObjectSurface(PathOp.ObjectOp): def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS): '''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)... This method applies any avoided faces or regions to the selected faces. - It then calls the correct scan method depending on the Algorithm and ScanType properties.''' + It then calls the correct scan method depending on the ScanType property.''' PathLog.debug('_processCutAreas()') final = list() @@ -1561,10 +1557,7 @@ class ObjectSurface(PathOp.ObjectOp): else: COMP = ADD - if obj.Algorithm == 'OCL Waterline': - final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - final.extend(self._waterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline - elif obj.ScanType == 'Planar': + if obj.ScanType == 'Planar': final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, 0)) elif obj.ScanType == 'Rotational': final.extend(self._processRotationalOp(obj, base, COMP)) @@ -1585,10 +1578,7 @@ class ObjectSurface(PathOp.ObjectOp): else: COMP = ADD - if obj.Algorithm == 'OCL Waterline': - final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - final.extend(self._waterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline - elif obj.ScanType == 'Planar': + if obj.ScanType == 'Planar': final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, fsi)) elif obj.ScanType == 'Rotational': final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP)) @@ -2366,7 +2356,11 @@ class ObjectSurface(PathOp.ObjectOp): p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) sp = (v1.X, v1.Y, 0.0) rad = p1.sub(COM).Length - tolrncAng = math.asin(space/rad) + spcRadRatio = space/rad + if spcRadRatio < 1.0: + tolrncAng = math.asin(spcRadRatio) + else: + tolrncAng = 0.999998 * math.pi X = COM.x + (rad * math.cos(tolrncAng)) Y = v1.Y - space # rad * math.sin(tolrncAng) @@ -2402,8 +2396,12 @@ class ObjectSurface(PathOp.ObjectOp): # Pop connected edge index values from arc segments index list iEi = EI.index(iE) iSi = EI.index(iS) - EI.pop(iEi) - EI.pop(iSi) + if iEi > iSi: + EI.pop(iEi) + EI.pop(iSi) + else: + EI.pop(iSi) + EI.pop(iEi) if len(EI) > 0: PRTS.append('BRK') chkGap = True @@ -2645,6 +2643,7 @@ class ObjectSurface(PathOp.ObjectOp): # Process each layer in depthparams prvLyrFirst = None prvLyrLast = None + lastPrvStpLast = None actvLyrs = 0 for lyr in range(0, lenDP): odd = True # ZigZag directional switch @@ -2653,8 +2652,12 @@ class ObjectSurface(PathOp.ObjectOp): actvSteps = 0 LYR = list() prvStpFirst = None + if lyr > 0: + if prvStpLast is not None: + lastPrvStpLast = prvStpLast prvStpLast = None lyrDep = depthparams[lyr] + PathLog.debug('Multi-pass lyrDep: {}'.format(round(lyrDep, 4))) # Cycle through step-over sections (line segments or arcs) for so in range(0, len(SCANDATA)): @@ -2695,6 +2698,7 @@ class ObjectSurface(PathOp.ObjectOp): # Manage step over transition and CircularZigZag direction if so > 0: + # PathLog.debug(' stepover index: {}'.format(so)) # Control ZigZag direction if obj.CutPattern == 'CircularZigZag': if odd is True: @@ -2702,6 +2706,8 @@ class ObjectSurface(PathOp.ObjectOp): else: odd = True # Control step over transition + if prvStpLast is None: + prvStpLast = lastPrvStpLast minTrnsHght = self._getMinSafeTravelHeight(safePDC, prvStpLast, first, minDep=None) # Check safe travel height against fullSTL transCmds.append(Path.Command('N (--Step {} transition)'.format(so), {})) transCmds.extend(self._stepTransitionCmds(obj, prvStpLast, first, minTrnsHght, tolrnc)) @@ -2714,6 +2720,7 @@ class ObjectSurface(PathOp.ObjectOp): for i in range(0, lenAdjPrts): prt = ADJPRTS[i] lenPrt = len(prt) + # PathLog.debug(' adj parts index - lenPrt: {} - {}'.format(i, lenPrt)) if prt == 'BRK' and prtsHasCmds is True: nxtStart = ADJPRTS[i + 1][0] minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart, minDep=None) # Check safe travel height against fullSTL @@ -3550,347 +3557,7 @@ class ObjectSurface(PathOp.ObjectOp): return output - # Main waterline functions - def _waterlineOp(self, JOB, obj, mdlIdx, subShp=None): - '''_waterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.''' - commands = [] - t_begin = time.time() - # JOB = PathUtils.findParentJob(obj) - base = JOB.Model.Group[mdlIdx] - bb = self.boundBoxes[mdlIdx] - stl = self.modelSTLs[mdlIdx] - - # Prepare global holdpoint and layerEndPnt containers - if self.holdPoint is None: - self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) - if self.layerEndPnt is None: - self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - # Set extra offset to diameter of cutter to allow cutter to move around perimeter of model - # Need to make DropCutterExtraOffset available for waterline algorithm - # cdeoX = obj.DropCutterExtraOffset.x - # cdeoY = obj.DropCutterExtraOffset.y - toolDiam = self.cutter.getDiameter() - cdeoX = 0.6 * toolDiam - cdeoY = 0.6 * toolDiam - - if subShp is None: - # Get correct boundbox - if obj.BoundBox == 'Stock': - BS = JOB.Stock - bb = BS.Shape.BoundBox - elif obj.BoundBox == 'BaseBoundBox': - BS = base - bb = base.Shape.BoundBox - - env = PathUtils.getEnvelope(partshape=BS.Shape, depthparams=self.depthParams) # Produces .Shape - - xmin = bb.XMin - xmax = bb.XMax - ymin = bb.YMin - ymax = bb.YMax - zmin = bb.ZMin - zmax = bb.ZMax - else: - xmin = subShp.BoundBox.XMin - xmax = subShp.BoundBox.XMax - ymin = subShp.BoundBox.YMin - ymax = subShp.BoundBox.YMax - zmin = subShp.BoundBox.ZMin - zmax = subShp.BoundBox.ZMax - - smplInt = obj.SampleInterval.Value - minSampInt = 0.001 # value is mm - if smplInt < minSampInt: - smplInt = minSampInt - - # Determine bounding box length for the OCL scan - bbLength = math.fabs(ymax - ymin) - numScanLines = int(math.ceil(bbLength / smplInt) + 1) # Number of lines - - # Compute number and size of stepdowns, and final depth - if obj.LayerMode == 'Single-pass': - depthparams = [obj.FinalDepth.Value] - else: - depthparams = [dp for dp in self.depthParams] - lenDP = len(depthparams) - - # Prepare PathDropCutter objects with STL data - safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx], - depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) - - # Scan the piece to depth at smplInt - oclScan = [] - oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines) - # oclScan = SCANS - lenOS = len(oclScan) - ptPrLn = int(lenOS / numScanLines) - - # Convert oclScan list of points to multi-dimensional list - scanLines = [] - for L in range(0, numScanLines): - scanLines.append([]) - for P in range(0, ptPrLn): - pi = L * ptPrLn + P - scanLines[L].append(oclScan[pi]) - lenSL = len(scanLines) - pntsPerLine = len(scanLines[0]) - PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line") - - # Extract Wl layers per depthparams - lyr = 0 - cmds = [] - layTime = time.time() - self.topoMap = [] - for layDep in depthparams: - cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) - commands.extend(cmds) - lyr += 1 - PathLog.debug("--All layer scans combined took " + str(time.time() - layTime) + " s") - return commands - - def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines): - '''_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ... - Perform OCL scan for waterline purpose.''' - pdc = ocl.PathDropCutter() # create a pdc - pdc.setSTL(stl) - pdc.setCutter(self.cutter) - pdc.setZ(fd) # set minimumZ (final / target depth value) - pdc.setSampling(smplInt) - - # Create line object as path - path = ocl.Path() # create an empty path object - for nSL in range(0, numScanLines): - yVal = ymin + (nSL * smplInt) - p1 = ocl.Point(xmin, yVal, fd) # start-point of line - p2 = ocl.Point(xmax, yVal, fd) # end-point of line - path.append(ocl.Line(p1, p2)) - # path.append(l) # add the line to the path - pdc.setPath(path) - pdc.run() # run drop-cutter on the path - - # return the list the points - return pdc.getCLPoints() - - def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine): - '''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.''' - commands = [] - cmds = [] - loopList = [] - self.topoMap = [] - # Create topo map from scanLines (highs and lows) - self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine) - # Add buffer lines and columns to topo map - self._bufferTopoMap(lenSL, pntsPerLine) - # Identify layer waterline from OCL scan - self._highlightWaterline(4, 9) - # Extract waterline and convert to gcode - loopList = self._extractWaterlines(obj, scanLines, lyr, layDep) - # save commands - for loop in loopList: - cmds = self._loopToGcode(obj, layDep, loop) - commands.extend(cmds) - return commands - - def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine): - '''_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.''' - topoMap = [] - for L in range(0, lenSL): - topoMap.append([]) - for P in range(0, pntsPerLine): - if scanLines[L][P].z > layDep: - topoMap[L].append(2) - else: - topoMap[L].append(0) - return topoMap - - def _bufferTopoMap(self, lenSL, pntsPerLine): - '''_bufferTopoMap(lenSL, pntsPerLine) ... Add buffer boarder of zeros to all sides to topoMap data.''' - pre = [0, 0] - post = [0, 0] - for p in range(0, pntsPerLine): - pre.append(0) - post.append(0) - for l in range(0, lenSL): - self.topoMap[l].insert(0, 0) - self.topoMap[l].append(0) - self.topoMap.insert(0, pre) - self.topoMap.append(post) - return True - - def _highlightWaterline(self, extraMaterial, insCorn): - '''_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.''' - TM = self.topoMap - lastPnt = len(TM[1]) - 1 - lastLn = len(TM) - 1 - highFlag = 0 - - # ("--Convert parallel data to ridges") - for lin in range(1, lastLn): - for pt in range(1, lastPnt): # Ignore first and last points - if TM[lin][pt] == 0: - if TM[lin][pt + 1] == 2: # step up - TM[lin][pt] = 1 - if TM[lin][pt - 1] == 2: # step down - TM[lin][pt] = 1 - - # ("--Convert perpendicular data to ridges and highlight ridges") - for pt in range(1, lastPnt): # Ignore first and last points - for lin in range(1, lastLn): - if TM[lin][pt] == 0: - highFlag = 0 - if TM[lin + 1][pt] == 2: # step up - TM[lin][pt] = 1 - if TM[lin - 1][pt] == 2: # step down - TM[lin][pt] = 1 - elif TM[lin][pt] == 2: - highFlag += 1 - if highFlag == 3: - if TM[lin - 1][pt - 1] < 2 or TM[lin - 1][pt + 1] < 2: - highFlag = 2 - else: - TM[lin - 1][pt] = extraMaterial - highFlag = 2 - - # ("--Square corners") - for pt in range(1, lastPnt): - for lin in range(1, lastLn): - if TM[lin][pt] == 1: # point == 1 - cont = True - if TM[lin + 1][pt] == 0: # forward == 0 - if TM[lin + 1][pt - 1] == 1: # forward left == 1 - if TM[lin][pt - 1] == 2: # left == 2 - TM[lin + 1][pt] = 1 # square the corner - cont = False - - if cont is True and TM[lin + 1][pt + 1] == 1: # forward right == 1 - if TM[lin][pt + 1] == 2: # right == 2 - TM[lin + 1][pt] = 1 # square the corner - cont = True - - if TM[lin - 1][pt] == 0: # back == 0 - if TM[lin - 1][pt - 1] == 1: # back left == 1 - if TM[lin][pt - 1] == 2: # left == 2 - TM[lin - 1][pt] = 1 # square the corner - cont = False - - if cont is True and TM[lin - 1][pt + 1] == 1: # back right == 1 - if TM[lin][pt + 1] == 2: # right == 2 - TM[lin - 1][pt] = 1 # square the corner - - # remove inside corners - for pt in range(1, lastPnt): - for lin in range(1, lastLn): - if TM[lin][pt] == 1: # point == 1 - if TM[lin][pt + 1] == 1: - if TM[lin - 1][pt + 1] == 1 or TM[lin + 1][pt + 1] == 1: - TM[lin][pt + 1] = insCorn - elif TM[lin][pt - 1] == 1: - if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1: - TM[lin][pt - 1] = insCorn - - return True - - def _extractWaterlines(self, obj, oclScan, lyr, layDep): - '''_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.''' - srch = True - lastPnt = len(self.topoMap[0]) - 1 - lastLn = len(self.topoMap) - 1 - maxSrchs = 5 - srchCnt = 1 - loopList = [] - loop = [] - loopNum = 0 - - if self.CutClimb is True: - lC = [-1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0] - pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] - else: - lC = [1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0] - pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] - - while srch is True: - srch = False - if srchCnt > maxSrchs: - PathLog.debug("Max search scans, " + str(maxSrchs) + " reached\nPossible incomplete waterline result!") - break - for L in range(1, lastLn): - for P in range(1, lastPnt): - if self.topoMap[L][P] == 1: - # start loop follow - srch = True - loopNum += 1 - loop = self._trackLoop(oclScan, lC, pC, L, P, loopNum) - self.topoMap[L][P] = 0 # Mute the starting point - loopList.append(loop) - srchCnt += 1 - PathLog.debug("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.") - return loopList - - def _trackLoop(self, oclScan, lC, pC, L, P, loopNum): - '''_trackLoop(oclScan, lC, pC, L, P, loopNum) ... Track the loop direction.''' - loop = [oclScan[L - 1][P - 1]] # Start loop point list - cur = [L, P, 1] - prv = [L, P - 1, 1] - nxt = [L, P + 1, 1] - follow = True - ptc = 0 - ptLmt = 200000 - while follow is True: - ptc += 1 - if ptc > ptLmt: - PathLog.debug("Loop number " + str(loopNum) + " at [" + str(nxt[0]) + ", " + str(nxt[1]) + "] pnt count exceeds, " + str(ptLmt) + ". Stopped following loop.") - break - nxt = self._findNextWlPoint(lC, pC, cur[0], cur[1], prv[0], prv[1]) # get next point - loop.append(oclScan[nxt[0] - 1][nxt[1] - 1]) # add it to loop point list - self.topoMap[nxt[0]][nxt[1]] = nxt[2] # Mute the point, if not Y stem - if nxt[0] == L and nxt[1] == P: # check if loop complete - follow = False - elif nxt[0] == cur[0] and nxt[1] == cur[1]: # check if line cannot be detected - follow = False - prv = cur - cur = nxt - return loop - - def _findNextWlPoint(self, lC, pC, cl, cp, pl, pp): - '''_findNextWlPoint(lC, pC, cl, cp, pl, pp) ... - Find the next waterline point in the point cloud layer provided.''' - dl = cl - pl - dp = cp - pp - num = 0 - i = 3 - s = 0 - mtch = 0 - found = False - while mtch < 8: # check all 8 points around current point - if lC[i] == dl: - if pC[i] == dp: - s = i - 3 - found = True - # Check for y branch where current point is connection between branches - for y in range(1, mtch): - if lC[i + y] == dl: - if pC[i + y] == dp: - num = 1 - break - break - i += 1 - mtch += 1 - if found is False: - # ("_findNext: No start point found.") - return [cl, cp, num] - - for r in range(0, 8): - l = cl + lC[s + r] - p = cp + pC[s + r] - if self.topoMap[l][p] == 1: - return [l, p, num] - - # ("_findNext: No next pnt found") - return [cl, cp, num] - - def _loopToGcode(self, obj, layDep, loop): '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.''' # generate the path commands output = [] @@ -3971,54 +3638,6 @@ class ObjectSurface(PathOp.ObjectOp): cmds.append(Path.Command('G0', {'Z': p2.z, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed return cmds - def holdStopEndCmds(self, obj, p2, txt): - '''holdStopEndCmds(obj, p2, txt) ... Gcode commands to be executed at end of hold stop.''' - cmds = [] - msg = 'N (' + txt + ')' - cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel - cmds.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) # Raise cutter rapid to zMax in line of travel - # cmds.append(Path.Command('G0', {'X': p2.x, 'Y': p2.y, 'F': self.horizRapid})) # horizontal rapid to current XY coordinate - return cmds - - def subsectionCLP(self, CLP, xmin, ymin, xmax, ymax): - '''subsectionCLP(CLP, xmin, ymin, xmax, ymax) ... - This function returns a subsection of the CLP scan, limited to the min/max values supplied.''' - section = list() - lenCLP = len(CLP) - for i in range(0, lenCLP): - if CLP[i].x < xmax: - if CLP[i].y < ymax: - if CLP[i].x > xmin: - if CLP[i].y > ymin: - section.append(CLP[i]) - return section - - def getMaxHeightBetweenPoints(self, finalDepth, p1, p2, cutter, CLP): - ''' getMaxHeightBetweenPoints(finalDepth, p1, p2, cutter, CLP) ... - This function connects two HOLD points with line. - Each point within the subsection point list is tested to determinie if it is under cutter. - Points determined to be under the cutter on line are tested for z height. - The highest z point is the requirement for clearance between p1 and p2, and returned as zMax with 2 mm extra. - ''' - dx = (p2.x - p1.x) - if dx == 0.0: - dx = 0.00001 # Need to employ a global tolerance here - m = (p2.y - p1.y) / dx - b = p1.y - (m * p1.x) - - avoidTool = round(cutter * 0.75, 1) # 1/2 diam. of cutter is theoretically safe, but 3/4 diam is used for extra clearance - zMax = finalDepth - lenCLP = len(CLP) - for i in range(0, lenCLP): - mSqrd = m**2 - if mSqrd < 0.0000001: # Need to employ a global tolerance here - mSqrd = 0.0000001 - perpDist = math.sqrt((CLP[i].y - (m * CLP[i].x) - b)**2 / (1 + 1 / (mSqrd))) - if perpDist < avoidTool: # if point within cutter reach on line of travel, test z height and update as needed - if CLP[i].z > zMax: - zMax = CLP[i].z - return zMax + 2.0 - def resetOpVariables(self, all=True): '''resetOpVariables() ... Reset class variables used for instance of operation.''' self.holdPoint = None @@ -4131,31 +3750,6 @@ class ObjectSurface(PathOp.ObjectOp): PathLog.error('Unable to set OCL cutter.') return False - def determineVectDirect(self, pnt, nxt, travVect): - if nxt.x == pnt.x: - travVect.x = 0 - elif nxt.x < pnt.x: - travVect.x = -1 - else: - travVect.x = 1 - - if nxt.y == pnt.y: - travVect.y = 0 - elif nxt.y < pnt.y: - travVect.y = -1 - else: - travVect.y = 1 - return travVect - - def determineLineOfTravel(self, travVect): - if travVect.x == 0 and travVect.y != 0: - lineOfTravel = "Y" - elif travVect.y == 0 and travVect.x != 0: - lineOfTravel = "X" - else: - lineOfTravel = "O" # used for turns - return lineOfTravel - def _getMinSafeTravelHeight(self, pdc, p1, p2, minDep=None): A = (p1.x, p1.y) B = (p2.x, p2.y) @@ -4173,7 +3767,6 @@ class ObjectSurface(PathOp.ObjectOp): def SetupProperties(): ''' SetupProperties() ... Return list of properties required for operation.''' setup = [] - setup.append('Algorithm') setup.append('AvoidLastX_Faces') setup.append('AvoidLastX_InternalFeatures') setup.append('BoundBox') @@ -4208,12 +3801,7 @@ def SetupProperties(): setup.append('AngularDeflection') setup.append('LinearDeflection') # For debugging - setup.append('AreaParams') setup.append('ShowTempObjects') - # Targeted for possible removal - setup.append('IgnoreWaste') - setup.append('IgnoreWasteDepth') - setup.append('ReleaseFromWaste') return setup diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py index 40feff2c70..41f11f6007 100644 --- a/src/Mod/Path/PathScripts/PathSurfaceGui.py +++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py @@ -39,89 +39,120 @@ __doc__ = "Surface operation page controller and command implementation." class TaskPanelOpPage(PathOpGui.TaskPanelPage): '''Page controller class for the Surface operation.''' + def initPage(self, obj): + self.setTitle("3D Surface") + self.updateVisibility() + def getForm(self): '''getForm() ... returns UI''' return FreeCADGui.PySideUic.loadUi(":/panels/PageOpSurfaceEdit.ui") def getFields(self, obj): '''getFields(obj) ... transfers values from UI to obj's proprties''' + self.updateToolController(obj, self.form.toolController) + self.updateCoolant(obj, self.form.coolantController) + PathGui.updateInputField(obj, 'DepthOffset', self.form.depthOffset) PathGui.updateInputField(obj, 'SampleInterval', self.form.sampleInterval) - if obj.StepOver != self.form.stepOver.value(): - obj.StepOver = self.form.stepOver.value() - - if obj.Algorithm != str(self.form.algorithmSelect.currentText()): - obj.Algorithm = str(self.form.algorithmSelect.currentText()) - if obj.BoundBox != str(self.form.boundBoxSelect.currentText()): obj.BoundBox = str(self.form.boundBoxSelect.currentText()) - if obj.DropCutterDir != str(self.form.dropCutterDirSelect.currentText()): - obj.DropCutterDir = str(self.form.dropCutterDirSelect.currentText()) + if obj.ScanType != str(self.form.scanType.currentText()): + obj.ScanType = str(self.form.scanType.currentText()) + + if obj.StepOver != self.form.stepOver.value(): + obj.StepOver = self.form.stepOver.value() + + if obj.LayerMode != str(self.form.layerMode.currentText()): + obj.LayerMode = str(self.form.layerMode.currentText()) + + if obj.CutPattern != str(self.form.cutPattern.currentText()): + obj.CutPattern = str(self.form.cutPattern.currentText()) obj.DropCutterExtraOffset.x = FreeCAD.Units.Quantity(self.form.boundBoxExtraOffsetX.text()).Value obj.DropCutterExtraOffset.y = FreeCAD.Units.Quantity(self.form.boundBoxExtraOffsetY.text()).Value + if obj.DropCutterDir != str(self.form.dropCutterDirSelect.currentText()): + obj.DropCutterDir = str(self.form.dropCutterDirSelect.currentText()) + + PathGui.updateInputField(obj, 'DepthOffset', self.form.depthOffset) + PathGui.updateInputField(obj, 'SampleInterval', self.form.sampleInterval) + + if obj.UseStartPoint != self.form.useStartPoint.isChecked(): + obj.UseStartPoint = self.form.useStartPoint.isChecked() + if obj.OptimizeLinearPaths != self.form.optimizeEnabled.isChecked(): obj.OptimizeLinearPaths = self.form.optimizeEnabled.isChecked() - self.updateToolController(obj, self.form.toolController) - self.updateCoolant(obj, self.form.coolantController) + if obj.OptimizeStepOverTransitions != self.form.optimizeStepOverTransitions.isChecked(): + obj.OptimizeStepOverTransitions = self.form.optimizeStepOverTransitions.isChecked() def setFields(self, obj): '''setFields(obj) ... transfers obj's property values to UI''' - self.selectInComboBox(obj.Algorithm, self.form.algorithmSelect) + self.setupToolController(obj, self.form.toolController) + self.setupCoolant(obj, self.form.coolantController) self.selectInComboBox(obj.BoundBox, self.form.boundBoxSelect) + self.selectInComboBox(obj.ScanType, self.form.scanType) + self.selectInComboBox(obj.LayerMode, self.form.layerMode) + self.selectInComboBox(obj.CutPattern, self.form.cutPattern) + self.form.boundBoxExtraOffsetX.setText(FreeCAD.Units.Quantity(obj.DropCutterExtraOffset.x, FreeCAD.Units.Length).UserString) + self.form.boundBoxExtraOffsetY.setText(FreeCAD.Units.Quantity(obj.DropCutterExtraOffset.y, FreeCAD.Units.Length).UserString) self.selectInComboBox(obj.DropCutterDir, self.form.dropCutterDirSelect) - - self.form.boundBoxExtraOffsetX.setText(str(obj.DropCutterExtraOffset.x)) - self.form.boundBoxExtraOffsetY.setText(str(obj.DropCutterExtraOffset.y)) self.form.depthOffset.setText(FreeCAD.Units.Quantity(obj.DepthOffset.Value, FreeCAD.Units.Length).UserString) - self.form.sampleInterval.setText(str(obj.SampleInterval)) self.form.stepOver.setValue(obj.StepOver) + self.form.sampleInterval.setText(FreeCAD.Units.Quantity(obj.SampleInterval.Value, FreeCAD.Units.Length).UserString) + + if obj.UseStartPoint: + self.form.useStartPoint.setCheckState(QtCore.Qt.Checked) + else: + self.form.useStartPoint.setCheckState(QtCore.Qt.Unchecked) if obj.OptimizeLinearPaths: self.form.optimizeEnabled.setCheckState(QtCore.Qt.Checked) else: self.form.optimizeEnabled.setCheckState(QtCore.Qt.Unchecked) - self.setupToolController(obj, self.form.toolController) - self.setupCoolant(obj, self.form.coolantController) + if obj.OptimizeStepOverTransitions: + self.form.optimizeStepOverTransitions.setCheckState(QtCore.Qt.Checked) + else: + self.form.optimizeStepOverTransitions.setCheckState(QtCore.Qt.Unchecked) def getSignalsForUpdate(self, obj): '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' signals = [] signals.append(self.form.toolController.currentIndexChanged) - signals.append(self.form.algorithmSelect.currentIndexChanged) + signals.append(self.form.coolantController.currentIndexChanged) signals.append(self.form.boundBoxSelect.currentIndexChanged) - signals.append(self.form.dropCutterDirSelect.currentIndexChanged) + signals.append(self.form.scanType.currentIndexChanged) + signals.append(self.form.layerMode.currentIndexChanged) + signals.append(self.form.cutPattern.currentIndexChanged) signals.append(self.form.boundBoxExtraOffsetX.editingFinished) signals.append(self.form.boundBoxExtraOffsetY.editingFinished) - signals.append(self.form.sampleInterval.editingFinished) - signals.append(self.form.stepOver.editingFinished) + signals.append(self.form.dropCutterDirSelect.currentIndexChanged) signals.append(self.form.depthOffset.editingFinished) + signals.append(self.form.stepOver.editingFinished) + signals.append(self.form.sampleInterval.editingFinished) + signals.append(self.form.useStartPoint.stateChanged) signals.append(self.form.optimizeEnabled.stateChanged) - signals.append(self.form.coolantController.currentIndexChanged) + signals.append(self.form.optimizeStepOverTransitions.stateChanged) return signals def updateVisibility(self): - if self.form.algorithmSelect.currentText() == "OCL Dropcutter": - self.form.boundBoxExtraOffsetX.setEnabled(True) - self.form.boundBoxExtraOffsetY.setEnabled(True) - self.form.boundBoxSelect.setEnabled(True) - self.form.dropCutterDirSelect.setEnabled(True) - self.form.stepOver.setEnabled(True) - else: + if self.form.scanType.currentText() == "Planar": + self.form.cutPattern.setEnabled(True) self.form.boundBoxExtraOffsetX.setEnabled(False) self.form.boundBoxExtraOffsetY.setEnabled(False) - self.form.boundBoxSelect.setEnabled(False) self.form.dropCutterDirSelect.setEnabled(False) - self.form.stepOver.setEnabled(False) + else: + self.form.cutPattern.setEnabled(False) + self.form.boundBoxExtraOffsetX.setEnabled(True) + self.form.boundBoxExtraOffsetY.setEnabled(True) + self.form.dropCutterDirSelect.setEnabled(True) def registerSignalHandlers(self, obj): - self.form.algorithmSelect.currentIndexChanged.connect(self.updateVisibility) + self.form.scanType.currentIndexChanged.connect(self.updateVisibility) Command = PathOpGui.SetupOperation('Surface', diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py new file mode 100644 index 0000000000..bdc4fda076 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -0,0 +1,3518 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 Russell Johnson (russ4262) * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +import FreeCAD +import MeshPart +import Path +import PathScripts.PathLog as PathLog +import PathScripts.PathUtils as PathUtils +import PathScripts.PathOp as PathOp + +from PySide import QtCore +import time +import math +import Part +import Draft + +if FreeCAD.GuiUp: + import FreeCADGui + +__title__ = "Path Waterline Operation" +__author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Class and implementation of Mill Facing operation." + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) + + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +# OCL must be installed +try: + import ocl +except ImportError: + FreeCAD.Console.PrintError( + translate("Path_Waterline", "This operation requires OpenCamLib to be installed.") + "\n") + import sys + sys.exit(translate("Path_Waterline", "This operation requires OpenCamLib to be installed.")) + + +class ObjectWaterline(PathOp.ObjectOp): + '''Proxy object for Surfacing operation.''' + + def baseObject(self): + '''baseObject() ... returns super of receiver + Used to call base implementation in overwritten functions.''' + return super(self.__class__, self) + + def opFeatures(self, obj): + '''opFeatures(obj) ... return all standard features and edges based geomtries''' + return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces + + def initOperation(self, obj): + '''initPocketOp(obj) ... + Initialize the operation - property creation and property editor status.''' + self.initOpProperties(obj) + + # For debugging + if PathLog.getLevel(PathLog.thisModule()) != 4: + obj.setEditorMode('ShowTempObjects', 2) # hide + + if not hasattr(obj, 'DoNotSetDefaultValues'): + self.setEditorProperties(obj) + + def initOpProperties(self, obj): + '''initOpProperties(obj) ... create operation specific properties''' + PROPS = [ + ("App::PropertyBool", "ShowTempObjects", "Debug", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")), + + ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")), + ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")), + + ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")), + ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")), + ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")), + ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")), + ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")), + ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")), + ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")), + + ("App::PropertyEnumeration", "Algorithm", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the algorithm to use: OCL Dropcutter*, or Experimental.")), + ("App::PropertyEnumeration", "BoundBox", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")), + ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")), + ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the ciruclar pattern.")), + ("App::PropertyEnumeration", "ClearLastLayer", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set to clear last layer in a `Multi-pass` operation.")), + ("App::PropertyEnumeration", "CutMode", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")), + ("App::PropertyEnumeration", "CutPattern", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")), + ("App::PropertyFloat", "CutPatternAngle", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")), + ("App::PropertyBool", "CutPatternReversed", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")), + ("App::PropertyDistance", "DepthOffset", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")), + ("App::PropertyEnumeration", "LayerMode", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")), + ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")), + ("App::PropertyDistance", "SampleInterval", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")), + ("App::PropertyPercent", "StepOver", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), + + ("App::PropertyBool", "OptimizeLinearPaths", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")), + ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")), + ("App::PropertyDistance", "GapThreshold", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")), + ("App::PropertyString", "GapSizes", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")), + + ("App::PropertyVectorDistance", "StartPoint", "Start Point", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")), + ("App::PropertyBool", "UseStartPoint", "Start Point", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) + ] + + missing = list() + for (prtyp, nm, grp, tt) in PROPS: + if not hasattr(obj, nm): + obj.addProperty(prtyp, nm, grp, tt) + missing.append(nm) + + # Set enumeration lists for enumeration properties + if len(missing) > 0: + ENUMS = self._propertyEnumerations() + for n in ENUMS: + if n in missing: + cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) + exec(cmdStr) + + self.addedAllProperties = True + + def _propertyEnumerations(self): + # Enumeration lists for App::PropertyEnumeration properties + return { + 'Algorithm': ['OCL Dropcutter', 'Experimental'], + 'BoundBox': ['BaseBoundBox', 'Stock'], + 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], + 'ClearLastLayer': ['Off', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'], + 'CutMode': ['Conventional', 'Climb'], + 'CutPattern': ['None', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] + 'HandleMultipleFeatures': ['Collectively', 'Individually'], + 'LayerMode': ['Single-pass', 'Multi-pass'], + 'ProfileEdges': ['None', 'Only', 'First', 'Last'], + } + + def setEditorProperties(self, obj): + # Used to hide inputs in properties list + show = 0 + hide = 2 + cpShow = 0 + expMode = 0 + obj.setEditorMode('BoundaryEnforcement', hide) + obj.setEditorMode('ProfileEdges', hide) + obj.setEditorMode('InternalFeaturesAdjustment', hide) + obj.setEditorMode('InternalFeaturesCut', hide) + obj.setEditorMode('GapSizes', hide) + obj.setEditorMode('GapThreshold', hide) + obj.setEditorMode('AvoidLastX_Faces', hide) + obj.setEditorMode('AvoidLastX_InternalFeatures', hide) + obj.setEditorMode('BoundaryAdjustment', hide) + obj.setEditorMode('HandleMultipleFeatures', hide) + if hasattr(obj, 'EnableRotation'): + obj.setEditorMode('EnableRotation', hide) + if obj.CutPattern == 'None': + show = 2 + hide = 2 + cpShow = 2 + # elif obj.CutPattern in ['Line', 'ZigZag']: + # show = 0 + # hide = 2 + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + show = 2 # hide + hide = 0 # show + # obj.setEditorMode('StepOver', cpShow) + obj.setEditorMode('CutPatternAngle', show) + obj.setEditorMode('CircularCenterAt', hide) + obj.setEditorMode('CircularCenterCustom', hide) + if obj.Algorithm == 'Experimental': + expMode = 2 + obj.setEditorMode('SampleInterval', expMode) + obj.setEditorMode('LinearDeflection', expMode) + obj.setEditorMode('AngularDeflection', expMode) + + def onChanged(self, obj, prop): + if hasattr(self, 'addedAllProperties'): + if self.addedAllProperties is True: + if prop in ['Algorithm', 'CutPattern']: + self.setEditorProperties(obj) + + def opOnDocumentRestored(self, obj): + self.initOpProperties(obj) + + if PathLog.getLevel(PathLog.thisModule()) != 4: + obj.setEditorMode('ShowTempObjects', 2) # hide + else: + obj.setEditorMode('ShowTempObjects', 0) # show + + self.setEditorProperties(obj) + + def opSetDefaultValues(self, obj, job): + '''opSetDefaultValues(obj, job) ... initialize defaults''' + job = PathUtils.findParentJob(obj) + + obj.OptimizeLinearPaths = True + obj.InternalFeaturesCut = True + obj.OptimizeStepOverTransitions = False + obj.BoundaryEnforcement = True + obj.UseStartPoint = False + obj.AvoidLastX_InternalFeatures = True + obj.CutPatternReversed = False + obj.StartPoint.x = 0.0 + obj.StartPoint.y = 0.0 + obj.StartPoint.z = obj.ClearanceHeight.Value + obj.Algorithm = 'OCL Dropcutter' + obj.ProfileEdges = 'None' + obj.LayerMode = 'Single-pass' + obj.CutMode = 'Conventional' + obj.CutPattern = 'None' + obj.HandleMultipleFeatures = 'Collectively' # 'Individually' + obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' + obj.GapSizes = 'No gaps identified.' + obj.ClearLastLayer = 'Off' + obj.StepOver = 100 + obj.CutPatternAngle = 0.0 + obj.DepthOffset.Value = 0.0 + obj.SampleInterval.Value = 1.0 + obj.BoundaryAdjustment.Value = 0.0 + obj.InternalFeaturesAdjustment.Value = 0.0 + obj.AvoidLastX_Faces = 0 + obj.CircularCenterCustom.x = 0.0 + obj.CircularCenterCustom.y = 0.0 + obj.CircularCenterCustom.z = 0.0 + obj.GapThreshold.Value = 0.005 + obj.LinearDeflection.Value = 0.0001 + obj.AngularDeflection.Value = 0.25 + # For debugging + obj.ShowTempObjects = False + + # need to overwrite the default depth calculations for facing + d = None + if job: + if job.Stock: + d = PathUtils.guessDepths(job.Stock.Shape, None) + PathLog.debug("job.Stock exists") + else: + PathLog.debug("job.Stock NOT exist") + else: + PathLog.debug("job NOT exist") + + if d is not None: + obj.OpFinalDepth.Value = d.final_depth + obj.OpStartDepth.Value = d.start_depth + else: + obj.OpFinalDepth.Value = -10 + obj.OpStartDepth.Value = 10 + + PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value)) + PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value)) + + def opApplyPropertyLimits(self, obj): + '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.''' + # Limit sample interval + if obj.SampleInterval.Value < 0.001: + obj.SampleInterval.Value = 0.001 + PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.001 to 25.4 millimeters.')) + if obj.SampleInterval.Value > 25.4: + obj.SampleInterval.Value = 25.4 + PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.001 to 25.4 millimeters.')) + + # Limit cut pattern angle + if obj.CutPatternAngle < -360.0: + obj.CutPatternAngle = 0.0 + PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +-360 degrees.')) + if obj.CutPatternAngle >= 360.0: + obj.CutPatternAngle = 0.0 + PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +- 360 degrees.')) + + # Limit StepOver to natural number percentage + if obj.StepOver > 100: + obj.StepOver = 100 + if obj.StepOver < 1: + obj.StepOver = 1 + + # Limit AvoidLastX_Faces to zero and positive values + if obj.AvoidLastX_Faces < 0: + obj.AvoidLastX_Faces = 0 + PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Only zero or positive values permitted.')) + if obj.AvoidLastX_Faces > 100: + obj.AvoidLastX_Faces = 100 + PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.')) + + def opExecute(self, obj): + '''opExecute(obj) ... process surface operation''' + PathLog.track() + + self.modelSTLs = list() + self.safeSTLs = list() + self.modelTypes = list() + self.boundBoxes = list() + self.profileShapes = list() + self.collectiveShapes = list() + self.individualShapes = list() + self.avoidShapes = list() + self.geoTlrnc = None + self.tempGroup = None + self.CutClimb = False + self.closedGap = False + self.gaps = [0.1, 0.2, 0.3] + CMDS = list() + modelVisibility = list() + FCAD = FreeCAD.ActiveDocument + + # Set debugging behavior + self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects + self.showDebugObjects = obj.ShowTempObjects + deleteTempsFlag = True # Set to False for debugging + if PathLog.getLevel(PathLog.thisModule()) == 4: + deleteTempsFlag = False + else: + self.showDebugObjects = False + + # mark beginning of operation and identify parent Job + PathLog.info('\nBegin Waterline operation...') + startTime = time.time() + + # Identify parent Job + JOB = PathUtils.findParentJob(obj) + if JOB is None: + PathLog.error(translate('PathWaterline', "No JOB")) + return + self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin + + # set cut mode; reverse as needed + if obj.CutMode == 'Climb': + self.CutClimb = True + if obj.CutPatternReversed is True: + if self.CutClimb is True: + self.CutClimb = False + else: + self.CutClimb = True + + # Begin GCode for operation with basic information + # ... and move cutter to clearance height and startpoint + output = '' + if obj.Comment != '': + self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {})) + self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {})) + self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {})) + self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {})) + self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {})) + self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {})) + self.commandlist.append(Path.Command('N ({})'.format(output), {})) + self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + if obj.UseStartPoint is True: + self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid})) + + # Instantiate additional class operation variables + self.resetOpVariables() + + # Impose property limits + self.opApplyPropertyLimits(obj) + + # Create temporary group for temporary objects, removing existing + # if self.showDebugObjects is True: + tempGroupName = 'tempPathWaterlineGroup' + if FCAD.getObject(tempGroupName): + for to in FCAD.getObject(tempGroupName).Group: + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName) # remove temp directory if already exists + if FCAD.getObject(tempGroupName + '001'): + for to in FCAD.getObject(tempGroupName + '001').Group: + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists + tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName) + tempGroupName = tempGroup.Name + self.tempGroup = tempGroup + tempGroup.purgeTouched() + # Add temp object to temp group folder with following code: + # ... self.tempGroup.addObject(OBJ) + + # Setup cutter for OCL and cutout value for operation - based on tool controller properties + self.cutter = self.setOclCutter(obj) + self.safeCutter = self.setOclCutter(obj, safe=True) + if self.cutter is False or self.safeCutter is False: + PathLog.error(translate('PathWaterline', "Canceling Waterline operation. Error creating OCL cutter.")) + return + toolDiam = self.cutter.getDiameter() + self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0)) + self.radius = toolDiam / 2.0 + self.gaps = [toolDiam, toolDiam, toolDiam] + + # Get height offset values for later use + self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value + self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value + + # Set deflection values for mesh generation + useDGT = False + try: # try/except is for Path Jobs created before GeometryTolerance + self.geoTlrnc = JOB.GeometryTolerance.Value + if self.geoTlrnc == 0.0: + useDGT = True + except AttributeError as ee: + PathLog.warning('{}\nPlease set Job.GeometryTolerance to an acceptable value. Using PathPreferences.defaultGeometryTolerance().'.format(ee)) + useDGT = True + if useDGT: + import PathScripts.PathPreferences as PathPreferences + self.geoTlrnc = PathPreferences.defaultGeometryTolerance() + + # Calculate default depthparams for operation + self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) + self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0 + + # make circle for workplane + self.wpc = Part.makeCircle(2.0) + + # Save model visibilities for restoration + if FreeCAD.GuiUp: + for m in range(0, len(JOB.Model.Group)): + mNm = JOB.Model.Group[m].Name + modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility) + + # Setup STL, model type, and bound box containers for each model in Job + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + self.modelSTLs.append(False) + self.safeSTLs.append(False) + self.profileShapes.append(False) + # Set bound box + if obj.BoundBox == 'BaseBoundBox': + if M.TypeId.startswith('Mesh'): + self.modelTypes.append('M') # Mesh + self.boundBoxes.append(M.Mesh.BoundBox) + else: + self.modelTypes.append('S') # Solid + self.boundBoxes.append(M.Shape.BoundBox) + elif obj.BoundBox == 'Stock': + self.modelTypes.append('S') # Solid + self.boundBoxes.append(JOB.Stock.Shape.BoundBox) + + # ###### MAIN COMMANDS FOR OPERATION ###### + + # Begin processing obj.Base data and creating GCode + # Process selected faces, if available + pPM = self._preProcessModel(JOB, obj) + if pPM is False: + PathLog.error('Unable to pre-process obj.Base.') + else: + (FACES, VOIDS) = pPM + + # Create OCL.stl model objects + if obj.Algorithm == 'OCL Dropcutter': + self._prepareModelSTLs(JOB, obj) + PathLog.debug('obj.LinearDeflection.Value: {}'.format(obj.LinearDeflection.Value)) + PathLog.debug('obj.AngularDeflection.Value: {}'.format(obj.AngularDeflection.Value)) + + for m in range(0, len(JOB.Model.Group)): + Mdl = JOB.Model.Group[m] + if FACES[m] is False: + PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label)) + else: + if m > 0: + # Raise to clearance between moddels + CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label))) + CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label)) + # make stock-model-voidShapes STL model for avoidance detection on transitions + if obj.Algorithm == 'OCL Dropcutter': + self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m]) + # time.sleep(0.2) + # Process model/faces - OCL objects must be ready + CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m])) + + # Save gcode produced + self.commandlist.extend(CMDS) + + # ###### CLOSING COMMANDS FOR OPERATION ###### + + # Delete temporary objects + # Restore model visibilities for restoration + if FreeCAD.GuiUp: + FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + M.Visibility = modelVisibility[m] + + if deleteTempsFlag is True: + for to in tempGroup.Group: + if hasattr(to, 'Group'): + for go in to.Group: + FCAD.removeObject(go.Name) + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName) + else: + if len(tempGroup.Group) == 0: + FCAD.removeObject(tempGroupName) + else: + tempGroup.purgeTouched() + + # Provide user feedback for gap sizes + gaps = list() + for g in self.gaps: + if g != toolDiam: + gaps.append(g) + if len(gaps) > 0: + obj.GapSizes = '{} mm'.format(gaps) + else: + if self.closedGap is True: + obj.GapSizes = 'Closed gaps < Gap Threshold.' + else: + obj.GapSizes = 'No gaps identified.' + + # clean up class variables + self.resetOpVariables() + self.deleteOpVariables() + + self.modelSTLs = None + self.safeSTLs = None + self.modelTypes = None + self.boundBoxes = None + self.gaps = None + self.closedGap = None + self.SafeHeightOffset = None + self.ClearHeightOffset = None + self.depthParams = None + self.midDep = None + self.wpc = None + del self.modelSTLs + del self.safeSTLs + del self.modelTypes + del self.boundBoxes + del self.gaps + del self.closedGap + del self.SafeHeightOffset + del self.ClearHeightOffset + del self.depthParams + del self.midDep + del self.wpc + + execTime = time.time() - startTime + PathLog.info('Operation time: {} sec.'.format(execTime)) + + return True + + # Methods for constructing the cut area + def _preProcessModel(self, JOB, obj): + PathLog.debug('_preProcessModel()') + + FACES = list() + VOIDS = list() + fShapes = list() + vShapes = list() + preProcEr = translate('PathWaterline', 'Error pre-processing Face') + warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face') + GRP = JOB.Model.Group + lenGRP = len(GRP) + + # Crete place holders for each base model in Job + for m in range(0, lenGRP): + FACES.append(False) + VOIDS.append(False) + fShapes.append(False) + vShapes.append(False) + + # The user has selected subobjects from the base. Pre-Process each. + if obj.Base and len(obj.Base) > 0: + PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') + + (FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS) + + # Cycle through each base model, processing faces for each + for m in range(0, lenGRP): + base = GRP[m] + (mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS) + fShapes[m] = mFS + vShapes[m] = mVS + self.profileShapes[m] = mPS + else: + PathLog.debug(' -No obj.Base data.') + for m in range(0, lenGRP): + self.modelSTLs[m] = True + + # Process each model base, as a whole, as needed + # PathLog.debug(' -Pre-processing all models in Job.') + for m in range(0, lenGRP): + if fShapes[m] is False: + PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label)) + if obj.BoundBox == 'BaseBoundBox': + base = GRP[m] + elif obj.BoundBox == 'Stock': + base = JOB.Stock + + pPEB = self._preProcessEntireBase(obj, base, m) + if pPEB is False: + PathLog.error(' -Failed to pre-process base as a whole.') + else: + (fcShp, prflShp) = pPEB + if fcShp is not False: + if fcShp is True: + PathLog.debug(' -fcShp is True.') + fShapes[m] = True + else: + fShapes[m] = [fcShp] + if prflShp is not False: + if fcShp is not False: + PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m])) + if vShapes[m] is not False: + PathLog.debug(' -Cutting void from base profile shape.') + adjPS = prflShp.cut(vShapes[m][0]) + self.profileShapes[m] = [adjPS] + else: + PathLog.debug(' -vShapes[m] is False.') + self.profileShapes[m] = [prflShp] + else: + PathLog.debug(' -Saving base profile shape.') + self.profileShapes[m] = [prflShp] + PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m])) + # Efor + + return (fShapes, vShapes) + + def _identifyFacesAndVoids(self, JOB, obj, F, V): + TUPS = list() + GRP = JOB.Model.Group + lenGRP = len(GRP) + + # Separate selected faces into (base, face) tuples and flag model(s) for STL creation + for (bs, SBS) in obj.Base: + for sb in SBS: + # Flag model for STL creation + mdlIdx = None + for m in range(0, lenGRP): + if bs is GRP[m]: + self.modelSTLs[m] = True + mdlIdx = m + break + TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) + + # Apply `AvoidXFaces` value + faceCnt = len(TUPS) + add = faceCnt - obj.AvoidLastX_Faces + for bst in range(0, faceCnt): + (m, base, sub) = TUPS[bst] + shape = getattr(base.Shape, sub) + if isinstance(shape, Part.Face): + faceIdx = int(sub[4:]) - 1 + if bst < add: + if F[m] is False: + F[m] = list() + F[m].append((shape, faceIdx)) + else: + if V[m] is False: + V[m] = list() + V[m].append((shape, faceIdx)) + return (F, V) + + def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS): + mFS = False + mVS = False + mPS = False + mIFS = list() + BB = base.Shape.BoundBox + + if FACES[m] is not False: + isHole = False + if obj.HandleMultipleFeatures == 'Collectively': + cont = True + fsL = list() # face shape list + ifL = list() # avoid shape list + outFCS = list() + + # Get collective envelope slice of selected faces + for (fcshp, fcIdx) in FACES[m]: + fNum = fcIdx + 1 + fsL.append(fcshp) + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + PathLog.debug('Attempting to get cross-section of collective faces.') + if len(outFCS) == 0: + PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) + cont = False + else: + cfsL = Part.makeCompound(outFCS) + + # Handle profile edges request + if cont is True and obj.ProfileEdges != 'None': + ofstVal = self._calculateOffsetValue(obj, isHole) + psOfst = self._extractFaceOffset(obj, cfsL, ofstVal) + if psOfst is not False: + mPS = [psOfst] + if obj.ProfileEdges == 'Only': + mFS = True + cont = False + else: + PathLog.error(' -Failed to create profile geometry for selected faces.') + cont = False + + if cont is True: + if self.showDebugObjects is True: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') + T.Shape = cfsL + T.purgeTouched() + self.tempGroup.addObject(T) + + ofstVal = self._calculateOffsetValue(obj, isHole) + faceOfstShp = self._extractFaceOffset(obj, cfsL, ofstVal) + if faceOfstShp is False: + PathLog.error(' -Failed to create offset face.') + cont = False + + if cont is True: + lenIfL = len(ifL) + if obj.InternalFeaturesCut is False: + if lenIfL == 0: + PathLog.debug(' -No internal features saved.') + else: + if lenIfL == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + if self.showDebugObjects is True: + C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') + C.Shape = casL + C.purgeTouched() + self.tempGroup.addObject(C) + ofstVal = self._calculateOffsetValue(obj, isHole=True) + intOfstShp = self._extractFaceOffset(obj, casL, ofstVal) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + mFS = [faceOfstShp] + # Eif + + elif obj.HandleMultipleFeatures == 'Individually': + for (fcshp, fcIdx) in FACES[m]: + cont = True + fsL = list() # face shape list + ifL = list() # avoid shape list + fNum = fcIdx + 1 + outerFace = False + + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + cont = False + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + cont = False + outerFace = False + else: + ((otrFace, raised), intWires) = gFW + outerFace = otrFace + if obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + if outerFace is not False: + PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) + + if obj.ProfileEdges != 'None': + ofstVal = self._calculateOffsetValue(obj, isHole) + psOfst = self._extractFaceOffset(obj, outerFace, ofstVal) + if psOfst is not False: + if mPS is False: + mPS = list() + mPS.append(psOfst) + if obj.ProfileEdges == 'Only': + if mFS is False: + mFS = list() + mFS.append(True) + cont = False + else: + PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) + cont = False + + if cont is True: + ofstVal = self._calculateOffsetValue(obj, isHole) + faceOfstShp = self._extractFaceOffset(obj, slc, ofstVal) + + lenIfl = len(ifL) + if obj.InternalFeaturesCut is False and lenIfl > 0: + if lenIfl == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + + ofstVal = self._calculateOffsetValue(obj, isHole=True) + intOfstShp = self._extractFaceOffset(obj, casL, ofstVal) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + if mFS is False: + mFS = list() + mFS.append(faceOfstShp) + # Eif + # Efor + # Eif + # Eif + + if len(mIFS) > 0: + if mVS is False: + mVS = list() + for ifs in mIFS: + mVS.append(ifs) + + if VOIDS[m] is not False: + PathLog.debug('Processing avoid faces.') + cont = True + isHole = False + outFCS = list() + intFEAT = list() + + for (fcshp, fcIdx) in VOIDS[m]: + fNum = fcIdx + 1 + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) + cont = False + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if obj.AvoidLastX_InternalFeatures is False: + if intWires is not False: + for (iFace, rsd) in intWires: + intFEAT.append(iFace) + + lenOtFcs = len(outFCS) + if lenOtFcs == 0: + cont = False + else: + if lenOtFcs == 1: + avoid = outFCS[0] + else: + avoid = Part.makeCompound(outFCS) + + if self.showDebugObjects is True: + PathLog.debug('*** tmpAvoidArea') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') + P.Shape = avoid + # P.recompute() + P.purgeTouched() + self.tempGroup.addObject(P) + + if cont is True: + if self.showDebugObjects is True: + PathLog.debug('*** tmpVoidCompound') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') + P.Shape = avoid + # P.recompute() + P.purgeTouched() + self.tempGroup.addObject(P) + ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True) + avdOfstShp = self._extractFaceOffset(obj, avoid, ofstVal) + if avdOfstShp is False: + PathLog.error('Failed to create collective offset avoid face.') + cont = False + + if cont is True: + avdShp = avdOfstShp + + if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: + if len(intFEAT) > 1: + ifc = Part.makeCompound(intFEAT) + else: + ifc = intFEAT[0] + ofstVal = self._calculateOffsetValue(obj, isHole=True) + ifOfstShp = self._extractFaceOffset(obj, ifc, ofstVal) + if ifOfstShp is False: + PathLog.error('Failed to create collective offset avoid internal features.') + else: + avdShp = avdOfstShp.cut(ifOfstShp) + + if mVS is False: + mVS = list() + mVS.append(avdShp) + + + return (mFS, mVS, mPS) + + def _getFaceWires(self, base, fcshp, fcIdx): + outFace = False + INTFCS = list() + fNum = fcIdx + 1 + # preProcEr = translate('PathWaterline', 'Error pre-processing Face') + warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face') + + PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) + WIRES = self._extractWiresFromFace(base, fcshp) + if WIRES is False: + PathLog.error('Failed to extract wires from Face{}'.format(fNum)) + return False + + # Process remaining internal features, adding to FCS list + lenW = len(WIRES) + for w in range(0, lenW): + (wire, rsd) = WIRES[w] + PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) + if wire.isClosed() is False: + PathLog.debug(' -wire is not closed.') + else: + slc = self._flattenWireToFace(wire) + if slc is False: + PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) + else: + if w == 0: + outFace = (slc, rsd) + else: + # add to VOIDS so cutter avoids area. + PathLog.warning(warnFinDep + str(fNum) + '.') + INTFCS.append((slc, rsd)) + if len(INTFCS) == 0: + return (outFace, False) + else: + return (outFace, INTFCS) + + def _preProcessEntireBase(self, obj, base, m): + cont = True + isHole = False + prflShp = False + # Create envelope, extract cross-section and make offset co-planar shape + # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) + + try: + baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as ee: + PathLog.error(str(ee)) + shell = base.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as eee: + PathLog.error(str(eee)) + cont = False + # time.sleep(0.2) + + if cont is True: + csFaceShape = self._getShapeSlice(baseEnv) + if csFaceShape is False: + PathLog.debug('_getShapeSlice(baseEnv) failed') + csFaceShape = self._getCrossSection(baseEnv) + if csFaceShape is False: + PathLog.debug('_getCrossSection(baseEnv) failed') + csFaceShape = self._getSliceFromEnvelope(baseEnv) + if csFaceShape is False: + PathLog.error('Failed to slice baseEnv shape.') + cont = False + + if cont is True and obj.ProfileEdges != 'None': + PathLog.debug(' -Attempting profile geometry for model base.') + ofstVal = self._calculateOffsetValue(obj, isHole) + psOfst = self._extractFaceOffset(obj, csFaceShape, ofstVal) + if psOfst is not False: + if obj.ProfileEdges == 'Only': + return (True, psOfst) + prflShp = psOfst + else: + PathLog.error(' -Failed to create profile geometry.') + cont = False + + if cont is True: + ofstVal = self._calculateOffsetValue(obj, isHole) + faceOffsetShape = self._extractFaceOffset(obj, csFaceShape, ofstVal) + if faceOffsetShape is False: + PathLog.error('_extractFaceOffset() failed.') + else: + faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) + return (faceOffsetShape, prflShp) + return False + + def _extractWiresFromFace(self, base, fc): + '''_extractWiresFromFace(base, fc) ... + Attempts to return all closed wires within a parent face, including the outer most wire of the parent. + The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). + ''' + PathLog.debug('_extractWiresFromFace()') + + WIRES = list() + lenWrs = len(fc.Wires) + PathLog.debug(' -Wire count: {}'.format(lenWrs)) + + def index0(tup): + return tup[0] + + # Cycle through wires in face + for w in range(0, lenWrs): + PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) + wire = fc.Wires[w] + checkEdges = False + cont = True + + # Check for closed edges (circles, ellipses, etc...) + for E in wire.Edges: + if E.isClosed() is True: + checkEdges = True + break + + if checkEdges is True: + PathLog.debug(' -checkEdges is True') + for e in range(0, len(wire.Edges)): + edge = wire.Edges[e] + if edge.isClosed() is True and edge.Mass > 0.01: + PathLog.debug(' -Found closed edge') + raised = False + ip = self._isPocket(base, fc, edge) + if ip is False: + raised = True + ebb = edge.BoundBox + eArea = ebb.XLength * ebb.YLength + F = Part.Face(Part.Wire([edge])) + WIRES.append((eArea, F.Wires[0], raised)) + cont = False + + if cont is True: + PathLog.debug(' -cont is True') + # If only one wire and not checkEdges, return first wire + if lenWrs == 1: + return [(wire, False)] + + raised = False + wbb = wire.BoundBox + wArea = wbb.XLength * wbb.YLength + if w > 0: + ip = self._isPocket(base, fc, wire) + if ip is False: + raised = True + WIRES.append((wArea, Part.Wire(wire.Edges), raised)) + + nf = len(WIRES) + if nf > 0: + PathLog.debug(' -number of wires found is {}'.format(nf)) + if nf == 1: + (area, W, raised) = WIRES[0] + return [(W, raised)] + else: + sortedWIRES = sorted(WIRES, key=index0, reverse=True) + return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size + + return False + + def _calculateOffsetValue(self, obj, isHole, isVoid=False): + '''_calculateOffsetValue(obj, isHole, isVoid) ... internal function. + Calculate the offset for the Path.Area() function.''' + JOB = PathUtils.findParentJob(obj) + tolrnc = JOB.GeometryTolerance.Value + + if isVoid is False: + if isHole is True: + offset = -1 * obj.InternalFeaturesAdjustment.Value + offset += self.radius # (self.radius + (tolrnc / 10.0)) + else: + offset = -1 * obj.BoundaryAdjustment.Value + if obj.BoundaryEnforcement is True: + offset += self.radius # (self.radius + (tolrnc / 10.0)) + else: + offset -= self.radius # (self.radius + (tolrnc / 10.0)) + offset = 0.0 - offset + else: + offset = -1 * obj.BoundaryAdjustment.Value + offset += self.radius # (self.radius + (tolrnc / 10.0)) + + return offset + + def _extractFaceOffset(self, obj, fcShape, offset, makeComp=True): + '''_extractFaceOffset(fcShape, offset) ... internal function. + Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. + Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' + PathLog.debug('_extractFaceOffset()') + + if fcShape.BoundBox.ZMin != 0.0: + fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin)) + + areaParams = {} + areaParams['Offset'] = offset + areaParams['Fill'] = 1 # 1 + areaParams['Coplanar'] = 0 + areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections + areaParams['Reorient'] = True + areaParams['OpenMode'] = 0 + areaParams['MaxArcPoints'] = 400 # 400 + areaParams['Project'] = True + # areaParams['Tolerance'] = 0.001 + + area = Path.Area() # Create instance of Area() class object + # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane + area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1 + area.add(fcShape) + area.setParams(**areaParams) # set parameters + + # Save parameters for debugging + # obj.AreaParams = str(area.getParams()) + # PathLog.debug("Area with params: {}".format(area.getParams())) + + offsetShape = area.getShape() + wCnt = len(offsetShape.Wires) + if wCnt == 0: + return False + elif wCnt == 1: + ofstFace = Part.Face(offsetShape.Wires[0]) + if not makeComp: + ofstFace = [ofstFace] + else: + W = list() + for wr in offsetShape.Wires: + W.append(Part.Face(wr)) + if makeComp: + ofstFace = Part.makeCompound(W) + else: + ofstFace = W + + return ofstFace # offsetShape + + def _isPocket(self, b, f, w): + '''_isPocket(b, f, w)... + Attempts to determing if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. + Returns True if pocket, False if raised protrusion.''' + e = w.Edges[0] + for fi in range(0, len(b.Shape.Faces)): + face = b.Shape.Faces[fi] + for ei in range(0, len(face.Edges)): + edge = face.Edges[ei] + if e.isSame(edge) is True: + if f is face: + # Alternative: run loop to see if all edges are same + pass # same source face, look for another + else: + if face.CenterOfMass.z < f.CenterOfMass.z: + return True + return False + + def _flattenWireToFace(self, wire): + PathLog.debug('_flattenWireToFace()') + if wire.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + + # If wire is planar horizontal, convert to a face and return + if wire.BoundBox.ZLength == 0.0: + slc = Part.Face(wire) + return slc + + # Attempt to create a new wire for manipulation, if not, use original + newWire = Part.Wire(wire.Edges) + if newWire.isClosed() is True: + nWire = newWire + else: + PathLog.debug(' -newWire.isClosed() is False') + nWire = wire + + # Attempt extrusion, and then try a manual slice and then cross-section + ext = self._getExtrudedShape(nWire) + if ext is False: + PathLog.debug('_getExtrudedShape() failed') + else: + slc = self._getShapeSlice(ext) + if slc is not False: + return slc + cs = self._getCrossSection(ext, True) + if cs is not False: + return cs + + # Attempt creating an envelope, and then try a manual slice and then cross-section + env = self._getShapeEnvelope(nWire) + if env is False: + PathLog.debug('_getShapeEnvelope() failed') + else: + slc = self._getShapeSlice(env) + if slc is not False: + return slc + cs = self._getCrossSection(env, True) + if cs is not False: + return cs + + # Attempt creating a projection + slc = self._getProjectedFace(nWire) + if slc is False: + PathLog.debug('_getProjectedFace() failed') + else: + return slc + + return False + + def _getExtrudedShape(self, wire): + PathLog.debug('_getExtrudedShape()') + wBB = wire.BoundBox + extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 + + try: + # slower, but renders collective faces correctly. Method 5 in TESTING + shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) + except Exception as ee: + PathLog.error(' -extrude wire failed: \n{}'.format(ee)) + return False + + SHP = Part.makeSolid(shell) + return SHP + + def _getShapeSlice(self, shape): + PathLog.debug('_getShapeSlice()') + + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + xmin = bb.XMin - 1.0 + xmax = bb.XMax + 1.0 + ymin = bb.YMin - 1.0 + ymax = bb.YMax + 1.0 + p1 = FreeCAD.Vector(xmin, ymin, mid) + p2 = FreeCAD.Vector(xmax, ymin, mid) + p3 = FreeCAD.Vector(xmax, ymax, mid) + p4 = FreeCAD.Vector(xmin, ymax, mid) + + e1 = Part.makeLine(p1, p2) + e2 = Part.makeLine(p2, p3) + e3 = Part.makeLine(p3, p4) + e4 = Part.makeLine(p4, p1) + face = Part.Face(Part.Wire([e1, e2, e3, e4])) + fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area + sArea = shape.BoundBox.XLength * shape.BoundBox.YLength + midArea = (fArea + sArea) / 2.0 + + slcShp = shape.common(face) + slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength + + if slcArea < midArea: + for W in slcShp.Wires: + if W.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + if len(slcShp.Wires) == 1: + wire = slcShp.Wires[0] + slc = Part.Face(wire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + else: + fL = list() + for W in slcShp.Wires: + slc = Part.Face(W) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + fL.append(slc) + comp = Part.makeCompound(fL) + if self.showDebugObjects is True: + PathLog.debug('*** tmpSliceCompound') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound') + P.Shape = comp + # P.recompute() + P.purgeTouched() + self.tempGroup.addObject(P) + return comp + + PathLog.debug(' -slcArea !< midArea') + PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) + return False + + def _getProjectedFace(self, wire): + PathLog.debug('_getProjectedFace()') + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') + F.Shape = wire + F.purgeTouched() + self.tempGroup.addObject(F) + try: + prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) + prj.recompute() + prj.purgeTouched() + self.tempGroup.addObject(prj) + except Exception as ee: + PathLog.error(str(ee)) + return False + else: + pWire = Part.Wire(prj.Shape.Edges) + if pWire.isClosed() is False: + # PathLog.debug(' -pWire.isClosed() is False') + return False + slc = Part.Face(pWire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + return False + + def _getCrossSection(self, shape, withExtrude=False): + PathLog.debug('_getCrossSection()') + wires = list() + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + + for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): + wires.append(i) + + if len(wires) > 0: + comp = Part.Compound(wires) # produces correct cross-section wire ! + comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) + csWire = comp.Wires[0] + if csWire.isClosed() is False: + PathLog.debug(' -comp.Wires[0] is not closed') + return False + if withExtrude is True: + ext = self._getExtrudedShape(csWire) + CS = self._getShapeSlice(ext) + else: + CS = Part.Face(csWire) + CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) + return CS + else: + PathLog.debug(' -No wires from .slice() method') + + return False + + def _getShapeEnvelope(self, shape): + PathLog.debug('_getShapeEnvelope()') + + wBB = shape.BoundBox + extFwd = wBB.ZLength + 10.0 + minz = wBB.ZMin + maxz = wBB.ZMin + extFwd + stpDwn = (maxz - minz) / 4.0 + dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) + + try: + env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape + except Exception as ee: + PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) + return False + else: + return env + + return False + + def _getSliceFromEnvelope(self, env): + PathLog.debug('_getSliceFromEnvelope()') + eBB = env.BoundBox + extFwd = eBB.ZLength + 10.0 + maxz = eBB.ZMin + extFwd + + maxMax = env.Edges[0].BoundBox.ZMin + emax = math.floor(maxz - 1.0) + E = list() + for e in range(0, len(env.Edges)): + emin = env.Edges[e].BoundBox.ZMin + if emin > emax: + E.append(env.Edges[e]) + tf = Part.Face(Part.Wire(Part.__sortEdges__(E))) + tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin)) + + return tf + + def _prepareModelSTLs(self, JOB, obj): + PathLog.debug('_prepareModelSTLs()') + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + + # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") + if self.modelTypes[m] == 'M': + mesh = M.Mesh + else: + # base.Shape.tessellate(0.05) # 0.5 original value + mesh = MeshPart.meshFromShape(Shape=M.Shape, + LinearDeflection=obj.LinearDeflection.Value, + AngularDeflection=obj.AngularDeflection.Value, + Relative=False) + + if self.modelSTLs[m] is True: + stl = ocl.STLSurf() + + for f in mesh.Facets: + p = f.Points[0] + q = f.Points[1] + r = f.Points[2] + t = ocl.Triangle(ocl.Point(p[0], p[1], p[2] + obj.DepthOffset.Value), + ocl.Point(q[0], q[1], q[2] + obj.DepthOffset.Value), + ocl.Point(r[0], r[1], r[2] + obj.DepthOffset.Value)) + stl.addTriangle(t) + self.modelSTLs[m] = stl + return + + def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): + '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... + Creates and OCL.stl object with combined data with waste stock, + model, and avoided faces. Travel lines can be checked against this + STL object to determine minimum travel height to clear stock and model.''' + PathLog.debug('_makeSafeSTL()') + + fuseShapes = list() + Mdl = JOB.Model.Group[mdlIdx] + FCAD = FreeCAD.ActiveDocument + mBB = Mdl.Shape.BoundBox + sBB = JOB.Stock.Shape.BoundBox + + # add Model shape to safeSTL shape + fuseShapes.append(Mdl.Shape) + + if obj.BoundBox == 'BaseBoundBox': + cont = False + extFwd = (sBB.ZLength) + zmin = mBB.ZMin + zmax = mBB.ZMin + extFwd + stpDwn = (zmax - zmin) / 4.0 + dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) + + try: + envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as ee: + PathLog.error(str(ee)) + shell = Mdl.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as eee: + PathLog.error(str(eee)) + + if cont is True: + stckWst = JOB.Stock.Shape.cut(envBB) + if obj.BoundaryAdjustment > 0.0: + cmpndFS = Part.makeCompound(faceShapes) + baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape + adjStckWst = stckWst.cut(baBB) + else: + adjStckWst = stckWst + fuseShapes.append(adjStckWst) + else: + PathLog.warning('Path transitions might not avoid the model. Verify paths.') + # time.sleep(0.3) + + else: + # If boundbox is Job.Stock, add hidden pad under stock as base plate + toolDiam = self.cutter.getDiameter() + zMin = JOB.Stock.Shape.BoundBox.ZMin + xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam + yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam + bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) + bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) + bH = 1.0 + crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) + B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) + fuseShapes.append(B) + + if voidShapes is not False: + voidComp = Part.makeCompound(voidShapes) + voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape + fuseShapes.append(voidEnv) + + f0 = fuseShapes.pop(0) + if len(fuseShapes) > 0: + fused = f0.fuse(fuseShapes) + else: + fused = f0 + + if self.showDebugObjects is True: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') + T.Shape = fused + T.purgeTouched() + self.tempGroup.addObject(T) + + # Extract mesh from fusion + meshFuse = MeshPart.meshFromShape(Shape=fused, + LinearDeflection=obj.LinearDeflection.Value, + AngularDeflection=obj.AngularDeflection.Value, + Relative=False) + # time.sleep(0.2) + stl = ocl.STLSurf() + for f in meshFuse.Facets: + p = f.Points[0] + q = f.Points[1] + r = f.Points[2] + t = ocl.Triangle(ocl.Point(p[0], p[1], p[2]), + ocl.Point(q[0], q[1], q[2]), + ocl.Point(r[0], r[1], r[2])) + stl.addTriangle(t) + + self.safeSTLs[mdlIdx] = stl + + def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS): + '''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)... + This method applies any avoided faces or regions to the selected faces. + It then calls the correct method.''' + PathLog.debug('_processCutAreas()') + + final = list() + base = JOB.Model.Group[mdlIdx] + + # Process faces Collectively or Individually + if obj.HandleMultipleFeatures == 'Collectively': + if FCS is True: + COMP = False + else: + ADD = Part.makeCompound(FCS) + if VDS is not False: + DEL = Part.makeCompound(VDS) + COMP = ADD.cut(DEL) + else: + COMP = ADD + + final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + if obj.Algorithm == 'OCL Dropcutter': + final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + else: + final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + + elif obj.HandleMultipleFeatures == 'Individually': + for fsi in range(0, len(FCS)): + fShp = FCS[fsi] + # self.deleteOpVariables(all=False) + self.resetOpVariables(all=False) + + if fShp is True: + COMP = False + else: + ADD = Part.makeCompound([fShp]) + if VDS is not False: + DEL = Part.makeCompound(VDS) + COMP = ADD.cut(DEL) + else: + COMP = ADD + + final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + if obj.Algorithm == 'OCL Dropcutter': + final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + else: + final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + COMP = None + # Eif + + return final + + # Methods for creating path geometry + def _planarMakePathGeom(self, obj, faceShp): + '''_planarMakePathGeom(obj, faceShp)... + Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp. + The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.''' + PathLog.debug('_planarMakePathGeom()') + GeoSet = list() + + # Apply drop cutter extra offset and set the max and min XY area of the operation + xmin = faceShp.BoundBox.XMin + xmax = faceShp.BoundBox.XMax + ymin = faceShp.BoundBox.YMin + ymax = faceShp.BoundBox.YMax + zmin = faceShp.BoundBox.ZMin + zmax = faceShp.BoundBox.ZMax + + # Compute weighted center of mass of all faces combined + fCnt = 0 + totArea = 0.0 + zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) + for F in faceShp.Faces: + comF = F.CenterOfMass + areaF = F.Area + totArea += areaF + fCnt += 1 + zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) + if fCnt == 0: + PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) + zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) + else: + avgArea = totArea / fCnt + zeroCOM.multiply(1 / fCnt) + zeroCOM.multiply(1 / avgArea) + COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) + + # get X, Y, Z spans; Compute center of rotation + deltaX = abs(xmax-xmin) + deltaY = abs(ymax-ymin) + deltaZ = abs(zmax-zmin) + deltaC = math.sqrt(deltaX**2 + deltaY**2) + lineLen = deltaC + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end + halfLL = math.ceil(lineLen / 2.0) + cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen + halfPasses = math.ceil(cutPasses / 2.0) + bbC = faceShp.BoundBox.Center + + # Generate the Draft line/circle sets to be intersected with the cut-face-area + if obj.CutPattern in ['ZigZag', 'Line']: + MaxLC = -1 + centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model + cAng = math.atan(deltaX / deltaY) # BoundaryBox angle + + # Determine end points and create top lines + x1 = centRot.x - halfLL + x2 = centRot.x + halfLL + diag = None + if obj.CutPatternAngle == 0 or obj.CutPatternAngle == 180: + MaxLC = math.floor(deltaY / self.cutOut) + diag = deltaY + elif obj.CutPatternAngle == 90 or obj.CutPatternAngle == 270: + MaxLC = math.floor(deltaX / self.cutOut) + diag = deltaX + else: + perpDist = math.cos(cAng - math.radians(obj.CutPatternAngle)) * deltaC + MaxLC = math.floor(perpDist / self.cutOut) + diag = perpDist + y1 = centRot.y + diag + # y2 = y1 + + p1 = FreeCAD.Vector(x1, y1, 0.0) + p2 = FreeCAD.Vector(x2, y1, 0.0) + topLineTuple = (p1, p2) + ny1 = centRot.y - diag + n1 = FreeCAD.Vector(x1, ny1, 0.0) + n2 = FreeCAD.Vector(x2, ny1, 0.0) + negTopLineTuple = (n1, n2) + + # Create end points for set of lines to intersect with cross-section face + pntTuples = list() + for lc in range((-1 * (halfPasses - 1)), halfPasses + 1): + # if lc == (cutPasses - MaxLC - 1): + # pntTuples.append(negTopLineTuple) + # if lc == (MaxLC + 1): + # pntTuples.append(topLineTuple) + x1 = centRot.x - halfLL + x2 = centRot.x + halfLL + y1 = centRot.y + (lc * self.cutOut) + # y2 = y1 + p1 = FreeCAD.Vector(x1, y1, 0.0) + p2 = FreeCAD.Vector(x2, y1, 0.0) + pntTuples.append( (p1, p2) ) + + # Convert end points to lines + for (p1, p2) in pntTuples: + line = Part.makeLine(p1, p2) + GeoSet.append(line) + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + zTgt = faceShp.BoundBox.ZMin + axisRot = FreeCAD.Vector(0.0, 0.0, 1.0) + cntr = FreeCAD.Placement() + cntr.Rotation = FreeCAD.Rotation(axisRot, 0.0) + + if obj.CircularCenterAt == 'CenterOfMass': + cntr.Base = FreeCAD.Vector(COM.x, COM.y, zTgt) # COM # Use center of Mass + elif obj.CircularCenterAt == 'CenterOfBoundBox': + cent = faceShp.BoundBox.Center + cntr.Base = FreeCAD.Vector(cent.x, cent.y, zTgt) + elif obj.CircularCenterAt == 'XminYmin': + cntr.Base = FreeCAD.Vector(faceShp.BoundBox.XMin, faceShp.BoundBox.YMin, zTgt) + elif obj.CircularCenterAt == 'Custom': + newCent = FreeCAD.Vector(obj.CircularCenterCustom.x, obj.CircularCenterCustom.y, zTgt) + cntr.Base = newCent + + # recalculate cutPasses value, if need be + radialPasses = halfPasses + if obj.CircularCenterAt != 'CenterOfBoundBox': + # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center + EBB = faceShp.BoundBox + CORNERS = [ + FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), + FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), + ] + dMax = 0.0 + for c in range(0, 4): + dist = CORNERS[c].sub(cntr.Base).Length + if dist > dMax: + dMax = dist + lineLen = dMax + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end + radialPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen + + # Update COM point and current CircularCenter + if obj.CircularCenterAt != 'Custom': + obj.CircularCenterCustom = cntr.Base + + minRad = self.cutter.getDiameter() * 0.45 + siX3 = 3 * obj.SampleInterval.Value + minRadSI = (siX3 / 2.0) / math.pi + if minRad < minRadSI: + minRad = minRadSI + + # Make small center circle to start pattern + if obj.StepOver > 50: + circle = Part.makeCircle(minRad, cntr.Base) + GeoSet.append(circle) + + for lc in range(1, radialPasses + 1): + rad = (lc * self.cutOut) + if rad >= minRad: + circle = Part.makeCircle(rad, cntr.Base) + GeoSet.append(circle) + # Efor + COM = cntr.Base + # Eif + + if obj.CutPatternReversed is True: + GeoSet.reverse() + + if faceShp.BoundBox.ZMin != 0.0: + faceShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceShp.BoundBox.ZMin)) + + # Create compound object to bind all lines in Lineset + geomShape = Part.makeCompound(GeoSet) + + # Position and rotate the Line and ZigZag geometry + if obj.CutPattern in ['Line', 'ZigZag']: + if obj.CutPatternAngle != 0.0: + geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), obj.CutPatternAngle) + geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) + + if self.showDebugObjects is True: + F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') + F.Shape = geomShape + F.purgeTouched() + self.tempGroup.addObject(F) + + # Identify intersection of cross-section face and lineset + cmnShape = faceShp.common(geomShape) + + if self.showDebugObjects is True: + F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') + F.Shape = cmnShape + F.purgeTouched() + self.tempGroup.addObject(F) + + self.tmpCOM = FreeCAD.Vector(COM.x, COM.y, faceShp.BoundBox.ZMin) + return cmnShape + + def _pathGeomToLinesPointSet(self, obj, compGeoShp): + '''_pathGeomToLinesPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' + PathLog.debug('_pathGeomToLinesPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + chkGap = False + lnCnt = 0 + ec = len(compGeoShp.Edges) + cutClimb = self.CutClimb + toolDiam = 2.0 * self.radius + cpa = obj.CutPatternAngle + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if cutClimb is True: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + else: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + inLine.append(tup) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + + for ei in range(1, ec): + chkGap = False + edg = compGeoShp.Edges[ei] # Get edge for vertexes + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0 + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1 + + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) + iC = self.isPointOnLine(sp, ep, cp) + if iC is True: + inLine.append('BRK') + chkGap = True + else: + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + lnCnt += 1 + inLine = list() # reset collinear container + if cutClimb is True: + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + else: + sp = ep + + if cutClimb is True: + tup = (v2, v1) + if chkGap is True: + gap = abs(toolDiam - lst.sub(ep).Length) + lst = cp + else: + tup = (v1, v2) + if chkGap is True: + gap = abs(toolDiam - lst.sub(cp).Length) + lst = ep + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + tup = (vA, tup[1]) + self.closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < self.gaps[0]: + self.gaps.insert(0, gap) + self.gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + + # Handle last inLine set, reversing it. + if obj.CutPatternReversed is True: + if cpa != 0.0 and cpa % 90.0 == 0.0: + F = LINES.pop(0) + rev = list() + for iL in F: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + rev.reverse() + LINES.insert(0, rev) + + isEven = lnCnt % 2 + if isEven == 0: + PathLog.debug('Line count is ODD.') + else: + PathLog.debug('Line count is even.') + + return LINES + + def _pathGeomToZigzagPointSet(self, obj, compGeoShp): + '''_pathGeomToZigzagPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings + with a ZigZag directional indicator included for each collinear group.''' + PathLog.debug('_pathGeomToZigzagPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + lnCnt = 0 + chkGap = False + ec = len(compGeoShp.Edges) + toolDiam = 2.0 * self.radius + + if self.CutClimb is True: + dirFlg = -1 + else: + dirFlg = 1 + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if dirFlg == 1: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + else: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point + inLine.append(tup) + otr = lst + + for ei in range(1, ec): + edg = compGeoShp.Edges[ei] + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) + + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + iC = self.isPointOnLine(sp, ep, cp) + if iC is True: + inLine.append('BRK') + chkGap = True + gap = abs(toolDiam - lst.sub(cp).Length) + else: + chkGap = False + if dirFlg == -1: + inLine.reverse() + LINES.append((dirFlg, inLine)) + lnCnt += 1 + dirFlg = -1 * dirFlg # Change zig to zag + inLine = list() # reset collinear container + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + otr = ep + + lst = ep + if dirFlg == 1: + tup = (v1, v2) + else: + tup = (v2, v1) + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + if dirFlg == 1: + tup = (vA, tup[1]) + else: + #tup = (vA, tup[1]) + #tup = (tup[1], vA) + tup = (tup[0], vB) + self.closedGap = True + else: + gap = round(gap, 6) + if gap < self.gaps[0]: + self.gaps.insert(0, gap) + self.gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + + # Fix directional issue with LAST line when line count is even + isEven = lnCnt % 2 + if isEven == 0: # Changed to != with 90 degree CutPatternAngle + PathLog.debug('Line count is even.') + else: + PathLog.debug('Line count is ODD.') + dirFlg = -1 * dirFlg + if obj.CutPatternReversed is False: + if self.CutClimb is True: + dirFlg = -1 * dirFlg + + if obj.CutPatternReversed is True: + dirFlg = -1 * dirFlg + + # Handle last inLine list + if dirFlg == 1: + rev = list() + for iL in inLine: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + + if obj.CutPatternReversed is False: + rev.reverse() + else: + rev2 = list() + for iL in rev: + if iL == 'BRK': + rev2.append(iL) + else: + (p1, p2) = iL + rev2.append((p2, p1)) + rev2.reverse() + rev = rev2 + + LINES.append((dirFlg, rev)) + else: + LINES.append((dirFlg, inLine)) + + return LINES + + def _pathGeomToArcPointSet(self, obj, compGeoShp): + '''_pathGeomToArcPointSet(obj, compGeoShp)... + Convert a compound set of arcs/circles to a set of directionally-oriented arc end points + and the corresponding center point.''' + # Extract intersection line segments for return value as list() + PathLog.debug('_pathGeomToArcPointSet()') + ARCS = list() + stpOvrEI = list() + segEI = list() + isSame = False + sameRad = None + COM = self.tmpCOM + toolDiam = 2.0 * self.radius + ec = len(compGeoShp.Edges) + + def gapDist(sp, ep): + X = (ep[0] - sp[0])**2 + Y = (ep[1] - sp[1])**2 + Z = (ep[2] - sp[2])**2 + # return math.sqrt(X + Y + Z) + return math.sqrt(X + Y) # the 'z' value is zero in both points + + # Separate arc data into Loops and Arcs + for ei in range(0, ec): + edg = compGeoShp.Edges[ei] + if edg.Closed is True: + stpOvrEI.append(('L', ei, False)) + else: + if isSame is False: + segEI.append(ei) + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + else: + # Check if arc is co-radial to current SEGS + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + if abs(sameRad - pnt.sub(COM).Length) > 0.00001: + isSame = False + + if isSame is True: + segEI.append(ei) + else: + # Move co-radial arc segments + stpOvrEI.append(['A', segEI, False]) + # Start new list of arc segments + segEI = [ei] + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + # Process trailing `segEI` data, if available + if isSame is True: + stpOvrEI.append(['A', segEI, False]) + + # Identify adjacent arcs with y=0 start/end points that connect + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'A': + startOnAxis = list() + endOnAxis = list() + EI = SO[1] # list of corresponding compGeoShp.Edges indexes + + # Identify startOnAxis and endOnAxis arcs + for i in range(0, len(EI)): + ei = EI[i] # edge index + E = compGeoShp.Edges[ei] # edge object + if abs(COM.y - E.Vertexes[0].Y) < 0.00001: + startOnAxis.append((i, ei, E.Vertexes[0])) + elif abs(COM.y - E.Vertexes[1].Y) < 0.00001: + endOnAxis.append((i, ei, E.Vertexes[1])) + + # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected + lenSOA = len(startOnAxis) + lenEOA = len(endOnAxis) + if lenSOA > 0 and lenEOA > 0: + delIdxs = list() + lstFindIdx = 0 + for soa in range(0, lenSOA): + (iS, eiS, vS) = startOnAxis[soa] + for eoa in range(0, len(endOnAxis)): + (iE, eiE, vE) = endOnAxis[eoa] + dist = vE.X - vS.X + if abs(dist) < 0.00001: # They connect on axis at same radius + SO[2] = (eiE, eiS) + break + elif dist > 0: + break # stop searching + # Eif + # Eif + # Efor + + # Construct arc data tuples for OCL + dirFlg = 1 + # cutPat = obj.CutPattern + if self.CutClimb is False: # True yields Climb when set to Conventional + dirFlg = -1 + + # Cycle through stepOver data + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'L': # L = Loop/Ring/Circle + # PathLog.debug("SO[0] == 'Loop'") + lei = SO[1] # loop Edges index + v1 = compGeoShp.Edges[lei].Vertexes[0] + + # space = obj.SampleInterval.Value / 2.0 + space = 0.0000001 + + # p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) + p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) + rad = p1.sub(COM).Length + spcRadRatio = space/rad + if spcRadRatio < 1.0: + tolrncAng = math.asin(spcRadRatio) + else: + tolrncAng = 0.9999998 * math.pi + EX = COM.x + (rad * math.cos(tolrncAng)) + EY = v1.Y - space # rad * math.sin(tolrncAng) + + sp = (v1.X, v1.Y, 0.0) + ep = (EX, EY, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + ARCS.append(('L', dirFlg, [arc])) + else: # SO[0] == 'A' A = Arc + # PathLog.debug("SO[0] == 'Arc'") + PRTS = list() + EI = SO[1] # list of corresponding Edges indexes + CONN = SO[2] # list of corresponding connected edges tuples (iE, iS) + chkGap = False + lst = None + + if CONN is not False: + (iE, iS) = CONN + v1 = compGeoShp.Edges[iE].Vertexes[0] + v2 = compGeoShp.Edges[iS].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + lst = sp + PRTS.append(arc) + # Pop connected edge index values from arc segments index list + iEi = EI.index(iE) + iSi = EI.index(iS) + if iEi > iSi: + EI.pop(iEi) + EI.pop(iSi) + else: + EI.pop(iSi) + EI.pop(iEi) + if len(EI) > 0: + PRTS.append('BRK') + chkGap = True + cnt = 0 + for ei in EI: + if cnt > 0: + PRTS.append('BRK') + chkGap = True + v1 = compGeoShp.Edges[ei].Vertexes[0] + v2 = compGeoShp.Edges[ei].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) + lst = sp + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = PRTS.pop() # pop off 'BRK' marker + (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current + arc = (vA, arc[1], vC) + self.closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < self.gaps[0]: + self.gaps.insert(0, gap) + self.gaps.pop() + PRTS.append(arc) + cnt += 1 + + if dirFlg == -1: + PRTS.reverse() + + ARCS.append(('A', dirFlg, PRTS)) + # Eif + if obj.CutPattern == 'CircularZigZag': + dirFlg = -1 * dirFlg + # Efor + + return ARCS + + def _getExperimentalWaterlinePaths(self, obj, PNTSET, csHght): + '''_getExperimentalWaterlinePaths(obj, PNTSET, csHght)... + Switching fuction for calling the appropriate path-geometry to OCL points conversion fucntion + for the various cut patterns.''' + PathLog.debug('_getExperimentalWaterlinePaths()') + SCANS = list() + + if obj.CutPattern == 'Line': + stpOvr = list() + for D in PNTSET: + for SEG in D: + if SEG == 'BRK': + stpOvr.append(SEG) + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = SEG + P1 = FreeCAD.Vector(A[0], A[1], csHght) + P2 = FreeCAD.Vector(B[0], B[1], csHght) + stpOvr.append((P1, P2)) + SCANS.append(stpOvr) + stpOvr = list() + elif obj.CutPattern == 'ZigZag': + stpOvr = list() + for (dirFlg, LNS) in PNTSET: + for SEG in LNS: + if SEG == 'BRK': + stpOvr.append(SEG) + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = SEG + P1 = FreeCAD.Vector(A[0], A[1], csHght) + P2 = FreeCAD.Vector(B[0], B[1], csHght) + stpOvr.append((P1, P2)) + SCANS.append(stpOvr) + stpOvr = list() + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + # PNTSET is list, by stepover. + # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp) + for so in range(0, len(PNTSET)): + stpOvr = list() + erFlg = False + (aTyp, dirFlg, ARCS) = PNTSET[so] + + if dirFlg == 1: # 1 + cMode = True # Climb mode + else: + cMode = False + + for a in range(0, len(ARCS)): + Arc = ARCS[a] + if Arc == 'BRK': + stpOvr.append('BRK') + else: + (sp, ep, cp) = Arc + S = FreeCAD.Vector(sp[0], sp[1], csHght) + E = FreeCAD.Vector(ep[0], ep[1], csHght) + C = FreeCAD.Vector(cp[0], cp[1], csHght) + scan = (S, E, C, cMode) + if scan is False: + erFlg = True + else: + ##if aTyp == 'L': + ## stpOvr.append(FreeCAD.Vector(scan[0][0].x, scan[0][0].y, scan[0][0].z)) + stpOvr.append(scan) + if erFlg is False: + SCANS.append(stpOvr) + + return SCANS + + # Main planar scan functions + def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc): + cmds = list() + rtpd = False + horizGC = 'G0' + hSpeed = self.horizRapid + height = obj.SafeHeight.Value + + if obj.CutPattern in ['Line', 'Circular']: + if obj.OptimizeStepOverTransitions is True: + height = minSTH + 2.0 + # if obj.LayerMode == 'Multi-pass': + # rtpd = minSTH + elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: + if obj.OptimizeStepOverTransitions is True: + zChng = first.z - lstPnt.z + # PathLog.debug('first.z: {}'.format(first.z)) + # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z)) + # PathLog.debug('zChng: {}'.format(zChng)) + # PathLog.debug('minSTH: {}'.format(minSTH)) + if abs(zChng) < tolrnc: # transitions to same Z height + # PathLog.debug('abs(zChng) < tolrnc') + if (minSTH - first.z) > tolrnc: + # PathLog.debug('(minSTH - first.z) > tolrnc') + height = minSTH + 2.0 + else: + # PathLog.debug('ELSE (minSTH - first.z) > tolrnc') + horizGC = 'G1' + height = first.z + elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z): + height = False # allow end of Zig to cut to beginning of Zag + + + # Create raise, shift, and optional lower commands + if height is not False: + cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) + cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) + if rtpd is not False: # ReturnToPreviousDepth + cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) + + return cmds + + def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc): + cmds = list() + rtpd = False + horizGC = 'G0' + hSpeed = self.horizRapid + height = obj.SafeHeight.Value + + if obj.CutPattern in ['Line', 'Circular']: + if obj.OptimizeStepOverTransitions is True: + height = minSTH + 2.0 + elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: + if obj.OptimizeStepOverTransitions is True: + zChng = first.z - lstPnt.z + if abs(zChng) < tolrnc: # transitions to same Z height + if (minSTH - first.z) > tolrnc: + height = minSTH + 2.0 + else: + height = first.z + 2.0 # first.z + + cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) + cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) + if rtpd is not False: # ReturnToPreviousDepth + cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) + + return cmds + + def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False): + pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object + pdc.setSTL(stl) # add stl model + if useSafeCutter is True: + pdc.setCutter(self.safeCutter) # add safeCutter + else: + pdc.setCutter(self.cutter) # add cutter + pdc.setZ(finalDep) # set minimumZ (final / target depth value) + pdc.setSampling(SampleInterval) # set sampling size + return pdc + + # Main waterline functions + def _oclWaterlineOp(self, JOB, obj, mdlIdx, subShp=None): + '''_oclWaterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.''' + commands = [] + + t_begin = time.time() + # JOB = PathUtils.findParentJob(obj) + base = JOB.Model.Group[mdlIdx] + bb = self.boundBoxes[mdlIdx] + stl = self.modelSTLs[mdlIdx] + + # Prepare global holdpoint and layerEndPnt containers + if self.holdPoint is None: + self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) + if self.layerEndPnt is None: + self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf")) + + # Set extra offset to diameter of cutter to allow cutter to move around perimeter of model + toolDiam = self.cutter.getDiameter() + cdeoX = 0.6 * toolDiam + cdeoY = 0.6 * toolDiam + + if subShp is None: + # Get correct boundbox + if obj.BoundBox == 'Stock': + BS = JOB.Stock + bb = BS.Shape.BoundBox + elif obj.BoundBox == 'BaseBoundBox': + BS = base + bb = base.Shape.BoundBox + + env = PathUtils.getEnvelope(partshape=BS.Shape, depthparams=self.depthParams) # Produces .Shape + + xmin = bb.XMin + xmax = bb.XMax + ymin = bb.YMin + ymax = bb.YMax + zmin = bb.ZMin + zmax = bb.ZMax + else: + xmin = subShp.BoundBox.XMin + xmax = subShp.BoundBox.XMax + ymin = subShp.BoundBox.YMin + ymax = subShp.BoundBox.YMax + zmin = subShp.BoundBox.ZMin + zmax = subShp.BoundBox.ZMax + + smplInt = obj.SampleInterval.Value + minSampInt = 0.001 # value is mm + if smplInt < minSampInt: + smplInt = minSampInt + + # Determine bounding box length for the OCL scan + bbLength = math.fabs(ymax - ymin) + numScanLines = int(math.ceil(bbLength / smplInt) + 1) # Number of lines + + # Compute number and size of stepdowns, and final depth + if obj.LayerMode == 'Single-pass': + depthparams = [obj.FinalDepth.Value] + else: + depthparams = [dp for dp in self.depthParams] + lenDP = len(depthparams) + + # Prepare PathDropCutter objects with STL data + safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx], + depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) + + # Scan the piece to depth at smplInt + oclScan = [] + oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines) + # oclScan = SCANS + lenOS = len(oclScan) + ptPrLn = int(lenOS / numScanLines) + + # Convert oclScan list of points to multi-dimensional list + scanLines = [] + for L in range(0, numScanLines): + scanLines.append([]) + for P in range(0, ptPrLn): + pi = L * ptPrLn + P + scanLines[L].append(oclScan[pi]) + lenSL = len(scanLines) + pntsPerLine = len(scanLines[0]) + PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line") + + # Extract Wl layers per depthparams + lyr = 0 + cmds = [] + layTime = time.time() + self.topoMap = [] + for layDep in depthparams: + cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) + commands.extend(cmds) + lyr += 1 + PathLog.debug("--All layer scans combined took " + str(time.time() - layTime) + " s") + return commands + + def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines): + '''_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ... + Perform OCL scan for waterline purpose.''' + pdc = ocl.PathDropCutter() # create a pdc + pdc.setSTL(stl) + pdc.setCutter(self.cutter) + pdc.setZ(fd) # set minimumZ (final / target depth value) + pdc.setSampling(smplInt) + + # Create line object as path + path = ocl.Path() # create an empty path object + for nSL in range(0, numScanLines): + yVal = ymin + (nSL * smplInt) + p1 = ocl.Point(xmin, yVal, fd) # start-point of line + p2 = ocl.Point(xmax, yVal, fd) # end-point of line + path.append(ocl.Line(p1, p2)) + # path.append(l) # add the line to the path + pdc.setPath(path) + pdc.run() # run drop-cutter on the path + + # return the list the points + return pdc.getCLPoints() + + def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine): + '''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.''' + commands = [] + cmds = [] + loopList = [] + self.topoMap = [] + # Create topo map from scanLines (highs and lows) + self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine) + # Add buffer lines and columns to topo map + self._bufferTopoMap(lenSL, pntsPerLine) + # Identify layer waterline from OCL scan + self._highlightWaterline(4, 9) + # Extract waterline and convert to gcode + loopList = self._extractWaterlines(obj, scanLines, lyr, layDep) + # save commands + for loop in loopList: + cmds = self._loopToGcode(obj, layDep, loop) + commands.extend(cmds) + return commands + + def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine): + '''_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.''' + topoMap = [] + for L in range(0, lenSL): + topoMap.append([]) + for P in range(0, pntsPerLine): + if scanLines[L][P].z > layDep: + topoMap[L].append(2) + else: + topoMap[L].append(0) + return topoMap + + def _bufferTopoMap(self, lenSL, pntsPerLine): + '''_bufferTopoMap(lenSL, pntsPerLine) ... Add buffer boarder of zeros to all sides to topoMap data.''' + pre = [0, 0] + post = [0, 0] + for p in range(0, pntsPerLine): + pre.append(0) + post.append(0) + for l in range(0, lenSL): + self.topoMap[l].insert(0, 0) + self.topoMap[l].append(0) + self.topoMap.insert(0, pre) + self.topoMap.append(post) + return True + + def _highlightWaterline(self, extraMaterial, insCorn): + '''_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.''' + TM = self.topoMap + lastPnt = len(TM[1]) - 1 + lastLn = len(TM) - 1 + highFlag = 0 + + # ("--Convert parallel data to ridges") + for lin in range(1, lastLn): + for pt in range(1, lastPnt): # Ignore first and last points + if TM[lin][pt] == 0: + if TM[lin][pt + 1] == 2: # step up + TM[lin][pt] = 1 + if TM[lin][pt - 1] == 2: # step down + TM[lin][pt] = 1 + + # ("--Convert perpendicular data to ridges and highlight ridges") + for pt in range(1, lastPnt): # Ignore first and last points + for lin in range(1, lastLn): + if TM[lin][pt] == 0: + highFlag = 0 + if TM[lin + 1][pt] == 2: # step up + TM[lin][pt] = 1 + if TM[lin - 1][pt] == 2: # step down + TM[lin][pt] = 1 + elif TM[lin][pt] == 2: + highFlag += 1 + if highFlag == 3: + if TM[lin - 1][pt - 1] < 2 or TM[lin - 1][pt + 1] < 2: + highFlag = 2 + else: + TM[lin - 1][pt] = extraMaterial + highFlag = 2 + + # ("--Square corners") + for pt in range(1, lastPnt): + for lin in range(1, lastLn): + if TM[lin][pt] == 1: # point == 1 + cont = True + if TM[lin + 1][pt] == 0: # forward == 0 + if TM[lin + 1][pt - 1] == 1: # forward left == 1 + if TM[lin][pt - 1] == 2: # left == 2 + TM[lin + 1][pt] = 1 # square the corner + cont = False + + if cont is True and TM[lin + 1][pt + 1] == 1: # forward right == 1 + if TM[lin][pt + 1] == 2: # right == 2 + TM[lin + 1][pt] = 1 # square the corner + cont = True + + if TM[lin - 1][pt] == 0: # back == 0 + if TM[lin - 1][pt - 1] == 1: # back left == 1 + if TM[lin][pt - 1] == 2: # left == 2 + TM[lin - 1][pt] = 1 # square the corner + cont = False + + if cont is True and TM[lin - 1][pt + 1] == 1: # back right == 1 + if TM[lin][pt + 1] == 2: # right == 2 + TM[lin - 1][pt] = 1 # square the corner + + # remove inside corners + for pt in range(1, lastPnt): + for lin in range(1, lastLn): + if TM[lin][pt] == 1: # point == 1 + if TM[lin][pt + 1] == 1: + if TM[lin - 1][pt + 1] == 1 or TM[lin + 1][pt + 1] == 1: + TM[lin][pt + 1] = insCorn + elif TM[lin][pt - 1] == 1: + if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1: + TM[lin][pt - 1] = insCorn + + return True + + def _extractWaterlines(self, obj, oclScan, lyr, layDep): + '''_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.''' + srch = True + lastPnt = len(self.topoMap[0]) - 1 + lastLn = len(self.topoMap) - 1 + maxSrchs = 5 + srchCnt = 1 + loopList = [] + loop = [] + loopNum = 0 + + if self.CutClimb is True: + lC = [-1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0] + pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] + else: + lC = [1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0] + pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] + + while srch is True: + srch = False + if srchCnt > maxSrchs: + PathLog.debug("Max search scans, " + str(maxSrchs) + " reached\nPossible incomplete waterline result!") + break + for L in range(1, lastLn): + for P in range(1, lastPnt): + if self.topoMap[L][P] == 1: + # start loop follow + srch = True + loopNum += 1 + loop = self._trackLoop(oclScan, lC, pC, L, P, loopNum) + self.topoMap[L][P] = 0 # Mute the starting point + loopList.append(loop) + srchCnt += 1 + PathLog.debug("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.") + return loopList + + def _trackLoop(self, oclScan, lC, pC, L, P, loopNum): + '''_trackLoop(oclScan, lC, pC, L, P, loopNum) ... Track the loop direction.''' + loop = [oclScan[L - 1][P - 1]] # Start loop point list + cur = [L, P, 1] + prv = [L, P - 1, 1] + nxt = [L, P + 1, 1] + follow = True + ptc = 0 + ptLmt = 200000 + while follow is True: + ptc += 1 + if ptc > ptLmt: + PathLog.debug("Loop number " + str(loopNum) + " at [" + str(nxt[0]) + ", " + str(nxt[1]) + "] pnt count exceeds, " + str(ptLmt) + ". Stopped following loop.") + break + nxt = self._findNextWlPoint(lC, pC, cur[0], cur[1], prv[0], prv[1]) # get next point + loop.append(oclScan[nxt[0] - 1][nxt[1] - 1]) # add it to loop point list + self.topoMap[nxt[0]][nxt[1]] = nxt[2] # Mute the point, if not Y stem + if nxt[0] == L and nxt[1] == P: # check if loop complete + follow = False + elif nxt[0] == cur[0] and nxt[1] == cur[1]: # check if line cannot be detected + follow = False + prv = cur + cur = nxt + return loop + + def _findNextWlPoint(self, lC, pC, cl, cp, pl, pp): + '''_findNextWlPoint(lC, pC, cl, cp, pl, pp) ... + Find the next waterline point in the point cloud layer provided.''' + dl = cl - pl + dp = cp - pp + num = 0 + i = 3 + s = 0 + mtch = 0 + found = False + while mtch < 8: # check all 8 points around current point + if lC[i] == dl: + if pC[i] == dp: + s = i - 3 + found = True + # Check for y branch where current point is connection between branches + for y in range(1, mtch): + if lC[i + y] == dl: + if pC[i + y] == dp: + num = 1 + break + break + i += 1 + mtch += 1 + if found is False: + # ("_findNext: No start point found.") + return [cl, cp, num] + + for r in range(0, 8): + l = cl + lC[s + r] + p = cp + pC[s + r] + if self.topoMap[l][p] == 1: + return [l, p, num] + + # ("_findNext: No next pnt found") + return [cl, cp, num] + + def _loopToGcode(self, obj, layDep, loop): + '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.''' + # generate the path commands + output = [] + optimize = obj.OptimizeLinearPaths + + prev = ocl.Point(float("inf"), float("inf"), float("inf")) + nxt = ocl.Point(float("inf"), float("inf"), float("inf")) + pnt = ocl.Point(float("inf"), float("inf"), float("inf")) + + # Create first point + pnt.x = loop[0].x + pnt.y = loop[0].y + pnt.z = layDep + + # Position cutter to begin loop + output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) + output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) + + lenCLP = len(loop) + lastIdx = lenCLP - 1 + # Cycle through each point on loop + for i in range(0, lenCLP): + if i < lastIdx: + nxt.x = loop[i + 1].x + nxt.y = loop[i + 1].y + nxt.z = layDep + else: + optimize = False + + if not optimize or not self.isPointOnLine(FreeCAD.Vector(prev.x, prev.y, prev.z), FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)): + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizFeed})) + + # Rotate point data + prev.x = pnt.x + prev.y = pnt.y + prev.z = pnt.z + pnt.x = nxt.x + pnt.y = nxt.y + pnt.z = nxt.z + + # Save layer end point for use in transitioning to next layer + self.layerEndPnt.x = pnt.x + self.layerEndPnt.y = pnt.y + self.layerEndPnt.z = pnt.z + + return output + + # Main waterline functions + def _experimentalWaterlineOp(self, JOB, obj, mdlIdx, subShp=None): + '''_waterlineOp(JOB, obj, mdlIdx, subShp=None) ... + Main waterline function to perform waterline extraction from model.''' + PathLog.debug('_experimentalWaterlineOp()') + + msg = translate('PathWaterline', 'Experimental Waterline does not currently support selected faces.') + PathLog.info('\n..... ' + msg) + + commands = [] + t_begin = time.time() + base = JOB.Model.Group[mdlIdx] + bb = self.boundBoxes[mdlIdx] + stl = self.modelSTLs[mdlIdx] + safeSTL = self.safeSTLs[mdlIdx] + self.endVector = None + + finDep = obj.FinalDepth.Value + (self.geoTlrnc / 10.0) + depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, finDep) + + # Compute number and size of stepdowns, and final depth + if obj.LayerMode == 'Single-pass': + depthparams = [finDep] + else: + depthparams = [dp for dp in depthParams] + lenDP = len(depthparams) + PathLog.debug('Experimental Waterline depthparams:\n{}'.format(depthparams)) + + # Prepare PathDropCutter objects with STL data + # safePDC = self._planarGetPDC(safeSTL, depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) + + buffer = self.cutter.getDiameter() * 2.0 + borderFace = Part.Face(self._makeExtendedBoundBox(JOB.Stock.Shape.BoundBox, buffer, 0.0)) + + # Get correct boundbox + if obj.BoundBox == 'Stock': + stockEnv = self._getShapeEnvelope(JOB.Stock.Shape) + bbFace = self._getCrossSection(stockEnv) # returned at Z=0.0 + elif obj.BoundBox == 'BaseBoundBox': + baseEnv = self._getShapeEnvelope(base.Shape) + bbFace = self._getCrossSection(baseEnv) # returned at Z=0.0 + + trimFace = borderFace.cut(bbFace) + if self.showDebugObjects is True: + TF = FreeCAD.ActiveDocument.addObject('Part::Feature', 'trimFace') + TF.Shape = trimFace + TF.purgeTouched() + self.tempGroup.addObject(TF) + + # Cycle through layer depths + CUTAREAS = self._getCutAreas(base.Shape, depthparams, bbFace, trimFace, borderFace) + if not CUTAREAS: + PathLog.error('No cross-section cut areas identified.') + return commands + + caCnt = 0 + ofst = obj.BoundaryAdjustment.Value + ofst -= self.radius # (self.radius + (tolrnc / 10.0)) + caLen = len(CUTAREAS) + lastCA = caLen - 1 + lastClearArea = None + lastCsHght = None + clearLastLayer = True + for ca in range(0, caLen): + area = CUTAREAS[ca] + csHght = area.BoundBox.ZMin + csHght += obj.DepthOffset.Value + cont = False + caCnt += 1 + if area.Area > 0.0: + cont = True + caWireCnt = len(area.Wires) - 1 # first wire is boundFace wire + PathLog.debug('cutAreaWireCnt: {}'.format(caWireCnt)) + if self.showDebugObjects is True: + CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'cutArea_{}'.format(caCnt)) + CA.Shape = area + CA.purgeTouched() + self.tempGroup.addObject(CA) + else: + PathLog.error('Cut area at {} is zero.'.format(round(csHght, 4))) + + # get offset wire(s) based upon cross-section cut area + if cont: + area.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - area.BoundBox.ZMin)) + activeArea = area.cut(trimFace) + activeAreaWireCnt = len(activeArea.Wires) # first wire is boundFace wire + PathLog.debug('activeAreaWireCnt: {}'.format(activeAreaWireCnt)) + if self.showDebugObjects is True: + CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'activeArea_{}'.format(caCnt)) + CA.Shape = activeArea + CA.purgeTouched() + self.tempGroup.addObject(CA) + ofstArea = self._extractFaceOffset(obj, activeArea, ofst, makeComp=False) + if not ofstArea: + PathLog.error('No offset area returned for cut area depth: {}'.format(csHght)) + cont = False + + if cont: + # Identify solid areas in the offset data + ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea) + if ofstSolidFacesList: + clearArea = Part.makeCompound(ofstSolidFacesList) + if self.showDebugObjects is True: + CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearArea_{}'.format(caCnt)) + CA.Shape = clearArea + CA.purgeTouched() + self.tempGroup.addObject(CA) + else: + cont = False + PathLog.error('ofstSolids is False.') + + if cont: + # Make waterline path for current CUTAREA depth (csHght) + commands.extend(self._wiresToWaterlinePath(obj, clearArea, csHght)) + clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clearArea.BoundBox.ZMin)) + lastClearArea = clearArea + lastCsHght = csHght + + # Clear layer as needed + (useOfst, usePat, clearLastLayer) = self._clearLayer(obj, ca, lastCA, clearLastLayer) + ##if self.showDebugObjects is True and (usePat or useOfst): + ## OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearPatternArea_{}'.format(round(csHght, 2))) + ## OA.Shape = clearArea + ## OA.purgeTouched() + ## self.tempGroup.addObject(OA) + if usePat: + commands.extend(self._makeCutPatternLayerPaths(JOB, obj, clearArea, csHght)) + if useOfst: + commands.extend(self._makeOffsetLayerPaths(JOB, obj, clearArea, csHght)) + # Efor + + if clearLastLayer: + (useOfst, usePat, cLL) = self._clearLayer(obj, 1, 1, False) + clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - lastClearArea.BoundBox.ZMin)) + if usePat: + commands.extend(self._makeCutPatternLayerPaths(JOB, obj, lastClearArea, lastCsHght)) + + if useOfst: + commands.extend(self._makeOffsetLayerPaths(JOB, obj, lastClearArea, lastCsHght)) + + PathLog.info("Waterline: All layer scans combined took " + str(time.time() - t_begin) + " s") + return commands + + def _getCutAreas(self, shape, depthparams, bbFace, trimFace, borderFace): + '''_getCutAreas(JOB, shape, depthparams, bbFace, borderFace) ... + Takes shape, depthparams and base-envelope-cross-section, and + returns a list of cut areas - one for each depth.''' + PathLog.debug('_getCutAreas()') + + CUTAREAS = list() + lastLayComp = None + isFirst = True + lenDP = len(depthparams) + + # Cycle through layer depths + for dp in range(0, lenDP): + csHght = depthparams[dp] + PathLog.debug('Depth {} is {}'.format(dp + 1, csHght)) + + # Get slice at depth of shape + csFaces = self._getModelCrossSection(shape, csHght) # returned at Z=0.0 + if not csFaces: + PathLog.error('No cross-section wires at {}'.format(csHght)) + else: + PathLog.debug('cross-section face count {}'.format(len(csFaces))) + if len(csFaces) > 0: + useFaces = self._getSolidAreasFromPlanarFaces(csFaces) + else: + useFaces = False + + if useFaces: + PathLog.debug('useFacesCnt: {}'.format(len(useFaces))) + compAdjFaces = Part.makeCompound(useFaces) + + if self.showDebugObjects is True: + CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSolids_{}'.format(dp + 1)) + CA.Shape = compAdjFaces + CA.purgeTouched() + self.tempGroup.addObject(CA) + + if isFirst: + allPrevComp = compAdjFaces + cutArea = borderFace.cut(compAdjFaces) + else: + preCutArea = borderFace.cut(compAdjFaces) + cutArea = preCutArea.cut(allPrevComp) # cut out higher layers to avoid cutting recessed areas + allPrevComp = allPrevComp.fuse(compAdjFaces) + cutArea.translate(FreeCAD.Vector(0.0, 0.0, csHght - cutArea.BoundBox.ZMin)) + CUTAREAS.append(cutArea) + isFirst = False + else: + PathLog.error('No waterline at depth: {} mm.'.format(csHght)) + # Efor + + if len(CUTAREAS) > 0: + return CUTAREAS + + return False + + def _wiresToWaterlinePath(self, obj, ofstPlnrShp, csHght): + PathLog.debug('_wiresToWaterlinePath()') + commands = list() + + # Translate path geometry to layer height + ofstPlnrShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - ofstPlnrShp.BoundBox.ZMin)) + if self.showDebugObjects is True: + OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'waterlinePathArea_{}'.format(round(csHght, 2))) + OA.Shape = ofstPlnrShp + OA.purgeTouched() + self.tempGroup.addObject(OA) + + commands.append(Path.Command('N (Cut Area {}.)'.format(round(csHght, 2)))) + for w in range(0, len(ofstPlnrShp.Wires)): + wire = ofstPlnrShp.Wires[w] + V = wire.Vertexes + if obj.CutMode == 'Climb': + lv = len(V) - 1 + startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z) + else: + startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z) + + commands.append(Path.Command('N (Wire {}.)'.format(w))) + (cmds, endVect) = self._wireToPath(obj, wire, startVect) + commands.extend(cmds) + commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + + return commands + + def _makeCutPatternLayerPaths(self, JOB, obj, clrAreaShp, csHght): + PathLog.debug('_makeCutPatternLayerPaths()') + commands = [] + + clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clrAreaShp.BoundBox.ZMin)) + pathGeom = self._planarMakePathGeom(obj, clrAreaShp) + pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin)) + # clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin)) + + if self.showDebugObjects is True: + OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'pathGeom_{}'.format(round(csHght, 2))) + OA.Shape = pathGeom + OA.purgeTouched() + self.tempGroup.addObject(OA) + + # Convert pathGeom to gcode more efficiently + if True: + if obj.CutPattern == 'Offset': + commands.extend(self._makeOffsetLayerPaths(JOB, obj, clrAreaShp, csHght)) + else: + clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin)) + if obj.CutPattern == 'Line': + pntSet = self._pathGeomToLinesPointSet(obj, pathGeom) + elif obj.CutPattern == 'ZigZag': + pntSet = self._pathGeomToZigzagPointSet(obj, pathGeom) + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + pntSet = self._pathGeomToArcPointSet(obj, pathGeom) + stpOVRS = self._getExperimentalWaterlinePaths(obj, pntSet, csHght) + # PathLog.debug('stpOVRS:\n{}'.format(stpOVRS)) + safePDC = False + cmds = self._clearGeomToPaths(JOB, obj, safePDC, stpOVRS, csHght) + commands.extend(cmds) + else: + # Use Path.fromShape() to convert edges to paths + for w in range(0, len(pathGeom.Edges)): + wire = pathGeom.Edges[w] + V = wire.Vertexes + if obj.CutMode == 'Climb': + lv = len(V) - 1 + startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z) + else: + startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z) + + commands.append(Path.Command('N (Wire {}.)'.format(w))) + (cmds, endVect) = self._wireToPath(obj, wire, startVect) + commands.extend(cmds) + commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + + return commands + + def _makeOffsetLayerPaths(self, JOB, obj, clrAreaShp, csHght): + PathLog.debug('_makeOffsetLayerPaths()') + PathLog.warning('Using `Offset` for clearing bottom layer.') + cmds = list() + # ofst = obj.BoundaryAdjustment.Value + ofst = 0.0 - self.cutOut # - self.cutter.getDiameter() # (self.radius + (tolrnc / 10.0)) + shape = clrAreaShp + cont = True + cnt = 0 + while cont: + ofstArea = self._extractFaceOffset(obj, shape, ofst, makeComp=True) + if not ofstArea: + PathLog.warning('No offset clearing area returned.') + break + for F in ofstArea.Faces: + cmds.extend(self._wiresToWaterlinePath(obj, F, csHght)) + shape = ofstArea + if cnt == 0: + ofst = 0.0 - self.cutOut # self.cutter.Diameter() + cnt += 1 + return cmds + + def _clearGeomToPaths(self, JOB, obj, safePDC, SCANDATA, csHght): + PathLog.debug('_clearGeomToPaths()') + + GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})] + tolrnc = JOB.GeometryTolerance.Value + prevDepth = obj.SafeHeight.Value + lenSCANDATA = len(SCANDATA) + gDIR = ['G3', 'G2'] + + if self.CutClimb is True: + gDIR = ['G2', 'G3'] + + # Send cutter to x,y position of first point on first line + first = SCANDATA[0][0][0] # [step][item][point] + GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) + + # Cycle through step-over sections (line segments or arcs) + odd = True + lstStpEnd = None + prevDepth = obj.SafeHeight.Value # Not used for Single-pass + for so in range(0, lenSCANDATA): + cmds = list() + PRTS = SCANDATA[so] + lenPRTS = len(PRTS) + first = PRTS[0][0] # first point of arc/line stepover group + start = PRTS[0][0] # will change with each line/arc segment + last = None + cmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) + + if so > 0: + if obj.CutPattern == 'CircularZigZag': + if odd is True: + odd = False + else: + odd = True + # minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL + minTrnsHght = obj.SafeHeight.Value + # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {})) + cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc)) + + # Cycle through current step-over parts + for i in range(0, lenPRTS): + prt = PRTS[i] + lenPrt = len(prt) + # PathLog.debug('prt: {}'.format(prt)) + if prt == 'BRK': + nxtStart = PRTS[i + 1][0] + # minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL + minSTH = obj.SafeHeight.Value + cmds.append(Path.Command('N (Break)', {})) + cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) + else: + cmds.append(Path.Command('N (part {}.)'.format(i + 1), {})) + if obj.CutPattern in ['Line', 'ZigZag']: + start, last = prt + cmds.append(Path.Command('G1', {'X': start.x, 'Y': start.y, 'Z': start.z, 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': last.x, 'Y': last.y, 'F': self.horizFeed})) + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + start, last, centPnt, cMode = prt + gcode = self._makeGcodeArc(start, last, odd, gDIR, tolrnc) + cmds.extend(gcode) + cmds.append(Path.Command('N (End of step {}.)'.format(so), {})) + GCODE.extend(cmds) # save line commands + lstStpEnd = last + # Efor + + return GCODE + + def _getSolidAreasFromPlanarFaces(self, csFaces): + PathLog.debug('_getSolidAreasFromPlanarFaces()') + holds = list() + cutFaces = list() + useFaces = list() + lenCsF = len(csFaces) + PathLog.debug('lenCsF: {}'.format(lenCsF)) + + if lenCsF == 1: + useFaces = csFaces + else: + fIds = list() + aIds = list() + pIds = list() + cIds = list() + + for af in range(0, lenCsF): + fIds.append(af) # face ids + aIds.append(af) # face ids + pIds.append(-1) # parent ids + cIds.append(False) # cut ids + holds.append(False) + + while len(fIds) > 0: + li = fIds.pop() + low = csFaces[li] # senior face + pIds = self._idInternalFeature(csFaces, fIds, pIds, li, low) + # Ewhile + ##PathLog.info('fIds: {}'.format(fIds)) + ##PathLog.info('pIds: {}'.format(pIds)) + + for af in range(lenCsF - 1, -1, -1): # cycle from last item toward first + ##PathLog.info('af: {}'.format(af)) + prnt = pIds[af] + ##PathLog.info('prnt: {}'.format(prnt)) + if prnt == -1: + stack = -1 + else: + stack = [af] + # get_face_ids_to_parent + stack.insert(0, prnt) + nxtPrnt = pIds[prnt] + # find af value for nxtPrnt + while nxtPrnt != -1: + stack.insert(0, nxtPrnt) + nxtPrnt = pIds[nxtPrnt] + cIds[af] = stack + # PathLog.debug('cIds: {}\n'.format(cIds)) + + for af in range(0, lenCsF): + # PathLog.debug('af is {}'.format(af)) + pFc = cIds[af] + if pFc == -1: + # Simple, independent region + holds[af] = csFaces[af] # place face in hold + # PathLog.debug('pFc == -1') + else: + # Compound region + # PathLog.debug('pFc is not -1') + cnt = len(pFc) + if cnt % 2.0 == 0.0: + # even is donut cut + # PathLog.debug('cnt is even') + inr = pFc[cnt - 1] + otr = pFc[cnt - 2] + # PathLog.debug('inr / otr: {} / {}'.format(inr, otr)) + holds[otr] = holds[otr].cut(csFaces[inr]) + else: + # odd is floating solid + # PathLog.debug('cnt is ODD') + holds[af] = csFaces[af] + # Efor + + for af in range(0, lenCsF): + if holds[af]: + useFaces.append(holds[af]) # save independent solid + + # Eif + + if len(useFaces) > 0: + return useFaces + + return False + + def _getModelCrossSection(self, shape, csHght): + PathLog.debug('_getCrossSection()') + wires = list() + + def byArea(fc): + return fc.Area + + for i in shape.slice(FreeCAD.Vector(0, 0, 1), csHght): + wires.append(i) + + if len(wires) > 0: + for w in wires: + if w.isClosed() is False: + return False + FCS = list() + for w in wires: + w.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - w.BoundBox.ZMin)) + FCS.append(Part.Face(w)) + FCS.sort(key=byArea, reverse=True) + return FCS + else: + PathLog.debug(' -No wires from .slice() method') + + return False + + def _isInBoundBox(self, outShp, inShp): + obb = outShp.BoundBox + ibb = inShp.BoundBox + + if obb.XMin < ibb.XMin: + if obb.XMax > ibb.XMax: + if obb.YMin < ibb.YMin: + if obb.YMax > ibb.YMax: + return True + return False + + def _idInternalFeature(self, csFaces, fIds, pIds, li, low): + Ids = list() + for i in fIds: + Ids.append(i) + while len(Ids) > 0: + hi = Ids.pop() + high = csFaces[hi] + if self._isInBoundBox(high, low): + cmn = high.common(low) + if cmn.Area > 0.0: + pIds[li] = hi + break + # Ewhile + return pIds + + def _wireToPath(self, obj, wire, startVect): + '''_wireToPath(obj, wire, startVect) ... wire to path.''' + PathLog.track() + + paths = [] + pathParams = {} # pylint: disable=assignment-from-no-return + V = wire.Vertexes + + pathParams['shapes'] = [wire] + pathParams['feedrate'] = self.horizFeed + pathParams['feedrate_v'] = self.vertFeed + pathParams['verbose'] = True + pathParams['resume_height'] = obj.SafeHeight.Value + pathParams['retraction'] = obj.ClearanceHeight.Value + pathParams['return_end'] = True + # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers + pathParams['preamble'] = False + pathParams['start'] = startVect + + (pp, end_vector) = Path.fromShapes(**pathParams) + paths.extend(pp.Commands) + # PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector)) + + self.endVector = end_vector # pylint: disable=attribute-defined-outside-init + + return (paths, end_vector) + + def _makeExtendedBoundBox(self, wBB, bbBfr, zDep): + pl = FreeCAD.Placement() + pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) + pl.Base = FreeCAD.Vector(0, 0, 0) + + p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep) + p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep) + p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep) + p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep) + bb = Part.makePolygon([p1, p2, p3, p4, p1]) + + return bb + + def _makeGcodeArc(self, strtPnt, endPnt, odd, gDIR, tolrnc): + cmds = list() + isCircle = False + inrPnt = None + gdi = 0 + if odd is True: + gdi = 1 + + # Test if pnt set is circle + if abs(strtPnt.x - endPnt.x) < tolrnc: + if abs(strtPnt.y - endPnt.y) < tolrnc: + isCircle = True + isCircle = False + + if isCircle is True: + # convert LN to G2/G3 arc, consolidating GCode + # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc + # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/ + # Dividing circle into two arcs allows for G2/G3 on inclined surfaces + + # ijk = self.tmpCOM - strtPnt # vector from start to center + ijk = self.tmpCOM - strtPnt # vector from start to center + xyz = self.tmpCOM.add(ijk) # end point + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed})) + ijk = self.tmpCOM - xyz # vector from start to center + rst = strtPnt # end point + cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + else: + # ijk = self.tmpCOM - strtPnt + ijk = self.tmpCOM.sub(strtPnt) # vector from start to center + xyz = endPnt + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed})) + + return cmds + + def _clearLayer(self, obj, ca, lastCA, clearLastLayer): + PathLog.debug('_clearLayer()') + usePat = False + useOfst = False + + if obj.ClearLastLayer == 'Off': + if obj.CutPattern != 'None': + usePat = True + else: + if ca == lastCA: + PathLog.debug('... Clearing bottom layer.') + if obj.ClearLastLayer == 'Offset': + obj.CutPattern = 'None' + useOfst = True + else: + obj.CutPattern = obj.ClearLastLayer + usePat = True + clearLastLayer = False + + return (useOfst, usePat, clearLastLayer) + + # Support functions for both dropcutter and waterline operations + def isPointOnLine(self, strtPnt, endPnt, pointP): + '''isPointOnLine(strtPnt, endPnt, pointP) ... Determine if a given point is on the line defined by start and end points.''' + tolerance = 1e-6 + vectorAB = endPnt - strtPnt + vectorAC = pointP - strtPnt + crossproduct = vectorAB.cross(vectorAC) + dotproduct = vectorAB.dot(vectorAC) + + if crossproduct.Length > tolerance: + return False + + if dotproduct < 0: + return False + + if dotproduct > vectorAB.Length * vectorAB.Length: + return False + + return True + + def resetOpVariables(self, all=True): + '''resetOpVariables() ... Reset class variables used for instance of operation.''' + self.holdPoint = None + self.layerEndPnt = None + self.onHold = False + self.SafeHeightOffset = 2.0 + self.ClearHeightOffset = 4.0 + self.layerEndzMax = 0.0 + self.resetTolerance = 0.0 + self.holdPntCnt = 0 + self.bbRadius = 0.0 + self.axialFeed = 0.0 + self.axialRapid = 0.0 + self.FinalDepth = 0.0 + self.clearHeight = 0.0 + self.safeHeight = 0.0 + self.faceZMax = -999999999999.0 + if all is True: + self.cutter = None + self.stl = None + self.fullSTL = None + self.cutOut = 0.0 + self.radius = 0.0 + self.useTiltCutter = False + return True + + def deleteOpVariables(self, all=True): + '''deleteOpVariables() ... Reset class variables used for instance of operation.''' + del self.holdPoint + del self.layerEndPnt + del self.onHold + del self.SafeHeightOffset + del self.ClearHeightOffset + del self.layerEndzMax + del self.resetTolerance + del self.holdPntCnt + del self.bbRadius + del self.axialFeed + del self.axialRapid + del self.FinalDepth + del self.clearHeight + del self.safeHeight + del self.faceZMax + if all is True: + del self.cutter + del self.stl + del self.fullSTL + del self.cutOut + del self.radius + del self.useTiltCutter + return True + + def setOclCutter(self, obj, safe=False): + ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. ''' + # Set cutter details + # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details + diam_1 = float(obj.ToolController.Tool.Diameter) + lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0 + FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0 + CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 + CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0 + + # Make safeCutter with 2 mm buffer around physical cutter + if safe is True: + diam_1 += 4.0 + if FR != 0.0: + FR += 2.0 + + PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType)) + if obj.ToolController.Tool.ToolType == 'EndMill': + # Standard End Mill + return ocl.CylCutter(diam_1, (CEH + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0: + # Standard Ball End Mill + # OCL -> BallCutter::BallCutter(diameter, length) + self.useTiltCutter = True + return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0: + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> BallCutter::BallCutter(diameter, length) + return ocl.BullCutter(diam_1, FR, (CEH + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0: + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) + return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) + + elif obj.ToolController.Tool.ToolType == 'ChamferMill': + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) + return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) + else: + # Default to standard end mill + PathLog.warning("Defaulting cutter to standard end mill.") + return ocl.CylCutter(diam_1, (CEH + lenOfst)) + + # http://www.carbidecutter.net/products/carbide-burr-cone-shape-sm.html + ''' + # Available FreeCAD cutter types - some still need translation to available OCL cutter classes. + Drill, CenterDrill, CounterSink, CounterBore, FlyCutter, Reamer, Tap, + EndMill, SlotCutter, BallEndMill, ChamferMill, CornerRound, Engraver + ''' + # Adittional problem is with new ToolBit user-defined cutter shapes. + # Some sort of translation/conversion will have to be defined to make compatible with OCL. + PathLog.error('Unable to set OCL cutter.') + return False + + +def SetupProperties(): + ''' SetupProperties() ... Return list of properties required for operation.''' + setup = [] + setup.append('Algorithm') + setup.append('AngularDeflection') + setup.append('AvoidLastX_Faces') + setup.append('AvoidLastX_InternalFeatures') + setup.append('BoundBox') + setup.append('BoundaryAdjustment') + setup.append('CircularCenterAt') + setup.append('CircularCenterCustom') + setup.append('ClearLastLayer') + setup.append('CutMode') + setup.append('CutPattern') + setup.append('CutPatternAngle') + setup.append('CutPatternReversed') + setup.append('DepthOffset') + setup.append('GapSizes') + setup.append('GapThreshold') + setup.append('HandleMultipleFeatures') + setup.append('InternalFeaturesCut') + setup.append('InternalFeaturesAdjustment') + setup.append('LayerMode') + setup.append('LinearDeflection') + setup.append('OptimizeStepOverTransitions') + setup.append('ProfileEdges') + setup.append('BoundaryEnforcement') + setup.append('SampleInterval') + setup.append('StartPoint') + setup.append('StepOver') + setup.append('UseStartPoint') + # For debugging + setup.append('ShowTempObjects') + return setup + + +def Create(name, obj=None): + '''Create(name) ... Creates and returns a Waterline operation.''' + if obj is None: + obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) + obj.Proxy = ObjectWaterline(obj, name) + return obj diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py new file mode 100644 index 0000000000..eed15fc3d3 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2020 sliptonic * +# * Copyright (c) 2020 russ4262 * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +import FreeCADGui +import PathScripts.PathWaterline as PathWaterline +import PathScripts.PathGui as PathGui +import PathScripts.PathOpGui as PathOpGui + +from PySide import QtCore + +__title__ = "Path Waterline Operation UI" +__author__ = "sliptonic (Brad Collette), russ4262 (Russell Johnson)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Waterline operation page controller and command implementation." + + +class TaskPanelOpPage(PathOpGui.TaskPanelPage): + '''Page controller class for the Waterline operation.''' + + def initPage(self, obj): + # self.setTitle("Waterline") + self.updateVisibility() + + def getForm(self): + '''getForm() ... returns UI''' + return FreeCADGui.PySideUic.loadUi(":/panels/PageOpWaterlineEdit.ui") + + def getFields(self, obj): + '''getFields(obj) ... transfers values from UI to obj's proprties''' + self.updateToolController(obj, self.form.toolController) + + if obj.Algorithm != str(self.form.algorithmSelect.currentText()): + obj.Algorithm = str(self.form.algorithmSelect.currentText()) + + if obj.BoundBox != str(self.form.boundBoxSelect.currentText()): + obj.BoundBox = str(self.form.boundBoxSelect.currentText()) + + if obj.LayerMode != str(self.form.layerMode.currentText()): + obj.LayerMode = str(self.form.layerMode.currentText()) + + if obj.CutPattern != str(self.form.cutPattern.currentText()): + obj.CutPattern = str(self.form.cutPattern.currentText()) + + PathGui.updateInputField(obj, 'BoundaryAdjustment', self.form.boundaryAdjustment) + + if obj.StepOver != self.form.stepOver.value(): + obj.StepOver = self.form.stepOver.value() + + PathGui.updateInputField(obj, 'SampleInterval', self.form.sampleInterval) + + if obj.OptimizeLinearPaths != self.form.optimizeEnabled.isChecked(): + obj.OptimizeLinearPaths = self.form.optimizeEnabled.isChecked() + + def setFields(self, obj): + '''setFields(obj) ... transfers obj's property values to UI''' + self.setupToolController(obj, self.form.toolController) + self.selectInComboBox(obj.Algorithm, self.form.algorithmSelect) + self.selectInComboBox(obj.BoundBox, self.form.boundBoxSelect) + self.selectInComboBox(obj.LayerMode, self.form.layerMode) + self.selectInComboBox(obj.CutPattern, self.form.cutPattern) + self.form.boundaryAdjustment.setText(FreeCAD.Units.Quantity(obj.BoundaryAdjustment.Value, FreeCAD.Units.Length).UserString) + self.form.stepOver.setValue(obj.StepOver) + self.form.sampleInterval.setText(FreeCAD.Units.Quantity(obj.SampleInterval.Value, FreeCAD.Units.Length).UserString) + + if obj.OptimizeLinearPaths: + self.form.optimizeEnabled.setCheckState(QtCore.Qt.Checked) + else: + self.form.optimizeEnabled.setCheckState(QtCore.Qt.Unchecked) + + def getSignalsForUpdate(self, obj): + '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' + signals = [] + signals.append(self.form.toolController.currentIndexChanged) + signals.append(self.form.algorithmSelect.currentIndexChanged) + signals.append(self.form.boundBoxSelect.currentIndexChanged) + signals.append(self.form.layerMode.currentIndexChanged) + signals.append(self.form.cutPattern.currentIndexChanged) + signals.append(self.form.boundaryAdjustment.editingFinished) + signals.append(self.form.stepOver.editingFinished) + signals.append(self.form.sampleInterval.editingFinished) + signals.append(self.form.optimizeEnabled.stateChanged) + + return signals + + def updateVisibility(self): + if self.form.algorithmSelect.currentText() == 'OCL Dropcutter': + self.form.cutPattern.setEnabled(False) + self.form.boundaryAdjustment.setEnabled(False) + self.form.stepOver.setEnabled(False) + self.form.sampleInterval.setEnabled(True) + self.form.optimizeEnabled.setEnabled(True) + else: + self.form.cutPattern.setEnabled(True) + self.form.boundaryAdjustment.setEnabled(True) + if self.form.cutPattern.currentText() == 'None': + self.form.stepOver.setEnabled(False) + else: + self.form.stepOver.setEnabled(True) + self.form.sampleInterval.setEnabled(False) + self.form.optimizeEnabled.setEnabled(False) + + def registerSignalHandlers(self, obj): + self.form.algorithmSelect.currentIndexChanged.connect(self.updateVisibility) + self.form.cutPattern.currentIndexChanged.connect(self.updateVisibility) + + +Command = PathOpGui.SetupOperation('Waterline', + PathWaterline.Create, + TaskPanelOpPage, + 'Path-Waterline', + QtCore.QT_TRANSLATE_NOOP("Waterline", "Waterline"), + QtCore.QT_TRANSLATE_NOOP("Waterline", "Create a Waterline Operation from a model"), + PathWaterline.SetupProperties) + +FreeCAD.Console.PrintLog("Loading PathWaterlineGui... done\n")