Deploy web Studio / deploy (push) Successful in 1m5s
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.
639 lines
27 KiB
Plaintext
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 }, "")); }
|
|
}
|