# Tools for handling tool-paths. # Last edited on 2021-02-19 14:19:42 by jstolfi import path_IMP; from path_IMP import Path_IMP import move import pyx class Path(Path_IMP): # An object of the {Path} class represents a /tool-path/, or /path/ # for short: the traveling of the nozzle along a polygonal path -- # sometimes down, extruding material, sometimes raised, not extruding. # It is a sequence of zero or more oriented moves (traces or jumps), # such that the final point of each one is the initial point of the # next one. # # Every {Path} object {ph} has an /initial point/ and a /final point/ # the initial and final position of the nozzle as it executes the # path. These points are defined even if for an /empty/ path with zero # moves, in which case they are the same point. If the path is not # empty, they are the initial point of the first move and the last # point of the last move. # # The path may begin and/or end with a jump. It makes no sense for a # path to have two consecutive jumps, since those can be condensed # into a single jump with significant time saving. It also does not # make sense to have jumps of zero length. On the other hand, a trace # of zero length makes sense, provided that it is not followed or # preceded by another trace: it deposits a dot of material. # # An /oriented path/ is a pair {(ph,dr)} where {ph} is a {path} object # and {dr} is 0 or 1 to indicate a direction of motion. If {dr} is zero # (the /native/ orientation), moves are assumed to be executed in the # order and orientations specified by the {Path} object. If {dr} is 1, # the moves are assumed to be executed in the reversed order, each in # the opposite direction. Note that the initial and final points of the # path are swapped in this case. A {Path} object {ph} by itself is # generally treated as the oriented path {(ph,0)}. pass # ATTRIBUTES def nelems(oph): # Returns the number of moves in the oriented path {oph}. return path_IMP.nelems(oph) def elem(oph, k): # Returns the oriented move with index {k} in the oriented path {oph=(ph,dr)}, # counting in the order implied by the orientation. The index {k} # must be in {0..n-1} where {n = nelems(oph)}. return path_IMP.elem(oph, k) def pini(oph): # Returns the initial point of the oriented path {oph}, taking the # orientation bit into account. return path_IMP.pini(oph) def pfin(oph): # Returns the final point of the oriented path {oph}, taking the # orientation bit into account. return path_IMP.pfin(oph) def bbox(oph): # Returns a bounding box of all moves of the oriented path {oph}, # as a pair of points {(plo, phi)} that its lower left and upper right # corners. # # The box is the smallest axis-aligned rectangle that contains all the # endpoints of all moves and jumps in {p}. Note that the nominal # "sausages" of traces, as well as decoration such as dots and # arrowheads, may extend outside the box. return path_IMP.bbox(oph) def find(oph, omv): # Given an oriented path {oph} and an oriented or unoriented move {omv}, # returns the index {i} such that {elem(oph, i)} is {omv} or its # reverse. If the move does not occur in {oph}, returns {None}. return path_IMP.find(oph, omv) # CREATION def make_empty(p): # Creates and returns an (unoriented) empty {Path} object, with zero # moves, that begins and ends at the point {p}. return path_IMP.make_empty(p) def from_move(p, q, wd, parms): # Creates and returns an (unoriented) {Path} object, with exactly # one move from {p} to {q}. The move is a jump if {wd} is true, # a move if {wd} is false. return path_IMP.from_move(p, q, wd, parms) def from_moves(omvs): # The argument {omvs} must be a list of one or more moves, # such that each move ends where the next one begins. # Creates and returns a path consisting of those moves. return path_IMP.from_moves(omvs) def from_points(pts, wd, parms): # The argument {pts} must be a list of one or more points. Creates and # returns a path consisting of traces of width {wd} that connect those # points in sequence. # # An element of {pts} may be itself a sub-list of points. In that case, # those points are connected by traces of nominal width {wd}, and that # sub-path is connected by jumps to the previous and following path # elements. return path_IMP.from_points(pts, wd, parms) def concat(ophs, jumps, parms): # The argument {ophs} must be a list of one or more oriented paths. # Returns a new {Path} object {ph} that is the concatenation of # all those paths. # # If the final point of one of the paths is not the initial point of the # next path, a /connector/ is inserted between them. If {jumps} is true, # the connector will always be a jump. If {jumps} is false, and certain # geometric conditions are satisfied, the connector will be a trace (a /link/). # # The argument {parms} is used to evaluate whether each connector should be # jump or link, and to compute the execution time of these connectors # (see {move.make}). # # The resulting path {ph} will share all the {move.Move} objects of # the given paths. Note that, depending on the inputs, it may have # multiple consecutive jumps (they are not automatically condensed). # # This operation does not modify the given paths. It takes time # proportional to the number of paths plus the total number of moves in # those paths. return path_IMP.concat(ophs, jumps, parms) def displace(oph, ang, v, parms): # Returns a copy of the oriented path {oph} rotated by {ang} radians # around the origin and translated by the vector {v}, # in that order. # # All moves will be new {move.Move} objects, even if {ang}and {v} are # zero. Recomputes all the timing data, in case the execution time of a # move depends on the move's direction. return path_IMP.displace(oph, ang, v, parms) # ORIENTATION def rev(oph): # Returns the reversal of the oriented path {oph}. return path_IMP.rev(oph) def orient(oph, dr): # Applies {rev} to the oriented path {oph}, {dr} times. # Thus the result is {oph} if {dr} is even, and {rev(oph)} # if {dr} is odd. # # If the {oph} argument is an unoriented {Path} object {ph}, the # result is {(ph,dr)}. return path_IMP.orient(oph, dr) def unpack(oph): # Checks whether {oph} is an oriented path. Returns the underlying # {Path} object and the direction bit as two separate results. # If {oph} is already an (undirected) {Path} object, returns {oph,0}. return path_IMP.unpack(oph) # TIMING def extime(oph): # Returns the total time to execute the path {oph}, including all jumps # in it. The orientation of {oph} is irrelevant. return path_IMP.extime(oph) def tini(oph, k): # Returns the total nozzle travel time from the initial point of the oriented path {oph} # to the INITIAL point of the oriented move {elem(oph,k)}, including all acceleration # and deceleration times and the raising and lowering of nozzle at the jumps. # # For convenience, if {k == nelems(oph)}, returns {extime(oph)}. return path_IMP.tini(oph, k) def tfin(oph, k): # Returns the total nozzle travel time from the initial point of the oriented path {oph} # to the FINAL point of the oriented move {elem(oph,k)}, including all acceleration # and deceleration times and the raising and lowering of the nozzle at the jumps. # # For convenience, if {k == -1}, returns zero. return path_IMP.tfin(oph, k) # PLOTTING def plot_standard(c, oph, dp, layer, ctraces, waxes, axes, dots, arrows, matter): # Plots the oriented path {oph} on the {pyx} context {c}, using a # standard style. # # If {axes} is true, draws the axes of traces, not just of jumps. # # If {dots} is true, prints dots at the ends of traces, not just of jumps. # # If {arrows} is true, draws arrowheads on traces, not just on jumps. # # If {matter} is {true}, shows the estimated area covered by the # material extruded during traces. # # The fat sausage of a trace will be filled with color {ctraces} and its # width will be the nominal width, reduced a bit for clarity. The axes # of moves (traces or jumps) will be drawn with width {waxes}: solid for # traces, dashed for jumps. The dots at the endpoints and arrows will be # drawn with size proportional to {waxes}. The color of these lines and # decorations will black for jumps, and darkened version of {ctraces} for # traces. # # If {ctraces} is {None}, some default color is used. If {dp} is {None}, # assumes {(0,0)} (no displacement). # # The plot is done in 4 passes or /layers/: (0) the material overflow # sausages of all traces, if reequested; (1) the main sausage of all # traces; (2) the axes, dots, and arrows of all traces, as requested; # and (3) the axes, dots, and arrows of all jumps. If {layer} is not # {None}, it must be an integer in {0..3}, in which case only that # layer is plotted. path_IMP.plot_standard(c, oph, dp, layer, ctraces, waxes, axes, dots, arrows, matter) def plot_layer(c, oph, dp, jmp, clr, rtraces, waxes, dashed, wdots, szarrows): # Plots selected elements of the oriented path {oph} on the {pyx} # context {c}. The plot will be displaced by the vector {dp} (a 2-tuple # of floats). # # Plots only the jumps if {jmp} is true, and only the traces if {jmp} is false. # # If {rtraces} and/or {waxes} are positive, the axis of each selected # move {omv} is drawn as a line with round caps at the endpoints. The # line will be dashed if {dashed} is true. The width of that line will # be {rtraces*width(omv) + waxes}. So, if the move is a jump, it will # be just {waxes}. # # If {wdots} is true, dots of that diameter will be plotted at both # ends of each selected move, even if the axis itself is not drawn. # # aIf {szarrows} is nonzero, an arrowhead of that size will be drawn # halfway along the axis of each selected move that is long enough, # even if the axis itself is not drawn. # # These items are drawn with color {clr}. If {clr} is {None}, the # procedure does nothing. If {dp} is {None}, assumes {(0,0)} (no # displacement). path_IMP.plot_layer(c, oph, dp, jmp, clr, rtraces, waxes, dashed, wdots, szarrows) # TESTING AND DEBUGGING def validate(oph): # Runs some consistency checks on the oriented path {oph}. # Aborts with assert failure if it detects any errors. path_IMP.validate(oph)