Deploy web Studio / deploy (push) Successful in 3m52s
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.
498 lines
21 KiB
Plaintext
498 lines
21 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 }
|
|
}
|
|
|
|
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);
|
|
}
|