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
@@ -103,19 +103,34 @@ fn meta_wall() {
|
||||
] }
|
||||
}
|
||||
fn prefab_wall(o) {
|
||||
// ends joined to the walls they meet; long walls in pieces of at most 30 m,
|
||||
// each its own sprite for the depth sort
|
||||
// ends joined to the walls they meet (mitred and slanted where a wall is
|
||||
// diagonal); long walls in pieces of at most 30 m, each its own sprite for
|
||||
// the depth sort
|
||||
let p = o.p;
|
||||
let x0 = -p.w / 2.0 - line_joint(o, -1.0, ["wall"], []).ext;
|
||||
let x1 = p.w / 2.0 + line_joint(o, 1.0, ["wall"], []).ext;
|
||||
let c0 = wall_end(o, -1.0);
|
||||
let c1 = wall_end(o, 1.0);
|
||||
let x0 = c0.x;
|
||||
let x1 = c1.x;
|
||||
let n = ((x1 - x0) / 30.0).ceiling().to_int();
|
||||
let L = (x1 - x0) / n;
|
||||
for i in 0..n {
|
||||
let a = x0 + i * L;
|
||||
sprite().box(a, -p.t / 2.0, a + L, p.t / 2.0, 0.0, p.h, "wallp", #{ top: "wallt" });
|
||||
let k0 = if i == 0 { c0.k } else { 0.0 };
|
||||
let k1 = if i == n - 1 { c1.k } else { 0.0 };
|
||||
sprite().cut_box(a, k0, a + L, k1, -p.t / 2.0, p.t / 2.0, 0.0, p.h, "wallp", #{ top: "wallt" });
|
||||
}
|
||||
}
|
||||
|
||||
/// Where site wall o ends at e (-1 or 1): #{ x, k } (see end_cut).
|
||||
fn wall_end(o, e) {
|
||||
let m = line_meet(o, e, ["wall"], []);
|
||||
let ht = o.p.t / 2.0;
|
||||
if m.kind == "axis" { return end_cut(o.p.w, e, #{ ext: line_joint(o, e, ["wall"], []).ext, k: 0.0 }); }
|
||||
if m.kind == "free" { return end_cut(o.p.w, e, #{ ext: ht, k: 0.0 }); }
|
||||
if m.kind == "run" { return end_cut(o.p.w, e, #{ ext: if m.lower { ht } else { -m.wt }, k: 0.0 }); }
|
||||
end_cut(o.p.w, e, m)
|
||||
}
|
||||
|
||||
fn fence_styles() { ["mesh", "wood", "bars", "half stone", "cobble", "brick"] }
|
||||
|
||||
/// Half the thickness of a fence design: its posts, base or pillars.
|
||||
@@ -135,10 +150,13 @@ fn meta_fence() {
|
||||
fn prefab_fence(o) {
|
||||
let p = o.p;
|
||||
let st = p.style;
|
||||
let ea = line_joint(o, -1.0, ["fence"], ["wall", "house_wall"]);
|
||||
let eb = line_joint(o, 1.0, ["fence"], ["wall", "house_wall"]);
|
||||
let x0 = -p.w / 2.0 - ea.ext;
|
||||
let x1 = p.w / 2.0 + eb.ext;
|
||||
let ea = fence_end(o, -1.0);
|
||||
let eb = fence_end(o, 1.0);
|
||||
// x0, x1: the base's ends (stone, cobble); u0, u1: the panels', rails' and boards'
|
||||
let x0 = ea.x;
|
||||
let x1 = eb.x;
|
||||
let u0 = ea.up;
|
||||
let u1 = eb.up;
|
||||
if x1 - x0 < 0.05 { return; }
|
||||
// posts (pillars) evenly along the length; the end ones only where this
|
||||
// fence draws them, kept inside the stretch it covers
|
||||
@@ -149,10 +167,17 @@ fn prefab_fence(o) {
|
||||
if st != "cobble" {
|
||||
for j in 0..(k + 1) {
|
||||
if (j == 0 && !ea.post) || (j == k && !eb.post) { continue; }
|
||||
posts.push(fmin(fmax(-p.w / 2.0 + j * p.w / k, x0 + ph), x1 - ph));
|
||||
posts.push(fmin(fmax(-p.w / 2.0 + j * p.w / k, u0 + ph), u1 - ph));
|
||||
}
|
||||
}
|
||||
if st == "brick" { brick_fence(x0, x1, posts, p.h); return; }
|
||||
// corner posts where a diagonal fence meets this one: their own sprites
|
||||
for c in [[ea, -1.0], [eb, 1.0]] {
|
||||
if c[0].corner { fence_corner_post(sprite(), c[1] * p.w / 2.0, p.h, st); }
|
||||
}
|
||||
if st == "brick" { brick_fence(u0, u1, posts, p.h); return; }
|
||||
// diagonal pieces keep a stone base and what stands on it in separate
|
||||
// sprites, so corner posts on the base sort against both
|
||||
let split = st == "half stone" && (!on_axes(o.rot) || ea.gen || eb.gen);
|
||||
// pieces of at most 30 m, each its own sprite for the depth sort
|
||||
let n = ((x1 - x0) / 30.0).ceiling().to_int();
|
||||
let L = (x1 - x0) / n;
|
||||
@@ -160,16 +185,83 @@ fn prefab_fence(o) {
|
||||
let a = x0 + i * L;
|
||||
let b = a + L;
|
||||
let ps = [];
|
||||
let ua = if i == 0 { u0 } else { a };
|
||||
let ub = if i == n - 1 { u1 } else { b };
|
||||
for x in posts {
|
||||
if x >= a - 1e-6 && (x < b - 1e-6 || i == n - 1) { ps.push(fmin(fmax(x, a + ph), b - ph)); }
|
||||
if x >= a - 1e-6 && (x < b - 1e-6 || i == n - 1) { ps.push(fmin(fmax(x, ua + ph), ub - ph)); }
|
||||
}
|
||||
fence_piece(sprite(), a, b, ps, p.h, st, i);
|
||||
let cut = #{ a: a, ka: if i == 0 { ea.k } else { 0.0 }, b: b, kb: if i == n - 1 { eb.k } else { 0.0 } };
|
||||
let s = sprite();
|
||||
fence_piece(s, if split { sprite() } else { s }, cut, ua, ub, ps, p.h, st, i);
|
||||
}
|
||||
}
|
||||
|
||||
/// One stretch a..b of a fence (not brick) with its posts ps.
|
||||
fn fence_piece(s, a, b, ps, h, st, seed) {
|
||||
/// Post half widths of the fence designs (0: none).
|
||||
fn fence_post_half(st) {
|
||||
switch st { "cobble" => 0.0, "wood" => 0.05, "brick" => 0.18, _ => 0.04 }
|
||||
}
|
||||
|
||||
/// Where fence o ends at e (-1 or 1): #{ x, k, up, post, corner, gen }: its
|
||||
/// base (stone, cobble) ends at x + k * y; panels, rails and boards at up
|
||||
/// (short of a corner post); post: it draws the end post among its own;
|
||||
/// corner: it draws the post at a diagonal corner as its own sprite; gen: a
|
||||
/// joint with a diagonal piece.
|
||||
fn fence_end(o, e) {
|
||||
let w = o.p.w;
|
||||
let st = o.p.style;
|
||||
let ht = fence_ht(st);
|
||||
let m = line_meet(o, e, ["fence"], ["wall", "house_wall"]);
|
||||
let plain = |ext, post, gen| {
|
||||
let c = end_cut(w, e, #{ ext: ext, k: 0.0 });
|
||||
#{ x: c.x, k: 0.0, up: c.x, post: post, corner: false, gen: gen }
|
||||
};
|
||||
if m.kind == "axis" {
|
||||
let j = line_joint(o, e, ["fence"], ["wall", "house_wall"]);
|
||||
return plain.call(j.ext, j.post, false);
|
||||
}
|
||||
if m.kind == "free" { return plain.call(ht, true, true); }
|
||||
if m.kind == "run" { return if m.lower { plain.call(ht, true, true) } else { plain.call(-m.wt, false, true) }; }
|
||||
let c = end_cut(w, e, m);
|
||||
let r = #{ x: c.x, k: c.k, up: c.x, post: false, corner: false, gen: true };
|
||||
if m.kind == "tee" {
|
||||
// panels stop at the face too; against a wall the fence keeps its post
|
||||
r.post = m.stop;
|
||||
return r;
|
||||
}
|
||||
// a corner: the base is mitred; the owner's post (or pillar) stands on the corner
|
||||
let pst = if m.owner { st } else { m.w.p.style };
|
||||
let pp = fence_post_half(pst);
|
||||
if pp == 0.0 { return r; }
|
||||
if m.owner && st == "brick" {
|
||||
r.up = end_cut(w, e, #{ ext: pp, k: 0.0 }).x;
|
||||
r.post = true;
|
||||
return r;
|
||||
}
|
||||
let reach = if m.owner { pp } else { pp * (m.cos + m.sin) };
|
||||
r.up = end_cut(w, e, #{ ext: -reach, k: 0.0 }).x;
|
||||
r.corner = m.owner && st != "brick";
|
||||
r
|
||||
}
|
||||
|
||||
/// The post at a diagonal corner, at local x: on the stone base for half stone.
|
||||
fn fence_corner_post(s, x, h, st) {
|
||||
if st == "wood" {
|
||||
s.box(x - 0.05, -0.05, x + 0.05, 0.05, 0.0, h + 0.06, "woodd");
|
||||
} else {
|
||||
let z0 = if st == "half stone" { fmin(0.6, h * 0.4) } else { 0.0 };
|
||||
s.box(x - 0.04, -0.04, x + 0.04, 0.04, z0, h + 0.05, "steel");
|
||||
}
|
||||
}
|
||||
|
||||
/// One stretch of a fence (not brick): the base (stone, cobble) in sprite s
|
||||
/// over cut #{ a, ka, b, kb } (slanted ends), what stands on it in sprite su
|
||||
/// from a to b, with posts ps.
|
||||
fn fence_piece(s, su, cut, a, b, ps, h, st, seed) {
|
||||
let L = b - a;
|
||||
let ca = cut.a;
|
||||
let cb = cut.b;
|
||||
let ka = cut.ka;
|
||||
let kb = cut.kb;
|
||||
if st == "wood" {
|
||||
s.box(a, -0.025, b, 0.025, 0.0, h, "wood");
|
||||
let boards = [];
|
||||
@@ -189,19 +281,22 @@ fn fence_piece(s, a, b, ps, h, st, seed) {
|
||||
for x in ps { s.box(x - 0.04, -0.04, x + 0.04, 0.04, 0.0, h + 0.05, "steel"); }
|
||||
} else if st == "half stone" {
|
||||
let hb = fmin(0.6, h * 0.4);
|
||||
s.box(a, -0.16, b, 0.16, 0.0, hb, "cobble", #{ top: "capst" });
|
||||
let j = stone_joints(L, 0.0, hb, seed);
|
||||
s.open([a, -0.16], [b, -0.16], j, "seam");
|
||||
s.open([b, 0.16], [a, 0.16], j, "seam");
|
||||
s.box(a, -0.03, b, 0.03, hb, h, "fence");
|
||||
for x in ps { s.box(x - 0.04, -0.04, x + 0.04, 0.04, hb, h + 0.05, "steel"); }
|
||||
s.cut_box(ca, ka, cb, kb, -0.16, 0.16, 0.0, hb, "cobble", #{ top: "capst" });
|
||||
let f0 = ca - ka * 0.16;
|
||||
let f1 = ca + ka * 0.16;
|
||||
let j = stone_joints(cb - kb * 0.16 - f0, 0.0, hb, seed);
|
||||
s.open([f0, -0.16], [cb - kb * 0.16, -0.16], j, "seam");
|
||||
s.open([cb + kb * 0.16, 0.16], [f1, 0.16], stone_joints(cb + kb * 0.16 - f1, 0.0, hb, seed), "seam");
|
||||
su.box(a, -0.03, b, 0.03, hb, h, "fence");
|
||||
for x in ps { su.box(x - 0.04, -0.04, x + 0.04, 0.04, hb, h + 0.05, "steel"); }
|
||||
} else if st == "cobble" {
|
||||
let hc = fmax(0.2, h - 0.08);
|
||||
s.box(a, -0.2, b, 0.2, 0.0, hc, "cobble");
|
||||
s.box(a, -0.22, b, 0.22, hc, h, "capst");
|
||||
let j = stone_joints(L, 0.0, hc, seed);
|
||||
s.open([a, -0.2], [b, -0.2], j, "seam");
|
||||
s.open([b, 0.2], [a, 0.2], j, "seam");
|
||||
s.cut_box(ca, ka, cb, kb, -0.2, 0.2, 0.0, hc, "cobble");
|
||||
s.cut_box(ca, ka, cb, kb, -0.22, 0.22, hc, h, "capst");
|
||||
let f0 = ca - ka * 0.2;
|
||||
let f1 = ca + ka * 0.2;
|
||||
s.open([f0, -0.2], [cb - kb * 0.2, -0.2], stone_joints(cb - kb * 0.2 - f0, 0.0, hc, seed), "seam");
|
||||
s.open([cb + kb * 0.2, 0.2], [f1, 0.2], stone_joints(cb + kb * 0.2 - f1, 0.0, hc, seed), "seam");
|
||||
} else {
|
||||
// mesh
|
||||
s.box(a, -0.03, b, 0.03, 0.0, h, "fence");
|
||||
|
||||
@@ -126,13 +126,26 @@ fn prefab_house_wall(o) {
|
||||
gaps.push(opening_span(o, m.u, m.p.w));
|
||||
}
|
||||
gaps.sort(|a, b| if a[0] < b[0] { -1 } else if a[0] > b[0] { 1 } else { 0 });
|
||||
let x = -p.w / 2.0 - wall_ext(o, -1.0);
|
||||
let c0 = house_wall_end(o, -1.0);
|
||||
let c1 = house_wall_end(o, 1.0);
|
||||
let x = c0.x;
|
||||
let k = c0.k;
|
||||
for g in gaps {
|
||||
if g[0] > x + 0.01 { wall_run(x, g[0], t, top, p.mat, cap); }
|
||||
if g[0] > x + 0.01 { wall_run(x, k, g[0], 0.0, t, top, p.mat, cap); }
|
||||
if g[1] > x { k = 0.0; }
|
||||
x = fmax(x, g[1]);
|
||||
}
|
||||
let end = p.w / 2.0 + wall_ext(o, 1.0);
|
||||
if end > x + 0.01 { wall_run(x, end, t, top, p.mat, cap); }
|
||||
if c1.x > x + 0.01 { wall_run(x, k, c1.x, c1.k, t, top, p.mat, cap); }
|
||||
}
|
||||
|
||||
/// Where house wall o ends at e (-1 or 1): #{ x, k } (see end_cut); mitred
|
||||
/// at corners and slanted at T-junctions where a wall is diagonal.
|
||||
fn house_wall_end(o, e) {
|
||||
let m = line_meet(o, e, wall_types(), []);
|
||||
if m.kind == "axis" { return end_cut(o.p.w, e, #{ ext: wall_ext(o, e), k: 0.0 }); }
|
||||
if m.kind == "free" { return end_cut(o.p.w, e, #{ ext: o.p.t / 2.0, k: 0.0 }); }
|
||||
if m.kind == "run" { return end_cut(o.p.w, e, #{ ext: 0.0, k: 0.0 }); }
|
||||
end_cut(o.p.w, e, m)
|
||||
}
|
||||
|
||||
/// How far wall o reaches past end e (-1 or 1) of its centre line. Alone:
|
||||
@@ -162,12 +175,14 @@ fn wall_ext(o, e) {
|
||||
ext
|
||||
}
|
||||
|
||||
/// A solid stretch of wall from x0 to x1, one sprite per 30 m at most.
|
||||
fn wall_run(x0, x1, t, top, mat, cap) {
|
||||
/// A solid stretch of wall from x0 to x1 (ends slanted by k0, k1), one
|
||||
/// sprite per 30 m at most.
|
||||
fn wall_run(x0, k0, x1, k1, t, top, mat, cap) {
|
||||
let n = ((x1 - x0) / 30.0).ceiling().to_int();
|
||||
let L = (x1 - x0) / n;
|
||||
for i in 0..n {
|
||||
sprite().box(x0 + i * L, -t, x0 + (i + 1) * L, t, 0.0, top, mat, #{ top: cap });
|
||||
sprite().cut_box(x0 + i * L, if i == 0 { k0 } else { 0.0 }, x0 + (i + 1) * L, if i == n - 1 { k1 } else { 0.0 },
|
||||
-t, t, 0.0, top, mat, #{ top: cap });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,15 @@ fn meta_hedge() {
|
||||
}
|
||||
fn prefab_hedge(o) {
|
||||
let p = o.p;
|
||||
sprite().box(-p.w / 2.0, -p.d / 2.0, p.w / 2.0, p.d / 2.0, 0.0, p.h, "hedge");
|
||||
let c0 = hedge_end(o, -1.0);
|
||||
let c1 = hedge_end(o, 1.0);
|
||||
sprite().cut_box(c0.x, c0.k, c1.x, c1.k, -p.d / 2.0, p.d / 2.0, 0.0, p.h, "hedge");
|
||||
}
|
||||
|
||||
/// Where hedge o ends at e: mitred or slanted where a diagonal hedge meets it.
|
||||
fn hedge_end(o, e) {
|
||||
let m = line_meet(o, e, ["hedge"], []);
|
||||
if m.kind == "corner" || m.kind == "tee" { end_cut(o.p.w, e, m) } else { end_cut(o.p.w, e, #{ ext: 0.0, k: 0.0 }) }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- pond
|
||||
|
||||
@@ -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)" },
|
||||
|
||||
@@ -61,9 +61,137 @@ fn drive(dx, dy, dur, delay) {
|
||||
/// True when a piece turned by rot runs along world x (rot 0 or 180).
|
||||
fn along_world_x(rot) { ((rot / 90.0).round().to_int() % 2) == 0 }
|
||||
|
||||
/// Half the thickness of a fence or wall piece: what the joints keep apart.
|
||||
/// True when rot (degrees) is a multiple of 90: the piece keeps to the world axes.
|
||||
fn on_axes(rot) {
|
||||
let q = rot / 90.0;
|
||||
(q - q.round()).abs() < 1e-5
|
||||
}
|
||||
|
||||
/// World plan direction of a piece's local +x axis.
|
||||
fn dir_of(o) {
|
||||
let a = o.rot * PI() / 180.0;
|
||||
[a.cos(), a.sin()]
|
||||
}
|
||||
|
||||
fn dot2(a, b) { a[0] * b[0] + a[1] * b[1] }
|
||||
|
||||
/// Half the thickness of a fence, wall or hedge piece: what the joints keep apart.
|
||||
fn line_ht(w) {
|
||||
if w.type == "fence" { fence_ht(w.p.style) } else { w.p.t / 2.0 }
|
||||
if w.type == "fence" { fence_ht(w.p.style) } else if w.type == "hedge" { w.p.d / 2.0 } else { w.p.t / 2.0 }
|
||||
}
|
||||
|
||||
/// How line piece o meets the pieces of `types` at its end e (-1 or 1), for
|
||||
/// joints where a piece is turned off the world axes (diagonal walls, fences,
|
||||
/// hedges). #{ kind: "axis" } when every piece there keeps to the axes (the
|
||||
/// prefab's own rules apply), else kind
|
||||
/// - "free": nothing there;
|
||||
/// - "run": a piece carrying on in line (lower: o's centre comes first along
|
||||
/// its axis; wt: the other's half thickness);
|
||||
/// - "corner": a piece ending there at an angle: a mitre, so the two never
|
||||
/// overlap (k below; owner: o draws a corner post if any, the thicker piece
|
||||
/// or, equal, the lower id; cos, sin: of the angle between the two axes);
|
||||
/// - "tee": o ends on a piece's side (or on two pieces in line ending there):
|
||||
/// it stops at that piece's face (stop: the piece is of a `stops` type).
|
||||
/// ext and k give the slanted end: the line v to o's left (local y) reaches
|
||||
/// ext + k * v past the end; the depth sort's outlines keep mitred and
|
||||
/// slanted pieces apart.
|
||||
fn line_meet(o, e, same, stops) {
|
||||
let ht = line_ht(o);
|
||||
let xo = dir_of(o);
|
||||
let yo = [-xo[1], xo[0]];
|
||||
let uo = [e * xo[0], e * xo[1]];
|
||||
let P = world_of(o, e * o.p.w / 2.0, 0.0);
|
||||
let axis = on_axes(o.rot);
|
||||
let tee = ();
|
||||
let run = ();
|
||||
let corners = [];
|
||||
for w in objects_on(same + stops, P[0], P[1], 0.05) {
|
||||
if w.id == o.id || (w.z - o.z).abs() > 0.5 { continue; }
|
||||
let wt = line_ht(w);
|
||||
if w.v.abs() > wt + 0.02 { continue; }
|
||||
if !on_axes(w.rot) { axis = false; }
|
||||
let xw = dir_of(w);
|
||||
let par = dot2(uo, xw).abs() > 0.999;
|
||||
let at_end = (w.u.abs() - w.p.w / 2.0).abs() < 0.02;
|
||||
if !par && (!same.contains(w.type) || !at_end) {
|
||||
if type_of(tee) == "()" { tee = w; }
|
||||
} else if par && at_end {
|
||||
run = w;
|
||||
} else if !par {
|
||||
corners.push(w);
|
||||
}
|
||||
}
|
||||
if axis { return #{ kind: "axis" }; }
|
||||
// stop at the face of w on o's side
|
||||
let face = |w| {
|
||||
let xw = dir_of(w);
|
||||
let yw = [-xw[1], xw[0]];
|
||||
let a = dot2(uo, yw);
|
||||
let s = if a < 0.0 { 1.0 } else { -1.0 };
|
||||
#{ ext: (s * line_ht(w) - w.v) / a, k: -dot2(yo, yw) / a }
|
||||
};
|
||||
if type_of(tee) != "()" {
|
||||
let f = face.call(tee);
|
||||
return #{ kind: "tee", ext: f.ext, k: f.k, stop: !same.contains(tee.type), w: tee };
|
||||
}
|
||||
if type_of(run) != "()" {
|
||||
return #{ kind: "run", ext: 0.0, k: 0.0, lower: dot2([o.x, o.y], xo) < dot2([run.x, run.y], xo), wt: line_ht(run), w: run };
|
||||
}
|
||||
if corners.len() > 1 {
|
||||
// two in line make a straight run o meets as a T; else (a Y) the
|
||||
// lowest id runs through and the others stop at it
|
||||
let t = ();
|
||||
for a in corners {
|
||||
for b in corners {
|
||||
if a.id < b.id && dot2(dir_of(a), dir_of(b)).abs() > 0.999 { t = a; }
|
||||
}
|
||||
}
|
||||
if type_of(t) == "()" {
|
||||
t = corners[0];
|
||||
for c in corners { if c.id < t.id { t = c; } }
|
||||
if o.id < t.id { return #{ kind: "free", ext: 0.0, k: 0.0 }; }
|
||||
}
|
||||
let f = face.call(t);
|
||||
return #{ kind: "tee", ext: f.ext, k: f.k, stop: false, w: t };
|
||||
}
|
||||
if corners.len() == 1 {
|
||||
let w = corners[0];
|
||||
let xw = dir_of(w);
|
||||
let sw = if w.u > 0.0 { 1.0 } else { -1.0 };
|
||||
let uw = [sw * xw[0], sw * xw[1]];
|
||||
let wt = line_ht(w);
|
||||
return #{ kind: "corner", ext: 0.0, k: dot2(yo, uw) / fmax(1.0 - dot2(uo, uw), 1e-6), w: w, wt: wt,
|
||||
owner: ht > wt + 0.005 || ((ht - wt).abs() <= 0.005 && o.id < w.id),
|
||||
cos: dot2(xo, xw).abs(), sin: (xo[0] * xw[1] - xo[1] * xw[0]).abs() };
|
||||
}
|
||||
#{ kind: "free", ext: 0.0, k: 0.0 }
|
||||
}
|
||||
|
||||
/// A box from x0 to x1 along local x whose ends may be slanted: at y the
|
||||
/// start lies at x0 + k0 * y and the end at x1 + k1 * y (k0 = k1 = 0: a box).
|
||||
fn cut_box(x0, k0, x1, k1, y0, y1, z0, z1, mat, o) {
|
||||
if k0 == 0.0 && k1 == 0.0 { return this.box(x0, y0, x1, y1, z0, z1, mat, o); }
|
||||
let a0 = x0 + k0 * y0;
|
||||
let a1 = x0 + k0 * y1;
|
||||
let b0 = fmax(x1 + k1 * y0, a0 + 0.001);
|
||||
let b1 = fmax(x1 + k1 * y1, a1 + 0.001);
|
||||
this.prism([[a0, y0], [b0, y0], [b1, y1], [a1, y1]], z0, z1, mat, o);
|
||||
this
|
||||
}
|
||||
fn cut_box(x0, k0, x1, k1, y0, y1, z0, z1, mat) {
|
||||
if k0 == 0.0 && k1 == 0.0 { return this.box(x0, y0, x1, y1, z0, z1, mat); }
|
||||
let a0 = x0 + k0 * y0;
|
||||
let a1 = x0 + k0 * y1;
|
||||
let b0 = fmax(x1 + k1 * y0, a0 + 0.001);
|
||||
let b1 = fmax(x1 + k1 * y1, a1 + 0.001);
|
||||
this.prism([[a0, y0], [b0, y0], [b1, y1], [a1, y1]], z0, z1, mat);
|
||||
this
|
||||
}
|
||||
|
||||
/// A cut from line_meet as the start (e = -1) or end (e = 1) of a piece
|
||||
/// w long: #{ x, k } with the end at x + k * y.
|
||||
fn end_cut(w, e, m) {
|
||||
if e < 0.0 { #{ x: -w / 2.0 - m.ext, k: -m.k } } else { #{ x: w / 2.0 + m.ext, k: m.k } }
|
||||
}
|
||||
|
||||
/// How line piece o (a fence or wall along its local x, length p.w) meets
|
||||
|
||||
Reference in new issue
Block a user