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.
251 lines
10 KiB
Plaintext
251 lines
10 KiB
Plaintext
// isokit/util: helpers shared by the prefabs.
|
|
|
|
/// Floor index of a level id: "gf" -> 0, "1f" -> 1, "2f" -> 2; anything else
|
|
/// (roof, site, "") -> -1, which means "closed buildings".
|
|
fn floor_of(lv) {
|
|
if lv == "gf" { return 0; }
|
|
if lv.len() >= 2 && lv.ends_with("f") {
|
|
let k = lv.sub_string(0, lv.len() - 1);
|
|
let digits = true;
|
|
for c in k.chars() { if !"0123456789".contains(c) { digits = false; } }
|
|
if digits { return parse_int(k); }
|
|
}
|
|
-1
|
|
}
|
|
|
|
/// Walls of a cut-open room over the rectangle [x0, y0, x1, y1] from height z:
|
|
/// walls facing the viewer are cut low (lo), the others stay high (hi), so
|
|
/// the room can be seen into from every rotation. Each wall is its own sprite
|
|
/// for the depth sort. skip: list of walls to leave out (0 S, 1 N, 2 W, 3 E).
|
|
fn cut_walls(x0, y0, x1, y1, z, lo, hi, t, mat, skip) {
|
|
let W = [
|
|
[x0, y0, x1, y0 + t, 0, -1],
|
|
[x0, y1 - t, x1, y1, 0, 1],
|
|
[x0, y0 + t, x0 + t, y1 - t, -1, 0],
|
|
[x1 - t, y0 + t, x1, y1 - t, 1, 0],
|
|
];
|
|
for i in 0..4 {
|
|
if skip.contains(i) { continue; }
|
|
let w = W[i];
|
|
let h = if vis(w[4], w[5]) { lo } else { hi };
|
|
sprite().box(w[0], w[1], w[2], w[3], z, z + h, mat, #{ top: "cut" });
|
|
}
|
|
}
|
|
|
|
/// Windows spread along the face a -> b (length L) for one storey:
|
|
/// returns [[u0, u1, z0, z1], ...]; leaves a gap around u = gap (or -1).
|
|
fn win_list(L, z0, sill, height, width, spacing, gap) {
|
|
let out = [];
|
|
let n = ((L - 1.0) / spacing).floor().to_int();
|
|
if n < 1 { return out; }
|
|
let start = (L - (n - 1) * spacing) / 2.0;
|
|
for i in 0..n {
|
|
let u = start + i * spacing;
|
|
if gap >= 0.0 && (u - gap).abs() < width + 0.6 { continue; }
|
|
out.push([u - width / 2.0, u + width / 2.0, z0 + sill, z0 + sill + height]);
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Animate the next drawing of sprite s along (dx, dy) metres of its frame:
|
|
/// call before drawing, then s.end(). Only animates when the motion runs
|
|
/// along the axis the depth sort compares second; returns whether it does.
|
|
fn drive(dx, dy, dur, delay) {
|
|
let ok = if dx != 0.0 { along_x() } else { !along_x() };
|
|
if !ok { this.begin(""); return false; }
|
|
let v = dv(dx, dy, 0.0);
|
|
this.begin("a drive", `--dx:${v[0]}px;--dy:${v[1]}px;animation-duration:${dur}s;animation-delay:-${delay}s`);
|
|
true
|
|
}
|
|
|
|
/// 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 }
|
|
|
|
/// 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 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
|
|
/// other pieces at its end e (-1 or 1): #{ ext, post }, how far it reaches
|
|
/// past the end and whether it draws the end post. `same`: the types it joins
|
|
/// with; `stops`: types it ends against. The rules of the house walls, so
|
|
/// pieces never overlap (the depth sort needs them apart):
|
|
/// - alone it reaches half its thickness past the end, flush with its post;
|
|
/// - on a straight run the lower piece runs through and draws the shared
|
|
/// post, the other starts at that post;
|
|
/// - at a corner the thicker piece (equal: the one along world x) runs
|
|
/// through, the other stops at its face;
|
|
/// - at a T it stops at the other's face without an end post (the other
|
|
/// piece has its own there); against a `stops` piece it keeps its post.
|
|
fn line_joint(o, e, same, stops) {
|
|
let ht = line_ht(o);
|
|
let p = to_world(e * o.p.w / 2.0, 0.0);
|
|
let c = to_world(0.0, 0.0);
|
|
let q = to_world(e * (o.p.w / 2.0 - 0.05), 0.0);
|
|
let ox = along_world_x(o.rot);
|
|
let face = 0.0;
|
|
let has_face = false;
|
|
let face_post = false;
|
|
let run = #{};
|
|
let corner = [];
|
|
for w in objects_on(same + stops, p[0], p[1], 0.5) {
|
|
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; }
|
|
let at_end = (w.u.abs() - w.p.w / 2.0).abs() < 0.02;
|
|
let wx = along_world_x(w.rot);
|
|
let side = if local_of(w, q[0], q[1])[1] >= w.v { 1.0 } else { -1.0 };
|
|
let stop = -(wt - side * w.v);
|
|
if !same.contains(w.type) || (!at_end && wx != ox) {
|
|
face = stop;
|
|
has_face = true;
|
|
face_post = !same.contains(w.type);
|
|
} else if wx == ox {
|
|
if at_end { run = #{ lower: if ox { c[0] < w.x } else { c[1] < w.y }, wt: wt }; }
|
|
} else {
|
|
corner.push(#{ wt: wt, stop: stop });
|
|
}
|
|
}
|
|
if has_face { return #{ ext: face, post: face_post }; }
|
|
if run.len() > 0 {
|
|
return if run.lower { #{ ext: ht, post: true } } else { #{ ext: -run.wt, post: false } };
|
|
}
|
|
// two pieces end here from both sides: a straight run this one meets as a T
|
|
if corner.len() > 1 { return #{ ext: corner[0].stop, post: false }; }
|
|
if corner.len() == 1 {
|
|
let k = corner[0];
|
|
let mine = ht > k.wt + 0.005 || ((ht - k.wt).abs() <= 0.005 && ox);
|
|
return if mine { #{ ext: ht, post: true } } else { #{ ext: k.stop, post: false } };
|
|
}
|
|
#{ ext: ht, post: true }
|
|
}
|