Files
create/src/Mod/Path/PathScripts/PathSurface.py
Russell Johnson aaae829704 Path: Integrate 4th-axis feature to PathPocketShape and 3D Surface ops (#2114)
* 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
2019-05-06 21:29:08 -05:00

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