[CAM] VCarve improvements (#14093)
* Add stepDown setting to Vcarve Op * fix UI issued, add finishing pass to Vcarve * Improve step-down performance, add debugVoronoi() * add debugVoronoi method * Add movement optimization * add CAM/Vcarve unit-tests * Disable debugging mode * Cache caller info in debug method * Format code
This commit is contained in:
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>197</height>
|
||||
<width>739</width>
|
||||
<height>379</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -55,65 +55,109 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget">
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="toolTip">
|
||||
<string></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Discretization Deflection</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QDoubleSpinBox" name="discretize">
|
||||
<property name="toolTip">
|
||||
<string>This value is used in discretizing arcs into segments. Smaller values will result in larger gcode. Larger values may cause unwanted segments in the medial line path.</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.001000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.010000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>0.010000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="toolTip">
|
||||
<string></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Filter Colinear lines</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="colinearFilter">
|
||||
<property name="toolTip">
|
||||
<string>Sets how aggressively colinear segments are filtered from the Voronoi diagram. Valid values are 0 - 90 degrees (larger numbers filter more). Default = 10</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>90</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="toolTip">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Discretization Deflection</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QDoubleSpinBox" name="discretize">
|
||||
<property name="toolTip">
|
||||
<string>This value is used in discretizing arcs into segments. Smaller values will result in larger gcode. Larger values may cause unwanted segments in the medial line path.</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.001000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.010000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>0.010000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="toolTip">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Filter Colinear lines</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="colinearFilter">
|
||||
<property name="toolTip">
|
||||
<string>Sets how aggressively colinear segments are filtered from the Voronoi diagram. Valid values are 0 - 90 degrees (larger numbers filter more). Default = 10</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>90</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="finishingPassZOffsetLabel">
|
||||
<property name="text">
|
||||
<string>Finishing pass Z offset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="Gui::QuantitySpinBox" name="finishingPassZOffset">
|
||||
<property name="toolTip">
|
||||
<string>Endmill offset for the finishing pass run. Use small value like -0.2 mm to help clean "fuzzy skin" or other artefacts.</string>
|
||||
</property>
|
||||
<property name="unit" stdset="0">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="finishingPassEnabled">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>After carving travel again the path to remove artifacts and imperfections</string>
|
||||
</property>
|
||||
<property name="statusTip">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Finishing pass</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QCheckBox" name="optimizeMovementsEnabled">
|
||||
<property name="toolTip">
|
||||
<string>Optimize path to avoid raising endmill when moving to adjacent edges. May result in sub-millimeter inaccuracies. </string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Optimize movements</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
@@ -130,6 +174,13 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>Gui::QuantitySpinBox</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>Gui/QuantitySpinBox.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
||||
@@ -102,6 +102,7 @@ def _caller():
|
||||
def _log(level, module_line_func, msg):
|
||||
"""internal function to do the logging"""
|
||||
module, line, func = module_line_func
|
||||
|
||||
if getLevel(module) >= level:
|
||||
message = "%s.%s: %s" % (module, Level.toString(level), msg)
|
||||
if _useConsole:
|
||||
@@ -122,9 +123,10 @@ def _log(level, module_line_func, msg):
|
||||
|
||||
def debug(msg):
|
||||
"""(message)"""
|
||||
module, line, func = _caller()
|
||||
caller_info = _caller()
|
||||
_, line, _ = caller_info
|
||||
msg = "({}) - {}".format(line, msg)
|
||||
return _log(Level.DEBUG, _caller(), msg)
|
||||
return _log(Level.DEBUG, caller_info, msg)
|
||||
|
||||
|
||||
def info(msg):
|
||||
|
||||
@@ -463,6 +463,15 @@ class ObjectOp(object):
|
||||
QT_TRANSLATE_NOOP("App::Property", "Operations Cycle Time Estimation"),
|
||||
)
|
||||
|
||||
if FeatureStepDown & features and not hasattr(obj, "StepDown"):
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"StepDown",
|
||||
"Depth",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Incremental Step Down of Tool"),
|
||||
)
|
||||
obj.StepDown = 0
|
||||
|
||||
self.setEditorModes(obj, features)
|
||||
self.opOnDocumentRestored(obj)
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ import FreeCADGui
|
||||
import Path
|
||||
import Path.Op.Gui.Base as PathOpGui
|
||||
import Path.Op.Vcarve as PathVcarve
|
||||
import Path.Base.Gui.Util as PathGuiUtil
|
||||
|
||||
import PathGui
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
from PySide import QtCore, QtGui
|
||||
@@ -35,6 +37,7 @@ __author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecad.org"
|
||||
__doc__ = "Vcarve operation page controller and command implementation."
|
||||
|
||||
# There is a bug in logging library. To enable debugging - set True also in Op/Vcarve.py
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
@@ -124,9 +127,37 @@ class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
|
||||
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
"""Page controller class for the Vcarve operation."""
|
||||
|
||||
def initPage(self, obj):
|
||||
self.finishingPassZOffsetSpinBox = PathGuiUtil.QuantitySpinBox(
|
||||
self.form.finishingPassZOffset, obj, "FinishingPassZOffset"
|
||||
)
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... returns UI"""
|
||||
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpVcarveEdit.ui")
|
||||
form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpVcarveEdit.ui")
|
||||
self.updateFormConditionalState(form)
|
||||
return form
|
||||
|
||||
def updateFormConditionalState(self, form):
|
||||
"""
|
||||
Update conditional form controls - i.e settings that should be
|
||||
visible only under certain conditions (other settings enabled, etc).
|
||||
"""
|
||||
|
||||
if form.finishingPassEnabled.isChecked():
|
||||
form.finishingPassZOffset.setVisible(True)
|
||||
form.finishingPassZOffsetLabel.setVisible(True)
|
||||
else:
|
||||
form.finishingPassZOffset.setVisible(False)
|
||||
form.finishingPassZOffsetLabel.setVisible(False)
|
||||
|
||||
def updateFormCallback(self):
|
||||
return self.updateFormConditionalState(self.form)
|
||||
|
||||
def registerSignalHandlers(self, obj):
|
||||
"""Register signal handlers to update conditiona UI states"""
|
||||
|
||||
self.form.finishingPassEnabled.stateChanged.connect(self.updateFormCallback)
|
||||
|
||||
def getFields(self, obj):
|
||||
"""getFields(obj) ... transfers values from UI to obj's properties"""
|
||||
@@ -134,6 +165,15 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
obj.Discretize = self.form.discretize.value()
|
||||
if obj.Colinear != self.form.colinearFilter.value():
|
||||
obj.Colinear = self.form.colinearFilter.value()
|
||||
|
||||
if obj.FinishingPass != self.form.finishingPassEnabled.isChecked():
|
||||
obj.FinishingPass = self.form.finishingPassEnabled.isChecked()
|
||||
|
||||
if obj.OptimizeMovements != self.form.optimizeMovementsEnabled.isChecked():
|
||||
obj.OptimizeMovements = self.form.optimizeMovementsEnabled.isChecked()
|
||||
|
||||
self.finishingPassZOffsetSpinBox.updateProperty()
|
||||
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
self.updateCoolant(obj, self.form.coolantController)
|
||||
|
||||
@@ -141,14 +181,27 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
"""setFields(obj) ... transfers obj's property values to UI"""
|
||||
self.form.discretize.setValue(obj.Discretize)
|
||||
self.form.colinearFilter.setValue(obj.Colinear)
|
||||
self.form.finishingPassEnabled.setChecked(obj.FinishingPass)
|
||||
self.form.optimizeMovementsEnabled.setChecked(obj.OptimizeMovements)
|
||||
|
||||
self.finishingPassZOffsetSpinBox.updateSpinBox()
|
||||
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self.setupCoolant(obj, self.form.coolantController)
|
||||
|
||||
self.updateFormConditionalState(self.form)
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
|
||||
signals = []
|
||||
signals.append(self.form.discretize.editingFinished)
|
||||
signals.append(self.form.colinearFilter.editingFinished)
|
||||
signals.append(self.form.finishingPassEnabled.stateChanged)
|
||||
signals.append(self.form.finishingPassZOffset.editingFinished)
|
||||
|
||||
signals.append(self.form.optimizeMovementsEnabled.stateChanged)
|
||||
|
||||
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
signals.append(self.form.coolantController.currentIndexChanged)
|
||||
return signals
|
||||
|
||||
@@ -29,7 +29,6 @@ import PathScripts.PathUtils as PathUtils
|
||||
import math
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
|
||||
from PySide import QtCore
|
||||
|
||||
__doc__ = "Class and implementation of CAM Vcarve operation"
|
||||
@@ -42,6 +41,8 @@ COLINEAR = 4
|
||||
TWIN = 5
|
||||
BORDERLINE = 6
|
||||
|
||||
# There is a bug in logging library. To enable debugging - set True also in Gui/Vcarve.py
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
@@ -50,7 +51,6 @@ else:
|
||||
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
_sorting = "global"
|
||||
|
||||
|
||||
def _collectVoronoiWires(vd):
|
||||
@@ -150,13 +150,50 @@ def _sortVoronoiWires(wires, start=FreeCAD.Vector(0, 0, 0)):
|
||||
class _Geometry(object):
|
||||
"""POD class so the limits only have to be calculated once."""
|
||||
|
||||
def __init__(self, zStart, zStop, zScale):
|
||||
def __init__(self, zStart, zStop, zScale, zStepDown):
|
||||
self.start = zStart
|
||||
self.stop = zStop
|
||||
self.scale = zScale
|
||||
self.stepDown = zStepDown
|
||||
self.stepDownPass = 1
|
||||
|
||||
# offset is used in finishing passes to override
|
||||
# any calculated vcarving depths. Usually going deeper 0.1-0.2 mm on finishing pass can help
|
||||
# remove "fuzzy skin" or other imperfections.
|
||||
self.offset = 0
|
||||
|
||||
def incrementStepDownDepth(self, maximumUsableDepth):
|
||||
"""
|
||||
Increase stepDown depth before staring new carving pass.
|
||||
:returns: True if successful, False if maximum depth achieved
|
||||
"""
|
||||
|
||||
# do not allow to increase depth if we are already at stop depth
|
||||
if self.maximumDepth == self.stop:
|
||||
return False
|
||||
|
||||
# do not allow to increase depth if we are already at
|
||||
# maximum usable depth
|
||||
|
||||
if self.maximumDepth <= maximumUsableDepth:
|
||||
return False
|
||||
|
||||
self.stepDownPass += 1
|
||||
return True
|
||||
|
||||
@property
|
||||
def maximumDepth(self):
|
||||
"""
|
||||
Return maximum vcarving depth computed from step down setting and pass number
|
||||
"""
|
||||
|
||||
if self.stepDown == 0:
|
||||
return self.stop
|
||||
|
||||
return max(self.stop, self.start - (self.stepDownPass * self.stepDown))
|
||||
|
||||
@classmethod
|
||||
def FromTool(cls, tool, zStart, zFinal):
|
||||
def FromTool(cls, tool, zStart, zFinal, zStepDown=0):
|
||||
rMax = float(tool.Diameter) / 2.0
|
||||
rMin = float(tool.TipDiameter) / 2.0
|
||||
toolangle = math.tan(math.radians(tool.CuttingEdgeAngle.Value / 2.0))
|
||||
@@ -164,32 +201,65 @@ class _Geometry(object):
|
||||
zStop = zStart - rMax * zScale
|
||||
zOff = rMin * zScale
|
||||
|
||||
return _Geometry(zStart + zOff, max(zStop + zOff, zFinal), zScale)
|
||||
return _Geometry(zStart + zOff, max(zStop + zOff, zFinal), zScale, zStepDown)
|
||||
|
||||
@classmethod
|
||||
def FromObj(cls, obj, model):
|
||||
zStart = model.Shape.BoundBox.ZMax
|
||||
finalDepth = obj.FinalDepth.Value
|
||||
stepDown = abs(obj.StepDown.Value)
|
||||
|
||||
return cls.FromTool(obj.ToolController.Tool, zStart, finalDepth)
|
||||
return cls.FromTool(obj.ToolController.Tool, zStart, finalDepth, stepDown)
|
||||
|
||||
|
||||
def _calculate_depth(MIC, geom):
|
||||
# given a maximum inscribed circle (MIC) and tool angle,
|
||||
# return depth of cut relative to zStart.
|
||||
depth = geom.start - round(MIC * geom.scale, 4)
|
||||
Path.Log.debug("zStart value: {} depth: {}".format(geom.start, depth))
|
||||
|
||||
return max(depth, geom.stop)
|
||||
return max(depth, geom.maximumDepth) + geom.offset
|
||||
|
||||
|
||||
def _getPartEdge(edge, depths):
|
||||
def _get_maximumUsableDepth(wires, geom):
|
||||
"""
|
||||
Calculate maximum engraving depth for a list of wires
|
||||
belonging to one face.
|
||||
"""
|
||||
|
||||
def _get_depth(MIC, geom):
|
||||
"""Similar logic to _calculate_depth but without stepdown and offset calculations"""
|
||||
depth = geom.start - round(MIC * geom.scale, 4)
|
||||
return max(depth, geom.stop)
|
||||
|
||||
min_depth = None
|
||||
|
||||
for wire in wires:
|
||||
for edge in wire:
|
||||
dist = edge.getDistances()
|
||||
depth = min(_get_depth(dist[0], geom), _get_depth(dist[1], geom))
|
||||
|
||||
if min_depth is None:
|
||||
min_depth = depth
|
||||
else:
|
||||
min_depth = min(min_depth, depth)
|
||||
|
||||
return min_depth
|
||||
|
||||
|
||||
def _getPartEdge(edge, geom):
|
||||
dist = edge.getDistances()
|
||||
zBegin = _calculate_depth(dist[0], depths)
|
||||
zEnd = _calculate_depth(dist[1], depths)
|
||||
zBegin = _calculate_depth(dist[0], geom)
|
||||
zEnd = _calculate_depth(dist[1], geom)
|
||||
return edge.toShape(zBegin, zEnd)
|
||||
|
||||
|
||||
def _getPartEdges(obj, vWire, geom):
|
||||
edges = []
|
||||
for e in vWire:
|
||||
edges.append(_getPartEdge(e, geom))
|
||||
return edges
|
||||
|
||||
|
||||
class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
"""Proxy class for Vcarve operation."""
|
||||
|
||||
@@ -199,6 +269,7 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
PathOp.FeatureTool
|
||||
| PathOp.FeatureHeights
|
||||
| PathOp.FeatureDepths
|
||||
| PathOp.FeatureStepDown
|
||||
| PathOp.FeatureBaseFaces
|
||||
| PathOp.FeatureCoolant
|
||||
)
|
||||
@@ -215,6 +286,30 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
)
|
||||
obj.setEditorMode("BaseShapes", 2) # hide
|
||||
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"OptimizeMovements",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Optimize movements"),
|
||||
)
|
||||
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"FinishingPass",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Add finishing pass"),
|
||||
)
|
||||
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"FinishingPassZOffset",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Finishing pass Z offset"),
|
||||
)
|
||||
|
||||
obj.FinishingPass = False
|
||||
obj.FinishingPassZOffset = "0.00"
|
||||
|
||||
def initOperation(self, obj):
|
||||
"""initOperation(obj) ... create vcarve specific properties."""
|
||||
obj.addProperty(
|
||||
@@ -241,6 +336,7 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Vcarve Tolerance"),
|
||||
)
|
||||
|
||||
obj.Colinear = 10.0
|
||||
obj.Discretize = 0.01
|
||||
obj.Tolerance = Path.Preferences.defaultGeometryTolerance()
|
||||
@@ -250,14 +346,13 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
# upgrade ...
|
||||
self.setupAdditionalProperties(obj)
|
||||
|
||||
def _getPartEdges(self, obj, vWire, geom):
|
||||
edges = []
|
||||
for e in vWire:
|
||||
edges.append(_getPartEdge(e, geom))
|
||||
return edges
|
||||
def buildMedialWires(self, obj, faces):
|
||||
"""
|
||||
constructs a medial axis path using openvoronoi
|
||||
:returns: dictionary - each face object is a key containing list of wires"""
|
||||
|
||||
def buildPathMedial(self, obj, faces):
|
||||
"""constructs a medial axis path using openvoronoi"""
|
||||
wires_by_face = dict()
|
||||
self.voronoiDebugCache = dict()
|
||||
|
||||
def insert_many_wires(vd, wires):
|
||||
for wire in wires:
|
||||
@@ -276,34 +371,18 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
dist = ptv[-1].distanceToPoint(ptv[0])
|
||||
if dist < FreeCAD.Base.Precision.confusion():
|
||||
Path.Log.debug(
|
||||
"Removing bad carve point: {} from polygon origin"
|
||||
.format(dist))
|
||||
"Removing bad carve point: {} from polygon origin".format(
|
||||
dist
|
||||
)
|
||||
)
|
||||
del ptv[-1]
|
||||
ptv.append(ptv[0])
|
||||
|
||||
for i in range(len(ptv)-1):
|
||||
for i in range(len(ptv) - 1):
|
||||
vd.addSegment(ptv[i], ptv[i + 1])
|
||||
|
||||
def cutWire(edges):
|
||||
path = []
|
||||
path.append(Path.Command("G0 Z{}".format(obj.SafeHeight.Value)))
|
||||
e = edges[0]
|
||||
p = e.valueAt(e.FirstParameter)
|
||||
path.append(
|
||||
Path.Command("G0 X{} Y{} Z{}".format(p.x, p.y, obj.SafeHeight.Value))
|
||||
)
|
||||
hSpeed = obj.ToolController.HorizFeed.Value
|
||||
vSpeed = obj.ToolController.VertFeed.Value
|
||||
path.append(
|
||||
Path.Command("G1 X{} Y{} Z{} F{}".format(p.x, p.y, p.z, vSpeed))
|
||||
)
|
||||
for e in edges:
|
||||
path.extend(Path.Geom.cmdsForEdge(e, hSpeed=hSpeed, vSpeed=vSpeed))
|
||||
|
||||
return path
|
||||
|
||||
voronoiWires = []
|
||||
for f in faces:
|
||||
voronoiWires = []
|
||||
vd = Path.Voronoi.Diagram()
|
||||
insert_many_wires(vd, f.Wires)
|
||||
|
||||
@@ -328,28 +407,136 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
vd.colorTwins(TWIN)
|
||||
|
||||
wires = _collectVoronoiWires(vd)
|
||||
if _sorting != "global":
|
||||
wires = _sortVoronoiWires(wires)
|
||||
wires = _sortVoronoiWires(wires)
|
||||
voronoiWires.extend(wires)
|
||||
|
||||
if _sorting == "global":
|
||||
voronoiWires = _sortVoronoiWires(voronoiWires)
|
||||
wires_by_face[f] = voronoiWires
|
||||
self.voronoiDebugCache = wires_by_face
|
||||
|
||||
geom = _Geometry.FromObj(obj, self.model[0])
|
||||
return wires_by_face
|
||||
|
||||
def buildCommandList(self, obj, faces):
|
||||
"""
|
||||
Build command list to cut wires - based on voronoi
|
||||
wire list from buildMedialWires
|
||||
"""
|
||||
|
||||
def getCurrentPosition(wire):
|
||||
"""
|
||||
Calculate CNC head position assuming it reached the end of the wire
|
||||
"""
|
||||
|
||||
if not wire:
|
||||
return None
|
||||
|
||||
lastEdge = wire[-1]
|
||||
return lastEdge.valueAt(lastEdge.LastParameter)
|
||||
|
||||
def cutWires(wires, pathlist, optimizeMovements=False):
|
||||
currentPosition = None
|
||||
for w in wires:
|
||||
pWire = _getPartEdges(obj, w, geom)
|
||||
if pWire:
|
||||
pathlist.extend(_cutWire(pWire, currentPosition))
|
||||
|
||||
# movement optimization only works if we provide current head position
|
||||
if optimizeMovements:
|
||||
currentPosition = getCurrentPosition(pWire)
|
||||
|
||||
def canSkipRepositioning(currentPosition, newPosition):
|
||||
"""
|
||||
Calculate if it makes sense to raise head to safe height and reposition before
|
||||
starting to cut another edge
|
||||
"""
|
||||
|
||||
if not currentPosition:
|
||||
return False
|
||||
|
||||
# get vertex position on X/Y plane only
|
||||
v0 = FreeCAD.Base.Vector(currentPosition.x, currentPosition.y)
|
||||
v1 = FreeCAD.Base.Vector(newPosition.x, newPosition.y)
|
||||
|
||||
return v0.distanceToPoint(v1) <= 0.5
|
||||
|
||||
def _cutWire(wire, currentPosition=None):
|
||||
path = []
|
||||
|
||||
e = wire[0]
|
||||
newPosition = e.valueAt(e.FirstParameter)
|
||||
|
||||
# raise and reposition the head only if new wire starts further than 0.5 mm
|
||||
# from current head position
|
||||
if not canSkipRepositioning(currentPosition, newPosition):
|
||||
path.append(Path.Command("G0 Z{}".format(obj.SafeHeight.Value)))
|
||||
path.append(
|
||||
Path.Command(
|
||||
"G0 X{} Y{} Z{}".format(
|
||||
newPosition.x, newPosition.y, obj.SafeHeight.Value
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
hSpeed = obj.ToolController.HorizFeed.Value
|
||||
vSpeed = obj.ToolController.VertFeed.Value
|
||||
path.append(
|
||||
Path.Command(
|
||||
"G1 X{} Y{} Z{} F{}".format(
|
||||
newPosition.x, newPosition.y, newPosition.z, vSpeed
|
||||
)
|
||||
)
|
||||
)
|
||||
for e in wire:
|
||||
path.extend(Path.Geom.cmdsForEdge(e, hSpeed=hSpeed, vSpeed=vSpeed))
|
||||
|
||||
return path
|
||||
|
||||
pathlist = []
|
||||
pathlist.append(Path.Command("(starting)"))
|
||||
for w in voronoiWires:
|
||||
pWire = self._getPartEdges(obj, w, geom)
|
||||
if pWire:
|
||||
wires.append(pWire)
|
||||
pathlist.extend(cutWire(pWire))
|
||||
|
||||
# iterate over each face separatedly
|
||||
for face, wires in self.buildMedialWires(obj, faces).items():
|
||||
|
||||
geom = _Geometry.FromObj(obj, self.model[0])
|
||||
|
||||
# If using depth step-down, calculate maximum usable depth for current face.
|
||||
# This is done to avoid adding additional step-down engraving passes when it
|
||||
# would make no sense as depth is limited by Maximum Inscribed Circle anyway.
|
||||
|
||||
maximumUsableDepth = geom.stop
|
||||
|
||||
if geom.stepDown > 0:
|
||||
_maximumUsableDepth = _get_maximumUsableDepth(wires, geom)
|
||||
if _maximumUsableDepth is not None:
|
||||
maximumUsableDepth = _maximumUsableDepth
|
||||
Path.Log.debug(
|
||||
f"Maximum usable depth for current face: {maximumUsableDepth}"
|
||||
)
|
||||
|
||||
# first pass
|
||||
cutWires(wires, pathlist, obj.OptimizeMovements)
|
||||
|
||||
# subsequent stepDown depth passes (if any)
|
||||
while geom.incrementStepDownDepth(maximumUsableDepth):
|
||||
cutWires(wires, pathlist, obj.OptimizeMovements)
|
||||
|
||||
# add finishing pass if enabled
|
||||
|
||||
# if obj.FinishingPass:
|
||||
# geom.offset = obj.FinishingPassZOffset.Value
|
||||
|
||||
# for w in wires:
|
||||
# pWire = self._getPartEdges(obj, w, geom)
|
||||
# if pWire:
|
||||
# pathlist.extend(cutWire(pWire))
|
||||
|
||||
self.commandlist = pathlist
|
||||
|
||||
def opExecute(self, obj):
|
||||
"""opExecute(obj) ... process engraving operation"""
|
||||
Path.Log.track()
|
||||
|
||||
self.voronoiDebugCache = None
|
||||
|
||||
if not hasattr(obj.ToolController.Tool, "CuttingEdgeAngle"):
|
||||
Path.Log.error(
|
||||
translate(
|
||||
@@ -386,7 +573,7 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
faces.extend(model.Shape.Faces)
|
||||
|
||||
if faces:
|
||||
self.buildPathMedial(obj, faces)
|
||||
self.buildCommandList(obj, faces)
|
||||
else:
|
||||
Path.Log.error(
|
||||
translate(
|
||||
@@ -399,6 +586,9 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
Path.Log.error(
|
||||
"Error processing Base object. Engraving operation will produce no output."
|
||||
)
|
||||
import traceback
|
||||
|
||||
Path.Log.error(f"Engraving operation exception: {traceback.format_exc()}")
|
||||
|
||||
def opUpdateDepths(self, obj, ignoreErrors=False):
|
||||
"""updateDepths(obj) ... engraving is always done at the top most z-value"""
|
||||
@@ -423,6 +613,38 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
and hasattr(tool, "TipDiameter")
|
||||
)
|
||||
|
||||
def debugVoronoi(self, obj):
|
||||
"""Debug function to display calculated voronoi edges"""
|
||||
|
||||
if not getattr(self, "voronoiDebugCache", None):
|
||||
Path.Log.error(
|
||||
"debugVoronoi: empty debug cache. Recompute VCarve operation first"
|
||||
)
|
||||
return
|
||||
|
||||
vPart = FreeCAD.activeDocument().addObject(
|
||||
"App::Part", f"{obj.Name}-VoronoiDebug"
|
||||
)
|
||||
|
||||
wiresToShow = []
|
||||
|
||||
for face, wires in self.voronoiDebugCache.items():
|
||||
for wire in wires:
|
||||
lastEdge = None
|
||||
currentPartWire = Part.Wire()
|
||||
currentPartWire.fixTolerance(0.01)
|
||||
for edge in wire:
|
||||
currentEdge = edge.toShape()
|
||||
|
||||
for v in currentEdge.Vertexes:
|
||||
v.fixTolerance(0.1)
|
||||
|
||||
currentPartWire.add(currentEdge)
|
||||
wiresToShow.append(currentPartWire)
|
||||
|
||||
for w in wiresToShow:
|
||||
vPart.addObject(Part.show(w))
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
return ["Discretize"]
|
||||
|
||||
@@ -111,3 +111,37 @@ class TestPathVcarve(PathTestBase):
|
||||
self.assertRoughly(geom.start, Scale45)
|
||||
self.assertRoughly(geom.stop, -3)
|
||||
self.assertRoughly(geom.scale, Scale45)
|
||||
|
||||
def test14(self):
|
||||
"""Verify if max dept is calculated properly when step-down is disabled"""
|
||||
|
||||
tool = VbitTool(10, 45, 2)
|
||||
geom = PathVcarve._Geometry.FromTool(tool, zStart=0, zFinal=-3, zStepDown=0)
|
||||
|
||||
self.assertEqual(geom.maximumDepth, -3)
|
||||
self.assertEqual(geom.maximumDepth, geom.stop)
|
||||
|
||||
def test15(self):
|
||||
"""Verify if step-down sections match final max depth"""
|
||||
|
||||
tool = VbitTool(10, 45, 2)
|
||||
geom = PathVcarve._Geometry.FromTool(tool, zStart=0, zFinal=-3, zStepDown=0.13)
|
||||
|
||||
while geom.incrementStepDownDepth(maximumUsableDepth=-3):
|
||||
pass
|
||||
|
||||
self.assertEqual(geom.maximumDepth, -3)
|
||||
|
||||
def test16(self):
|
||||
"""Verify 90 deg with tip dia depth calculation with step-down enabled"""
|
||||
tool = VbitTool(10, 90, 2)
|
||||
geom = PathVcarve._Geometry.FromTool(tool, zStart=0, zFinal=-10, zStepDown=0.13)
|
||||
|
||||
while geom.incrementStepDownDepth(maximumUsableDepth=-10):
|
||||
pass
|
||||
|
||||
# in order for the width to be correct the height needs to be shifted
|
||||
self.assertRoughly(geom.start, 1)
|
||||
self.assertRoughly(geom.stop, -4)
|
||||
self.assertRoughly(geom.scale, 1)
|
||||
self.assertRoughly(geom.maximumDepth, -4)
|
||||
|
||||
Reference in New Issue
Block a user