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
@@ -201,6 +201,17 @@ pub enum Prim {
|
||||
},
|
||||
/// Items drawn back to front by the depth of their plan anchor.
|
||||
Seq { items: Vec<(P2, Vec<Prim>)> },
|
||||
/// 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<Prim>,
|
||||
},
|
||||
}
|
||||
|
||||
/// One fish of a [`Prim::Swim`]: it goes round an ellipse of radii (rx, ry)
|
||||
|
||||
@@ -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 `<style>` at the end.
|
||||
keyframes: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl Ctx<'_> {
|
||||
@@ -572,6 +574,31 @@ impl Ctx<'_> {
|
||||
}
|
||||
self.out.push_str("</g>");
|
||||
}
|
||||
Prim::Travel {
|
||||
from,
|
||||
to,
|
||||
dur,
|
||||
wins,
|
||||
items,
|
||||
} => {
|
||||
let (a, b) = (
|
||||
self.p.s(from[0], from[1], from[2]),
|
||||
self.p.s(to[0], to[1], to[2]),
|
||||
);
|
||||
let kf = travel_keyframes(b[0] - a[0], b[1] - a[1], wins);
|
||||
// named by its content: views on one page never clash, equal ones share
|
||||
let name = format!("iso-tv{:08x}", fnv(&kf));
|
||||
let _ = write!(
|
||||
self.out,
|
||||
"<g class=\"iso-travel\" style=\"animation:{name} {}s linear infinite\">",
|
||||
r3s(*dur)
|
||||
);
|
||||
self.keyframes.entry(name).or_insert(kf);
|
||||
for q in items {
|
||||
self.prim(q);
|
||||
}
|
||||
self.out.push_str("</g>");
|
||||
}
|
||||
Prim::Seq { items } => {
|
||||
let p = self.p;
|
||||
let mut idx: Vec<usize> = (0..items.len()).collect();
|
||||
@@ -589,6 +616,61 @@ impl Ctx<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// FNV-1a hash of a text (keyframe names).
|
||||
fn fnv(s: &str) -> u32 {
|
||||
s.bytes().fold(0x811c_9dc5u32, |h, b| {
|
||||
(h ^ b as u32).wrapping_mul(0x0100_0193)
|
||||
})
|
||||
}
|
||||
|
||||
/// Keyframes of a [`Prim::Travel`]: hidden outside the windows, visible inside, sliding
|
||||
/// by (dx, dy) screen units from each window's start to its end.
|
||||
fn travel_keyframes(dx: f64, dy: f64, wins: &[[f64; 2]]) -> String {
|
||||
const EPS: f64 = 2e-5;
|
||||
// (time, visible, how far along) at the ends of every window, wrapped into 0..1
|
||||
let mut pts: Vec<(f64, bool, f64)> = vec![];
|
||||
for w in wins {
|
||||
let (s, e) = (w[0].rem_euclid(1.0), w[0].rem_euclid(1.0) + (w[1] - w[0]));
|
||||
if e - s <= 2.0 * EPS {
|
||||
continue;
|
||||
}
|
||||
let at = |t: f64| ((t - s) / (e - s)).clamp(0.0, 1.0);
|
||||
pts.push((s, false, 0.0));
|
||||
pts.push((s + EPS, true, at(s + EPS)));
|
||||
if e <= 1.0 {
|
||||
pts.push((e, true, 1.0));
|
||||
pts.push(((e + EPS).min(1.0), false, 1.0));
|
||||
} else {
|
||||
pts.push((1.0, true, at(1.0)));
|
||||
pts.push((0.0, true, at(1.0)));
|
||||
pts.push((e - 1.0, true, 1.0));
|
||||
pts.push((e - 1.0 + EPS, false, 1.0));
|
||||
}
|
||||
}
|
||||
pts.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let mut out = String::new();
|
||||
let put = |t: f64, vis: bool, f: f64, out: &mut String| {
|
||||
let _ = write!(
|
||||
out,
|
||||
"{}%{{visibility:{};transform:translate({}px,{}px)}}",
|
||||
r3s(t * 100.0),
|
||||
if vis { "visible" } else { "hidden" },
|
||||
r3s(dx * f),
|
||||
r3s(dy * f)
|
||||
);
|
||||
};
|
||||
if pts.first().is_none_or(|p| p.0 > 0.0) {
|
||||
put(0.0, false, pts.first().map_or(0.0, |p| p.2), &mut out);
|
||||
}
|
||||
for &(t, vis, f) in &pts {
|
||||
put(t, vis, f, &mut out);
|
||||
}
|
||||
if pts.last().is_none_or(|p| p.0 < 1.0) {
|
||||
put(1.0, false, pts.last().map_or(0.0, |p| p.2), &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The sprite's convex plan outline in view space when it is not its box.
|
||||
pub fn hull(s: &Sprite, p: Proj, bb: &Bb) -> Option<sort::Hull> {
|
||||
if s.ext.len() < 3 {
|
||||
@@ -627,6 +709,7 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
|
||||
p,
|
||||
uid: &uid,
|
||||
out: String::with_capacity(64 * 1024),
|
||||
keyframes: BTreeMap::new(),
|
||||
};
|
||||
let mut frame = [1e9f64, 1e9, -1e9, -1e9];
|
||||
let mut sprites = 0;
|
||||
@@ -743,6 +826,13 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
|
||||
);
|
||||
}
|
||||
cx.out.push_str("</g>");
|
||||
if !cx.keyframes.is_empty() {
|
||||
cx.out.push_str("<style>");
|
||||
for (name, kf) in &cx.keyframes {
|
||||
let _ = write!(cx.out, "@keyframes {name}{{{kf}}}");
|
||||
}
|
||||
cx.out.push_str("</style>");
|
||||
}
|
||||
|
||||
// frame: the scene bounds (plan rectangle down to the ground depth) or the drawing
|
||||
if let Some((b, zb)) = scene.bounds {
|
||||
|
||||
@@ -123,6 +123,13 @@ enum Frame {
|
||||
items: Vec<(P2, Vec<Prim>)>,
|
||||
cur: Option<(P2, Vec<Prim>)>,
|
||||
},
|
||||
Travel {
|
||||
from: P3,
|
||||
to: P3,
|
||||
dur: f64,
|
||||
wins: Vec<[f64; 2]>,
|
||||
items: Vec<Prim>,
|
||||
},
|
||||
}
|
||||
|
||||
#[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 { .. } => {
|
||||
|
||||
@@ -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<Value> {
|
||||
"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)" }));
|
||||
|
||||
@@ -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<SpriteH> {
|
||||
let wins = arr(&wins, "travel windows")?
|
||||
.iter()
|
||||
.map(|w| p2(w).map(|p| [p[0], p[1]]))
|
||||
.collect::<RR<Vec<_>>>()?;
|
||||
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<SpriteH> {
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn n(d: &Dynamic) -> RR<f64> {
|
||||
err(format!("expected a number, got {}", d.type_name()))
|
||||
}
|
||||
|
||||
fn arr(d: &Dynamic, what: &str) -> RR<Array> {
|
||||
pub fn arr(d: &Dynamic, what: &str) -> RR<Array> {
|
||||
match d.read_lock::<Array>() {
|
||||
Some(a) => Ok(a.clone()),
|
||||
None => err(format!("{what}: expected an array, got {}", d.type_name())),
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
// isokit/routes: movers on a path of waypoints - cars, buses, motorbikes,
|
||||
// cyclists, people, deer, elephants - going round a loop or there and back.
|
||||
//
|
||||
// The path is cut into straight pieces (corners rounded into short ones), each
|
||||
// piece its own sprite whose sort box is the lane strip, mitred at the corners
|
||||
// so neighbouring pieces never overlap. A mover is drawn at a piece's start and
|
||||
// slides to its end while it is on that piece (`travel`), so the depth sort
|
||||
// stays right as it goes past trees, lamps and houses.
|
||||
|
||||
fn route_kinds() { ["car", "bus", "motorbike", "cyclist", "person", "deer", "elephant"] }
|
||||
|
||||
/// A mover of `kind`: length, width, height, usual speed (m/s), lane offset
|
||||
/// from the drawn line, corner radius and its name.
|
||||
fn mover_spec(kind) {
|
||||
switch kind {
|
||||
"car" => #{ l: 4.4, w: 1.8, h: 1.5, v: 8.0, lane: 1.5, r: 4.0, one: "car", many: "cars" },
|
||||
"bus" => #{ l: 10.0, w: 2.5, h: 3.0, v: 6.0, lane: 1.6, r: 7.0, one: "bus", many: "buses" },
|
||||
"motorbike" => #{ l: 1.9, w: 0.7, h: 1.7, v: 7.0, lane: 1.2, r: 2.5, one: "motorbike", many: "motorbikes" },
|
||||
"cyclist" => #{ l: 1.8, w: 0.6, h: 1.9, v: 4.0, lane: 0.7, r: 2.0, one: "cyclist", many: "cyclists" },
|
||||
"deer" => #{ l: 1.6, w: 0.5, h: 1.6, v: 1.2, lane: 0.5, r: 1.0, one: "deer", many: "deer" },
|
||||
"elephant" => #{ l: 3.6, w: 1.8, h: 3.0, v: 1.0, lane: 1.3, r: 3.0, one: "elephant", many: "elephants" },
|
||||
// people look the same from every side: no rounded corners, fewer pieces
|
||||
_ => #{ l: 0.5, w: 0.5, h: 1.75, v: 1.3, lane: 0.4, r: 0.0, one: "person", many: "people" },
|
||||
}
|
||||
}
|
||||
|
||||
fn route_colours() { ["mixed"] + car_colours() + people_colours() }
|
||||
|
||||
/// The look (colour class) of mover k.
|
||||
fn mover_look(kind, colour, k) {
|
||||
if colour != "mixed" { return colour; }
|
||||
if kind == "car" || kind == "bus" || kind == "motorbike" { car_colours()[k % 5] }
|
||||
else if kind == "person" || kind == "cyclist" { people_colours()[k % 7] }
|
||||
else { "" }
|
||||
}
|
||||
|
||||
/// Draws a mover of `kind` centred at (0, 0), facing local +x.
|
||||
fn mover(kind, look) {
|
||||
if kind == "car" {
|
||||
this.car(-2.2, -0.9, 2.2, 0.9, look);
|
||||
} else if kind == "bus" {
|
||||
this.box(-5.0, -1.25, 5.0, 1.25, 0.35, 1.6, look);
|
||||
this.box(-4.9, -1.2, 4.9, 1.2, 1.6, 2.6, "glassd");
|
||||
this.box(-5.0, -1.25, 5.0, 1.25, 2.6, 3.0, look);
|
||||
} else if kind == "motorbike" {
|
||||
this.box(-0.95, -0.3, 0.95, 0.3, 0.3, 0.85, look);
|
||||
this.person(0.0, 0.0, 0.55);
|
||||
} else if kind == "cyclist" {
|
||||
this.box(-0.85, -0.05, 0.85, 0.05, 0.3, 0.7, "steel");
|
||||
this.person(0.0, 0.0, 0.5, look);
|
||||
} else if kind == "deer" {
|
||||
let a = animal_parts("deer", "doe");
|
||||
this.draw_parts(a.body + a.up);
|
||||
} else if kind == "elephant" {
|
||||
let a = animal_parts("elephant", "cow");
|
||||
this.draw_parts(a.body + a.up);
|
||||
} else {
|
||||
this.person(0.0, 0.0, 0.0, look);
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- plan helpers
|
||||
|
||||
fn rt_sub(a, b) { [a[0] - b[0], a[1] - b[1]] }
|
||||
fn rt_add(a, b) { [a[0] + b[0], a[1] + b[1]] }
|
||||
fn rt_mul(a, k) { [a[0] * k, a[1] * k] }
|
||||
fn rt_len(a) { (a[0] * a[0] + a[1] * a[1]).sqrt() }
|
||||
fn rt_unit(a) { let l = fmax(rt_len(a), 1e-9); [a[0] / l, a[1] / l] }
|
||||
/// The left normal of a direction.
|
||||
fn rt_left(u) { [-u[1], u[0]] }
|
||||
|
||||
/// The waypoints in the object's frame: its pts, or a 20 m line along local x.
|
||||
/// Points closer than 5 cm to the one before are dropped, and a loop's last point
|
||||
/// when it repeats the first.
|
||||
fn route_pts(o, closed) {
|
||||
let src = if o.pts != () && o.pts.len() >= 2 { o.pts } else { [[-10.0, 0.0], [10.0, 0.0]] };
|
||||
let out = [];
|
||||
for q in src {
|
||||
let p = [q[0] + 0.0, q[1] + 0.0];
|
||||
if out.len() == 0 || rt_len(rt_sub(p, out[out.len() - 1])) > 0.05 { out.push(p); }
|
||||
}
|
||||
if closed && out.len() > 2 && rt_len(rt_sub(out[0], out[out.len() - 1])) <= 0.05 { out.pop(); }
|
||||
out
|
||||
}
|
||||
|
||||
/// The line `d` metres to the left of a path (mitred corners).
|
||||
fn rt_offset(pts, d, closed) {
|
||||
if d == 0.0 { return pts; }
|
||||
let n = pts.len();
|
||||
let out = [];
|
||||
for i in 0..n {
|
||||
let has_prev = closed || i > 0;
|
||||
let has_next = closed || i < n - 1;
|
||||
let n1 = if has_prev { rt_left(rt_unit(rt_sub(pts[i], pts[(i + n - 1) % n]))) } else { () };
|
||||
let n2 = if has_next { rt_left(rt_unit(rt_sub(pts[(i + 1) % n], pts[i]))) } else { () };
|
||||
if type_of(n1) == "()" { n1 = n2; }
|
||||
if type_of(n2) == "()" { n2 = n1; }
|
||||
let s = 1.0 + n1[0] * n2[0] + n1[1] * n2[1];
|
||||
let m = if s < 0.3 { n2 } else { rt_mul(rt_add(n1, n2), 1.0 / s) };
|
||||
out.push(rt_add(pts[i], rt_mul(m, d)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A closed path with its corners rounded to radius r: each corner becomes an
|
||||
/// arc of short straight pieces (at most 45 degrees each).
|
||||
fn rt_round(pts, r) {
|
||||
let n = pts.len();
|
||||
if r <= 0.0 || n < 3 { return pts; }
|
||||
let out = [];
|
||||
for i in 0..n {
|
||||
let p = pts[i];
|
||||
let a = pts[(i + n - 1) % n];
|
||||
let b = pts[(i + 1) % n];
|
||||
let la = rt_len(rt_sub(p, a));
|
||||
let lb = rt_len(rt_sub(b, p));
|
||||
let u = rt_unit(rt_sub(p, a));
|
||||
let v = rt_unit(rt_sub(b, p));
|
||||
let c = fmax(-1.0, fmin(1.0, u[0] * v[0] + u[1] * v[1]));
|
||||
let turn = c.acos();
|
||||
if turn < 0.05 { out.push(p); continue; }
|
||||
let t = fmin(r * (turn / 2.0).tan(), fmin(0.45 * la, 0.45 * lb));
|
||||
let rr = t / (turn / 2.0).tan();
|
||||
let side = if u[0] * v[1] - u[1] * v[0] > 0.0 { 1.0 } else { -1.0 };
|
||||
let p0 = rt_sub(p, rt_mul(u, t));
|
||||
let cc = rt_add(p0, rt_mul(rt_left(u), rr * side));
|
||||
let a0 = (p0[1] - cc[1]).atan(p0[0] - cc[0]);
|
||||
let steps = fmax(1.0, (turn / (PI() / 4.0) - 1e-6).ceiling()).to_int();
|
||||
out.push(p0);
|
||||
for k in 1..=steps {
|
||||
let ang = a0 + side * turn * k / steps;
|
||||
out.push([cc[0] + rr * ang.cos(), cc[1] + rr * ang.sin()]);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The closed path the movers follow, in the object's frame: a loop keeps
|
||||
/// `lane` metres to the left of the drawn line (right when negative); there and
|
||||
/// back runs on each side and turns at the ends. Corners rounded to r.
|
||||
fn route_path(o, spec, lane, r) {
|
||||
let p = o.p;
|
||||
let pts = route_pts(o, p.closed);
|
||||
if pts.len() < 2 { return []; }
|
||||
let path = if p.closed && pts.len() >= 3 {
|
||||
rt_offset(pts, lane, true)
|
||||
} else {
|
||||
// each way keeps to its side, so the two strips never overlap
|
||||
let s = if lane < 0.0 { -1.0 } else { 1.0 };
|
||||
let d = s * fmax(lane.abs(), spec.w / 2.0 + 0.15);
|
||||
let back = pts;
|
||||
back.reverse();
|
||||
rt_offset(pts, d, false) + rt_offset(back, d, false)
|
||||
};
|
||||
rt_round(path, r)
|
||||
}
|
||||
|
||||
/// For movers on one piece: the windows (fractions of the cycle) in which mover
|
||||
/// k is on it, grouped into tracks that never show two at once; movers on one
|
||||
/// track share one drawing, so they must look alike. Returns [#{ look, wins }].
|
||||
fn rt_tracks(kind, colour, n, s0, w) {
|
||||
let gap = 1e-4;
|
||||
let by_look = #{};
|
||||
for k in 0..n {
|
||||
let st = s0 - k.to_float() / n;
|
||||
st -= st.floor();
|
||||
let look = mover_look(kind, colour, k);
|
||||
let key = `l${look}`;
|
||||
let g = if by_look.contains(key) { by_look[key] } else { #{ look: look, wins: [] } };
|
||||
g.wins.push([st, st + w]);
|
||||
by_look[key] = g;
|
||||
}
|
||||
let out = [];
|
||||
for key in by_look.keys() {
|
||||
let g = by_look[key];
|
||||
let ws = g.wins;
|
||||
ws.sort(|a, b| if a[0] < b[0] { -1 } else if a[0] > b[0] { 1 } else { 0 });
|
||||
let tracks = [];
|
||||
for win in ws {
|
||||
let placed = false;
|
||||
for i in 0..tracks.len() {
|
||||
let t = tracks[i];
|
||||
if !placed && win[0] >= t.last + gap && win[1] <= t.first + 1.0 - gap {
|
||||
t.wins.push(win);
|
||||
t.last = win[1];
|
||||
tracks[i] = t;
|
||||
placed = true;
|
||||
}
|
||||
}
|
||||
if !placed { tracks.push(#{ first: win[0], last: win[1], wins: [win] }); }
|
||||
}
|
||||
for t in tracks { out.push(#{ look: g.look, wins: t.wins }); }
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn meta_route() {
|
||||
#{ label: "Route", category: "Routes", icon: "〰", shape: "path",
|
||||
help: "Movers on a path of waypoints: click point after point on the map (double-click or Enter ends it; ending on the first point makes a loop). Cars, buses, motorbikes, cyclists, people, deer or elephants go round the loop, or there and back, keeping to one side of the line.",
|
||||
props: [
|
||||
#{ key: "kind", type: "choice", def: "car", choices: route_kinds(), label: "Movers" },
|
||||
#{ key: "n", type: "int", def: 3, min: 1, max: 30, step: 1, label: "How many" },
|
||||
#{ key: "speed", type: "float", def: 0.0, min: 0.0, max: 40.0, step: 0.1, label: "Speed m/s (0: usual)" },
|
||||
#{ key: "closed", type: "bool", def: false, label: "Loop" },
|
||||
#{ key: "keep", type: "choice", def: "left", choices: ["left", "right"], label: "Keep to" },
|
||||
#{ key: "colour", type: "choice", def: "mixed", choices: route_colours(), label: "Colour" },
|
||||
#{ key: "path", type: "bool", def: true, label: "Show path" },
|
||||
] }
|
||||
}
|
||||
|
||||
fn prefab_route(o) {
|
||||
let p = o.p;
|
||||
let spec = mover_spec(p.kind);
|
||||
let v = if p.speed > 0.0 { p.speed } else { spec.v };
|
||||
let lane = if p.keep == "right" { -spec.lane } else { spec.lane };
|
||||
let pts = route_pts(o, p.closed);
|
||||
if p.path && pts.len() >= 2 {
|
||||
layer("ground");
|
||||
let line = pts;
|
||||
if p.closed && pts.len() >= 3 { line.push(pts[0]); }
|
||||
sprite().polyline(line, 0.03, "route");
|
||||
layer("main");
|
||||
}
|
||||
let path = route_path(o, spec, lane, spec.r);
|
||||
let n = path.len();
|
||||
if n < 2 { return; }
|
||||
// pieces, their lengths and where each starts along the loop
|
||||
let segs = [];
|
||||
let total = 0.0;
|
||||
for i in 0..n {
|
||||
let a = path[i];
|
||||
let b = path[(i + 1) % n];
|
||||
let len = rt_len(rt_sub(b, a));
|
||||
if len > 1e-3 { segs.push(#{ a: a, b: b, len: len, at: total }); total += len; }
|
||||
}
|
||||
if segs.len() < 2 || total <= 0.0 { return; }
|
||||
let dur = total / v;
|
||||
let m = segs.len();
|
||||
// the sort strip of each piece, mitred at both ends
|
||||
let hw = spec.w / 2.0 + 0.1;
|
||||
let mitre = [];
|
||||
for i in 0..m {
|
||||
let n1 = rt_left(rt_unit(rt_sub(segs[(i + m - 1) % m].b, segs[(i + m - 1) % m].a)));
|
||||
let n2 = rt_left(rt_unit(rt_sub(segs[i].b, segs[i].a)));
|
||||
let s = 1.0 + n1[0] * n2[0] + n1[1] * n2[1];
|
||||
mitre.push(if s < 0.3 { n2 } else { rt_mul(rt_add(n1, n2), 1.0 / s) });
|
||||
}
|
||||
for i in 0..m {
|
||||
let sg = segs[i];
|
||||
let u = rt_unit(rt_sub(sg.b, sg.a));
|
||||
let ang = u[1].atan(u[0]);
|
||||
let w = world_of(o, sg.a[0], sg.a[1]);
|
||||
frame(w[0], w[1], o.z, o.rot + ang * 180.0 / PI());
|
||||
let s = sprite();
|
||||
for t in rt_tracks(p.kind, p.colour, p.n, sg.at / total, sg.len / total) {
|
||||
s.travel([0.0, 0.0, 0.0], [sg.len, 0.0, 0.0], dur, t.wins);
|
||||
s.mover(p.kind, t.look);
|
||||
s.end();
|
||||
}
|
||||
// sort by the strip only: the start drawing is never seen standing there
|
||||
s.ext_reset();
|
||||
let ends = [[sg.a, mitre[i]], [sg.b, mitre[(i + 1) % m]]];
|
||||
for e in ends {
|
||||
for side in [1.0, -1.0] {
|
||||
let q = rt_sub(rt_add(e[0], rt_mul(e[1], side * hw)), sg.a);
|
||||
let lx = q[0] * u[0] + q[1] * u[1];
|
||||
let ly = -q[0] * u[1] + q[1] * u[0];
|
||||
s.ext(lx, ly, 0.0);
|
||||
s.ext(lx, ly, spec.h);
|
||||
}
|
||||
}
|
||||
}
|
||||
frame(o);
|
||||
let noun = if p.n == 1 { spec.one } else { spec.many };
|
||||
info_row("Movers", `${p.n} ${noun}, ${num1(v)} m/s, round in ${num1(dur)} s`);
|
||||
let base = 0.0;
|
||||
for i in 1..pts.len() { base += rt_len(rt_sub(pts[i], pts[i - 1])); }
|
||||
if p.closed && pts.len() >= 3 { base += rt_len(rt_sub(pts[0], pts[pts.len() - 1])); }
|
||||
info_row("Route", if p.closed && pts.len() >= 3 { `loop, ${num1(base)} m` } else { `there and back, ${num1(base)} m each way` });
|
||||
}
|
||||
@@ -272,6 +272,8 @@ mach3 = "#5b6370"
|
||||
mach4 = "#b8692b"
|
||||
robot = "#e07a1f"
|
||||
robotb = "#3a3f48"
|
||||
# route paths
|
||||
route = "#e0b84a"
|
||||
# wildlife and garden
|
||||
deer = "#8a5a32"
|
||||
deerl = "#d6c4a6"
|
||||
@@ -581,6 +583,7 @@ mach3 = "#8a93a1"
|
||||
mach4 = "#e08a43"
|
||||
robot = "#f08c2b"
|
||||
robotb = "#5a616d"
|
||||
route = "#b0802a"
|
||||
deer = "#a8703f"
|
||||
deerl = "#ecdfc8"
|
||||
nose = "#3a302a"
|
||||
@@ -918,6 +921,7 @@ f-conc = "conc"
|
||||
".iso-svg .iso-win" = "fill: var(--iso-win); stroke: none"
|
||||
".iso-svg .iso-school" = "pointer-events: none"
|
||||
".iso-svg .iso-swimbox" = "fill: none; stroke: none"
|
||||
".iso-svg .iso-route" = "fill: none; stroke: var(--iso-route); stroke-width: 1.2; stroke-dasharray: 5 4; stroke-linecap: round; stroke-linejoin: round; opacity: .75"
|
||||
".iso-svg .iso-antler" = "fill: none; stroke: var(--iso-antler); stroke-width: .35; stroke-linecap: round; stroke-linejoin: round"
|
||||
".iso-svg .iso-bristle" = "fill: none; stroke: var(--iso-bristle); stroke-width: .5; stroke-linecap: round"
|
||||
".iso-svg .iso-bird, .iso-svg .iso-wing" = "stroke: none"
|
||||
|
||||
Reference in new issue
Block a user