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.
610 lines
21 KiB
Rust
610 lines
21 KiB
Rust
//! Runtime tests: the scripting API, prefabs, the document driver, errors.
|
|
|
|
use crate::doc::{Doc, Object};
|
|
use crate::{Bundle, Runtime};
|
|
use isokit_core::{render, RenderOpts};
|
|
use serde_json::{json, Map, Value};
|
|
|
|
fn rt(main: &str) -> Runtime {
|
|
let mut b = Bundle {
|
|
main: Some("scene.rhai".into()),
|
|
..Default::default()
|
|
};
|
|
b.sources.insert("scene.rhai".into(), main.into());
|
|
Runtime::new(&b).unwrap_or_else(|e| panic!("{e}"))
|
|
}
|
|
|
|
fn cfg() -> Map<String, Value> {
|
|
Map::new()
|
|
}
|
|
|
|
fn obj(id: &str, kind: &str, x: f64, y: f64, props: Value) -> Object {
|
|
Object {
|
|
id: id.into(),
|
|
kind: kind.into(),
|
|
x,
|
|
y,
|
|
props: props.as_object().cloned().unwrap_or_default(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn script_draws_with_mixed_numbers() {
|
|
let r = rt(r#"
|
|
let s = sprite("hall");
|
|
s.box(0, 0, 10, 6.5, 0, 3, "wall", #{ top: "roof" });
|
|
info("hall", "Hall", "10 x 6.5 m");
|
|
tag(5, 3, 4, "Hall", 5);
|
|
"#);
|
|
let b = r.build("roof", 0, &cfg(), &Doc::default()).unwrap();
|
|
let out = render(&b.scene, 0, &RenderOpts::default());
|
|
assert!(out.html.contains("iso-roof iso-t"), "{}", out.html);
|
|
assert!(out.html.contains("iso-wall iso-l"));
|
|
assert!(out.html.contains(">Hall<"));
|
|
assert_eq!(out.info["hall"].detail, "10 x 6.5 m");
|
|
}
|
|
|
|
#[test]
|
|
fn methods_from_the_library_work_on_sprites() {
|
|
let r = rt(r#"
|
|
let s = sprite("t");
|
|
s.tree_at(0, 0, 1.5);
|
|
sprite("c").car(0, 0, 4.4, 1.8, "car2");
|
|
"#);
|
|
let b = r.build("site", 1, &cfg(), &Doc::default()).unwrap();
|
|
let out = render(&b.scene, 1, &RenderOpts::default());
|
|
assert!(out.html.contains("iso-leaf iso-l"));
|
|
assert!(out.html.contains("iso-glassd"));
|
|
}
|
|
|
|
#[test]
|
|
fn seq_group_and_closures() {
|
|
let r = rt(r#"
|
|
let s = sprite("x");
|
|
let h = 2.0;
|
|
s.seq([[0, 0, |q| q.box(0, 0, 1, 1, 0, h, "wall")], [5, 0, |q| q.box(5, 0, 6, 1, 0, h, "roof")]]);
|
|
s.group("a drive", |q| q.box(0, 3, 1, 4, 0, 1, "car1"));
|
|
"#);
|
|
let b = r.build("site", 0, &cfg(), &Doc::default()).unwrap();
|
|
let out = render(&b.scene, 0, &RenderOpts::default());
|
|
// from the south-east the box at x = 5 is nearer, so it is drawn after the one at x = 0
|
|
let (a, c) = (
|
|
out.html.find("iso-wall").unwrap(),
|
|
out.html.find("iso-roof").unwrap(),
|
|
);
|
|
assert!(a < c);
|
|
assert!(out.html.contains("<g class=\"iso-a iso-drive\">"));
|
|
}
|
|
|
|
#[test]
|
|
fn errors_point_at_the_line() {
|
|
let mut b = Bundle {
|
|
main: Some("scene.rhai".into()),
|
|
..Default::default()
|
|
};
|
|
b.sources.insert(
|
|
"scene.rhai".into(),
|
|
"let s = sprite(\"a\");\n\ns.box(0, 0, 1, 1, 0, \"high\", \"wall\");\n".into(),
|
|
);
|
|
let r = Runtime::new(&b).unwrap();
|
|
let e = r
|
|
.build("site", 0, &cfg(), &Doc::default())
|
|
.err()
|
|
.expect("error");
|
|
assert_eq!(e.file, "scene.rhai");
|
|
assert_eq!(e.line, Some(3), "{e}");
|
|
assert!(e.message.contains("number"), "{e}");
|
|
|
|
b.sources.insert("scene.rhai".into(), "let x = ;".into());
|
|
let e = Runtime::new(&b).err().expect("parse error");
|
|
assert_eq!(e.line, Some(1), "{e}");
|
|
}
|
|
|
|
#[test]
|
|
fn imports_resolve_from_the_bundle() {
|
|
let mut b = Bundle {
|
|
main: Some("scene.rhai".into()),
|
|
..Default::default()
|
|
};
|
|
b.sources.insert(
|
|
"lib/kiosk.rhai".into(),
|
|
"fn kiosk(x, y) { sprite(\"kiosk\").box(x, y, x + 2, y + 2, 0, 2.5, \"wall\"); }".into(),
|
|
);
|
|
b.sources.insert(
|
|
"scene.rhai".into(),
|
|
"import \"lib/kiosk\" as k;\nk::kiosk(1, 2);".into(),
|
|
);
|
|
let r = Runtime::new(&b).unwrap();
|
|
let s = r.build("site", 0, &cfg(), &Doc::default()).unwrap().scene;
|
|
assert_eq!(s.sprite_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn catalog_lists_library_and_project_prefabs() {
|
|
let mut b = Bundle {
|
|
main: None,
|
|
prelude: vec!["lib/kiosk.rhai".into()],
|
|
..Default::default()
|
|
};
|
|
b.sources.insert(
|
|
"lib/kiosk.rhai".into(),
|
|
r#"
|
|
fn meta_kiosk() { #{ label: "Kiosk", category: "Shop", props: [ #{ key: "h", type: "float", def: 2.5 } ] } }
|
|
fn prefab_kiosk(o) { sprite().box(-1, -1, 1, 1, 0, o.p.h, "wall"); }
|
|
"#
|
|
.into(),
|
|
);
|
|
let r = Runtime::new(&b).unwrap();
|
|
let kinds: Vec<&str> = r.catalog().iter().map(|m| m.kind.as_str()).collect();
|
|
for k in [
|
|
"site", "area", "road", "building", "tree", "car", "person", "tank", "kiosk",
|
|
] {
|
|
assert!(kinds.contains(&k), "{k} missing from {kinds:?}");
|
|
}
|
|
let k = r.prefab("kiosk").unwrap();
|
|
assert_eq!(k.label, "Kiosk");
|
|
assert_eq!(k.source, "lib/kiosk.rhai");
|
|
assert_eq!(k.props[0].default, json!(2.5));
|
|
}
|
|
|
|
#[test]
|
|
fn document_objects_are_placed_in_their_frame() {
|
|
let r = Runtime::new(&Bundle::default()).unwrap();
|
|
let mut doc = Doc::default();
|
|
let mut b = obj(
|
|
"hall",
|
|
"building",
|
|
30.0,
|
|
20.0,
|
|
json!({"w": 10, "d": 6, "floors": 1}),
|
|
);
|
|
b.rot = 90.0;
|
|
b.name = "Main hall".into();
|
|
b.tag = "Hall".into();
|
|
doc.objects.push(b);
|
|
doc.objects.push(obj("t1", "tree", 5.0, 5.0, json!({})));
|
|
let built = r.build("roof", 1, &cfg(), &doc).unwrap();
|
|
// turned a quarter: 6 m along x, 10 m along y, centred on (30, 20)
|
|
let bx = *built
|
|
.boxes
|
|
.get("hall")
|
|
.unwrap_or_else(|| panic!("{:?}", built.scene.warnings));
|
|
assert!(
|
|
(bx[0] - 27.0).abs() < 1e-9 && (bx[3] - 33.0).abs() < 1e-9,
|
|
"{bx:?}"
|
|
);
|
|
assert!(
|
|
(bx[1] - 15.0).abs() < 1e-9 && (bx[4] - 25.0).abs() < 1e-9,
|
|
"{bx:?}"
|
|
);
|
|
let out = render(&built.scene, 1, &RenderOpts::default());
|
|
assert!(out.html.contains("data-obj=\"hall\""));
|
|
assert!(out.html.contains("data-obj=\"t1\""));
|
|
assert_eq!(out.info["hall"].name, "Main hall");
|
|
assert_eq!(out.info["t1"].name, "Tree");
|
|
assert!(out.html.contains(">Hall<"));
|
|
assert!(
|
|
built.scene.warnings.is_empty(),
|
|
"{:?}",
|
|
built.scene.warnings
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn levels_cut_buildings_open_and_filter_objects() {
|
|
let r = Runtime::new(&Bundle::default()).unwrap();
|
|
let mut doc = Doc::default();
|
|
doc.objects
|
|
.push(obj("b", "building", 0.0, 0.0, json!({"floors": 2})));
|
|
let mut desk = obj("desk", "tree", 0.0, 0.0, json!({}));
|
|
desk.levels = vec!["gf".into()];
|
|
doc.objects.push(desk);
|
|
let closed = r.build("roof", 0, &cfg(), &doc).unwrap();
|
|
let gf = r.build("gf", 0, &cfg(), &doc).unwrap();
|
|
let up = r.build("3f", 0, &cfg(), &doc).unwrap();
|
|
assert!(!closed.boxes.contains_key("desk"));
|
|
assert!(gf.boxes.contains_key("desk"));
|
|
// cut open: a floor sprite plus four wall sprites
|
|
assert!(gf.scene.sprite_count() > closed.scene.sprite_count());
|
|
let html = render(&gf.scene, 0, &RenderOpts::default()).html;
|
|
assert!(html.contains("iso-z-floor iso-t"));
|
|
let html = render(&up.scene, 0, &RenderOpts::default()).html;
|
|
assert!(
|
|
html.contains("class=\"iso-dim\""),
|
|
"ghosted above the top storey"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn every_library_prefab_renders_cleanly_in_every_rotation() {
|
|
let r = Runtime::new(&Bundle::default()).unwrap();
|
|
let mut doc = Doc::default();
|
|
for (i, m) in r.catalog().iter().enumerate() {
|
|
let mut o = obj(&format!("o{i}"), &m.kind, i as f64 * 15.0, 0.0, json!({}));
|
|
// switch every bool prop on to exercise all branches
|
|
for p in &m.props {
|
|
if p.kind == "bool" {
|
|
o.props.insert(p.key.clone(), json!(true));
|
|
}
|
|
}
|
|
doc.objects.push(o);
|
|
}
|
|
for level in ["site", "gf", "1f"] {
|
|
for rot in 0..4 {
|
|
let b = r
|
|
.build(level, rot, &cfg(), &doc)
|
|
.unwrap_or_else(|e| panic!("{level}/{rot}: {e}"));
|
|
assert!(
|
|
b.scene.warnings.is_empty(),
|
|
"{level}/{rot}: {:?}",
|
|
b.scene.warnings
|
|
);
|
|
let out = render(&b.scene, rot, &RenderOpts::default());
|
|
assert!(!out.html.contains("NaN"), "{level}/{rot}");
|
|
for m in r.catalog() {
|
|
let id = format!(
|
|
"o{}",
|
|
r.catalog().iter().position(|q| q.kind == m.kind).unwrap()
|
|
);
|
|
assert!(
|
|
b.boxes.contains_key(&id),
|
|
"{} drew nothing in {level}/{rot}",
|
|
m.kind
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_types_and_failing_prefabs_warn_but_render() {
|
|
let mut b = Bundle {
|
|
main: None,
|
|
prelude: vec!["lib/bad.rhai".into()],
|
|
..Default::default()
|
|
};
|
|
b.sources.insert(
|
|
"lib/bad.rhai".into(),
|
|
"fn prefab_bad(o) { sprite().box(0, 0, 1, 1, 0, o.p.nope, \"wall\"); }".into(),
|
|
);
|
|
let r = Runtime::new(&b).unwrap();
|
|
let mut doc = Doc::default();
|
|
doc.objects.push(obj("u", "ufo", 0.0, 0.0, json!({})));
|
|
doc.objects.push(obj("x", "bad", 0.0, 0.0, json!({})));
|
|
doc.objects.push(obj("t", "tree", 5.0, 0.0, json!({})));
|
|
let built = r.build("site", 0, &cfg(), &doc).unwrap();
|
|
let w = built.scene.warnings.join("\n");
|
|
assert!(w.contains("object u: unknown type"), "{w}");
|
|
assert!(w.contains("object x:"), "{w}");
|
|
assert!(built.boxes.contains_key("t"));
|
|
}
|
|
|
|
#[test]
|
|
fn cfg_reaches_the_script() {
|
|
let r = rt(
|
|
r#"if cfg("big") { sprite("a").box(0, 0, 9, 9, 0, 9, "wall"); } else { sprite("a").box(0, 0, 1, 1, 0, 1, "wall"); }"#,
|
|
);
|
|
let mut c = Map::new();
|
|
c.insert("big".into(), json!(true));
|
|
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);
|
|
}
|
|
|
|
/// A 10 x 6 m fence perimeter, a fence across it (a T at both ends; at the
|
|
/// north it meets the joint of the north fence's two pieces) and a spur from
|
|
/// the east fence to a site wall.
|
|
fn fence_doc(style: &str) -> Doc {
|
|
let mut doc = Doc::default();
|
|
for (id, x, y, rot, w) in [
|
|
("south", 0.0, -3.0, 0.0, 10.0),
|
|
("north", -2.5, 3.0, 0.0, 5.0),
|
|
("north2", 2.5, 3.0, 0.0, 5.0),
|
|
("west", -5.0, 0.0, 90.0, 6.0),
|
|
("east", 5.0, 0.0, 90.0, 6.0),
|
|
("inner", 0.0, 0.0, 90.0, 6.0),
|
|
("spur", 6.5, 0.0, 0.0, 3.0),
|
|
] {
|
|
let mut o = obj(id, "fence", x, y, json!({ "w": w, "style": style }));
|
|
o.rot = rot;
|
|
doc.objects.push(o);
|
|
}
|
|
let mut wall = obj("wall", "wall", 8.0, 0.0, json!({"w": 10.0}));
|
|
wall.rot = 90.0;
|
|
doc.objects.push(wall);
|
|
doc
|
|
}
|
|
|
|
#[test]
|
|
fn fences_join_like_walls_in_every_design() {
|
|
let r = Runtime::new(&Bundle::default()).unwrap();
|
|
for (style, ht) in [
|
|
("mesh", 0.04),
|
|
("wood", 0.05),
|
|
("bars", 0.04),
|
|
("half stone", 0.16),
|
|
("cobble", 0.22),
|
|
("brick", 0.18),
|
|
] {
|
|
let doc = fence_doc(style);
|
|
let b = r.build("site", 0, &cfg(), &doc).unwrap();
|
|
assert!(
|
|
b.scene.warnings.is_empty(),
|
|
"{style}: {:?}",
|
|
b.scene.warnings
|
|
);
|
|
let bx = |id: &str| b.boxes[id];
|
|
let near = |a: f64, e: f64| (a - e).abs() < 1e-6;
|
|
// corners: the fence along x runs through, the other stops at its face
|
|
let (s, w) = (bx("south"), bx("west"));
|
|
assert!(
|
|
near(s[0], -5.0 - ht) && near(s[3], 5.0 + ht),
|
|
"{style} south {s:?}"
|
|
);
|
|
assert!(
|
|
near(w[1], -3.0 + ht) && near(w[4], 3.0 - ht),
|
|
"{style} west {w:?}"
|
|
);
|
|
// a T at the south, the end of a straight run at the north: it stops at the faces
|
|
let i = bx("inner");
|
|
assert!(
|
|
near(i[1], -3.0 + ht) && near(i[4], 3.0 - ht),
|
|
"{style} inner {i:?}"
|
|
);
|
|
// in line: the lower piece runs through the shared post, the other starts at it
|
|
let (n1, n2) = (bx("north"), bx("north2"));
|
|
assert!(
|
|
near(n1[3], ht) && near(n2[0], ht),
|
|
"{style} north {n1:?} {n2:?}"
|
|
);
|
|
// the spur stops at the east fence and at the site wall's face
|
|
let sp = bx("spur");
|
|
assert!(
|
|
near(sp[0], 5.0 + ht) && near(sp[3], 8.0 - 0.12),
|
|
"{style} spur {sp:?}"
|
|
);
|
|
for rot in 0..4u8 {
|
|
let b = r.build("site", rot, &cfg(), &doc).unwrap();
|
|
let out = render(&b.scene, rot, &RenderOpts::default());
|
|
assert_eq!(out.cycles, 0, "{style}/{rot}: depth-sort cycles");
|
|
}
|
|
}
|
|
// the brick wall has pillars as their own sprites, with brick courses on its faces
|
|
let b = r.build("site", 0, &cfg(), &fence_doc("brick")).unwrap();
|
|
assert!(sprites_of(&b, "south") > 4);
|
|
assert!(render(&b.scene, 0, &RenderOpts::default())
|
|
.html
|
|
.contains("iso-seam"));
|
|
}
|
|
|
|
#[test]
|
|
fn site_walls_join_at_corners() {
|
|
let r = Runtime::new(&Bundle::default()).unwrap();
|
|
let mut doc = Doc::default();
|
|
for (id, x, y, rot, w) in [
|
|
("s", 0.0, -3.0, 0.0, 10.0),
|
|
("n", 0.0, 3.0, 0.0, 10.0),
|
|
("w", -5.0, 0.0, 90.0, 6.0),
|
|
("e", 5.0, 0.0, 90.0, 6.0),
|
|
] {
|
|
let mut o = obj(id, "wall", x, y, json!({ "w": w }));
|
|
o.rot = rot;
|
|
doc.objects.push(o);
|
|
}
|
|
let b = r.build("site", 0, &cfg(), &doc).unwrap();
|
|
let (s, w) = (b.boxes["s"], b.boxes["w"]);
|
|
assert!(
|
|
(s[0] + 5.12).abs() < 1e-6 && (s[3] - 5.12).abs() < 1e-6,
|
|
"{s:?}"
|
|
);
|
|
assert!(
|
|
(w[1] + 2.88).abs() < 1e-6 && (w[4] - 2.88).abs() < 1e-6,
|
|
"{w:?}"
|
|
);
|
|
for rot in 0..4u8 {
|
|
let b = r.build("site", rot, &cfg(), &doc).unwrap();
|
|
assert_eq!(render(&b.scene, rot, &RenderOpts::default()).cycles, 0);
|
|
}
|
|
}
|
|
|
|
#[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");
|
|
}
|