// 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 } } fn wall_cut_h() { 1.0 } fn wall_types() { ["house_wall"] } fn opening_types() { ["window", "door"] } fn room_types() { ["floor"] } fn house_wall_materials() { ["render", "render2", "brick", "wood", "wall", "wall2", "wallp", "concb", "office", "furnw"] } 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 house_view(o) == 2 { 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 x = -p.w / 2.0 - wall_ext(o, -1.0); for g in gaps { if g[0] > x + 0.01 { wall_run(x, g[0], t, top, p.mat, cap); } x = fmax(x, g[1]); } let end = p.w / 2.0 + wall_ext(o, 1.0); if end > x + 0.01 { wall_run(x, end, t, top, p.mat, cap); } } /// 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, one sprite per 30 m at most. fn wall_run(x0, x1, t, top, mat, cap) { let n = ((x1 - x0) / 30.0).ceiling().to_int(); let L = (x1 - x0) / n; for i in 0..n { sprite().box(x0 + i * L, -t, x0 + (i + 1) * L, 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 house_view(w) == 2 { 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 house_view(w) == 2 { 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 house_view(o) == 2 { 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 house_view(o) == 2 { 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); }