[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:
phaseloop
2024-05-28 14:57:07 +02:00
committed by GitHub
parent 797f8f9cf3
commit 888ffcf6d5
6 changed files with 486 additions and 115 deletions

View File

@@ -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 &quot;fuzzy skin&quot; 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>

View File

@@ -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):

View File

@@ -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)

View File

@@ -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

View File

@@ -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"]

View File

@@ -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)