* PathSurface: Add StepDown support Add StepDown support for PathSurface operation. A low-level optimization algorithm is included. * Update PageOpSurfaceEdit.ui * PathSurface: Activate CompletionMode input Update code to use CompletionMode selection from user. * PathSurfaceGui: Add Completion Mode selection box Add Completion Mode selection box for user input. * PathSurface: Use depth_params() for step control Implemented PathUtils.depth_params() for control of step downs to maintain consistency throughout PathWB. Suggested by Sliptonic. * PathSurface: Improve stability - Rev3 Organized code better, improved path optimization algorithm * PathSurface: Add XY rotational surfacing Add options to perform 3D surfacing by rotating object around X or Y axis. * PathSurface: update setup function Add properties to setup function * PathSurface: Remove extraneous code removed commented lines that consume processing time * PathSurface: Correct nxt.z point assignment Corrected error in nxt.z value assignment. Was assigning value to pnt.z instead of nxt.z * PathSurface: Update 4th axis operation Provided both parallel and perpendicular to axis scans when performing rotational operation. * PathSurface: Corrected Y-axis rotation to gcode Inverted Y-axis scan values in gcode to produce correct result * PathSurface: Adjust gcode feed rates Adjust feed rates for axial rotations * PathSurface: Implement waterline alternative Used OCL dropcutter scan as data base for python algorithm to extract a waterline. Waterline also incorporates step down feature. * PathSurface: Update to 3m-Usable stability improvements fixed step on multi-pass 4th axis scans initiate code to accommodate cut modes: zigzag, line initiate code to accommodate offset cutter on 4th axis scans to simulate a tilted cutter for ball end mills * PathSurface: Update 3m-Usable_r1 change instantiated static class attributes from value None, to 0.0 * PathSurface: Revert to 3i-Usable_r1 Further testing needed for 3m-Usable release * PathSurface: Update to Rev. 3s Usable fixed step over in rotational scans start/stop indexes tilt cutter adaptation for ball end mill cut pattern: zigzag, line partially implemented ignore waste option initiated for planar scans cut mode partially implemented: conventional & climb * PathSurface: Update to Rev. 3t Usable Corrected rotational errors corrected cutter tilt rotation added "ignore waste" feature for planar surface operation * Implement 4th-axis feature Implement 4th-axis feature: add gcode commands for rotation re-compute operation heights, to consider rotation radii * Deleted reference to undeclared variable * Added missing class attributes/variables * Added missing class variables/attributes * Relocate class variables to higher class * Remove references to undeclared variables relocated class variables here delete references to undeclared variables * Correct rotation direction for normal-to-Y faces * Remove X oriented 180 degree rotation adjustment Probable error in FreeCAD gcode renderer, producing incorrect B axis rotation * Update 4th-axis integration Address FC error with B axis rotation rendering: user input to override error for visual alignment with model. Correct pocket depths with Final Depth input enforced. Add Reverse Direction flag input for reversing direction of pocket: necessary in some use cases. NOTE: 4th-axis throws errors with Extension Corners feature. EC feature needs 4th-axis rotation integration when calculating extensions for a particular part feature/face. General testing needed. * PathSurface: Add StepDown support Add StepDown support for PathSurface operation. A low-level optimization algorithm is included. * Update PageOpSurfaceEdit.ui * PathSurface: Activate CompletionMode input Update code to use CompletionMode selection from user. * PathSurfaceGui: Add Completion Mode selection box Add Completion Mode selection box for user input. * PathSurface: Use depth_params() for step control Implemented PathUtils.depth_params() for control of step downs to maintain consistency throughout PathWB. Suggested by Sliptonic. * PathSurface: Improve stability - Rev3 Organized code better, improved path optimization algorithm * PathSurface: Add XY rotational surfacing Add options to perform 3D surfacing by rotating object around X or Y axis. * PathSurface: update setup function Add properties to setup function * PathSurface: Remove extraneous code removed commented lines that consume processing time * PathSurface: Correct nxt.z point assignment Corrected error in nxt.z value assignment. Was assigning value to pnt.z instead of nxt.z * PathSurface: Update 4th axis operation Provided both parallel and perpendicular to axis scans when performing rotational operation. * PathSurface: Corrected Y-axis rotation to gcode Inverted Y-axis scan values in gcode to produce correct result * PathSurface: Adjust gcode feed rates Adjust feed rates for axial rotations * PathSurface: Implement waterline alternative Used OCL dropcutter scan as data base for python algorithm to extract a waterline. Waterline also incorporates step down feature. * PathSurface: Update to 3m-Usable stability improvements fixed step on multi-pass 4th axis scans initiate code to accommodate cut modes: zigzag, line initiate code to accommodate offset cutter on 4th axis scans to simulate a tilted cutter for ball end mills * PathSurface: Update 3m-Usable_r1 change instantiated static class attributes from value None, to 0.0 * PathSurface: Revert to 3i-Usable_r1 Further testing needed for 3m-Usable release * PathSurface: Update to Rev. 3s Usable fixed step over in rotational scans start/stop indexes tilt cutter adaptation for ball end mill cut pattern: zigzag, line partially implemented ignore waste option initiated for planar scans cut mode partially implemented: conventional & climb * PathSurface: Update to Rev. 3t Usable Corrected rotational errors corrected cutter tilt rotation added "ignore waste" feature for planar surface operation * Implement 4th-axis feature Implement 4th-axis feature: add gcode commands for rotation re-compute operation heights, to consider rotation radii * Deleted reference to undeclared variable * Added missing class attributes/variables * Added missing class variables/attributes * Relocate class variables to higher class * Remove references to undeclared variables relocated class variables here delete references to undeclared variables * Remove X oriented 180 degree rotation adjustment Probable error in FreeCAD gcode renderer, producing incorrect B axis rotation * Correct rotation direction for normal-to-Y faces * Update 4th-axis integration Address FC error with B axis rotation rendering: user input to override error for visual alignment with model. Correct pocket depths with Final Depth input enforced. Add Reverse Direction flag input for reversing direction of pocket: necessary in some use cases. NOTE: 4th-axis throws errors with Extension Corners feature. EC feature needs 4th-axis rotation integration when calculating extensions for a particular part feature/face. General testing needed. * Apply XYZ Model translations to paths; Add rotation axis selection property * Spelling correction ln 110 * Geometry list box: shrink height 560 to 400 height * Update 4th-axis integration Clean up code Add comments for visionary elements Apply axis selection to opHeights rotational calculations * Update 4th-axis integration Prepare sorting of vertical faces per axis_angle grouping, just as horizontal faces are sorted, for analysis as connected shapes * Reset to FreeCAD master current * Reset to current FreeCAD master version * Update PathPocketBase.py * Reset to current FreeCAD master * Corrected __created__ date * Convert all print() to PathLog.debug() * Convert print() to PathLog.debug(); Clean up code Use PathLog.debug over print() for troubleshooting Clean up large commented sections of code. * Convert print() to PathLog.debug(); combine duplicate methods * Update last modified stamp * Update last mofified timestamp Also, removed commented-out code * Typo corrected: "selt." to "self." Discovered in https://api.travis-ci.org/v3/job/526456071/log.txt on 30 April 2019 while searching for errors related to failure of PR #2114 * Restore initialization of self.depthparams PR is failing Travis CI build test. Previous deletion of this code appears to be the reason. See report at: https://api.travis-ci.org/v3/job/526492709/log.txt * Convert tuple sizes from other PathWB ops * B-axis rotation: render correction * Revert "rebase to FC master 2019-05-02" * Correct depthparams opOnDocumentRestored() issue * Relocate "UseRotation" property from PathAreaOp.py * Improve meshFromShape() calculation per @sliptonic faster mesh creation helps smooth jagged lines in path results * Part: Make 3rd party libraries into PCH * PartDesign: 3rdParty to PCH * Sketcher: PCH * PartDesign: PCH GUI * Sketcher: GUI PCH * Travis windows build (#2110) Travis windows build * Travis: enable clcache debug * disable travis clcache debug * respect user settings for dialog used in merge-document command * msbuild verbose and logo * Travis force wait 60 minutes * Travis: Use & between vars sourcing and msbuild * Travis: Move building to external batch script (#2131) * Travis: Move building to external batch script * Travis: double quotes to escape route * Travis: use cmd.exe to call batch file * Crowdin: fix grammatical error + whitespace https://crowdin.com/translate/freecad/569/en-fi#6498705 * Crowdin: experiment to fix crowdin 'ghost' strings The following translation strings still perpetuate after updating crowdin translations. This commit experiments with removing their source counterparts in the FC .ts files. I'm not sure why they are not rendered obsolete by the normal operations. * https://crowdin.com/translate/freecad/6766/en-en#6499472 * https://crowdin.com/translate/freecad/6766/en-en#6573450 * fix for edit tracker draw styles * deleted commented code * Travis: filter out * AddonManager: allow to add custom repositories * Revert "rebase to FC master 2019-05-02" * Draft: Fixed Draft text justification * Arch: Fixed bug in arch windows creation * Arch: Fixed export of windows colors to IFC * Draft: Fixed 0.18.1 bug in DXF importer * Arch: Fixed wrong exported objects in IFC export * Arch: Fixed export of sites to IFC * Start: Fixed bad default icon in start page * Correct depthparams opOnDocumentRestored() issue * Relocate "UseRotation" property from PathAreaOp.py * Improve meshFromShape() calculation per @sliptonic faster mesh creation helps smooth jagged lines in path results * Relocate "UseRotation" property in PathAreaOp.py * Syntax correction Travis CI python 3.7 build test fails regularly due to syntax error at this point * Correct rebase issue: PR #2114 * Correct rebase issue: PR #2114 * Correct rebase issue: PR #2114 * Correct rebase issue: PR #2114 * Correct rebase issue: PR #2114 * Correct rebase issue: PR #2114 * path pep8 cleanup * Multi-face geometry selection repaired Previous version broke ability to select process multiple faces selected by user in geometry tab in operation task window. Also performed some syntax cleaning per PEP8 linter * Update script version info
1922 lines
82 KiB
Python
1922 lines
82 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Copyright (c) 2016 sliptonic <shopinthewoods@gmail.com> *
|
|
# * *
|
|
# * This program is free software; you can redistribute it and/or modify *
|
|
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
|
# * as published by the Free Software Foundation; either version 2 of *
|
|
# * the License, or (at your option) any later version. *
|
|
# * for detail see the LICENCE text file. *
|
|
# * *
|
|
# * This program is distributed in the hope that it will be useful, *
|
|
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
# * GNU Library General Public License for more details. *
|
|
# * *
|
|
# * You should have received a copy of the GNU Library General Public *
|
|
# * License along with this program; if not, write to the Free Software *
|
|
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
|
# * USA *
|
|
# * *
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Additional modifications and contributions beginning 2019 *
|
|
# * by Russell Johnson <russ4262@gmail.com> *
|
|
# * Version: Rev. 3t Usable *
|
|
# * *
|
|
# ***************************************************************************
|
|
# Revision Notes
|
|
# - Continue implementation of cut patterns: zigzag, line (line is used to force cut modes: climb, conventional)
|
|
# - Continue implementation of ignore waste feature for planar scans
|
|
# - Planar Op: move scan result(self.CLP) to local scope(CLP)
|
|
# - Implemented @sliptonic's meshFromShape() parameter improvements for better, faster mesh creation
|
|
|
|
# RELEASE NOTES
|
|
# Only G0 and G1 gcode commands are used throughout.
|
|
# CutMode: Climb, Conventional is only functional for some operations, like waterline and rotational scans
|
|
# CutPattern: only functional for some operations
|
|
# IgnoreWaste: not implemented yet - target op is for planar dropcutter
|
|
# High density/resolution, mult-layer scans require significantly more processing time.
|
|
# Rotational scans are very processor intensive.
|
|
# Rotational scans take a longer time to complete.
|
|
# Multi-pass rotational scans require a very long time to complete,
|
|
# even at larger SampleInterval values - ex: 1mm, 0.5mm
|
|
# Remember, the larger the model, the more time to complete an operation!
|
|
# Rotational require even more time. Multi-pass require much, much more time.
|
|
# This release is NOT bug-free.
|
|
# After changing an operational value in the Properties list, press the ENTER key.
|
|
# Change all desired values, one at a time, pressing ENTER key after each change.
|
|
# When all property changes complete, click the blue recompute icon under user menus.
|
|
# If you have an existing 3D Surface Op in your Job(s), you will need to delete it and recreate
|
|
# with this updated script installed because it may contain additional properties not
|
|
# created with the original version.
|
|
|
|
|
|
from __future__ import print_function
|
|
|
|
import FreeCAD
|
|
import MeshPart
|
|
# import Part
|
|
import Path
|
|
import PathScripts.PathLog as PathLog
|
|
# import PathScripts.PathPocketBase as PathPocketBase
|
|
import PathScripts.PathUtils as PathUtils
|
|
import PathScripts.PathOp as PathOp
|
|
|
|
from PySide import QtCore
|
|
import time
|
|
import math
|
|
|
|
|
|
__title__ = "Path Surface Operation"
|
|
__author__ = "sliptonic (Brad Collette)"
|
|
__url__ = "http://www.freecadweb.org"
|
|
__doc__ = "Class and implementation of Mill Facing operation."
|
|
__contributors__ = "roivai[FreeCAD], russ4262 (Russell Johnson)"
|
|
__created__ = "2016"
|
|
__scriptVersion__ = "3t Usable"
|
|
__lastModified__ = "2019-05-02 10:44 CST"
|
|
|
|
if False:
|
|
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
|
|
PathLog.trackModule(PathLog.thisModule())
|
|
else:
|
|
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
|
|
|
|
|
|
# Qt tanslation handling
|
|
def translate(context, text, disambig=None):
|
|
return QtCore.QCoreApplication.translate(context, text, disambig)
|
|
|
|
|
|
# OCL must be installed
|
|
try:
|
|
import ocl
|
|
except:
|
|
FreeCAD.Console.PrintError(
|
|
translate("Path_Surface", "This operation requires OpenCamLib to be installed.") + "\n")
|
|
import sys
|
|
sys.exit(translate("Path_Surface", "This operation requires OpenCamLib to be installed."))
|
|
|
|
|
|
class ObjectSurface(PathOp.ObjectOp):
|
|
'''Proxy object for Surfacing operation.'''
|
|
|
|
# These are static while document is open, if it contains a 3D Surface Op
|
|
initFinalDepth = None
|
|
initOpFinalDepth = None
|
|
initOpStartDepth = None
|
|
docRestored = False
|
|
|
|
def baseObject(self):
|
|
'''baseObject() ... returns super of receiver
|
|
Used to call base implementation in overwritten functions.'''
|
|
return super(__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
|
|
|
|
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", "DropCutterDir", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction along which dropcutter lines are created"))
|
|
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::PropertyVectorDistance", "DropCutterExtraOffset", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box"))
|
|
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::PropertyEnumeration", "LayerMode", "Algorithm", QtCore.QT_TRANSLATE_NOOP("App::Property", "The completion mode for the operation: single or multi-pass"))
|
|
obj.addProperty("App::PropertyEnumeration", "CutMode", "Path", 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", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Clearing pattern to use"))
|
|
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::PropertyFloat", "CutterTilt", "Rotational", QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan"))
|
|
obj.addProperty("App::PropertyPercent", "StepOver", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Step over percentage of the drop cutter path"))
|
|
obj.addProperty("App::PropertyDistance", "DepthOffset", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Z-axis offset from the surface of the object"))
|
|
# obj.addProperty("App::PropertyFloatConstraint", "SampleInterval", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times"))
|
|
obj.addProperty("App::PropertyFloat", "SampleInterval", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "The Sample Interval. Small values cause long wait times"))
|
|
obj.addProperty("App::PropertyBool", "Optimize", "Surface", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization which removes unnecessary points from G-Code output"))
|
|
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.CutMode = ['Conventional', 'Climb']
|
|
obj.BoundBox = ['BaseBoundBox', 'Stock']
|
|
obj.DropCutterDir = ['X', 'Y']
|
|
obj.Algorithm = ['OCL Dropcutter', 'OCL Waterline']
|
|
# obj.SampleInterval = (0.04, 0.01, 1.0, 0.01)
|
|
obj.LayerMode = ['Single-pass', 'Multi-pass']
|
|
obj.ScanType = ['Planar', 'Rotational']
|
|
obj.RotationAxis = ['X', 'Y']
|
|
# obj.CutPattern = ['ZigZag', 'Offset', 'Spiral', 'ZigZagOffset', 'Line', 'Grid', 'Triangle']
|
|
obj.CutPattern = ['ZigZag', 'Line']
|
|
|
|
if not hasattr(obj, 'DoNotSetDefaultValues'):
|
|
self.setEditorProperties(obj)
|
|
|
|
def setEditorProperties(self, obj):
|
|
# Used to hide inputs in properties list
|
|
if obj.Algorithm == 'OCL Dropcutter':
|
|
obj.setEditorMode('DropCutterDir', 0)
|
|
# obj.setEditorMode('DropCutterExtraOffset', 0)
|
|
else:
|
|
obj.setEditorMode('DropCutterDir', 2)
|
|
# obj.setEditorMode('DropCutterExtraOffset', 2)
|
|
|
|
def onChanged(self, obj, prop):
|
|
if prop == "Algorithm":
|
|
self.setEditorProperties(obj)
|
|
|
|
def opOnDocumentRestored(self, obj):
|
|
self.setEditorProperties(obj)
|
|
# Import FinalDepth from existing operation for use in recompute() operations
|
|
self.initFinalDepth = obj.FinalDepth.Value
|
|
self.initOpFinalDepth = obj.OpFinalDepth.Value
|
|
self.docRestored = True
|
|
PathLog.debug("Imported existing OpFinalDepth of " + str(self.initOpFinalDepth) + " for recompute() purposes.")
|
|
PathLog.debug("Imported existing FinalDepth of " + str(self.initFinalDepth) + " for recompute() purposes.")
|
|
|
|
def opExecute(self, obj):
|
|
'''opExecute(obj) ... process surface operation'''
|
|
PathLog.track()
|
|
initIdx = 0.0
|
|
|
|
# Instantiate additional class operation variables
|
|
rtn = self.resetOpVariables()
|
|
|
|
# mark beginning of operation
|
|
self.startTime = time.time()
|
|
|
|
# Set cutter for OCL based on tool controller properties
|
|
# rtn = self.setOclCutter(obj)
|
|
|
|
self.reportThis("\n-----\n-----\nBegin 3D surface operation")
|
|
self.reportThis("Script version: " + __scriptVersion__ + " Lm: " + __lastModified__)
|
|
|
|
output = ""
|
|
if obj.Comment != "":
|
|
output += '(' + str(obj.Comment) + ')\n'
|
|
|
|
output += "(" + obj.Label + ")"
|
|
output += "(Compensated Tool Path. Diameter: " + str(obj.ToolController.Tool.Diameter) + ")"
|
|
|
|
parentJob = PathUtils.findParentJob(obj)
|
|
if parentJob is None:
|
|
self.reportThis("No parentJob")
|
|
return
|
|
self.SafeHeightOffset = parentJob.SetupSheet.SafeHeightOffset.Value
|
|
self.ClearHeightOffset = parentJob.SetupSheet.ClearanceHeightOffset.Value
|
|
|
|
# Import OpFinalDepth from pre-existing operation for recompute() scenarios
|
|
if obj.OpFinalDepth.Value != self.initOpFinalDepth:
|
|
if obj.OpFinalDepth.Value == obj.FinalDepth.Value:
|
|
obj.FinalDepth.Value = self.initOpFinalDepth
|
|
obj.OpFinalDepth.Value = self.initOpFinalDepth
|
|
if self.initOpFinalDepth is not None:
|
|
obj.OpFinalDepth.Value = self.initOpFinalDepth
|
|
|
|
# Limit start index
|
|
if obj.StartIndex < 0.0:
|
|
obj.StartIndex = 0.0
|
|
if obj.StartIndex > 360.0:
|
|
obj.StartIndex = 360.0
|
|
|
|
# Limit stop index
|
|
if obj.StopIndex > 360.0:
|
|
obj.StopIndex = 360.0
|
|
if obj.StopIndex < 0.0:
|
|
obj.StopIndex = 0.0
|
|
|
|
# Limit cutter tilt
|
|
if obj.CutterTilt < -90.0:
|
|
obj.CutterTilt = -90.0
|
|
if obj.CutterTilt > 90.0:
|
|
obj.CutterTilt = 90.0
|
|
|
|
# Limit sample interval
|
|
if obj.SampleInterval < 0.001:
|
|
obj.SampleInterval = 0.001
|
|
if obj.SampleInterval > 25.4:
|
|
obj.SampleInterval = 25.4
|
|
|
|
# Limit StepOver to natural number percentage
|
|
if obj.Algorithm == 'OCL Dropcutter':
|
|
if obj.StepOver > 100:
|
|
obj.StepOver = 100
|
|
if obj.StepOver < 1:
|
|
obj.StepOver = 1
|
|
self.cutOut = (self.cutter.getDiameter() * (float(obj.StepOver) / 100.0))
|
|
self.reportThis("Cut out: " + str(self.cutOut) + " mm")
|
|
|
|
# Cycle through parts of model
|
|
for base in self.model:
|
|
self.reportThis("BASE object: " + str(base.Name))
|
|
|
|
# Rotate model to initial index
|
|
if obj.ScanType == 'Rotational':
|
|
initIdx = obj.CutterTilt + obj.StartIndex
|
|
if initIdx != 0.0:
|
|
self.basePlacement = FreeCAD.ActiveDocument.getObject(base.Name).Placement
|
|
if obj.RotationAxis == 'X':
|
|
# FreeCAD.ActiveDocument.getObject(base.Name).Placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0),FreeCAD.Rotation(FreeCAD.Vector(1,0,0), initIdx))
|
|
base.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), initIdx))
|
|
else:
|
|
# FreeCAD.ActiveDocument.getObject(base.Name).Placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0),FreeCAD.Rotation(FreeCAD.Vector(0,1,0), initIdx))
|
|
base.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), initIdx))
|
|
|
|
if base.TypeId.startswith('Mesh'):
|
|
mesh = base.Mesh
|
|
else:
|
|
# try/except is for Path Jobs created before GeometryTolerance
|
|
try:
|
|
deflection = parentJob.GeometryTolerance
|
|
except AttributeError:
|
|
import PathScripts.PathPreferences as PathPreferences
|
|
deflection = PathPreferences.defaultGeometryTolerance()
|
|
# base.Shape.tessellate(0.05) # 0.5 original value
|
|
# mesh = MeshPart.meshFromShape(base.Shape, Deflection=deflection)
|
|
mesh = MeshPart.meshFromShape(Shape=base.Shape, LinearDeflection=deflection, AngularDeflection=0.5, Relative=False)
|
|
|
|
# Set bound box
|
|
if obj.BoundBox == "BaseBoundBox":
|
|
bb = mesh.BoundBox
|
|
else:
|
|
bb = parentJob.Stock.Shape.BoundBox
|
|
|
|
# Objective is to remove material from surface in StepDown layers rather than one pass to FinalDepth
|
|
final = []
|
|
if obj.Algorithm == 'OCL Waterline':
|
|
self.reportThis("--CutMode: " + str(obj.CutMode))
|
|
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))
|
|
aT = stl.addTriangle(t)
|
|
final = self._waterlineOp(obj, stl, bb)
|
|
elif obj.Algorithm == 'OCL Dropcutter':
|
|
# Create stl object via OCL
|
|
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]),
|
|
ocl.Point(q[0], q[1], q[2]),
|
|
ocl.Point(r[0], r[1], r[2]))
|
|
aT = stl.addTriangle(t)
|
|
|
|
# Rotate model back to original index
|
|
if obj.ScanType == 'Rotational':
|
|
if initIdx != 0.0:
|
|
initIdx = 0.0
|
|
base.Placement = self.basePlacement
|
|
# if obj.RotationAxis == 'X':
|
|
# FreeCAD.ActiveDocument.getObject(base.Name).Placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0),FreeCAD.Rotation(FreeCAD.Vector(1,0,0), initIdx))
|
|
# else:
|
|
# FreeCAD.ActiveDocument.getObject(base.Name).Placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0),FreeCAD.Rotation(FreeCAD.Vector(0,1,0), initIdx))
|
|
|
|
# Prepare global holdpoint container
|
|
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"))
|
|
|
|
if obj.ScanType == 'Rotational':
|
|
# Remove extended material from Stock and re-assign bb
|
|
if hasattr(parentJob.Stock, 'ExtXneg'):
|
|
parentJob.Stock.ExtXneg = 0
|
|
parentJob.Stock.ExtXpos = 0
|
|
parentJob.Stock.ExtYneg = 0
|
|
parentJob.Stock.ExtYpos = 0
|
|
parentJob.Stock.ExtZneg = 0
|
|
parentJob.Stock.ExtZpos = 0
|
|
|
|
# Avoid division by zero in rotational scan calculations
|
|
if obj.FinalDepth.Value <= 0.0:
|
|
zero = obj.SampleInterval # 0.00001
|
|
self.FinalDepth = zero
|
|
obj.FinalDepth.Value = 0.0
|
|
else:
|
|
self.FinalDepth = obj.FinalDepth.Value
|
|
|
|
# Determine boundbox radius based upon xzy limits data
|
|
if math.fabs(bb.ZMin) > math.fabs(bb.ZMax):
|
|
vlim = bb.ZMin
|
|
else:
|
|
vlim = bb.ZMax
|
|
if obj.RotationAxis == 'X':
|
|
# Rotation is around X-axis, cutter moves along same axis
|
|
if math.fabs(bb.YMin) > math.fabs(bb.YMax):
|
|
hlim = bb.YMin
|
|
else:
|
|
hlim = bb.YMax
|
|
else:
|
|
# Rotation is around Y-axis, cutter moves along same axis
|
|
if math.fabs(bb.XMin) > math.fabs(bb.XMax):
|
|
hlim = bb.XMin
|
|
else:
|
|
hlim = bb.XMax
|
|
|
|
# Compute max radius of stock, as it rotates, and rotational clearance & safe heights
|
|
self.bbRadius = math.sqrt(hlim**2 + vlim**2)
|
|
self.clearHeight = self.bbRadius + parentJob.SetupSheet.ClearanceHeightOffset.Value
|
|
self.safeHeight = self.bbRadius + parentJob.SetupSheet.ClearanceHeightOffset.Value
|
|
|
|
final = self._rotationalDropCutterOp(obj, stl, bb)
|
|
elif obj.ScanType == 'Planar':
|
|
final = self._planarDropCutOp(obj, stl, bb)
|
|
# End IF
|
|
# Send final list of commands to operation object
|
|
self.commandlist.extend(final)
|
|
# End IF
|
|
|
|
self.endTime = time.time()
|
|
self.reportThis("OPERATION time: " + str(self.endTime - self.startTime) + " sec.")
|
|
|
|
print(self.opReport)
|
|
|
|
def _planarDropCutOp(self, obj, stl, bb):
|
|
# t_before = time.time()
|
|
pntsPerLine = 0
|
|
ignoreWasteFlag = obj.IgnoreWaste
|
|
ignoreMap = [1]
|
|
|
|
def createTopoMap(scanCLP, ignoreDepth):
|
|
topoMap = []
|
|
for pt in scanCLP:
|
|
if pt.z < ignoreDepth:
|
|
topoMap.append(0)
|
|
else:
|
|
topoMap.append(2)
|
|
return topoMap
|
|
|
|
# Convert linear list of points to multi-dimensional list
|
|
def listToMultiDimensional(points, nL, pPL):
|
|
multiDim = []
|
|
for L in range(0, nL):
|
|
multiDim.append([])
|
|
for P in range(0, pPL):
|
|
pi = L * pPL + P
|
|
multiDim[L].append(points[pi])
|
|
return multiDim
|
|
|
|
# De-buffer multi dimensional list
|
|
def debufferMultiDimenList(multi):
|
|
multi.pop(0)
|
|
multi.pop()
|
|
for i in range(0, len(multi)):
|
|
multi[i].pop(0)
|
|
multi[i].pop()
|
|
return multi
|
|
|
|
# Convert multi-dimensional list to linear list of points
|
|
def multiDimensionalToList(multi):
|
|
points = []
|
|
for L in multi:
|
|
points.extend(L)
|
|
return points
|
|
|
|
# Prepare global holdpoint container
|
|
if self.holdPoint is None:
|
|
self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf"))
|
|
|
|
# Set crop cutter extra offset
|
|
cdeoX = obj.DropCutterExtraOffset.x
|
|
cdeoY = obj.DropCutterExtraOffset.y
|
|
|
|
# the max and min XY area of the operation
|
|
xmin = bb.XMin - cdeoX
|
|
xmax = bb.XMax + cdeoX
|
|
ymin = bb.YMin - cdeoY
|
|
ymax = bb.YMax + cdeoY
|
|
|
|
# Compute number and size of stepdowns, and final depth
|
|
if obj.LayerMode == 'Single-pass':
|
|
depthparams = [obj.FinalDepth.Value]
|
|
else:
|
|
dep_par = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value)
|
|
depthparams = [i for i in dep_par]
|
|
# self.reportThis("--depthparams:" + str(depthparams))
|
|
lenDP = len(depthparams)
|
|
prevDepth = depthparams[0]
|
|
|
|
# Determine bounding box length
|
|
bbLength = bb.YLength
|
|
if obj.DropCutterDir == 'Y':
|
|
bbLength = bb.XLength
|
|
|
|
# Determine number of lines for OCL scan
|
|
if obj.DropCutterDir == 'X':
|
|
exOff = obj.DropCutterExtraOffset.y
|
|
else:
|
|
exOff = obj.DropCutterExtraOffset.x
|
|
numLines = int(math.ceil((bbLength + (2 * exOff)) / self.cutOut)) # Number of lines
|
|
|
|
# Scan the piece to depth
|
|
scanCLP = self._planarDropCutScan(obj, stl, bbLength, xmin, ymin, xmax, ymax, depthparams[lenDP - 1], numLines, self.cutOut)
|
|
|
|
# Apply depth offset
|
|
if obj.DepthOffset.Value != 0:
|
|
self.reportThis("--Applying DepthOffset")
|
|
for pt in range(0, len(scanCLP)):
|
|
scanCLP[pt].z += obj.DepthOffset.Value
|
|
|
|
numPts = len(scanCLP)
|
|
pntsPerLine = numPts / numLines
|
|
# self.reportThis("--points: " + str(numPts))
|
|
# self.reportThis("--lines: " + str(numLines))
|
|
# self.reportThis("--pntsPerLine: " + str(pntsPerLine))
|
|
if math.ceil(pntsPerLine) != math.floor(pntsPerLine):
|
|
pntsPerLine = None
|
|
|
|
# Create topo map for ignoring waste material
|
|
if ignoreWasteFlag is True:
|
|
topoMap = createTopoMap(scanCLP, obj.IgnoreWasteDepth)
|
|
self.topoMap = listToMultiDimensional(topoMap, numLines, pntsPerLine)
|
|
rtnA = self._bufferTopoMap(numLines, pntsPerLine)
|
|
rtnB = self._highlightWaterline(4, 1)
|
|
self.topoMap = debufferMultiDimenList(self.topoMap)
|
|
ignoreMap = multiDimensionalToList(self.topoMap)
|
|
|
|
# Extract layers per depthparams
|
|
for lyr in range(0, lenDP):
|
|
# Convert current layer data to gcode
|
|
self._planarScanToGcode(obj, lyr, prevDepth, depthparams[lyr], scanCLP, pntsPerLine, ignoreMap)
|
|
prevDepth = depthparams[lyr]
|
|
|
|
commands = self._processPlanarHolds(obj, scanCLP)
|
|
# self.reportThis("--Elapsed time after processing gcode holds is " + str(time.time() - t_before) + " s") # self.keepTime
|
|
return commands
|
|
|
|
def _planarDropCutScan(self, obj, stl, bbLength, xmin, ymin, xmax, ymax, fd, Nl, cOut):
|
|
t_before = time.time()
|
|
|
|
def cutPatternLine(obj, n, p1, p2):
|
|
if obj.CutPattern == 'ZigZag':
|
|
if (n % 2 == 0.0): # even
|
|
lo = ocl.Line(p1, p2) # line-object
|
|
else: # odd
|
|
lo = ocl.Line(p2, p1) # line-object
|
|
elif obj.CutPattern == 'Line':
|
|
if obj.CutMode == 'Conventional':
|
|
lo = ocl.Line(p1, p2) # line-object
|
|
else: # odd
|
|
lo = ocl.Line(p2, p1) # line-object
|
|
return lo
|
|
|
|
pdc = ocl.PathDropCutter() # create a pdc
|
|
pdc.setSTL(stl)
|
|
pdc.setCutter(self.cutter)
|
|
pdc.setZ(fd) # set minimumZ (final / target depth value)
|
|
pdc.setSampling(obj.SampleInterval)
|
|
|
|
path = ocl.Path() # create an empty path object
|
|
|
|
if obj.DropCutterDir == 'X':
|
|
# add Line objects to the path in this loop
|
|
for n in range(0, Nl):
|
|
if n == Nl - 1:
|
|
if obj.StepOver > 50:
|
|
cOut = (self.cutter.getDiameter() / 2)
|
|
y = ymax - cOut
|
|
else:
|
|
y = ymin - (self.cutter.getDiameter() / 2) + ((n + 1) * cOut) # all lines are offest by 1/2 cutter diameter
|
|
p1 = ocl.Point(xmin, y, 0) # start-point of line
|
|
p2 = ocl.Point(xmax, y, 0) # end-point of line
|
|
|
|
lo = cutPatternLine(obj, n, p1, p2)
|
|
path.append(lo) # add the line to the path
|
|
else:
|
|
# add Line objects to the path in this loop
|
|
for n in range(0, Nl):
|
|
if n == Nl - 1:
|
|
if obj.StepOver > 50:
|
|
cOut = (self.cutter.getDiameter() / 2)
|
|
x = xmax - cOut
|
|
else:
|
|
x = xmin - (self.cutter.getDiameter() / 2) + ((n + 1) * cOut) # all lines are offest by 1/2 cutter diameter
|
|
p1 = ocl.Point(x, ymin, 0) # start-point of line
|
|
p2 = ocl.Point(x, ymax, 0) # end-point of line
|
|
|
|
lo = cutPatternLine(obj, n, p1, p2)
|
|
path.append(lo) # add the line to the path
|
|
|
|
pdc.setPath(path)
|
|
|
|
# run drop-cutter on the path
|
|
pdc.run()
|
|
self.reportThis("--OCL scan took " + str(time.time() - t_before) + " s")
|
|
|
|
clp = pdc.getCLPoints()
|
|
# return the list the points
|
|
return clp
|
|
|
|
def _planarScanToGcode(self, obj, lc, prvDep, layDep, CLP, pntsPerLine, ignoreMap):
|
|
output = []
|
|
optimize = obj.Optimize
|
|
ignWF = obj.IgnoreWaste
|
|
lenCLP = len(CLP)
|
|
lastCLP = len(CLP) - 1
|
|
holdStart = False
|
|
holdStop = False
|
|
holdCount = 0
|
|
holdLine = 0
|
|
onLine = False
|
|
lineOfTravel = "X"
|
|
pointsOnLine = 0
|
|
zMin = prvDep
|
|
zMax = prvDep
|
|
prcs = False
|
|
prcsCnt = 0
|
|
pntCount = 0
|
|
rowCount = 1
|
|
begLayCmds = ["aaa", "bbb"]
|
|
minIgnVal = 1
|
|
|
|
def makePnt(pnt):
|
|
p = ocl.Point(float("inf"), float("inf"), float("inf"))
|
|
p.x = pnt.x
|
|
p.y = pnt.y
|
|
p.z = pnt.z
|
|
return p
|
|
|
|
def beginLayerCommand(pnt, lc):
|
|
# Send cutter to starting position(first point)
|
|
begcmd = []
|
|
begcmd.append(Path.Command('N (Beginning of layer ' + str(lc) + ')', {}))
|
|
begcmd.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
|
|
begcmd.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}))
|
|
begcmd.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed}))
|
|
begcmd.reverse()
|
|
return begcmd
|
|
|
|
# Create containers for x,y,z points
|
|
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"))
|
|
travVect = ocl.Point(float("inf"), float("inf"), float("inf"))
|
|
|
|
# Determine if releasing model from ignore waste areas
|
|
if obj.ReleaseFromWaste == True:
|
|
minIgnVal = 0
|
|
|
|
# Set values for first gcode point in layer
|
|
pnt.x = CLP[0].x
|
|
pnt.y = CLP[0].y
|
|
pnt.z = CLP[0].z
|
|
if CLP[0].z < layDep:
|
|
pnt.z = layDep
|
|
|
|
# generate the path commands
|
|
# Begin processing ocl points list into gcode
|
|
for i in range(0, lenCLP):
|
|
# Calculate next point for consideration of next point
|
|
if i < lastCLP:
|
|
nxt.x = CLP[i + 1].x
|
|
nxt.y = CLP[i + 1].y
|
|
if CLP[i + 1].z < layDep:
|
|
nxt.z = layDep
|
|
else:
|
|
nxt.z = CLP[i + 1].z
|
|
else:
|
|
optimize = False
|
|
|
|
pntCount += 1
|
|
if pntCount == 1:
|
|
# PathLog.debug("--Start row: " + str(rowCount))
|
|
nn = 1
|
|
elif pntCount == pntsPerLine:
|
|
# PathLog.debug("--End row: " + str(rowCount))
|
|
pntCount = 0
|
|
rowCount += 1
|
|
# Add rise to clear height before beginning next row in CutPattern: Line
|
|
# if obj.CutPattern == 'Line':
|
|
# output.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
|
|
|
|
# determine vector direction for each axis
|
|
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
|
|
|
|
# Determine cutter line of travel
|
|
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
|
|
|
|
# determine if lineOfTravel is same as obj.DropCutterDir line
|
|
if onLine is False:
|
|
if lineOfTravel == obj.DropCutterDir:
|
|
onLine = True
|
|
self.lineCNT += 1 # increment line count
|
|
pointsOnLine += 1
|
|
else:
|
|
if lineOfTravel != obj.DropCutterDir:
|
|
if self.onHold is False:
|
|
zMax = prvDep
|
|
onLine = False
|
|
pointsOnLine = 0
|
|
else:
|
|
pointsOnLine += 1
|
|
|
|
# Ignore waste operation triggers
|
|
if ignWF is False:
|
|
prcs = True
|
|
else: # ignWF is TRUE
|
|
if obj.LayerMode == 'Single-pass':
|
|
if ignoreMap[i] > minIgnVal:
|
|
if ignoreMap[i] == 1:
|
|
pnt.z = obj.IgnoreWasteDepth
|
|
if prcs is False:
|
|
# Move cutter to current xy position
|
|
output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}))
|
|
prcs = True
|
|
else:
|
|
if prcs is True:
|
|
# Raise cutter to safe height
|
|
output.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
|
|
prcs = False
|
|
else: # Multi-pass mode
|
|
if ignoreMap[i] > minIgnVal:
|
|
if prcs is False:
|
|
# Move cutter to current xy position
|
|
output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}))
|
|
prcs = True
|
|
else:
|
|
prcs = False
|
|
# End of ignWF
|
|
|
|
if prcs is True:
|
|
prcsCnt += 1
|
|
if prcsCnt == 1:
|
|
begLayCmds = beginLayerCommand(pnt, lc) # start layer at this point
|
|
|
|
if obj.LayerMode == 'Multi-pass':
|
|
# if z travels above previous layer, start/continue hold high cycle
|
|
if pnt.z > prvDep:
|
|
if self.onHold is False:
|
|
holdStart = True
|
|
self.onHold = True
|
|
|
|
# Update zMin and zMax values
|
|
if pnt.z < zMin:
|
|
zMin = pnt.z
|
|
if pnt.z > zMax:
|
|
zMax = pnt.z
|
|
|
|
if self.onHold is True:
|
|
if holdStart is True:
|
|
# go to current coordinate
|
|
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
|
|
# Save holdStart coordinate and prvDep values
|
|
self.holdPoint.x = pnt.x
|
|
self.holdPoint.y = pnt.y
|
|
self.holdPoint.z = pnt.z
|
|
|
|
holdCount += 1 # Increment hold count
|
|
holdLine = self.lineCNT # remember holdLine
|
|
self.holdStartPnts.append(makePnt(pnt))
|
|
self.holdPrevLayerVals.append(prvDep)
|
|
holdStart = False # cancel holdStart
|
|
|
|
# hold cutter high until Z value drops below prvDep
|
|
if pnt.z <= prvDep:
|
|
holdStop = True
|
|
# End of onHold
|
|
|
|
if holdStop is True:
|
|
# Send hold and current points to
|
|
if holdLine == self.lineCNT:
|
|
# if start and stop points on same line, process has simple hold
|
|
self.holdStartPnts.pop()
|
|
self.holdPrevLayerVals.pop()
|
|
zMax += 2.0
|
|
for cmd in self.holdStopCmds(obj, zMax, prvDep, pnt, "Hold Stop: in-line"):
|
|
output.append(cmd)
|
|
else:
|
|
# if start and stop points on different lines, process has complex hold
|
|
self.holdStopPnts.append(makePnt(pnt))
|
|
self.holdZMaxVals.append(zMax)
|
|
self.holdStopTypes.append("Mid")
|
|
output.append("HD") # add placeholder for processing of hold
|
|
self.holdPntCnt += 1
|
|
|
|
# reset necessary hold related settings
|
|
zMax = prvDep
|
|
holdStop = False
|
|
self.onHold = False
|
|
self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf"))
|
|
# End of holdStop
|
|
|
|
if self.onHold is 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, 'Z': pnt.z, 'F': self.horizFeed}))
|
|
elif i == lastCLP:
|
|
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
|
|
|
|
# Rotate point data
|
|
prev.x = pnt.x
|
|
prev.y = pnt.y
|
|
prev.z = pnt.z
|
|
# End of prcs
|
|
pnt.x = nxt.x
|
|
pnt.y = nxt.y
|
|
pnt.z = nxt.z
|
|
|
|
# save current layer end point
|
|
if self.onHold is True:
|
|
if holdLine == self.lineCNT:
|
|
self.holdStartPnts.pop()
|
|
self.holdPrevLayerVals.pop()
|
|
for cmd in self.holdStopCmds(obj, obj.SafeHeight.Value, obj.SafeHeight.Value, pnt, "Hold Stop: Layer endpoint online"): # zMax as prvDep removes drop to prvDep
|
|
output.append(cmd)
|
|
else:
|
|
self.holdStopPnts.append(makePnt(pnt))
|
|
self.holdZMaxVals.append(zMax)
|
|
output.append("HD") # add placeholder for processing of hold
|
|
self.holdStopTypes.append("End") # tag hold type
|
|
self.holdPntCnt += 1
|
|
self.onHold = False
|
|
|
|
# save last point for insertion into next layer CLP as start point
|
|
endPnt = pnt
|
|
endPnt.z = obj.OpStockZMax.Value + obj.DepthOffset.Value
|
|
self.layerEndPnt = endPnt
|
|
# self.reportThis("----Points after linear optimization: " + str(len(output)))
|
|
# append layer commands to operation command list
|
|
for cmd in begLayCmds:
|
|
output.insert(0, cmd)
|
|
for o in output:
|
|
self.gcodeCmds.append(o)
|
|
self.gcodeCmds.append(Path.Command('N (End of layer ' + str(lc) + ')', {}))
|
|
# return output
|
|
|
|
def _processPlanarHolds(self, obj, scanCLP):
|
|
# Process all HOLDs in gcode command list
|
|
self.keepTime = time.time()
|
|
hldcnt = 0
|
|
hpCmds = []
|
|
lenHP = len(self.holdStartPnts)
|
|
commands = [Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})]
|
|
self.reportThis("--Processing " + str(lenHP) + " HOLD optimizations---")
|
|
# cycle throug hold points
|
|
if lenHP > 0:
|
|
hdCnt = 0
|
|
lenGC = len(self.gcodeCmds)
|
|
for idx in range(0, lenGC):
|
|
if self.gcodeCmds[idx] == "HD":
|
|
hdCnt += 1
|
|
# process hold here
|
|
p1 = self.holdStartPnts.pop(0)
|
|
p2 = self.holdStopPnts.pop(0)
|
|
pd = self.holdPrevLayerVals.pop(0)
|
|
hscType = self.holdStopTypes.pop(0)
|
|
|
|
if hscType == "End":
|
|
N = None
|
|
# Create gcode commands to connect p1 and p2
|
|
hpCmds = self.holdStopEndCmds(obj, p2, "Hold Stop: End point")
|
|
elif hscType == "Mid":
|
|
# Set the max and min XY boundaries of the HOLD connection operation
|
|
cutterClearance = self.cutter.getDiameter() / 1.25
|
|
if p1.x < p2.x:
|
|
xmin = p1.x - cutterClearance
|
|
xmax = p2.x + cutterClearance
|
|
else:
|
|
xmin = p2.x - cutterClearance
|
|
xmax = p1.x + cutterClearance
|
|
|
|
if p1.y < p2.y:
|
|
ymin = p1.y - cutterClearance
|
|
ymax = p2.y + cutterClearance
|
|
else:
|
|
ymin = p2.y - cutterClearance
|
|
ymax = p1.y + cutterClearance
|
|
# get focused list of points based on bound box with p1 and p2 as corners, with cutter diam. as additional buffer
|
|
subCLP = self.subsectionCLP(scanCLP, xmin, ymin, xmax, ymax)
|
|
# Determine max z height for clearance between p1 and p2
|
|
zMax = self.getMaxHeight(self.targetDepth, p1, p2, self.cutter.getDiameter(), subCLP)
|
|
# Create gcode commands to connect p1 and p2
|
|
hpCmds = self.holdStopCmds(obj, zMax, pd, p2, "Hold Stop: Group processed")
|
|
# Add commands to list
|
|
for cmd in hpCmds:
|
|
commands.append(cmd)
|
|
hldcnt += 1
|
|
else:
|
|
commands.append(self.gcodeCmds[idx])
|
|
else:
|
|
commands = self.gcodeCmds
|
|
return commands
|
|
|
|
def _rotationalDropCutterOp(self, obj, stl, bb):
|
|
eT = time.time()
|
|
self.resetTolerance = 0.0000001 # degrees
|
|
self.layerEndzMax = 0.0
|
|
commands = []
|
|
scanLines = []
|
|
advances = []
|
|
iSTG = []
|
|
rSTG = []
|
|
rings = []
|
|
lCnt = 0
|
|
rNum = 0
|
|
stepDeg = 1.1
|
|
layCircum = 1.1
|
|
begIdx = 0.0
|
|
endIdx = 0.0
|
|
arc = 0.0
|
|
sumAdv = 0.0
|
|
bbRad = self.bbRadius
|
|
|
|
def invertAdvances(advances):
|
|
idxs = [1.1]
|
|
for adv in advances:
|
|
idxs.append(-1 * adv)
|
|
idxs.pop(0)
|
|
return idxs
|
|
|
|
def linesToPointRings(scanLines):
|
|
rngs = []
|
|
numPnts = len(scanLines[0]) # Number of points per line along axis, at obj.SampleInterval spacing
|
|
for line in scanLines: # extract circular set(ring) of points from scan lines
|
|
if len(line) != numPnts:
|
|
PathLog.debug("Error: line lengths not equal")
|
|
return rngs
|
|
|
|
for num in range(0, numPnts):
|
|
rngs.append([1.1]) # Initiate new ring
|
|
for line in scanLines: # extract circular set(ring) of points from scan lines
|
|
rngs[num].append(line[num])
|
|
nn = rngs[num].pop(0)
|
|
return rngs
|
|
|
|
def indexAdvances(arc, stepDeg):
|
|
indexes = [0.0]
|
|
numSteps = int(math.floor(arc / stepDeg))
|
|
for ns in range(0, numSteps):
|
|
indexes.append(stepDeg)
|
|
|
|
travel = sum(indexes)
|
|
if arc == 360.0:
|
|
indexes.insert(0, 0.0)
|
|
else:
|
|
indexes.append(arc - travel)
|
|
|
|
return indexes
|
|
|
|
# Compute number and size of stepdowns, and final depth
|
|
if obj.LayerMode == 'Single-pass':
|
|
depthparams = [self.FinalDepth]
|
|
else:
|
|
dep_par = PathUtils.depth_params(self.clearHeight, self.safeHeight, self.bbRadius, obj.StepDown.Value, 0.0, self.FinalDepth)
|
|
depthparams = [i for i in dep_par]
|
|
prevDepth = depthparams[0]
|
|
lenDP = len(depthparams)
|
|
|
|
# Set drop cutter extra offset
|
|
cdeoX = obj.DropCutterExtraOffset.x
|
|
cdeoY = obj.DropCutterExtraOffset.y
|
|
|
|
# Set updated bound box values and redefine the new min/mas XY area of the operation based on greatest point radius of model
|
|
bb.ZMin = -1 * bbRad
|
|
bb.ZMax = bbRad
|
|
if obj.RotationAxis == 'X':
|
|
bb.YMin = -1 * bbRad
|
|
bb.YMax = bbRad
|
|
ymin = 0.0
|
|
ymax = 0.0
|
|
xmin = bb.XMin - cdeoX
|
|
xmax = bb.XMax + cdeoX
|
|
else:
|
|
bb.XMin = -1 * bbRad
|
|
bb.XMax = bbRad
|
|
ymin = bb.YMin - cdeoY
|
|
ymax = bb.YMax + cdeoY
|
|
xmin = 0.0
|
|
xmax = 0.0
|
|
|
|
# Calculate arc
|
|
begIdx = obj.StartIndex
|
|
endIdx = obj.StopIndex
|
|
if endIdx < begIdx:
|
|
begIdx -= 360.0
|
|
arc = endIdx - begIdx
|
|
|
|
# Begin gcode operation with raising cutter to safe height
|
|
commands.append(Path.Command('G0', {'Z': self.safeHeight, 'F': self.vertRapid}))
|
|
|
|
# Complete rotational scans at layer and translate into gcode
|
|
for layDep in depthparams:
|
|
t_before = time.time()
|
|
self.reportThis("--layDep " + str(layDep))
|
|
|
|
# Compute circumference and step angles for current layer
|
|
layCircum = 2 * math.pi * layDep
|
|
if lenDP == 1:
|
|
layCircum = 2 * math.pi * bbRad
|
|
|
|
# Set axial feed rates
|
|
self.axialFeed = 360 / layCircum * self.horizFeed
|
|
self.axialRapid = 360 / layCircum * self.horizRapid
|
|
|
|
# Determine step angle.
|
|
if obj.RotationAxis == obj.DropCutterDir: # Same == indexed
|
|
stepDeg = (self.cutOut / layCircum) * 360.0
|
|
else:
|
|
stepDeg = (obj.SampleInterval / layCircum) * 360.0
|
|
|
|
# Limit step angle and determine rotational index angles [indexes].
|
|
if stepDeg > 120.0:
|
|
stepDeg = 120.0
|
|
advances = indexAdvances(arc, stepDeg) # Reset for each step down layer
|
|
|
|
# Perform rotational indexed scans to layer depth
|
|
if obj.RotationAxis == obj.DropCutterDir: # Same == indexed OR parallel
|
|
sample = obj.SampleInterval
|
|
else:
|
|
sample = self.cutOut
|
|
scanLines = self._indexedDropCutScan(obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample)
|
|
|
|
# Complete rotation if necessary
|
|
if arc == 360.0:
|
|
advances.append(360.0 - sum(advances))
|
|
advances.pop(0)
|
|
zero = scanLines.pop(0)
|
|
scanLines.append(zero)
|
|
|
|
# Translate OCL scans into gcode
|
|
if obj.RotationAxis == obj.DropCutterDir: # Same == indexed (cutter runs parallel to axis)
|
|
# Invert advances if RotationAxis == Y
|
|
if obj.RotationAxis == 'Y':
|
|
advances = invertAdvances(advances)
|
|
|
|
# Translate scan to gcode
|
|
# sumAdv = 0.0
|
|
sumAdv = begIdx
|
|
for sl in range(0, len(scanLines)):
|
|
sumAdv += advances[sl]
|
|
# Translate scan to gcode
|
|
iSTG = self._indexedScanToGcode(obj, sl, scanLines[sl], sumAdv, prevDepth, layDep, lenDP)
|
|
commands.extend(iSTG)
|
|
|
|
# Add rise to clear height before beginning next index in CutPattern: Line
|
|
# if obj.CutPattern == 'Line':
|
|
# commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
|
|
|
|
# Raise cutter to safe height after each index cut
|
|
commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
|
|
# Eol
|
|
else:
|
|
if obj.CutMode == 'Conventional':
|
|
advances = invertAdvances(advances)
|
|
rt = advances.reverse()
|
|
rtn = scanLines.reverse()
|
|
|
|
# Invert advances if RotationAxis == Y
|
|
if obj.RotationAxis == 'Y':
|
|
advances = invertAdvances(advances)
|
|
|
|
# Begin gcode operation with raising cutter to safe height
|
|
commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
|
|
|
|
# Convert rotational scans into gcode
|
|
rings = linesToPointRings(scanLines)
|
|
rNum = 0
|
|
for rng in rings:
|
|
rSTG = self._rotationalScanToGcode(obj, rng, rNum, prevDepth, layDep, advances)
|
|
commands.extend(rSTG)
|
|
if arc != 360.0:
|
|
clrZ = self.layerEndzMax + self.SafeHeightOffset
|
|
commands.append(Path.Command('G0', {'Z': clrZ, 'F': self.vertRapid}))
|
|
rNum += 1
|
|
# Eol
|
|
|
|
# Add rise to clear height before beginning next index in CutPattern: Line
|
|
# if obj.CutPattern == 'Line':
|
|
# commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
|
|
|
|
prevDepth = layDep
|
|
lCnt += 1 # increment layer count
|
|
self.reportThis("--Layer " + str(lCnt) + ": " + str(len(advances)) + " OCL scans and gcode in " + str(time.time() - t_before) + " s")
|
|
time.sleep(0.2)
|
|
# Eol
|
|
return commands
|
|
|
|
def _indexedDropCutScan(self, obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample):
|
|
cutterOfst = 0.0
|
|
radsRot = 0.0
|
|
reset = 0.0
|
|
iCnt = 0
|
|
Lines = []
|
|
result = None
|
|
|
|
pdc = ocl.PathDropCutter() # create a pdc
|
|
pdc.setCutter(self.cutter)
|
|
pdc.setZ(layDep) # set minimumZ (final / ta9rget depth value)
|
|
pdc.setSampling(sample)
|
|
|
|
# if self.useTiltCutter == True:
|
|
if obj.CutterTilt != 0.0:
|
|
cutterOfst = layDep * math.sin(obj.CutterTilt * math.pi / 180.0)
|
|
self.reportThis("CutterTilt: cutterOfst is " + str(cutterOfst))
|
|
|
|
sumAdv = 0.0
|
|
for adv in advances:
|
|
sumAdv += adv
|
|
if adv > 0.0:
|
|
# Rotate STL object using OCL method
|
|
radsRot = math.radians(adv)
|
|
if obj.RotationAxis == 'X':
|
|
rStl = stl.rotate(radsRot, 0.0, 0.0)
|
|
else:
|
|
rStl = stl.rotate(0.0, radsRot, 0.0)
|
|
|
|
# Set STL after rotation is made
|
|
pdc.setSTL(stl)
|
|
|
|
# add Line objects to the path in this loop
|
|
if obj.RotationAxis == 'X':
|
|
p1 = ocl.Point(xmin, cutterOfst, 0.0) # start-point of line
|
|
p2 = ocl.Point(xmax, cutterOfst, 0.0) # end-point of line
|
|
else:
|
|
p1 = ocl.Point(cutterOfst, ymin, 0.0) # start-point of line
|
|
p2 = ocl.Point(cutterOfst, ymax, 0.0) # end-point of line
|
|
|
|
# Create line object
|
|
if obj.RotationAxis == obj.DropCutterDir: # parallel cut
|
|
if obj.CutPattern == 'ZigZag':
|
|
if (iCnt % 2 == 0.0): # even
|
|
lo = ocl.Line(p1, p2)
|
|
else: # odd
|
|
lo = ocl.Line(p2, p1)
|
|
elif obj.CutPattern == 'Line':
|
|
if obj.CutMode == 'Conventional':
|
|
lo = ocl.Line(p1, p2)
|
|
else:
|
|
lo = ocl.Line(p2, p1)
|
|
else:
|
|
lo = ocl.Line(p1, p2) # line-object
|
|
|
|
path = ocl.Path() # create an empty path object
|
|
path.append(lo) # add the line to the path
|
|
pdc.setPath(path) # set path
|
|
pdc.run() # run drop-cutter on the path
|
|
result = pdc.getCLPoints()
|
|
Lines.append(result) # request the list of points
|
|
|
|
iCnt += 1
|
|
# End loop
|
|
# Rotate STL object back to original position using OCL method
|
|
reset = -1 * math.radians(sumAdv - self.resetTolerance)
|
|
if obj.RotationAxis == 'X':
|
|
rStl = stl.rotate(reset, 0.0, 0.0)
|
|
else:
|
|
rStl = stl.rotate(0.0, reset, 0.0)
|
|
self.resetTolerance = 0.0
|
|
|
|
return Lines
|
|
|
|
def _indexedScanToGcode(self, obj, li, CLP, idxAng, prvDep, layerDepth, numDeps):
|
|
# generate the path commands
|
|
output = []
|
|
optimize = obj.Optimize
|
|
holdCount = 0
|
|
holdStart = False
|
|
holdStop = False
|
|
zMax = prvDep
|
|
lenCLP = len(CLP)
|
|
lastCLP = lenCLP - 1
|
|
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 frist point
|
|
pnt.x = CLP[0].x
|
|
pnt.y = CLP[0].y
|
|
pnt.z = CLP[0].z + float(obj.DepthOffset.Value)
|
|
|
|
# Rotate to correct index location
|
|
if obj.RotationAxis == 'X':
|
|
output.append(Path.Command('G0', {'A': idxAng, 'F': self.axialFeed}))
|
|
else:
|
|
output.append(Path.Command('G0', {'B': idxAng, 'F': self.axialFeed}))
|
|
|
|
if li > 0:
|
|
if pnt.z > self.layerEndPnt.z:
|
|
clrZ = pnt.z + 2.0
|
|
output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid}))
|
|
else:
|
|
output.append(Path.Command('G0', {'Z': self.clearHeight, '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}))
|
|
|
|
for i in range(0, lenCLP):
|
|
if i < lastCLP:
|
|
nxt.x = CLP[i + 1].x
|
|
nxt.y = CLP[i + 1].y
|
|
nxt.z = CLP[i + 1].z + float(obj.DepthOffset.Value)
|
|
else:
|
|
optimize = False
|
|
|
|
# Update zMax values
|
|
if pnt.z > zMax:
|
|
zMax = pnt.z
|
|
|
|
if obj.LayerMode == 'Multi-pass':
|
|
# if z travels above previous layer, start/continue hold high cycle
|
|
if pnt.z > prvDep and optimize is True:
|
|
if self.onHold is False:
|
|
holdStart = True
|
|
self.onHold = True
|
|
|
|
if self.onHold is True:
|
|
if holdStart is True:
|
|
# go to current coordinate
|
|
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
|
|
# Save holdStart coordinate and prvDep values
|
|
self.holdPoint.x = pnt.x
|
|
self.holdPoint.y = pnt.y
|
|
self.holdPoint.z = pnt.z
|
|
holdCount += 1 # Increment hold count
|
|
holdStart = False # cancel holdStart
|
|
|
|
# hold cutter high until Z value drops below prvDep
|
|
if pnt.z <= prvDep:
|
|
holdStop = True
|
|
|
|
if holdStop is True:
|
|
# Send hold and current points to
|
|
zMax += 2.0
|
|
for cmd in self.holdStopCmds(obj, zMax, prvDep, pnt, "Hold Stop: in-line"):
|
|
output.append(cmd)
|
|
# reset necessary hold related settings
|
|
zMax = prvDep
|
|
holdStop = False
|
|
self.onHold = False
|
|
self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf"))
|
|
|
|
if self.onHold is 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, 'Z': pnt.z, 'F': self.horizFeed}))
|
|
elif i == lastCLP:
|
|
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, '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
|
|
# self.reportThis("points after optimization: " + str(len(output)))
|
|
output.append(Path.Command('N (End index angle ' + str(round(idxAng, 4)) + ')', {}))
|
|
|
|
# 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
|
|
|
|
def _rotationalScanToGcode(self, obj, RNG, rN, prvDep, layDep, advances):
|
|
# generate the path commands
|
|
output = []
|
|
nxtAng = 0
|
|
zMax = 0.0
|
|
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"))
|
|
|
|
begIdx = obj.StartIndex
|
|
endIdx = obj.StopIndex
|
|
if endIdx < begIdx:
|
|
begIdx -= 360.0
|
|
|
|
# Rotate to correct index location
|
|
axisOfRot = 'A'
|
|
if obj.RotationAxis == 'Y':
|
|
axisOfRot = 'B'
|
|
|
|
# Create frist point
|
|
ang = 0.0 + obj.CutterTilt
|
|
pnt.x = RNG[0].x
|
|
pnt.y = RNG[0].y
|
|
pnt.z = RNG[0].z + float(obj.DepthOffset.Value)
|
|
|
|
# Adjust feed rate based on radius/circumferance of cutter.
|
|
# Original feed rate based on travel at circumferance.
|
|
if rN > 0:
|
|
# if pnt.z > self.layerEndPnt.z:
|
|
if pnt.z >= self.layerEndzMax:
|
|
clrZ = pnt.z + 5.0
|
|
output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid}))
|
|
else:
|
|
output.append(Path.Command('G1', {'Z': self.clearHeight, 'F': self.vertRapid}))
|
|
|
|
output.append(Path.Command('G0', {axisOfRot: ang, 'F': self.axialFeed}))
|
|
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.axialFeed}))
|
|
output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.axialFeed}))
|
|
|
|
lenRNG = len(RNG)
|
|
lastIdx = lenRNG - 1
|
|
for i in range(0, lenRNG):
|
|
if i < lastIdx:
|
|
nxtAng = ang + advances[i + 1]
|
|
nxt.x = RNG[i + 1].x
|
|
nxt.y = RNG[i + 1].y
|
|
nxt.z = RNG[i + 1].z + float(obj.DepthOffset.Value)
|
|
|
|
if pnt.z > zMax:
|
|
zMax = pnt.z
|
|
|
|
output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, axisOfRot: ang, 'F': self.axialFeed}))
|
|
pnt.x = nxt.x
|
|
pnt.y = nxt.y
|
|
pnt.z = nxt.z
|
|
ang = nxtAng
|
|
# self.reportThis("points after optimization: " + str(len(output)))
|
|
|
|
# Save layer end point for use in transitioning to next layer
|
|
self.layerEndPnt.x = RNG[0].x
|
|
self.layerEndPnt.y = RNG[0].y
|
|
self.layerEndPnt.z = RNG[0].z
|
|
self.layerEndIdx = ang
|
|
self.layerEndzMax = zMax
|
|
|
|
# Move cutter to final point
|
|
# output.append(Path.Command('G1', {'X': self.layerEndPnt.x, 'Y': self.layerEndPnt.y, 'Z': self.layerEndPnt.z, axisOfRot: endang, 'F': self.axialFeed}))
|
|
|
|
return output
|
|
|
|
def _waterlineOp(self, obj, stl, bb):
|
|
t_begin = time.time() # self.keepTime = time.time()
|
|
commands = []
|
|
|
|
# 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
|
|
cdeoX = 0.6 * self.cutter.getDiameter()
|
|
cdeoY = 0.6 * self.cutter.getDiameter()
|
|
|
|
# the max and min XY area of the operation
|
|
xmin = bb.XMin - cdeoX
|
|
xmax = bb.XMax + cdeoX
|
|
ymin = bb.YMin - cdeoY
|
|
ymax = bb.YMax + cdeoY
|
|
|
|
smplInt = obj.SampleInterval
|
|
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:
|
|
dep_par = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value)
|
|
depthparams = [dp for dp in dep_par]
|
|
lenDP = len(depthparams)
|
|
|
|
# Scan the piece to depth at smplInt
|
|
oclScan = []
|
|
oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines)
|
|
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])
|
|
self.reportThis("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line")
|
|
self.reportThis("--Setup, OCL scan, and scan conversion to multi-dimen. list took " + str(time.time() - t_begin) + " s")
|
|
|
|
# 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
|
|
self.reportThis("--All layer scans combined took " + str(time.time() - layTime) + " s")
|
|
return commands
|
|
|
|
def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines):
|
|
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
|
|
l = ocl.Line(p1, p2) # line-object
|
|
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):
|
|
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
|
|
rtn = self._bufferTopoMap(lenSL, pntsPerLine)
|
|
# Identify layer waterline from OCL scan
|
|
rtn = self._highlightWaterline(4, 9)
|
|
# Extract waterline and convert to gcode
|
|
loopList = self._extractWaterlines(obj, scanLines, lyr, layDep)
|
|
time.sleep(0.1)
|
|
# save commands
|
|
for loop in loopList:
|
|
cmds = self._loopToGcode(obj, layDep, loop)
|
|
commands.extend(cmds)
|
|
return commands
|
|
|
|
def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine):
|
|
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):
|
|
# 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):
|
|
TM = self.topoMap
|
|
lastPnt = len(TM[1]) - 1
|
|
lastLn = len(TM) - 1
|
|
highFlag = 0
|
|
|
|
# self.reportThis("--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
|
|
|
|
# self.reportThis("--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
|
|
# self.reportThis("--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
|
|
# self.reportThis("--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
|
|
|
|
# PathLog.debug("\n-------------")
|
|
# for li in TM:
|
|
# PathLog.debug("Line: " + str(li))
|
|
return True
|
|
|
|
def _extractWaterlines(self, obj, oclScan, lyr, layDep):
|
|
srch = True
|
|
lastPnt = len(self.topoMap[0]) - 1
|
|
lastLn = len(self.topoMap) - 1
|
|
maxSrchs = 5
|
|
srchCnt = 1
|
|
loopList = []
|
|
loop = []
|
|
loopNum = 0
|
|
|
|
if obj.CutMode == 'Conventional':
|
|
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:
|
|
self.reportThis("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
|
|
# self.reportThis("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.")
|
|
return loopList
|
|
|
|
def _trackLoop(self, oclScan, lC, pC, L, P, loopNum):
|
|
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:
|
|
self.reportThis("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):
|
|
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:
|
|
# self.reportThis("_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]
|
|
|
|
# self.reportThis("_findNext: No next pnt found")
|
|
return [cl, cp, num]
|
|
|
|
def _loopToGcode(self, obj, layDep, loop):
|
|
# generate the path commands
|
|
output = []
|
|
optimize = obj.Optimize
|
|
|
|
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 frist 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
|
|
# self.reportThis("points after optimization: " + str(len(output)))
|
|
|
|
# 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
|
|
|
|
def isPointOnLine(self, lineA, lineB, pointP):
|
|
tolerance = 1e-6
|
|
vectorAB = lineB - lineA
|
|
vectorAC = pointP - lineA
|
|
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 holdStopCmds(self, obj, zMax, pd, p2, txt):
|
|
cmds = []
|
|
msg = 'N (' + txt + ')'
|
|
cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel
|
|
cmds.append(Path.Command('G0', {'Z': zMax, '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
|
|
if zMax != pd:
|
|
cmds.append(Path.Command('G0', {'Z': pd, 'F': self.vertRapid})) # drop cutter down rapidly to prevDepth depth
|
|
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):
|
|
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):
|
|
# This function returns a subsection of the CLP scan, limited to the min/max values supplied
|
|
section = []
|
|
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 getMaxHeight(self, 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
|
|
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:
|
|
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 holdStopPerpCmds(self, obj, zMax, pd, p2, aor, ang, txt):
|
|
cmds = []
|
|
msg = 'N (' + txt + ')'
|
|
cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel
|
|
cmds.append(Path.Command('G0', {'Z': zMax, '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
|
|
if zMax != pd:
|
|
cmds.append(Path.Command('G0', {'Z': pd, 'F': self.vertRapid})) # drop cutter down rapidly to prevDepth depth
|
|
cmds.append(Path.Command('G0', {'Z': p2.z, aor: ang, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed
|
|
return cmds
|
|
|
|
def reportThis(self, txt):
|
|
self.opReport += "\n" + txt
|
|
|
|
def resetOpVariables(self):
|
|
# reset operation variables
|
|
self.opReport = ""
|
|
self.cutter = None
|
|
self.holdPoint = None
|
|
self.stl = None
|
|
self.layerEndPnt = None
|
|
self.onHold = False
|
|
self.useTiltCutter = False
|
|
self.holdStartPnts = []
|
|
self.holdStopPnts = []
|
|
self.holdStopTypes = []
|
|
self.holdZMaxVals = []
|
|
self.holdPrevLayerVals = []
|
|
self.gcodeCmds = []
|
|
self.CLP = []
|
|
self.SafeHeightOffset = 2.0
|
|
self.ClearHeightOffset = 4.0
|
|
self.layerEndIdx = 0.0
|
|
self.layerEndzMax = 0.0
|
|
self.resetTolerance = 0.0
|
|
self.holdPntCnt = 0
|
|
self.startTime = 0.0
|
|
self.endTime = 0.0
|
|
self.lineCNT = 0
|
|
self.keepTime = 0.0
|
|
self.lineScanTime = 0.0
|
|
self.bbRadius = 0.0
|
|
self.targetDepth = 0.0
|
|
self.stepDeg = 0.0
|
|
self.stepRads = 0.0
|
|
self.axialFeed = 0.0
|
|
self.axialRapid = 0.0
|
|
self.FinalDepth = 0.0
|
|
self.cutOut = 0.0
|
|
self.clearHeight = 0.0
|
|
self.safeHeight = 0.0
|
|
return True
|
|
|
|
def setOclCutter(self, obj):
|
|
# Set cutter details
|
|
# https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details
|
|
diam_1 = obj.ToolController.Tool.Diameter
|
|
lenOfst = obj.ToolController.Tool.LengthOffset
|
|
FR = obj.ToolController.Tool.FlatRadius
|
|
CEH = obj.ToolController.Tool.CuttingEdgeHeight
|
|
|
|
if obj.ToolController.Tool.ToolType == 'EndMill':
|
|
# Standard End Mill
|
|
self.cutter = 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.cutter = ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst))
|
|
self.useTiltCutter = True
|
|
|
|
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)
|
|
self.cutter = 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)
|
|
self.cutter = ocl.ConeCutter(diam_1, (obj.ToolController.Tool.CuttingEdgeAngle / 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)
|
|
self.cutter = ocl.ConeCutter(diam_1, (obj.ToolController.Tool.CuttingEdgeAngle / 2), lenOfst)
|
|
|
|
# http://www.carbidecutter.net/products/carbide-burr-cone-shape-sm.html
|
|
'''
|
|
return "Drill";
|
|
return "CenterDrill";
|
|
return "CounterSink";
|
|
return "CounterBore";
|
|
return "FlyCutter";
|
|
return "Reamer";
|
|
return "Tap";
|
|
return "EndMill";
|
|
return "SlotCutter";
|
|
return "BallEndMill";
|
|
return "ChamferMill";
|
|
return "CornerRound";
|
|
return "Engraver";
|
|
return "Undefined";
|
|
'''
|
|
return True
|
|
|
|
def pocketInvertExtraOffset(self):
|
|
return True
|
|
|
|
def opSetDefaultValues(self, obj, job):
|
|
'''opSetDefaultValues(obj, job) ... initialize defaults'''
|
|
|
|
obj.StepOver = 100
|
|
obj.Optimize = True
|
|
obj.IgnoreWaste = False
|
|
obj.ReleaseFromWaste = False
|
|
obj.LayerMode = 'Single-pass'
|
|
obj.ScanType = 'Planar'
|
|
obj.RotationAxis = 'X'
|
|
obj.CutMode = 'Conventional'
|
|
obj.CutPattern = 'ZigZag'
|
|
obj.CutterTilt = 0.0
|
|
obj.StartIndex = 0.0
|
|
obj.StopIndex = 360.0
|
|
obj.SampleInterval = 1.0
|
|
|
|
# need to overwrite the default depth calculations for facing
|
|
job = PathUtils.findParentJob(obj)
|
|
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 self.docRestored is True: # This op is NOT the first in the Operations list
|
|
PathLog.debug("doc restored")
|
|
obj.FinalDepth.Value = obj.OpFinalDepth.Value
|
|
else:
|
|
PathLog.debug("new operation")
|
|
obj.OpFinalDepth.Value = d.final_depth
|
|
obj.OpStartDepth.Value = d.start_depth
|
|
if self.initOpFinalDepth is None and self.initFinalDepth is None:
|
|
self.initFinalDepth = d.final_depth
|
|
self.initOpFinalDepth = d.final_depth
|
|
else:
|
|
PathLog.debug("-initFinalDepth" + str(self.initFinalDepth))
|
|
PathLog.debug("-initOpFinalDepth" + str(self.initOpFinalDepth))
|
|
obj.IgnoreWasteDepth = obj.FinalDepth.Value + 0.001
|
|
|
|
|
|
def SetupProperties():
|
|
setup = []
|
|
setup.append("Algorithm")
|
|
setup.append("DropCutterDir")
|
|
setup.append("BoundBox")
|
|
setup.append("StepOver")
|
|
setup.append("DepthOffset")
|
|
setup.append("LayerMode")
|
|
setup.append("ScanType")
|
|
setup.append("RotationAxis")
|
|
setup.append("CutMode")
|
|
setup.append("SampleInterval")
|
|
setup.append("StartIndex")
|
|
setup.append("StopIndex")
|
|
setup.append("CutterTilt")
|
|
setup.append("CutPattern")
|
|
setup.append("IgnoreWasteDepth")
|
|
setup.append("IgnoreWaste")
|
|
setup.append("ReleaseFromWaste")
|
|
return setup
|
|
|
|
|
|
def Create(name, obj=None):
|
|
'''Create(name) ... Creates and returns a Surface operation.'''
|
|
if obj is None:
|
|
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
|
proxy = ObjectSurface(obj, name)
|
|
return obj
|