Files
isokit/crates/isokit-script/stdlib/house.rhai
T
grabowski 2810477f1b
Deploy web Studio / deploy (push) Successful in 1m4s
Data centres: multi-storey shell, racks and pods, cooling, fire safety, power
datacentre.rhai (palette groups Data centre, Cooling, Fire safety, Power):

- dc_building: multi-storey shell (storeys of storey_h), walls in pieces,
  slabs, roof with parapet, louvres, front door and loading dock; levels
  cut it open storey by storey
- raised_floor; rack_row and dc_pod (cabinets by U, width, depth,
  equipment, fill, doors, LEDs; in-row coolers; cold / hot aisle
  containment with vent tiles, roof, end doors, ceiling duct, airflow);
  dc_cage for colocation
- crah, chiller (air-cooled, dry, adiabatic), cooling_tower, pipe_run
- smoke_detectors (grid + coverage), fire_pipe (VESDA, sprinkler, gas),
  gas_bank (Novec 1230, FM-200, Inergen sized for its halls), fire_device
  (panels, call points, abort switches, beacons, extinguishers)
- power_cab (UPS, battery, PDU, switchgear, RPP), busway, cable_tray,
  transformer
- tooltips sum IT load, kW/m2, cooling redundancy per storey, UPS load,
  battery runtime, plant cooling and agent quantity (warning when short)

Engine and stdlib:

- part_hidden(o) in house.rhai: parts on closed storeys inside a
  dc_building are not drawn (they only made sort cycles under the slab
  above); house parts, furniture and people use it. Existing examples
  render byte-identical.
- overhead services sort in their own "ceiling" layer with tight boxes
- sort records which sprites each broken cycle involved; isokit check
  names them (Rendered.cycle_at)

examples/datacentre (three storeys and a roof plant), Data centre demo in
the web Studio, tests, SCRIPTING/README/ROADMAP, MCP instructions.
2026-10-27 17:08:39 +07:00

528 lines
22 KiB
Plaintext

// isokit/house: build a house from parts. Walls run along their local x;
// a window or door placed on a wall line sits on that wall (the wall leaves
// the gap, the window draws the opening); floors tell rooms from outside;
// roofs and stairs. Furniture is in isokit/furniture.
//
// Storeys: a part's storey is floor((z + 0.5) / storey_h), storey_h being
// the project parameter of that name (default 3 m). Level "gf" shows storey
// 0 cut open, "1f" storey 1 and so on: walls in front of a room are cut low,
// roofs show as an outline, parts on the storeys above are left out. Any
// other level ("site", "roof") shows the house closed.
fn storey_h() {
let h = cfg("storey_h");
let t = type_of(h);
let v = if t == "i64" { h.to_float() } else if t == "f64" { h } else { 0.0 };
if v > 0.0 { v } else { 3.0 }
}
fn storey_of(z) { ((z + 0.5) / storey_h()).floor().to_int() }
/// How part o shows on this level: 0 closed (a normal level, or a storey
/// below the one cut open), 1 on the storey cut open, 2 above it (left out).
fn house_view(o) {
let k = floor_of(o.level);
if k < 0 { return 0; }
let s = storey_of(o.z);
if s < k { 0 } else if s == k { 1 } else { 2 }
}
/// Leave part o out? On a storey above the one a level cuts open, or on a
/// closed storey inside a data centre shell (dc_building), whose walls and
/// slabs hide it anyway (and would sort badly against what stands above).
fn part_hidden(o) {
let hv = house_view(o);
if hv == 2 { return true; }
if hv == 1 { return false; }
let s = storey_of(o.z);
for b in objects_at(["dc_building"], o.x, o.y) {
let s0 = storey_of(b.z);
if s >= s0 && s < s0 + b.p.storeys { return true; }
}
false
}
fn wall_cut_h() { 1.0 }
fn wall_types() { ["house_wall"] }
fn opening_types() { ["window", "door"] }
fn room_types() { ["floor", "raised_floor"] }
fn house_wall_materials() { ["render", "render2", "brick", "wood", "wall", "wall2", "wallp", "concb", "office", "furnw", "firewall"] }
fn flooring_materials() { ["f-oak", "f-wood", "f-tile", "f-stone", "f-carpet", "f-conc", "z-floor", "z-living", "z-kitchen", "z-bed", "z-wc"] }
fn house_roof_materials() { ["rooft", "roofr", "roof", "roof2", "roof3", "roofcap", "canopy"] }
/// Is there a room (a floor on wall w's storey) at (u, v) of the current frame?
fn room_at(w, u, v) {
let q = to_world(u, v);
let s = storey_of(w.z);
for f in objects_at(room_types(), q[0], q[1]) {
if storey_of(f.z) == s { return true; }
}
false
}
/// Height wall w is drawn to; call it in the wall's frame. On the storey cut
/// open a wall keeps its full height only when it stands behind a room (a
/// floor on the viewer's side of it, none behind it); the others are cut low
/// so the rooms can be seen into. Walls without floors around are cut low.
fn wall_top(w) {
let p = w.p;
if house_view(w) != 1 { return p.h; }
let near = if vis(0, -1) { -1.0 } else { 1.0 };
let off = p.t / 2.0 + 0.3;
let seen = false;
let behind = false;
for u in [-0.3 * p.w, 0.0, 0.3 * p.w] {
if room_at(w, u, near * off) { seen = true; }
if room_at(w, u, -near * off) { behind = true; }
}
if seen && !behind { p.h } else { fmin(p.h, wall_cut_h()) }
}
/// Span [u0, u1] along wall h of an opening `width` wide centred at u,
/// kept inside the wall's length.
fn opening_span(h, u, width) {
let L = h.p.w / 2.0;
let hw = fmin(width, h.p.w) / 2.0;
let c = clamp(u, -L + hw, L - hw);
[c - hw, c + hw]
}
/// Where opening o sits: #{ wall, u0, u1, rel }, with the frame set to the
/// wall's. rel: o's rotation relative to the wall in quarter turns. A
/// window or door on no wall stands alone as a short wall piece.
fn opening_at(o) {
let h = mount_of(o, wall_types());
if type_of(h) == "()" {
let w = #{ id: o.id, type: "house_wall", x: o.x, y: o.y, z: o.z, rot: o.rot, level: o.level,
p: #{ w: o.p.w, h: 2.8, t: 0.15, mat: "render" } };
return #{ wall: w, u0: -o.p.w / 2.0, u1: o.p.w / 2.0, rel: 0 };
}
frame(h);
let sp = opening_span(h, h.u, o.p.w);
let q = ((o.rot - h.rot) / 90.0).round().to_int() % 4;
if q < 0 { q += 4; }
#{ wall: h, u0: sp[0], u1: sp[1], rel: q }
}
/// A thin upright slab from plan point a to b, th thick (on the left of
/// a -> b), from z0 to z1: door leaves at any angle.
fn slab(a, b, th, z0, z1, mat) {
let dx = b[0] - a[0];
let dy = b[1] - a[1];
let L = fmax((dx * dx + dy * dy).sqrt(), 0.001);
let nx = -dy / L * th;
let ny = dx / L * th;
this.prism([a, b, [b[0] + nx, b[1] + ny], [a[0] + nx, a[1] + ny]], z0, z1, mat);
this
}
// ---------------------------------------------------------------- walls
fn meta_house_wall() {
#{ label: "House wall", category: "House", icon: "▮", shape: "line", storey: true,
help: "A wall along its length (local x); drag on the map to draw one. Windows and doors placed on it cut their openings. GF / 1F cut walls in front of a room low; walls tell rooms by their floors.",
props: [
#{ key: "w", type: "float", def: 4.0, min: 0.3, max: 60.0, step: 0.1, label: "Length (m)" },
#{ key: "h", type: "float", def: 2.8, min: 0.3, max: 8.0, step: 0.1, label: "Height (m)" },
#{ key: "t", type: "float", def: 0.15, min: 0.05, max: 0.6, step: 0.01, label: "Thickness (m)" },
#{ key: "mat", type: "material", def: "render", choices: house_wall_materials(), label: "Material" },
] }
}
fn prefab_house_wall(o) {
let p = o.p;
if part_hidden(o) { return; }
let top = wall_top(o);
let cap = if top < p.h { "cut" } else { p.mat };
let t = p.t / 2.0;
// gaps for the windows and doors on this wall, left to right
let gaps = [];
for m in mounted(o, opening_types(), wall_types()) {
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 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, k, g[0], 0.0, t, top, p.mat, cap); }
if g[1] > x { k = 0.0; }
x = fmax(x, g[1]);
}
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:
/// half its thickness, so a corner closes. Where other walls meet it,
/// the walls must not overlap (the depth sort needs them apart): along a
/// line the two just meet; at a T this wall stops at the other's face; at
/// a corner the wall along world x runs through, the other stops at it.
fn wall_ext(o, e) {
let t = o.p.t / 2.0;
let p = to_world(e * o.p.w / 2.0, 0.0);
let ox = along_world_x(o.rot);
let s = storey_of(o.z);
let ext = t;
for w in objects_on(wall_types(), p[0], p[1], 0.0) {
if w.id == o.id || storey_of(w.z) != s { continue; }
if along_world_x(w.rot) == ox { return 0.0; }
let corner = (w.u.abs() - w.p.w / 2.0).abs() < 0.02;
if corner && ox {
ext = fmax(ext, w.p.t / 2.0);
} else {
// stop at the face of w on this wall's side
let q = to_world(e * (o.p.w / 2.0 - 0.05), 0.0);
let side = if local_of(w, q[0], q[1])[1] >= w.v { 1.0 } else { -1.0 };
return -(w.p.t / 2.0 - side * w.v);
}
}
ext
}
/// 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().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 });
}
}
// ---------------------------------------------------------------- windows and doors
fn meta_window() {
#{ label: "Window", category: "House", icon: "⊞", mount: wall_types(),
help: "Place it on a house wall: it snaps onto the wall, which leaves the opening for it, and moves with the wall. On no wall it stands as a short wall piece.",
props: [
#{ key: "w", type: "float", def: 1.2, min: 0.3, max: 8.0, step: 0.05, label: "Width (m)" },
#{ key: "h", type: "float", def: 1.3, min: 0.3, max: 3.5, step: 0.05, label: "Height (m)" },
#{ key: "sill", type: "float", def: 0.9, min: 0.0, max: 2.5, step: 0.05, label: "Sill height (m)" },
#{ key: "panes", type: "int", def: 2, min: 1, max: 8, step: 1, label: "Panes" },
#{ key: "lit", type: "bool", def: false, label: "Lit at night" },
] }
}
fn prefab_window(o) {
let c = opening_at(o);
let w = c.wall;
if part_hidden(w) { return; }
let H = w.p.h;
let t = w.p.t / 2.0;
let top = wall_top(w);
let cap = if top < H { "cut" } else { w.p.mat };
let u0 = c.u0;
let u1 = c.u1;
let z0 = clamp(o.p.sill, 0.0, H - 0.3);
let z1 = fmin(z0 + o.p.h, H - 0.1);
let s = sprite();
// wall below the sill, the glazing with its frame, the lintel above
if z0 > 0.0 {
s.box(u0, -t, u1, t, 0.0, fmin(z0, top), w.p.mat, #{ top: if top > z0 { "sill" } else { cap } });
}
if top > z0 {
let zt = fmin(z1, top);
let pane = if o.p.lit { "glassl" } else { "glassp" };
s.box(u0, -0.025, u1, 0.025, z0, zt, pane);
let y = if vis(0, -1) { -0.03 } else { 0.03 };
let n = o.p.panes;
for i in 0..(n + 1) {
s.stem(u0 + (u1 - u0) * i / n, y, z0, zt, 0.2, "wframe");
}
s.lines([[u0, y, u1, y]], z0 + 0.01, "wframe");
if zt >= z1 { s.lines([[u0, y, u1, y]], z1, "wframe"); }
if top > z1 { s.box(u0, -t, u1, t, z1, top, w.p.mat, #{ top: cap }); }
}
}
fn meta_door() {
#{ label: "Door", category: "House", icon: "🚪", mount: wall_types(),
help: "Place it on a house wall: it snaps onto the wall and swings to the side you click on. Turning it (R) moves the hinge and then the swing side.",
props: [
#{ key: "w", type: "float", def: 0.9, min: 0.5, max: 4.0, step: 0.05, label: "Width (m)" },
#{ key: "h", type: "float", def: 2.1, min: 1.6, max: 3.5, step: 0.05, label: "Height (m)" },
#{ key: "style", type: "choice", def: "panel", choices: ["panel", "glass", "double", "sliding", "opening"], label: "Style" },
#{ key: "open", type: "bool", def: false, label: "Open" },
#{ key: "mat", type: "material", def: "wood", choices: ["wood", "woodd", "doorw", "doorg", "steel"], label: "Colour" },
] }
}
fn prefab_door(o) {
let c = opening_at(o);
let w = c.wall;
if part_hidden(w) { return; }
let p = o.p;
let H = w.p.h;
let t = w.p.t / 2.0;
let top = wall_top(w);
let cap = if top < H { "cut" } else { w.p.mat };
let u0 = c.u0;
let u1 = c.u1;
let zh = fmin(p.h, H - 0.05);
let zt = fmin(zh, top);
let leafcap = if top < zh { "cut" } else { "" };
// rel 0 / 1 swing to +v, 2 / 3 to -v; 0 / 2 hinge at u0, 1 / 3 at u1
let sw = if c.rel < 2 { 1.0 } else { -1.0 };
let lo = c.rel == 0 || c.rel == 2;
let s = sprite();
s.flat(rect(u0, -t, u1, t), 0.008, "sill t");
let st = p.style;
let mat = if st == "glass" { "glassp" } else { p.mat };
let y = if vis(0, -1) { -0.03 } else { 0.03 };
if st == "sliding" {
// two glass panes in a frame; open: the second one slid over the first
let m = (u0 + u1) / 2.0;
let b0 = if p.open { u0 } else { m - 0.03 };
s.box(u0 + 0.02, -0.045, m + 0.03, -0.01, 0.0, zt, "glassp");
s.box(b0, 0.01, b0 + (m + 0.03 - u0 - 0.02), 0.045, 0.0, zt, "glassp");
for u in [u0 + 0.02, m, u1 - 0.02] { s.stem(u, y, 0.0, zt, 0.2, "wframe"); }
} else if st != "opening" {
let leaves = if st == "double" { [[u0, (u0 + u1) / 2.0, true], [(u0 + u1) / 2.0, u1, false]] }
else if lo { [[u0, u1, true]] } else { [[u0, u1, false]] };
for l in leaves {
let a = l[0] + 0.02;
let b = l[1] - 0.02;
if !p.open {
s.box(a, -0.025, b, 0.025, 0.0, zt, mat, door_cap(leafcap, mat));
} else {
// swung open 80 degrees towards the swing side, hinged at a or b
let hu = if l[2] { a } else { b };
let L = b - a;
let du = if l[2] { 0.174 } else { -0.174 };
let hp = [hu, sw * t];
let tip = [hu + du * L, sw * (t + 0.985 * L)];
if l[2] == (sw > 0.0) { s.slab(hp, tip, 0.04, 0.0, zt, mat); } else { s.slab(tip, hp, 0.04, 0.0, zt, mat); }
}
if st == "glass" && !p.open {
for u in [a, b] { s.stem(u, y, 0.0, zt, 0.25, "wframe"); }
}
}
}
if top > zh { s.box(u0, -t, u1, t, zh, top, w.p.mat, #{ top: cap }); }
}
fn door_cap(cap, mat) { if cap == "" { #{ top: mat } } else { #{ top: cap } } }
// ---------------------------------------------------------------- floors, roofs, stairs
fn meta_floor() {
#{ label: "Floor", category: "House", icon: "▦", shape: "rect", storey: true,
help: "Flooring of a room; drag a rectangle on the map. Walls use floors to tell rooms from outside. Above the ground it is a slab whose top is at the object's height.",
props: [
#{ key: "w", type: "float", def: 4.0, min: 0.5, max: 100.0, step: 0.1, label: "Width (m)" },
#{ key: "d", type: "float", def: 3.0, min: 0.5, max: 100.0, step: 0.1, label: "Depth (m)" },
#{ key: "mat", type: "material", def: "f-oak", choices: flooring_materials(), label: "Flooring" },
#{ key: "pattern", type: "bool", def: true, label: "Boards / tiles" },
] }
}
fn prefab_floor(o) {
if part_hidden(o) { return; }
let p = o.p;
let w = p.w / 2.0;
let d = p.d / 2.0;
let ground = o.z.abs() < 0.25;
// on the ground: a surface under everything standing on it; higher up: a slab
if ground { layer("floor"); }
let s = sprite();
let z = if ground { 0.005 } else { 0.0 };
if ground {
s.flat(rect(-w, -d, w, d), z, p.mat + " t");
} else {
s.box(-w, -d, w, d, -0.2, 0.0, "plinth", #{ top: p.mat });
}
if p.pattern { floor_pattern(s, p.mat, w, d, z + 0.002); }
}
/// Board or tile joints on a floor of half size w x d at height z.
fn floor_pattern(s, m, w, d, z) {
let boards = m == "f-oak" || m == "f-wood";
let step = if boards { 0.2 } else if m == "f-tile" { 0.6 } else if m == "f-stone" { 0.8 } else { 0.0 };
if step == 0.0 { return; }
let segs = [];
let y = -d + step;
while y < d - 0.01 {
segs.push([-w, y, w, y]);
y += step;
}
if !boards {
let x = -w + step;
while x < w - 0.01 {
segs.push([x, -d, x, d]);
x += step;
}
}
if segs.len() > 0 && segs.len() < 500 { s.lines(segs, z, "grout"); }
}
fn meta_roof() {
#{ label: "Roof", category: "House", icon: "🏠", shape: "rect", storey: true,
help: "A roof over the walls: w x d is the outer size of the walls, the eaves sit h above the object. Levels that cut the house open show only its outline.",
props: [
#{ key: "w", type: "float", def: 8.0, min: 1.0, max: 100.0, step: 0.1, label: "Width (m)" },
#{ key: "d", type: "float", def: 6.0, min: 1.0, max: 100.0, step: 0.1, label: "Depth (m)" },
#{ key: "h", type: "float", def: 2.8, min: 0.0, max: 30.0, step: 0.1, label: "Eaves height (m)" },
#{ key: "style", type: "choice", def: "hip", choices: ["hip", "gable", "flat"], label: "Style" },
#{ key: "ridge", type: "choice", def: "x", choices: ["x", "y"], label: "Gable ridge along" },
#{ key: "rise", type: "float", def: 1.8, min: 0.3, max: 10.0, step: 0.1, label: "Rise (m)" },
#{ key: "over", type: "float", def: 0.4, min: 0.0, max: 2.0, step: 0.05, label: "Overhang (m)" },
#{ key: "mat", type: "material", def: "roofr", choices: house_roof_materials(), label: "Roof colour" },
#{ key: "wall_mat", type: "material", def: "render", choices: house_wall_materials(), label: "Gable ends" },
#{ key: "pv", type: "bool", def: false, label: "Solar panels" },
] }
}
fn prefab_roof(o) {
let p = o.p;
let hv = house_view(o);
if hv == 2 { return; }
if hv == 1 {
// the house is cut open on this storey: the roof's outline on the
// floor, under everything (it shows around the walls)
let w = p.w / 2.0 + p.over;
let d = p.d / 2.0 + p.over;
layer("floor");
sprite().lines(rsegs([-w, -d, w, d]), 0.02, "mark");
return;
}
let w = p.w;
let d = p.d;
// hip ridges run along the longer side; gables along the chosen axis
if (p.style == "hip" && d > w) || (p.style == "gable" && p.ridge == "y") {
frame(o.x, o.y, o.z, o.rot + 90.0);
w = p.d;
d = p.w;
}
let s = sprite();
if p.style == "flat" {
let W = w / 2.0 + p.over;
let D = d / 2.0 + p.over;
s.box(-W, -D, W, D, p.h, p.h + 0.25, p.mat, #{ side: "fascia" });
if p.pv && w > 3.0 && d > 3.0 {
let mods = [];
let y = -D + 0.8;
while y + 1.0 < D - 0.6 {
mods.push([[-W + 0.6, y, p.h + 0.35], [W - 0.6, y, p.h + 0.35], [W - 0.6, y + 1.0, p.h + 0.8], [-W + 0.6, y + 1.0, p.h + 0.8]]);
y += 2.0;
}
if mods.len() > 0 { s.poly(mods, "pv"); }
}
} else if p.style == "gable" {
house_gable(s, p, w, d);
} else {
house_hip(s, p, w, d);
}
}
/// Gable roof, ridge along local x over walls w x d; it starts at the eaves
/// height so it never reaches down beside the walls it sits on.
fn house_gable(s, p, w, d) {
let a = w / 2.0;
let h = d / 2.0;
let W = a + p.over;
let D = h + p.over;
let ze = p.h;
let k = p.rise / D;
let zr = ze + p.rise;
let zw = ze + p.over * k;
// gable ends up to the roof's underside, then the two slabs back to front
s.face([[a, -h, ze], [a, h, ze], [a, h, zw], [a, 0.0, zr], [a, -h, zw]], p.wall_mat);
s.face([[-a, h, ze], [-a, -h, ze], [-a, -h, zw], [-a, 0.0, zr], [-a, h, zw]], p.wall_mat);
let m = p.mat;
s.seq([
[0.0, -D / 2.0, |q| q.prism(rect(-W, -D, W, 0.0), [ze, ze, zr, zr], [ze + 0.15, ze + 0.15, zr + 0.15, zr + 0.15], m, #{ side: "fascia" })],
[0.0, D / 2.0, |q| q.prism(rect(-W, 0.0, W, D), [zr, zr, ze, ze], [zr + 0.15, zr + 0.15, ze + 0.15, ze + 0.15], m, #{ side: "fascia" })],
]);
if p.pv { roof_pv(s, W - 0.4, -1.0, D, k, ze + 0.15); }
}
/// Hip roof over walls w x d (w >= d), ridge along local x: a fascia band
/// at the eaves and four slopes, back to front.
fn house_hip(s, p, w, d) {
let W = w / 2.0 + p.over;
let D = d / 2.0 + p.over;
let ze = p.h;
let zb = ze + 0.15;
let zr = zb + p.rise;
let r = fmax(W - D, 0.0);
let f = 0.05;
let fm = "fascia";
s.box(-W, -D, W, -D + f, ze, zb, fm);
s.box(-W, D - f, W, D, ze, zb, fm);
s.box(-W, -D + f, -W + f, D - f, ze, zb, fm);
s.box(W - f, -D + f, W, D - f, ze, zb, fm);
let m = p.mat;
s.seq([
[0.0, -D / 2.0, |q| q.face([[-W, -D, zb], [W, -D, zb], [r, 0.0, zr], [-r, 0.0, zr]], m)],
[0.0, D / 2.0, |q| q.face([[W, D, zb], [-W, D, zb], [-r, 0.0, zr], [r, 0.0, zr]], m)],
[W - D / 2.0, 0.0, |q| q.face([[W, -D, zb], [W, D, zb], [r, 0.0, zr]], m)],
[-W + D / 2.0, 0.0, |q| q.face([[-W, D, zb], [-W, -D, zb], [-r, 0.0, zr]], m)],
]);
if p.pv { roof_pv(s, 0.0, r, D, p.rise / D, zb); }
}
/// Solar modules on the -y slope of a roof (eaves at y = -D and height ze,
/// slope k): rows L long each side of x = 0, or on a hip roof (hip = half
/// the ridge length) as long as the slope is wide at the row's top.
fn roof_pv(s, L, hip, D, k, ze) {
let cw = 1.13;
let rh = 1.7;
let rows = ((D - 0.6) / rh).floor().to_int();
let mods = [];
for i in 0..rows {
let ya0 = -D + 0.4 + i * rh + 0.03;
let half = if hip >= 0.0 { hip - (ya0 + rh) - 0.35 } else { L };
let cols = ((2.0 * half - 0.2) / cw).floor().to_int();
for j in 0..cols {
let ya = ya0;
let yb = ya + rh - 0.06;
let xa = -cols * cw / 2.0 + j * cw + 0.03;
let xb = xa + cw - 0.06;
let za = ze + (ya + D) * k + 0.08;
let zb = ze + (yb + D) * k + 0.08;
mods.push([[xa, ya, za], [xb, ya, za], [xb, yb, zb], [xa, yb, zb]]);
}
}
if mods.len() > 0 { s.poly(mods, "pv"); }
}
fn meta_stairs() {
#{ label: "Stairs", category: "House", icon: "≣", storey: true,
help: "A straight flight rising towards local +y, one storey high by default.",
props: [
#{ key: "w", type: "float", def: 1.0, min: 0.6, max: 4.0, step: 0.05, label: "Width (m)" },
#{ key: "run", type: "float", def: 3.6, min: 0.6, max: 10.0, step: 0.1, label: "Length (m)" },
#{ key: "rise", type: "float", def: 3.0, min: 0.2, max: 8.0, step: 0.05, label: "Rise (m)" },
#{ key: "mat", type: "material", def: "wood", choices: ["wood", "woodd", "concb", "steel", "furnw", "f-stone"], label: "Material" },
] }
}
fn prefab_stairs(o) {
if part_hidden(o) { return; }
let p = o.p;
let w = p.w / 2.0;
let n = fmax(2.0, (p.rise / 0.18).round()).to_int();
let g = p.run / n;
let r = p.rise / n;
let m = p.mat;
let items = [];
for i in 0..n {
let y0 = -p.run / 2.0 + i * g;
let zt = (i + 1) * r;
items.push([0.0, y0 + g / 2.0, |q| q.box(-w, y0, w, y0 + g, 0.0, zt, m)]);
}
sprite().seq(items);
}