Build your own house: walls, windows, doors, floors, roof, stairs, furniture
Deploy web Studio / deploy (push) Successful in 3m27s
Deploy web Studio / deploy (push) Successful in 3m27s
House parts (stdlib isokit/house): house_wall drawn along a line, with ends that adapt at corners, T-junctions and straight runs so no two walls overlap; window and door that sit on a wall and cut its opening (door styles panel, glass, double, sliding, opening; swing and hinge by rotation); floor (boards, tiles, carpet, stone); roof (hip, gable, flat, optional solar); stairs. Furniture (stdlib isokit/furniture): chair, sofa / armchair, table with chairs, desk with monitor and chair, TV, bed, cupboard, bookshelf, kitchen run, bathtub, shower, toilet, washbasin, plant, rug, floor lamp. Cut-open levels: on gf / 1f the walls in front of a room are cut low (rooms are found by their floors), the roof becomes an outline on the floor, and storeys above are left out. A part's storey comes from z (storey_h, default 3 m). Scripting: objects_of, objects_at, objects_on, mount_of, mounted, frame, to_world, to_local, local_of. Prefab meta: shape (line / rect), mount, storey. Studio: drag to draw lines and rectangles (walls, roads, fences, hedges, floors, areas, buildings, roofs). The Room tool draws a floor plus walls, without doubling walls that already stand on an edge. Windows and doors snap onto walls with a hover preview, and they move, turn, copy and delete with their wall in one undo step. New parts land on the storey the view shows. Palette categories collapse, and the palette scrolls apart from the object list. The MCP instructions explain house building, and list_prefabs shows sits_on. examples/house is a furnished bungalow; the web Studio gets a House demo button. Tests: wall corners and junctions, zero depth-sort cycles in every view, cut levels, and the house demo in every view; check_studio covers the room / window / door / storey flow, check_web the House demo.
This commit is contained in:
1 parent
84309a7a08
commit
95ea4a8512
26 files changed
+2137
-64
No files matched your search
@@ -3,7 +3,7 @@
|
||||
use crate::conv::*;
|
||||
use crate::State;
|
||||
use isokit_core::fmt::{hash, round1, thousands};
|
||||
use isokit_core::{classes, LayerOpts, Sprite, C};
|
||||
use isokit_core::{classes, LayerOpts, Sprite, Xf, C};
|
||||
use rhai::{Array, Dynamic, Engine, FnPtr, Map, NativeCallContext};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
@@ -702,6 +702,7 @@ pub(crate) fn register_scene(e: &mut Engine, st: &Shared) {
|
||||
});
|
||||
let s = st.clone();
|
||||
e.register_fn("objects", move || s.borrow().all_objects.clone());
|
||||
register_objects(e, st);
|
||||
// is a face with outward normal (nx, ny) (object coordinates) turned towards the viewer?
|
||||
let s = st.clone();
|
||||
e.register_fn("vis", move |nx: D, ny: D| -> RR<bool> {
|
||||
@@ -783,3 +784,202 @@ pub(crate) fn register_scene(e: &mut Engine, st: &Shared) {
|
||||
Ok(m)
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- objects and frames
|
||||
|
||||
/// A type name or a list of them.
|
||||
fn kinds(d: &Dynamic) -> Vec<String> {
|
||||
match d.read_lock::<Array>() {
|
||||
Some(a) => a.iter().map(|v| v.to_string()).collect(),
|
||||
None => vec![d.to_string()],
|
||||
}
|
||||
}
|
||||
|
||||
fn fnum(m: &Map, k: &str) -> f64 {
|
||||
m.get(k).and_then(|v| n(v).ok()).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
fn sget(m: &Map, k: &str) -> String {
|
||||
m.get(k).map(|v| v.to_string()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// A number prop of a document object (`o.p.<k>`).
|
||||
fn prop(m: &Map, k: &str) -> Option<f64> {
|
||||
m.get("p").and_then(|p| {
|
||||
p.read_lock::<Map>()
|
||||
.and_then(|p| p.get(k).and_then(|v| n(v).ok()))
|
||||
})
|
||||
}
|
||||
|
||||
/// The placement frame of a document object.
|
||||
fn xf_of(m: &Map) -> Xf {
|
||||
Xf::new(fnum(m, "x"), fnum(m, "y"), fnum(m, "z"), fnum(m, "rot"))
|
||||
}
|
||||
|
||||
fn of_type(d: &Dynamic, ks: &[String]) -> bool {
|
||||
d.read_lock::<Map>()
|
||||
.is_some_and(|m| ks.iter().any(|k| *k == sget(&m, "type")))
|
||||
}
|
||||
|
||||
/// The object with (u, v), its position in another object's frame.
|
||||
fn with_uv(d: &Dynamic, u: f64, v: f64) -> Dynamic {
|
||||
let mut m = d.read_lock::<Map>().map(|m| m.clone()).unwrap_or_default();
|
||||
m.insert("u".into(), Dynamic::from_float(u));
|
||||
m.insert("v".into(), Dynamic::from_float(v));
|
||||
Dynamic::from_map(m)
|
||||
}
|
||||
|
||||
/// The host `o` sits on: of the objects of the `hosts` types at the same
|
||||
/// height, the one whose centre line (local x, length `p.w`) passes within
|
||||
/// `p.t / 2 + tol` of o's position; the nearest wins, then the first.
|
||||
/// Returns its index and o's position (u, v) in the host's frame.
|
||||
fn host_index(objs: &[Dynamic], o: &Map, hosts: &[String], tol: f64) -> Option<(usize, f64, f64)> {
|
||||
let (x, y, z) = (fnum(o, "x"), fnum(o, "y"), fnum(o, "z"));
|
||||
let id = sget(o, "id");
|
||||
let mut best: Option<(usize, f64, f64)> = None;
|
||||
for (i, d) in objs.iter().enumerate() {
|
||||
let Some(h) = d.read_lock::<Map>() else {
|
||||
continue;
|
||||
};
|
||||
if !hosts.iter().any(|k| *k == sget(&h, "type")) || sget(&h, "id") == id {
|
||||
continue;
|
||||
}
|
||||
let xf = xf_of(&h);
|
||||
if (z - xf.z).abs() > 0.5 {
|
||||
continue;
|
||||
}
|
||||
let l = xf.inv(x, y);
|
||||
let half = prop(&h, "w").unwrap_or(1.0) / 2.0;
|
||||
let lim = prop(&h, "t").unwrap_or(0.0) / 2.0 + tol;
|
||||
if l[0].abs() <= half + 1e-6
|
||||
&& l[1].abs() <= lim + 1e-6
|
||||
&& best.is_none_or(|b| l[1].abs() < b.2.abs() - 1e-9)
|
||||
{
|
||||
best = Some((i, l[0], l[1]));
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// Queries over the document objects shown on this level, and frames.
|
||||
fn register_objects(e: &mut Engine, st: &Shared) {
|
||||
// objects of a type (or a list of types) shown on this level
|
||||
let s = st.clone();
|
||||
e.register_fn("objects_of", move |k: D| -> Array {
|
||||
let ks = kinds(&k);
|
||||
s.borrow()
|
||||
.objects
|
||||
.iter()
|
||||
.filter(|d| of_type(d, &ks))
|
||||
.cloned()
|
||||
.collect()
|
||||
});
|
||||
// ... whose footprint (p.w x p.d around the origin) holds the world point
|
||||
let s = st.clone();
|
||||
e.register_fn("objects_at", move |k: D, x: D, y: D| -> RR<Array> {
|
||||
let (ks, x, y) = (kinds(&k), n(&x)?, n(&y)?);
|
||||
Ok(s.borrow()
|
||||
.objects
|
||||
.iter()
|
||||
.filter(|d| {
|
||||
of_type(d, &ks)
|
||||
&& d.read_lock::<Map>().is_some_and(|m| {
|
||||
let (Some(w), Some(dd)) = (prop(&m, "w"), prop(&m, "d")) else {
|
||||
return false;
|
||||
};
|
||||
let l = xf_of(&m).inv(x, y);
|
||||
l[0].abs() <= w / 2.0 + 1e-6 && l[1].abs() <= dd / 2.0 + 1e-6
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
.collect())
|
||||
});
|
||||
// ... whose centre line (local x, length p.w) passes within p.t / 2 + tol
|
||||
// of the world point, each with (u, v): the point in its frame
|
||||
let s = st.clone();
|
||||
e.register_fn("objects_on", move |k: D, x: D, y: D, tol: D| -> RR<Array> {
|
||||
let (ks, x, y, tol) = (kinds(&k), n(&x)?, n(&y)?, n(&tol)?);
|
||||
Ok(s.borrow()
|
||||
.objects
|
||||
.iter()
|
||||
.filter(|d| of_type(d, &ks))
|
||||
.filter_map(|d| {
|
||||
let m = d.read_lock::<Map>()?;
|
||||
let l = xf_of(&m).inv(x, y);
|
||||
let half = prop(&m, "w").unwrap_or(1.0) / 2.0;
|
||||
let lim = prop(&m, "t").unwrap_or(0.0) / 2.0 + tol;
|
||||
(l[0].abs() <= half + 1e-6 && l[1].abs() <= lim + 1e-6)
|
||||
.then(|| with_uv(d, l[0], l[1]))
|
||||
})
|
||||
.collect())
|
||||
});
|
||||
// the host an object sits on (with u, v: the object's position in the
|
||||
// host's frame), or ()
|
||||
fn mount_of(s: &Shared, o: &Map, hosts: &D, tol: f64) -> Dynamic {
|
||||
let b = s.borrow();
|
||||
match host_index(&b.objects, o, &kinds(hosts), tol) {
|
||||
Some((i, u, v)) => with_uv(&b.objects[i], u, v),
|
||||
None => Dynamic::UNIT,
|
||||
}
|
||||
}
|
||||
let s = st.clone();
|
||||
e.register_fn("mount_of", move |o: Map, hosts: D| {
|
||||
mount_of(&s, &o, &hosts, 0.3)
|
||||
});
|
||||
let s = st.clone();
|
||||
e.register_fn("mount_of", move |o: Map, hosts: D, tol: D| -> RR<Dynamic> {
|
||||
Ok(mount_of(&s, &o, &hosts, n(&tol)?))
|
||||
});
|
||||
// the objects of the given types that sit on host h (each with u, v)
|
||||
fn mounted(s: &Shared, h: &Map, k: &D, hosts: &D, tol: f64) -> Array {
|
||||
let b = s.borrow();
|
||||
let (ks, hs, hid) = (kinds(k), kinds(hosts), sget(h, "id"));
|
||||
b.objects
|
||||
.iter()
|
||||
.filter(|d| of_type(d, &ks))
|
||||
.filter_map(|d| {
|
||||
let m = d.read_lock::<Map>()?;
|
||||
let (i, u, v) = host_index(&b.objects, &m, &hs, tol)?;
|
||||
let on = b.objects[i]
|
||||
.read_lock::<Map>()
|
||||
.is_some_and(|x| sget(&x, "id") == hid);
|
||||
on.then(|| with_uv(d, u, v))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
let s = st.clone();
|
||||
e.register_fn("mounted", move |h: Map, k: D, hosts: D| {
|
||||
mounted(&s, &h, &k, &hosts, 0.3)
|
||||
});
|
||||
let s = st.clone();
|
||||
e.register_fn(
|
||||
"mounted",
|
||||
move |h: Map, k: D, hosts: D, tol: D| -> RR<Array> {
|
||||
Ok(mounted(&s, &h, &k, &hosts, n(&tol)?))
|
||||
},
|
||||
);
|
||||
// draw in another frame from here on (sprites made later, vis, tags)
|
||||
let s = st.clone();
|
||||
e.register_fn("frame", move |o: Map| {
|
||||
s.borrow_mut().xf = xf_of(&o);
|
||||
});
|
||||
let s = st.clone();
|
||||
e.register_fn("frame", move |x: D, y: D, z: D, rot: D| -> RR<()> {
|
||||
s.borrow_mut().xf = Xf::new(n(&x)?, n(&y)?, n(&z)?, n(&rot)?);
|
||||
Ok(())
|
||||
});
|
||||
let s = st.clone();
|
||||
e.register_fn("to_world", move |x: D, y: D| -> RR<Array> {
|
||||
let p = s.borrow().xf.p(n(&x)?, n(&y)?);
|
||||
Ok(vec![Dynamic::from_float(p[0]), Dynamic::from_float(p[1])])
|
||||
});
|
||||
let s = st.clone();
|
||||
e.register_fn("to_local", move |x: D, y: D| -> RR<Array> {
|
||||
let p = s.borrow().xf.inv(n(&x)?, n(&y)?);
|
||||
Ok(vec![Dynamic::from_float(p[0]), Dynamic::from_float(p[1])])
|
||||
});
|
||||
e.register_fn("local_of", |o: Map, x: D, y: D| -> RR<Array> {
|
||||
let p = xf_of(&o).inv(n(&x)?, n(&y)?);
|
||||
Ok(vec![Dynamic::from_float(p[0]), Dynamic::from_float(p[1])])
|
||||
});
|
||||
}
|
||||
@@ -105,9 +105,20 @@ pub struct PrefabMeta {
|
||||
pub icon: String,
|
||||
#[serde(default)]
|
||||
pub help: String,
|
||||
/// How the Studio places it: "point" (click), "path" (click points).
|
||||
/// How the Studio places it: "point" (click), "line" (drag along: sets
|
||||
/// `rot` and the length `w`), "rect" (drag a rectangle: sets `w` and
|
||||
/// `d`), "path" (click points).
|
||||
#[serde(default = "default_shape")]
|
||||
pub shape: String,
|
||||
/// Types this prefab sits on (windows on walls): the Studio snaps it
|
||||
/// onto the nearest one and moves, turns, copies and deletes it with
|
||||
/// that host. The prefab finds its host with `mount_of`.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub mount: Vec<String>,
|
||||
/// Placed on the storey the view shows: on level "1f" the Studio puts
|
||||
/// it at z = 1 x storey height (parameter `storey_h`, default 3 m).
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub storey: bool,
|
||||
#[serde(default)]
|
||||
pub props: Vec<PropDef>,
|
||||
/// Module the prefab comes from ("isokit/site", "lib/kiosk.rhai").
|
||||
|
||||
@@ -13,4 +13,6 @@ pub const MODULES: &[(&str, &str)] = &[
|
||||
("isokit/vehicles", include_str!("../stdlib/vehicles.rhai")),
|
||||
("isokit/people", include_str!("../stdlib/people.rhai")),
|
||||
("isokit/equipment", include_str!("../stdlib/equipment.rhai")),
|
||||
("isokit/house", include_str!("../stdlib/house.rhai")),
|
||||
("isokit/furniture", include_str!("../stdlib/furniture.rhai")),
|
||||
];
|
||||
@@ -290,3 +290,204 @@ fn cfg_reaches_the_script() {
|
||||
let b = r.build("site", 0, &c, &Doc::default()).unwrap();
|
||||
assert_eq!(b.scene.layers[2].sprites[0].world_box().unwrap()[5], 9.0);
|
||||
}
|
||||
|
||||
/// A 4 x 3 m room: floor, four walls on its edges, a window and a door.
|
||||
fn room_doc() -> Doc {
|
||||
let mut doc = Doc::default();
|
||||
doc.objects
|
||||
.push(obj("floor", "floor", 0.0, 0.0, json!({"w": 4, "d": 3})));
|
||||
for (id, x, y, rot, w) in [
|
||||
("south", 0.0, -1.5, 0.0, 4.0),
|
||||
("north", 0.0, 1.5, 0.0, 4.0),
|
||||
("west", -2.0, 0.0, 90.0, 3.0),
|
||||
("east", 2.0, 0.0, 90.0, 3.0),
|
||||
] {
|
||||
let mut o = obj(id, "house_wall", x, y, json!({ "w": w }));
|
||||
o.rot = rot;
|
||||
doc.objects.push(o);
|
||||
}
|
||||
// a little off the wall line: still on the south wall
|
||||
doc.objects
|
||||
.push(obj("win", "window", 0.5, -1.4, json!({"w": 1.2})));
|
||||
let mut door = obj("door", "door", 2.05, 0.4, json!({"open": true}));
|
||||
door.rot = 90.0;
|
||||
doc.objects.push(door);
|
||||
doc
|
||||
}
|
||||
|
||||
fn sprites_of(b: &crate::Built, id: &str) -> usize {
|
||||
b.scene
|
||||
.layers
|
||||
.iter()
|
||||
.flat_map(|l| l.sprites.iter())
|
||||
.filter(|s| s.obj.as_deref() == Some(id))
|
||||
.count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_and_doors_sit_on_walls() {
|
||||
let r = Runtime::new(&Bundle::default()).unwrap();
|
||||
let doc = room_doc();
|
||||
let b = r.build("site", 0, &cfg(), &doc).unwrap();
|
||||
assert!(b.scene.warnings.is_empty(), "{:?}", b.scene.warnings);
|
||||
// the window snaps onto the south wall's line and the wall leaves a gap
|
||||
let w = b.boxes["win"];
|
||||
assert!(
|
||||
(w[1] + 1.575).abs() < 1e-6 && (w[4] + 1.425).abs() < 1e-6,
|
||||
"{w:?}"
|
||||
);
|
||||
assert!(
|
||||
(w[0] + 0.1).abs() < 1e-6 && (w[3] - 1.1).abs() < 1e-6,
|
||||
"{w:?}"
|
||||
);
|
||||
assert_eq!(sprites_of(&b, "south"), 2, "wall split around the window");
|
||||
assert_eq!(sprites_of(&b, "north"), 1);
|
||||
assert_eq!(sprites_of(&b, "east"), 2, "wall split around the door");
|
||||
// the open door swings into the room (rot 90 on a rot 90 wall: towards -x)
|
||||
let d = b.boxes["door"];
|
||||
assert!(d[0] < 1.5 && d[3] <= 2.08, "{d:?}");
|
||||
let html = render(&b.scene, 0, &RenderOpts::default()).html;
|
||||
assert!(html.contains("iso-glassp"));
|
||||
// corners: the walls along x run through, the others stop at their faces
|
||||
let (sw, ww) = (b.boxes["south"], b.boxes["west"]);
|
||||
assert!(
|
||||
(sw[0] + 2.075).abs() < 1e-6 && (sw[3] - 2.075).abs() < 1e-6,
|
||||
"{sw:?}"
|
||||
);
|
||||
assert!(
|
||||
(ww[1] + 1.425).abs() < 1e-6 && (ww[4] - 1.425).abs() < 1e-6,
|
||||
"{ww:?}"
|
||||
);
|
||||
for rot in 0..4u8 {
|
||||
for lv in ["site", "gf"] {
|
||||
let b = r.build(lv, rot, &cfg(), &doc).unwrap();
|
||||
let out = render(&b.scene, rot, &RenderOpts::default());
|
||||
assert_eq!(out.cycles, 0, "{lv}/{rot}: depth-sort cycles");
|
||||
}
|
||||
}
|
||||
// moved off every wall, the window stands alone as a wall piece
|
||||
let mut alone = doc.clone();
|
||||
alone.get_mut("win").unwrap().y = -5.0;
|
||||
let b = r.build("site", 0, &cfg(), &alone).unwrap();
|
||||
assert_eq!(sprites_of(&b, "south"), 1);
|
||||
assert!((b.boxes["win"][5] - 2.8).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walls_meeting_at_a_t_or_in_line_do_not_overlap() {
|
||||
let r = Runtime::new(&Bundle::default()).unwrap();
|
||||
let mut doc = room_doc();
|
||||
let mut inner = obj("inner", "house_wall", 0.0, 0.0, json!({"w": 3.0, "t": 0.1}));
|
||||
inner.rot = 90.0;
|
||||
doc.objects.push(inner);
|
||||
// the north wall in two pieces meeting at x = 1
|
||||
doc.get_mut("north").unwrap().props = json!({"w": 3.0}).as_object().cloned().unwrap();
|
||||
doc.get_mut("north").unwrap().x = -0.5;
|
||||
doc.objects
|
||||
.push(obj("north2", "house_wall", 1.5, 1.5, json!({"w": 1.0})));
|
||||
let b = r.build("site", 1, &cfg(), &doc).unwrap();
|
||||
let i = b.boxes["inner"];
|
||||
assert!(
|
||||
(i[1] + 1.425).abs() < 1e-6 && (i[4] - 1.425).abs() < 1e-6,
|
||||
"{i:?}"
|
||||
);
|
||||
let (n1, n2) = (b.boxes["north"], b.boxes["north2"]);
|
||||
assert!(
|
||||
(n1[3] - 1.0).abs() < 1e-6 && (n2[0] - 1.0).abs() < 1e-6,
|
||||
"{n1:?} {n2:?}"
|
||||
);
|
||||
assert!((n1[0] + 2.075).abs() < 1e-6 && (n2[3] - 2.075).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cut_levels_lower_the_walls_in_front_of_rooms() {
|
||||
let r = Runtime::new(&Bundle::default()).unwrap();
|
||||
let mut doc = room_doc();
|
||||
doc.objects
|
||||
.push(obj("roof", "roof", 0.0, 0.0, json!({"w": 4.15, "d": 3.15})));
|
||||
let mut up = obj("chair", "chair", 0.0, 0.0, json!({}));
|
||||
up.z = 3.0;
|
||||
doc.objects.push(up);
|
||||
for rot in 0..4u8 {
|
||||
let site = r.build("site", rot, &cfg(), &doc).unwrap();
|
||||
let gf = r.build("gf", rot, &cfg(), &doc).unwrap();
|
||||
let tops = |b: &crate::Built| -> Vec<f64> {
|
||||
["south", "north", "west", "east"]
|
||||
.iter()
|
||||
.map(|id| b.boxes[*id][5])
|
||||
.collect()
|
||||
};
|
||||
assert!(tops(&site).iter().all(|z| (z - 2.8).abs() < 1e-6));
|
||||
// two walls face the viewer: cut low; the two behind the room stay full
|
||||
let t = tops(&gf);
|
||||
let low = t.iter().filter(|z| (*z - 1.0).abs() < 1e-6).count();
|
||||
let full = t.iter().filter(|z| (*z - 2.8).abs() < 1e-6).count();
|
||||
assert_eq!((low, full), (2, 2), "rotation {rot}: {t:?}");
|
||||
// the roof is a full roof on the site level, an outline when cut open
|
||||
assert!(site.boxes["roof"][5] > 4.0);
|
||||
assert!(
|
||||
(gf.boxes["roof"][5] - 0.02).abs() < 1e-6,
|
||||
"outline on the floor"
|
||||
);
|
||||
// the chair on the first floor: left out on GF, there on 1F
|
||||
assert!(!gf.boxes.contains_key("chair"));
|
||||
let f1 = r.build("1f", rot, &cfg(), &doc).unwrap();
|
||||
assert!(f1.boxes.contains_key("chair"));
|
||||
assert!(f1.boxes["south"][5] > 2.7, "GF closed below the cut storey");
|
||||
assert!(gf.scene.warnings.is_empty(), "{:?}", gf.scene.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_queries_and_frames() {
|
||||
let r = rt(r#"
|
||||
let s = objects_of("floor");
|
||||
let a = objects_at(["floor"], 1.9, 1.4);
|
||||
let b = objects_at(["floor"], 2.1, 0.0);
|
||||
let w = objects_of("house_wall")[3];
|
||||
frame(w);
|
||||
let p = to_world(1.0, 0.5);
|
||||
let q = to_local(p[0], p[1]);
|
||||
let m = mount_of(objects_of("door")[0], ["house_wall"]);
|
||||
let on = mounted(w, ["window", "door"], ["house_wall"]);
|
||||
frame(0, 0, 0, 0);
|
||||
sprite("probe").box(0, 0, s.len(), a.len() + b.len() * 10, 0, 1, "wall");
|
||||
sprite("frame").box(p[0], p[1], p[0] + q[0], p[1] + q[1], 0, 1, "wall");
|
||||
sprite("mount").box(0, 0, m.u + 10.0, on.len(), 0, m.v + 10.0, "wall");
|
||||
"#);
|
||||
let b = r.build("site", 0, &cfg(), &room_doc()).unwrap();
|
||||
let probe = b.scene.layers[2]
|
||||
.sprites
|
||||
.iter()
|
||||
.find(|s| s.id == "probe")
|
||||
.unwrap()
|
||||
.world_box()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
(probe[3], probe[4]),
|
||||
(1.0, 1.0),
|
||||
"one floor, holding one point"
|
||||
);
|
||||
let f = b.scene.layers[2]
|
||||
.sprites
|
||||
.iter()
|
||||
.find(|s| s.id == "frame")
|
||||
.unwrap()
|
||||
.world_box()
|
||||
.unwrap();
|
||||
// east wall at (2, 0) turned 90: local (1, 0.5) is world (1.5, 1)
|
||||
assert_eq!([f[0], f[1], f[3], f[4]], [1.5, 1.0, 2.5, 1.5]);
|
||||
let m = b.scene.layers[2]
|
||||
.sprites
|
||||
.iter()
|
||||
.find(|s| s.id == "mount")
|
||||
.unwrap()
|
||||
.world_box()
|
||||
.unwrap();
|
||||
// the door at (2.05, 0.4) is 0.4 along the east wall, 0.05 off it
|
||||
assert!(
|
||||
(m[3] - 10.4).abs() < 1e-9 && (m[5] - 9.95).abs() < 1e-9,
|
||||
"{m:?}"
|
||||
);
|
||||
assert_eq!(m[4], 1.0, "one opening on the east wall");
|
||||
}
|
||||
@@ -11,7 +11,7 @@ fn roof_materials() { ["roof", "roof2", "roof3", "rooft", "roofcap", "canopy"] }
|
||||
fn floor_materials() { ["z-floor", "z-living", "z-offices", "z-kitchen", "z-shop", "z-lab", "z-wh", "z-plant", "z-garage", "z-hall", "z-sport"] }
|
||||
|
||||
fn meta_building() {
|
||||
#{ label: "Building", category: "Buildings", icon: "🏢",
|
||||
#{ label: "Building", category: "Buildings", icon: "🏢", shape: "rect",
|
||||
help: "Storeys with windows, a door on the front (local -y) face, a flat or gable roof and optional solar. The GF / 1F levels cut it open.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 20.0, min: 2.0, max: 300.0, step: 0.5, label: "Width (m)" },
|
||||
@@ -165,7 +165,7 @@ fn flat_roof_kit(s, p, zt) {
|
||||
}
|
||||
|
||||
fn meta_carport() {
|
||||
#{ label: "Carport", category: "Buildings", icon: "⌂",
|
||||
#{ label: "Carport", category: "Buildings", icon: "⌂", shape: "rect",
|
||||
help: "A canopy on posts with cars parked under it.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 6.0, min: 2.5, max: 60.0, step: 0.5, label: "Width (m)" },
|
||||
@@ -203,7 +203,7 @@ fn prefab_carport(o) {
|
||||
}
|
||||
|
||||
fn meta_shed() {
|
||||
#{ label: "Shed / kiosk", category: "Buildings", icon: "▣",
|
||||
#{ label: "Shed / kiosk", category: "Buildings", icon: "▣", shape: "rect",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 4.0, min: 1.0, max: 30.0, step: 0.5, label: "Width (m)" },
|
||||
#{ key: "d", type: "float", def: 3.0, min: 1.0, max: 30.0, step: 0.5, label: "Depth (m)" },
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
// isokit/furniture: interior pieces for houses and offices. Every piece
|
||||
// faces local -y (its front; the back goes against a wall at +y) and stands
|
||||
// on its object's z, so it can sit on any storey (see isokit/house). Pieces
|
||||
// on storeys above the one a level cuts open are left out.
|
||||
|
||||
fn furniture_hidden(o) { house_view(o) == 2 }
|
||||
fn wood_choices() { ["wood", "woodd", "furnw", "steel", "kitchenc"] }
|
||||
fn fabric_choices() { ["uph1", "uph2", "uph3", "uph4", "sofa", "bed", "cloth"] }
|
||||
|
||||
/// A chair at (x, y) with its back towards (bx, by) (one of the axes).
|
||||
fn chair_at(x, y, bx, by, mat) {
|
||||
let h = 0.22;
|
||||
for c in [[-1, -1], [1, -1], [1, 1], [-1, 1]] {
|
||||
let lx = x + c[0] * 0.19;
|
||||
let ly = y + c[1] * 0.19;
|
||||
this.box(lx - 0.02, ly - 0.02, lx + 0.02, ly + 0.02, 0.02, 0.42, mat);
|
||||
}
|
||||
this.box(x - h, y - h, x + h, y + h, 0.42, 0.47, mat);
|
||||
if bx != 0 {
|
||||
let a = x + bx * 0.18;
|
||||
let b = x + bx * 0.22;
|
||||
this.box(fmin(a, b), y - h, fmax(a, b), y + h, 0.47, 0.92, mat);
|
||||
} else {
|
||||
let a = y + by * 0.18;
|
||||
let b = y + by * 0.22;
|
||||
this.box(x - h, fmin(a, b), x + h, fmax(a, b), 0.47, 0.92, mat);
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
/// An office chair at (x, y) with its back towards -y.
|
||||
fn office_chair(x, y, mat) {
|
||||
this.disc(x, y, 0.03, 0.27, "keyb");
|
||||
this.stem(x, y, 0.03, 0.45, 0.25, "metal");
|
||||
this.box(x - 0.24, y - 0.22, x + 0.24, y + 0.24, 0.45, 0.52, mat);
|
||||
this.box(x - 0.22, y - 0.26, x + 0.22, y - 0.21, 0.52, 1.0, mat);
|
||||
this
|
||||
}
|
||||
|
||||
fn meta_chair() {
|
||||
#{ label: "Chair", category: "Furniture", icon: "🪑", storey: true,
|
||||
help: "A dining chair; its back is on the +y side.",
|
||||
props: [ #{ key: "mat", type: "material", def: "wood", choices: wood_choices(), label: "Colour" } ] }
|
||||
}
|
||||
fn prefab_chair(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
sprite().chair_at(0.0, 0.0, 0, 1, o.p.mat);
|
||||
}
|
||||
|
||||
fn meta_sofa() {
|
||||
#{ label: "Sofa", category: "Furniture", icon: "🛋", storey: true,
|
||||
help: "Seats face -y; one seat makes an armchair.",
|
||||
props: [
|
||||
#{ key: "seats", type: "int", def: 3, min: 1, max: 5, step: 1, label: "Seats" },
|
||||
#{ key: "mat", type: "material", def: "uph1", choices: fabric_choices(), label: "Fabric" },
|
||||
] }
|
||||
}
|
||||
fn prefab_sofa(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let m = o.p.mat;
|
||||
let n = o.p.seats;
|
||||
let W = n * 0.7 / 2.0 + 0.18;
|
||||
let s = sprite();
|
||||
s.box(-W, -0.45, W, 0.45, 0.02, 0.4, m);
|
||||
s.seq([
|
||||
[0.0, 0.35, |q| q.box(-W, 0.25, W, 0.45, 0.4, 0.85, m)],
|
||||
[0.0, -0.1, |q| {
|
||||
for i in 0..n {
|
||||
let x0 = -W + 0.18 + i * 0.7;
|
||||
q.box(x0 + 0.01, -0.44, x0 + 0.69, 0.25, 0.4, 0.5, m);
|
||||
}
|
||||
}],
|
||||
[-W + 0.09, -0.1, |q| q.box(-W, -0.45, -W + 0.18, 0.25, 0.4, 0.64, m)],
|
||||
[W - 0.09, -0.1, |q| q.box(W - 0.18, -0.45, W, 0.25, 0.4, 0.64, m)],
|
||||
]);
|
||||
}
|
||||
|
||||
fn meta_table() {
|
||||
#{ label: "Table", category: "Furniture", icon: "🍽", storey: true,
|
||||
help: "A table with chairs pulled out along its long sides (and its ends from six chairs on).",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 1.6, min: 0.6, max: 6.0, step: 0.05, label: "Length (m)" },
|
||||
#{ key: "d", type: "float", def: 0.9, min: 0.5, max: 2.0, step: 0.05, label: "Width (m)" },
|
||||
#{ key: "h", type: "float", def: 0.75, min: 0.4, max: 1.1, step: 0.01, label: "Height (m)" },
|
||||
#{ key: "chairs", type: "int", def: 4, min: 0, max: 16, step: 1, label: "Chairs" },
|
||||
#{ key: "mat", type: "material", def: "wood", choices: wood_choices(), label: "Colour" },
|
||||
#{ key: "chair_mat", type: "material", def: "woodd", choices: wood_choices(), label: "Chairs" },
|
||||
] }
|
||||
}
|
||||
fn prefab_table(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let p = o.p;
|
||||
let w = p.w / 2.0;
|
||||
let d = p.d / 2.0;
|
||||
let s = sprite();
|
||||
for c in [[-1, -1], [1, -1], [1, 1], [-1, 1]] {
|
||||
let x = c[0] * (w - 0.07);
|
||||
let y = c[1] * (d - 0.07);
|
||||
s.box(x - 0.03, y - 0.03, x + 0.03, y + 0.03, 0.02, p.h - 0.04, p.mat);
|
||||
}
|
||||
s.box(-w, -d, w, d, p.h - 0.04, p.h, p.mat);
|
||||
// each chair its own sprite, clear of the table so the depth sort can order them
|
||||
let n = p.chairs;
|
||||
let ends = if n >= 6 { 2 } else { 0 };
|
||||
let front = (n - ends + 1) / 2;
|
||||
let back = n - ends - front;
|
||||
for i in 0..front { sprite().chair_at(-w + (i + 0.5) * p.w / front, -d - 0.24, 0, -1, p.chair_mat); }
|
||||
for i in 0..back { sprite().chair_at(-w + (i + 0.5) * p.w / back, d + 0.24, 0, 1, p.chair_mat); }
|
||||
if ends > 0 {
|
||||
sprite().chair_at(-w - 0.24, 0.0, -1, 0, p.chair_mat);
|
||||
sprite().chair_at(w + 0.24, 0.0, 1, 0, p.chair_mat);
|
||||
}
|
||||
}
|
||||
|
||||
fn meta_desk() {
|
||||
#{ label: "Desk", category: "Furniture", icon: "🖥", storey: true,
|
||||
help: "A desk with a monitor and an office chair on its -y side.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 1.4, min: 0.8, max: 4.0, step: 0.05, label: "Width (m)" },
|
||||
#{ key: "d", type: "float", def: 0.7, min: 0.5, max: 1.2, step: 0.05, label: "Depth (m)" },
|
||||
#{ key: "mat", type: "material", def: "furnw", choices: wood_choices(), label: "Colour" },
|
||||
#{ key: "monitor", type: "bool", def: true, label: "Monitor" },
|
||||
#{ key: "chair", type: "bool", def: true, label: "Chair" },
|
||||
#{ key: "chair_mat", type: "material", def: "uph1", choices: fabric_choices(), label: "Chair colour" },
|
||||
] }
|
||||
}
|
||||
fn prefab_desk(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let p = o.p;
|
||||
let w = p.w / 2.0;
|
||||
let d = p.d / 2.0;
|
||||
let h = 0.75;
|
||||
let s = sprite();
|
||||
s.box(-w, -d, -w + 0.04, d, 0.02, h - 0.03, p.mat);
|
||||
s.box(w - 0.04, -d, w, d, 0.02, h - 0.03, p.mat);
|
||||
s.box(-w + 0.04, d - 0.04, w - 0.04, d, 0.3, h - 0.03, p.mat);
|
||||
s.box(-w, -d, w, d, h - 0.03, h, p.mat);
|
||||
if p.monitor {
|
||||
s.flat(rect(-0.22, -d + 0.1, 0.22, -d + 0.25), h + 0.004, "keyb");
|
||||
s.box(-0.04, d - 0.2, 0.04, d - 0.12, h, h + 0.12, "tvbody");
|
||||
s.box(-0.3, d - 0.18, 0.3, d - 0.15, h + 0.1, h + 0.45, "tvbody");
|
||||
s.open([-0.3, d - 0.18], [0.3, d - 0.18], [[0.02, 0.58, h + 0.12, h + 0.43]], "tvs on");
|
||||
}
|
||||
if p.chair { sprite().office_chair(0.0, -d - 0.3, p.chair_mat); }
|
||||
}
|
||||
|
||||
fn meta_tv() {
|
||||
#{ label: "TV", category: "Furniture", icon: "📺", storey: true,
|
||||
help: "A TV on a low cabinet (or on the wall), screen towards -y.",
|
||||
props: [
|
||||
#{ key: "size", type: "choice", def: "55", choices: ["32", "43", "55", "65", "75", "85"], label: "Size (inch)" },
|
||||
#{ key: "stand", type: "bool", def: true, label: "On a cabinet" },
|
||||
#{ key: "on", type: "bool", def: true, label: "Switched on" },
|
||||
#{ key: "mat", type: "material", def: "woodd", choices: wood_choices(), label: "Cabinet" },
|
||||
] }
|
||||
}
|
||||
fn prefab_tv(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let p = o.p;
|
||||
let sw = parse_float(p.size) * 0.0254 * 0.8716;
|
||||
let sh = sw * 9.0 / 16.0;
|
||||
let s = sprite();
|
||||
let zb = 1.0;
|
||||
let y0 = 0.15;
|
||||
if p.stand {
|
||||
let cw = fmax(1.6, sw + 0.3) / 2.0;
|
||||
s.box(-cw, -0.22, cw, 0.22, 0.02, 0.5, p.mat);
|
||||
s.open([-cw, -0.22], [cw, -0.22], [[0.04, cw - 0.02, 0.08, 0.44], [cw + 0.02, 2.0 * cw - 0.04, 0.08, 0.44]], "seam");
|
||||
s.box(-0.15, -0.08, 0.15, 0.08, 0.5, 0.53, "tvbody");
|
||||
s.box(-0.03, 0.0, 0.03, 0.04, 0.53, 0.62, "tvbody");
|
||||
zb = 0.6;
|
||||
y0 = 0.0;
|
||||
}
|
||||
s.box(-sw / 2.0, y0, sw / 2.0, y0 + 0.05, zb, zb + sh, "tvbody");
|
||||
s.open([-sw / 2.0, y0], [sw / 2.0, y0], [[0.02, sw - 0.02, zb + 0.02, zb + sh - 0.02]], if p.on { "tvs on" } else { "tvs" });
|
||||
}
|
||||
|
||||
fn meta_bed() {
|
||||
#{ label: "Bed", category: "Furniture", icon: "🛏", storey: true,
|
||||
help: "Head (headboard, pillows) at +y.",
|
||||
props: [
|
||||
#{ key: "size", type: "choice", def: "double", choices: ["single", "double", "king"], label: "Size" },
|
||||
#{ key: "mat", type: "material", def: "uph2", choices: fabric_choices(), label: "Duvet" },
|
||||
#{ key: "frame", type: "material", def: "woodd", choices: wood_choices(), label: "Frame" },
|
||||
#{ key: "nightstands", type: "bool", def: true, label: "Nightstands" },
|
||||
] }
|
||||
}
|
||||
fn prefab_bed(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let p = o.p;
|
||||
let w = (if p.size == "single" { 0.9 } else if p.size == "king" { 1.8 } else { 1.6 }) / 2.0;
|
||||
let L = 1.0;
|
||||
let fr = p.frame;
|
||||
let duvet = p.mat;
|
||||
let two = w > 0.6;
|
||||
sprite().seq([
|
||||
[0.0, L + 0.04, |q| q.box(-w - 0.04, L, w + 0.04, L + 0.08, 0.02, 0.95, fr)],
|
||||
[0.0, 0.0, |q| {
|
||||
q.box(-w - 0.04, -L, w + 0.04, L, 0.02, 0.3, fr);
|
||||
q.box(-w, -L + 0.02, w, L - 0.02, 0.3, 0.5, "cloth");
|
||||
if two {
|
||||
q.box(-w + 0.06, L - 0.45, -0.03, L - 0.08, 0.5, 0.62, "cloth");
|
||||
q.box(0.03, L - 0.45, w - 0.06, L - 0.08, 0.5, 0.62, "cloth");
|
||||
} else {
|
||||
q.box(-w + 0.06, L - 0.45, w - 0.06, L - 0.08, 0.5, 0.62, "cloth");
|
||||
}
|
||||
q.box(-w - 0.02, -L, w + 0.02, L - 0.55, 0.5, 0.58, duvet);
|
||||
}],
|
||||
]);
|
||||
if p.nightstands {
|
||||
for side in [-1, 1] {
|
||||
let x = side * (w + 0.32);
|
||||
let n = sprite();
|
||||
n.box(x - 0.22, L - 0.42, x + 0.22, L - 0.02, 0.02, 0.5, fr);
|
||||
n.cyl(x, L - 0.2, 0.08, 0.5, 0.75, "lshade");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn meta_cupboard() {
|
||||
#{ label: "Cupboard", category: "Furniture", icon: "▯", storey: true,
|
||||
help: "A wardrobe or cabinet, doors on the -y side.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 1.2, min: 0.3, max: 6.0, step: 0.05, label: "Width (m)" },
|
||||
#{ key: "d", type: "float", def: 0.6, min: 0.3, max: 1.0, step: 0.05, label: "Depth (m)" },
|
||||
#{ key: "h", type: "float", def: 2.0, min: 0.4, max: 3.0, step: 0.05, label: "Height (m)" },
|
||||
#{ key: "mat", type: "material", def: "wardrobe", choices: ["wardrobe", "wood", "woodd", "furnw", "kitchenc"], label: "Colour" },
|
||||
] }
|
||||
}
|
||||
fn prefab_cupboard(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let p = o.p;
|
||||
let w = p.w / 2.0;
|
||||
let d = p.d / 2.0;
|
||||
let s = sprite();
|
||||
s.box(-w, -d, w, d, 0.02, p.h, p.mat);
|
||||
let n = fmax(1.0, (p.w / 0.5).round()).to_int();
|
||||
let dw = p.w / n;
|
||||
let doors = [];
|
||||
for i in 0..n { doors.push([i * dw + 0.015, (i + 1) * dw - 0.015, 0.06, p.h - 0.04]); }
|
||||
s.open([-w, -d], [w, -d], doors, "seam");
|
||||
}
|
||||
|
||||
fn meta_shelf() {
|
||||
#{ label: "Bookshelf", category: "Furniture", icon: "📚", storey: true,
|
||||
help: "Open shelves facing -y.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 1.0, min: 0.3, max: 6.0, step: 0.05, label: "Width (m)" },
|
||||
#{ key: "d", type: "float", def: 0.35, min: 0.2, max: 0.8, step: 0.05, label: "Depth (m)" },
|
||||
#{ key: "h", type: "float", def: 1.9, min: 0.4, max: 3.0, step: 0.05, label: "Height (m)" },
|
||||
#{ key: "books", type: "bool", def: true, label: "Books" },
|
||||
#{ key: "mat", type: "material", def: "wood", choices: wood_choices(), label: "Colour" },
|
||||
] }
|
||||
}
|
||||
fn prefab_shelf(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let p = o.p;
|
||||
let w = p.w / 2.0;
|
||||
let d = p.d / 2.0;
|
||||
let s = sprite();
|
||||
s.box(-w, -d, w, d, 0.02, p.h, p.mat);
|
||||
let n = fmax(1.0, ((p.h - 0.1) / 0.36).floor()).to_int();
|
||||
let sh = (p.h - 0.08) / n;
|
||||
let comp = [];
|
||||
let bks = [[], [], []];
|
||||
for i in 0..n {
|
||||
let z0 = 0.06 + i * sh;
|
||||
let z1 = z0 + sh - 0.03;
|
||||
comp.push([0.03, p.w - 0.03, z0, z1]);
|
||||
if p.books {
|
||||
let u = 0.05;
|
||||
let j = 0;
|
||||
while u < p.w - 0.12 {
|
||||
let bw = 0.04 + 0.08 * hash(i * 13.1 + j * 3.7 + p.w);
|
||||
if hash(j * 7.9 + i * 2.3) > 0.88 { u += bw + 0.06; j += 1; continue; }
|
||||
let bh = (z1 - z0) * (0.62 + 0.3 * hash(j * 5.3 + i));
|
||||
bks[j % 3].push([u, fmin(u + bw, p.w - 0.05), z0, z0 + bh]);
|
||||
u += bw + 0.006;
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let a = [-w, -d];
|
||||
let b = [w, -d];
|
||||
s.open(a, b, comp, "open");
|
||||
for k in 0..3 {
|
||||
if bks[k].len() > 0 { s.open(a, b, bks[k], "bk" + (k + 1)); }
|
||||
}
|
||||
}
|
||||
|
||||
fn meta_kitchen() {
|
||||
#{ label: "Kitchen", category: "Furniture", icon: "🍳", storey: true,
|
||||
help: "A kitchen run: base cabinets and worktop with sink and hob, wall cabinets and a fridge at the +x end. Front towards -y.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 3.0, min: 0.6, max: 10.0, step: 0.05, label: "Length (m)" },
|
||||
#{ key: "mat", type: "material", def: "kitchenc", choices: ["kitchenc", "furnw", "wood", "woodd", "doorg"], label: "Cabinets" },
|
||||
#{ key: "top", type: "material", def: "worktop", choices: ["worktop", "wood", "steel", "furnw"], label: "Worktop" },
|
||||
#{ key: "sink", type: "bool", def: true, label: "Sink" },
|
||||
#{ key: "hob", type: "bool", def: true, label: "Hob" },
|
||||
#{ key: "upper", type: "bool", def: true, label: "Wall cabinets" },
|
||||
#{ key: "fridge", type: "bool", def: true, label: "Fridge" },
|
||||
] }
|
||||
}
|
||||
fn prefab_kitchen(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let p = o.p;
|
||||
let w = p.w / 2.0;
|
||||
let fr = p.fridge && p.w >= 1.3;
|
||||
let x1 = if fr { w - 0.64 } else { w };
|
||||
let L = x1 + w;
|
||||
let m = p.mat;
|
||||
let tp = p.top;
|
||||
let items = [[(x1 - w) / 2.0, 0.0, |q| {
|
||||
q.box(-w, -0.28, x1, 0.3, 0.02, 0.86, m);
|
||||
let n = fmax(1.0, (L / 0.6).round()).to_int();
|
||||
let dw = L / n;
|
||||
let fronts = [];
|
||||
let uppers = [];
|
||||
for i in 0..n {
|
||||
fronts.push([i * dw + 0.01, (i + 1) * dw - 0.01, 0.1, 0.84]);
|
||||
uppers.push([i * dw + 0.01, (i + 1) * dw - 0.01, 1.47, 2.13]);
|
||||
}
|
||||
q.open([-w, -0.28], [x1, -0.28], fronts, "seam");
|
||||
q.box(-w, -0.32, x1, 0.3, 0.86, 0.9, tp);
|
||||
if p.sink && L >= 1.0 {
|
||||
let sx = -w + fmin(0.5, L * 0.3);
|
||||
q.flat(rect(sx - 0.24, -0.2, sx + 0.24, 0.14), 0.904, "sink");
|
||||
q.stem(sx, 0.22, 0.9, 1.18, 0.16, "metal");
|
||||
}
|
||||
if p.hob && L >= 1.8 {
|
||||
let hx = x1 - 0.5;
|
||||
q.flat(rect(hx - 0.3, -0.24, hx + 0.3, 0.22), 0.904, "hob");
|
||||
for c in [[-1, -1], [1, -1], [1, 1], [-1, 1]] { q.disc(hx + c[0] * 0.14, c[1] * 0.11, 0.906, 0.08, "hobr"); }
|
||||
}
|
||||
if p.upper {
|
||||
q.box(-w, -0.05, x1, 0.3, 1.45, 2.15, m);
|
||||
q.open([-w, -0.05], [x1, -0.05], uppers, "seam");
|
||||
}
|
||||
}]];
|
||||
if fr {
|
||||
items.push([x1 + 0.33, 0.0, |q| {
|
||||
q.box(x1 + 0.02, -0.32, w, 0.3, 0.02, 1.9, "fridge");
|
||||
q.open([x1 + 0.02, -0.32], [w, -0.32], [[0.03, w - x1 - 0.05, 0.06, 1.12], [0.03, w - x1 - 0.05, 1.16, 1.86]], "seam");
|
||||
}]);
|
||||
}
|
||||
sprite().seq(items);
|
||||
}
|
||||
|
||||
fn meta_bath() {
|
||||
#{ label: "Bathtub", category: "Furniture", icon: "🛁", storey: true,
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 1.7, min: 1.0, max: 2.4, step: 0.05, label: "Length (m)" },
|
||||
#{ key: "d", type: "float", def: 0.75, min: 0.6, max: 1.4, step: 0.05, label: "Width (m)" },
|
||||
] }
|
||||
}
|
||||
fn prefab_bath(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let w = o.p.w / 2.0;
|
||||
let d = o.p.d / 2.0;
|
||||
let s = sprite();
|
||||
s.box(-w, -d, w, d, 0.02, 0.56, "sanit");
|
||||
s.flat(rect(-w + 0.08, -d + 0.08, w - 0.08, d - 0.08), 0.562, "poolw t");
|
||||
s.lines(rsegs([-w + 0.08, -d + 0.08, w - 0.08, d - 0.08]), 0.563, "seam");
|
||||
s.stem(w - 0.1, 0.0, 0.56, 0.76, 0.15, "metal");
|
||||
}
|
||||
|
||||
fn meta_shower() {
|
||||
#{ label: "Shower", category: "Furniture", icon: "🚿", storey: true,
|
||||
help: "A tray with glass on its -y and +x sides; the riser is in the back left corner.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 0.9, min: 0.7, max: 2.0, step: 0.05, label: "Width (m)" },
|
||||
#{ key: "d", type: "float", def: 0.9, min: 0.7, max: 2.0, step: 0.05, label: "Depth (m)" },
|
||||
] }
|
||||
}
|
||||
fn prefab_shower(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let w = o.p.w / 2.0;
|
||||
let d = o.p.d / 2.0;
|
||||
let s = sprite();
|
||||
s.box(-w, -d, w, d, 0.02, 0.08, "sanit");
|
||||
s.flat(rect(-w + 0.05, -d + 0.05, w - 0.05, d - 0.05), 0.082, "shower t");
|
||||
s.disc(0.0, 0.0, 0.083, 0.04, "keyb");
|
||||
s.stem(-w + 0.06, d - 0.06, 0.08, 2.05, 0.14, "metal");
|
||||
s.disc(-w + 0.2, d - 0.2, 2.0, 0.1, "metal");
|
||||
s.box(-w, -d, w, -d + 0.02, 0.08, 2.0, "glassp");
|
||||
s.box(w - 0.02, -d + 0.02, w, d, 0.08, 2.0, "glassp");
|
||||
}
|
||||
|
||||
fn meta_toilet() {
|
||||
#{ label: "Toilet", category: "Furniture", icon: "🚽", storey: true, help: "Cistern at +y.", props: [] }
|
||||
}
|
||||
fn prefab_toilet(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
sprite().seq([
|
||||
[0.0, 0.25, |q| q.box(-0.19, 0.17, 0.19, 0.33, 0.02, 0.8, "sanit")],
|
||||
[0.0, -0.05, |q| {
|
||||
q.frust(0.0, -0.05, 0.13, 0.19, 0.02, 0.42, "sanit", 12);
|
||||
q.disc(0.0, -0.05, 0.422, 0.14, "seam");
|
||||
}],
|
||||
]);
|
||||
}
|
||||
|
||||
fn meta_basin() {
|
||||
#{ label: "Washbasin", category: "Furniture", icon: "🚰", storey: true,
|
||||
help: "A vanity unit with a basin and a mirror on the wall behind (+y).",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 0.6, min: 0.4, max: 2.4, step: 0.05, label: "Width (m)" },
|
||||
#{ key: "mirror", type: "bool", def: true, label: "Mirror" },
|
||||
#{ key: "mat", type: "material", def: "furnw", choices: wood_choices(), label: "Cabinet" },
|
||||
] }
|
||||
}
|
||||
fn prefab_basin(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let w = o.p.w / 2.0;
|
||||
let s = sprite();
|
||||
s.box(-w, -0.22, w, 0.23, 0.02, 0.82, o.p.mat);
|
||||
s.open([-w, -0.22], [w, -0.22], [[0.02, w - 0.01, 0.1, 0.8], [w + 0.01, 2.0 * w - 0.02, 0.1, 0.8]], "seam");
|
||||
s.box(-w, -0.24, w, 0.23, 0.82, 0.86, "sanit");
|
||||
s.flat(rect(-w + 0.08, -0.17, w - 0.08, 0.1), 0.862, "sink");
|
||||
s.stem(0.0, 0.17, 0.86, 1.02, 0.14, "metal");
|
||||
if o.p.mirror { s.box(-w + 0.04, 0.21, w - 0.04, 0.23, 1.05, 1.75, "glassp"); }
|
||||
}
|
||||
|
||||
fn meta_plant() {
|
||||
#{ label: "Plant", category: "Furniture", icon: "🪴", storey: true,
|
||||
props: [
|
||||
#{ key: "h", type: "float", def: 1.0, min: 0.3, max: 2.5, step: 0.05, label: "Height (m)" },
|
||||
#{ key: "pot", type: "material", def: "terra", choices: ["terra", "furnw", "concb", "woodd"], label: "Pot" },
|
||||
] }
|
||||
}
|
||||
fn prefab_plant(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let h = o.p.h;
|
||||
let r = 0.1 + h * 0.08;
|
||||
let zp = 0.02 + r * 1.5;
|
||||
let cr = 0.15 + h * 0.2;
|
||||
let s = sprite();
|
||||
s.frust(0.0, 0.0, r * 0.75, r, 0.02, zp, o.p.pot, 10);
|
||||
s.ext_box(-cr, -cr, cr, cr, zp, zp + h);
|
||||
s.stem(0.0, 0.0, zp, zp + h * 0.5, 0.25, "trunk");
|
||||
let R = cr * px() * 1.18;
|
||||
s.circle(0.0, 0.0, zp + h * 0.62, R, "leaf l");
|
||||
s.circle(0.0, 0.0, zp + h * 0.62, R * 0.58, "leaf t", -R * 0.28, -R * 0.3);
|
||||
}
|
||||
|
||||
fn meta_rug() {
|
||||
#{ label: "Rug", category: "Furniture", icon: "▭", shape: "rect", storey: true,
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 2.0, min: 0.4, max: 10.0, step: 0.05, label: "Width (m)" },
|
||||
#{ key: "d", type: "float", def: 1.4, min: 0.4, max: 10.0, step: 0.05, label: "Depth (m)" },
|
||||
#{ key: "mat", type: "material", def: "rug1", choices: ["rug1", "rug2", "rug3", "uph1", "uph3", "cloth"], label: "Colour" },
|
||||
] }
|
||||
}
|
||||
fn prefab_rug(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let w = o.p.w / 2.0;
|
||||
let d = o.p.d / 2.0;
|
||||
if o.z.abs() < 0.25 { layer("floor"); }
|
||||
let s = sprite();
|
||||
s.flat(rect(-w, -d, w, d), 0.012, o.p.mat + " t");
|
||||
let i = fmin(0.1, fmin(w, d) * 0.2);
|
||||
s.lines(rsegs([-w + i, -d + i, w - i, d - i]), 0.013, "rugl");
|
||||
}
|
||||
|
||||
fn meta_floor_lamp() {
|
||||
#{ label: "Floor lamp", category: "Furniture", icon: "💡", storey: true,
|
||||
props: [ #{ key: "on", type: "bool", def: true, label: "Lit" } ] }
|
||||
}
|
||||
fn prefab_floor_lamp(o) {
|
||||
if furniture_hidden(o) { return; }
|
||||
let s = sprite();
|
||||
s.disc(0.0, 0.0, 0.025, 0.16, "keyb");
|
||||
s.stem(0.0, 0.0, 0.02, 1.4, 0.12, "metal");
|
||||
s.frust(0.0, 0.0, 0.22, 0.14, 1.32, 1.62, "lshade", 10);
|
||||
if o.p.on { s.circle(0.0, 0.0, 1.5, 1.1, "lamp"); }
|
||||
}
|
||||
@@ -7,7 +7,7 @@ fn ground_materials() {
|
||||
}
|
||||
|
||||
fn meta_site() {
|
||||
#{ label: "Site ground", category: "Ground", icon: "▭",
|
||||
#{ 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)" },
|
||||
@@ -26,7 +26,7 @@ fn prefab_site(o) {
|
||||
}
|
||||
|
||||
fn meta_area() {
|
||||
#{ label: "Area", category: "Ground", icon: "▱",
|
||||
#{ 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)" },
|
||||
@@ -46,7 +46,7 @@ fn prefab_area(o) {
|
||||
}
|
||||
|
||||
fn meta_road() {
|
||||
#{ label: "Road", category: "Ground", icon: "═",
|
||||
#{ label: "Road", category: "Ground", icon: "═", shape: "line",
|
||||
help: "A straight road along its length, with a dashed centre line.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 40.0, min: 2.0, max: 2000.0, step: 1.0, label: "Length (m)" },
|
||||
@@ -90,7 +90,7 @@ fn prefab_parking(o) {
|
||||
}
|
||||
|
||||
fn meta_wall() {
|
||||
#{ label: "Wall", category: "Site", icon: "▬",
|
||||
#{ label: "Wall", category: "Site", icon: "▬", shape: "line",
|
||||
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)" },
|
||||
@@ -109,7 +109,7 @@ fn prefab_wall(o) {
|
||||
}
|
||||
|
||||
fn meta_fence() {
|
||||
#{ label: "Fence", category: "Site", icon: "┼",
|
||||
#{ label: "Fence", category: "Site", icon: "┼", shape: "line",
|
||||
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)" },
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
// 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); }
|
||||
}
|
||||
|
||||
fn along_world_x(rot) { ((rot / 90.0).round().to_int() % 2) == 0 }
|
||||
|
||||
/// 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);
|
||||
}
|
||||
@@ -20,7 +20,7 @@ fn prefab_tree(o) {
|
||||
}
|
||||
|
||||
fn meta_tree_row() {
|
||||
#{ label: "Row of trees", category: "Nature", icon: "🌲",
|
||||
#{ label: "Row of trees", category: "Nature", icon: "🌲", shape: "line",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 30.0, min: 2.0, max: 500.0, step: 1.0, label: "Length (m)" },
|
||||
#{ key: "spacing", type: "float", def: 6.0, min: 2.0, max: 30.0, step: 0.5, label: "Spacing (m)" },
|
||||
@@ -38,7 +38,7 @@ fn prefab_tree_row(o) {
|
||||
}
|
||||
|
||||
fn meta_hedge() {
|
||||
#{ label: "Hedge", category: "Nature", icon: "▬",
|
||||
#{ label: "Hedge", category: "Nature", icon: "▬", shape: "line",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 10.0, min: 0.5, max: 300.0, step: 0.5, label: "Length (m)" },
|
||||
#{ key: "d", type: "float", def: 0.6, min: 0.3, max: 3.0, step: 0.1, label: "Thickness (m)" },
|
||||
|
||||
Reference in new issue
Block a user