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");
|
||||
}
|
||||
Reference in new issue
Block a user