New route prefab (routes.rhai): cars, buses, motorbikes, cyclists, people, deer or elephants move along waypoints, round a loop or there and back. Each way keeps to the left of the line (or right), corners are rounded into pieces of at most 45 degrees; there-and-back routes become loops with U-turns, so the two directions never share a strip. Engine: Prim::Travel / sprite.begin_travel / script travel(a, b, dur, windows) ... end(): the mover is drawn at a piece's start and slides to its end while it is on that piece, hidden otherwise. The renderer writes keyframes per piece (named by a hash of their content) into one <style> per view. Each piece is its own sprite with the lane strip as its sort extent (ext_reset), mitred at the corners, so movers pass in front of and behind trees, lamps and houses. Movers that look alike and never meet on a piece share one drawing. iso-travel pauses (not hides) in still, reduced-motion and ghosted views. Studio: meta shape "path" draws point by point (snapping like runs) and saves one object with pts; ending on the first point closes the loop. MCP: pts in the object schema, route guidance. Demos: streets gets traffic round the block, a bus, motorbikes, cyclists and strolling people (replacing the one-axis cars); country gets cars on the lane. Tests: Rust 79 (each mover on exactly one piece at every moment, in every rotation), Studio 125, web 99.
This commit is contained in:
1 parent
ccfd619a99
commit
5ec164a633
20 files changed
+698
-23
No files matched your search
@@ -858,6 +858,121 @@ fn piece(id: &str, kind: &str, a: [f64; 2], b: [f64; 2], props: Value) -> Object
|
||||
o
|
||||
}
|
||||
|
||||
/// The windows of every route mover piece of object `id`: (piece sprite, windows).
|
||||
fn travel_windows(b: &crate::Built, id: &str) -> Vec<Vec<[f64; 2]>> {
|
||||
fn walk(p: &isokit_core::Prim, out: &mut Vec<Vec<[f64; 2]>>) {
|
||||
match p {
|
||||
isokit_core::Prim::Travel { wins, .. } => out.push(wins.clone()),
|
||||
isokit_core::Prim::Group { items, .. } => items.iter().for_each(|q| walk(q, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
for s in b.scene.layers.iter().flat_map(|l| l.sprites.iter()) {
|
||||
if s.obj.as_deref() == Some(id) {
|
||||
s.prims.iter().for_each(|p| walk(p, &mut out));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_movers_show_once_each_at_every_moment() {
|
||||
let r = Runtime::new(&Bundle::default()).unwrap();
|
||||
let mut cars = obj(
|
||||
"cars",
|
||||
"route",
|
||||
0.0,
|
||||
0.0,
|
||||
json!({"kind": "car", "n": 4, "closed": true}),
|
||||
);
|
||||
cars.pts = Some(vec![
|
||||
[-25.0, -10.0],
|
||||
[25.0, -10.0],
|
||||
[25.0, 10.0],
|
||||
[-25.0, 10.0],
|
||||
]);
|
||||
let mut walk = obj(
|
||||
"walk",
|
||||
"route",
|
||||
0.0,
|
||||
0.0,
|
||||
json!({"kind": "person", "n": 3, "colour": "hivis"}),
|
||||
);
|
||||
walk.pts = Some(vec![[-15.0, -4.0], [5.0, -4.0], [12.0, 3.0]]);
|
||||
let mut deer = obj(
|
||||
"deer",
|
||||
"route",
|
||||
30.0,
|
||||
20.0,
|
||||
json!({"kind": "deer", "n": 2, "path": false}),
|
||||
);
|
||||
deer.rot = 30.0;
|
||||
deer.pts = Some(vec![[-6.0, 0.0], [6.0, 0.0]]);
|
||||
let doc = Doc {
|
||||
objects: vec![cars, walk, deer, obj("t1", "tree", 0.0, -14.0, json!({}))],
|
||||
..Default::default()
|
||||
};
|
||||
for view in 0..4u8 {
|
||||
let b = r.build("site", view, &cfg(), &doc).unwrap();
|
||||
assert!(b.scene.warnings.is_empty(), "{:?}", b.scene.warnings);
|
||||
let out = render(&b.scene, view, &RenderOpts::default());
|
||||
assert_eq!(out.cycles, 0, "view {view}: depth-sort cycles");
|
||||
// one <style> with every keyframe set, each used
|
||||
assert_eq!(out.html.matches("<style>").count(), 1);
|
||||
for (id, n) in [("cars", 4), ("walk", 3), ("deer", 2)] {
|
||||
let wins = travel_windows(&b, id);
|
||||
assert!(!wins.is_empty(), "{id}");
|
||||
// at any moment, each mover is on exactly one piece
|
||||
for k in 0..60 {
|
||||
let t = (k as f64 + 0.37) / 60.0;
|
||||
let on: usize = wins
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter(|w| {
|
||||
let s = w[0].rem_euclid(1.0);
|
||||
let e = s + (w[1] - w[0]);
|
||||
(t >= s && t < e) || (t + 1.0 >= s && t + 1.0 < e)
|
||||
})
|
||||
.count();
|
||||
assert_eq!(on, n, "{id} view {view} at {t}");
|
||||
}
|
||||
}
|
||||
}
|
||||
let b = r.build("site", 0, &cfg(), &doc).unwrap();
|
||||
let out = render(&b.scene, 0, &RenderOpts::default());
|
||||
let row = |id: &str, k: &str| {
|
||||
out.info[id]
|
||||
.rows
|
||||
.iter()
|
||||
.find(|r| r.0 == k)
|
||||
.map(|r| r.1.clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
// the loop keeps 1.5 m inside the drawn square, its corners rounded
|
||||
assert_eq!(row("cars", "Route"), "loop, 140 m");
|
||||
assert!(
|
||||
row("cars", "Movers").starts_with("4 cars, 8 m/s, round in 15."),
|
||||
"{}",
|
||||
row("cars", "Movers")
|
||||
);
|
||||
assert_eq!(row("walk", "Route"), "there and back, 29.9 m each way");
|
||||
// one colour for every walker: they share drawings where their windows allow
|
||||
let pieces = b
|
||||
.scene
|
||||
.layers
|
||||
.iter()
|
||||
.flat_map(|l| l.sprites.iter())
|
||||
.filter(|s| s.obj.as_deref() == Some("walk"))
|
||||
.count();
|
||||
assert!(
|
||||
travel_windows(&b, "walk").len() < 3 * (pieces - 1),
|
||||
"walkers share drawings"
|
||||
);
|
||||
// the deer route turns with its object: its strips run at 30 degrees
|
||||
assert!(out.html.contains("iso-route"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wildlife_and_garden_prefabs_draw_from_every_side() {
|
||||
let r = Runtime::new(&Bundle::default()).unwrap();
|
||||
|
||||
Reference in new issue
Block a user