// 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` }); }