diff --git a/README.md b/README.md index 90c9e42..08c8aa6 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,15 @@ trunks, rabbits hop, birds flap. `examples/country` (Country demo in the web Studio) puts them in a countryside corner with a lane, fence and hedgerow at an angle. +**Routes**: a Route moves cars, buses, motorbikes, cyclists, people, deer or +elephants along waypoints. Pick Route and click point after point on the map +(points snap onto road and wall ends; double-click or Enter ends it, the +first point again makes a loop). A loop is driven round, an open route there +and back; each way keeps to the left of the line (or the right), corners are +rounded, and the movers pass in front of and behind trees and houses as they +go. The streets demo has traffic round the block, a bus, motorbikes, cyclists +and people strolling. + **Fences and walls** around the plot: the Fence has six designs (mesh, wooden boards, steel bars, mesh on a stone base, cobble wall, brick wall with pillars). Fences and site walls whose ends meet join like house walls: corners diff --git a/crates/isokit-core/src/prim.rs b/crates/isokit-core/src/prim.rs index 08ccd6c..7ca255c 100644 --- a/crates/isokit-core/src/prim.rs +++ b/crates/isokit-core/src/prim.rs @@ -201,6 +201,17 @@ pub enum Prim { }, /// Items drawn back to front by the depth of their plan anchor. Seq { items: Vec<(P2, Vec)> }, + /// A mover on one straight piece of a route: the items, drawn at `from`, slide to + /// `to` (world points) during each window and are hidden the rest of the cycle of + /// `dur` seconds. A window is [start, end] in fractions of the cycle; it may run + /// past 1 and wraps round. + Travel { + from: P3, + to: P3, + dur: f64, + wins: Vec<[f64; 2]>, + items: Vec, + }, } /// One fish of a [`Prim::Swim`]: it goes round an ellipse of radii (rx, ry) diff --git a/crates/isokit-core/src/render.rs b/crates/isokit-core/src/render.rs index 5484beb..ceba699 100644 --- a/crates/isokit-core/src/render.rs +++ b/crates/isokit-core/src/render.rs @@ -87,6 +87,8 @@ struct Ctx<'a> { p: Proj, uid: &'a str, out: String, + /// Keyframes of the route movers by name, written once in one `"); + } // frame: the scene bounds (plan rectangle down to the ground depth) or the drawing if let Some((b, zb)) = scene.bounds { diff --git a/crates/isokit-core/src/sprite.rs b/crates/isokit-core/src/sprite.rs index cdc7015..c4cde72 100644 --- a/crates/isokit-core/src/sprite.rs +++ b/crates/isokit-core/src/sprite.rs @@ -123,6 +123,13 @@ enum Frame { items: Vec<(P2, Vec)>, cur: Option<(P2, Vec)>, }, + Travel { + from: P3, + to: P3, + dur: f64, + wins: Vec<[f64; 2]>, + items: Vec, + }, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] @@ -172,7 +179,7 @@ impl Sprite { /// the sprite. pub fn push(&mut self, p: Prim) -> &mut Self { match self.frames.last_mut() { - Some(Frame::Group { items, .. }) => items.push(p), + Some(Frame::Group { items, .. }) | Some(Frame::Travel { items, .. }) => items.push(p), Some(Frame::Seq { cur, .. }) => match cur { Some((_, items)) => items.push(p), // a primitive before the first seq_item starts an item at the origin @@ -622,12 +629,46 @@ impl Sprite { } pub fn end_group(&mut self) -> &mut Self { - if let Some(Frame::Group { cls, style, items }) = self.frames.pop() { - self.push(Prim::Group { cls, style, items }); + match self.frames.pop() { + Some(Frame::Group { cls, style, items }) => { + self.push(Prim::Group { cls, style, items }); + } + Some(Frame::Travel { + from, + to, + dur, + wins, + items, + }) => { + self.push(Prim::Travel { + from, + to, + dur, + wins, + items, + }); + } + Some(f) => self.frames.push(f), + None => {} } self } + /// Open a mover on a straight piece from `a` to `b` (points of this frame): what + /// is drawn until [`Sprite::end_group`] slides from a to b during each window of + /// the `dur`-second cycle and is hidden otherwise (see [`Prim::Travel`]). + pub fn begin_travel(&mut self, a: P3, b: P3, dur: f64, wins: Vec<[f64; 2]>) -> &mut Self { + let (from, to) = (self.xf.p3(a), self.xf.p3(b)); + self.frames.push(Frame::Travel { + from, + to, + dur, + wins, + items: vec![], + }); + self + } + /// Start a back-to-front sequence; each [`Sprite::seq_item`] starts an /// item anchored at a plan point; [`Sprite::end_seq`] closes it. pub fn begin_seq(&mut self) -> &mut Self { @@ -674,7 +715,7 @@ impl Sprite { let n = self.frames.len(); while let Some(f) = self.frames.last() { match f { - Frame::Group { .. } => { + Frame::Group { .. } | Frame::Travel { .. } => { self.end_group(); } Frame::Seq { .. } => { diff --git a/crates/isokit-project/src/mcp.rs b/crates/isokit-project/src/mcp.rs index 74d2654..1cde4ea 100644 --- a/crates/isokit-project/src/mcp.rs +++ b/crates/isokit-project/src/mcp.rs @@ -29,6 +29,9 @@ fences, hedges, house walls) can run at any angle: a piece from (x0, y0) to (x1, w = the distance and rot = atan2(y1 - y0, x1 - x0) in degrees; where diagonal pieces meet, corners are mitred. \ Animals (category Animals: deer, boar, fox, elephant, rabbits, birds, butterflies) are herds: props.n animals \ scattered within props.r metres of x, y, facing about the rotation; keep them out of roads, ponds and buildings. \ +A route (type route) moves props.n movers (props.kind: car, bus, motorbike, cyclist, person, deer, elephant) along \ +waypoints pts (relative to x, y): round them when props.closed, else there and back; each way keeps to the left \ +of the line (props.keep). For traffic, put the points on the road centre lines; keep routes clear of buildings. \ Production lines (category Industry): a producer (props.rate per minute, product) puts goods out on its local +x \ side; a conveyor (rot 0 runs towards +x, 90 +y, 180 -x, 270 -y; w = length, props.speed m/s) starting exactly \ at that side carries them, and a conveyor whose start is at another's end carries on with them (corners join). \ @@ -58,6 +61,8 @@ pub fn tools() -> Vec { "info": { "type": "string", "description": "tooltip text" }, "tag": { "type": "string", "description": "label pinned above the object" }, "levels": { "type": "array", "items": { "type": "string" }, "description": "levels it shows on (empty: all)" }, + "pts": { "type": "array", "items": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 }, + "description": "points [[x, y], ...] relative to x, y (in the object's turned frame): a route's waypoints, an area's outline" }, }); let mut add = obj_fields.as_object().unwrap().clone(); add.insert("type".into(), json!({ "type": "string", "description": "prefab type, e.g. building, tree, car (list_prefabs)" })); diff --git a/crates/isokit-script/src/api.rs b/crates/isokit-script/src/api.rs index 0881468..8653e8f 100644 --- a/crates/isokit-script/src/api.rs +++ b/crates/isokit-script/src/api.rs @@ -519,6 +519,24 @@ pub fn register_sprite(e: &mut Engine) { s.0.borrow_mut().end_group(); s.clone() }); + // a mover on one straight piece of a route (Prim::Travel); close with end() + e.register_fn( + "travel", + |s: &mut SpriteH, a: D, b: D, dur: D, wins: D| -> RR { + let wins = arr(&wins, "travel windows")? + .iter() + .map(|w| p2(w).map(|p| [p[0], p[1]])) + .collect::>>()?; + s.0.borrow_mut() + .begin_travel(p3(&a)?, p3(&b)?, n(&dur)?, wins); + Ok(s.clone()) + }, + ); + // forget the sort extent drawn so far (set it with ext() afterwards) + e.register_fn("ext_reset", |s: &mut SpriteH| -> SpriteH { + s.0.borrow_mut().ext.clear(); + s.clone() + }); e.register_fn( "group", |ctx: NativeCallContext, s: &mut SpriteH, cls: &str, f: FnPtr| -> RR { diff --git a/crates/isokit-script/src/conv.rs b/crates/isokit-script/src/conv.rs index bc2af53..9e8a5e1 100644 --- a/crates/isokit-script/src/conv.rs +++ b/crates/isokit-script/src/conv.rs @@ -23,7 +23,7 @@ pub fn n(d: &Dynamic) -> RR { err(format!("expected a number, got {}", d.type_name())) } -fn arr(d: &Dynamic, what: &str) -> RR { +pub fn arr(d: &Dynamic, what: &str) -> RR { match d.read_lock::() { Some(a) => Ok(a.clone()), None => err(format!("{what}: expected an array, got {}", d.type_name())), diff --git a/crates/isokit-script/src/stdlib.rs b/crates/isokit-script/src/stdlib.rs index 92136e2..3a3bd93 100644 --- a/crates/isokit-script/src/stdlib.rs +++ b/crates/isokit-script/src/stdlib.rs @@ -10,6 +10,7 @@ pub const MODULES: &[(&str, &str)] = &[ ("isokit/ground", include_str!("../stdlib/ground.rhai")), ("isokit/streets", include_str!("../stdlib/streets.rhai")), ("isokit/wildlife", include_str!("../stdlib/wildlife.rhai")), + ("isokit/routes", include_str!("../stdlib/routes.rhai")), ("isokit/nature", include_str!("../stdlib/nature.rhai")), ("isokit/building", include_str!("../stdlib/building.rhai")), ("isokit/vehicles", include_str!("../stdlib/vehicles.rhai")), diff --git a/crates/isokit-script/src/tests.rs b/crates/isokit-script/src/tests.rs index b9e7bc2..a2cf96d 100644 --- a/crates/isokit-script/src/tests.rs +++ b/crates/isokit-script/src/tests.rs @@ -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> { + fn walk(p: &isokit_core::Prim, out: &mut Vec>) { + 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