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.
397 lines
17 KiB
Plaintext
397 lines
17 KiB
Plaintext
// isokit/ground: site plate, paved and green areas, roads, parking, walls,
|
|
// fences and lamps. Ground surfaces go to the unsorted "ground" layer in
|
|
// document order (later objects lie on top).
|
|
|
|
fn ground_materials() {
|
|
["g-grass", "g-grass2", "g-green", "g-garden", "g-asph", "g-conc", "g-pad", "g-path", "g-sand", "g-court", "dirt"]
|
|
}
|
|
|
|
fn meta_site() {
|
|
#{ label: "Site ground", category: "Ground", icon: "▭", shape: "rect",
|
|
help: "The ground plate: a surface with earth sides. Frames the view.",
|
|
props: [
|
|
#{ key: "w", type: "float", def: 60.0, min: 4.0, max: 1000.0, step: 1.0, label: "Width (m)" },
|
|
#{ key: "d", type: "float", def: 40.0, min: 4.0, max: 1000.0, step: 1.0, label: "Depth (m)" },
|
|
#{ key: "mat", type: "material", def: "g-grass", choices: ground_materials(), label: "Surface" },
|
|
#{ key: "base", type: "float", def: 1.6, min: 0.2, max: 10.0, step: 0.1, label: "Earth depth (m)" },
|
|
] }
|
|
}
|
|
fn prefab_site(o) {
|
|
let p = o.p;
|
|
let w = p.w / 2.0;
|
|
let d = p.d / 2.0;
|
|
layer("ground");
|
|
sprite().box(-w, -d, w, d, -p.base, 0.0, "dirt", #{ top: p.mat });
|
|
bounds(-w, -d, w, d, -p.base);
|
|
}
|
|
|
|
fn meta_area() {
|
|
#{ label: "Area", category: "Ground", icon: "▱", shape: "rect",
|
|
help: "A paved or planted area lying on the ground.",
|
|
props: [
|
|
#{ key: "w", type: "float", def: 10.0, min: 0.5, max: 1000.0, step: 0.5, label: "Width (m)" },
|
|
#{ key: "d", type: "float", def: 6.0, min: 0.5, max: 1000.0, step: 0.5, label: "Depth (m)" },
|
|
#{ key: "mat", type: "material", def: "g-conc", choices: ground_materials(), label: "Surface" },
|
|
] }
|
|
}
|
|
fn prefab_area(o) {
|
|
let p = o.p;
|
|
layer("ground");
|
|
let s = sprite();
|
|
if o.pts != () && o.pts.len() >= 3 {
|
|
s.flat(o.pts, 0.006, p.mat + " t");
|
|
} else {
|
|
s.flat(rect(-p.w / 2.0, -p.d / 2.0, p.w / 2.0, p.d / 2.0), 0.006, p.mat + " t");
|
|
}
|
|
}
|
|
|
|
fn meta_road() {
|
|
#{ label: "Road", category: "Ground", icon: "═", shape: "line", corner: "road_curve",
|
|
help: "A straight road along its length, with a dashed centre line and sidewalks if you like (left and right looking along local +x). Roads ending on another road's side make a junction; a run drawn corner by corner gets a curve in each corner.",
|
|
props: [
|
|
#{ key: "w", type: "float", def: 40.0, min: 2.0, max: 2000.0, step: 1.0, label: "Length (m)" },
|
|
#{ 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(o) {
|
|
let p = o.p;
|
|
layer("ground");
|
|
let others = other_street_ends(o);
|
|
let ends = street_ends(o);
|
|
let c = line_path([[-p.w / 2.0, 0.0], [p.w / 2.0, 0.0]]);
|
|
let j0 = street_join(o, ends[0], others);
|
|
let j1 = street_join(o, ends[1], others);
|
|
let gaps = street_gaps(o, others);
|
|
street_path(o, c, j0, j1, gaps);
|
|
street_info(p, `${num1(p.w)} m long, ${num1(p.d)} m wide`, [j0.meet, j1.meet], gaps[2]);
|
|
}
|
|
|
|
fn meta_parking() {
|
|
#{ label: "Parking bays", category: "Ground", icon: "P",
|
|
props: [
|
|
#{ key: "n", type: "int", def: 6, min: 1, max: 60, step: 1, label: "Bays" },
|
|
#{ key: "bay_w", type: "float", def: 2.5, min: 2.0, max: 4.0, step: 0.1, label: "Bay width (m)" },
|
|
#{ key: "bay_d", type: "float", def: 5.0, min: 4.0, max: 7.0, step: 0.1, label: "Bay depth (m)" },
|
|
] }
|
|
}
|
|
fn prefab_parking(o) {
|
|
let p = o.p;
|
|
let w = p.n * p.bay_w / 2.0;
|
|
let d = p.bay_d / 2.0;
|
|
layer("ground");
|
|
let s = sprite();
|
|
s.flat(rect(-w, -d, w, d), 0.008, "g-pad t");
|
|
let segs = [];
|
|
for i in 0..(p.n + 1) {
|
|
let x = -w + i * p.bay_w;
|
|
segs.push([x, -d, x, d]);
|
|
}
|
|
s.lines(segs, 0.014, "bayline");
|
|
}
|
|
|
|
fn meta_wall() {
|
|
#{ label: "Wall", category: "Site", icon: "▬", shape: "line",
|
|
help: "Drag along the map, or click corner after corner for a run around a plot. Walls that meet join at corners and T-junctions.",
|
|
props: [
|
|
#{ key: "w", type: "float", def: 20.0, min: 0.5, max: 500.0, step: 0.5, label: "Length (m)" },
|
|
#{ key: "h", type: "float", def: 2.2, min: 0.3, max: 8.0, step: 0.1, label: "Height (m)" },
|
|
#{ key: "t", type: "float", def: 0.24, min: 0.1, max: 1.0, step: 0.02, label: "Thickness (m)" },
|
|
] }
|
|
}
|
|
fn prefab_wall(o) {
|
|
// 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 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;
|
|
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.
|
|
fn fence_ht(style) {
|
|
switch style { "wood" => 0.05, "half stone" => 0.16, "cobble" => 0.22, "brick" => 0.18, _ => 0.04 }
|
|
}
|
|
|
|
fn meta_fence() {
|
|
#{ label: "Fence", category: "Site", icon: "┼", shape: "line",
|
|
help: "Drag along the map, or click corner after corner for a run around a plot. Fences that meet join at corners and T-junctions and stop at walls. Designs: mesh, wooden boards, steel bars, mesh on a stone base, cobble wall, brick wall with pillars.",
|
|
props: [
|
|
#{ key: "w", type: "float", def: 20.0, min: 1.0, max: 500.0, step: 0.5, label: "Length (m)" },
|
|
#{ key: "h", type: "float", def: 1.8, min: 0.5, max: 4.0, step: 0.1, label: "Height (m)" },
|
|
#{ key: "style", type: "choice", def: "mesh", choices: fence_styles(), label: "Design" },
|
|
] }
|
|
}
|
|
fn prefab_fence(o) {
|
|
let p = o.p;
|
|
let st = p.style;
|
|
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
|
|
let ph = fence_ht(st);
|
|
let gap = if st == "brick" { 3.0 } else { 2.5 };
|
|
let k = fmax(1.0, (p.w / gap).round()).to_int();
|
|
let posts = [];
|
|
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, u0 + ph), u1 - ph));
|
|
}
|
|
}
|
|
// 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;
|
|
for i in 0..n {
|
|
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, ua + ph), ub - ph)); }
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// 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 = [];
|
|
let m = fmax(1.0, (L / 0.2).round()).to_int();
|
|
for j in 0..m { boards.push([j * L / m, (j + 1) * L / m, 0.04, h - 0.02]); }
|
|
s.open([a, -0.025], [b, -0.025], boards, "seam");
|
|
s.open([b, 0.025], [a, 0.025], boards, "seam");
|
|
for x in ps { s.box(x - 0.05, -0.05, x + 0.05, 0.05, 0.0, h + 0.06, "woodd"); }
|
|
} else if st == "bars" {
|
|
let bars = [];
|
|
let m = fmax(2.0, (L / 0.16).round()).to_int();
|
|
for j in 1..m { let u = j * L / m; bars.push([u - 0.014, u + 0.014, 0.0, h - 0.05]); }
|
|
s.open([a, 0.0], [b, 0.0], bars, "steel");
|
|
s.open([b, 0.0], [a, 0.0], bars, "steel");
|
|
s.box(a, -0.025, b, 0.025, 0.1, 0.15, "steel");
|
|
s.box(a, -0.025, b, 0.025, h - 0.06, h, "steel");
|
|
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.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.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");
|
|
for x in ps { s.box(x - 0.04, -0.04, x + 0.04, 0.04, 0.0, h + 0.05, "steel"); }
|
|
}
|
|
}
|
|
|
|
/// A brick wall between pillars: pillars and wall stretches are separate
|
|
/// sprites (the pillars are thicker), with a coping on top and brick courses.
|
|
fn brick_fence(x0, x1, posts, h) {
|
|
let hw = fmax(0.3, h - 0.06);
|
|
let u = x0;
|
|
for x in posts + [x1 + 1.0] {
|
|
let b = fmin(x - 0.18, x1);
|
|
if b > u + 0.02 {
|
|
let n = ((b - u) / 30.0).ceiling().to_int();
|
|
let L = (b - u) / n;
|
|
for k in 0..n {
|
|
let s = sprite();
|
|
let a = u + k * L;
|
|
s.box(a, -0.12, a + L, 0.12, 0.0, hw, "brick");
|
|
s.box(a, -0.14, a + L, 0.14, hw, h, "plinth");
|
|
let j = brick_joints(L, 0.0, hw, a);
|
|
s.open([a, -0.12], [a + L, -0.12], j, "seam");
|
|
s.open([a + L, 0.12], [a, 0.12], j, "seam");
|
|
}
|
|
}
|
|
if x <= x1 {
|
|
let s = sprite();
|
|
s.box(x - 0.17, -0.17, x + 0.17, 0.17, 0.0, h + 0.1, "brick");
|
|
s.box(x - 0.18, -0.18, x + 0.18, 0.18, h + 0.1, h + 0.17, "plinth");
|
|
u = x + 0.18;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Joints of a stone face L long from z0 to z1: rows of stones of varying length.
|
|
fn stone_joints(L, z0, z1, seed) {
|
|
let out = [];
|
|
let rows = fmax(1.0, ((z1 - z0) / 0.22).round()).to_int();
|
|
let rh = (z1 - z0) / rows;
|
|
for r in 0..rows {
|
|
let k = seed * 31 + r * 17 + 7;
|
|
let u = 0.0;
|
|
while u < L - 0.05 {
|
|
k = (k * 1103 + 12345) % 9973;
|
|
let u1 = fmin(L, u + 0.28 + (k % 100) / 100.0 * 0.34);
|
|
if L - u1 < 0.12 { u1 = L; }
|
|
out.push([u, u1, z0 + r * rh, z0 + (r + 1) * rh]);
|
|
u = u1;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Brick courses on a face L long from z0 to z1, in running bond; `at`
|
|
/// shifts the bond so neighbouring stretches line up.
|
|
fn brick_joints(L, z0, z1, at) {
|
|
let out = [];
|
|
let rows = fmax(1.0, ((z1 - z0) / 0.25).round()).to_int();
|
|
let rh = (z1 - z0) / rows;
|
|
for r in 0..rows {
|
|
let off = if r % 2 == 0 { 0.0 } else { 0.25 };
|
|
let u = 0.0;
|
|
let first = 0.5 - ((at + off) % 0.5 + 0.5) % 0.5;
|
|
let u1 = if first < 0.05 { first + 0.5 } else { first };
|
|
while u < L - 0.02 {
|
|
let e = fmin(L, u1);
|
|
out.push([u, e, z0 + r * rh, z0 + (r + 1) * rh]);
|
|
u = e;
|
|
u1 = e + 0.5;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn meta_lamp() {
|
|
#{ label: "Street lamp", category: "Site", icon: "†",
|
|
help: "Keep lamps on verges and kerbs, not on drive areas.",
|
|
props: [ #{ key: "h", type: "float", def: 5.0, min: 2.0, max: 12.0, step: 0.5, label: "Height (m)" } ] }
|
|
}
|
|
fn prefab_lamp(o) {
|
|
let s = sprite();
|
|
s.box(-0.07, -0.07, 0.07, 0.07, 0.0, o.p.h, "lampost", #{ no_top: true });
|
|
s.circle(0.0, 0.0, o.p.h + 0.1, 1.2, "lamp");
|
|
}
|
|
|
|
fn meta_marker() {
|
|
#{ label: "Marker", category: "Site", icon: "⚑",
|
|
help: "A pin with a label: set the label in Tag and the tooltip in Info.",
|
|
props: [ #{ key: "h", type: "float", def: 1.5, min: 0.3, max: 20.0, step: 0.1, label: "Height (m)" } ] }
|
|
}
|
|
fn prefab_marker(o) {
|
|
let s = sprite();
|
|
s.box(-0.05, -0.05, 0.05, 0.05, 0.0, o.p.h, "steel", #{ no_top: true });
|
|
s.circle(0.0, 0.0, o.p.h, 2.0, "led ledo");
|
|
}
|