# Tools and types for representing contacts between traces.
# Last edited on 2021-03-21 16:01:38 by jstolfi

import contact_IMP; from contact_IMP import Contact_IMP
import move
import move_parms
import path
import pyx

class Contact(Contact_IMP):
  # An object of the {Contact} class represents a /contact/, a line
  # segment (possibly a single point) on the boundary of two distinct
  # traces, that must become a physical weld after both are extruded.
  # These traces are the /sides/ of the contact, arbitrarily indexed 0
  # and 1.
  #
  # A {Contact} object {ct} also has two /endpoints/. Their order is
  # arbitrary. Each endpoint is a list of 2 float coordinates, in
  # millimeters.
  #
  # A contact {ct} is /covered/ by a move {mv} if {mv} is one of its 
  # sides.  
  #
  # Acontact {ct} is /covered/ a path {ph} if at least one of its sides
  # is an element of {ph}, and is /closed/ by {ph} if both sides are.
  pass
  

# All the procedures below that take an oriented path as parameter will
# also accept an unoriented {Path} object, which is taken in its native
# orientation.

def make(p0, p1, mv0, mv1, bc0, bc1, rl0, rl1):
  # Creates and returns a {Contact} object {ct} with endpoints {(p0,p1)}
  # whose sides are the traces {mv0} and {mv1} -- which must be (unoriented)
  # {move.Move} objects, and should not be jumps.
  return contact_IMP.make(p0, p1, mv0, mv1, bc0, bc1, rl0, rl1)

def endpoints(ct):
  # Returns the endpoints of {ct}, as a pair of points, in no
  # particular order.
  return contact_IMP.endpoints(ct)

def pmid(ct):
  # Retuns the midpoint of the contact {ct}.
  return contact_IMP.pmid(ct)

def get_raster_link(ct):
  return contact_IMP.get_raster_link(ct)

def side_block(ct, i) :
  return contact_IMP.side_block(ct, i)

def side(ct, i):
  # The parameter {i} must be 0 or 1. Returns the (unoriented) {Move}
  # object {mv} that is the trace on side {i} of the contact.
  return contact_IMP.side(ct, i)

def tcov(ct, i):
  # The parameter {i} must be 0 or 1. Returns the (precomputed) time
  # that the nozzle will take to move from the starting point of the move
  # {side(ct,i)} to the point on the move axis that is closest to
  # {pmid(ct)}.
  return contact_IMP.tcov(ct, i)

def which_side(mv, ct):
  # The parameter {mv} must be a {Move} object, and {ct} must be a
  # {Contact} object. If {mv} is one of the sides of {ct}, returns the
  # index (0 or 1) of that side. Otherwise returns {None}.
  return contact_IMP.which_side(mv, ct)

# COVERAGE BY PATHS

def covindices(oph, ct):
  # Returns a pair {ixs} of indices such that the move
  # {path.elem(oph,ixs[i])} is {side(ct,i)}, in either orientation. If the
  # move {side(ct,i)} does not occur in {oph}, {ixs[i]} is {None}.
  #
  # Note that the indices are usually different for {oph} and for
  # {path.rev(oph)}. In fact, they are complemented relative to {nmv-1},
  # where {nmv} is the number of moves in the path.
  #
  # This procedure executes in {O(nmv}) time.
  return contact_IMP.covindices(oph, ct)

def covtime(oph, imv, ct, i):
  # The parameter {ct} must be a {Contact} object, {oph} must be an
  # oriented path, and {imv} must be the index such that {side(ct,i)} is
  # {path.elem(oph,imv} or its reverse. 
  #
  # The procedure returns the time {tcs} when the nozzle executes that
  # move, in the direction specified in the path, and and passes next to
  # the midpoint {m} of the contact {ct} -- specifically, on the point
  # that is closest to {m} on the axis of that trace.
  #
  # The time is counted from the beginning of the execution of the
  # oriented path {oph}, assuming that it is executed in the direction
  # specified.
  # 
  # Note that the resultis usually different for {oph} and for
  # {path.rev(oph)}. In fact, the two times are complementary relative
  # to {path.extime(oph)}.
  #
  # For convenience, the procedure returns {None} if {imv} is {None}.
  #
  # This procedure executes in {O(1)} time.
  return contact_IMP.covtime(oph, imv, ct, i)

def covtimes(oph, ct):
  # Returns a pair {tcs} of floats, where {tcs[i] = covtime(oph,imv[i],ct,i)}
  # where {imv[0],imv[1]} are the indices returned by {covindices(oph, ct)}.
  # However, if {imv[i]} is {None}, {tcs[i]} is {None} too.
  #
  # This procedure executes in {O(nmv)} time because it calls {covindices}.
  return contact_IMP.covtimes(oph, ct)

def tcool(oph, ct):
  # The parameter {oph} must be an oriented path and {ct} must be a
  # {Contact} object. Returns the cooling time of the contact {ct} if
  # the slice were to be executed using the path {oph}; or {None} if
  # that path does not cover both sides of {ct}
  #
  # Namely, let {covtimes(oph, ct)} be {(t0,t1)}. If either of those
  # times is {None}, returns {None}. Otherwise returns {abs(t0-t1)}.
  # Note that the result is the same for {oph} and for {path.rev(oph)}.
  #
  # This procedure executes in time {O(nmv)} where {nmv} is {path.nelems(oph)},
  # because it calls {covindices}.
  return contact_IMP.tcool(oph, ct)
  
def pair_tcool(oph0, tconn, oph1, ct):
  # Sides 0 and 1 of the {Contact} object {ct} must be in the oriented
  # paths {oph0} and {oph1}, respectively. Returns {tcool(oph,ct)} where
  # {oph} is the hypothetical concatenation of {oph0} and {oph1} with a
  # connector whose execution time (including any applicable trace/path
  # transition penalties at the ends) is {tconn}.
  #
  # This procedure saves computing time because it does not actually
  # construct the path {oph}. However, it only considers the case of
  # {oph0} being added to the path before {oph1}.
  #
  # This procedure executes in time {O(nmv0+nmv1)}, where {nmv0} and {nmv1}
  # are the number of moves in the two paths.
  return contact_IMP.pair_tcool(oph0, tconn, oph1, ct)
  
# TOOLS FOR LISTS OF CONTACTS

def max_tcool(oph, CTS):
  # The parameter {oph} must be an oriented path and {CTS} must be a
  # list or tuple of {Contact} objects. Returns the maximum value of
  # {tcool(oph,ct)} over every contact {ct} of {CTS} that has been
  # closed by path {oph}.
  #
  # If there are no such contacts, returns {-inf}. The result will be
  # the same for {oph} and for {path.rev(oph)}.
  #
  # This procedure runs in time {nct*nmv) where {nct} is the number
  # of contacts in {CTS} and {nmv} is the number of moves in {oph},
  # because it calls {path.find_move} twice for each contact.
  return contact_IMP.max_tcool(oph, CTS)

def pair_max_tcool(oph0, tconn, oph1, CTS):
  # Equivalent to {max_tcool(oph, CTS)} where {oph} is the hypothetical
  # concatenation of {oph0} and {oph1} with a connector whose execution
  # time (including any applicable trace/path transition penalties at
  # the ends) is {tconn}.
  # 
  # The result is the maximum value of {pair_tcool(oph0,tconn,oph1,ct)}
  # over every contact {ct} of {CTS} that would be closed by path {oph}.
  #
  # This procedure saves time because it does not actually construct the
  # path {oph}. However, if side 0 of a contact in {CTS} is covered by
  # {oph}, it must be covered by {oph0} only; and if side 1 is covered
  # by {oph}, it must be covered by {oph1} only.
  # 
  # This procedure runs in time {nct*(nmv0+nmv1)) where {nct} is the number
  # of contacts in {CTS} and {nmv0,nm1} are the move counts of {oph0,oph1},
  # because it calls {path.find_move} once for each contact and path.
  return contact_IMP.pair_max_tcool(oph0, tconn, oph1, CTS)

def min_tcov(oph, CTS):
  # The parameter {oph} must be an oriented path and {CTS} must be a
  # list or tuple of {Contact} objects. Returns the minimum of
  # {tcov(oph,imv,ct,i)} over every contact {ct} of {CTS} that has {side(ct,i)}
  # in {oph} and {side(cr,1-i)} not in {oph}, for {i=0} or {i=1}.
  # 
  # If there are no such contacts, returns {+inf}.
  # 
  # Note that the results for {oph} and for {path.rev(oph)} are usually
  # different, and may NOT be complementary relative to
  # {path.extime(oph)}(since they may be realized by different
  # contacts).  
  #
  # This procedure takes time {O(nct*nmv}) where {nct} is the number
  # of contacts and {nmv} is the move count of {oph}, because it calls
  # {path.find} twice for each contact.
  return contact_IMP.min_tcov(oph, CTS)

# PLOTTING

def plot(c, ct, dp, clr, wd_line, sz_tic, arrow):
  # Plots the contact {ct} on the {pyx} context {c}, as a solid line
  # segment of width {wd_line} and color {clr}, with round caps. The plot
  # will be displaced by the vector {dp} (a 2-tuple of floats).
  #
  # If the float {sz_tic} is positive, draws a short tic perpendicular
  # to the contact line at its midpoint.  However, if the boolean {arrow} is true, 
  # plots a short arrowhead pointing from side 0 to side 1 instead of 
  # the tic.
  contact_IMP.plot(c, ct, dp, clr, wd_line, sz_tic, arrow)

def plot_link(c, oph, clr, wd):
  # Plots the contact {ct} on the {pyx} context {c}, as a solid line
  # segment of width {wd_line} and color {clr}, with round caps. The plot
  # will be displaced by the vector {dp} (a 2-tuple of floats).
  #
  # If the float {sz_tic} is positive, draws a short tic perpendicular
  # to the contact line at its midpoint.  However, if the boolean {arrow} is true, 
  # plots a short arrowhead pointing from side 0 to side 1 instead of 
  # the tic.
  contact_IMP.plot_link(c, oph, clr, wd)

