Streets: sidewalks, road curves, junctions and roundabouts
Deploy web Studio / deploy (push) Successful in 4m31s

Roads take sidewalks (none, both, left, right): 12 cm high paving with
kerb faces drawn where they face the viewer. Road curve (radius, angle,
left/right) carries a street round a bend; centre lines are true arcs
when cut and offset, so curve joints are seamless. A road ending on
another road's centre line opens that road's kerb and sidewalk (T
junction); two roads ending at one corner close it. Roundabout: ring
radius and width, one or two lanes, up to four arms with flared kerbs,
splitter islands, give-way lines on the entry side (traffic on the left
or right) and a central island with apron.

Studio: a road run drawn corner by corner gets a curve in each corner
(prefab meta `corner`). Tooltips list the streets each one meets.
Engine: open `polyline` primitive (Prim::Poly open) so dashes run on
round curves. examples/streets and a Streets demo in the web Studio;
MCP guidance, docs. ROADMAP: backlog 5-7 done; multi-select and the
hosted copy marked done.

Tests: Rust 72, Studio 113, web 95.
This commit is contained in:
grabowski committed 2026-10-25 12:32:52 +07:00
1 parent 2442966743
commit c91d63663b
24 files changed
+989 -29

No files matched your search

+12 -8
View File
@@ -46,24 +46,28 @@ fn prefab_area(o) {
}
fn meta_road() {
#{ label: "Road", category: "Ground", icon: "═", shape: "line",
help: "A straight road along its length, with a dashed centre line.",
#{ label: "Road", category: "Ground", icon: "═", shape: "line", corner: "road_curve",
help: "A straight road along its length, with a dashed centre line and sidewalks if you like (left and right looking along local +x). Roads ending on another road's side make a junction; a run drawn corner by corner gets a curve in each corner.",
props: [
#{ key: "w", type: "float", def: 40.0, min: 2.0, max: 2000.0, step: 1.0, label: "Length (m)" },
#{ 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(o) {
let p = o.p;
let w = p.w / 2.0;
let d = p.d / 2.0;
layer("ground");
let s = sprite();
s.flat(rect(-w, -d, w, d), 0.007, "g-asph t");
if p.dashed { s.lines([[-w, 0.0, w, 0.0]], 0.012, "dash"); }
if p.kerb { s.lines([[-w, -d, w, -d], [-w, d, w, d]], 0.012, "kerb"); }
let others = other_street_ends(o);
let ends = street_ends(o);
let c = line_path([[-p.w / 2.0, 0.0], [p.w / 2.0, 0.0]]);
let j0 = street_join(o, ends[0], others);
let j1 = street_join(o, ends[1], others);
let gaps = street_gaps(o, others);
street_path(o, c, j0, j1, gaps);
street_info(p, `${num1(p.w)} m long, ${num1(p.d)} m wide`, [j0.meet, j1.meet], gaps[2]);
}
fn meta_parking() {
+566
View File
@@ -0,0 +1,566 @@
// 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 }, "")); }
}