// 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] } // ---------------------------------------------------------- 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 (to the left): its paving (with seam patches, /// polygons laid over mitred joints), 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, patches) { if k.len() < 2 || o.len() < 2 { return this; } let h = walk_h(); if patches.len() == 0 { this.flat(k + rev(o), h, "g-walk t"); } else { this.flat([k + rev(o)] + patches, h, "g-walk t"); } let fs = kerb_faces(o, h, false); if caps[0] { fs += kerb_faces([o[0], k[0]], h, true); } if caps[1] { fs += kerb_faces([k[k.len() - 1], o[o.len() - 1]], h, true); } if fs.len() > 0 { this.poly(fs, "kerbf"); } this } fn walk_strip(k, o, caps) { this.walk_strip(k, o, caps, []) } // ---------------------------------------------------------- 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, kerb, walk, free, meet, pd }. Each of the first four is a /// cut #{ e, k }: the line w to the left looking out of the end (its /// carriageway edge, kerb or sidewalk edge) reaches e + k * w past it /// (negative: it stops short), so ends can be slanted. free: nothing meets it /// there; meet: the id of the street that does; pd: at a mitred corner, the /// direction (o's frame) into the other street, for seam patches. /// - end to end (a curve carrying a road on): everything overlaps 4 cm; /// - two streets ending at one point at any angle: a mitre on the bisector; /// - ending on a road's centre line (a T, at any angle): the asphalt stops on /// it, centre line and kerbs at its kerb, sidewalks at its sidewalk's back. fn street_join(o, e, others) { let flat = #{ e: 0.0, k: 0.0 }; let j = #{ asph: flat, dash: flat, kerb: flat, walk: flat, free: true, meet: "", pd: () }; // pieces that carry on overlap by a few cm: edge to edge, the ground shows through as a hairline let lap = 0.04; let u = [e.dx, e.dy]; let n = [-e.dy, e.dx]; 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 uf = [f.dx, f.dy]; let dot = dot2(u, uf); if dot < -0.9999 { let l = #{ e: lap, k: 0.0 }; j.asph = l; j.kerb = l; j.walk = l; continue; } if dot > 0.9 { continue; } // a corner at any angle: every part ends on the bisector let m = #{ e: 0.0, k: dot2(n, uf) / (1.0 - dot) }; j.asph = m; j.dash = m; j.kerb = m; j.walk = m; let q0 = local_of(o, e.x, e.y); let q1 = local_of(o, e.x - f.dx, e.y - f.dy); j.pd = [q1[0] - q0[0], q1[1] - q0[1]]; return j; } if !j.free { return j; } // ending on a road's centre line (a T): stop at lines along that road 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 c0 = world_of(c, 0.0, 0.0); let c1 = world_of(c, 0.0, 1.0); let yc = [c1[0] - c0[0], c1[1] - c0[1]]; let a = dot2(u, yc); if a.abs() < 0.05 { continue; } // o's body lies on c's left (+v) when its way out runs to c's right let sd = if a < 0.0 { 1.0 } else { -1.0 }; let ws = walk_widths(c.p); let h = c.p.d / 2.0; let w = if sd > 0.0 { ws[0] } else { ws[1] }; let k = -dot2(n, yc) / a; let at = |tv| #{ e: (tv - c.v) / a, k: k }; j.free = false; j.meet = c.id; j.asph = at.call(0.0); j.dash = at.call(sd * h); j.kerb = at.call(sd * h); let wb = at.call(sd * (h + w)); j.walk = #{ e: wb.e + 0.04, k: k }; return j; } j } /// Where the streets in `others` end on road o's sides: gaps [left, right] /// in its kerbs and sidewalks, each #{ lo, hi, c }: from lo + c * v to /// hi + c * v along it from its start at side offset v (slanted where a /// street comes in at an angle), 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; } // the street's way in, in o's frame, and its carriageway edges across o let b = local_of(o, f.x - f.dx, f.y - f.dy); let d = [l[0] - b[0], l[1] - b[1]]; let a = -d[1]; if a.abs() < 0.05 { continue; } let sl = -d[0] / a; let hw = f.d / 2.0 / a.abs(); let s = l[0] + o.p.w / 2.0 - sl * l[1]; let r = #{ lo: s - hw, hi: s + hw, c: sl }; 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 } /// A bound along a street: at side offset v it lies at b.s + b.c * v. fn bnd_at(b, v) { b.s + b.c * v } /// Pieces of [a, b] (bounds) without the gaps (street_gaps entries), as /// they fall at side offset v: [[lo, hi], ...] of bounds, at least 5 cm long. fn ranges_b(a, b, gaps, v) { let out = [[a, b]]; for g in gaps { let glo = #{ s: g.lo, c: g.c }; let ghi = #{ s: g.hi, c: g.c }; let next = []; for r in out { if bnd_at(ghi, v) <= bnd_at(r[0], v) || bnd_at(glo, v) >= bnd_at(r[1], v) { next.push(r); continue; } if bnd_at(glo, v) > bnd_at(r[0], v) { next.push([r[0], glo]); } if bnd_at(ghi, v) < bnd_at(r[1], v) { next.push([ghi, r[1]]); } } out = next; } out.filter(|r| bnd_at(r[1], v) - bnd_at(r[0], v) > 0.05) } /// A thin patch over the seam from p to q, reaching `lap` along d: laid /// into the street on the other side of a mitre, so no hairline shows. fn seam_patch(p, q, d) { let l = fmax((d[0] * d[0] + d[1] * d[1]).sqrt(), 1e-9); let x = d[0] / l * 0.06; let y = d[1] / l * 0.06; [p, q, [q[0] + x, q[1] + y], [p[0] + x, p[1] + y]] } /// 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] /// (from street_gaps). 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(); // a part's ends as bounds (v: left of the way; at the start that is the // right looking out, so w = -v there) let b0 = |cut| #{ s: -cut.e, c: cut.k }; let b1 = |cut| #{ s: L + cut.e, c: cut.k }; let seg = |a, b, v| pl_off(pl_cut(c, bnd_at(a, v), bnd_at(b, v)), v); // patches over mitred seams between the lines at v0 and v1 (at the start, the end) let patches = |l0, l1| { let out = []; if type_of(j0.pd) != "()" { out.push(seam_patch(l0[0], l1[0], j0.pd)); } if type_of(j1.pd) != "()" { out.push(seam_patch(l0[l0.len() - 1], l1[l1.len() - 1], j1.pd)); } out }; let A0 = b0.call(j0.asph); let A1 = b1.call(j1.asph); let al = seg.call(A0, A1, h); let ar = seg.call(A0, A1, -h); let ap = patches.call(al, ar); if ap.len() == 0 { s.flat(al + rev(ar), 0.007, "g-asph t"); } else { s.flat([al + rev(ar)] + ap, 0.007, "g-asph t"); } let D0 = b0.call(j0.dash); let D1 = b1.call(j1.dash); if p.dashed && bnd_at(D1, 0.0) - bnd_at(D0, 0.0) > 0.05 { s.polyline(seg.call(D0, D1, 0.0), 0.012, "dash"); } let K0 = b0.call(j0.kerb); let K1 = b1.call(j1.kerb); let W0 = b0.call(j0.walk); let W1 = b1.call(j1.walk); // side 0: left of the way; side 1: right for side in [0, 1] { let sg = if side == 0 { 1.0 } else { -1.0 }; let vk = sg * h; let ww = ws[side]; for r in ranges_b(K0, K1, gaps[side], vk) { let line = seg.call(r[0], r[1], vk); 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 vb = sg * (h + ww); let rs = ranges_b(W0, W1, gaps[side], (vk + vb) / 2.0); for i in 0..rs.len() { let r = rs[i]; let line = seg.call(r[0], r[1], vk); let back = seg.call(r[0], r[1], vb); // seams only where the piece reaches the street's mitred ends let wp = []; if i == 0 && type_of(j0.pd) != "()" { wp.push(seam_patch(line[0], back[0], j0.pd)); } if i + 1 == rs.len() && type_of(j1.pd) != "()" { wp.push(seam_patch(line[line.len() - 1], back[back.len() - 1], j1.pd)); } // 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] }, wp); } } } 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 (turned by its rotation, any angle) 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 }, "")); } }