# Types and tools to handle moves (traces or jumps).
# Last edited on 2021-10-31 05:06:50 by stolfi

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/
  # {fabtime(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.  Returns {False} if it is {None} or a trace.
  return move_IMP.is_jump(omv)

def is_trace(omv):
  # Returns {True} if {omv} is an oriented or unoriented trace -- that is,
  # if its nominal width is positive.  Returns {False} if it is {None} or a jump.
  return move_IMP.is_trace(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(omv0, omv1, mvdir, tol):
  # The procedure checks whether the moves {omv0,omv1} are roughly
  # parallel to the unit direction vector {mvdir} (a pair of floats),
  # their fat segments (nominal extents) touch each other, and their
  # projections along {mvdir} overlap by a significant amount. If that is
  # the case, the procedure returns the endpoints {p0,p1} of the line
  # segment that is the intersection of their boundaries, in increasing
  # order along {mvdir}. The orientation of the moves is ignored.
  #
  # If the fat segments of the two moves are separated or overlap
  # in the in the direction perpendicular to {mvdir} by more than {tol}
  # millimeters, returns {None,None}. The same is returned if 
  # either or both moves are jumps.
  # 
  # The procedure uses a small tolerance when deciding whether the moves
  # are parallel, their boundaries touch, and the {mvdir} projections overlap.
  #
  # The {mvdir} can be {None}, in which case the procedure will try to
  # guess a suitable {mvdir} from the mean direction of the two moves.
  # Iin this case, if the guess fails, the procedures returns
  # {None,None}.
  return move_IMP.shared_border(omv0, omv1, mvdir, tol)

def sort_by_midpoint(omv0, om1, ydir):
  # Returns the oriented moves {omv0} and {omv1}, swapped
  # if needed so that the midpoint of {omv0} is lower than that
  # of {omv1} in the direction {ydir}.
  #
  # If {ydir} is {None}, uses {(0,1)}, that is, sort s by the Y coordinate.
  return move_IMP.sort_by_midpoint(omv0, om1, ydir)

# TIMING
  
def fabtime(omv):
  # Returns the execution time of the oriented move {omv}.
  # Does NOT include the nozzle up/down or filament suck/spit 
  # at the start/end of jumps.
  return move_IMP.fabtime(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 transition_penalty(omv0, omv1):
  # If either or both of {omv0} or {omv1} is {None}, returns 0.
  # Otherwise they must be oriented moves, and {pfin(omv0)} must be the
  # same as {pini(omv1)}. If one is a trace and the other is a jump,
  # returns the trace/jump transition time penalty that would apply were
  # consecutive elements of a tool-path. Otherwise returns 0.
  return move_IMP.transition_penalty(omv0, omv1)

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 {fabtime(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)

# PLOTTING

def plot_to_files(fname, OMVS, CLRS, rwd, 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}.  Each trace is drawn as a sausage of width
  # {rwd*wd} where {wd} is its nominal width.
  #
  # 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, rwd, wd_axes)
 
def plot_standard(c, OMVS, dp, layer, CLRS, rwd, 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 width {rwd*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, rwd, 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 has_name(omv):
  # Returns {True} if and only if the name attribute of {omv}'s
  # underlying {Move} object is not {None}.
  return move_IMP.has_name(omv)

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}, returns 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 tag_names(OMVS, tag):
  # Prepends the string {tag} to the names of all moves in {OMVS}.
  # The {Move} objects had better be all  distinct.
  move_IMP.tag_names(OMVS, tag)

def show(wr, pref, omv, suff, wna):
  # Writes to {wr} a readable descrption of the oriented move {omv}.
  #
  # The description has: 
  #
  #   * the orientation bit {dr} of {omv}, as " " or "~"
  #   * the name of the underlying {Move} object, as returned by {get_name}
  #   * the initial and final points
  #   * the nominal width (or "jmp" if it is a jump)
  #   * an optional observation 'vertiacal', 'horizontal', 'trivial', etc.
  #
  # The name is padded to {wna} columns and left-aligned. The whole
  # output fits in one line, is preceded by {pref} (if not {None}, and
  # followed by {suff} (if not {None}). It is NOT terminated by a
  # newline, unless provided by {suff}.
  move_IMP.show(wr, pref, omv, suff, wna)

def show_list(wr, pref, OMVS, suff):
  # Writes to file {wr} a readable summary description of the oriented moves in the
  # list {OMVS}.
  #
  # The output consists of a header and then the description of each
  # move, one per line, produced by {show}. Each line is prefixed by
  # {pref} and its index in {OMVS}, and followed by {suff} and a
  # newline.
  move_IMP.show_list(wr, pref, OMVS, suff)
