Diagonals: streets, walls, fences, hedges and house walls at any angle
Deploy web Studio / deploy (push) Successful in 1m5s
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.
This commit is contained in:
1 parent
c91d63663b
commit
96d472625f
18 files changed
+1051
-178
No files matched your search
@@ -152,21 +152,6 @@ fn pl_end(p, e) {
|
||||
[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
|
||||
@@ -185,20 +170,21 @@ fn kerb_faces(k, h, right) {
|
||||
}
|
||||
|
||||
/// 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; }
|
||||
/// 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();
|
||||
this.flat(k + rev(o), h, "g-walk t");
|
||||
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);
|
||||
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 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
|
||||
|
||||
@@ -233,61 +219,94 @@ fn street_ends(o) {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// #{ 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 j = #{ asph: 0.0, dash: 0.0, kl: 0.0, kr: 0.0, wl: 0.0, wr: 0.0, free: true, meet: "" };
|
||||
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 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; }
|
||||
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;
|
||||
}
|
||||
// ending on a road's side (a T): kerbs stop at its kerb, sidewalks at its sidewalk
|
||||
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 l = local_of(c, e.x - e.dx, e.y - e.dy);
|
||||
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 l[1] > c.v { ws[0] } else { ws[1] };
|
||||
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.dash = -h;
|
||||
j.kl = -h;
|
||||
j.kr = -h;
|
||||
j.wl = -(h + w) + 0.04;
|
||||
j.wr = -(h + w) + 0.04;
|
||||
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, as distance ranges along it from its start,
|
||||
/// then the ids of those streets.
|
||||
/// 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 s = l[0] + o.p.w / 2.0;
|
||||
let r = [s - f.d / 2.0, s + f.d / 2.0];
|
||||
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);
|
||||
}
|
||||
@@ -314,31 +333,80 @@ fn other_street_ends(o) {
|
||||
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]
|
||||
/// (distance ranges along c).
|
||||
/// (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();
|
||||
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");
|
||||
// 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");
|
||||
}
|
||||
// side 0: left of the way (the start end's right, the end's left); side 1: right
|
||||
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 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 vk = sg * h;
|
||||
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);
|
||||
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);
|
||||
@@ -348,17 +416,21 @@ fn street_path(o, c, j0, j1, gaps) {
|
||||
}
|
||||
}
|
||||
if ww > 0.0 {
|
||||
let rs = ranges(-w0, L + w1, gaps[side]);
|
||||
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 cut = pl_cut(c, r[0], r[1]);
|
||||
let line = pl_off(cut, sg * h);
|
||||
let back = pl_off(cut, sg * (h + ww));
|
||||
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] });
|
||||
s.walk_strip(line, back, if side == 0 { [at0, at1] } else { [at1, at0] }, wp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,7 +449,7 @@ fn curve_line(p) {
|
||||
|
||||
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.",
|
||||
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)" },
|
||||
|
||||
Reference in new issue
Block a user