Files
isokit/crates/isokit-script/stdlib/streets.rhai
T
grabowski 96d472625f
Deploy web Studio / deploy (push) Successful in 1m5s
Diagonals: streets, walls, fences, hedges and house walls at any angle
Object frames turn by any angle (quarter turns stay exact, so scenes along
the axes render byte for byte as before); a box in a turned frame is a
prism with its own corners. Rotation is kept as given (normalised to
0..360), no longer snapped to quarter turns.

Depth sort: sprites whose plan outline is not their box carry their convex
outline; when boxes overlap, an outline edge that separates two sprites
orders them. A long diagonal wall sorts right against both sides.

Joints (line_meet, end_cut, cut_box in util.rhai): where a piece off the
axes is involved, corners are mitred and T-junctions slanted; fences put a
corner post or pillar in its own sprite and mitre stone bases; Y joints let
the lowest id run through. Streets: every end part is a cut e + k*w, so
side streets come in at any angle (slanted sidewalk gaps), two streets meet
in a mitre with seam patches; fixes a curve's outer sidewalk that was
skipped when its kerb and back lines had different point counts.

Studio: runs and drags keep to x/y or snap to 45 degree diagonals, Ctrl
gives any angle; a road run turning 45 degrees gets a 45 degree curve;
Rotation is a number box. Streets demo: a diagonal lane with a hedge.
MCP guidance, SCRIPTING/README/ROADMAP.

Tests: Rust 76, Studio and web checks green.
2026-10-25 13:22:59 +07:00

639 lines
27 KiB
Plaintext

// 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 }, "")); }
}