# Tools and types for representing contacts between traces. # Last edited on 2021-10-31 05:27:05 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, positive number # that is the maximum time (in seconds) that should elapse betweem its # first covering and its closing by the tool-path. The default for new contacts is # {+inf}, meaning that there is no limit on the cooling time. # # 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): # 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 cooling time limit is set to {+inf} (no limit). # # The contact's name will be "C({na0}:{na1})" where {na0} and {na1} # are the names of the two moves, if they are both defined; otherwise # it will be {None}. return contact_IMP.make(p0, p1, omv0, omv1) def side_move(ct, isd): # The parameter {isd} must be 0 or 1. Returns the (unoriented) {Move} # object {mv} that is the trace on side {isd} of the contact. return contact_IMP.side_move(ct, isd) 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.side_moves(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 pmid(ct): # Retuns the midpoint of the contact {ct}. return contact_IMP.pmid(ct) def side_tcov(ct, isd): # The parameter {isd} 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,isd)} to the point on the move axis that is closest to # {pmid(ct)}. return contact_IMP.side_tcov(ct, isd) 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, mvdir, szmin, rszmin, tol): # The parameters {omv0} and {omv1} must be oriented moves. If they are # both traces with distinct {Move} objects, are approximately parallel # to the unit vector {mvdir}, and share a sufficient length of common # border, the procedure returns a contact between them spanning that # shared segment. Otherwise it returns {None}. The nominal traces # ("fat segments") of the two moves may overlap or be separated by up # to {tol}. The orientations of the moves are irrelevant. # # The procedure uses {move.shared_border(omv0,omv1,mvdir,tol)} to check the # geometry constraints and compute the shared border. The # {mvdir} parameter may be {None} to mean the approximate # mean direction of the two moves. # # The contact is created only if its length (in mm) is # at least {szmin}. 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, mvdir, szmin, rszmin, tol) def from_move_lists(OMVS0, OMVS1, szmin, rszmin, tol, ydir): # Returns a list (possibly empty) {CTS} of {Contact} # objects between moves in the lists {OMVS0} and {OMVS1}. # # The parameters {OMVS0} and {OMVS1} must be lists of oriented moves. # The procedure calls {from_moves(mv0,mv1,None,szmin,rszmin)} with every # pair of distinct {Move} objects {mv0} from {OMVS0} and {mv1} from # {OMVS1}. # # Side 0 of each contact will be the move whose midpoint is lower in # the direction {ydir}. If {ydir} is {None}, it defaults to {(0,1)} # (the Y coordinate). # # If two moves {mv0} and {mv1} appear in both lists, only one contact # will be created for the pair. return contact_IMP.from_move_lists(OMVS0, OMVS1, szmin, rszmin, tol, ydir) def from_paths(oph0, oph1, szmin, rszmin, tol, ydir): # Same as {from_move_lists}, with {OMVS0} and {OMVS1} being the moves # of the oriented paths {oph0} and {oph1}, respectively. # # The procedue also adds any contact created {ct} to the contact sets # of {oph0} and {oph1}, with {path.add_contact}; and adds {oph0} and # {oph1} to the side paths sets of {ct}, with {add_side_path} -- # whenever the constact lies on those paths. Note that these # connections will not exclude pre-existing ones. return contact_IMP.from_paths(oph0, oph1, szmin, rszmin, tol, ydir) def from_path_lists(OPHS0, OPHS1, szmin, rszmin, tol, ydir): # The parameters {OPHS0} and {OPHS1} must be lists of oriented paths. # Same as {from_move_lists}, with {OMVS0} and {OMVS1} being the moves in all paths # of {OPHS0} and {OPHS1}, respectively. # # The proceure also adds all contact-path interactions in with # {path.add_contact} and {add_side_path}. Note that these connections # will not exclude pre-existing ones. return contact_IMP.from_path_lists(OPHS0, OPHS1, szmin, rszmin, tol, ydir) def from_blocks(bc0, bc1, szmin, rszmin, tol, ydir): # Same as {from_path_lists}, with {OPHS0} and {OPHS1} being # all choices of {Block} objects {bc0} and {bc1}, respectively. # # The proceure also adds all contact-path interactions in with # {path.add_contact} and {add_side_path}. Note that these connections # will not exclude pre-existing ones. return contact_IMP.from_blocks(bc0, bc1, szmin, rszmin, tol, ydir) # COVERAGE BY PATHS def path_tcov(oph, imv, ct, isd): # The parameter {ct} must be a {Contact} object, {oph} must be an # oriented path, and {imv} must be the index such that {side(ct,isd)} is # the move {path.elem(oph,imv} or its reverse. # # The procedure returns the time {tcs} when the nozzle fabricates 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 fabricated 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, isd) def path_ixcovs(oph, ct): # Returns a pair {ixs} of indices such that the move # {path.elem(oph,ixs[isd])} is {side(ct,isd)}, in either orientation. If the # move {side(ct,isd)} does not occur in {oph}, {ixs[isd]} 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. # # NOTE: this procedure should not be used if the path-contact links # {contact.get_side_paths(ct,isd)} have been set, snce those links # provide the indices of the moves in {O(1)} 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[isd] = path_tcov(oph,imv[isd],ct,isd)} # where {imv[0],imv[1]} are the indices returned by {path_ixcovs(oph, ct)}. # However, if {imv[isd]} is {None}, {tcs[isd]} is {None} too. # # This procedure executes in {O(nmv)} time because it calls {path_ixcovs}. return contact_IMP.path_tcovs(oph, ct) # PATH-CONTACT GRAPH # Each side of a contact has two sets of paths that use the # moves that are the sides of that contact. # # More precisely, for any contact {ct}, the two sets are {PS[isd]=get_side_paths(ct,isd)} for # {isd} in {0..1}. Each element of {PS[isd]} is a triple # {(ph, dr, imv)} such that {oph=(ph,dr)} is an oriented path # and {imv} is an index such that {path.elem(oph,imv)} is the move that is # side {isd} of {ct}, in either orientation. # # If a set contains a triple {(ph,dr,imv)} it may or may not contain # also the triple that refers to the reverse of that pat, namely {(ph, # 1-dr, n-1-imv)}. Note that, since all the paths in {PS[isd]} share the # same {Move} object, the final tool-path may include at most one path # from each set. # # This information should be consistent with that provided by # {path.get_contacts}. Namely, for every oriented path {oph} and every # contact {ct} of interest, the path {oph} or its reverse must be in # {contact.get_side_paths(ct,isd)} if and only if the contact {ct} is in # {path.get_contacts(oph,isd)} def clear_side_paths(ct, isd): # Resets the set of paths associated with side {isd} of {ct} to empty. contact_IMP.clear_side_paths(ct, isd) def add_side_path(ct, isd, oph, imv): # Adds the oriented path {oph} to the set of paths associated with side {isd} of {ct}. The path {oph} # must contain the {Move} object {mv} which is side {isd} (0 or 1) of {ct}, in any orientation. # # if {imv} is not {None}, it should be the index of the move {mv} in {oph}, and the operation has computing cost {O(1)}. Otherwise # the procedure will compute the index with {path.find_move}, which may have cost proportional to the number of moves in the path. contact_IMP.add_side_path(ct, isd, oph, imv) def get_side_paths(ct, isd): # Returns a copy of the set of triples {(ph,dr,imv)} that specify the # oriented paths of interest that cover side {isd} (0 or 1) {ct}, as # created by {add_side_path}. return contact_IMP.get_side_paths(ct, isd) def check_side_paths(CTS,OPHS,ydir): # Given a list of contacts {CTS} and a list of raster paths {OPHS}, # checks whether the contact information provided by {get_side_paths} # and {path.get_contacts} are consistent. # # In particular, checks whether the set of all paths returned by # {get_side_paths} is {OPHS}, and all contacts returned # by {path.get_contacts} are in {CTS}. # # If {ydir} is not {None}, its should be a unit 2-vector. The # procedure then also checks whether the two sides of each contact are # ordered by increasing coordinate of their midpoints along {ydir}. return contact_IMP.check_side_paths(CTS,OPHS,ydir) # COOLING TIME def tcool_limit(ct): # Returns the cooling time limit of contact {ct}. return contact_IMP.tcool_limit(ct) def set_tcool_limit(ct, tclim): # Sets the cooling time limit of contact {ct} to {tclim}. contact_IMP.set_tcool_limit(ct, tclim) def tcool(oph, ct): # The parameter {oph} must be an oriented path and {ct} must be a # {Contact} object, which is closed by {oph} (meaning that # the move on each side of {ct}, or its reverse, occurs in {oph}). # # Returns cooling time of {ct} in that path -- namely the fabtime of the # part of {oph} between the the covering of the two contacts. # # If the path {oph} does not close {ct}, returns {None}. return contact_IMP.tcool(oph, ct) 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. # # In any case, if {tcool_limit(ct)} is {+inf}, returns 0. # # Otherwise, if {oph} covers both sides of {ct}, returns the cooling # time {tcool(oph,ct)} of {ct} for that path, divided by # {tcool_limit(ct)}. # # Othwerwise, if the path {oph} covers exactly one side of {ct}, side # {isd}, returns a lower bound for {max_rcool(P,ct)} over any path {P} # that begins with {oph} and eventually covers side {1-isd} by including # some path {oph1} in {get_side_paths(ct,1-isd)}. # # 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 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-isd)} # that gives the minumum cooling time. The procedure fails if no paths # were associated to that side. # # If the path {oph} does not cover any side of {ct}, returns 0. # # 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, or no contact of {CTS} is partially or totally # covered bt {oph}, returns {0}. # # 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, dashpat, ext, 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}, dash # patterm {dashpat}, and extension amnount {ext}. Will draw tics at # the contacts' midpoints if {tics} is true, or arrows if {arrows} is # true. (See {plot_single} for details.) # # 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, dashpat, ext, OPHS, CLRS, rwd, wd_axes, tics, arrows) def plot_single(c, ct, dp, clr, dashpat, ext, 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 the nominal endpoints, with round caps. The plot # will be displaced by the vector {dp} (a 2-tuple of floats). # # If {dashpat} is {None} the line will be solid, else {dashpat} should # be a dashing pattern spec as for {hacks.plot_line}. If {ext} is # nonzero, the contact endpoints will be displaced out by {ext}. # # 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, dashpat, ext, wd, sz_tic, arrow) # PRINTING AND DEBUGGING def has_name(ct): # Returns {True} if and only {ct}'s name atribute is not {None}. return contact_IMP.has_name(ct) 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 compare(ct0, ct1, tol, die): # Checks if the contacts {ct0} and {ct1} have the same endpoints, and # if the moves that are their sides are coincident traces, apart from # differences of at most {tol} in each coordinate. # # Does NOT compare the names and cooling time limits, or the {MoveParms} records # of the two moves. Also does not compare the paths associated by # {get_side_paths}. # # If the contacts coincide, returns {True}, and {die} is irrelevant. # If they don't coincide, retuns {False} if {die} is false, and # bombs out if {die} is true. return contact_IMP.compare(ct0, ct1, tol, die) def show(wr, pref, ct, suff, wna): # Writes {ct} on {wr} in a human-readable format. # # The output has the name and endpoints of the contact, the cooling # time limit, and the moves and relevant paths that are the sides of # the contact. # # Namely, for {isd} in {0,1}, the output will show name of the {Move} # object that is side {isd} of the contact, and (between "@{" and "}") # the names of the paths attached to the contact that are supposed to # contain that move. The index {imv} of the {Move} in each path is # also shown as "[{imv}]" or "[~{imv}]" depending on whether it is # used in its native or reversed orientation. # # The name of the contact is padded to {wna} columns, left-aligned. . # The whole description uses one line, is preceded by {pref} (if not # {None}, and followed by {suff} (if not {None}). It is NOT terminated # by a newline, unless provided by {suff}. contact_IMP.show(wr, pref, ct, suff, wna) def show_list(wr, pref, CTS, suff): # Writes to file {wr} a readable summary description of the contacts # in the list {CTS}. # # The output consists of a header and then the description of each # contact, one per line, produced by {show}. Each line is prefixed by # {pref} and the index of the contact in {CTS}, and followed by {suff} # and a newline. contact_IMP.show_list(wr, pref, CTS, suff)