# Implementation of the module {txt_read}. # Last edited on 2021-06-04 14:16:59 by jstolfi import txt_read import block import block_hp import move import move_parms import move_hp import path import path_hp import contact import contact_hp import hacks import rn import sys from math import sqrt, sin, cos, atan2, log, exp, floor, ceil, inf, nan, pi def read(rd, mp_cont, mp_fill, angle, shift): debug = False Z = None # {Z} coordinate of slice. OPHS = None # Filling elements (single-raster paths) indexed by {ife}. PTSS_cont = None # List of lists of vertices of contours. CTS = [] # List of contacts. wdf = move_parms.width(mp_fill) # Expected spacing of rasters. xdir = (cos(angle), sin(angle)) # Dir vector parallel to rasters, "left" to "right". ydir = (-xdir[1], xdir[0]) # Dir vector perpendicular to rasters, "up". def unrotate(xp, yp): # Given the {float} coordinates {xp,yp} of a point as read from the file, # return a point (pair of {float}s) whith those coordinates rotated by {-angle} radians, # using the unit vectors {xdir,ydir} and the displacement {shift}. xr = rn.dot((xp,yp),xdir) + shift[0] yr = rn.dot((xp,yp),ydir) + shift[1] return (xr,yr) # ...................................................................... def parse_link(vstr): # Takes a list of vertices of a link, encoded as a string {vstr}, # and returns them a list of points, un-rotated and shifted. # The points should be separated by ';', and each point should be two floats separated # by '&'. Alternatively {verts} may be the string 'None' to indicate that there # is no link between the two elements; in which case the procedure returns {None}. # if 'None' in verts: return None # Parse vertices of link path and collect them in the list {VTS}: aux = verts.split(';') VTS = [] # Vertices of link. for ipt in range(len(aux)): ps = aux[ipt].split('&') VTS.append(unrotate(float(ps[0]), float(ps[1])))) return VTS # ...................................................................... nread = 0 for line in rd: nread += 1 line = line.rstrip() if debug: sys.stderr.write("%08d %s\n" % (nread, line)) code = line[0] rest = line[1:].replace('\n', '') if code == 'Z': # {Z} coordinate of slice. Z = float(rest) elif code == 'K': # Number of contours. ncr = int(rest) PTSS_cont = [] for icr in range(ncr): PTSS_cont.append([]) elif code == 'N': # Number of filling raster elements. nfe = int(rest) OPHS = [None]*nfe elif code == 'C': # Vertex of a contour: line = line.replace('\n', '') c = rest.split(',') icr = int(c[0]) # Contour index. assert icr >= 0 and icr < ncr p = unrotate(float(c[1]), float(c[2])) # Vertex coordinates. PTSS_cont[icr].append(p) elif code == 'R': # Raster element. r = rest.split(",") ife = int(r[0]) # Index of raster element. assert ife >= 0 and ife < nfe p = unroatet(float(r[1]), float(r[2])) # One endpoint. q = unrotate(float(r[3]), float(r[4])) # The other endpoint. dr = int(r[5]) # Direction bit. igr = int(r[6]) # Group index. oph = create_single_raster_path(ife, p, q, dr, mp_fill, igr) # Save the element in {OPHS} indexed by {ife}: OPHS[ife] = oph elif code == 'L': # Contact and possibly pair of links between two raster elements l = rest.split(",") # Get the indices of the two rasters connected by the links ife0 = int(l[0].replace('L', '')); oph0 = OPHS[ife0] # First filing element. assert ife0 >= 0 and ife0 < nfe ife1 = int(l[1].replace('L', '')); oph1 = OPHS[ife1] # Second filing element. assert ife1 >= 0 and ife1 < nfe # Checks that the two rasters are in adjacent scan-lines, in proper order: check_consecutive_scan_lines(oph0, oph1, wdf) # Create contact and attach it to the paths: ct = create_and_attach_contact(ife0, oph0, ife1, oph1) CTS.append(ct) # Get the two links and attack them to the filling elements: VTS0 = parse_link(l[2]) # Vertices of first link, or {None}. create_and_attach_link(VTS0, oph0, oph1, mp_fill) VTS1 = parse_link(l[3]) # Vertices of second link, or {None}. create_and_attach_link(verts1, oph0, oph1, mp_fill) # Assemble the contours: OCRS = [] # Contours. for PTS in PTSS_cont: if PTS != None and len(PTS) != 0: assert len(PTS) >= 3 ocr = path.from_points(PTS + [PTS[0]], mp_cont, None) OCRS.append(ocr) ncr = len(OCRS) nfe = len(OPHS) nct = len(CTS) sys.stderr.write("read %d lines: %d contours, %d rasters, %d contacts\n" % (nread,ncr,nfe,nct)) path.compute_contour_nesting(OCRS) return OCRS, OPHS, CTS, Z # ---------------------------------------------------------------------- def create_single_raster_path(ife, p, q, dr, mp_fill, igr): # Creates a path with a single raster trace. The trace wiil have # endpoints {p,q} and parameters {mp_fill}. The direction will be from # {p} to {q} if {dr} is zero, and reversed otherwise. # # The line {p--q} must be horizontal, apart from roundoff errors. # The raster endpoints will be precisely horizontal. # # The link fields used by {path_hp.get_links} are cleared with # {path_hp.clear_links}. The contact information used by # {path_hp.get_contacts} is cleared with {path_hp.clear_contacts}. The # move's name is set to "R{ife}" and the path's name to "P{ife}". # # Also sets the group index of the move to {igr} with # {move_hp.set_group}. # p_adj, q_adj = check_and_fix_raster_direction(p, q) if dr == 0: mv = move.make(p_adj, q_adj, mp_fill) else: mv = move.make(q_adj, p_adj, mp_fill) move.set_name(mv, "R%d" % ife) ph = path.from_moves((mv,)) path.set_name(ph, "R%d" % ife) path_hp.clear_links(ph) path_hp.clear_contacts(ph) path_hp.set_group(ph, igr) return ph # ---------------------------------------------------------------------- def check_and_fix_raster_direction(p, q): # Checks that the segment {p,q} is horizontal apart from roundoff # error, with {p} to the left of {q}. Returns the coordinates of {p} # and {q} adjusted so that they are precisely horizontal. assert abs(p[1] - q[1]) <= 0.0005 assert p[0] < q[0] ym = (p[1] + q[1])/2 # Mean {Y} coordinate. # sys.stderr.write(" p = ( %20.16f %20.16f )\n" % (p[0],p[1])) # sys.stderr.write(" q = ( %20.16f %20.16f )\n" % (q[0],q[1])) p_adj = (p[0], ym) q_adj = (q[0], ym) # sys.stderr.write(" p_adj = ( %20.16f %20.16f )\n" % (p_adj[0],p_adj[1])) # sys.stderr.write(" q_adj = ( %20.16f %20.16f )\n" % (q_adj[0],q_adj[1])) return p_adj, q_adj # ---------------------------------------------------------------------- def create_and_attach_link(VTS, oph0, oph1, mp_link): # The parameters {oph0} and {oph1} must be oriented paths that # represent adjacent filling elements. The {VTS} parameter should be # the list of vertices of a link path connecting two endpoins, or {None} # # If {VTS} is {None}, the procedure does nothing. If {VS} is not {None}, the procedure # makes a path {link} from those points, whose traces have parameters # {mp_link}. It then attaches {link} to the lists of links of {oph0} # and {oph1}, using {path_hp.add_link} with the proper orientations. # It assumes that the link lists of both paths have been initialized # previously with {path_hp.clear_links}. # Snap vertices of the link path to the raster element endpoints: tol = 0.05 # Snap vertices that are this close to the endpoint (in mm). for oph in oph0, oph1: for t in path.pini(oph), path.pfin(oph): for ipt in range(len(VTS)): if hacks.same_point(VTS[ipt], t, 0.05): VTS[ipt] = t # Create the moves of the link path: TRS = [] # Traces of link. for ipt in range(len(VTS) - 1): p = VTS[ipt] q = VTS[ipt + 1] if p != q: mv = move.make(p, q, mp_link) TRS.append(mv) assert len(TRS) > 0, "zero-length link" link = path.from_moves(TRS) # Attach the link to the paths: nat = 0 # Number of ends that were attached. for oph in oph0, path.rev(oph0), oph1, path.rev(oph1): for olk in link, path.rev(link): if path.pfin(olk) == path.pini(oph): path_hp.add_link(oph, olk) nat += 1 assert nat >= 2, "link path does not connect to raster(s)" assert nat <= 2, "link path connects to more than 2 raster ends" return # ---------------------------------------------------------------------- def create_and_attach_contact(ife0, oph0, ife1, oph1): # Assumes that the oriented paths {oph0} and {oph1} are sngle-trace # filling elements with indices {ief0} and {ife1}, in consecutive scan # lines, with {oph0} "below" {oph1}. # # If the two traces have a non-trivial shared border, creates a # {Contact} object between them, and attaches it to the two paths with # {path_hp.add_contact}. assert path.nelems(oph0) == 1 assert path.nelems(oph1) == 1 mv0, dr0 = move.unpack(path.elem(oph0,0)) mv1, dr1 = move.unpack(path.elem(oph1,0)) ct = contact.from_moves(mv0, mv1, 0.1, 0.05) if ct != None: contact.set_name(ct, "C%d:%d" % (ife0, ife1)) path_hp.add_contact(oph0, 1, ct) path_hp.add_contact(oph1, 0, ct) return ct # ---------------------------------------------------------------------- def check_consecutive_scan_lines(oph0, oph1, ydir, wdf): # Assumes that {oph0} and {oph1} are oriented paths, each consisting of a single trace # perpendicular to the unit vector {ydir}. Checks whether the two # traces are in consecutive scanlines, with {oph0} lower than {oph1} in # the direction {ydir}. x0, y0 = path.mean_projections(oph0, None, ydir) x1, y1 = path.mean_projections(oph1, None, ydir) dy = floor((y1 - y0)/wdf + 0.5) # sys.stderr.write("y0 = %20.16f y1 = %20.16f wdf = %6.3f dy = %20.16f\n" % (y0,y1,wdf,dy)) assert dy == 1 return # ----------------------------------------------------------------------