# 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): # 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) 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 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) 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 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 bbox(CTS): # The parameter {CTS} must be a list of {Contact} objects. Returns a # bounding box for those contacts, 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 contacts. Note that # decorations such as tics and arrowheads, as well as bits of the # contact line itself, may extend outside the box. Moreover, the # traces that are the sides of those contacts are NOT included in the # result. return contact_IMP.bbox(CTS) # AUTOMATIC CREATION def from_moves(mv0, mv1, szmin, rszmin): # The parameters {mv0} and {mv1} must be distinct {Move} # objects. If they are both traces and share a sufficient length of common border, # within some tolerance, the procedure returns a contact between them # spanning that shared segment. Otherwise it returns {None}. # # If {szmin} is positive, the contact is created only if its length is # at least {szmin} millimeters. If {rszmin} is positive, the contact # is created only if its length is at least {rszmin*L} where {L} is # the length of the shortest of the two traces. return contact_IMP.from_moves(mv0, mv1, szmin, rszmin) def from_move_lists(MVS0, MVS1, szmin, rszmin): # The parameters {MVS0} and {MVS1} must be disjoint lists of {Move} # objects, without duplicates. The procedure calls {from_moves} # with every pair of moves {mv0} from {MVS0} and {mv1} from # {MVS1}, with parametrers {szmin,rszmn}, and returns a list # of the {Contact} objects that were created. return contact_IMP.from_move_lists(MVS0, MVS1, szmin, rszmin) def from_paths(oph0, oph1, szmin, rszmin): # Same as {from_move_lists}, with {MVS0} and {MVS1} being # the moves of the oriented paths {oph0} and {oph1}. return contact_IMP.from_paths(oph0, oph1, szmin, rszmin) # COVERAGE BY PATHS 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 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 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_to_files(fname, CTS, clr, OPHS, CLRS, wd_axes, tics, arrows): # The arguments {CTS}, {OPHS}, and {CLRS} must be lists or tuples of # {Contact} objects, oriented paths (or plain {path.Path} objects), # and {pyx.color.rgb} objects, respectively. # # Plots each path in {OPHS} using using some default sytle and the # correspondng color of {CLRS} for the trace sausages, over a # millimeter grid. Trace axes and jump lines will be drawn with line # width {wd_axes}. # # Then draws each contact from {CTS} using the color {clr}. Will draw # tics at the contacts' midpoints if {tics} is true, or arrows if # {arrows} is true. # # Then writes the plot to files "{fname}.{ext}" where {ext} is "eps", # "png", and "jpg". # # Typically, both sides of every contact in {CTS} should be traces of # paths in {OPHS}. # # 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}. contact_IMP.plot_to_files(fname, CTS, clr, OPHS, CLRS, wd_axes, tics, arrows) def plot_single(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_single(c, ct, dp, clr, wd_line, sz_tic, arrow) # PRINTING AND DEBUGGING def show(wr, ct): # Writes {ct} on {wr} in a human-readable format. contact_IMP.show(wr, ct)