diff --git a/src/Mod/CAM/CAMTests/TestPathGeom.py b/src/Mod/CAM/CAMTests/TestPathGeom.py index 6e7cb20bef..158b914eb4 100644 --- a/src/Mod/CAM/CAMTests/TestPathGeom.py +++ b/src/Mod/CAM/CAMTests/TestPathGeom.py @@ -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)) diff --git a/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py b/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py index 34673e1548..32ca7f09cc 100644 --- a/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py +++ b/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py @@ -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): diff --git a/src/Mod/CAM/Path/Dressup/Gui/RampEntry.py b/src/Mod/CAM/Path/Dressup/Gui/RampEntry.py index f96d68658a..870fc66d9b 100644 --- a/src/Mod/CAM/Path/Dressup/Gui/RampEntry.py +++ b/src/Mod/CAM/Path/Dressup/Gui/RampEntry.py @@ -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)) diff --git a/src/Mod/CAM/Path/Dressup/Tags.py b/src/Mod/CAM/Path/Dressup/Tags.py index 1fcd8e5db5..5a3d489d53 100644 --- a/src/Mod/CAM/Path/Dressup/Tags.py +++ b/src/Mod/CAM/Path/Dressup/Tags.py @@ -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 diff --git a/src/Mod/CAM/Path/Geom.py b/src/Mod/CAM/Path/Geom.py index d625fd00d6..13f3154892 100644 --- a/src/Mod/CAM/Path/Geom.py +++ b/src/Mod/CAM/Path/Geom.py @@ -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)):