// isokit/streets: roads drawn along a centre line (straight roads, curves, // roundabout arms) with kerbs and raised sidewalks, and how streets meet: // - end to end they carry on seamlessly (a curve continues a road); // - a road ending on another road's side leaves a gap in that road's // sidewalk and kerb, and stops its own kerbs and sidewalks at it; // - two roads ending at one corner close it: each runs over the corner, the // inner sidewalks stop at the other road's kerb, the outer ones go round. // Everything lies on the unsorted ground layer, so nothing can upset the // depth sort; sidewalks are 12 cm high paving with their kerb face drawn // where it is turned to the viewer. fn walk_sides() { ["none", "both", "left", "right"] } fn walk_h() { 0.12 } fn street_kinds() { ["road", "road_curve", "roundabout"] } /// Sidewalk widths [left, right] of a street looking along its local +x. fn walk_widths(p) { let w = p.walk_w; if p.walk == "both" { [w, w] } else if p.walk == "left" { [w, 0.0] } else if p.walk == "right" { [0.0, w] } else { [0.0, 0.0] } } // ---------------------------------------------------------- polylines fn seg_len(a, b) { ((b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1])).sqrt() } fn path_len(c) { let l = 0.0; for i in 1..c.len() { l += seg_len(c[i - 1], c[i]); } l } fn rev(a) { let r = a; r.reverse(); r } /// The point at distance s along polyline c; before its start and past its /// end it goes on straight along the first and last segment. fn path_at(c, s) { let n = c.len(); if s <= 0.0 { let a = c[0]; let b = c[1]; let l = seg_len(a, b); return [a[0] + (b[0] - a[0]) / l * s, a[1] + (b[1] - a[1]) / l * s]; } let acc = 0.0; for i in 1..n { let a = c[i - 1]; let b = c[i]; let l = seg_len(a, b); if acc + l >= s || i == n - 1 { let t = (s - acc) / l; return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]; } acc += l; } c[n - 1] } /// The part of polyline c from distance s0 to s1 along it (either may lie /// beyond its ends). fn path_cut(c, s0, s1) { let out = []; if s1 - s0 < 0.01 { return out; } out.push(path_at(c, s0)); let acc = 0.0; for i in 0..c.len() { if i > 0 { acc += seg_len(c[i - 1], c[i]); } if acc > s0 + 0.01 && acc < s1 - 0.01 { out.push(c[i]); } } out.push(path_at(c, s1)); out } /// Polyline c moved sideways by off (positive: to the left of its way): /// each segment along its own normal, neighbours meeting at their mitre. fn path_offset(c, off) { let n = c.len(); let ns = []; for i in 1..n { let dx = c[i][0] - c[i - 1][0]; let dy = c[i][1] - c[i - 1][1]; let l = fmax((dx * dx + dy * dy).sqrt(), 1e-12); ns.push([-dy / l, dx / l]); } let out = []; for i in 0..n { let m = if i == 0 { ns[0] } else if i == n - 1 { ns[n - 2] } else { let a = ns[i - 1]; let b = ns[i]; let k = 1.0 / fmax(1.0 + a[0] * b[0] + a[1] * b[1], 0.2); [(a[0] + b[0]) * k, (a[1] + b[1]) * k] }; out.push([c[i][0] + m[0] * off, c[i][1] + m[1] * off]); } out } /// Points of the arc round (cx, cy), radius r, from angle a0 to a1 /// (radians, either way round): chords of at most 1.5 m and 6 degrees. fn arc_pts(cx, cy, r, a0, a1) { let da = (a1 - a0).abs(); let n = fmin(160.0, fmax(2.0, fmax((da * r / 1.5).ceiling(), (da / 0.105).ceiling()))).to_int(); let out = []; for i in 0..(n + 1) { let t = a0 + (a1 - a0) * i / n; out.push([cx + r * t.cos(), cy + r * t.sin()]); } out } // A street's centre line: #{ arc: false, pts } (a polyline) or #{ arc: true, // cx, cy, r, a0, a1 } (an arc from angle a0 to a1, either way round). Arcs // stay arcs when cut and moved sideways, so kerbs and sidewalks meet the // streets at their ends exactly (a polyline's end chord is a few degrees off). fn line_path(pts) { #{ arc: false, pts: pts } } fn arc_path(cx, cy, r, a0, a1) { #{ arc: true, cx: cx, cy: cy, r: r, a0: a0, a1: a1 } } fn pl_len(p) { if p.arc { p.r * (p.a1 - p.a0).abs() } else { path_len(p.pts) } } /// The part of centre line p from distance s0 to s1 along it. fn pl_cut(p, s0, s1) { if !p.arc { return line_path(path_cut(p.pts, s0, s1)); } let dir = if p.a1 >= p.a0 { 1.0 } else { -1.0 }; arc_path(p.cx, p.cy, p.r, p.a0 + dir * s0 / p.r, p.a0 + dir * s1 / p.r) } /// Plan points of centre line p moved sideways by off (positive: left of its way). fn pl_off(p, off) { if !p.arc { return path_offset(p.pts, off); } let dir = if p.a1 >= p.a0 { 1.0 } else { -1.0 }; arc_pts(p.cx, p.cy, p.r - dir * off, p.a0, p.a1) } /// [x, y, tx, ty] at the start (e = 0) or end (e = 1) of p: the point and /// the unit direction of the way there. fn pl_end(p, e) { if p.arc { let dir = if p.a1 >= p.a0 { 1.0 } else { -1.0 }; let a = if e == 0 { p.a0 } else { p.a1 }; return [p.cx + p.r * a.cos(), p.cy + p.r * a.sin(), -dir * a.sin(), dir * a.cos()]; } let c = p.pts; let n = c.len(); let a = if e == 0 { c[0] } else { c[n - 2] }; let b = if e == 0 { c[1] } else { c[n - 1] }; let l = seg_len(a, b); let q = if e == 0 { a } else { b }; [q[0], q[1], (b[0] - a[0]) / l, (b[1] - a[1]) / l] } /// [a, b] without the ranges in gaps: the pieces left, at least 5 cm long. fn ranges(a, b, gaps) { let out = [[a, b]]; for g in gaps { let next = []; for r in out { if g[1] <= r[0] || g[0] >= r[1] { next.push(r); continue; } if g[0] > r[0] { next.push([r[0], g[0]]); } if g[1] < r[1] { next.push([g[1], r[1]]); } } out = next; } out.filter(|r| r[1] - r[0] > 0.05) } // ---------------------------------------------------------- kerbs and sidewalks /// The faces up from line k to height h whose outward normal (to the right /// of k, or to its left) is turned to the viewer: polygons for poly(). fn kerb_faces(k, h, right) { let fs = []; for i in 1..k.len() { let a = k[i - 1]; let b = k[i]; let nx = b[1] - a[1]; let ny = a[0] - b[0]; if !right { nx = -nx; ny = -ny; } if vis(nx, ny) { fs.push([[a[0], a[1], 0.0], [b[0], b[1], 0.0], [b[0], b[1], h], [a[0], a[1], h]]); } } fs } /// A sidewalk between kerb line k (plan points with the road on their /// right) and its back line o (as many points, w to the left): its paving, /// back edge and the ends where caps says [start, end] (not where another /// sidewalk carries on). The kerb face goes with the kerb. fn walk_strip(k, o, caps) { if k.len() < 2 || o.len() != k.len() { return this; } let h = walk_h(); this.flat(k + rev(o), h, "g-walk t"); let fs = kerb_faces(o, h, false); let n = k.len(); if caps[0] { fs += kerb_faces([o[0], k[0]], h, true); } if caps[1] { fs += kerb_faces([k[n - 1], o[n - 1]], h, true); } if fs.len() > 0 { this.poly(fs, "kerbf"); } this } // ---------------------------------------------------------- where streets meet /// The open ends of street o in world plan coordinates: [#{ x, y, dx, dy, /// d, wl, wr }] with (dx, dy) the unit direction out of the street, d its /// carriageway width and wl, wr its sidewalk widths left and right looking out. fn street_ends(o) { let ends = []; let p = o.p; let add = |u, v, du, dv, d, wl, wr| { let a = world_of(o, u, v); let b = world_of(o, u + du, v + dv); ends.push(#{ x: a[0], y: a[1], dx: b[0] - a[0], dy: b[1] - a[1], d: d, wl: wl, wr: wr, id: o.id }); }; if o.type == "road" || o.type == "road_curve" { let ws = walk_widths(p); let c = if o.type == "road" { line_path([[-p.w / 2.0, 0.0], [p.w / 2.0, 0.0]]) } else { curve_line(p) }; let a = pl_end(c, 0); let b = pl_end(c, 1); add.call(a[0], a[1], -a[2], -a[3], p.d, ws[1], ws[0]); add.call(b[0], b[1], b[2], b[3], p.d, ws[0], ws[1]); } else if o.type == "roundabout" { let w = if p.walk { p.walk_w } else { 0.0 }; let R = p.r + p.arm_len; for a in round_arms(p) { let ca = a.cos(); let sa = a.sin(); add.call(R * ca, R * sa, ca, sa, p.road_d, w, w); } } ends } /// How street o meets the others at its end e (one of street_ends(o)): /// #{ asph, dash, kl, kr, wl, wr, free, meet }: how far its asphalt, centre /// line, kerbs and sidewalks (left and right looking out) reach past e /// (negative: they stop short of it), whether nothing meets it there, and /// the id of the street that does. fn street_join(o, e, others) { let j = #{ asph: 0.0, dash: 0.0, kl: 0.0, kr: 0.0, wl: 0.0, wr: 0.0, free: true, meet: "" }; // pieces that carry on overlap by a few cm: edge to edge, the ground shows through as a hairline let lap = 0.04; for f in others { if (f.x - e.x).abs() > 0.3 || (f.y - e.y).abs() > 0.3 { continue; } j.free = false; j.meet = f.id; let dot = e.dx * f.dx + e.dy * f.dy; if dot < -0.9 { j.asph = lap; j.kl = lap; j.kr = lap; j.wl = lap; j.wr = lap; } if dot.abs() > 0.2 { continue; } // a corner: the other street's body lies to the left (cross > 0) or right let left = e.dx * -f.dy - e.dy * -f.dx > 0.0; // its outer side is the one away from this street's body let mine_on_its_left = f.dx * -e.dy - f.dy * -e.dx > 0.0; let wo = if mine_on_its_left { f.wr } else { f.wl }; let h = f.d / 2.0; j.asph = h; if left { j.kl = -h; j.wl = -h; j.kr = h; j.wr = h + wo; } else { j.kr = -h; j.wr = -h; j.kl = h; j.wl = h + wo; } return j; } // ending on a road's side (a T): kerbs stop at its kerb, sidewalks at its sidewalk for c in objects_on(["road"], e.x, e.y, 0.3) { if c.id == o.id || c.v.abs() > 0.3 || c.u.abs() > c.p.w / 2.0 - 0.3 { continue; } let l = local_of(c, e.x - e.dx, e.y - e.dy); let ws = walk_widths(c.p); let h = c.p.d / 2.0; let w = if l[1] > c.v { ws[0] } else { ws[1] }; j.free = false; j.meet = c.id; j.dash = -h; j.kl = -h; j.kr = -h; j.wl = -(h + w) + 0.04; j.wr = -(h + w) + 0.04; return j; } j } /// Where the streets in `others` end on road o's sides: gaps [left, right] /// in its kerbs and sidewalks, as distance ranges along it from its start, /// then the ids of those streets. fn street_gaps(o, others) { let g = [[], [], []]; for f in others { let l = local_of(o, f.x, f.y); if l[1].abs() > 0.3 || l[0].abs() > o.p.w / 2.0 - 0.3 { continue; } let b = local_of(o, f.x - f.dx, f.y - f.dy); let s = l[0] + o.p.w / 2.0; let r = [s - f.d / 2.0, s + f.d / 2.0]; if b[1] > l[1] { g[0].push(r); } else { g[1].push(r); } g[2].push(f.id); } g } /// Tooltip rows of a street: its size, sidewalks and the streets it meets /// (ids, at its ends and along its sides). fn street_info(p, what, ends, sides) { info_row("Street", what); let ws = walk_widths(p); if ws[0] > 0.0 && ws[1] > 0.0 { info_row("Sidewalks", `${num1(p.walk_w)} m on both sides`); } else if ws[0] > 0.0 { info_row("Sidewalks", `${num1(p.walk_w)} m on the left`); } else if ws[1] > 0.0 { info_row("Sidewalks", `${num1(p.walk_w)} m on the right`); } let meets = []; for id in ends + sides { if id != "" && !meets.contains(id) { meets.push(id); } } if meets.len() > 0 { info_row("Meets", meets.reduce(|s, x| if s == "" { x } else { s + ", " + x }, "")); } } /// The ends of every other street on this level. fn other_street_ends(o) { let out = []; for c in objects_of(street_kinds()) { if c.id != o.id { out += street_ends(c); } } out } /// Draws street o along centre line c (a line_path or arc_path in its /// frame, in its way), its carriageway o.p.d wide, joined at its start and /// end as j0 and j1 (from street_join), with sidewalk gaps [left, right] /// (distance ranges along c). fn street_path(o, c, j0, j1, gaps) { let p = o.p; let L = pl_len(c); let h = p.d / 2.0; let ws = walk_widths(p); let s = sprite(); let ca = pl_cut(c, -j0.asph, L + j1.asph); s.flat(pl_off(ca, h) + rev(pl_off(ca, -h)), 0.007, "g-asph t"); if p.dashed && L + j1.dash + j0.dash > 0.05 { s.polyline(pl_off(pl_cut(c, -j0.dash, L + j1.dash), 0.0), 0.012, "dash"); } // side 0: left of the way (the start end's right, the end's left); side 1: right for side in [0, 1] { let sg = if side == 0 { 1.0 } else { -1.0 }; let k0 = if side == 0 { j0.kr } else { j0.kl }; let k1 = if side == 0 { j1.kl } else { j1.kr }; let w0 = if side == 0 { j0.wr } else { j0.wl }; let w1 = if side == 0 { j1.wl } else { j1.wr }; let ww = ws[side]; for r in ranges(-k0, L + k1, gaps[side]) { let line = pl_off(pl_cut(c, r[0], r[1]), sg * h); if side == 1 { line.reverse(); } if ww > 0.0 { let fs = kerb_faces(line, walk_h(), true); if fs.len() > 0 { s.poly(fs, "kerbf"); } } else if p.kerb { s.polyline(line, 0.012, "kerb"); } } if ww > 0.0 { let rs = ranges(-w0, L + w1, gaps[side]); for i in 0..rs.len() { let r = rs[i]; let cut = pl_cut(c, r[0], r[1]); let line = pl_off(cut, sg * h); let back = pl_off(cut, sg * (h + ww)); // caps where a piece ends at a gap or a free street end (sides run opposite ways) let at0 = i > 0 || j0.free; let at1 = i + 1 < rs.len() || j1.free; if side == 1 { line.reverse(); back.reverse(); } s.walk_strip(line, back, if side == 0 { [at0, at1] } else { [at1, at0] }); } } } s } // ---------------------------------------------------------- curves /// The centre line of curve p in its frame: from (0, 0) along +x, turning /// left (towards +y) or right by p.angle degrees on radius p.r. fn curve_line(p) { let A = p.angle * PI() / 180.0; let sg = if p.turn == "right" { -1.0 } else { 1.0 }; arc_path(0.0, sg * p.r, p.r, -sg * PI() / 2.0, -sg * PI() / 2.0 + sg * A) } fn meta_road_curve() { #{ label: "Road curve", category: "Ground", icon: "◜", help: "A curved road piece. It starts at its position heading along local +x (turn it in 90 degree steps) and bends left or right; its ends meet roads and other curves end to end. Drawing a road run corner by corner puts one in each corner.", props: [ #{ key: "r", type: "float", def: 10.0, min: 3.0, max: 300.0, step: 0.5, label: "Radius (m)" }, #{ key: "angle", type: "float", def: 90.0, min: 5.0, max: 180.0, step: 5.0, label: "Angle (deg)" }, #{ key: "turn", type: "choice", def: "left", choices: ["left", "right"], label: "Turns" }, #{ key: "d", type: "float", def: 6.0, min: 2.0, max: 40.0, step: 0.5, label: "Width (m)" }, #{ key: "dashed", type: "bool", def: true, label: "Centre line" }, #{ key: "kerb", type: "bool", def: true, label: "Kerb lines" }, #{ key: "walk", type: "choice", def: "none", choices: walk_sides(), label: "Sidewalks" }, #{ key: "walk_w", type: "float", def: 2.0, min: 0.8, max: 8.0, step: 0.1, label: "Walk width (m)" }, ] } } fn prefab_road_curve(o) { let p = o.p; if p.r <= p.d / 2.0 + 0.2 { warn(`${o.id}: radius ${p.r} m is too tight for a ${p.d} m road`); } layer("ground"); let c = curve_line(p); let others = other_street_ends(o); let ends = street_ends(o); let j0 = street_join(o, ends[0], others); let j1 = street_join(o, ends[1], others); street_path(o, c, j0, j1, [[], []]); street_info(p, `${num1(p.angle)} degree turn to the ${p.turn}, ${num1(p.r)} m radius, ${num1(p.d)} m wide`, [j0.meet, j1.meet], []); } // ---------------------------------------------------------- roundabouts /// The angles (radians, in the roundabout's frame) of its arms. fn round_arms(p) { let out = []; if p.arm_e { out.push(0.0); } if p.arm_n { out.push(PI() / 2.0); } if p.arm_w { out.push(PI()); } if p.arm_s { out.push(PI() * 1.5); } out } /// (x, y) turned by angle a. fn turned(a, x, y) { [x * a.cos() - y * a.sin(), x * a.sin() + y * a.cos()] } fn meta_roundabout() { #{ label: "Roundabout", category: "Ground", icon: "◎", help: "A roundabout with up to four arms (east, north, west, south in its frame) ending Radius + Arm length from its centre, where straight roads carry on. Kerbs flare into the ring; splitter islands and give-way lines sit at the entries (traffic on the left or the right).", props: [ #{ key: "r", type: "float", def: 14.0, min: 6.0, max: 60.0, step: 0.5, label: "Radius (m)" }, #{ key: "lane", type: "float", def: 7.0, min: 4.0, max: 16.0, step: 0.5, label: "Ring width (m)" }, #{ key: "lanes", type: "int", def: 1, min: 1, max: 2, step: 1, label: "Ring lanes" }, #{ key: "arm_e", type: "bool", def: true, label: "Arm east" }, #{ key: "arm_n", type: "bool", def: true, label: "Arm north" }, #{ key: "arm_w", type: "bool", def: true, label: "Arm west" }, #{ key: "arm_s", type: "bool", def: true, label: "Arm south" }, #{ key: "arm_len", type: "float", def: 10.0, min: 4.0, max: 80.0, step: 0.5, label: "Arm length (m)" }, #{ key: "road_d", type: "float", def: 6.0, min: 3.0, max: 20.0, step: 0.5, label: "Arm width (m)" }, #{ key: "island", type: "choice", def: "tree", choices: ["tree", "grass", "paved"], label: "Island" }, #{ key: "splitters", type: "bool", def: true, label: "Splitter islands" }, #{ key: "drive", type: "choice", def: "left", choices: ["left", "right"], label: "Traffic on the" }, #{ key: "walk", type: "bool", def: true, label: "Sidewalks" }, #{ key: "walk_w", type: "float", def: 2.0, min: 0.8, max: 8.0, step: 0.1, label: "Walk width (m)" }, ] } } fn prefab_roundabout(o) { let p = o.p; let r = p.r; let ri = fmax(1.5, r - p.lane); let h = p.road_d / 2.0; let R = r + p.arm_len; let ww = if p.walk { p.walk_w } else { 0.0 }; let arms = round_arms(p); // arm ends where a street carries on: no sidewalk end faces there let others = other_street_ends(o); let carried = |a| { let e = world_of(o, R * a.cos(), R * a.sin()); others.some(|f| (f.x - e[0]).abs() < 0.3 && (f.y - e[1]).abs() < 0.3) }; layer("ground"); let s = sprite(); // flared kerbs: a fillet of radius f between each arm edge and the ring let f = fmax(ww + 1.0, fmin(6.0, p.arm_len * 0.6)); let cx = ((r + f) * (r + f) - (h + f) * (h + f)).sqrt(); if cx > R - 0.3 { f = fmax(0.5, ww + 0.5); cx = ((r + f) * (r + f) - (h + f) * (h + f)).sqrt(); } let phi = plan_angle(cx, h + f); let xi = (r * r - h * h).sqrt(); // carriageway: ring, arms and the flares between them s.flat([arc_pts(0.0, 0.0, r, 0.0, 2.0 * PI()), arc_pts(0.0, 0.0, ri, 0.0, 2.0 * PI())], 0.007, "g-asph t"); // each arm with its flares is one outline reaching into the ring's // asphalt (pieces sharing an edge would show a hairline seam) let flares = []; for a in arms { let fr = turned(a, cx, -(h + f)); let fl = turned(a, cx, h + f); let out = [turned(a, R, -h), turned(a, cx, -h)]; out += arc_pts(fr[0], fr[1], f, a + PI() / 2.0, a + PI() - phi); out += [turned(a, xi - 0.5, -h), turned(a, xi - 0.5, h)]; out += arc_pts(fl[0], fl[1], f, a - PI() + phi, a - PI() / 2.0); out += [turned(a, cx, h), turned(a, R, h)]; flares.push(out); } if flares.len() > 0 { s.flat(flares, 0.007, "g-asph t"); } // the kerb line round the outside, sector by sector between the arms let chains = []; let caps = []; if arms.len() == 0 { chains.push(arc_pts(0.0, 0.0, r, 2.0 * PI(), 0.0)); caps.push([false, false]); } else { for i in 0..arms.len() { let a = arms[i]; let b = if i + 1 < arms.len() { arms[i + 1] } else { arms[0] + 2.0 * PI() }; let ch = [turned(a, R, h), turned(a, cx, h)]; let fa = turned(a, cx, h + f); ch += arc_pts(fa[0], fa[1], f, a - PI() / 2.0, a - PI() + phi); ch += arc_pts(0.0, 0.0, r, a + phi, b - phi); let fb = turned(b, cx, -(h + f)); ch += arc_pts(fb[0], fb[1], f, b + PI() - phi, b + PI() / 2.0); ch.push(turned(b, R, -h)); // drawn with the road on its right: from arm b's end round to arm a's chains.push(rev(ch)); caps.push([!carried.call(b), !carried.call(a)]); } } for i in 0..chains.len() { let ch = chains[i]; if ww > 0.0 { let fs = kerb_faces(ch, walk_h(), true); if fs.len() > 0 { s.poly(fs, "kerbf"); } s.walk_strip(ch, path_offset(ch, ww), caps[i]); } else { s.polyline(ch, 0.012, "kerb"); } } // markings: give-way lines across the entry lanes, the lane line, the arms' centre lines let entry = if p.drive == "right" { 1.0 } else { -1.0 }; let sl = if p.splitters { fmin(6.0, p.arm_len - 1.5) } else { 0.0 }; let segs = []; let gw = []; for a in arms { let x0 = r + 0.5; let ia = if p.splitters { 1.0 } else { 0.2 }; let g0 = turned(a, x0, entry * ia); let g1 = turned(a, x0, entry * (h - 0.25)); gw.push([g0[0], g0[1], g1[0], g1[1]]); let d0 = turned(a, x0 + sl + 0.6, 0.0); let d1 = turned(a, R, 0.0); if R - (x0 + sl + 0.6) > 0.5 { segs.push([d0[0], d0[1], d1[0], d1[1]]); } } if gw.len() > 0 { s.lines(gw, 0.012, "giveway"); } if segs.len() > 0 { s.lines(segs, 0.012, "dash"); } if p.lanes > 1 { s.polyline(arc_pts(0.0, 0.0, (r + ri) / 2.0, 0.0, 2.0 * PI()), 0.012, "lane"); } // the raised islands: splitters at the entries, the centre island with its apron let kh = walk_h(); if p.splitters { for a in arms { let x0 = r + 0.8; let isl = [turned(a, x0, -0.9), turned(a, x0 + sl, -0.25), turned(a, x0 + sl, 0.25), turned(a, x0, 0.9)]; s.flat(isl, kh, "g-walk t"); let fs = kerb_faces(isl + [isl[0]], kh, true); if fs.len() > 0 { s.poly(fs, "kerbf"); } } } let ring = arc_pts(0.0, 0.0, ri, 0.0, 2.0 * PI()); let apron = fmin(1.5, ri * 0.35); let top = if p.island == "paved" { "g-walk" } else { "g-grass" }; s.flat(ring, kh, "g-apron t"); s.flat(arc_pts(0.0, 0.0, ri - apron, 0.0, 2.0 * PI()), kh + 0.01, top + " t"); let fs = kerb_faces(ring, kh, true); if fs.len() > 0 { s.poly(fs, "kerbf"); } if p.island == "tree" { layer("main"); sprite().tree_at(0.0, 0.0, fmin(3.0, (ri - apron) * 0.7)); } let names = ["east", "north", "west", "south"]; let on = [p.arm_e, p.arm_n, p.arm_w, p.arm_s]; let which = []; for i in 0..4 { if on[i] { which.push(names[i]); } } info_row("Ring", `${num1(r)} m radius, ${num1(p.lane)} m wide, ${p.lanes} lane${if p.lanes > 1 { "s" } else { "" }}`); if arms.len() > 0 { info_row("Arms", `${arms.len()}: ${which.reduce(|s, x| if s == "" { x } else { s + ", " + x }, "")}; ${num1(p.road_d)} m wide`); } info_row("Traffic", `on the ${p.drive}, round the island ${if p.drive == "left" { "clockwise" } else { "anticlockwise" }}`); let meets = []; for a in arms { let e = world_of(o, R * a.cos(), R * a.sin()); for f in others { if (f.x - e[0]).abs() < 0.3 && (f.y - e[1]).abs() < 0.3 && !meets.contains(f.id) { meets.push(f.id); } } } if meets.len() > 0 { info_row("Meets", meets.reduce(|s, x| if s == "" { x } else { s + ", " + x }, "")); } }