isokit: project layer, Studio GUI, MCP server, viewer, CLI, demo project and docs
This commit is contained in:
1 parent
95c80ec6b1
commit
564aeb75f6
47 files changed
+5978
-366
No files matched your search
@@ -6,7 +6,10 @@ 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() };
|
||||
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}"))
|
||||
}
|
||||
@@ -16,7 +19,14 @@ fn cfg() -> Map<String, Value> {
|
||||
}
|
||||
|
||||
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() }
|
||||
Object {
|
||||
id: id.into(),
|
||||
kind: kind.into(),
|
||||
x,
|
||||
y,
|
||||
props: props.as_object().cloned().unwrap_or_default(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -59,17 +69,29 @@ fn seq_group_and_closures() {
|
||||
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());
|
||||
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 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");
|
||||
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}");
|
||||
@@ -81,9 +103,18 @@ fn errors_point_at_the_line() {
|
||||
|
||||
#[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 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);
|
||||
@@ -91,7 +122,11 @@ fn imports_resolve_from_the_bundle() {
|
||||
|
||||
#[test]
|
||||
fn catalog_lists_library_and_project_prefabs() {
|
||||
let mut b = Bundle { main: None, prelude: vec!["lib/kiosk.rhai".into()], ..Default::default() };
|
||||
let mut b = Bundle {
|
||||
main: None,
|
||||
prelude: vec!["lib/kiosk.rhai".into()],
|
||||
..Default::default()
|
||||
};
|
||||
b.sources.insert(
|
||||
"lib/kiosk.rhai".into(),
|
||||
r#"
|
||||
@@ -102,7 +137,9 @@ fn catalog_lists_library_and_project_prefabs() {
|
||||
);
|
||||
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"] {
|
||||
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();
|
||||
@@ -115,7 +152,13 @@ fn catalog_lists_library_and_project_prefabs() {
|
||||
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}));
|
||||
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();
|
||||
@@ -123,23 +166,37 @@ fn document_objects_are_placed_in_their_frame() {
|
||||
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 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);
|
||||
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})));
|
||||
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);
|
||||
@@ -153,7 +210,10 @@ fn levels_cut_buildings_open_and_filter_objects() {
|
||||
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");
|
||||
assert!(
|
||||
html.contains("class=\"iso-dim\""),
|
||||
"ghosted above the top storey"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -172,13 +232,26 @@ fn every_library_prefab_renders_cleanly_in_every_rotation() {
|
||||
}
|
||||
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 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);
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,8 +259,15 @@ fn every_library_prefab_renders_cleanly_in_every_rotation() {
|
||||
|
||||
#[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 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!({})));
|
||||
@@ -202,7 +282,9 @@ fn unknown_types_and_failing_prefabs_warn_but_render() {
|
||||
|
||||
#[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 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();
|
||||
|
||||
Reference in new issue
Block a user