[CAM] Fix ramp dressup performance (#21944)

* [CAM] fix biggest performance problems in ramp dressup

key items:

- finding the index of the current edge with edeges.index(edge) was very
  expensive; fixed by tracking the index while looping

- checking which edges were rapids with edge equality was also
  expensive; fixed by keeping a list of indexes of rapid input edges,
and tagging output edges with whether or not they are rapids

* [CAM] comment out Path.Log.debug in hot segments of ramping code

Even when low level logs are supposed to be suppressed, Path.Log.debug
takes take invoking traceback.extract_stack. This time adds up if logs
are invoked in frequently run loops.

* Fix CAM test

* [CAM] reimplment ramp method 1 with faster code

* [CAM] reimplement ramp methods 2, 3, and helix

* [CAM] patch to make output match original

* [CAM] ramping full performance + functionality fix
This commit is contained in:
David Kaufman
2025-09-02 11:32:03 -04:00
committed by GitHub
parent 540ce39cb1
commit f156e467c9
5 changed files with 397 additions and 498 deletions

View File

@@ -557,7 +557,7 @@ class TestPathGeom(PathTestBase):
commands.append(Path.Command("G0", {"X": 0}))
commands.append(Path.Command("G1", {"Y": 0}))
wire, rapid = Path.Geom.wireForPath(Path.Path(commands))
wire, rapid, rapid_indexes = Path.Geom.wireForPath(Path.Path(commands))
self.assertEqual(len(wire.Edges), 4)
self.assertLine(wire.Edges[0], Vector(0, 0, 0), Vector(1, 0, 0))
self.assertLine(wire.Edges[1], Vector(1, 0, 0), Vector(1, 1, 0))

View File

@@ -28,7 +28,6 @@ import Path
import Path.Base.Language as PathLanguage
import Path.Dressup.Utils as PathDressup
import PathScripts.PathUtils as PathUtils
from Path.Geom import wireForPath
import math
__doc__ = """LeadInOut Dressup USE ROLL-ON ROLL-OFF to profile"""
@@ -130,9 +129,6 @@ class ObjectDressup:
)
obj.Proxy = self
self.wire = None
self.rapids = None
def dumps(self):
return None
@@ -172,7 +168,6 @@ class ObjectDressup:
)
obj.LengthOut = 0.1
self.wire, self.rapids = wireForPath(PathUtils.getPathWithPlacement(obj.Base))
obj.Path = self.generateLeadInOutCurve(obj)
def onDocumentRestored(self, obj):

View File

@@ -22,6 +22,7 @@
from PathScripts import PathUtils
from PySide.QtCore import QT_TRANSLATE_NOOP
import copy
import FreeCAD
import Path
import Path.Dressup.Utils as PathDressup
@@ -47,6 +48,117 @@ else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
class AnnotatedGCode:
def __init__(self, command, start_point):
self.start_point = start_point
self.command = command
self.end_point = (
command.Parameters.get("X", start_point[0]),
command.Parameters.get("Y", start_point[1]),
command.Parameters.get("Z", start_point[2]),
)
self.is_line = command.Name in Path.Geom.CmdMoveStraight
self.is_arc = command.Name in Path.Geom.CmdMoveArc
self.xy_length = None
if self.is_line:
self.xy_length = (
(start_point[0] - self.end_point[0]) ** 2
+ (start_point[1] - self.end_point[1]) ** 2
) ** 0.5
elif self.is_arc:
self.center_xy = (
start_point[0] + command.Parameters.get("I", 0),
start_point[1] + command.Parameters.get("J", 0),
)
self.start_angle = math.atan2(
start_point[1] - self.center_xy[1],
start_point[0] - self.center_xy[0],
)
self.end_angle = math.atan2(
self.end_point[1] - self.center_xy[1],
self.end_point[0] - self.center_xy[0],
)
if self.command.Name in Path.Geom.CmdMoveCCW and self.end_angle < self.start_angle:
self.end_angle += 2 * math.pi
if self.command.Name in Path.Geom.CmdMoveCW and self.end_angle > self.start_angle:
self.end_angle -= 2 * math.pi
self.radius = (
(start_point[0] - self.center_xy[0]) ** 2
+ (start_point[1] - self.center_xy[1]) ** 2
) ** 0.5
self.xy_length = self.radius * abs(self.end_angle - self.start_angle)
"""Makes a copy of this annotated gcode at the given z height"""
def clone(self, z_start=None, z_end=None, reverse=False):
z_start = z_start if z_start is not None else self.start_point[2]
z_end = z_end if z_end is not None else self.end_point[2]
other = copy.copy(self)
otherParams = copy.copy(self.command.Parameters)
otherCommandName = self.command.Name
other.start_point = (self.start_point[0], self.start_point[1], z_start)
other.end_point = (self.end_point[0], self.end_point[1], z_end)
otherParams.update({"Z": z_end})
if reverse:
other.start_point, other.end_point = other.end_point, other.start_point
otherParams.update(
{"X": other.end_point[0], "Y": other.end_point[1], "Z": other.end_point[2]}
)
if other.is_arc:
other.start_angle, other.end_angle = other.end_angle, other.start_angle
otherCommandName = (
Path.Geom.CmdMoveCW[0]
if self.command.Name in Path.Geom.CmdMoveCCW
else Path.Geom.CmdMoveCCW[0]
)
otherParams.update(
{
"I": other.center_xy[0] - other.start_point[0],
"J": other.center_xy[1] - other.start_point[1],
}
)
other.command = Path.Command(otherCommandName, otherParams)
return other
"""Splits the edge into two parts, the first split_length (if less than xy_length) long. Only supported for lines and arcs (no rapids)"""
def split(self, split_length):
split_length = min(split_length, self.xy_length)
p = split_length / self.xy_length
firstParams = copy.copy(self.command.Parameters)
secondParams = copy.copy(self.command.Parameters)
split_point = None
if self.is_line:
split_point = (
self.start_point[0] * (1 - p) + self.end_point[0] * p,
self.start_point[1] * (1 - p) + self.end_point[1] * p,
self.start_point[2] * (1 - p) + self.end_point[2] * p,
)
elif self.is_arc:
angle = self.start_angle * (1 - p) + self.end_angle * p
split_point = (
self.center_xy[0] + self.radius * math.cos(angle),
self.center_xy[1] + self.radius * math.sin(angle),
self.start_point[2] * (1 - p) + self.end_point[2] * p,
)
secondParams.update(
{
"I": self.center_xy[0] - split_point[0],
"J": self.center_xy[1] - split_point[1],
}
)
else:
raise Exception("Invalid type, can only split (non-rapid) lines and arcs")
firstParams.update({"X": split_point[0], "Y": split_point[1], "Z": split_point[2]})
first_command = Path.Command(self.command.Name, firstParams)
second_command = Path.Command(self.command.Name, secondParams)
return AnnotatedGCode(first_command, self.start_point), AnnotatedGCode(
second_command, split_point
)
class ObjectDressup:
def __init__(self, obj):
self.obj = obj
@@ -222,45 +334,69 @@ class ObjectDressup:
self.angle = obj.Angle
self.method = obj.Method
self.wire, self.rapids = Path.Geom.wireForPath(PathUtils.getPathWithPlacement(obj.Base))
positioned_path = PathUtils.getPathWithPlacement(obj.Base)
cmds = positioned_path.Commands if hasattr(positioned_path, "Commands") else []
self.edges = []
start_point = (0, 0, 0)
last_params = {}
for cmd in cmds:
# Skip repeat move commands
params = cmd.Parameters
if (
cmd.Name in Path.Geom.CmdMoveAll
and len(self.edges) > 0
and cmd.Name == self.edges[-1].command.Name
):
found_diff = False
for k, v in params.items():
if last_params.get(k, None) != v:
found_diff = True
break
if not found_diff:
continue
last_params.update(params)
if cmd.Name in Path.Geom.CmdMoveAll and (
not "X" in params or not "Y" in params or not "Z" in params
):
params["X"] = params.get("X", start_point[0])
params["Y"] = params.get("Y", start_point[1])
params["Z"] = params.get("Z", start_point[2])
cmd = Path.Command(cmd.Name, params)
annotated = AnnotatedGCode(cmd, start_point)
self.edges.append(annotated)
start_point = annotated.end_point
if self.method in ["RampMethod1", "RampMethod2", "RampMethod3"]:
self.outedges = self.generateRamps()
else:
self.outedges = self.generateHelix()
obj.Path = self.createCommands(obj, self.outedges)
def generateRamps(self, allowBounce=True):
edges = self.wire.Edges
def generateRamps(self):
edges = self.edges
outedges = []
for edge in edges:
israpid = False
for redge in self.rapids:
if Path.Geom.edgesMatch(edge, redge):
israpid = True
if not israpid:
bb = edge.BoundBox
p0 = edge.Vertexes[0].Point
p1 = edge.Vertexes[1].Point
for edgei, edge in enumerate(edges):
if edge.is_line or edge.is_arc:
rampangle = self.angle
if bb.XLength < 1e-6 and bb.YLength < 1e-6 and bb.ZLength > 0 and p0.z > p1.z:
# check for plunge
if edge.xy_length < 1e-6 and edge.end_point[2] < edge.start_point[2]:
# check if above ignoreAbove parameter - do not generate ramp if it is
newEdge, cont = self.checkIgnoreAbove(edge)
if newEdge is not None:
outedges.append(newEdge)
p0.z = self.ignoreAbove
if cont:
noramp_edge, edge = self.processIgnoreAbove(edge)
if noramp_edge is not None:
outedges.append(noramp_edge)
if edge is None:
continue
plungelen = abs(p0.z - p1.z)
plungelen = abs(edge.start_point[2] - edge.end_point[2])
projectionlen = plungelen * math.tan(
math.radians(rampangle)
) # length of the forthcoming ramp projected to XY plane
Path.Log.debug(
"Found plunge move at X:{} Y:{} From Z:{} to Z{}, length of ramp: {}".format(
p0.x, p0.y, p0.z, p1.z, projectionlen
)
)
# Path.Log.debug(
# "Found plunge move at X:{} Y:{} From Z:{} to Z{}, length of ramp: {}".format(
# p0.x, p0.y, p0.z, p1.z, projectionlen
# )
# )
if self.method == "RampMethod3":
projectionlen = projectionlen / 2
@@ -269,63 +405,54 @@ class ObjectDressup:
covered = False
coveredlen = 0
rampedges = []
i = edges.index(edge) + 1
while not covered:
i = edgei + 1
while not covered and i < len(edges):
candidate = edges[i]
cp0 = candidate.Vertexes[0].Point
cp1 = candidate.Vertexes[1].Point
if abs(cp0.z - cp1.z) > 1e-6:
# this edge is not parallel to XY plane, not qualified for ramping.
if abs(candidate.start_point[2] - candidate.end_point[2]) > 1e-6 or (
not candidate.is_line and not candidate.is_arc
):
# this edge is not an edge/arc in the XY plane; not qualified for ramping
break
# Path.Log.debug("Next edge length {}".format(candidate.Length))
rampedges.append(candidate)
coveredlen = coveredlen + candidate.Length
coveredlen = coveredlen + candidate.xy_length
if coveredlen > projectionlen:
covered = True
i = i + 1
if i >= len(edges):
break
if len(rampedges) == 0:
Path.Log.debug("No suitable edges for ramping, plunge will remain as such")
Path.Log.warn("No suitable edges for ramping, plunge will remain as such")
outedges.append(edge)
else:
if not covered:
if (not allowBounce) or self.method == "RampMethod2":
l = 0
for redge in rampedges:
l = l + redge.Length
if self.method == "RampMethod3":
rampangle = math.degrees(math.atan(l / (plungelen / 2)))
else:
rampangle = math.degrees(math.atan(l / plungelen))
Path.Log.warning(
"Cannot cover with desired angle, tightening angle to: {}".format(
rampangle
)
)
# Path.Log.debug("Doing ramp to edges: {}".format(rampedges))
if self.method == "RampMethod1":
outedges.extend(
self.createRampMethod1(rampedges, p0, projectionlen, rampangle)
self.createRampMethod1(
rampedges, edge.start_point, projectionlen, rampangle
)
)
elif self.method == "RampMethod2":
outedges.extend(
self.createRampMethod2(rampedges, p0, projectionlen, rampangle)
self.createRampMethod2(
rampedges, edge.start_point, projectionlen, rampangle
)
)
else:
# if the ramp cannot be covered with Method3, revert to Method1
# because Method1 support going back-and-forth and thus results in same path as Method3 when
# length of the ramp is smaller than needed for single ramp.
if (not covered) and allowBounce:
if not covered:
projectionlen = projectionlen * 2
outedges.extend(
self.createRampMethod1(rampedges, p0, projectionlen, rampangle)
self.createRampMethod1(
rampedges, edge.start_point, projectionlen, rampangle
)
)
else:
outedges.extend(
self.createRampMethod3(rampedges, p0, projectionlen, rampangle)
self.createRampMethod3(
rampedges, edge.start_point, projectionlen, rampangle
)
)
else:
outedges.append(edge)
@@ -334,66 +461,49 @@ class ObjectDressup:
return outedges
def generateHelix(self):
edges = self.wire.Edges
edges = self.edges
minZ = self.findMinZ(edges)
Path.Log.debug("Minimum Z in this path is {}".format(minZ))
outedges = []
i = 0
while i < len(edges):
edge = edges[i]
israpid = False
for redge in self.rapids:
if Path.Geom.edgesMatch(edge, redge):
israpid = True
if not israpid:
bb = edge.BoundBox
p0 = edge.Vertexes[0].Point
p1 = edge.Vertexes[1].Point
if bb.XLength < 1e-6 and bb.YLength < 1e-6 and bb.ZLength > 0 and p0.z > p1.z:
# plungelen = abs(p0.z-p1.z)
Path.Log.debug(
"Found plunge move at X:{} Y:{} From Z:{} to Z{}, Searching for closed loop".format(
p0.x, p0.y, p0.z, p1.z
)
)
# check if above ignoreAbove parameter - do not generate helix if it is
newEdge, cont = self.checkIgnoreAbove(edge)
if newEdge is not None:
outedges.append(newEdge)
p0.z = self.ignoreAbove
if cont:
if edge.is_line or edge.is_arc:
if edge.xy_length < 1e-6 and edge.end_point[2] < edge.start_point[2]:
noramp_edge, edge = self.processIgnoreAbove(edge)
if noramp_edge is not None:
outedges.append(noramp_edge)
if edge is None:
i = i + 1
continue
# next need to determine how many edges in the path after plunge are needed to cover the length:
# next need to find a loop
loopFound = False
rampedges = []
j = i + 1
while not loopFound:
while j < len(edges) and not loopFound:
candidate = edges[j]
cp0 = candidate.Vertexes[0].Point
cp1 = candidate.Vertexes[1].Point
if Path.Geom.pointsCoincide(p1, cp1):
# found closed loop
loopFound = True
rampedges.append(candidate)
break
if abs(cp0.z - cp1.z) > 1e-6:
if abs(candidate.start_point[2] - candidate.end_point[2]) > 1e-6:
# this edge is not parallel to XY plane, not qualified for ramping.
# exit early, no loop found
break
# Path.Log.debug("Next edge length {}".format(candidate.Length))
if (
Path.Geom.isRoughly(edge.end_point[0], candidate.end_point[0])
and Path.Geom.isRoughly(edge.end_point[1], candidate.end_point[1])
and Path.Geom.isRoughly(edge.end_point[2], candidate.end_point[2])
):
loopFound = True
rampedges.append(candidate)
j = j + 1
if j >= len(edges):
break
if len(rampedges) == 0 or not loopFound:
Path.Log.debug("No suitable helix found")
if not loopFound:
Path.Log.warn("No suitable helix found, leaving as a plunge")
outedges.append(edge)
else:
outedges.extend(self.createHelix(rampedges, p0, p1))
if not Path.Geom.isRoughly(p1.z, minZ):
outedges.extend(self.createHelix(rampedges, edge.start_point[2]))
if not Path.Geom.isRoughly(edge.end_point[2], minZ):
# the edges covered by the helix not handled again,
# unless reached the bottom height
i = j
i = j - 1
else:
outedges.append(edge)
@@ -402,176 +512,122 @@ class ObjectDressup:
i = i + 1
return outedges
def checkIgnoreAbove(self, edge):
if self.ignoreAboveEnabled:
p0 = edge.Vertexes[0].Point
p1 = edge.Vertexes[1].Point
if p0.z > self.ignoreAbove and (
p1.z > self.ignoreAbove or Path.Geom.isRoughly(p1.z, self.ignoreAbove.Value)
):
Path.Log.debug("Whole plunge move above 'ignoreAbove', ignoring")
return (edge, True)
elif p0.z > self.ignoreAbove and not Path.Geom.isRoughly(p0.z, self.ignoreAbove.Value):
Path.Log.debug("Plunge move partially above 'ignoreAbove', splitting into two")
newPoint = FreeCAD.Base.Vector(p0.x, p0.y, self.ignoreAbove)
return (Part.makeLine(p0, newPoint), False)
else:
return None, False
else:
return None, False
"""
Edges, or parts of edges, above self.ignoreAbove should not be ramped.
This method is a helper for splitting edges into a portion that should be
ramped and a portion that should not be ramped.
def createHelix(self, rampedges, startPoint, endPoint):
Returns (noramp_edge, ramp_edge). Either of these variables may be None
"""
def processIgnoreAbove(self, edge):
if not self.ignoreAboveEnabled:
return None, edge
z0, z1 = edge.start_point[2], edge.end_point[2]
if z0 > self.ignoreAbove.Value:
if z1 > self.ignoreAbove.Value or Path.Geom.isRoughly(z1, self.ignoreAbove.Value):
# Entire plunge is above ignoreAbove
return edge, None
elif not Path.Geom.isRoughly(z0, self.ignoreAbove.Value):
# Split the edge into regions above and below
return (
edge.clone(z0, self.ignoreAbove.Value),
edge.clone(self.ignoreAbove.Value, z1),
)
# Entire plunge is below ignoreAbove
return None, edge
def createHelix(self, rampedges, startZ):
outedges = []
ramplen = 0
for redge in rampedges:
ramplen = ramplen + redge.Length
rampheight = abs(endPoint.z - startPoint.z)
ramplen = ramplen + redge.xy_length
rampheight = abs(startZ - rampedges[-1].end_point[2])
max_rise_over_run = 1 / math.tan(math.radians(self.angle))
num_loops = math.ceil(rampheight / ramplen / max_rise_over_run)
rampedges *= num_loops
ramplen *= num_loops
rampangle_rad = math.atan(ramplen / rampheight)
curPoint = startPoint
curZ = startZ
for i, redge in enumerate(rampedges):
if i < len(rampedges) - 1:
deltaZ = redge.Length / math.tan(rampangle_rad)
newPoint = FreeCAD.Base.Vector(
redge.valueAt(redge.LastParameter).x,
redge.valueAt(redge.LastParameter).y,
curPoint.z - deltaZ,
)
outedges.append(self.createRampEdge(redge, curPoint, newPoint))
curPoint = newPoint
else:
# on the last edge, force it to end to the endPoint
# this should happen automatically, but this avoids any rounding error
outedges.append(self.createRampEdge(redge, curPoint, endPoint))
return outedges
def createRampEdge(self, originalEdge, startPoint, endPoint):
# Path.Log.debug("Create edge from [{},{},{}] to [{},{},{}]".format(startPoint.x,startPoint.y, startPoint.z, endPoint.x, endPoint.y, endPoint.z))
if type(originalEdge.Curve) == Part.Line or type(originalEdge.Curve) == Part.LineSegment:
return Part.makeLine(startPoint, endPoint)
elif type(originalEdge.Curve) == Part.Circle:
firstParameter = originalEdge.Curve.parameter(startPoint)
lastParameter = originalEdge.Curve.parameter(endPoint)
arcMid = originalEdge.valueAt((firstParameter + lastParameter) / 2)
arcMid.z = (startPoint.z + endPoint.z) / 2
return Part.Arc(startPoint, arcMid, endPoint).toShape()
else:
Path.Log.error("Edge should not be helix")
def getreversed(self, edges):
"""
Reverses the edge array and the direction of each edge
"""
outedges = []
for edge in reversed(edges):
# reverse the start and end points
startPoint = edge.valueAt(edge.LastParameter)
endPoint = edge.valueAt(edge.FirstParameter)
if type(edge.Curve) == Part.Line or type(edge.Curve) == Part.LineSegment:
outedges.append(Part.makeLine(startPoint, endPoint))
elif type(edge.Curve) == Part.Circle:
arcMid = edge.valueAt((edge.FirstParameter + edge.LastParameter) / 2)
outedges.append(Part.Arc(startPoint, arcMid, endPoint).toShape())
else:
Path.Log.error("Edge should not be helix")
deltaZ = redge.xy_length / math.tan(rampangle_rad)
# compute new z, or clamp to end segment to avoid rounding error
newZ = curZ - deltaZ if i < len(rampedges) - 1 else rampedges[-1].end_point[2]
outedges.append(redge.clone(curZ, newZ))
curZ = newZ
return outedges
def findMinZ(self, edges):
minZ = 99999999999
for edge in edges[1:]:
for v in edge.Vertexes:
if v.Point.z < minZ:
minZ = v.Point.z
for edge in edges:
if edge.end_point[2] < minZ:
minZ = edge.end_point[2]
return minZ
def getSplitPoint(self, edge, remaining):
if type(edge.Curve) == Part.Line or type(edge.Curve) == Part.LineSegment:
return edge.valueAt(remaining)
elif type(edge.Curve) == Part.Circle:
param = remaining / edge.Curve.Radius
return edge.valueAt(param)
def createRampMethod1(self, rampedges, p0, projectionlen, rampangle):
"""
This method generates ramp with following pattern:
1. Start from the original startpoint of the plunge
2. Ramp down along the path that comes after the plunge
3. When reaching the Z level of the original plunge, return back to the beginning
by going the path backwards until the original plunge end point is reached
by going the path backwards until the original plunge end point is reached
4. Continue with the original path
This method causes many unnecessary moves with tool down.
"""
outedges = []
ramp, reset = self._createRampMethod1(rampedges, p0, projectionlen, rampangle)
return ramp + reset
def _createRampMethod1(self, rampedges, p0, projectionlen, rampangle):
"""
Helper method for generating ramps. Computes ramp method 1, but returns the result in pieces to allow for implementing the other ramp methods.
Returns (ramp, reset)
- ramp: array of commands ramping down
- reset: array of commands returning from the bottom of the ramp to the bottom of the original plunge
"""
ramp = []
reset = []
reversed_edges = [redge.clone(reverse=True) for redge in rampedges]
rampremaining = projectionlen
curPoint = p0 # start from the upper point of plunge
done = False
z = p0[2] # start from the upper point of plunge
goingForward = True
i = 0
while not done:
for i, redge in enumerate(rampedges):
if redge.Length >= rampremaining:
# will reach end of ramp within this edge, needs to be split
p1 = self.getSplitPoint(redge, rampremaining)
splitEdge = Path.Geom.splitEdgeAt(redge, p1)
Path.Log.debug("Ramp remaining: {}".format(rampremaining))
Path.Log.debug(
"Got split edge (index: {}) (total len: {}) with lengths: {}, {}".format(
i, redge.Length, splitEdge[0].Length, splitEdge[1].Length
)
)
# ramp ends to the last point of first edge
p1 = splitEdge[0].valueAt(splitEdge[0].LastParameter)
outedges.append(self.createRampEdge(splitEdge[0], curPoint, p1))
# now we have reached the end of the ramp. Go back to plunge position with constant Z
# start that by going to the beginning of this splitEdge
if goingForward:
outedges.append(
self.createRampEdge(
splitEdge[0], p1, redge.valueAt(redge.FirstParameter)
)
)
else:
# if we were reversing, we continue to the same direction as the ramp
outedges.append(
self.createRampEdge(
splitEdge[0], p1, redge.valueAt(redge.LastParameter)
)
)
done = True
break
else:
deltaZ = redge.Length / math.tan(math.radians(rampangle))
newPoint = FreeCAD.Base.Vector(
redge.valueAt(redge.LastParameter).x,
redge.valueAt(redge.LastParameter).y,
curPoint.z - deltaZ,
)
outedges.append(self.createRampEdge(redge, curPoint, newPoint))
curPoint = newPoint
rampremaining = rampremaining - redge.Length
i = 0 # current position = start of this edge. May be len(rampremaining) if going backwards
while rampremaining > 0:
redge = rampedges[i] if goingForward else reversed_edges[i - 1]
if not done:
# we did not reach the end of the ramp going this direction, lets reverse.
rampedges = self.getreversed(rampedges)
Path.Log.debug("Reversing")
# for i, redge in enumerate(rampedges):
if redge.xy_length > rampremaining:
# will reach end of ramp within this edge, needs to be split
split_first, split_remaining = redge.split(rampremaining)
ramp.append(split_first.clone(z_start=z))
# now we have reached the end of the ramp. Go back to plunge position with constant Z
# start that by going to the beginning of this splitEdge
if goingForward:
goingForward = False
reset.append(split_first.clone(reverse=True))
else:
# if we were reversing, we continue to the same direction as the ramp
reset.append(split_remaining)
i = i - 1
rampremaining = 0
break
else:
deltaZ = redge.xy_length / math.tan(math.radians(rampangle))
new_z = z - deltaZ
ramp.append(redge.clone(z, new_z))
z = new_z
rampremaining = rampremaining - redge.xy_length
i = i + 1 if goingForward else i - 1
if i == 0:
goingForward = True
if i == len(rampedges):
goingForward = False
# now we need to return to original position.
if goingForward:
# if the ramp was going forward, the return edges are the edges we already covered in ramping,
# except the last one, which was already covered inside for loop. Direction needs to be reversed also
returnedges = self.getreversed(rampedges[:i])
else:
# if the ramp was already reversing, the edges needed for return are the ones
# which were not covered in ramp
returnedges = rampedges[(i + 1) :]
while i >= 1:
reset.append(reversed_edges[i - 1])
i = i - 1
# add the return edges:
outedges.extend(returnedges)
return outedges
return ramp, reset
def createRampMethod3(self, rampedges, p0, projectionlen, rampangle):
"""
@@ -582,237 +638,83 @@ class ObjectDressup:
3. Change direction and ramp backwards to the original plunge end point
4. Continue with the original path
This method causes many unnecessary moves with tool down.
This path is computed using ramp method 1.
"""
outedges = []
rampremaining = projectionlen
curPoint = p0 # start from the upper point of plunge
done = False
i = 0
while not done:
for i, redge in enumerate(rampedges):
if redge.Length >= rampremaining:
# will reach end of ramp within this edge, needs to be split
p1 = self.getSplitPoint(redge, rampremaining)
splitEdge = Path.Geom.splitEdgeAt(redge, p1)
Path.Log.debug(
"Got split edge (index: {}) with lengths: {}, {}".format(
i, splitEdge[0].Length, splitEdge[1].Length
)
)
# ramp ends to the last point of first edge
p1 = splitEdge[0].valueAt(splitEdge[0].LastParameter)
deltaZ = splitEdge[0].Length / math.tan(math.radians(rampangle))
p1.z = curPoint.z - deltaZ
outedges.append(self.createRampEdge(splitEdge[0], curPoint, p1))
curPoint.z = p1.z - deltaZ
# now we have reached the end of the ramp. Reverse direction of ramp
# start that by going back to the beginning of this splitEdge
outedges.append(self.createRampEdge(splitEdge[0], p1, curPoint))
done = True
break
elif i == len(rampedges) - 1:
# last ramp element but still did not reach the full length?
# Probably a rounding issue on floats.
p1 = redge.valueAt(redge.LastParameter)
deltaZ = redge.Length / math.tan(math.radians(rampangle))
p1.z = curPoint.z - deltaZ
outedges.append(self.createRampEdge(redge, curPoint, p1))
# and go back that edge
newPoint = FreeCAD.Base.Vector(
redge.valueAt(redge.FirstParameter).x,
redge.valueAt(redge.FirstParameter).y,
p1.z - deltaZ,
)
outedges.append(self.createRampEdge(redge, p1, newPoint))
curPoint = newPoint
done = True
else:
deltaZ = redge.Length / math.tan(math.radians(rampangle))
newPoint = FreeCAD.Base.Vector(
redge.valueAt(redge.LastParameter).x,
redge.valueAt(redge.LastParameter).y,
curPoint.z - deltaZ,
)
outedges.append(self.createRampEdge(redge, curPoint, newPoint))
curPoint = newPoint
rampremaining = rampremaining - redge.Length
returnedges = self.getreversed(rampedges[:i])
# ramp backwards to the plunge position
for i, redge in enumerate(returnedges):
deltaZ = redge.Length / math.tan(math.radians(rampangle))
newPoint = FreeCAD.Base.Vector(
redge.valueAt(redge.LastParameter).x,
redge.valueAt(redge.LastParameter).y,
curPoint.z - deltaZ,
z_half = (p0[2] + rampedges[0].start_point[2]) / 2
r1_rampedges = [redge.clone(z_half, z_half) for redge in rampedges]
ramp, _ = self._createRampMethod1(r1_rampedges, p0, projectionlen, rampangle)
ramp_back = [
redge.clone(
2 * z_half - redge.start_point[2], 2 * z_half - redge.end_point[2], reverse=True
)
if i == len(rampedges) - 1:
# make sure that the last point of the ramps ends to the original position
newPoint = redge.valueAt(redge.LastParameter)
outedges.append(self.createRampEdge(redge, curPoint, newPoint))
curPoint = newPoint
return outedges
for redge in ramp[::-1]
]
return ramp + ramp_back
def createRampMethod2(self, rampedges, p0, projectionlen, rampangle):
"""
This method generates ramp with following pattern:
1. Start from the original startpoint of the plunge
2. Calculate the distance on the path which is needed to implement the ramp
and travel that distance while maintaining start depth
3. Start ramping while traveling the original path backwards until reaching the
original plunge end point
2. Travel at start depth along the path, for a distance required for step 3
3. Ramp backwards along the path at rampangle, arriving exactly at the bottom of the plunge
4. Continue with the original path
This path is computed using ramp method 1:
1. Move all edges up to the start height
2. Perform ramp method 1 from the bottom of the plunge *up* to the relocated path
3. Reverse the resulting path (both edge order and direction)
"""
outedges = []
rampremaining = projectionlen
curPoint = p0 # start from the upper point of plunge
if Path.Geom.pointsCoincide(
Path.Geom.xy(p0),
Path.Geom.xy(rampedges[-1].valueAt(rampedges[-1].LastParameter)),
):
Path.Log.debug("The ramp forms a closed wire, needless to move on original Z height")
else:
for i, redge in enumerate(rampedges):
if redge.Length >= rampremaining:
# this edge needs to be split
p1 = self.getSplitPoint(redge, rampremaining)
splitEdge = Path.Geom.splitEdgeAt(redge, p1)
Path.Log.debug(
"Got split edges with lengths: {}, {}".format(
splitEdge[0].Length, splitEdge[1].Length
)
)
# ramp starts at the last point of first edge
p1 = splitEdge[0].valueAt(splitEdge[0].LastParameter)
p1.z = p0.z
outedges.append(self.createRampEdge(splitEdge[0], curPoint, p1))
# now we have reached the beginning of the ramp.
# start that by going to the beginning of this splitEdge
deltaZ = splitEdge[0].Length / math.tan(math.radians(rampangle))
newPoint = FreeCAD.Base.Vector(
splitEdge[0].valueAt(splitEdge[0].FirstParameter).x,
splitEdge[0].valueAt(splitEdge[0].FirstParameter).y,
p1.z - deltaZ,
)
outedges.append(self.createRampEdge(splitEdge[0], p1, newPoint))
curPoint = newPoint
elif i == len(rampedges) - 1:
# last ramp element but still did not reach the full length?
# Probably a rounding issue on floats.
# Lets start the ramp anyway
p1 = redge.valueAt(redge.LastParameter)
p1.z = p0.z
outedges.append(self.createRampEdge(redge, curPoint, p1))
# and go back that edge
deltaZ = redge.Length / math.tan(math.radians(rampangle))
newPoint = FreeCAD.Base.Vector(
redge.valueAt(redge.FirstParameter).x,
redge.valueAt(redge.FirstParameter).y,
p1.z - deltaZ,
)
outedges.append(self.createRampEdge(redge, p1, newPoint))
curPoint = newPoint
else:
# we are traveling on start depth
newPoint = FreeCAD.Base.Vector(
redge.valueAt(redge.LastParameter).x,
redge.valueAt(redge.LastParameter).y,
p0.z,
)
outedges.append(self.createRampEdge(redge, curPoint, newPoint))
curPoint = newPoint
rampremaining = rampremaining - redge.Length
# the last edge got handled previously
rampedges.pop()
# ramp backwards to the plunge position
for i, redge in enumerate(reversed(rampedges)):
deltaZ = redge.Length / math.tan(math.radians(rampangle))
newPoint = FreeCAD.Base.Vector(
redge.valueAt(redge.FirstParameter).x,
redge.valueAt(redge.FirstParameter).y,
curPoint.z - deltaZ,
)
if i == len(rampedges) - 1:
# make sure that the last point of the ramps ends to the original position
newPoint = redge.valueAt(redge.FirstParameter)
outedges.append(self.createRampEdge(redge, curPoint, newPoint))
curPoint = newPoint
r1_rampedges = [redge.clone(p0[2], p0[2]) for redge in rampedges]
r1_p0 = rampedges[0].start_point
r1_rampangle = -rampangle
r1_result = self.createRampMethod1(r1_rampedges, r1_p0, projectionlen, r1_rampangle)
outedges = [redge.clone(reverse=True) for redge in r1_result[::-1]]
return outedges
def createCommands(self, obj, edges):
commands = []
for edge in edges:
israpid = False
for redge in self.rapids:
if Path.Geom.edgesMatch(edge, redge):
israpid = True
if israpid:
v = edge.valueAt(edge.LastParameter)
commands.append(Path.Command("G0", {"X": v.x, "Y": v.y, "Z": v.z}))
else:
commands.extend(Path.Geom.cmdsForEdge(edge))
lastCmd = Path.Command("G0", {"X": 0.0, "Y": 0.0, "Z": 0.0})
commands = [edge.command for edge in edges]
outCommands = []
tc = PathDressup.toolController(obj.Base)
horizFeed = tc.HorizFeed.Value
vertFeed = tc.VertFeed.Value
if obj.RampFeedRate == "Horizontal Feed Rate":
rampFeed = tc.HorizFeed.Value
elif obj.RampFeedRate == "Vertical Feed Rate":
rampFeed = tc.VertFeed.Value
elif obj.RampFeedRate == "Ramp Feed Rate":
rampFeed = math.sqrt(pow(tc.VertFeed.Value, 2) + pow(tc.HorizFeed.Value, 2))
else:
rampFeed = obj.CustomFeedRate.Value
horizRapid = tc.HorizRapid.Value
vertRapid = tc.VertRapid.Value
if obj.RampFeedRate == "Horizontal Feed Rate":
rampFeed = horizFeed
elif obj.RampFeedRate == "Vertical Feed Rate":
rampFeed = vertFeed
elif obj.RampFeedRate == "Ramp Feed Rate":
rampFeed = math.sqrt(pow(vertFeed, 2) + pow(horizFeed, 2))
else:
rampFeed = obj.CustomFeedRate.Value
lastX = lastY = lastZ = 0
for cmd in commands:
params = cmd.Parameters
zVal = params.get("Z", None)
zVal2 = lastCmd.Parameters.get("Z", None)
x = params.get("X", lastX)
y = params.get("Y", lastY)
z = params.get("Z", lastZ)
xVal = params.get("X", None)
xVal2 = lastCmd.Parameters.get("X", None)
yVal2 = lastCmd.Parameters.get("Y", None)
yVal = params.get("Y", None)
zVal = zVal and round(zVal, 8)
zVal2 = zVal2 and round(zVal2, 8)
z = z and round(z, 8)
if cmd.Name in ["G1", "G2", "G3", "G01", "G02", "G03"]:
if zVal is not None and zVal2 != zVal:
if Path.Geom.isRoughly(xVal, xVal2) and Path.Geom.isRoughly(yVal, yVal2):
# this is a straight plunge
if lastZ != z:
if Path.Geom.isRoughly(x, lastX) and Path.Geom.isRoughly(y, lastY):
params["F"] = vertFeed
else:
# this is a ramp
params["F"] = rampFeed
else:
params["F"] = horizFeed
lastCmd = cmd
elif cmd.Name in ["G0", "G00"]:
if zVal is not None and zVal2 != zVal:
if lastZ != z:
params["F"] = vertRapid
else:
params["F"] = horizRapid
lastCmd = cmd
lastX, lastY, lastZ = x, y, z
outCommands.append(Path.Command(cmd.Name, params))

View File

@@ -642,7 +642,7 @@ class PathData:
Path.Log.track(obj.Base.Name)
self.obj = obj
path = PathUtils.getPathWithPlacement(obj.Base)
self.wire, rapid = Path.Geom.wireForPath(path)
self.wire, rapid, rapid_indexes = Path.Geom.wireForPath(path)
self.rapid = _RapidEdges(rapid)
if self.wire:
self.edges = self.wire.Edges

View File

@@ -320,24 +320,24 @@ def cmdsForEdge(edge, flip=False, useHelixForBSpline=True, segm=50, hSpeed=0, vS
offset = edge.Curve.Center - pt
else:
pd = Part.Circle(xy(p1), xy(p2), xy(p3)).Center
Path.Log.debug(
"**** %s.%d: (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) -> center=(%.2f, %.2f)"
% (
cmd,
flip,
p1.x,
p1.y,
p1.z,
p2.x,
p2.y,
p2.z,
p3.x,
p3.y,
p3.z,
pd.x,
pd.y,
)
)
# Path.Log.debug(
# "**** %s.%d: (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) -> center=(%.2f, %.2f)"
# % (
# cmd,
# flip,
# p1.x,
# p1.y,
# p1.z,
# p2.x,
# p2.y,
# p2.z,
# p3.x,
# p3.y,
# p3.z,
# pd.x,
# pd.y,
# )
# )
# Have to calculate the center in the XY plane, using pd leads to an error if this is a helix
pa = xy(p1)
@@ -345,15 +345,15 @@ def cmdsForEdge(edge, flip=False, useHelixForBSpline=True, segm=50, hSpeed=0, vS
pc = xy(p3)
offset = Part.Circle(pa, pb, pc).Center - pa
Path.Log.debug(
"**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)"
% (pa.x, pa.y, pa.z, pc.x, pc.y, pc.z)
)
Path.Log.debug(
"**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)"
% (pb.x, pb.y, pb.z, pd.x, pd.y, pd.z)
)
Path.Log.debug("**** (%.2f, %.2f, %.2f)" % (offset.x, offset.y, offset.z))
# Path.Log.debug(
# "**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)"
# % (pa.x, pa.y, pa.z, pc.x, pc.y, pc.z)
# )
# Path.Log.debug(
# "**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)"
# % (pb.x, pb.y, pb.z, pd.x, pd.y, pd.z)
# )
# Path.Log.debug("**** (%.2f, %.2f, %.2f)" % (offset.x, offset.y, offset.z))
params.update({"I": offset.x, "J": offset.y, "K": (p3.z - p1.z) / 2})
# G2/G3 commands are always performed at hSpeed
@@ -387,8 +387,8 @@ def edgeForCmd(cmd, startPoint):
"""edgeForCmd(cmd, startPoint).
Returns an Edge representing the given command, assuming a given startPoint."""
Path.Log.debug("cmd: {}".format(cmd))
Path.Log.debug("startpoint {}".format(startPoint))
# Path.Log.debug("cmd: {}".format(cmd))
# Path.Log.debug("startpoint {}".format(startPoint))
endPoint = commandEndPoint(cmd, startPoint)
if (cmd.Name in CmdMoveStraight) or (cmd.Name in CmdMoveRapid) or (cmd.Name in CmdMoveDrill):
@@ -403,9 +403,9 @@ def edgeForCmd(cmd, startPoint):
d = -B.x * A.y + B.y * A.x
if isRoughly(d, 0, 0.005):
Path.Log.debug(
"Half circle arc at: (%.2f, %.2f, %.2f)" % (center.x, center.y, center.z)
)
# Path.Log.debug(
# "Half circle arc at: (%.2f, %.2f, %.2f)" % (center.x, center.y, center.z)
# )
# we're dealing with half a circle here
angle = getAngle(A) + math.pi / 2
if cmd.Name in CmdMoveCW:
@@ -413,34 +413,34 @@ def edgeForCmd(cmd, startPoint):
else:
C = A + B
angle = getAngle(C)
Path.Log.debug(
"Arc (%8f) at: (%.2f, %.2f, %.2f) -> angle=%f"
% (d, center.x, center.y, center.z, angle / math.pi)
)
# Path.Log.debug(
# "Arc (%8f) at: (%.2f, %.2f, %.2f) -> angle=%f"
# % (d, center.x, center.y, center.z, angle / math.pi)
# )
R = A.Length
Path.Log.debug(
"arc: p1=(%.2f, %.2f) p2=(%.2f, %.2f) -> center=(%.2f, %.2f)"
% (startPoint.x, startPoint.y, endPoint.x, endPoint.y, center.x, center.y)
)
Path.Log.debug("arc: A=(%.2f, %.2f) B=(%.2f, %.2f) -> d=%.2f" % (A.x, A.y, B.x, B.y, d))
Path.Log.debug("arc: R=%.2f angle=%.2f" % (R, angle / math.pi))
# Path.Log.debug(
# "arc: p1=(%.2f, %.2f) p2=(%.2f, %.2f) -> center=(%.2f, %.2f)"
# % (startPoint.x, startPoint.y, endPoint.x, endPoint.y, center.x, center.y)
# )
# Path.Log.debug("arc: A=(%.2f, %.2f) B=(%.2f, %.2f) -> d=%.2f" % (A.x, A.y, B.x, B.y, d))
# Path.Log.debug("arc: R=%.2f angle=%.2f" % (R, angle / math.pi))
if isRoughly(startPoint.z, endPoint.z):
midPoint = center + Vector(math.cos(angle), math.sin(angle), 0) * R
Path.Log.debug(
"arc: (%.2f, %.2f) -> (%.2f, %.2f) -> (%.2f, %.2f)"
% (
startPoint.x,
startPoint.y,
midPoint.x,
midPoint.y,
endPoint.x,
endPoint.y,
)
)
Path.Log.debug("StartPoint:{}".format(startPoint))
Path.Log.debug("MidPoint:{}".format(midPoint))
Path.Log.debug("EndPoint:{}".format(endPoint))
# Path.Log.debug(
# "arc: (%.2f, %.2f) -> (%.2f, %.2f) -> (%.2f, %.2f)"
# % (
# startPoint.x,
# startPoint.y,
# midPoint.x,
# midPoint.y,
# endPoint.x,
# endPoint.y,
# )
# )
# Path.Log.debug("StartPoint:{}".format(startPoint))
# Path.Log.debug("MidPoint:{}".format(midPoint))
# Path.Log.debug("EndPoint:{}".format(endPoint))
if pointsCoincide(startPoint, endPoint, 0.001):
return Part.makeCircle(R, center, FreeCAD.Vector(0, 0, 1))
@@ -472,17 +472,19 @@ def wireForPath(path, startPoint=Vector(0, 0, 0)):
Returns a wire representing all move commands found in the given path."""
edges = []
rapid = []
rapid_indexes = set()
if hasattr(path, "Commands"):
for cmd in path.Commands:
edge = edgeForCmd(cmd, startPoint)
if edge:
if cmd.Name in CmdMoveRapid:
rapid.append(edge)
rapid_indexes.add(len(edges))
edges.append(edge)
startPoint = commandEndPoint(cmd, startPoint)
if not edges:
return (None, rapid)
return (Part.Wire(edges), rapid)
return (None, rapid, rapid_indexes)
return (Part.Wire(edges), rapid, rapid_indexes)
def wiresForPath(path, startPoint=Vector(0, 0, 0)):