Deploy web Studio / deploy (push) Successful in 1m4s
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.
528 lines
22 KiB
Plaintext
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);
|
|
}
|