# Last edited on 2021-11-03 18:22:33 by stolfi def add_contact(oph, ct): # Appends the contact {ct} (which should be an object of class {Contact}) # to the list of contacts of path {ph}. Assumes that the fields # {ct.ph[i],ct.ix[i],ct.tcov[i]} are defined, and {ct.ph[i] = ph} # for {i=0} or {i=1}, where {ph} is the {Path} object underlying {oph}. # The orientation of {oph} is irrelevant. path_IMP.add_contact(oph, ct) def contacts(oph): # Returns a list of the contacts of the {Path} object underlying the oriented # path {oph}, in arbitrary order. The orientation of {oph} is irrelevant. def append_element(ogr, oph, ocn0, ocn1): # Adds the oriented path {oph} as a new element at the end of the oriented group {ogr}, # considering its orientation. Namely, if the group had {n} elements, # after this operation {elem(ogr,n)} will be {oph}. # # Also adds the two connectors {ocn0} and {ocn1} to the link lists of {ogr}. If # {ophant = elem(ogr,n-1)}, then the # connector {ocn0} must go from {path.pfin(ophant)} to {path.pini(oph)}, # and {ocn1} must go from {path.pini(ophant)} to {path.pfin(oph)}. group_IMP.append_element(ogr, oph, ocn0, ocn1) def segment_clip_by_circle(p, q, ctr, R): # Let {r(t)} be {t*p + (1-t)*q} for {t} in {[0 _ 1]} . Determine {t0} # and {t1} so that {r(t)} is inside or on the boundary of {C} if {t} # is in {[t0 _ t1]}, and outside otherwise: if R <= 0: # Circle has no interior, all outside: t0 = 0; t1 = 0 else: # Determine the coefficients of the quadratic formula # {A t^2 + B t + C} for the characteristic function # {|r(t) - ctr|^2 - R^2} where {r(t) = : px = p[0] - ctr[0]; vx = q[0] - p[0] py = p[1] - ctr[1]; vy = q[1] - p[1] A = vx*vx + vy*vy B = 2*(vx*px + vy*py) C = px*px + py*py - R*R assert A >= 0 if A < 1.0e-8: # Segment is too short, replace by midpoint: if A/4 + B/2 + C <= 0: # Midpoint is inside, assume all inside: t0 = 0; t1 = 1 else: # Midpoint is outside, assume all outside: t0 = 0; t1 = 0 else: Delta = B*B - 4*A*C if Delta <= 0: # Zero or one root -- the line of the segment does not enter the circle: t0 = 0; t1 = 0 else: sD = sqrt(Delta) t0 = (-B - sD)/(2*A) t1 = (-B + sD)/(2*A) assert t0 <= t1 # Now pick the non-empty parts: Lin = []; Lot = [] if t0 >= 1 or t1 <= 0 or t0 >= t1: # Whole segment is outside: Lot.append((p,q)) elif t0 <= 0 and t1 >= 1: # Whole segment is inside: Lin.append((p,q)) elif t0 <= 0 and 0 < t1 and t1 < 1: # Inside from 0 to {t1}, then outside: r1 = rn.mix(t1, p, 1-t1, q) Lin.append((p,r1)) Lot.append((r1,q)) elif 0 < t0 and t0 < 1 and t1 >= 1: # Outside from 0 to {t0}, rest inside: r0 = rn.mix(t0, p, 1-t0, q) Lot.append((p,r0)) Lin.append((r0,q)) else: assert 0 < t0 and t0 < t1 and t1 < 1 # Inside between {t0} and {t1}, rest outside: r0 = rn.mix(t0, p, 1-t0, q) r1 = rn.mix(t1, p, 1-t1, q) Lot.append((p,r0)) Lin.append((r0,r1)) Lot.append((r1,q)) return Lin, Lot def circle_line_meet(ctr, R, d, ang): if R < 0: return None if abs(d) > R: return None else: h = sqrt(max(0, R*R - d*d)) c = cos(ang); s = sin(ang) m = rn.add(ctr, (-d*s, +d*c)) u = (+h*c, +h*s) p = rn.sub(m, u) q = rn.add(m, u) return (p, q) def ring_line_meet(ctr, Rin, Rot, d, ang): if Rot < 0 or Rot < Rin: return () if abs(d) > Rot: return () Sot = circle_line_meet(ctr, Rot, d, ang) if Sot == None: return () Sin = circle_line_meet(ctr, Rin, d, ang) if Sin == None: return (Sot,) else: Slo = (Sot[0],Sin[0]) Shi = (Sin[1],Sot[1]) return (Slo,Shi) # Two shapes are said to be /equivalent/ if {Int(F)} and {Int(G)} have # the same closure, and {Ext(F)} and {Ext(G)} have the same closure. # Note that there may be infinitely may shapes equivalent to the vacuum # or to the plenum. ??? Is this concept ecessary? ??? # For example, let {P0,P1,P2,P3} be four raster lines in consecutive # scan-lines, each oriented left to right and making significant contact # with the previous and following ones. Suppose that we have decided # that they should be extruded in succession, in alternating directions. # We can join them into a single path in four different ways, depending # on whether we start with {P0}, {rev(P0)}, {P3}, or {rev(P3)}. The first # choice requires two connectors along the right edge and one on the # left edge. The second choice requres the different connectors{od} field may be 0 to mean that they should be # concatenated in that order, and 1 to mean that they be executed in the # opposite order (but still each in the same direction). The {dr} bit # then tells whether the resulting path should be used as is, or # entirely reversed. That is, the four possibilities mean: # # dr od execution order initial and final points # -- -- ---------------- ------------------------- # 0 0 P0,P1,P2,P3 pini(P0),pfin(P3) # 0 1 P3,P2,P1,P0 pini(P3),pfin(P0) # 1 0 rev(P3),rev(P2),rev(P1),rev(P0) pfin(P3),pini(P0) # 1 1 rev(P0),rev(P1),rev(P2),rev(P3) pfin(P0),pini(P3) # # As implied by this example, the paths in a block do not need to form a # single continuous path by themselves. Therefore, besides the {m} # elementary paths, each block also has one or more lists of {m-1} # /connectors/ that bridge the endpoints of suceessive elements. # Depending on the order and direction of execution, one of these lists # of connectors must be intercalated with the elementary paths, as given # or reversed, to form a single continuous path. # # In the above example, to form a single continuous path, one set of # connectors must be used for the 00 orientation, and another one for # the 01 orientation. These are returned by the function {conn(obc,k)} # for {k=0,1,..m-2}. In the example, if {obc = (bc,0,0)}, then # {conn(obc,0)} is a path that connect {pfin(P1)} to {pini(P2)}. If # {obc} is {(bc,0,1)}, then {conn(obc,0)} will instead connect {fin(P3)} # to {pini(P2)}. And so on. def conn(obc, ch): # Returns the oriented path {ocn} that is the connector between # element {oel0=choice(obc,ch)} and {oel1=choice(obc,ch+1)}, oriented # according to the execution order. That is, {path.pini(ocn)} is # {path.pfin(oel0)} and {path.pfin(ocn)} is {path.pini(oel1)}. # # The index {ch} must be in {0..nel-2} where {nel=nchoices(obc)}. return block_IMP.conn(obc, ch) def pini(obc): # Returns the initial point of the oriented block {obc}, # as it would be executed. return block_IMP.pini(obc) def pfin(obc): # Returns the final point of the oriented block {obc}, # as it would be executed. return block_IMP.pfin(obc) def join(obc): # # Returns a single tool-path that is the result of concatenating the # # elements of the oriented block {obc}, in the specified order and direction, # # intercalated with the appropriate list of connectors. The block {obc} is not affected. # return block_IMP.join(obc) # de# f rev(obc): # Returns an oriented block {obc'} that has the same elements of {obc}, but # in the opposite order and each with the opposite direction. # That is, {join(rev(obc))} is the same sequence of moves as {path.rev(join(obc))}. # # Note that this operation reverses the order and direction of the # connectors of {obc}, but does not change the set of connectors. Thus # the total execution time {T=extime(obc)} is preserved, and the cover # times of contacts relative to {join(obc)} are completmented relative # to {T}. return block_IMP.rev(obc) # TIMING def extime(obc): # Returns the total execution time of the block {obc}, # considering its orientation, including the appropriate # set of connectors. Same as {path.extime(join(obc))}, but does not # actually build the path. return block_IMP.extime(obc) def tini(obc, ch): # Returns the time at which the execution of element {choice(obc,ch)} # would begin, counted since the beginning of the block's execution. # Considers the block's orientation, and includes the time of # interevening connectors from the appropriate set. # # For convenience, if {ch} is the number of block elements {nel}, returns # {extime(obc)}. return block_IMP.tini(obc, ch) def tfin(obc, ch): # Returns the time at which execution of element {choice(obc,ch)} would be complete, # counted since the beginning of the # block's execution. Considers the block's orientation, and includes the time of # interevening connectors from the appropriate set. # # For convenience, if {k} is {-1}, returns zero. return block_IMP.tfin(obc,k) # PLOTTING def plot(c, obc, dp, wblock, cblock, wconn, cconn, wtrace, ctrace, wjump, cjump, axes, dots, arrows): # Plots the oriented block {obc} on the {pyx} canvas {c}. The plot will be displaced by # the vector {dp} (a 2-tuple of floats). # # The stroke width {wblock} and the color {cblock} are used to paint a background # behind all traces in the elements and both connector sets, to show the region # potentially covered by the block. # # The width {wconn} and the color {cconn} are used to paint the fat traces # of the set of connectors that is determined by the orientation of {obc}. # # The width {wtrace} and the color {ctrace} are used to show the fat traces # in the elements of {obc} (not the connectors). # # The width {wjump} is used to draw all jumps in the elements (with color {cjump}) # and in the selected connectors set (with color {cconn}), as dashed lines. # # Any of these parts can be suppressed by giving {None} or 0 as the width. # # If {axes} is true, the axes of the traces in the elements are drawn # with width {wjump} and a darkened version of the respective trace color. # # If {arrows} is true, arrowheads will be drawn on all jumps and # trace axes (even if the axes are not drawn). block_IMP.plot(c, obc, dp, wblock, cblock, wconn, cconn, wtrace, ctrace, wjump, cjump, axes, dots, arrows) def make_test_raster(R, Ymin, Ymax, strace, side, parms): # Makes a test block consisting of a set of filling elements in the space # between two concentric circles of radius {R} and {0.5*R} # with center at the point {(R+2)*(1,1)}. The {Y} ordinates of the trace axes will # be in the interval {[Ymin _ Ymax]} and will be spaced {strace} apart. # # If the filling elements are interrupted by the inner circle, chooses the left # part if {side} is negative, and the right part if {side} is positive. # # ??? Should use {parms['road_width']} instead of {strace} ??? return block_IMP.make_test_raster(R, Ymin, Ymax, side, strace, parms) def __init__(self, ...): self.cumtex_el = cumtex_el self.eord = eord self.ocn = ocn self.cumtex_cn = cumtex_cn def make_trivial(oph): tex = path.extime(oph) return block.Block\ ( oel = ( oph, ), cumtex_el = ( tex, ), eord = ( ( +1, ), ( -1, ), ), ocn = ( (), () ) cumtex_cn = ( (), () ) ) # TIMING def extime(obc): bc, ch = unor(obc) nch = len(bc.ochs) # Bits {dr} and {ch} determine which connector set to use: if dr == ch: i = 0 # The native orientation connector set. else: i = 1 # The alternative connector set. return bc.cumtex_el[nch-1] + (0 if nch == 1 else bc.cumtex_cn[i][nch-2]) def tini(obc, k): bc, ch = unor(obc) nch = len(bc.ochs) assert k >= 0 and k <= nch if k == 0: return 0 if k == nch: return extime(obc) # Bits {dr} and {ch} determine which connector set to use: if dr == ch: i = 0 # The native orientation connector set. else: i = 1 # The alternative connector set. # Bit {ch} determines the choice of elements and connectors: if ch == 0: # Native choice: return bc.cumtex_el[k-1] + bc.cumtex_cn[i][k-1] else: # Reversed choice assert nch >= 2 cumtex_el = bc.cumtex_el[nch-1] - bc.cumtex_el[nch-1-k] cumtex_cn = bc.cumtex_cn[i][nch-2] - (0 if k == nch-1 else bc.cumtex_cn[i][nch-2-k]) return cumtex_el + cumtex_cn def tfin(obc, k): bc, ch = unor(obc) nch = len(bc.ochs) assert k >= -1 and k <= nch-1 if k == -1: return 0 return tini(obc, k) + path.extime(choice(obc, k)) # PLOTTING def plot(c, obc, dp, wblock, cblock, wconn, cconn, wtrace, ctrace, wjump, cjump, axes, arrows): bc, ch = unor(obc) nch = len(bc.ochs) # Plot the block background where there are traces (ncluding both connectors): for k in range(nch): ophk = choice(obc,k) path.plot(c, ophk, dp, jmp=False, clr=cblock, waxes=wblock, dashed=False, wdots=0, szarrows=0) if k < nch-1: ocnk0 = conn(obc,k) ocnk1 = conn(rev(obc),k) path.plot(c, ocnk0, dp, jmp=False, clr=cblock, waxes=wblock, dashed=False, wdots=0, szarrows=0) path.plot(c, ocnk1, jmp=False, dp, clr=cblock, waxes=wblock, dashed=False, wdots=0, szarrows=0) # Plot the nominal areas of traces - first the proper connectors, then elements: for k in range(nch-1): ocnk = conn(obc,k) path.plot(c, ocnk, dp, jmp=False, clr=cconn, waxes=wconn, dashed=False, wdots=0, szarrows=0) for k in range(nch): ophk = choice(obc,k) path.plot(c, ophk, dp, jmp=False, clr=ctrace, waxes=wtrace, dashed=False, wdots=0, szarrows=0) # Plot trace axes and/or dots and/or arrows: trc_waxis = wjump if axes else 0 trc_wdots = 2*wjump if dots else 0 trc_szarrows = szarrows if arrows else 0 for k in range(nch-1): ocnk = conn(obc,k) path.plot(c, ocnk, dp, jmp=False, clr=caxis, waxes=trc_waxis, dashed=False, wdots=0, szarrows=0) for k in range(nch): ophk = choice(obc,k) path.plot(c, ophk, dp, jmp=False, clr=caxis, waxes=trc_waxis, dashed=False, wdots=trc_wdots, szarrows=trc_szarrows) # Plot jumps: jmp_waxis = wjump jmp_wdots = 2*wjump for k in range(nch-1): ocnk = conn(obc,k) path.plot(c, ocnk, dp, jmp=True, clr=cjump, waxes=wjump, dashed=True, wdots=jmp_wdots, szarrows=True) for k in range(nch): ophk = choice(obc,k) path.plot(c, ophk, dp, jmp=True, clr=cjump, waxes=wjump, dashed=True, wdots=jmp_wdots, szarrows=True) # DEBUGGING pant = pini(obc) # Final point of previous path. tant = 0 # Cumulative execution time. for k in range(nch): # --- validate the element {k} --------------------- oelk = choice(obc, k) # sys.stderr.write("oelk = %s\n" % str(oelk)) elk, drk = path.unor(oelk) # Just to typecheck. # sys.stderr.write("elk = %s drk = %s\n" % (str(elk), str(drk))) el_pini = path.pini(oelk) el_pfin = path.pfin(oelk) # sys.stderr.write("pant = %s pini = %s\n" % (str(pant), str(el_pini))) assert pant == el_pini el_tini = tini(obc, k) el_tfin = tfin(obc, k) el_tex = path.extime(oelk) # sys.stderr.write("\n") # sys.stderr.write("k = %d tant = %12.8f el_tini = %12.8f el_tfin = %12.8f el_tex = %12.8f\n" % (k,tant,el_tini,el_tfin,el_tex)) assert abs(tant - el_tini) < 1.0e-8 assert abs((el_tfin - el_tini) - el_tex) < 1.0e-8 pant = el_pfin tant = el_tfin if k < nch-1: # --- validate the connector after that element: ocnk = conn(obc, k) # sys.stderr.write("ocnk = %s\n" % str(ocnk)) cnk, drk = path.unor(ocnk) # Just to typecheck. # sys.stderr.write("cnk = %s drk = %s\n" % (str(cnk), str(drk))) cn_pini = path.pini(ocnk) cn_pfin = path.pfin(ocnk) # sys.stderr.write("pant = %s pini = %s\n" % (str(pant), str(cn_pini))) assert pant == cn_pini cn_tex = path.extime(ocnk) # sys.stderr.write("k = %d cn_tex = %12.8f\n" % (k,cn_tex)) pant = cn_pfin tant = tant + cn_tex assert pant == pfin(obc) assert abs(tant - extime(obc)) < 1.0e-8 def make_test_raster(R, Ymin, Ymax, strace, side, parms): Rot = R - strace/2 Rin = 0.5*R + strace/2 ctr = (R+2, R+2) nch = int(ceil(R/strace)) oels = [] ocn0s = [] ocn1s = [] oelant = None for k in range(2*nch+1): Y = (k-nch)*strace if Ymin <= Y and Y <= Ymax: elk = make_test_raster_path(Rot, Rin, ctr, Y, side, parms) oelk = elk if (k % 2) == 0 else path.rev(elk) oels.append(oelk) if oelant != None: cn0k = make_test_raster_connector(oelant, oelk, strace, parms) cn1k = make_test_raster_connector(path.rev(oelant), path.rev(oelk), strace, parms) ocn0s.append(cn0k) ocn1s.append(cn1k) oelant = oelk bc = from_paths(oels, ocn0s, ocn1s) return bc def make_test_raster_path(Rot, Rin, ctr, Y, side, parms): # Returns a path that is a single raster line inside the annulus, # at the given {Y} ordinate relative to the center {ctr}. # If the raster is interrupted, uses the part on the specified {side}. Xot = sqrt(max(0, Rot*Rot - Y*Y)) if abs(Y) > Rin: # Single trace: Xmin = -Xot Xmax = +Xot else: # Interrupted trace: Xin = sqrt(max(0, Rin*Rin - Y*Y)) if side < 0: Xmin = -Xot Xmax = -Xin else: Xmin = +Xin Xmax = +Xot # Create the single-trace element: p0 = rn.add(ctr, (Xmin,Y)) p1 = rn.add(ctr, (Xmax,Y)) ph = path.from_points((p0, p1), parms) return ph def circle_raster(ctr, R, d, ang, parms): # Let {L} be the straight line that makes an angle {ang} with the # {X}-axis and passes at distance {d} from the point {ctr}. Returns a # path {P} consisting of only one trace along {L} that lies inside the # circle {C} with center {ctr} and radius {R}. # # The raster line is assumed to have width {strace = # parms['solid_raster_width']}. The trace's end caps will just touch the # circle's boundary. That is, the axis of the trace will be the part of # {L} inside the circle with radius {R-strace/2}. Note that the # radius {R} must be adjusted by the caller to account for # the condour's trace width. # # If no such trace is possible (in particular, if {R} is negative or # less than {strace/2}), returns {None}. # # Takes the sign of {d} into account. The trace is oriented left to # right relative to the {XY} coord system rotated by {ang}. # . return example_path_IMP.circle_line_trace(ctr, R, d, ang, parms) def circle_raster(ctr, R, d, ang, parms): strace = parms['solid_raster_width'] ctrace = parms['contour_trace_width'] # Find the scan line intersections with the circle, # accounting for the raster line's round caps: S = hacks.circle_line_meet(ctr, R - strace/2, d, ang) if S == None: return None ph = path.from_move(S[0], S[1], False, parms) return ph def test_cover_time(): sys.stderr.write("--- testing {cover_time} -----------------\n") wdfill = parms['solid_raster_width'] # Make an incomplete a square frame: Rot = 3 ctr2 = (3*Rot + 3, Rot + 1) q1 = rn.add(ctr2, (+Rot, -Rot)) q2 = rn.add(ctr2, (+Rot, +Rot)) q3 = rn.add(ctr2, (-Rot, +Rot)) q4 = rn.add(ctr2, (-Rot, -Rot)) q5 = rn.add(ctr2, ( 0, -Rot)) phb = path.from_points([q1, q2, q3, q4, q5], wdfill, parms) # Make a single move next to the top of the square: u2 = rn.add(ctr2, (-Rot+wdfill, +Rot-wdfill)) u3 = rn.add(ctr2, (+Rot-wdfill, +Rot-wdfill)) phc = path.from_move(u2, u3, wdfill, parms) # Make a contact between paths {phb} and {phc}: v20 = rn.add(ctr2, (00.0, +Rot-wdfill/2)) # Start of contact. v21 = rn.add(ctr2, (+2.0, +Rot-wdfill/2)) # End of contact. v2m = rn.mix(0.5, v20, 0.5, v21) # Midpoint of contact. c = pyx.canvas.canvas() pyx.unit.set(uscale=2.0, wscale=2.0, vscale=2.0) ctraces2 = pyx.color.rgb.green ctraces3 = pyx.color.rgb.red ccont = pyx.color.rgb.blue waxes = 0.05*wdfill; # Axes of traces. wcont = 0.15*wdfill; axes = True dots = True arrows = True matter = False path.plot_standard(c, phb, (0,0), ctraces2, waxes, axes, dots, arrows, matter) path.plot_standard(c, phc, (0,0), ctraces3, waxes, axes, dots, arrows, matter) sty = [pyx.style.linewidth(wcont), pyx.style.linecap.round, ccont] c.stroke(pyx.path.line(v2m[0], v2m[1], v2m[0], v2m[1]), sty) hacks.write_plot(c, "tests/out/path_TST") # Check total path times {phb}: assert path.nelems(phb) == 4 extime2a = 0 tex20 = move.extime(path.elem(phb,0)); extime2a += tex20 tex21 = move.extime(path.elem(phb,1)); extime2a += tex21 tex22 = move.extime(path.elem(phb,2)); extime2a += tex22 tex23 = move.extime(path.elem(phb,3)); extime2a += tex23 extime2b = path.extime(phb) # sys.stderr.write("move times = %s\n" % str((tex20,tex21,tex22,tex23))) # sys.stderr.write("extime2a = %12.8f extime2b = %12.8f\n" % (extime2a,extime2b)) assert abs(extime2a-extime2b) <= 1.0e-8 dq23 = rn.dist(q2, q3) rmq23 = abs(v2m[0] - q2[0])/abs(q3[0] - q2[0]) # Rel pos of contact on {mvq23} # Contact cover tims on paths {phb} and {rev(phb)}, computed "by hand": tcov2a = \ move.extime(path.elem(phb,0)) + \ move.nozzle_travel_time(dq23, False, rmq23*dq23, parms) tcov2c = \ move.extime(path.elem(phb,3)) + \ move.extime(path.elem(phb,2)) + \ move.nozzle_travel_time(dq23, False, (1-rmq23)*dq23, parms) # sys.stderr.write("tcov2a = %12.8f tcov2c = %12.8f sum = %12.8f extime2b = %12.8f\n" % (tcov2a, tcov2c, tcov2a+tcov2c,extime2b)) assert abs(tcov2a+tcov2c-extime2b) <= 1.0e-8 # Contact cover time on paths {phb} and {rev(phb)}, computed by {path.cover_time}: tcov2b = path.cover_time(phb, 1, v2m, parms) tcov2d = path.cover_time(path.rev(phb), 2, v2m, parms) # sys.stderr.write("tcov2b = %12.8f tcov2d = %12.8f sum = %12.8f extime2b = %12.8f\n" % (tcov2b, tcov2d, tcov2b+tcov2d, extime2b)) assert abs(tcov2b+tcov2d-extime2b) < 1.e-8 assert abs(tcov2a - tcov2b) <= 1.0e-8 assert abs(tcov2c - tcov2d) <= 1.0e-8 def colors_rgb(): colors = [ pyx.color.rgb( 1.000, 0.000, 0.400 ), pyx.color.rgb( 0.000, 0.400, 1.000 ), pyx.color.rgb( 0.200, 0.800, 0.000 ), pyx.color.rgb( 0.702, 0.000, 0.702 ), pyx.color.rgb( 1.000, 0.392, 0.000 ), pyx.color.rgb( 0.329, 0.443, 0.596 ), pyx.color.rgb( 0.894, 0.000, 0.000 ), pyx.color.rgb( 0.459, 0.698, 0.439 ), pyx.color.rgb( 0.773, 0.239, 0.239 ), pyx.color.rgb( 0.000, 0.761, 0.729 ), pyx.color.rgb( 0.973, 0.761, 0.376 ), pyx.color.rgb( 0.000, 0.529, 0.267 ), pyx.color.rgb( 0.973, 0.329, 0.475 ), pyx.color.rgb( 0.000, 0.549, 0.651 ), pyx.color.rgb( 0.957, 0.498, 0.482 ), pyx.color.rgb( 0.616, 0.839, 0.306 ), pyx.color.rgb( 0.588, 0.373, 0.678 ), pyx.color.rgb( 0.212, 0.671, 0.710 ), pyx.color.rgb( 0.996, 0.369, 0.318 ), pyx.color.rgb( 0.361, 0.722, 0.361 ), pyx.color.rgb( 0.620, 0.239, 0.392 ), pyx.color.rgb( 0.000, 0.682, 0.859 ), pyx.color.rgb( 0.745, 0.161, 0.925 ), pyx.color.rgb( 0.000, 0.694, 0.349 ), ] return colors def colors_tex(): # Returns a list of color names for {TeX} figures. colors = [ 'magenta', 'cyan', 'orange', 'green', 'teal', 'purple', 'pink', 'gray', 'lime' ] return colors def circle_parms(R, wd, nt): # Returns four parameters for a {circle} path: the angular increment # {astep} between successive endpoints, the angular gap {agap} between # the starting and ending points, and the radii {Rin,Rot} of the # inscribed and circumscribed circles. def circle_parms(R, wd, nt): agap = wd/2/R # Angular size of gap between the endpoints. astep = 2*pint Rin = R*cos(astep/2) - wd/2 Rot = R + wd/2 return astep, agap, Rin, Rot # Compute the bbox {B} of all blocks: B = None for bc in BS: Bbc = block.bbox(bc, True, True) B = Bbc if B == None else rn.box_join(B, Bbc) assert B != None # Draw a grid covering it: plo,phi = B plog = (min(0,floor(plo[0])-1), min(0,floor(plo[1])-1)) xszg = ceil(phi[0] - plog[0])+1 yszg = ceil(phi[1] - plog[1])+1 wgrid = 0.5*waxes hacks.plot_grid(c, None, wgrid, plog, xszg, yszg, 3*wgrid, 1,1) # Split the choices of block {bc} into two synced lists {phs0,phs1}: original paths and reversals: ophs0 = []; ophs1 = [] has_rev = False # True if {bc} has both a path and its reversal. for ip in range(np): ophi = block.choice(bc, ip) phi, dri = path.unpack(ophi) new = True # True if choice {ophi} is not the reversal of a previous choice. for jp in range(len(ophs0)): ophj = ophs0[j] phj, drj = path.unpack(ophj) if phi == phj: assert dri != drj # Choices should be distinct paths. assert new and ophs1[jp] == None # Can't have more than 2 reversals ophs1[jp] = ophi has_rev = True new = False if new: ophs0.append(phi); ophs1.append(None) npd = len(ophs0) assert len(ophs1) == npd # BLOCK PICKS # # A /block pick/ is a pair (2-tuple) {bcp = (bc, ip)} where {bc} # is a {Block} object, {ip} is an integer code in {i=0,1,...n-1}, # and {n} is the number of choices. It specfies that alternative # path {ip} of {bc} is to be used. def pick(bcp): # Given a block pick {bcp=(bc,ip)}, returns {choice(bc,ip)}. return block_IMP.pick(bcp) def unpack(bcp): # Given a block pick {bcp = (bc,ic)}, returns the unoriented {Block} object {bc} # and the choice index {c} separate results. # # It is equivalent to the unpacking assignment {bc,ic = bcp}, except that # it performs some type checking. return block_IMP.unpack(bcp) def pick(bcp): bc, ip = unpack(bcp) return choice(bc, ip) def unpack(bcp): assert type(bcp) is tuple assert len(bcp) == 2 bc, ip = bcp assert isinstance(bc, block.Block) n = len(bc.ochs) assert type(ip) is int assert ip >= 0 and ip < n return bc, ip # Testing {unpack, pick}: bcp = (bcw, 2) assert (block.unpack(bcp)) == (bcw, 2) assert block.pick(bcp) == phb def test_stair_pair(m, nx, ny, ang, jmp, udnoz): wd = wd_fill xd = 5*wd yd = 3*wd pt0 = (1,1) oph0 = timing_path.stair(pt0, m, nx, +xd, ny, +yd, ang, jmp, udnoz, mp_jump, mp_fill) pt1 = path.pfin(oph0) eps = wd/nx # Stretching amount of connecting horizontal step. xd1 = xd + (+eps if xd > 0 else -eps) oph1 = timing_path.stair(pt1, m, ny, -yd, nx, +xd, ang+90, jmp, udnoz, mp_jump, mp_fill) tag = "stair_pair_m%02d_nx%02d_ny%02d_ang%03d_jm%d_ud%d" % \ (m,nx,ny,int(floor(ang+0.5)),int(jmp),int(udnoz)) oph = path.concat([oph0, oph1], True, False, None) do_plot(oph, tag) return # ---------------------------------------------------------------------- def check_plot_gearloose(): # Generates plots for the {example_path.gearloose()} path, using # {plot_standard} and mutiple {plot_layer}. The file names will be # tests/out/path_TST_gearloose_{proc}_zz{Z}.{ext}" where {proc} is # "STD" or "BYL" and {Z} is the {zigzag} option (0 or 1). R = 10 szx = 2*R+2 szy = 2*R+2 ctr = (R+1, R+1) matter = False axes = False dots = True arrows = False for zz in True, False: oph = example_path.gearloose(R, zz, mp_jump, mp_cont, mp_fill) for byl in False, True: c = pyx.canvas.canvas() pyx.unit.set(uscale=0.5, wscale=0.5, vscale=0.5) hacks.plot_frame(c, pyx.color.rgb.white, 0.30, (0,0), szx, szy, -0.15) if byl: do_path_plot_standard(c, (0,0), oph, axes, dots, arrows, matter) name = "STD" else: do_path_plot_layer(c, (0,0), oph, axes, dots, arrows, matter) name = "BYL" name = name + ("_zz%d" % int(zz)) hacks.write_plot(c, "tests/out/path_TST_plot_gearloose_" + name) # The procedure assumes that any directed contact {ct} in {CS} is an # ordering constraint on *all* choices of the two blocks, including # choices that do not include sides of {ct}. Thus, if there is sucha # contact, and {bc0} covers side 1 and {bc1} covers side 0 of # any directed contact in {CS} the procedure # will consider only concatenations of choices return {+inf} if. # def list_min_tcool(BS, bct, mp_jump): # Given a list {BS} of {Block} objects, the procedure finds the two # blocks {bc0} and {bc1} that are sides 0 and 1 of the block contact # {bct}, respectively. If the contact is not directed, returns the # smallest of {pair_min_tcool} for the orders {bc0,bc1} and # {bc1,bc0}. If it is directed, only considers the order {bc0,bc1}. # # The blocks {bc0} and {bc1}, if they exist, must be unique and # distinct. If only one of them exists, the procedure returns {+inf}. # If neither of them exists, the procedure fails with error (the # contact is not relevant for the blocks {BS}). return block_IMP.list_min_tcool(BS, bct, mp_jump) def list_min_tcool(BS, ct, mp_jump): mv0 = contact.side(ct,0) mv1 = contact.side(ct,1) # Find the two blocks that have {mv0} and {mv1}, if any: kb0 = find_with_move(BS,mv0) kb1 = find_with_move(BS,mv1) assert kb0 != None or kb1 != None, "contact is not relevant to blocks of {BS}" assert kb0 != kb1, "contact has two sides in same block" if kb0 != None and kb1 != None: # Lowerbound the cooling time of {ct} if the proper # choices of bc0 = BS[kb0] bc1 = BS[kb1] tcmin01 = pair_min_tcool(bc0, bc1, ct, mp_jump) if ct.drc: tcmin = tcmin01 else: tcmin10 = pair_min_tcool(bc1, bc0, ct, mp_jump) tcmin = min(tcmin01, tcmin10) else: tcmin = +inf return tcmin # ---------------------------------------------------------------------- def max_tcool(oph, CS): # The parameter {oph} must be an oriented path and {CS} must be a list # or tuple of {Block_Contact} objects. Returns the maximum value of # {tcool(oph,bct)} over every contact {ct} in every block contact {bct} of {CS} that has been closed # by path {oph}. # # If there are no such contacts, returns {-inf}. If any contact {bct} # in {CS} is directed, and its side 0 occurs in {oph} after its side # 1, returns {+inf}. Otherwise, the result will be the same for {oph} # and for {path.rev(oph)}. return block_contact_IMP.max_tcool(oph, CS) def max_tcool(oph, CS): assert type(CS) is list or type(CS) is tuple tmax = -inf for bct in CS: tc = tcool(oph, bct) if tc != None and tc > tmax: tmax = tc return tmax # ---------------------------------------------------------------------- def pair_max_tcool(oph0, tconn, oph1, CS): assert type(CS) is list or type(CS) is tuple tmax = -inf for bct in CS: tc = pair_tcool(oph0, tconn, oph1, bct) if tc != None and tc > tmax: tmax = tc return tmax # ---------------------------------------------------------------------- def min_tcov(oph, CS): assert type(CS) is list or type(CS) is tuple tmin = +inf for bct in CS: tcs = covtimes(oph, bct) if (tcs[0] == None) != (tcs[1] == None): if is_directed(bct) and tcs[1] != None: return -inf else: tci = tcs[0] if tcs[0] != None else tcs[1] if tci < tmin: tmin = tci return tmin # --- block contact plot with arrows? ---------------------------------------------------------------------- def plot(c, bct, dp, wd, clr, sz_tic): p = bct.pt[0] q = bct.pt[1] dpq = rn.dist(p,q) peps = 0.01*wd if dpq < 1.0e-6 else 0 # Perturbation for equal points. sty_basic = [ pyx.style.linecap.round, clr, ] sty_line = sty_basic + [ pyx.style.linewidth(wd), ] if dp != None: sty_line.append(pyx.trafo.translate(dp[0], dp[1])) c.stroke(pyx.path.line(p[0]-peps, p[1]-peps, q[0]+peps, q[1]+peps), sty_line) # Should we plot a transversal tic or arrowhead? if sz_tic == None: sz_tic = 0 # Simplification. if sz_tic > 0 or bct.drc: # Plot the transversal tic and/or arrowhead. m = rn.mix(0.5, p, 0.5, q) # Midpoint. u = get_perp_dir(m, bct.bc[0], bct.bc[1]) sz_arrow = 3*wd if bct.drc else 0 # We need a tic with a certain min size for the arrowhead: sz_tic = max(sz_tic, 0.80*sz_arrow) a = rn.mix(1.0, m, -0.5*sz_tic, u) b = rn.mix(1.0, m, +0.5*sz_tic, u) sty_tic = sty_basic if sz_arrow > 0: # Add the arrowhead to the tic. arrowpos = 0.5 # Position of arrow on transversal line. wd_arrow = sz_arrow/5 # Linewidth for stroking the arrowhead (guess). sty_arrow = sty_basic + [ pyx.deco.stroked([pyx.style.linewidth(wd_arrow), pyx.style.linejoin.round]), pyx.deco.filled([]) ] sty_tic = sty_tic + \ [ pyx.deco.earrow(sty_arrow, size=sz_arrow, constriction=None, pos=arrowpos, angle=35) ] sys.stderr.write("sz_arrow = %.3f wd_arrow = %3f sz_tic = %.3f\n" % (sz_arrow, wd_arrow, sz_tic)) sty_tic = sty_tic + [ pyx.style.linewidth(wd), ] if dp != None: sty_tic.append(pyx.trafo.translate(dp[0], dp[1])) c.stroke(pyx.path.line(a[0], a[1], b[0], b[1]), sty_tic) def test_data_lists_ten_five(): sys.stderr.write("--- testing {test_data_lists_ten_five} -------------\n") TS, JS = move.test_data_lists_ten_five(mp_jump, mp_fill) assert type(TS) is list assert isinstance(TS[0], move.Move) assert type(JS) is list assert isinstance(JS[0], move.Move) MS = TS + JS # sys.stderr.write("TS = %d JS = %d MS = \n" % (len(TS),len(JS),len(MS))) c = pyx.canvas.canvas() pyx.unit.set(uscale=1.00, wscale=1.00, vscale=1.00) # Copute the plot's bounding box: B = None for mv in MS: Bm = move.bbox(mv) B = rn.box_join(B, Bm) plo = ( floor(B[0][0]) - 1, floor(B[0][1]) - 1 ) phi = ( ceil(B[1][0]) + 1, ceil(B[1][1]) + 1 ) szx = phi[0] - plo[0] szy = phi[1] - plo[1] # Absolute (in mm): wd_ref = 0.15*min(wd_fill,wd_cont) # Reference line width. wd_axes = wd_ref; wd_grid = 0.02 hacks.plot_grid(c, pyx.color.rgb.blue, wd_grid, plo, szx, szy, -5*wd_grid, 1, 1) for layer in range(4): # sys.stderr.write("--- layer %d ---\n" % layer) for mv in MS: p = move.pini(mv) q = move.pfin(mv) # sys.stderr.write("mv = ( %3f %3f ) --> ( %3f %3f )\n" % (p[0],p[1],q[0],q[1])) move.plot_standard \ ( c, mv, None, layer, None, wd_axis = wd_axes, axis = True, dots = True, arrow = True, matter = True ) hacks.write_plot(c, "tests/out/move_TST_plot_ten_five") return # ---------------------------------------------------------------------- def raster_square(nrs, mp_trace): # Returns a block with eight serpentine raster paths, each consisting # of {nrs} raster lines and {nrs-1} links filling a square. # # Choices 0-4 have horizontal rasters while choices 4-7 have vertical # ones. The raster traces are shared within each set. Choice {2*k+1} # is the reversal of choice {2*k} for each {k} and shares the same # links but reversed. Choices 0 and 2 start with the bottom-most # raster, left-to-right and right-to-left. Choices 4 and 6 start with # the leftmost raster, bottom-up and top-down. return block_example_IMP.raster_square(nrs, mp_trace) def raster_square(nrs, mp_trace): wd = move_parms.width(mp_trace) xsz = (nrs-1)*wd oph0 = path_example.raster_rectangle(nrs, (0,0), xsz, wd, False, True, mp_trace, None) oph1 = path.displace(oph0, math.pi/2, (xsz,0)) oph2 = path.displace(oph1, math.pi/2, (xsz,0)) oph3 = path.displace(oph2, math.pi/2, (xsz,0)) bc = from_paths([oph0, oph1, oph2, oph3,]) return bc # ---------------------------------------------------------------------- def get_name(omv, name, MVS, add): # Makes up a short name for the oriented move {omv} as # "{name}{i}" where {i} is the index of the move in the list {MVS}. # # The list {MVS} must contain only unoriented {Move} objects, and the # orientation of {omv} is ignored in the look-up. However, a prefix # "~" is printed if the orientation of {omv} is opposite to its # natural orientation. So, for example, the result of # {get_name(MVS[6],"M",MVS,add)} will be "M6", and that of # {get_name(rev(MVS[6]),"M",MVS,add)} will be "~M6". # # If the {Move} object of {omv} is not in the list {MVS}, # then, if {add} is false, the result is {None}; if {add} # is true, the object is appended to the list {MVS}, # and the name is produced as above. # return move_IMP.get_name(omv, name, MVS, add) def get_name(omv, name, MVS, add): mv, dr = move.unpack(omv) if mv in MVS: i = MVS.index(mv) else: if add: i = len(MVS); MVS.append(mv) else: i = None if i == None: xmv = None else: xmv = ("%s%d" % (name,i)) if dr == 1: xmv = "~" + xmv return xmv # ---------------------------------------------------------------------- def describe(wr, OPHS, trname, TRS, jmname, JMS): assert type(OPHS) is list or type (OPHS) is tuple assert trname == None or type (trname) is str assert TRS == None or type(TRS) is list or type (TRS) is tuple assert jmname == None or type (jmname) is str assert JMS == None or type(JMS) is list or type (JMS) is tuple nph = len(OPHS) nna = len(phname) ndg = 1 if nph <= 10 else 2 if nph <= 100 else 3 if nph <= 1000 else 4 # Num digits in index. nhd = max(4, nna + ndg) # Width of first col header. wr.write("\n") # Write header: wr.write("%-*s d n %-*s %-*s moves \n" % (nhd,"path",15,"pini",15,"pfin")) wr.write("%s - - %s %s %s\n" % ("-"*nhd,"-"*15,"-"*15,"-"*15)) # Write paths: for k in range(len(OPHS)): oph = OPHS[k] xph = "%s%d" % (phname,k) wr.write("%-*s" % (nhd,xph)) ph, dr = unpack(oph) wr.write(" %d" % dr) wr.write(" %d" % nelems(oph)) p = pini(oph) q = pfin(oph) wr.write(" (%6.1f,%6.1f)" % (p[0],p[1])) wr.write(" (%6.1f,%6.1f)" % (q[0],q[1])) if TRS != None: wr.write(" ") nmv = nelems(oph) for imv in range(nmv): omv = elem(oph, imv) xmv = move.get_name(omv, trname, TRS) if xmv == None and JMS != None: xmv = move.get_name(omv, jmname, JMS) assert xmv != None, "move missing in {TRS,JMS} lists" if imv > 0: wr.write(",") wr.write(xmv) wr.write("\n") wr.write("\n") return # ---------------------------------------------------------------------- def covindex(mv): # Returns /coverage index/ of the input {Move} object {mv}, which must be a trace. This is # the index {imv} of move {mv} (or its reverse) in the current # tentative path {Q}. # # The procedure returns {None} if the trace {mv} has not been included # in {Q}, eiher because the block {bc} that owns it has not been used # yet, or because the choice of {bc} that was included in {Q} does not # own {mv}. It also returns {None} if {mv} is a jump or not an input # trace, even if it does not occur in {BCS} (even if it is part of a # link between blocks) It also returns {None} it # # This procedure fails if {mv} does not occur in the input block set # {BCS}. It runs in {O(1)} time since it uses information stored in # the {Move} object by {set_covindex}. return move_hp_IMP.get_coverage_status(ct) def set_covindex(mv, imv): # Sets the coverage index of {Move} object {mv} to {imv} # # This procedure runs in {O(1)} time. move_hp_IMP.set_covindex(mv, imv) def covindex(mv): sys.stderr.write("** {} NOT IMPLEMENTED **\n") return None # ---------------------------------------------------------------------- def set_covindex(mv, imv): sys.stderr.write("** {} NOT IMPLEMENTED **\n") return # ---------------------------------------------------------------------- def get_paths(sm): # Returns the /intial candidate paths/ of the {Seam} object {sm}. # This is a pair {ophss} of lists of oriented paths, such that {ophss[i]} has # all choices of the block {bc = side(sm,i)}. return seam_IMP.get_paths(sm) def set_paths(sm, ophss): # Sets the initial candidate paths of {sm} to {ophss}, which should be a pair of # lists of oriented paths. See {get_paths}. seam_IMP.set_paths(sm, ophss) def get_paths(sm): assert isinstance(sm, seam.Seam) return sm.ophss # ---------------------------------------------------------------------- def set_paths(sm, ophss): assert isinstance(sm, seam.Seam) assert type(ophss) is list assert len(ophss) == 2 for i in range(2): assert type(ophss[i]) is list sm.ophss = ophss return # ---------------------------------------------------------------------- # Some paths that contain the sides of the seam: assert seam.side(sm, 0) == BCS[0] assert seam.side(sm, 1) == BCS[1] bc0 = BCS[0] # Move 0 should be {TRS[0]) bc1 = BCS[2] # Move 2 should be {TRS[1]) bc2 = path.concat([ path.from_moves([TRS[0],]), path.from_moves([TRS[9],])], True, False, mp_jump) bc3 = path.from_moves([TRS[1],]) bcss = [ [ bc0, path.rev(bc0), bc2 ], [ bc1, bc3, path.rev(bc3), ] ] seam.set_paths(sm, bcss) bcss1 = seam.get_paths(sm) assert bcss1 == bcss def plot_path(c, OPHS, CLRS, wd_axes, d = None): assert isinstance(OPHS, path.Path) def pick_colors(k): nclr = len(CLRS) # Returns the colors for trace sausages and axes of move {OMVS[k]}. if nclr == 1: ctrace = CLRS[0] else: ctrace = CLRS[k] caxis = pyx.color.rgb(0.6*ctrace.r, 0.6*ctrace.g, 0.6*ctrace.b) # Color of trace axis, dots, arrow. return ctrace, caxis moves_list = split_at_jumps(OPHS, d) block_id = 0 isBlock = False # Dimensions relative to nominal trace widths: rtraces = 0.80; # Trace sausage width. # Absolute dimensions (mm): j_wd_dots = 2.5*wd_axes; # Dots at ends of moves. j_sz_arrows = 8*wd_axes # Size of arrows. if CLRS == None: CLRS = hacks.trace_colors(len(moves_list), None) for moves in moves_list: if len(moves) == 1 and move.is_jump(moves[0]): isBlock = False clr = pyx.color.rgb.black rwd = 0 wd = wd_axes dashed = True wd_dots = j_wd_dots sz_arrows = j_sz_arrows else: pini = move.pini(moves[0]) pfin = move.pfin(moves[-1]) if pini == pfin: isBlock = False clr = pyx.color.rgb.black else: isBlock = True clr, caxis = pick_colors(block_id) block_id += 1 rwd = rtraces wd = 0 dashed = False wd_dots = 0 sz_arrows = 0 for mv in moves: wdk = rwd*move.width(mv) + wd move.plot_layer(c, mv, None, clr, wdk, dashed, wd_dots, sz_arrows) if isBlock: move.plot_layer(c, mv, None, caxis, wd_axes, True, j_wd_dots, j_sz_arrows) return moves_list def split_at_jumps(oph, d): moves_list = [] moves_aux = [] for k in range(path.nelems(oph)): omvk_aux = path.elem(oph, k) omvk = omvk_aux if not isinstance(omvk_aux, move.Move): omvk_aux = omvk_aux[0] elif d != None: omvk = (omvk, d) if move.is_jump(omvk_aux): if len(moves_aux) > 0: moves_list.append(moves_aux) moves_list.append([omvk]) moves_aux = [] else: moves_aux.append(omvk) if len(moves_aux) > 0: moves_list.append(moves_aux) return moves_list def blocks_min_starttime(p, BCS_R, parms): # Given a point {p} and the list {BCS_R} of blocks that have not been yet # included in the tentative tool-path, returns the minimum time needed # to jump to the nearest starting point of any choice of any of those # blocks. dmin = +inf for bc in BCS_R: for ip in range(block.nchoices(bc)): oph = block.choice(bc, ip) di = rn.dist(p, path.pini(oph)) if di < dmin: dmin = di assert dmin != +inf tmin = nozzle_travel_time(dmin, True, None, parms) return tmin # ---------------------------------------------------------------------- def rest_min_extime(BCS_R): # Given the list{BCS_R} of blocks that have not been yet included in the # tentative tool-path, returns an estimate of the minimum time needed to # execute them, even if the best alternative could be chosen in each # block. mex = 0 for bc in BCS_R: mex += block.min_extime(bc) return mex def delete_link(R): for index1 in range(len(R)): for edge in range(2): for indexLink in range(len(R[index1]['links'][edge])): index2 = R[index1]['links'][edge][indexLink][0] if R[index1]['group'] != R[index2]['group']: R[index1]['links'][edge][indexLink][1] = None R[index1]['links'][edge][indexLink][2] = None for indexLink2 in range(len(R[index2]['links'][1-edge])): if R[index2]['links'][1-edge][indexLink2][0] == index1: R[index2]['links'][1-edge][indexLink2][1] = None R[index2]['links'][1-edge][indexLink2][2] = None def read_input_gcode(fname, islice, mp_cont, mp_fill, mp_jump, angle, split, maxlines, scan_line): Z, PTSS, raster_points = gcode_read_elis.read(fname, islice) CRS = contour.from_point_lists(PTSS) R, S, NB = gcode_read_elis.create_raster_lines(islice, raster_points, mp_fill, angle) number_lines = len(R) if split: NB = gcode_read_elis.split_block(R, NB, S) if maxlines != None and maxlines > 0: NB = gcode_read_elis.limit_block(R, NB, S, maxlines) BCS, CTS = gcode_read_elis.create_blocks(R, S, CRS, NB, mp_fill, mp_jump, angle, scan_line) return CRS, BCS, CTS, number_lines, Z # ---------------------------------------------------------------------- # Create the filling element {fe}: fe = { 'index': ife, 'rot_ends': [p_rot, q_rot], 'move': mv, 'dir_bit': dr, 'group': group, 'links': [[].copy(),[].copy()] } def add_side(R1, R2, link0, link1): y1 = R1['rot_ends'][0][1] y2 = R2['rot_ends'][0][1] if y2 > y1: R1['links'][1].append([R2['index'], link0, link1]) R2['links'][0].append([R1['index'], link0, link1]) elif y1 > y2: R1['links'][0].append([R2['index'], link0, link1]) R2['links'][1].append([R1['index'], link0, link1]) def create_contact(S, R, BCS, mp_trace, mp_jump, angle, scan_line): for bc in BCS: block_hp.clear_contacts(bc) CTS = [] angle = -angle for s in S: r0 = R[s[0]] r1 = R[s[1]] if r0['group'] != r1['group']: p0 = r0['rot_ends'][0][0] q0 = r0['rot_ends'][1][0] y0 = r0['rot_ends'][0][1] wd0 = move.width(r0['move']) mv0 = r0['move'] bc0 = BCS[r0['group']] p1 = r1['rot_ends'][0][0] q1 = r1['rot_ends'][1][0] y1 = r1['rot_ends'][0][1] wd1 = move.width(r1['move']) mv1 = r1['move'] bc1 = BCS[r1['group']] xlo = max(min(p0, q0), min(p1, q1)) xhi = min(max(p0, q0), max(p1, q1)) ylo = (y0 + y1 + (wd0 - wd1)/2)/2 yhi = (y0 + y1 + (wd0 - wd1)/2)/2 if scan_line: rl0 = None rl1 = None else: rl0, rl1 = check_raster_link(BCS, r0['group'], r1['group'], s) end0 = move.endpoints(mv0) end1 = move.endpoints(mv1) if (end0[0][1] != end0[1][1]) or (end1[0][1] != end1[1][1]): xlo_rotate = rotate_x(xlo, ylo, angle) ylo_rotate = rotate_y(xlo, ylo, angle) xlo = xlo_rotate ylo = ylo_rotate xhi_rotate = rotate_x(xhi, yhi, angle) yhi_rotate = rotate_y(xhi, yhi, angle) xhi = xhi_rotate yhi = yhi_rotate ct = contact.make((xlo, ylo), (xhi, yhi), mv0, mv1) contact_hp.set_raster_links(ct, rl0, rl1) contact_hp.set_side_block(ct, 0, bc0) contact_hp.set_side_block(ct, 1, bc1) CTS.append(ct) block_hp.add_contact(bc0, ct) block_hp.add_contact(bc1, ct) return CTS # ---------------------------------------------------------------------- def check_raster_link(BCS, g0, g1, s): rl0 = s[2] ; rl1 = s[3] p0 = path.pini(block.choice(BCS[g0], 0)) q0 = path.pfin(block.choice(BCS[g0], 0)) p1 = path.pini(block.choice(BCS[g1], 0)) q1 = path.pfin(block.choice(BCS[g1], 0)) if rl0 != None: pr0 = path.pini(rl0) qr0 = path.pfin(rl0) if pr0 != p0 and pr0 != q0 and pr0 != p1 and pr0 != q1: rl0 = None elif qr0 != p0 and qr0 != q0 and qr0 != p1 and qr0 != q1: rl0 = None if rl1 != None: pr1 = path.pini(rl1) qr1 = path.pfin(rl1) if pr1 != p0 and pr1 != q0 and pr1 != p1 and pr1 != q1: rl1 = None elif qr1 != p0 and qr1 != q0 and qr1 != p1 and qr1 != q1: rl1 = None return rl0, rl1 self.iscov = [False, False] # Status of contact rel to current {Q}. self.rl = [None,None] # Elis? iscov = [ False, True, ] contact.set_coverage_status(ct, iscov) iscov1 = contact.get_coverage_status(ct) assert iscov1 == iscov def rotate_x(x, y, angle): return ((x*m.cos(-angle) - y*m.sin(-angle))*100)/100.0 def rotate_y(x, y, angle): return ((x*m.sin(-angle) + y*m.cos(-angle))*100)/100.0 def split_groups_by_scanline(OPHS, ydir): # Splits the current groups of filler elements from {OPHS} into # maxmal sub-groups where all elements of each subgroup are on the # same scan-line. Assumes that the elements are parallel single-trace # paths perpendicular to the unit vector {ydir} Returns the number of # such new groups. return input_data_elis_IMP.split_groups_by_scanline(OPHS, ydir) def split_groups_by_scanline(OPHS, ydir): # Separate fillers by group, preserving their order: GRS_old, ngr_old = separate_fillers_by_group(OPHS) ngr_new = 0 # Number of new groups created so far. for OPHS_gr in GRS_old: if OPHS_gr != None and len(OPHS_gr) > 0: # Split the elements of old group {OPHS_gr} into scanline subgroups. oph_prev = None # Last path processed y_prev = -inf # {Y} coord of path {oph_prev}. wd_prev = None # Width of trace of {oph_prev} # Scan elements of old group: for oph_this in OPHS_gr: # Get {Y} coordinate x_this, y_this = path.mean_position(oph_this, None, ydir) # Still the same scanline? wd_this = move.width(path.elem(oph_this, 0)) sep = 0 if wd_prev == None else (wd_prev + wd_this)/2 # Expected separation between scanlines. if y_this - y_prev < 0.1*sep: # Looks like they are on the same scanline: assert oph_prev != None path_hp.set_group(oph_this, path_hp.get_group(oph_prev)) else: # Looks like we got to a new scanline: assert y_this - y_prev > 0.9*sep, "inconsistent scanline spacing" path_hp.set_group(oph_this, ngr_new) ngr_new += 1 oph_prev = oph_this y_prev = y_this wd_prev = wd_this return ngr_new # ---------------------------------------------------------------------- def make_raster_block(imv0, imv1, ird0, ird1): # Returns a block that is a serpentine path comprising raster traces # on scanlines from number {imv0} to number {imv1} # spanning roads from {ird0} to {ird1}, in the four orders and # orientations. xlo = org[0]+ ird0*xstep xhi = xlo + (ird1-ird0)*xstep + xsz_rd y = org[1] + imv0*wdf nr = imv1 - imv0 + 1 bc = block_example.raster_rectangle(xlo, xhi, y, nr, mp_fill) return bc def make_single_raster_block(imv, ird0, ird1): # Returns a block that is a raster line on scan-line number {imv}, # spanning roads from {ird0} to {ird1}, in both orientations. xlo = org[0]+ ird0*xstep xhi = xlo + (ird1-ird0)*xstep + xsz_rd y = org[1] + imv*wdf bc = block_example.single_raster(xlo, xhi, y, mp_fill) return bc def make_link(ph0, ph1, iend, name): # Creates a vertcal link path betwen ends {iend} (0 = left, 1 = right) # of rasters {ph0} and {ph1}. Attaches that link to the lists of # links of the two paths. If the two ends are not vertically aligned, does # nothing. p0 = path.pini(ph0) if iend == 0 else path.pfin(ph0) p1 = path.pini(ph1) if iend == 0 else path.pfin(ph1) if p0[0] == p1[0]: mlk = move.make(p0,p1,mp_fill); move.set_name(mlk, "ML" + name) plk = path.from_moves((mlk,)); path.set_name(plk, "PL" + name) if iend == 0: path_hp.add_link(ph0, path.rev(plk)) path_hp.add_link(ph1, plk) else: path_hp.add_link(path.rev(ph0), path.rev(plk)) path_hp.add_link(path.rev(ph1), plk) return # ...................................................................... if rph_prev[col] != None: # Add links between {rph_prev[col]} and {rph_this}: for iend in 0, 1: lkname = "%d.%d.%d.%d" % (iend,row, col, irp) # Add a contact between {rph_prev[col]} and {rph_this}: omv_prev = path.elem(rph_prev[col], 0) omv_this = path.elem(rph_this, 0) ct = contact.from_moves(omv_prev, omv_this, 0.9*wdf, 0, None) contact.set_name(ct, "CT.%d.%d.%d" % (row, col, irp)) CTS.append(ct) # Remember contact in the two paths: path_hp.add_contact(rph_prev[col], 1, ct) path_hp.add_contact(rph_this, 0, ct) def plot_choice_list_elis(c, BCS, ip, ctraces, rwd, wd_axes, CTS, clr_ct, wd_conts): # Plots the blocks {BCS} and the contacts {CTS} on the canvas {c) # displaced by {dp}. Takes choice {ip} of each block. Only plots # contacts that have at least one side on those choices. If a block # does not have a choice {ip}, shows only the material shadow of # choice 0. block_IMP.plot_choice_list_elis(c, BCS, ip, ctraces, rwd, wd_axes, CTS, clr_ct, wd_conts) def plot_choice_list_elis(c, BCS, ip, clr_tr, rwd, wd_axes, CTS, clr_ct, wd_conts): if len(BCS) == 0: # Nothing to plot: assert len(CTS) == 0 return if isinstance(clr_tr, list): # Plots the blocks: for i in range(len(BCS)): bc = BCS[i] np = block.nchoices(bc) if ip >= np: # Show only the ghost: oph = block.choice(bc, 0) layer = 0 # Only the "matter" layer. else: # Plot the choice {ip}: oph = block.choice(bc, ip) layer = None # All layers. path.plot_standard \ ( c, [oph], None, layer, [clr_tr[i]], rwd=rwd, wd_axes=wd_axes, axes = True, dots = True, arrows = True, matter = True ) else: # Plots the blocks: for i in range(len(BCS)): bc = BCS[i] np = block.nchoices(bc) if ip >= np: # Show only the ghost: oph = block.choice(bc,0) layer = 0 # Only the "matter" layer. else: # Plot the choice {ip}: oph = block.choice(bc,ip) layer = None # All layers. path.plot_standard \ ( c, [oph], None, layer, [clr_tr], rwd = rwd, wd_axes = wd_axes, axes = True, dots = True, arrows = True, matter = True ) # plot the contacts: for ct in CTS: # Decide whether the contact is relevant for choice {ip} of some block: relevant = False for bc in BCS: if relevant: break np = block.nchoices(bc) if ip < np: oph = block.choice(bc, ip) ixs = contact.covindices(oph, ct) if ixs[0] != None or ixs[1] != None: relevant = True if relevant: # Plot the contact in a contrasting color: #c, ct, dp, clr, wd_line, sz_tic, arrow contact.plot_single(c, ct, None, clr = clr_ct, wd_line = wd_conts, sz_tic = 0, arrow = False) return # ---------------------------------------------------------------------- x_this, y_this = path.mean_projections(oph_this, None, ydir) # Still the same scanline? wd_this = move.width(path.elem(oph_this, 0)) sep = 0 if wd_prev == None else (wd_prev + wd_this)/2 # Expected separation between scanlines. if y_this - y_prev < 0.1*sep: # Looks like they are on the same scanline: assert oph_prev != None assert len(OPHS_sc) > 0 OPHS_sc.append(oph_this) else: # Looks like we got to a new scanline: assert y_this - y_prev > 0.9*sep, "inconsistent scanline spacing" if len(OPHS_sc) > 0: SCSS.append(OPHS_sc) OPHS_sc = [ oph_this, ] def discrete(k, Ymin, Ymax, Smin, Smax): assert 0 <= Ymin and Ymin <= Ymax and Ymax <= 1.000, "invalid {Ymin,Ymax}" assert 0 <= Smin and Smin <= Smax and Smax <= 1.000, "invalid {Smin,Smax}" # Point on the torus, hopefully far away from other colors: phi = (sqrt(5)-1)/2 azm = k*phi*2*pi # Azimuth, longitude: angle on the medial circle. ele = k # Elevation, latitude: angle on meridian circles. # Compute the {Y} of the ideal color, in log scale: rYi = (1 + sin(ele))/2 Ybias = 0.010 Yi = exp((1-rYi)*log(Ymin+Ybias) + rYi*log(Ymax+Ybias)) - Ybias Yi = min(1, max(0, Yi)) # Just in case. assert Ymin <= Yi and Yi <= Ymax # Compute the {Y} and {S} of the ideal color, in log scale: rSi = (1 + cos(ele))/2 Sbias = 0.010 Si = exp((1-rSi)*log(Smin+Sbias) + rSi*log(Smax+Sbias)) - Sbias Si = max(0, Si) # Just in case. # Compute the ideal color, {YUV} and {RGB} coords: YUVi = (Yi, Si*cos(azm), Si*sin(azm)) RGBi = color.RGB_from_YUV(YUVi) # Computes the color {YUVz,RGBz} with same {Y} and hue but with max saturation: Sz = Smax YUVz = (Yi, Sz*cos(azm), Sz*sin(azm)) RGBz = color.RGB_from_YUV(YUVz) # Compute the scaling factor {f} to bring {YUVz} into the unit {RGB} cube: f = color.sat_clip_factor(RGBz) # Now use {f} to correct the ideal color: RGB = tuple( min(1, max(0, Yi + f*(RGBi[i] - Yi))) for i in range(3) ) return RGB # ---------------------------------------------------------------------- def nearest(CRS, pt): dMin = None for cr in CRS: i, d = find_nearest_point(cr.PTS, pt) if dMin == None or dMin > d: dMin = d crMin = cr return crMin def shift(PTS, i): # Returns a copy of the point list {PTS}, cyclically shifted so as to # bring {PTS[i]} to the front. PTS = PTS[i:] + PTS[:i] return PTS def make_path(pg, mp_cont, org): PTS = list(pg.PTS).copy() if org != None: i, d = find_nearest_point(PTS, org) PTS = shift(PTS, i) PTS.append(PTS[0]) polygon_path = path.from_points(PTS, mp_cont, None) return polygon_path # ---------------------------------------------------------------------- test_parms['input_folder'] = './tests/in/2021-05-15-elis/' + partname + '/' parms_tag = "" if test_parms['heuristic'] == 'original': test_parms['split_lines'] = False test_parms['limit_lines'] = False elif test_parms['heuristic'] == 'greedy': test_parms['split_lines'] = False test_parms['limit_lines'] = False elif test_parms['heuristic'] == 'scan_line': test_parms['split_lines'] = True test_parms['limit_lines'] = True test_parms['max_lines'] = 1 parms_tag = ("del%.3f" % test_parms['delta']) elif test_parms['heuristic'] == 'alternating_scan_line': test_parms['split_lines'] = True test_parms['limit_lines'] = True test_parms['max_lines'] = 1 parms_tag = ("del%.3f" % test_parms['delta']) elif test_parms['heuristic'] == 'hotpath': parms_tag = ("del%.3f" % test_parms['delta']) parms_tag += "_" + ("mxc%08d" % test_parms['max_calls']) if test_parms['split_lines']: parms_tag += "_split" if test_parms['limit_lines']: parms_tag += ("_lim%06d" % test_parms['max_lines']) # ---------------------------------------------------------------------- def shrink_mats(Adir,Ainv): # Repeatedly removes the last row and the last column of {Adir} and {Ainv} # if they are all zeros and with a 1 at the end. d = max(len(Adir),len(Ainv)) while d > 0 and identity_at(Adir,d-1) and identity_at(Ainv,d-1): Adir = lower_dim(Adir, d-1) Ainv = lower_dim(Ainv, d-1) d = d-1 assert d == 0 or not (identity_at(Adir,d-1) or identity_at(Ainv,d-1)) return tuple((Adir, Ainv)) # ---------------------------------------------------------------------- def identity_at(M, i): # True if the matrix {M} is an identity on coordinate {i}. if M == None: return True d = len(M) for k in range(d): assert len(M[k]) == d, "matrix is not square" if i < d: v = 1 if k == i else 0 if M[i][k] != v: return False if M[k][i] != v: return False return True # ---------------------------------------------------------------------- def lower_dim(M, n): # Returns the matrix {M} restricted to the first {n} rows and columns. if M == None: return None d = len(M) if d <= n: return M N = [] for k in range(n): assert len(M[k]) == d N.append(tuple(M[k][0:n])) return tuple(N) # ---------------------------------------------------------------------- def shrink_vec(v): # Eliminates trailing zeros of {v}. d = len(v) n = d while n > 0 and v[n-1] == 0: n = n-1 return tuple(v[0:n]) # ---------------------------------------------------------------------- def cbit(S): # Returns the complement bit of representation of the shape {S}. # # Specifically, {S} is {None} or a {Shape} object, returns 0. # Otherwise, {S} must be a list or tuple {(c,T,S1,S2,...,Sn)}; # in that case, returns the bit {c}. return shape_IMP.cbit(S) def trafo(S): # Returns the top-level transformation in the representation of the shape {S}. # # Specifically, {S} is {None} or a {Shape} object, returns {None} (the identity # trafo). Otherwise, {S} must be a list or tuple {(c,T,S1,S2,...,Sn)}; # in that case, returns the trafo {T}. return shape_IMP.trafo(S) def nterms(S): # Returns the number of sub-shapes in the representation of the shape {S}. # # Specifically, {S} is {None}, returns 0. If {S} is a {Shape} object, returns 1. # Otherwise, {S} must be a list or tuple {(c,T,S1,S2,...,Sn)}; in that case, # returns {len(S)-2}. return shape_IMP.nterms(S) def term(S,k): # Returns the sub-shape with index {k} in the sub-shapes of the representation {S}. # The index {k} must be in {0..n-1} where {n = nterms(S)}. # # Specifically, if {S} is a {Shape} object, {k} must be 0, and the result is {S} itself. # Otherwise, {S} must be a list or tuple {(c,T,S1,S2,...,Sn)}; in that case, # returns {S[k+2]}. If {n} is zero (for example, if {S} is {None} or {(1,T)}), # the procedure aborts with error. return shape_IMP.term(S,k) def cbit(S): if S == None or isinstance(S, shape.Shape): return 0 assert type(S) is tuple assert len(S) >= 2 return S[0] def trafo(S): if S == None or isinstance(S, shape.Shape): return None assert type(S) is tuple assert len(S) >= 2 return S[1] def nterms(S): if S == None: return 0 if isinstance(S, shape.Shape): return 1 assert type(S) is tuple m = len(S) assert m >= 2 return m - 2 def term(S,k): assert S != None, "{None} has no terms" if isinstance(S, shape.Shape): assert k == 0, "invalid term index" return S assert type(S) is tuple m = len(S) assert m >= 2 and 0 <= k and k < m-2 return S[k+2] def nfactors(T): if T == None: return 0 elif isinstance(T, trafo.Trafo): return 1 else: assert type(T) is tuple, "invalid trafo type" m = len(T) assert m > 0, "trafo cannot be empty tuple" return m-1 def factor(T,k): if T == None: assert False, "trafo {None} has no factors" elif isinstance(T, trafo.Trafo): assert k == 0, "a {Trafo} object has only one factor" return T else: assert type(T) is tuple, "invalid trafo type" m = len(T) assert m > 0, "trafo cannot be empty tuple" assert 0 <= k and k < m-1, "invalid trafo factor index" kinv = T[0] assert type(kinv) is int, "invalid trafo inversion bit" if kinv == +1: return T[k+1] elif kinv == -1: return inv(T[m-1-k]) else: assert False, "invalid trafo inversion bit" def nfactors(T): # The argument {T} must be a trafo. Returns the number of factors # that appear in the representation of {T}. # # Specifically, if {T} is {None}, returns 0. If {T} is a {Trafo} object, returns 1. # Otherwise {T} must be a tuple {(s,...)} where {s} is {+1} or {-1}; then # the procedure returns 1 less than the length of that tuple. return trafo_IMP.nfactors(T) def factor(T,k): # The arguments {T} and {k} must be a trafo and an integer in {0..n-1} # where {n = nfactors(T)}. Returns the factor of {T} with index {k}. # # Specifically, if {n} is zero, the procedure fails. If {T} is # a {Trafo} object and {k=0}, returns {T}. Otherwise {T} must be a tuple # {(s,...)} where {s} is {+1} or {-1}; then the procedure returns # {T[k+1]} or {inv(T[n-1-k])}, respectively. return trafo_IMP.factor(T,k) def to_string(T): # Returns a string representation of {T}. {None} is # represented as "*", a {Trafo} object is represented by # its name, and other types are represented as "(kinv T1 T2...)" # where {kinv} is the inversion bit and {T1,T2} are # the string representations of its factors, in the order # they are stored. return trafo_IMP.to_string(T) def to_string(T): if T == None: return "*" elif isinstance(T, Trafo_IMP): name = T.get_name() if name == None: name = str(T) return name else: kinv = T[0] s = "( %+2d" % kinv m = len(T) for k in range(m-1): s = s + " " + to_string(T[k+1]) s += " )" return s # ---------------------------------------------------------------------- def ball(p, q): assert p != q, "points must be distinct" d = len(p) # Let {r(t)} be {t*p + (1-t)*q}. # Determine the coefficients of the quadratic formula # {A t^2 + B t + C} for the characteristic function # {|r(t)|^2 - 1} v = rn.sub(q, p) A = rn.dot(v,v) B = 2*rn.dot(v,p) C = rn.dot(p,p) - 1 assert A > 0 t0, t1 = hacks.real_quadratic_roots(A, B, C) if t0 == None and t1 == None: # Line does not cross the circle: f = rootray.vacuum() else: assert t0 != None and t1 != None assert t0 < t1 if t0 == -inf and t1 == +inf: # Roots are very far apart: f = rootray.plenum() elif t0 == -inf: # Line entrance is very far in the past: f = (-1, [t1]) elif t1 == +inf: # Line exit is very far in the future: f = (+1, [t0]) else: f = (+1, [t0, t1]) return f def cylinder(p, q, m): d = len(p) assert 0 <= m and m <= d if m == 0: f = rootray.plenum() elif m == d: f = ball(p, q) else: pm = p[0:m]; qm = q[0:m]; if pm == qm: # Line {p--q} is parallel to the cylinder: Rp = rn.norm_sqr(pm) if Rp > 1: # Line is outside: f = rootray.vacuum() elif RP < 1: # Line is inside: f = rootray.plenum() else: # Line is on surface: pretend it crosses at {t = 0}: f = (+1, [0]) else: f = ball(pm, qm) return f def cone(p, q, m): assert p != q, "points must be distinct" d = len(p) assert 0 <= m and m <= d if m == 0: f = rootray.plenum() elif m == d: f = rootray.vacuum() else: # Let {r(t)} be {t*p + (1-t)*q}. # Determine the coefficients of the quadratic formula # {A t^2 + B t + C} for the characteristic function # {|r(t)|^2 - 1} v = rn.sub(q, p) vm = v[0:m]; vn = v[m:] pm = p[0:m]; pn = p[m:] A = rn.dot(vm,vm) - rn.dot(vn,vn) B = 2*(rn.dot(vm,pm) - rn.dot(vn,pn)) C = rn.dot(pm,pm) - rn.dot(pn,pn) if A == 0 or abs(A) < 1.0e-16: # Line is on the surface? if B == 0 or abs(B) < 1.0e-16: if C > 0: f = rootray.vacuum() elif C < 0: f = rootray.plenum() else: f = (+1, [0]) else: s = -1 if B > 0 else +1 f = (s, [ -C/B ]) else: t0, t1 = hacks.real_quadratic_roots(A, B, C) if t0 == None and t1 == None: # Line does not cross the cone: f = rootray.vacuum() else: assert t0 != None and t1 != None assert t0 < t1 s = -1 if A < 0 else +1 if t0 == -inf and t1 == +inf: # Roots are very far apart: f = (-s, []) elif t0 == -inf: # Line entrance is very far in the past: f = (-s, [t1]) elif t1 == +inf: # Line exit is very far in the future: f = (+s, [t0]) else: f = (+s, [t0, t1]) return f def raytrace_contour_forest(ROOTS, p, q, sgn, ind): # Expects each root contour to be CCW (island) if sgn = -1, CW (hole) if sgn = +1 names = [ path.get_name(ocr) for ocr in ROOTS ] sys.stderr.write("%*s> forest roots = %s sgn = %+d\n" % (2*ind,"",str(names),sgn)) f = rootray.vacuum() if sgn == +1 else rootray.plenum() for ocr in ROOTS: f1 = raytrace_contour_tree(ocr, p, q, sgn, ind+2) assert f1[0] == sgn if sgn == +1: # Trees are islands: f = rootray.union(f, f1) else: # Trees are holes: f = rootray.intersection(f, f1) sys.stderr.write("%*s< forest f = %s\n" % (2*ind,"",str(f))) return f # ---------------------------------------------------------------------- def raytrace_contour_tree(ocr, p, q, sgn, ind): sys.stderr.write("%*s>tree ocr = %s sgn = %+d\n" % (2*ind,"",path.get_name(ocr),sgn)) PTS = path.points(ocr) fr = rootray_cart.prism(p, q, PTS) ori = hacks.poly_orientation(PTS) sys.stderr.write("%*sfr = %s\n" % (2*ind+2,"",str(fr))) assert fr[0] == sgn; CHILDS = path.inner_contours(ocr) fc = raytrace_contour_forest(CHILDS, p, q, -sgn, ind+2) sys.stderr.write("%*sfc = %s\n" % (2*ind+2,"",str(fc))) if sgn == +1: # Root is an island: fo = rootray.intersection(fr, fc) else: # Root is a hole: fo = rootray.union(fr, fc) sys.stderr.write("%*s ya - eps and y < yz + eps: # Compute left edge point {xpa} if y < yq1 and y < yk + eps: xpa = xn - circle_x(y, yr, ro) elif y >= yq1 and y <= yk + eps: xpa = xt + circle_x(y, yt, ri) elif y >= yk + eps and y <= yq3: xpa = xm - circle_x(y, ym, ro) elif y >= yq3 and y <= yq4: xpa = xq4 - (yq4 - y)*cms/sms else: xpa = xs - circle_x(y, ys, ro) XS.append(xpa) # Compute the right edge point {xpf}: if y <= yr: xpf = xi + circle_x(y, yr, ro) elif y > yr and y <= ys: xpf = xj else: xpf = xi + circle_x(y, ys, ro) XS.append(xpf) if y > yk + eps and y < yh - eps: # Add points in filet: if y < yq2: xpi = xm + circle_x(y, ym, ro) else: xpi = xt - circle_x(y, yt, ri) XS.append(xpi) if y < yq1: xpj = xn - circle_x(y, yr, ro) else: xpj = xt + circle_x(y, yt, ri) XS.append(xpj) if y > yu + eps and y < yz + eps: # Compute points notch sides {xpb,xpc}: if y <= yr: xpb = xn - circle_x(y, yr, ri) xpc = xn + circle_x(y, yr, ri) elif y > yr and y <= ys: xpb = xc xpc = xf else: xpb = xs + circle_x(y, ys, ro) xpc = xg - circle_x(y, ys, ro) XS.append(xpb) XS.append(xpc) if y > yu + eps and y < yv - eps: # Compute points slot sides {xpd,xpe}: if y < yr: xpd = xg - circle_x(y, yr, ri) xpe = xg + circle_x(y, yr, ri) elif y > ys: xpd = xg - circle_x(y, ys, ri) xpe = xg + circle_x(y, ys, ri) else: xpd = xg - ri xpe = xg + ri XS.append(xpd) XS.append(xpe) if y > yd + eps and y < ye - eps: # Append abscissas of endpoints on hole of hole col 1: xlo = xm - circle_x(y, ym, ri) xhi = xm + circle_x(y, ym, ri) XS.append(xlo) XS.append(xhi) if y > yf + eps and y < yg - eps: # Append abscissas of endpoints on leftmost hole: xlo = xs - circle_x(y, ys, ri) xhi = xs + circle_x(y, ys, ri) XS.append(xlo) XS.append(xhi) # Sort endpoints left to right: XS.sort() return XS # ---------------------------------------------------------------------- def test_from_contours_aux(OCRS, xdir,ydir, tag): ystep = wd_fill yphase = 0.48*wd_fill sys.stderr.write("... sub-test %s...\n" % tag) OPHS = raster.from_contours(OCRS, xdir, ydir, ystep, yphase, mp_fill) plot(OPHS, [], [], "test_from_contours_" + tag) return # ---------------------------------------------------------------------- def make_rect_contour(name, plo, phi, mag, sgn): # Makes a closed rectangular path with those two corners, scaled by {mag}. # If {sgn} is {+1} the path is CCW, if {sgn} is {-1} it is CW. PTS = [ plo, (phi[0],plo[1]), phi, (plo[0],phi[1]), plo ] PMS = [ rn.scale(mag, p) for p in PTS ] ph = path.from_points(PMS, mp_cont, mp_jump) path.set_name(ph, name) if sgn > 0: return ph else: return path.rev(ph) # ---------------------------------------------------------------------- def make_circ_contour(name, ctr, R, mag, sgn): ph = path_example.circle(rn.scale(mag, ctr), mag*R, 11, 0.1, mp_cont, 0) path.set_name(ph, name) if sgn > 0: return ph else: return path.rev(ph) # ---------------------------------------------------------------------- def test_from_contours(): sys.stderr.write("--- testing {from_contours} ---\n") # Some nested contours: A = make_rect_contour("A", ( 0, 0), (16,10), 3, +1) B = make_rect_contour("B", ( 1, 1), ( 2, 9), 3, -1) C = make_rect_contour("C", ( 3, 1), (10, 9), 3, -1) D = make_rect_contour("D", (11, 6), (15, 9), 3, -1) E = make_circ_contour("E", (13, 3), 2, 3, -1) F = make_rect_contour("F", ( 4, 6), ( 9, 8), 3, +1) G = make_rect_contour("G", ( 4, 2), ( 5, 5), 3, +1) H = make_rect_contour("H", ( 6, 2), ( 9, 5), 3, +1) I = make_rect_contour("I", ( 7, 3), ( 8, 4), 3, -1) J = make_circ_contour("J", (13, 3), 1, 3, +1) U = make_rect_contour("U", (17, 0), (19, 4), 3, +1) V = make_rect_contour("V", (17, 8), (19,10), 3, +1) xdir = ( 1, 0 ) ydir = ( 0, 1 ) # ---------------------------------------------------------------------- OCRS = [ J ] test_from_contours_aux(OCRS, xdir,ydir, "circ") # ---------------------------------------------------------------------- OCRS = [ A, U ] test_from_contours_aux(OCRS, xdir,ydir, "2sqr") # ---------------------------------------------------------------------- OCRS = [ A, C ] test_from_contours_aux(OCRS, xdir,ydir, "sqhole") # ---------------------------------------------------------------------- OCRS = [ A, path.rev(F) ] test_from_contours_aux(OCRS, xdir,ydir, "revhole") # ---------------------------------------------------------------------- xdir = ( cos(pi/6), sin(pi/6) ) ydir = ( -xdir[1], +xdir[0] ) # Rotate the paths so sides are parallel to {xdir,ydir}: OCRS = [] for oph in [ A, B, C, D, E, F, G, H, I, J, U, V ]: PS = path.points(oph); PSrot = [ rn.mix(p[0], xdir, p[1], ydir) for p in PS ] ophrot = path.from_points(PSrot, mp_cont, mp_jump) path.set_name(ophrot, path.get_name(oph)) OCRS.append(ophrot) test_from_contours_aux(OCRS, xdir,ydir, "bigtilt") return # ---------------------------------------------------------------------- iylo = ceil(ya/wdf - eps) # Index of lowest scanline that intercepts {D} iyhi = floor(yz/wdf + eps) # Index of highest scanline that intercepts {D} nsc = iyhi - iylo + 1 # Number of scan-lines in filling. sys.stderr.write("filling has %d scan-lines {%d..%d}\n" % (nsc, iylo, iyhi)) for diy in range(nsc): y = (iylo + diy)*wdf # Ordinate of axis of scan-line. # Compute raster endpoint abscissas on outer perimeter of {D} at ordinate {y}: XSi = cut_region_by_scan_line(y, 0) ?? # Compute the bounding box of the contours, in the {xdir,ydir} system: ?? B = None ?? for ocr in OCRS: ?? for p in path.points(ocr): ?? x = rn.dot(p, xdir) ?? y = rn.dot(p, ydir) ?? B = rn.box_include_point(B, (x, y)) # Collect pairs and store in PTRS: assert len(XSi) % 2 == 0 nr = len(XSi)//2 # Number of raster traces in scanline. RS = [] for jr in range(nr): xlo = XSi[2*jr] xhi = XSi[2*jr + 1] assert xlo <= xhi RSj = ((xlo, y), (xhi, y)) RS.append(RSj) PTRS.append(RS) sty = [ pyx.style.linecap.round, pyx.style.linejoin.round, ] sty = sty + [ pyx.style.linewidth(wd), clr ] if dashed: sty = sty + [ pyx.style.linestyle(pyx.style.linecap.round, pyx.style.dash([2])) ] c.stroke(pyx.path.line(p[0], p[1], q[0], q[1]), sty) def make_colors(nc, Ymin, Ymax, Tmin, Tmax): # Used by {trace_colors,link_colors} colors = [None]*nc if nc == 1: colors[0] = pyx.color.rgb(0.200, 0.600, 0.000) else: # Fill half the table with palette colors, half with complements: nh = nc//2 for k in range(nh): RGBk = palette.discrete(k, Ymin,Ymax,Tmin,Tmax) colors[k] = pyx.color.rgb(RGBk[0], RGBk[1], RGBk[2]) colors[nc-1-k] = pyx.color.rgb(1-RGBk[0], 1-RGBk[1], 1- RGBk[2]) if nc % 2 == 1: # Insert a gray color in the middle entry: colors[nh] = pyx.color.rgb(0.500,0.500,0.500) return colors # ---------------------------------------------------------------------- def discrete(k, Ymin, Ymax, Tmin, Tmax): # Returns the color of index {k} in a sequence of colors chosen to be # as distinct as possible from each other. # # The brightness of the colors will vary between {Ymin} and {Ymax}, # and their relative saturation (in the {YHT} color system) between # {Tmin} and {Tmax}. The {Y} and {T} values must be in {[0.005 _ # 0.995]}. return palette_IMP.discrete(k, Ymin, Ymax, Tmin, Tmax) def discrete(k, Ymin, Ymax, Tmin, Tmax): assert 0 <= Ymin and Ymin <= Ymax and Ymax <= 1.000, "invalid {Ymin,Ymax}" assert 0 <= Tmin and Tmin <= Tmax and Tmax <= 1.000, "invalid {Tmin,Tmax}" # Point on the torus, hopefully far away from other colors: phi = (sqrt(5)-1)/2 azm = k*phi # Azimuth, longitude: angle (turns) on the medial circle. ele = k # Elevation, latitude: angle (radians) on meridian circles. # Compute the {Y} of the ideal color, in log scale: rYi = (1 + sin(ele))/2 Ybias = 0.010 Yi = exp((1-rYi)*log(Ymin+Ybias) + rYi*log(Ymax+Ybias)) - Ybias Y = min(1, max(0, Yi)) # Just in case. assert Ymin <= Y and Y <= Ymax # Compute the {T} of the ideal color, in log scale: rTi = (1 + cos(ele))/2 Tbias = 0.010 Ti = exp((1-rTi)*log(Tmin+Tbias) + rTi*log(Tmax+Tbias)) - Tbias T = min(1, max(0, Ti)) # Just in case. assert Tmin <= T and T <= Tmax # Compute the hue {H}: H = azm - floor(azm) # Convert to {RGB} coordinates: YHT = ( Y, H, T ) YHS = color.YHS_from_YHT(YHT) YUV = color.YUV_from_YHS(YHS) RGB = color.RGB_from_YUV(YUV) RGB = tuple( min(1, max(0, RGB[k])) for k in range(3) ) # Just in case. # Paranoia: Ycheck = color.Y_from_RGB(RGB) assert Ymin - 1.0e-3 <= Ycheck <= Ymax + 1.0e-3 return RGB # ---------------------------------------------------------------------- def test_discrete(c, ns, dp, szx_sm, szy_sm, Ymin, Ymax, Tmin, Tmax): # Plots {ns} samples of {palette.discrete} on {c} starting at position {dp}. # Each sample has height {szx_sm, szy_sm} with 1 unit of space below and above. sys.stderr.write("--- testing{discrete} ---\n") sys.stderr.write("Y = [ %5.3f _ %5.3f ] S = [ %5.3f _ %5.3f ]\n" % (Ymin,Ymax,Tmin,Tmax)) for k in range(ns): RGBk = palette.discrete(k, Ymin, Ymax, Tmin, Tmax) sys.stderr.write("k = %d RGBk = ( %5.3f %5.3f %5.3f )" % ((k,) + RGBk)) plot_sample(c, k, RGBk, dp, szx_sm, szy_sm, ns) YUVk = color.YUV_from_RGB(RGBk) sys.stderr.write(" YUVk = ( %5.3f %+6.3f %+6.3f )" % YUVk) YHSk = color.YHS_from_YUV(YUVk) sys.stderr.write(" YHSk = ( %5.3f %5.3f %5.3f )\n" % YHSk) assert Ymin-1.0e-6 <= YUVk[0] and YUVk[0] <= Ymax+1.0e-6 plot_tic(c, 0.0, dp, szx_sm, szy_sm, ns) return # ---------------------------------------------------------------------- def multi_raster_roads_OLD(nrd, nbc, nmv, mp_fill): # Returns two results: a list {BCS} of # zero or more {Block} objects, and a list {CTS} of zero or more # {Contact} objects. # # Each block of the returned set {BCS} will be either a single raster # line in its two possible directions, or a serpentine path of raster # lines linked by short vertical traces, in its four different orders # and directions. # # The main part of the input will be {nrd} "roads", side by side, each # consisting of a stack of {nbc} blocks with {nmv} raster lines each. # There will be also a horizontal raster line {H0} below and spanning # these roads, and another one {H1} at the top. # # All rasters and links will have parameters {mp_fill}, and will be # spaced vertically by {wdf = move_parms.width(mp_fill)}. The width of # each road and the gap between the roads will be fixed multiples of # {wdf}. Some {Move} objects may be shared by two or more choices of # the same block. # # There will be a contact between every pair or blocks, including {H0} # and {H1}, that are adjacent (have traces on successive scanlines and # overlapping {X} ranges). return contact_example_IMP.multi_raster_roads_OLD(nrd, nbc, nmv, mp_fill) def multi_raster_roads_OLD(nrd, nbc, nmv, mp_fill): wdf = move_parms.width(mp_fill) # In multiples of {wdf}, relative to {org}: nx_rd = 4 # Width of each road. nx_gp = 3 # Width of gap between roads. ixstep_rd = nx_rd + nx_gp # Scan-column step between roads. nxtot_rd = (nrd-1)*ixstep_rd + nx_rd # Total width of roads. nytot_rd = nbc*nmv # Total height of roads. org = (2,2) # Lower left corner of leftmost road. H0 = make_raster_block(org, 0, nxtot_rd-1, -1, -1, mp_fill, mp_jump) H1 = make_raster_block(org, 0, nxtot_rd-1, nytot_rd,nytot_rd, mp_fill, mp_jump) BCS = [ H0, H1 ] CTS = [] for ird in range(nrd): ix0 = ird*ixstep_rd ix1 = ix0 + nx_rd - 1 bc_prev = H0 for ibc in range(nbc): iy0 = ibc*nmv iy1 = iy0 + nmv - 1 bc_this = make_raster_block(org, ix0, ix1, iy0, iy1, mp_fill, mp_jump) BCS.append(bc_this) # ??? Should use {seam.from_blocks} ??? oph_prev = block.choice(bc_prev, 0) oph_this = block.choice(bc_this, 0) ct = contact_example.raster_raster_contact(oph_prev, oph_this) CTS.append(ct) bc_prev = bc_this oph_prev = block.choice(bc_prev, 0) oph_H1 = block.choice(H1, 0) ct = contact_example.raster_raster_contact(oph_prev, oph_H1) CTS.append(ct) return BCS, CTS # ---------------------------------------------------------------------- def test_multi_raster_roads(nrd, nbc, nmv): sys.stderr.write("--- testing {multi_raster_roads} nrd = %d nbc = %d nmv = %d ---\n" % (nrd,nbc,nmv)) tag = "multi_raster_roads_nrd%02d_nbc%02d_nmv%03d" % (nrd,nbc,nmv) BCS, CTS = contact_example.multi_raster_roads(nrd, nbc, nmv, mp_fill) block.show_list(sys.stderr, BCS, True, 2) contact.show_list(sys.stderr, CTS, 2) # ??? seam.print_contact_table(sys.stderr, BCS, CTS, None) o = (1,1) deco = True ct_arrows = False plot_blocks(tag, BCS, CTS, deco, ct_arrows) return # ---------------------------------------------------------------------- test_multi_raster_roads(3,3,4) # ---------------------------------------------------------------------- # from move.py def connector_extime(omv_prev, omv_next, use_jump, use_link, mp_jump): # Same as {connector}, but only computes the execution time of the # connector, without creating it. # # If the insertion of the connector would create new trace/jump # transtitions at the end of {omv_prev} or at the start of {omv_next}, # the corresponting time penalties will be added to the result. return move_IMP.connector_extime(omv_prev, omv_next, use_jump, use_link, mp_jump) def connector_parameters(omv_prev, omv_next, use_jump, use_link, mp_jump): # Returns the {Move_Parms} object that {connector} should use. # It will be either {mp_jump}, or the common parameters object # of {omv_prev} and {omv_next}. return move_IMP.connector_parameters(omv_prev, omv_next, use_jump, use_link, mp_jump) def connector_must_be_jump(vpq, v_prev, v_next, dmax, mp_jump, mp_link): # Returns {True} if a connecting move between two traces with the same # parameters is better be a jump than a trace of width {wd}. Assumes # that {vpq} is the displacement vector to be covered by the # connector, {v_prev,v_next} are the displacement vectors of the # previous and _next traces, {dmax} is the maximum allowed link length, # and {mp_jump,mp_link} are the {Move_Parms} objects to be used in each case. return move_IMP.connector_must_be_jump(vpq, v_prev, v_next, dmax, mp_jump, mp_link) def connector_extime(omv_prev, omv_next, use_jump, use_link, mp_jump): mp = connector_parameters(omv_prev, omv_next, use_jump, use_link, mp_jump) p = pfin(omv_prev) q = pini(omv_next) dpq = rn.dist(p, q) tex = move_parms.nozzle_travel_time(dpq, None, mp) tex += move_parms.transition_penalty(parameters(omv_prev), mp) tex += move_parms.transition_penalty(mp, parameters(omv_next)) return tex if quick: # Check the min fabtime to cover {ct} by any path that includes the move on the other side: tcM = +inf mv = contact.side(ct, 1-i) # The move on the other side of {ct} tcov_mv = contact.side_tcov(ct, i-1) for omv, tcov_omv in (mv, tcov_mv), (move.rev(mv), move.fabtime(mv) - tcov_mv): # Compute the min time to go from end of {oph} to start of {omv}: tconn_omv = path.min_connection_time(oph, path.from_moves([omv,]) tcM = min(tcM, tconn_omv + tcov_omv) if tc0 + tcM > tc_lim: return +inf # Time to close {ct} with ANY extension exceeds limit. mpc = move.connector_parameters(mvp, mvq, mp_link, mp_jump) tex = move.connector_extime(mvp, mvq, mp_link, mp_jump) assert tex == move.extime(cn) \ + move_parms.transition_penalty(move.parameters(mvp), mpc) \ + move_parms.transition_penalty(mpc, move.parameters(mvq)) # Since both moves are traces with same parameters, the decision must have been geometric: p0, p1 = move.endpoints(mvp) q0, q1 = move.endpoints(mvq) vpq = rn.sub(q0, p1) v_prev = rn.sub(p1, p0) v_next = rn.sub(q1, q0) dmax = 3*move_parms.width(mp) jmp = move.connector_must_be_jump(vpq, v_prev, v_next, dmax, mp_jump, mp) assert jmp == (mpc == mp_jump) def plot_link_elis(c, oph, clr, wd): # ??? contact_IMP.plot_link_elis(c, oph, clr, wd) def plot_link_elis(c, oph, clr, wd): for k in range(path.nelems(oph)): omvk = path.elem(oph, k) move.plot_layer(c, omvk, None, clr, wd, False, 0, 0) return def est_rcool_open(oph, ct): # The parameter {oph} must be an oriented path and {ct} must be a # {Contact} object.] # # If the path covers exactly one side of {ct}, returns the cooling time of {ct}, # if the slice were to be fabricated using the path {oph}, divided by {tcool_limit(ct)}. # If the result is greater than 1, the path is invalid, because it exceeds the cooling time # of that contact. # # If the path does not cover both sides of {ct}, returns None. # # Namely, let {covtimes(oph, ct)} be {(t0,t1)}. If either of those # times is {None}, returns {None}. Otherwise returns {abs(t0-t1)/tcool_limit(ct)}. # 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.est_rcool_open(oph, ct) def max_rcov(oph, CTS): assert type(CTS) is list or type(CTS) is tuple rcmin = +inf tfab_oph = path.fabtime(oph) for ct in CTS: tcs = path_tcovs(oph, ct) if (tcs[0] == None) != (tcs[1] == None): i = 0 if tcs[0] != None else 1 # Which side is covered # Min time {tcj_min} to cover the other side after {oph}> tcj_min = side_tcov(ct,1-i) tcj_min = min(tcj_min, move.fabtime(side(ct,1-i)) - tcj_min) rci = (tfab_oph - tcs[i] + tcj_min)/tcool_limit(ct) if rci < rcmin: rcmin = rci return rcmin def min_max_rcool(CTS, BCS): rc_max = 0 for ct in CTS: rcmin = blocks_min_rcool(BCS, ct) assert rcmin < +inf, "some side of a contact is not covered by {BCS}" if rcmin > rc_max: rc_max = rcmin return rc_max # ---------------------------------------------------------------------- # ELIS def get_raster_links(ct): # ??? return contact_hp_IMP.get_raster_links(ct) def set_raster_links(ct, rl0, rl1): # ??? return contact_hp_IMP.set_raster_links(ct, rl0, rl1) # ELIS def get_raster_links(ct): return ct.rl # ---------------------------------------------------------------------- def set_raster_links(ct, rl0, rl1): ct.rl = (rl0, rl1) return # ---------------------------------------------------------------------- def compare_contact_lists(CTS, CTS_re): # Compare the original and readback contacts: nct = len(CTS) CTS = sorted(CTS, key = contact.pmid) CTS_re = sorted(CTS_re, key = contact.pmid) for ict in range(nct): cti = CTS[ict] # contact.show(sys.stderr, "orig: ", cti, "\n", 0) cti_re = CTS_re[ict] # contact.show(sys.stderr, "rere: ", cti_re, "\n", 0) compare_contacts(cti, cti_re, True) return # ---------------------------------------------------------------------- for ct in CTS: bc0 = contact_hp.side_block(ct, 0) bc1 = contact_hp.side_block(ct, 1) done0 = None if bc0 == None else block_hp.used_choice(bc0) done1 = None if bc1 == None else block_hp.used_choice(bc1) if done0 != None and done1 != None: # Contact {ct} is closed by {Q}: clr = clr_C if (done0 == None) != (done1 == None): # Contact is active (exctly one side covered by {Q}): clr = clr_A elif done0 == None and done1 == None: # Contact {ct} is still inactive (neither side closed by {Q}): clr = clr_I contact.plot_single(c, ct, None, clr, wd = wd_contact, sz_tic = 0, arrow = False) def plot_choice(c, bc, ich, dp, clr, rwd, wd_axes, axes, dots, arrows, matter, links, contacts): # Plots onto the {pyx} canvas {c} the choice with index {ich} of block # {bc}, if there is such a choice. # # Each trace sausage is plotted by {plot_standard} with the {Pyx} # color {clr} and width {rwd*wd} where {wd} is its nominal width. If # {clr} is {None}, the procedure uses some default color. The # parameters {dp,wd_axes,axes,dots,arrows} are passed to # {plot_standard}. # # If {links} is true, also plots the link paths of the choices, # obtained {path.get_links}, with a fixed color. # # If {contacts} is true, also plots the contacts whose sides are on each choice, # obtained {path.get_contacts}, with a fixed color. # # If {matter} is true, the procedure plots the block's matter shadow # beneath the traces and jumps -- even if the block {bc} has no choice # with index {ich}. The parameters {wd_axes,axes,dots,arrows} are as in # {path.plot_standard}. block_IMP.plot_choice(c, bc, ich, dp, clr, rwd, wd_axes, axes, dots, arrows, matter, links, contacts) def plot_choice_list(c, BCS, ich, dp, CLRS, rwd, wd_axes, axes, dots, arrows, matter, links): # Plots choice {ich} of every {Block} object {bc} in the list {BCS} on # the {Pyx} canvas {c} with {plot_choice}. If {links} is true, also # plots the link paths of the choice, with a fixed color. # # If a block does not have a choice {ich}, and {matter} is true, shows # only its matter shadow. # # If {CLRS} is {None}, the traces are plotted with some default color. # Otherwise, if {CLRS} is a list with a single element which is a # {Pyx} color, they are all plotted with that color. Otherwise, {CLRS} # must be a list of {Pyx} colors with the same length as {BCS}, and # the choice of {BCS[i]} is plotted with color {CLRS[i]}. Tha parameters # {dp,rwd,wd_axes,axes,dots,matter} are passed to {plot_choice}. block_IMP.plot_choice_list(c, BCS, ich, dp, CLRS, rwd, wd_axes, axes, dots, arrows, matter, links) def plot_choice(c, bc, ich, dp, clr, rwd, wd_axes, axes, dots, arrows, matter, links, contacts): nch = block.nchoices(bc) if matter: plot_matter_shadow(c, bc, dp, links) if ich < nch: # sys.stderr.write(" plotting it\n") ophi = block.choice(bc, ich) path.plot_standard \ ( c, [ophi,], dp, None, [clr,], rwd=rwd, wd_axes=wd_axes, axes=axes, dots=dots, arrows=arrows, matter=False ) if links: clr_lk = pyx.color.rgb( 1.000, 0.700, 0.000 ) rwd_lk = 0.8*rwd axes_lk = False dots_lk = False for olkij in path.get_links(ophi) + path.get_links(path.rev(ophi)): path.plot_standard \ ( c, [olkij,], dp, None, [clr_lk,], rwd=rwd_lk, wd_axes=wd_axes, axes=axes_lk, dots=dots_lk, arrows=arrows_lk, matter=False ) if contacts: for isd in range(2): CTS = path.get_contacts(oph, isd) for ct in CTS: contact.plot_single(c, ct, dp, clr_ct, wd=wd_ct, sz_tic=sz_tic, arrow=arrow) return # ---------------------------------------------------------------------- def plot_choice_list(c, BCS, ich, dp, CLRS, rwd, wd_axes, axes, dots, arrows, matter, links): assert type(ich) is int and ich >= 0 nbc = len(BCS) if nbc == 0: # Nothing to plot: assert len(CTS) == 0 return # Plots the blocks: for ib in range(nbc): bc = BCS[ib] # Choose a color for this block: clr_bc = None if CLRS == None else CLRS[0] if len(CLRS) == 1 else CLRS[ib] plot_choice \ ( c, bc, ich, dp, clr_bc, rwd = rwd, wd_axes = wd_axes, axes = axes, dots = dots, arrows = arrows, matter = matter, links = links ) return # ---------------------------------------------------------------------- def test_plot_choice(): sys.stderr.write("--- testing {plot_choice} ---\n") tag = "plot_choice" bc = block_example.raster_rectangle((1,1), 5, 4, False, True, True, mp_fill, mp_jump) B = block.bbox([bc,], True, True) ich = 2 dp = (2,3) clr = pyx.color.rgb(1.000, 0.700, 0.000) rwd = 0.80 wd_axes = 0.05*wdf c, szx,szy = hacks.make_canvas(B, dp, True, True, 1, 1) links = True block.plot_choice(c, bc, ich, dp, clr, rwd, wd_axes, axes=True, dots=True, arrows=True, matter=True, links=links) fname = ("tests/out/block_TST_%s" % tag) hacks.write_plot(c, fname) return # ---------------------------------------------------------------------- def test_plot_choice_list(): sys.stderr.write("--- testing {plot_choice_list} ---\n") tag = "plot_choice_list" BCS = block_example.misc_D(mp_fill) nbc = len(BCS) CLRS = hacks.trace_colors(nbc) # Colors to use for different blocks. dp = (2,3) rwd = 0.80 wd_axes = 0.05*wdf B = block.bbox(BCS, True, True) c, szx,szy = hacks.make_canvas(B, dp, True, True, 1, 1) ich = 2 axes = True dots = True arrows = True matter = True links = True block.plot_choice_list(c, BCS, ich, dp, CLRS, rwd, wd_axes, axes, dots, arrows, matter, links) fname = ("tests/out/block_TST_%s" % tag) hacks.write_plot(c, fname) return # ---------------------------------------------------------------------- test_plot_choice() test_plot_choice_list() clt_ct = pyx.color.rgb(1.000, 0.050, 0.000) # Contact color. clr_cr = pyx.color.rgb(0.200, 1.000, 1.000) # Contour color. clr_lk = pyx.color.rgb(1.000, 0.800, 0.000) # Link color. dp = None # Plot the paths: rwd = 0.80 wd_axes = 0.15*min(wd_fill,wd_cont) # Width of jumps and axis lines. grid = True if debug: sys.stderr.write("bef sort: OLKS_org = %d, OLKS_rdb = %d\n" % (len(OLKS_org), len(OLKS_rdb))) if debug: sys.stderr.write("aft sort: OLKS_org = %d, OLKS_rdb = %d\n" % (len(OLKS_org), len(OLKS_rdb))) def connector(omv_prev, omv_next, mp_link, mp_jump): # Returns a {move.Move} object {mvcn} that connects the end of oriented # move {omv_prev} to the start of the oriented move {omv_next}. # # The connector may be either a jump or a single trace. The choice # will be based on the widths and directions of the two moves and the # distance between the endpoints. # # In the last case, the procedure uses a jump if either of the two # moves is a jump, or if they have different nominal widths and {mp_link} is {None}, or if the # distance to be spanned is large (compared to their nominal widths), # or if they are too short (ditto), or the turning angles from # {omv_prev} to {mvcn} and from {mvcn} to {omv_next) are both zero or # have opposite signs, or if a jump would be faster than a link. # # In any case, if a jump is chosen, it will have parameters {mp_jump}; # if {mp_jump} is {None}, the procedure returns {None} instead of the jump. # If the connector is certain to be a link, {mp_jump} may be {None}. # # If a link is chosen, it will have parameters {mp_link}, if it is not {None}; # otherwise it will have the same parameters as {omv_prev} and {omv_next}. return move_IMP.connector(omv_prev, omv_next, mp_link, mp_jump) def connector_parameters(omv_prev, omv_next, mp_link, mp_jump): assert mp_link == None or isinstance(mp_link, move_parms.Move_Parms) assert mp_jump == None or isinstance(mp_jump, move_parms.Move_Parms) p = pfin(omv_prev) q = pini(omv_next) # sys.stderr.write("p = ( %20.16f %20.16f ) q = ( %20.16f %20.16f )\n" % (p[0],p[1],q[0],q[1])) assert p != q mp_prev = parameters(omv_prev) mp_next = parameters(omv_next) if move_parms.is_jump(mp_prev) or move_parms.is_jump(mp_next): # Use a jump: mp = mp_jump elif mp_prev != mp_next and mp_link == None: # Use a jump: mp = mp_jump else: # Get the reference width: if mp_link != None: wd = move_parms.width(mp_link) else: assert mp_prev == mp_next wd = move_parms.width(mp_prev) # tentatively decide based on geometry: vpq = rn.sub(q, p) v_prev = rn.sub(p, pini(omv_prev)) v_next = rn.sub(pfin(omv_next), q) dmax = 3*wd if connector_must_be_jump(vpq, v_prev, v_next, dmax, mp_jump, mp_prev): mp = mp_jump # Even if it is {None} else: if mp_link != None: mp = mp_link else: assert mp_prev == mp_next mp = mp_prev return mp def connector_must_be_jump(vpq, v_prev, v_next, dmax, mp_jump, mp_link): debug = False dpq = rn.norm(vpq) # If the distance to be covered is too big, use a jump: if dpq >= dmax: if debug: sys.stderr.write("dist = %12.0f too big\n" % (dpq/dmax)) return True # If either of the adjacent moves is too short, use a jump: lmin = 0.9 * move_parms.width(mp_link) if rn.norm(v_prev) <= lmin or rn.norm(v_next) <= lmin: if debug: sys.stderr.write("dists = %12.f %12.6f too small\n" % (rn.norm(v_prev)/lmin, rn.norm(v_next)/lmin)) return True # Compute the sign of the turning angles: eps2 = 1.0e-6*dmax*dmax s1 = rn.cross2d(v_prev,vpq) s2 = rn.cross2d(vpq,v_next) if (s1 >= -eps2 and s2 <= +eps2) or (s1 <= +eps2 and s2 >= -eps2): # Angles have opposite signs, or nearly so -- use a jump: if debug: sys.stderr.write("signs = %12.f %12.6f not consistent\n" % (s1,s2)) return True # Risk a trace: return False def connector(omv_prev, omv_next, mp_link, mp_jump): mp = connector_parameters(omv_prev, omv_next, mp_link, mp_jump) if mp != None: p = pfin(omv_prev) q = pini(omv_next) mv = make(p, q, mp) else: mv = None return mv def test_connector(): sys.stderr.write("--- testing {connector} ---\n") nang = 7 # Number of angles for the second move. nq = 5 # Number of starting positions in {X} and {Y} for the second move. hq = (nq-1)/2 pstep = 1.5*wd_cont # {X,Y} increment between starting points. L = 6*wd_fill # Length of each move. R = ceil(L + (nq/2)*pstep) # max extent of second move p1 = (1+R, 1+R) # End of first move. p0 = rn.sub(p1, (L,0)) # Start of first move. B = ((1, 1), (1 + 2*R, 1 + 2*R)) def test_connector_one(c, dp, p0,p1, q0,q1, mp): # Tests a connector between the traces {p0-->p1} and {q0-->q1}. mvp = move.make(p0, p1, mp) mvq = move.make(q0, q1, mp) # Create a connector. Let it choose between move or jump: mp_link = mp_fill cn = move.connector(mvp, mvq, mp_link, mp_jump) if cn != None: assert move.pfin(mvp) == move.pini(cn) assert move.pfin(cn) == move.pini(mvq) # Plot style: wd_ref = 0.05*wd_fill rwd = 0.80 wd_axes = wd_ref clr_pq = pyx.color.rgb(0.050, 0.850, 0.000) clr_cn = pyx.color.rgb(0.000, 0.150, 0.850) # Draw the traces: move.plot_standard(c, [mvp,], dp, None, [clr_pq,], rwd, wd_axes, False, True, False, False) move.plot_standard(c, [mvq,], dp, None, [clr_pq,], rwd, wd_axes, False, True, False, False) if cn != None: move.plot_standard(c, [cn,], dp, None, [clr_cn,], rwd, wd_axes, False, True, False, False) return # .................................................................... for iang in range(nang): ang = 2*pi*iang/nang dp = (0, 0) c, szx,szy = hacks.make_canvas(B, dp, True, True, nq, nq) for iqx in range(nq): for iqy in range(nq): # Choose the parameter record: mp = mp_fill if (iqx+iqy) % 2 == 0 else mp_cont; # Avoid {q0--q1} crossing {p0--p1}: ok = True if iqy == hq and iqx <= hq: ok = False if iqy == hq and abs(ang - pi) < 1.0e-8: ok = False if iqy > hq and iqx < hq and ang > pi: ok = False if iqy < hq and iqx < hq and ang < pi: ok = False if ok: # Compute the endpoints of the second move: dpk = rn.add(dp, (iqx*szx, iqy*szy)) q0 = rn.add(p1, rn.scale(pstep, (iqx-hq,iqy-hq))) q1 = rn.add(q0, (L*cos(ang),L*sin(ang))) test_connector_one(c, dpk, p0,p1, q0,q1, mp) hacks.write_plot(c, "tests/out/move_TST_cn%02d" % iang) return # ---------------------------------------------------------------------- test_connector() def rebuild_snake_block_contacts(OPHS, ibc): # Given a list of raster-fill paths # !!! Should use # Get all the contacts {CTS} whose sides were used in this block: CTS = set() for ophi in OPHS: CTS = union(CTS, path.get_contacts(ophi)) # Fix the attachments of all those contacts: for ct in CTS: for ksd in range(2): # Get the paths that are attached to side {ksd} of {ct}. # These are either a single old raster path, possibly used in this block, # one or more choices of a previously conctructed block. SDPSk = contact.get_side_paths(ct, ksd) assert len(SDPSk) > 0 # Since the contact must originally have had have both sides on raster paths of {OPHSD}. # Set {ibck} to the index of the block that contains side {k} of {ct} (which may be {ibc}) # or to {None} if that side is still not in any block: ibck = None for phkj, drkj, imvkj in SDPSk: ibckj = path.get_group(phkj) if ibckj == ibc or ibckj == None: # This side of the contact is in one of original rasters, either one of those used by this block, # or one not yet used by any block: assert ibck == None and len(SDPSk) == 1 # Since there should be only one such raster. assert imvkj == 0 # Since those original rasters paths have length 1. else: # This side of the contact is in a choice of a previously constructed block. assert ibckj < ibc # Assuming blocks are numbered consecutively by creation. assert ibck == None or ibck == ibckj # Since all side paths must be in the same block. ibck = ibckj if ibck == ibc: # Path attachments on side {ksd} of {ct} must be changed from old raster to choices of new block: contact.clear_side_paths(ct, ksd) for phkj, drkj, imvkj in SDPSk: path.clear_contacts(phkj, ksd) # Attach side {ksd} of {ct} to the choices of {bc} mvk = contact.side_move(ct, ksd) for ich in range(block.nchoices(bc)): ochi = block.choice(bc, ich) jmvki = path.find_move(ochi, mvk) if jmvki != None: path.add_contact(ophi, ksd, ct) contact.add_side_path(ct, ksd, ophi, jmvki) return # ---------------------------------------------------------------------- # # Modifies the contact-path attachments. Any contact between # two raster traces used in the block will remain attached to the # original raster paths. Any contact between this block and other blocks # will be disconneted from the original raster path and attached # to the choices of the block. # # As a side effect, sets the group index of all original raster paths used in the # block to {ibc} path.set_group(ors, ibc) def rebuild_contact_path_attachments(OPHS, BCS): # Redefines the attachments between the raster paths of {OPHS} and their contacts # to refer to the chocies of the blocks in {BCS} istead. Assumes that the # moves in those blocks are a subset of the traces in {OPHS} # # The existing attachments between the raster paths of {OPHS} and their and # contacts, as given by {path.get_contacst} and # {contact.get_side_paths}, are erased. Contacts between rasters in # the same block are left unattached, and those between rasters of # different blocks are attached to the new snakes. # # As a side effect, the procedure sets the group index of every choice # of each block {bc} to the index of that blcok in {BCS} nbc = len(BCS) # Number of blocks # Assign all the choices of each block to a group equal to the block index: for ibc in range(len(BCS)): bc = BCS[ibc] for ich in range(block.nchoices(bc)): path.set_group(block.choice(bc, ich), ibc) ??? # Collect the contacts that have the two sides in different blocks: CTS_sel = [] for ct in CTS_org: ibc = [None, None] # {ibc[isd]} is the index of the block that contains side {isd} of {ct}. for isd in range(2): for phj, drj, imvj in contact.get_side_paths(ct, isd): ibcj = path.get_group(phj) assert ibcj != ibc_none, ("path attached to side %d of contact is not in any block" % isd) if ibc[isd] == None: ibc[isd] == ibcj else: assert ibc[isd] == ibcj, ("contact has side %d in two different blocks" % isd) assert ibc[j] != None, ("side %d of contact has no side paths" % isd) if ibc[0] != ibc[1]: CTS_sel.append(ct) return # ---------------------------------------------------------------------- SCS = raster.separate_by_scanline(OPHS, xdir,ydir, ystep,yphase) ??? plot_path_links? rwdl = style['rwd_link'] wd_axes = style['wd_axes'] axes = False dots = False arrows = False matter = False for isc in range(len(SCS)): SCSi = SCS[isc] # Rasters on scaline {isc} for irs in SCSi: ors = OPHS[irs] # A raster element on that scanline. ysc = path.pini(ors)[1] # Y coordinate of scanline. for olk in path.get_links(ors) + path.get_links(path.rev(ors)): # Orient the link to go up: if path.pini(olk)[1] > path.pfin(olk)[1]: olk = path.rev(olk) # Plot only links that go up from the current scanline: if path.pini(olk)[1] > ysc - 0.001*wd_axes: clk = (color['link0'], color['link1'])[isc%2] plot_paths(c, OPHS, dp, CLRS, rwd,deco, style,color)@@path.plot_standard(c, [olk,], dp, None, [clk,], rwdl, wd_axes, axes, dots, arrows, matter) elif fig == "contacts": c = plot_figure_contacts(subfig, style,color) def plot_figure_contacts(subfig, style,color): # Plots the figure that shows a smple path {OPHS[0]} with labels "pini", etc. # and its reverse. assert subfig == "basic", ("invalid subfig %s" % subfig) # Get the paths: mp_cont, mp_fill, mp_link, mp_jump = make_move_parms(); OPHS, CTS = paper_example_B.make_simple_contacts(mp_fill) nph = len(OPHS) nct = len(CTS) B = path.bbox(OPHS) # Bounding box of all move endpoints. B = rn.box_join(B, contact.bbox(CTS)) # Just paranoia. # Compute the figure's bounding box {Bfig}: Bfig = B # # Widen {Bfig} symmetrically to standard "math figure" widtdh: # Bfig = widen_box_for_math_figure(Bfig, style) autoscale = False c = make_figure_canvas(Bfig, autoscale, style,color) dp = (0, 0) # Plot the matter shadow: plot_trace_matter(c, OPHS, dp, style,color) # Plot the paths: rwdf = style['rwd_fill'] clr = color['fill'] deco = False plot_paths(c, OPHS, dp, [clr,], rwdf,deco, style,color) # Plot the contacts: interbc = False shadow = True trim = False plot_contacts(c, CTS, dp, interbc, shadow,trim, style,color) return c # ---------------------------------------------------------------------- tol = 0.20*max(wd0,wd1) # Tolerance for overlaps, tilts, etc. def max_rcool(oph): # Returns the maximum value of rcool(oph.ct) for every contact {ct} # that is closed by {oph}. Ignoers contacts that are covered on only # one side by {oph}. # # Uses the {path.get_contacts} function to locate those contacts. If # there are no such contacts, returns {-inf}. return contact_IMP.max_rcool(oph) def max_rcool(oph): # Get the contacts attached to {oph} that are closed by it: CTS0 = path.get_contacts(oph, 0) # Contacts whose side 0 is covered by {oph}. CTS1 = path.get_contacts(oph, 1) # Contacts whose side 1 is covered by {oph}. CTS = CTS0 & CTS1 # Contacts with both sides covered by {oph}. ph, dr = path.unpack(oph) rc_max = -inf for ct in CTS: tcs = path_tcovs(oph, ct) assert tcs[0] != None or tcs[1] == None if tcs[0] != None and tcs[1] == None tc = tcool_closed(ct, tcs) tc_lim = contact.tcool_limit(ct) assert tc_lim > 0 rc = 0 if tc_lim == +inf else tc/tc_lim if rc > rc_max: rc_max = rc return rc # ----------------------------------------------------------------------