# Types and tools to handle moves (traces or jumps). # Last edited on 2021-03-21 10:01:16 by jstolfi import move_IMP; from move_IMP import Move_IMP import move_parms import pyx class Move(Move_IMP): # An object of the {Move} class represents a /move/, a # straight-line movement of the nozzle: either a /trace/ if the nozzle # is down and extruding material, or a /jump/ if the nozzle is raised # and not extruding. # # The /axis/ of a {Move} object {mv} is the line segment traversed by # the center of the nozzle. The /endpoints/ of the move are those of # that segment. The order of the two points is not necessarily the # direction of motion of the nozzle (which may be unknown). Each # endpoint is a list of 2 float coordinates, in millimeters. # # A {Move} object {mv} has a read-only set of /move parameters/, # encoded as a {Move_Parms} object. See {move_parms.py}. These include # the move's /nominal width/ in mm, {width(mv)}, that is mostly used # when generating G-code or plotting. For a trace of length {L} (mm), # the volume of material deposited will be {V = L*T*width(mv)} (mm^3), # where {T} is the slice's thickness (mm). (This formula is valid in # the limit of large {L}, since it does not account for the roundish # "caps" of material deposited at the ends of the move.) The nominal # width is zero if and only if the move is a jump. # # Another read-only attribute of a {Move} is its /execution time/ # {extime(mv)}, in seconds. It depends on the length of the move and # on the dynamic parameters specified by the {Move_Parms} object. The # execution time of a jump does NOT include the trace/jump transition # penalties; these are accounted for when computing the execution time # of multi-move tool-paths. # # A {Move} object also has a mutable /name/ attribute, that can be useful # in debugging or documentation. It must be either a string or {None}. # # An /oriented move/ is a pair {(mv,dr)} where {mv} is a {Move} object # and {dr} is 0 or 1 to indicate the direction of motion of the nozzle # along the axis. A {Move} object {mv} by itself is assumed to be the # oriented move {(mv,0)}. pass def make(p0, p1, mp): # Creates and returns a {Move} object with endpoints {(p0,p1)}, with # the nominal width and timing parameters specified by the # {Move_Parms} object {mp}. The latter specifies whether the move is a # trace or a jump. The {name} is set to {None}. return move_IMP.make(p0, p1, mp) def parameters(omv): # Returns the {Move_Parms} object of the move {omv}. return move_IMP.parameters(omv) def is_jump(omv): # Returns {True} if {omv} is an oriented or unoriented jump -- that is, # if its nominal width is zero. return move_IMP.is_jump(omv) def rev(omv): # Returns the reversal of the oriented move {omv}, namely the same # {Move} object with the orientation bit {dr} complemented. return move_IMP.rev(omv) def spin(omv, dr): # Applies {rev} to the oriented move {omv}, {dr} times. # Thus the result is {omv} if {dr} is even, and {rev(omv)} # if {dr} is odd. # # If the {omv} argument is an unoriented {Move} object {mv}, the # result is {(mv,dr)}. return move_IMP.spin(omv, dr) def unpack(omv): # If {omv} is an oriented move, returns the underlying # {Move} object and the direction bit as two separate results. # If {omv} is already a move object, returns {omv} and {0}. # Also checks whether the object is indeed a {Move}. return move_IMP.unpack(omv) # GEOMETRY def width(omv): # Returns the nominal width of the oriented or unoriented move {omv}. return move_IMP.width(omv) def pini(omv): # Retuns the initial endpoint of an oriented move {omv}, taking the # orientation bit into account. return move_IMP.pini(omv) def pfin(omv): # Retuns the final endpoint of an oriented move {omv}, taking the # orientation bit into account. return move_IMP.pfin(omv) def endpoints(omv): # Returns the endpoints of the axis of the oriented move {omv}, namely the pair # {(pini(omv), pfin(omv))}. Note that the order depends on the # orientation bit of {omv}. return move_IMP.endpoints(omv) def length(omv): # Returns the Euclidean length of the axis of {omv}, that is, the # distance between {pini(omv)} and {pfin(omv)}. Note that it does not # consider the round caps at the ends of a trace. return move_IMP.length(omv) def bbox(OMVS): # The parameter {OMVS} must be a list of oriented moves. Returns a # bounding box for those moves, as a pair of points {(plo, phi)} that # its lower left and upper right corners. If the list is empty, # returns {None}. # # The bounding box is the smallest axis-aligned rectangle that # contains endpoints (only) of all those moves. Note that decorations # such as dots and arrowheads, as well as the nominal "sausages" of # traces, may extend outside the box. return move_IMP.bbox(OMVS) def displace(omv, ang, disp, mp): # Returns a copy of the oriented move {omv} rotated by {ang} radians # around the origin and translated by the vector {disp}, # in that order. # # The move will be a new {Move} object, even if {ang} and {disp} are # zero. The parameters are replaced by the {Move_Parms} object {mp}, # and the execution time is recomputed. return move_IMP.displace(omv, ang, disp, mp) def shared_border(mv0, mv1): # If the nominal extents of moves {mv0} and {mv1} touch each other, # returns the endpoints {p0,p1} of the line segment that is the # intersection of their boundaries. In particular, if they touch # at a single point, returns that point twice. # # If the nominal extents of the two moves do not touch, returns {None,None}. # This is the case, in particular, if either or both moves are jumps. # # The procedure uses a small tolerance when deciding whether the # moves touch. But the result is undefined if the two moves overlap # significantly. return move_IMP.shared_border(mv0, mv1) # TIMING def extime(omv): # Returns the execution time of the oriented move {omv}. return move_IMP.extime(omv) def ud_penalty(omv): # If {omv} is a jump, returns the additional time penalty # for raising/lowerng the nozzle and/or retracting/refeeding the filament if # the jump were to be preceded or followed by a trace in a path. # If {omv} is a trace, returns zero. return move_IMP.ud_penalty(omv) def cover_time(omv, m): # Returns the time when the nozzle passes by the point {m} while # executing the oriented move {omv} in the specified direction, # counted from the beginning of the execution. # # Specificaly, returns the time that the nozzle takes to travel from # {pini(omv)} to the point on the axis of the move that is closest to # point {m}. The result is always between 0 and {extime(omv)}. # # This function is intended for traces. If used for a jump, # it does not include any trace/jump transition penalty time. return move_IMP.cover_time(omv, m) # CONNECTORS def connector(omv_prev, omv_next, use_jump, use_link, mp_jump): # Returns a {move.Move} object {mvcn} that connects the end of oriented # move {omv_prev} to the start of the oriented move {omv_next}. # # The connector may be either a jump or a trace (a /link/). If # {use_jump} is true, or the two moves have distinct {Move_Parms} # objects (even if they contain the same value), of they are jumps, # the connector will be a jump. Otherwise, if {use_link} is true, the # connector will be a link. Otherwise the decision will be based on # the widths and directions of the two moves and the distance between # the endpoints. # # In the last case, the procedure uses a jump if either of the two # moves is a jump, or if they have different nominal widths, or if the # distance to be spanned is large (compared to their nominal widths), # or if they are too short (ditto), or the turning angles from # {omv_prev} to {mvcn} and from {mvcn} to {omv_next) are both zero or # have opposite signs, or if a jump would be faster than a link. # # In any case, if a jump is chosen, it will have parameters {mp_jump}. # If the connector is certain to be a link, {mp_jump} may be {None}. # If a link is chosen, it will have the same parameters as {omv_prev} and # {omv_next}. return move_IMP.connector(omv_prev, omv_next, use_jump, use_link, mp_jump) def connector_extime(omv_prev, omv_next, use_jump, use_link, mp_jump): # Same as {connector}, but only computes the execution time of the # connector, without creating it. # # If the insertion of the connector would create new trace/jump # transtitions at the end of {omv_prev} or at the start of {omv_next}, # the corresponting time penalties will be added to the result. return move_IMP.connector_extime(omv_prev, omv_next, use_jump, use_link, mp_jump) def connector_parameters(omv_prev, omv_next, use_jump, use_link, mp_jump): # Returns the {Move_Parms} object that {connector} should use. # It will be either {mp_jump}, or the common parameters object # of {omv_prev} and {omv_next}. return move_IMP.connector_parameters(omv_prev, omv_next, use_jump, use_link, mp_jump) def connector_must_be_jump(vpq, v_prev, v_next, dmax, mp_jump, mp_link): # Returns {True} if a connecting move between two traces with the same # parameters is better be a jump than a trace of width {wd}. Assumes # that {vpq} is the displacement vector to be covered by the # connector, {v_prev,v_next} are the displacement vectors of the # previous and _next traces, {dmax} is the maximum allowed link length, # and {mp_jump,mpLink} are the {Move_Parms} objects to be used in each case. return move_IMP.connector_must_be_jump(vpq, v_prev, v_next, dmax, mp_jump, mp_link) # PLOTTING def plot_to_files(fname, OMVS, CLRS, wd_axes): # The argument {OMVS} must be a tuple or list of oriented moves (or # plain {Move} objects). The {CLRS} argument must be a list or tuple # of {pyx.color.rgb} objects. # # Plots each move in {OMVS} using some default sytle and the # correspondng color of {CLRS} for trace sausages, with a millimeter # grid in the backgrounds. The trace axes and jump lines will be drawn # with line width {wd_axes}. # # Writes the plot to files "{fname}.{ext}" where {ext} # is "png", "eps", and "jpg". # # If {CLRS} has a single element, uses that color for all trace # sausages. If {CLRS} is {None}, uses some default color, Otherwise {CLRS} # must have the same length as {OMVS}. return move_IMP.plot_to_files(fname, OMVS, CLRS, wd_axes) def plot_standard(c, OMVS, dp, layer, CLRS, wd_axes, axes, dots, arrows, matter): # The argument {OMVS} must be a tuple or list of oriented moves (or # plain {Move} objects). The {CLRS} argument must be a list or tuple # of {pyx.color.rgb} objects. # # Plots each move {omv} in {OMVS}, displaced by the vector {dp}, using the # corresponding color of {CLRS} for the trace sausages. # # If {omv} is a trace, draws it as a "sausage" -- a thick rectangle # with round caps at the end, with a width slightly smaller than its # nominal width {wd=width(mv)}. # # If {axes} is true, also plots the axis of each trace, not just of jumps. # # If {dots} is true, also plots dots at the endpoints of each trace, not # just of jumps # # If {arrows} is true, also plots an arrowhead midway along each trace, # showing its orientation. # # If {matter} is {true}, shows the estimated area covered by the # material extruded during traces, as a gray sausage slightly wider # than the nominal width, painted under the trace. # # The axes, dots, and arrows of traces are drawn with a darkened # version of the color used for the sausage. # # If {omv} is a jump, the main and overflow sausages is omitted, and # the axis always drawn drawn as a dashed black line with dots and # arrowhead. # # The width of the axis of a trace or jump will be {wd_axes}, which # should be positive. The sizes of dots and arrows will be fixed # proportions of {wd_axes}. # # If {CLRS} has a single element, uses it for all traces. If {CLRS} is # {None}, uses a default color. Otherwise {CLRS} must have the same # length as {OMVS}. If {dp} is {None}, assumes {(0,0)} (no # displacement). # # The plot is done in 4 passes or /layers/: (0) the overflow sausage # of the estimated extruded material, if {omv} is a trace and {matter} # is true, (1) the main sausage, if {omv} is a trace, (2) the axis, # dots, and arrowheads, if {omv} is a trace and these items have been # requested, and (3) the axis, dots, and arrowhead, if {omv} is a # jump. # # If the parameter {layer} is not {None}, it must be an integer # in {0..3}, in which case only that layer is plotted. move_IMP.plot_standard(c, OMVS, dp, layer, CLRS, wd_axes, axes, dots, arrows, matter) def plot_layer(c, omv, dp, clr, wd, dashed, wd_dots, sz_arrow): # Plots the move (trace or jump) {omv} on the {pyx} context {c}. # # Specifically, If {wd} is a positive number, draws the move's axis as # a fat line segment: a rectangle of width {wd} with round caps # centered at the move's endpoints. The move's nominal width is # ignored. # # If {dashed} is true, the axis line will be dashed. # # If {wd_dots} is a positive number, plots round dots of that diameter at the # endpoints # # If {sz_arrow} is a positive number, draws an arrowhead with that size # at the midpoint of the axis, to show the move's direction. # The arrow is drawn even if the axis is not drawn. # # All items will be drawn with the color {clr}. If {clr} is {None}, does nothing. # # The plot will be displaced by the vector {dp} (a 2-tuple of floats). # If {dp} is {None}, assumes {(0,0)} (no displacement). # # If {None} is given as {wd}, {wd_dots}, or {sz_arrow}, the value 0 is # assumed. move_IMP.plot_layer(c, omv, dp, clr, wd, dashed, wd_dots, sz_arrow) # DEBUGGING AND TESTING def get_name(omv): # Given an oriented move {omv}, returns the name attribute of the # underlying {Move} object {mv}, if not {None}. If the name of {mv} is # {None}, uses instead "T?" or "J?" depending on whether it is a trace # or a jump. In any case, that name is prefixed "~" if {omv} is # {rev(mv)}. return move_IMP.get_name(omv) def set_name(omv, name): # Given an oriented move {omv}, saves the string {name} as the name attrbute of the underlying # {Move} object {mv}. The orientation of {omv} is ignored. move_IMP.set_name(omv, name) def describe(wr, OMVS): # Writes to file {wr} a readable description of the oriented moves in the # list {OMVS}. move_IMP.describe(wr, OMVS)