Files
isokit/crates/isokit-script/stdlib/ground.rhai
T
grabowski e4017ce2a8
Deploy web Studio / deploy (push) Successful in 3m52s
Fences join like walls, fence designs, draw runs corner by corner
Joints: line_joint (util.rhai) applies the house-wall rules to fences and
site walls, so pieces never overlap and the depth sort stays clean.
- A corner is closed: the thicker piece (or, if equal, the one along
  world x) runs through, and the other stops at its face.
- On a straight run, two pieces share one post.
- At a T, the stem stops at the other piece's face without a post of its
  own.
- A fence that ends at a site or house wall stops at the wall's face.

Fence designs (prop style): mesh, wooden boards, steel bars, mesh on a
stone base, cobble wall, and brick wall with pillars. Board, stone and
brick joints are drawn on the face turned to the viewer. Brick pillars
are separate sprites.

Studio: line prefabs (walls, fences, hedges, roads) can be drawn as a
run by clicking corner after corner. Double-click or Enter ends the run,
clicking the first corner closes it, and Backspace drops the last
corner. A run is one undo step. A drag still draws one piece. Corners
and ends snap onto the ends and lines of the pieces they meet, and a
marker shows where the next corner will snap.

The house demo gets a brick front wall with gate pillars and wooden side
fences in place of the old east fence, which ran over the road. Tests
cover every design's corners, Ts, straight runs and wall stops, with no
depth-sort cycles in any rotation, plus site walls. check_studio draws
and closes a fence run, undoes it in one step, snaps a dragged spur,
and picks a design.
2026-10-24 18:07:40 +07:00

298 lines
12 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",
help: "A straight road along its length, with a dashed centre line.",
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" },
] }
}
fn prefab_road(o) {
let p = o.p;
let w = p.w / 2.0;
let d = p.d / 2.0;
layer("ground");
let s = sprite();
s.flat(rect(-w, -d, w, d), 0.007, "g-asph t");
if p.dashed { s.lines([[-w, 0.0, w, 0.0]], 0.012, "dash"); }
if p.kerb { s.lines([[-w, -d, w, -d], [-w, d, w, d]], 0.012, "kerb"); }
}
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; 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 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" });
}
}
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 = 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;
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, x0 + ph), x1 - ph));
}
}
if st == "brick" { brick_fence(x0, x1, posts, p.h); return; }
// 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 = [];
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)); }
}
fence_piece(sprite(), a, b, 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) {
let L = b - a;
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.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"); }
} 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");
} 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");
}