# 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

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)
