# Last edited on 2021-06-04 04:09:50 by jstolfi 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) 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)) 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) 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'])