Files
isokit/crates/isokit-script/stdlib/ground.rhai
T
grabowski c91d63663b
Deploy web Studio / deploy (push) Successful in 4m31s
Streets: sidewalks, road curves, junctions and roundabouts
Roads take sidewalks (none, both, left, right): 12 cm high paving with
kerb faces drawn where they face the viewer. Road curve (radius, angle,
left/right) carries a street round a bend; centre lines are true arcs
when cut and offset, so curve joints are seamless. A road ending on
another road's centre line opens that road's kerb and sidewalk (T
junction); two roads ending at one corner close it. Roundabout: ring
radius and width, one or two lanes, up to four arms with flared kerbs,
splitter islands, give-way lines on the entry side (traffic on the left
or right) and a central island with apron.

Studio: a road run drawn corner by corner gets a curve in each corner
(prefab meta `corner`). Tooltips list the streets each one meets.
Engine: open `polyline` primitive (Prim::Poly open) so dashes run on
round curves. examples/streets and a Streets demo in the web Studio;
MCP guidance, docs. ROADMAP: backlog 5-7 done; multi-select and the
hosted copy marked done.

Tests: Rust 72, Studio 113, web 95.
2026-10-25 12:32:52 +07:00

302 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", 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; 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");
}