# Tools and types for representing contacts between traces. # Last edited on 2021-10-03 15:15:39 by stolfi 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. # # Acontact {ct} has a /cooling time limit/ attribute, that is the # maximum time (in seconds) that should elapse betweem its first # covering and its closing by the tool-path. # # A {Contact} object also has a mutable /name/ attribute, that is used # for debugging and documentation. It must be either a string or {None}. # 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, omv0, omv1, tclim): # Creates and returns a {Contact} object {ct} with endpoints {(p0,p1)} # whose sides are the oriented moves {omv0} and {omv1} -- which must be # traces,not jumps. The parameter {tclim} is the cooling time limit. return contact_IMP.make(p0, p1, omv0, omv1, tclim) def side_move(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 side_moves(ct): # Returns a tuple with the two (unoriented) {Move} objects # which are the sides of {ct}, namely {(side(ct,0), side(ct,1))}. return contact_IMP.sides(ct) def which_side(omv, ct): # The parameter {omv} must be an unoriented move, and {ct} must be a # {Contact} object. If {omv} (ignoring its orientation) is one of the sides of {ct}, returns the # index (0 or 1) of that side. Otherwise returns {None}. return contact_IMP.which_side(omv, ct) def endpoints(ct): # Returns the endpoints of {ct}, as a pair of points, in no # particular order. return contact_IMP.endpoints(ct) def tcool_limit(ct): # Returns the cooling time limit of contact {ct}. return contact_IMP.tcool_limit(ct) def pmid(ct): # Retuns the midpoint of the contact {ct}. return contact_IMP.pmid(ct) def side_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.side_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(omv0, omv1, szmin, rszmin, tclim): # The parameters {omv0} and {omv1} must be oriented moves with # 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, with cooling time limit {tclim}. 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(omv0, omv1, szmin, rszmin, tclim) def from_move_lists(OMVS0, OMVS1, szmin, rszmin, tclim): # The parameters {OMVS0} and {OMVS1} must be disjoint lists of oriented # moves, without pairwise distinct {Move} objects. The procedure calls {from_moves} # with every pair of moves {omv0} from {OMVS0} and {omv1} from # {OMVS1}, with parametrers {szmin,rszmn,tclim}, and returns a list # (possibly empty) of the {Contact} objects that were created. return contact_IMP.from_move_lists(OMVS0, OMVS1, szmin, rszmin, tclim) def from_paths(oph0, oph1, szmin, rszmin, tclim): # Same as {from_move_lists}, with {OMVS0} and {OMVS1} being the moves # of the oriented paths {oph0} and {oph1}. Assumes that the paths are # proper and disjoint; that is, no {Move} object occurs in both paths, # or twice in the same path. return contact_IMP.from_paths(oph0, oph1, szmin, rszmin, tclim) def from_blocks(bc0, bc1, szmin, rszmin, tclim): # Same as {from_move_lists}, with {OMVS0} and {OMVS1} being # the moves of all choices of {Block} objects {bc0} and {bc1}. return contact_IMP.from_blocks(bc0, bc1, szmin, rszmin, tclim) # COVERAGE BY PATHS def path_tcov(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.fabtime(oph)}. # # For convenience, the procedure returns {None} if {imv} is {None}. # # This procedure executes in {O(1)} time. return contact_IMP.path_tcov(oph, imv, ct, i) def path_ixcovs(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.path_ixcovs(oph, ct) def is_relevant(ct, BCS, ich): # Returns true if and only if at least one side of the contact {ct} is # a trace of at least one {Block} in {BCS}. # # If {ich} is {None}, considers all choices of every block. Otherwise # considers only choice {ich} of each block (if it exists). return contact_IMP.is_relevant(ct, BCS) def path_tcovs(oph, ct): # Returns a pair {tcs} of floats, where {tcs[i] = path_tcov(oph,imv[i],ct,i)} # where {imv[0],imv[1]} are the indices returned by {path_ixcovs(oph, ct)}. # However, if {imv[i]} is {None}, {tcs[i]} is {None} too. # # This procedure executes in {O(nmv)} time because it calls {path_ixcovs}. return contact_IMP.path_tcovs(oph, ct) # PATHS CONTAINING SIDES OF CONTACT # These functions are used to accelerate the algorithms that build a tool-path from # precomputed paths, grouped into blocks. Each contact has two lists of paths, # one for each side. The final tool-path is supposed to include at most one (??? exactly one???) path # from each list. def clear_side_paths(ct): # Resets the list of paths associated with each side of {c} to empty. contact_IMP.clear_side_paths(ct) def add_side_path(ct, i, oph): # Appends the oriented path {oph} to the list of paths associated with side {i} of {ct}. The path {oph} # must contain the {Move} object which is side i (0 or 1) of {ct}, in any orientation. # If that side may be covered also by the reverse of {oph}, the reverse should be added too. contact_IMP.add_side_path(ct, i, oph) def get_side_paths(ct, i): # Returns the list of alternative oriented paths that cover # side {i} (0 or 1) {ct}, as set by {add_side_path}. return contact_IMP.get_side_paths(ct, i) # COOLING TIME ESTIMATORS def est_rcool(oph, ct, mp_jump, quick): # The parameter {oph} must be an oriented path and {ct} must be a # {Contact} object. Returns a lower bound for the cooling time ratio # of {ct} for any path that begins with {oph} and closes the contact. # # Namely, if {oph} covers both sides of {ct}, returns the cooling time of # {ct} for that path, divided by {tcool_limit(ct)}. # # If the path {oph} covers exactly one side of {ct}, side {i}, returns # a lower bound for {max_rcool(P,ct)} over any path {P} that begins # with {oph} and eventually covers side {1-i} by including some path # {oph1} in {get_side_paths(ct,1-i)}. # # The above estimate considers how much fabtime elapsed between the # first coverage of {ct} and the end of {oph}, plus the minimum # fabtime that would be neeed to go from the end of {oph} to the start # of {oph1}, plus plus the time needed to fabricate {oph1} up to the # midpoint of {ct}; the sum divided by {tcool_limit(ct)}. The gap # between {oph} and {op1} will assume the link paths associated with # {oph0,oph1} if available, otherwise it will assume a jump with # parameters {mp_jump} # # The above estimate uses the path {oph1} in {get_side_paths(ct,1-i)} # that gives the minumum cooling time. The procedure fails if no paths # were associated to that side. # # In either case, if {tcool_limit(ct)} is {+inf}, returns 0. # # If the path {oph} does not cover any side of {ct}, returns {-inf}. # # In any case, if the result is greater than 1, any path that starts # with {oph} (using a choice of {bc}, if needed, to close {ct}) will # be invalid, because it will exceed the cooling time of that contact. # # The above description applies if {quick} is false. If {quick} is # true, the procedure may prematurely return {+inf} as soon as it # determines that the result would be greater than 1. return contact_IMP.est_rcool(oph, ct, mp_jump, quick) def est_max_rcool(oph, CTS, mp_jump, quick): # The parameter {oph} must be an oriented path, {CTS} must be a list # or tuple of {Contact} objects. Returns the maximum value of # {est_rcool(oph,ct,mp_jump,quick)} over every contact {ct} of {CTS}. # # If the list is empty, returns {-inf}. # # All contacts in {CTS} must have at least one side covered by {oph}, # and must be closed by {oph} plus some of the paths associated with {ct}. # Specificaly, if {oph} does not cover side {i} of {ct}, it must cover # side {1-i}; and {get_side_paths(ct,i)} must be non-empty. # # In any case, if the result is greater than 1, any path that starts # with {oph} and closes all contacts of {CTS} with the paths # associated to them, will be invalid, because it will exceed the # cooling time of at least one closed or partially covered contact. # # The above applies if {quick} is false. If {quick} is true, the # procedure may prematurely return {+inf} if it determines that the # result would be greater than 1. return contact_IMP.est_max_rcool(oph, CTS, mp_jump, quick) # PLOTTING def plot_to_files(fname, CTS, clr, OPHS, CLRS, rwd, 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}. Each trace is drawn as a sausage of width {rwd*wd} # where {wd} is the nominal width. # # 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, rwd, wd_axes, tics, arrows) def plot_single(c, ct, dp, clr, wd, sz_tic, arrow): # Plots the contact {ct} on the {pyx} context {c}, as a solid line # segment of width {wd} 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, sz_tic, arrow) def plot_link_elis(c, oph, clr, wd): # ??? contact_IMP.plot_link_elis(c, oph, clr, wd) # PRINTING AND DEBUGGING def get_name(ct): # Given a {Contact} object {ct}, returns its name attribute, if not {None}. # If that attribute is {None}, returns "C?" instead. return contact_IMP.get_name(ct) def set_name(ct, name): # Saves the string {name} as the name attrbute of the {Contact} object {ct}. contact_IMP.set_name(ct, name) def tag_names(CTS, tag): # Prepends the string {tag} (if not {None}) to the names of all # {Contact} objects in {CTS}. The objects had better be all distinct. contact_IMP.tag_names(CTS, tag) def show(wr, ct, ind, wna): # Writes {ct} on {wr} in a human-readable format. # # The output has the name and endpoints of the contact, # then the names of the two traces which are # the sides of the contact. # # The name is padded to {wna} columns, left-aligned. # Every line is indented by {2*ind} blanks. contact_IMP.show(wr, ct, ind, wna) def show_list(wr, CTS, ind): # Writes to file {wr} a readable summary description of the contacts in the # list {CTS}. The list has a header and one contact per line, # produced by {show}, prefixed by with {2*ind} spaces and its index in {CTS}. contact_IMP.show_list(wr, CTS, ind)