Improve VCarve edge routing speed
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
|
||||
import FreeCAD
|
||||
import Part
|
||||
import Path
|
||||
import Path.Main.Job as PathJob
|
||||
import Path.Op.Vcarve as PathVcarve
|
||||
import math
|
||||
@@ -182,3 +183,96 @@ class TestPathVcarve(PathTestWithAssets):
|
||||
self.assertRoughly(geom.stop, -4)
|
||||
self.assertRoughly(geom.scale, 1)
|
||||
self.assertRoughly(geom.maximumDepth, -4)
|
||||
|
||||
def test17(self):
|
||||
"""Verify if canSkipRepositioning allows to skip if new point is < 0.5 mm"""
|
||||
|
||||
positionHistory = [
|
||||
FreeCAD.Base.Vector(0, 0, 0), # previous position
|
||||
FreeCAD.Base.Vector(0, 1, 5), # current position
|
||||
]
|
||||
|
||||
newPosition = FreeCAD.Base.Vector(0, 1.4, 3)
|
||||
assert PathVcarve.canSkipRepositioning(positionHistory, newPosition, 0.01)
|
||||
|
||||
newPosition = FreeCAD.Base.Vector(0, 1.7, 3)
|
||||
assert not PathVcarve.canSkipRepositioning(positionHistory, newPosition, 0.01)
|
||||
|
||||
|
||||
def test18(self):
|
||||
"""Verify if canSkipRepositioning allows to skip if new edge ends in current position"""
|
||||
|
||||
defaultTolerance = Path.Preferences.defaultGeometryTolerance()
|
||||
|
||||
positionHistory = [
|
||||
FreeCAD.Base.Vector(0, 0, 0), # previous position
|
||||
FreeCAD.Base.Vector(0, 1, 5), # current position
|
||||
]
|
||||
|
||||
# new position is same as previous position so we can G1 from (0,1,5) to (0,0,0) because
|
||||
# we already travelled this path before and it's carved - no need to raise toolbit
|
||||
newPosition = FreeCAD.Base.Vector(0, 0, 0)
|
||||
assert PathVcarve.canSkipRepositioning(positionHistory, newPosition, defaultTolerance)
|
||||
|
||||
|
||||
# same but should fail because we are out of tolerance
|
||||
newPosition = FreeCAD.Base.Vector(0, 0.1, 0)
|
||||
assert not PathVcarve.canSkipRepositioning(positionHistory, newPosition, defaultTolerance)
|
||||
|
||||
# same but is OK because we are within tolerance
|
||||
newPosition = FreeCAD.Base.Vector(0, 0.1, 0)
|
||||
assert PathVcarve.canSkipRepositioning(positionHistory, newPosition, 0.1)
|
||||
|
||||
def test19(self):
|
||||
"""Verify virtualBackTrackEdges() various scenarios
|
||||
"""
|
||||
|
||||
defaultTolerance = Path.Preferences.defaultGeometryTolerance()
|
||||
|
||||
# test scenario 1 - refer to function comments for explanation
|
||||
|
||||
positionHistory = [
|
||||
FreeCAD.Base.Vector(0, 0, 0), # previous position
|
||||
FreeCAD.Base.Vector(0, 1, 5), # current position
|
||||
]
|
||||
|
||||
# new edge ends at current position
|
||||
newEdge = Part.Edge(Part.LineSegment(FreeCAD.Base.Vector(1,2,3), FreeCAD.Base.Vector(0,1,5)))
|
||||
|
||||
virtualEdges = PathVcarve.generateVirtualBackTrackEdges(positionHistory, newEdge, defaultTolerance)
|
||||
|
||||
assert len(virtualEdges) == 1
|
||||
|
||||
virtualEdge = virtualEdges[0]
|
||||
# virtualEdge is essentially a reversed newEdge
|
||||
assert virtualEdge.valueAt(virtualEdge.FirstParameter) == newEdge.valueAt(newEdge.LastParameter)
|
||||
assert virtualEdge.valueAt(virtualEdge.LastParameter) == newEdge.valueAt(newEdge.FirstParameter)
|
||||
|
||||
|
||||
# test scenario 2 - refer to function comments for explanation
|
||||
|
||||
positionHistory = [
|
||||
FreeCAD.Base.Vector(0, 0, 0), # previous position
|
||||
FreeCAD.Base.Vector(0, 1, 5), # current position
|
||||
]
|
||||
|
||||
# new edge ends at previous position
|
||||
newEdge = Part.Edge(Part.LineSegment(FreeCAD.Base.Vector(1,2,3), FreeCAD.Base.Vector(0,0,0)))
|
||||
|
||||
virtualEdges = PathVcarve.generateVirtualBackTrackEdges(positionHistory, newEdge, defaultTolerance)
|
||||
|
||||
assert len(virtualEdges) == 2
|
||||
|
||||
virtualEdge1 = virtualEdges[0]
|
||||
virtualEdge2 = virtualEdges[1]
|
||||
|
||||
# 2 virtual edges (current position, previous position) and (previous position, new edge start)
|
||||
|
||||
assert virtualEdge1.valueAt(virtualEdge1.FirstParameter) == positionHistory[-1]
|
||||
assert virtualEdge1.valueAt(virtualEdge1.LastParameter) == positionHistory[-2]
|
||||
|
||||
|
||||
assert virtualEdge2.valueAt(virtualEdge2.FirstParameter) == positionHistory[-2]
|
||||
assert virtualEdge2.valueAt(virtualEdge2.LastParameter) == newEdge.valueAt(newEdge.FirstParameter)
|
||||
|
||||
|
||||
@@ -145,6 +145,113 @@ def _sortVoronoiWires(wires, start=FreeCAD.Vector(0, 0, 0)):
|
||||
|
||||
return result
|
||||
|
||||
def getReversedEdge(edge):
|
||||
# returns a reversed edge (copy of original edge)
|
||||
curve = edge.Curve
|
||||
first = edge.FirstParameter
|
||||
last = edge.LastParameter
|
||||
curve_c = curve.copy()
|
||||
curve_c.reverse()
|
||||
return Part.Edge(
|
||||
curve_c, curve_c.reversedParameter(last), curve_c.reversedParameter(first)
|
||||
)
|
||||
|
||||
|
||||
def generateVirtualBackTrackEdges(positionHistory, nextEdge, tolerance) -> list:
|
||||
"""
|
||||
Generate a list of "virtual edges" to backtrack using normal G1 moves instead lifting
|
||||
toolbit and repositioning using G0 to get to beginning of nextEdge.
|
||||
Those virtual edges are either already carved or are part of nextEdge anyway so it's safe
|
||||
to follow them without lifting toolbit. This approach makes carving a lot of faster.
|
||||
"""
|
||||
|
||||
if not positionHistory:
|
||||
return []
|
||||
|
||||
backTrackEdges = []
|
||||
|
||||
currentPosition = positionHistory[-1]
|
||||
previousPosition = positionHistory[-2]
|
||||
|
||||
nextEdgeStart = nextEdge.valueAt(nextEdge.FirstParameter)
|
||||
nextEdgeEnd = nextEdge.valueAt(nextEdge.LastParameter)
|
||||
|
||||
# Scenario 1
|
||||
#
|
||||
# in some cases travelling between wires looks like that:
|
||||
# A ========= B ------- D
|
||||
# |
|
||||
# C
|
||||
#
|
||||
# we follow first wire from A to B - new wire starts at C and goes through B -> D
|
||||
# Repositioning to position C using G0 command does not make sense and it's slow
|
||||
# We can insert "virtual" edge B->C at the beginning of a second wire to make
|
||||
# continous CNC head movement
|
||||
#
|
||||
|
||||
if nextEdgeEnd.isEqual(currentPosition, tolerance):
|
||||
# virtual edge is "reversed"
|
||||
virtualEdge = Part.Edge(Part.LineSegment(nextEdgeEnd, nextEdgeStart))
|
||||
backTrackEdges.append(virtualEdge)
|
||||
|
||||
# Scenario 2
|
||||
# next edge has common node with previous position but it's reversed
|
||||
# A C
|
||||
# \ //
|
||||
# \ //
|
||||
# B
|
||||
# We went from B to C and next wire edge starts at A and goes back to B
|
||||
# Normally we would G0 jump from C to A and start from there,
|
||||
# but we can go back from C to B and then to A (by adding extra edge which
|
||||
# is reversed A->B edge).
|
||||
|
||||
elif nextEdgeEnd.isEqual(previousPosition, tolerance):
|
||||
# travel back to the previous toolbit position
|
||||
virtualEdge = Part.Edge(Part.LineSegment(currentPosition, previousPosition))
|
||||
backTrackEdges.append(virtualEdge)
|
||||
# instead of G0 - just carve the edge in reverse direction
|
||||
backTrackEdges.append(getReversedEdge(nextEdge))
|
||||
|
||||
|
||||
return backTrackEdges
|
||||
|
||||
|
||||
|
||||
def canSkipRepositioning(positionHistory, newPosition, tolerance):
|
||||
"""
|
||||
Calculate if it makes sense to raise head to safe height and reposition before
|
||||
starting to cut another edge
|
||||
"""
|
||||
|
||||
if not positionHistory:
|
||||
return False
|
||||
|
||||
currentPosition = positionHistory[-1]
|
||||
previousPosition = positionHistory[-2]
|
||||
|
||||
# get vertex position on X/Y plane only
|
||||
v0 = FreeCAD.Base.Vector(currentPosition.x, currentPosition.y)
|
||||
v1 = FreeCAD.Base.Vector(newPosition.x, newPosition.y)
|
||||
|
||||
# do not bother with G0 if new and current position differ by less than 0.5 mm in X/Y
|
||||
if v0.distanceToPoint(v1) <= 0.5:
|
||||
return True
|
||||
|
||||
# if new position is same as previous head position we can essentially
|
||||
# go back traversing same edge. This is handy with short "detour" edges like that:
|
||||
#
|
||||
# A--------------B===============C
|
||||
# |
|
||||
# D
|
||||
# We are travelling wire from A -> B -> D within first wire and ending at D. New wire starts with edge going from
|
||||
# B to C. We don't need to G0 to point B, we can skip positioning because if we travel G1 move from D to B we will follow already
|
||||
# carved path
|
||||
|
||||
if newPosition.isEqual(previousPosition, tolerance):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class _Geometry(object):
|
||||
"""POD class so the limits only have to be calculated once."""
|
||||
@@ -352,6 +459,8 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
obj.Colinear = 10.0
|
||||
obj.Discretize = 0.25
|
||||
obj.Tolerance = Path.Preferences.defaultGeometryTolerance()
|
||||
# keep copy in local object to use in methods which do not operate directly on obj
|
||||
self.Tolerance = obj.Tolerance
|
||||
self.setupAdditionalProperties(obj)
|
||||
|
||||
def opOnDocumentRestored(self, obj):
|
||||
@@ -363,8 +472,11 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
constructs a medial axis path using openvoronoi
|
||||
:returns: dictionary - each face object is a key containing list of wires"""
|
||||
|
||||
wires_by_face = dict()
|
||||
self.voronoiDebugCache = dict()
|
||||
medial_wires_by_face = dict()
|
||||
edges_by_face = dict() # non processed voronoi edges, for debugging
|
||||
|
||||
self.voronoiDebugMedialCache = dict()
|
||||
self.voronoiDebugEdgeCache = dict()
|
||||
|
||||
def is_exterior(vertex, face):
|
||||
vector = FreeCAD.Vector(vertex.toPoint(face.BoundBox.ZMin))
|
||||
@@ -403,6 +515,7 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
insert_many_wires(vd, f.Wires)
|
||||
|
||||
vd.construct()
|
||||
edges_by_face[f] = vd.Edges
|
||||
|
||||
for e in vd.Edges:
|
||||
if e.isPrimary():
|
||||
@@ -429,10 +542,12 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
wires = _sortVoronoiWires(wires)
|
||||
voronoiWires.extend(wires)
|
||||
|
||||
wires_by_face[f] = voronoiWires
|
||||
self.voronoiDebugCache = wires_by_face
|
||||
medial_wires_by_face[f] = voronoiWires
|
||||
|
||||
return wires_by_face
|
||||
self.voronoiDebugMedialCache = medial_wires_by_face
|
||||
self.voronoiDebugEdgeCache = edges_by_face
|
||||
|
||||
return medial_wires_by_face
|
||||
|
||||
def buildCommandList(self, obj, faces):
|
||||
"""
|
||||
@@ -440,52 +555,55 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
wire list from buildMedialWires
|
||||
"""
|
||||
|
||||
def getCurrentPosition(wire):
|
||||
def getPositionHistory(wire):
|
||||
"""
|
||||
Calculate CNC head position assuming it reached the end of the wire
|
||||
Get CNC current and previous head position assuming it reached the end of the wire
|
||||
returns: previousPosition, currentPostion tuple
|
||||
"""
|
||||
|
||||
if not wire:
|
||||
return None
|
||||
|
||||
lastEdge = wire[-1]
|
||||
return lastEdge.valueAt(lastEdge.LastParameter)
|
||||
return (
|
||||
lastEdge.valueAt(lastEdge.FirstParameter),
|
||||
lastEdge.valueAt(lastEdge.LastParameter),
|
||||
)
|
||||
|
||||
def cutWires(wires, pathlist, optimizeMovements=False):
|
||||
currentPosition = None
|
||||
|
||||
positionHistory = None
|
||||
|
||||
for w in wires:
|
||||
pWire = _getPartEdges(obj, w, geom)
|
||||
if pWire:
|
||||
pathlist.extend(_cutWire(pWire, currentPosition))
|
||||
pathlist.extend(_cutWire(pWire, positionHistory))
|
||||
|
||||
# movement optimization only works if we provide current head position
|
||||
# movement optimization only works if we provide head position history
|
||||
if optimizeMovements:
|
||||
currentPosition = getCurrentPosition(pWire)
|
||||
positionHistory = getPositionHistory(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):
|
||||
def _cutWire(wire, positionHistory=None):
|
||||
path = []
|
||||
|
||||
e = wire[0]
|
||||
backtrack_edges = []
|
||||
|
||||
# we start vcarving another wire which may not be connected to previous one
|
||||
# but using some routing logic we may avoid raising CNC toolbit and using G0
|
||||
# and instead traverse back already carved edges at full speed
|
||||
|
||||
|
||||
|
||||
edge_list = backtrack_edges + wire
|
||||
|
||||
e = edge_list[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):
|
||||
hSpeed = obj.ToolController.HorizFeed.Value
|
||||
vSpeed = obj.ToolController.VertFeed.Value
|
||||
|
||||
# check if we can smart-skip using G0 repositioning which is slow
|
||||
if not canSkipRepositioning(positionHistory, newPosition, obj.Tolerance):
|
||||
path.append(Path.Command("G0", {"Z": obj.SafeHeight.Value}))
|
||||
path.append(
|
||||
Path.Command(
|
||||
@@ -493,14 +611,25 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
)
|
||||
)
|
||||
|
||||
hSpeed = obj.ToolController.HorizFeed.Value
|
||||
vSpeed = obj.ToolController.VertFeed.Value
|
||||
path.append(
|
||||
Path.Command(
|
||||
"G1", {"X": newPosition.x, "Y": newPosition.y, "Z": newPosition.z, "F": vSpeed}
|
||||
path.append(
|
||||
Path.Command(
|
||||
"G1",
|
||||
{"X": newPosition.x, "Y": newPosition.y, "Z": newPosition.z, "F": vSpeed},
|
||||
)
|
||||
)
|
||||
)
|
||||
for e in wire:
|
||||
else: # skip repositioning
|
||||
# technically hSpeed + vSpeed should be properly recalculated into F parameter
|
||||
# as cmdsForEdge does but we either cut max 0.5 mm through stock or backtrack
|
||||
# over already carved edges, so hSpeed will be just fine
|
||||
path.append(
|
||||
Path.Command(
|
||||
"G1 X{} Y{} Z{} F{}".format(
|
||||
newPosition.x, newPosition.y, newPosition.z, hSpeed
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for e in edge_list:
|
||||
path.extend(Path.Geom.cmdsForEdge(e, hSpeed=hSpeed, vSpeed=vSpeed))
|
||||
|
||||
return path
|
||||
@@ -545,7 +674,8 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
"""opExecute(obj) ... process engraving operation"""
|
||||
Path.Log.track()
|
||||
|
||||
self.voronoiDebugCache = None
|
||||
self.voronoiDebugMedialCache = None
|
||||
self.voronoiDebugEdgesCache = None
|
||||
|
||||
if obj.ToolController is None:
|
||||
return
|
||||
@@ -625,18 +755,18 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
and hasattr(tool, "TipDiameter")
|
||||
)
|
||||
|
||||
def debugVoronoi(self, obj):
|
||||
"""Debug function to display calculated voronoi edges"""
|
||||
def debugVoronoiMedial(self, obj):
|
||||
"""Debug function to display calculated voronoi medial wires"""
|
||||
|
||||
if not getattr(self, "voronoiDebugCache", None):
|
||||
if not getattr(self, "voronoiDebugMedialCache", None):
|
||||
Path.Log.error("debugVoronoi: empty debug cache. Recompute VCarve operation first")
|
||||
return
|
||||
|
||||
vPart = FreeCAD.activeDocument().addObject("App::Part", f"{obj.Name}-VoronoiDebug")
|
||||
vPart = FreeCAD.activeDocument().addObject("App::Part", f"{obj.Name}-VoronoiDebugMedial")
|
||||
|
||||
wiresToShow = []
|
||||
|
||||
for face, wires in self.voronoiDebugCache.items():
|
||||
for face, wires in self.voronoiDebugMedialCache.items():
|
||||
for wire in wires:
|
||||
currentPartWire = Part.Wire()
|
||||
currentPartWire.fixTolerance(0.01)
|
||||
@@ -652,6 +782,26 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
for w in wiresToShow:
|
||||
vPart.addObject(Part.show(w))
|
||||
|
||||
def debugVoronoiEdges(self, obj):
|
||||
"""Debug function to display calculated voronoi edges"""
|
||||
|
||||
if not getattr(self, "voronoiDebugEdgeCache", None):
|
||||
Path.Log.error("debugVoronoi: empty debug cache. Recompute VCarve operation first")
|
||||
return
|
||||
|
||||
vPart = FreeCAD.activeDocument().addObject("App::Part", f"{obj.Name}-VoronoiDebugEdge")
|
||||
|
||||
edgesToShow = []
|
||||
|
||||
for face, edges in self.voronoiDebugEdgeCache.items():
|
||||
for edge in edges: # those are voronoi Edge objects, not FC Edge
|
||||
currentEdge = edge.toShape()
|
||||
|
||||
edgesToShow.append(currentEdge)
|
||||
|
||||
for e in edgesToShow:
|
||||
vPart.addObject(Part.show(e))
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
return ["Discretize"]
|
||||
|
||||
Reference in New Issue
Block a user