# Tools for handling tool-paths.
# Last edited on 2021-03-21 15:40:22 by jstolfi

import path_IMP; from path_IMP import Path_IMP

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.
  #
  # The /execution time/ of path is the time needed to execute
  # all its moves, plus one /transition pernalty time/ 
  # for each internal transition from a trace to a jump or 
  # vice-versa.  This penalty is assumed to be part of the 
  # execution time of the jump.
  #
  # A {Path} object also has a mutable /name/ attribute, that is used
  # for debugging and documentation. It must be either a string or {None}.
  #
  # 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
   
# 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, mp):
  # Creates and returns an (unoriented) {Path} object, with exactly
  # one move from {p} to {q}.  The width and dynamics will be as defined
  # by the {Move_Parms} object {mp}
  return path_IMP.from_move(p, q, mp)
  
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, mp_trace, mp_jump):
  # The argument {pts} must be a list whose elements are points or lists of points.
  #
  # If {n} consecutive elements of {pts} are points, the procedure creates
  # a sub-path of {n-1} traces with parameters {mp_trace}, connecting those points
  #
  # An element of {pts} may be itself a sub-list of points. In that
  # case, those points are connected by traces, as above, and that
  # sub-path is connected by jumps to the previous and following path
  # elements.
  return path_IMP.from_points(pts, mp_trace, mp_jump)
 
def concat(ophs, use_jumps, use_links, mp_jump):
  # 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. The connector
  # is created with {move.connector} with parameters
  # {(omv0,omv1,use_jumps,use_links,mp_jump)}. The argument {mp_jump}
  # may be {None} if all connectors are certain to be links.
  #
  # 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 computing time
  # proportional to the number of paths plus the total number of moves
  # in those paths.
  return path_IMP.concat(ophs, use_jumps, use_links, mp_jump)
 
# ATTRIBUTES

def nelems(oph):
  # Returns the number of moves in the oriented path {oph}.
  return path_IMP.nelems(oph)

def elem(oph, imv):
  # Returns the oriented move with index {imv} in the oriented path {oph=(ph,dr)},
  # counting in the order implied by the orientation.  The index {imv}
  # must be in {0..nmv-1} where {nmv = nelems(oph)}.
  return path_IMP.elem(oph, imv)

# GEOMETRY

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(OPHS):
  # The parameter {OPHS} must be a list or tuple of oriented paths.
  # Returns a bounding box of all moves of all those paths,
  # as a pair of points {(plo, phi)} that its lower left and upper right
  # corners.  If {OPHS} is empty, returns {None}
  #
  # The bounding box is the smallest axis-aligned rectangle that
  # contains all the endpoints of all moves and jumps in all those
  # paths, as well as the starting/ending point of any empty paths. 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(OPHS)

def find(oph, omv):
  # Given an oriented path {oph} and an oriented or unoriented move {omv},
  # returns the index {imv} such that {elem(oph, imv)} is {omv} or its 
  # reverse.  If the move does not occur in {oph}, returns {None}.
  return path_IMP.find(oph, omv)


# ORIENTATION
  
def rev(oph):
  # Returns the reversal of the oriented path {oph}.
  return path_IMP.rev(oph)

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.  The time
  # includes the penalty times for all internal jump/trace transitions,
  # as specified in the {Move_Parms} records of the jumps.
  return path_IMP.extime(oph)

def tini(oph, imv):
  # 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,imv)}, including all acceleration
  # and deceleration times. 
  #
  # This time includes all the trace/jump transition penalties that
  # occur in moves {0..imv-1}, which are counted as adding to the
  # execution time of the jumps. Thus, the result includes a penalty for
  # the transition from move {imv-1} to {imv} if the former is a jump and
  # the latter is a trace -- but not vice-versa.
  #
  # For convenience, if {imv == nelems(oph)}, returns {extime(oph)}.
  return path_IMP.tini(oph, imv)
  
def tfin(oph, imv):
  # 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,imv)}, including all acceleration and deceleration times.
  #
  # This time includes all the trace/jump transition penalties that
  # occur in moves {0..imv}, which are counted as adding to the execution
  # time of the jumps. Thus, the result includes a penalty for the
  # transition from move {imv} to {imv+1} if the former is a jump and the
  # latter is a trace -- not vice-versa.
  #
  # For convenience, if {imv == -1}, returns zero.
  return path_IMP.tfin(oph, imv)

# PLOTTING

def plot_standard(c, OPHS, CLRS, wd_axes, d = None):
  # The argument {OPHS} must be a list or tuple of oriented paths (or
  # plain {Path} objects). The {CLRS} argument must be a list or tuple
  # of {pyx.color.rgb} objects.
  #
  # Plots each path in {OPHS}, displaced by the vector {dp}, using the
  # correspondng color of {CLRS} for the trace sausages.
  #
  # 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 {mv} will have width {move.width(mv)}, reduced a bit for clarity. 
  # The axes
  # of moves (traces or jumps) will be drawn with width {wd_axes}: solid for
  # traces, dashed for jumps. The dots at the endpoints and arrows will be
  # drawn with size proportional to {wd_axes}. The color of these lines and
  # decorations will black for jumps, and darkened version of the sausage color for trace axes.  
  #
  # 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 {OPHS}. 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.
  return path_IMP.plot_standard(c, OPHS, CLRS, wd_axes, d)
