Files
isokit/crates/isokit-script/stdlib/industry.rhai
T
grabowski f49798583e
Deploy web Studio / deploy (push) Successful in 1m3s
Robot arms pass goods from belt to belt; native beam primitive
- Robot arm (category Industry): picks goods off the end of the nearest conveyor within its reach
  and puts them on the start of another conveyor within reach, or on a pallet a quarter turn on
  (pick / put: auto or a side). One move per item coming in (cycle 0) or a set cycle; waits while
  the line before it stands. The belt it feeds carries its goods at its pace and starts its loop
  when they land (feeds carry a time offset `ph`), so arm and belts stay in step.
- Drawn as a 16-pose flipbook: two-link inverse kinematics per pose, each pose shown for a
  sixteenth of the cycle by a stepped CSS animation (iso-flip; pose 0 alone when unanimated).
  Poses pointing away from the viewer are drawn before the base, the others after it.
- New sprite method beam(a, b, n, w, mat): a square bar between two 3D points, six faces culled
  and shaded like face(). Native, so the factory demo renders in 4.7 ms instead of 18.8 ms.
- Factory demo: a palletiser at the end of the carton line, a loading robot passing drums from the
  incline onto the loading belt into the truck. MCP instructions on robot arms.

Checks: 70 Rust tests (arms and belts in step, waiting arm, beam faces from every side),
Studio 109/109, web 90/90.
2026-10-24 22:05:46 +07:00

460 lines
22 KiB
Plaintext

// isokit/industry: conveyors, producers (machines that make goods at a set
// rate) and robot arms. Goods flow from a producer onto the conveyor starting
// at its output and on along the conveyors that start where another one ends:
// each belt carries them at its own speed, so the gap between them is
// speed / rate. A robot arm picks them off the end of a belt in its reach and
// puts them on the start of another belt, or on a pallet.
fn goods_kinds() { ["carton", "crate", "drum", "sack", "part"] }
/// x for a tooltip: whole numbers as they are, others to one decimal.
fn num1(x) {
let r = round1(x);
if (r - r.round()).abs() < 1e-9 { `${r.round().to_int()}` } else { `${r}` }
}
/// Half the length of one of the goods along the belt (at full size).
fn goods_half(kind) {
if kind == "crate" || kind == "sack" { 0.3 } else if kind == "part" { 0.22 } else { 0.25 }
}
/// Height of one of the goods (at full size).
fn goods_h(kind) {
if kind == "crate" { 0.36 } else if kind == "drum" { 0.6 } else if kind == "sack" { 0.16 } else if kind == "part" { 0.26 } else { 0.32 }
}
/// One of the goods standing at (x, y, z), its size scaled by k.
fn goods(kind, x, y, z, k) {
if kind == "crate" {
this.box(x - 0.3 * k, y - 0.25 * k, x + 0.3 * k, y + 0.25 * k, z, z + 0.36 * k, "crate");
} else if kind == "drum" {
this.cyl(x, y, 0.24 * k, z, z + 0.6 * k, "drum");
} else if kind == "sack" {
this.box(x - 0.3 * k, y - 0.2 * k, x + 0.3 * k, y + 0.2 * k, z, z + 0.16 * k, "sacks");
} else if kind == "part" {
this.box(x - 0.22 * k, y - 0.22 * k, x + 0.22 * k, y + 0.22 * k, z, z + 0.1 * k, "steel");
this.cyl(x, y, 0.1 * k, z + 0.1 * k, z + 0.26 * k, "steel");
} else {
this.box(x - 0.25 * k, y - 0.2 * k, x + 0.25 * k, y + 0.2 * k, z, z + 0.32 * k, "carton");
}
this
}
/// Height of conveyor p's belt top at u along it (it rises by p.rise from
/// its start to its end).
fn belt_z(p, u) { p.h + p.rise * (u + p.w / 2.0) / p.w }
/// How far conveyor o reaches past its end e (-1 start, 1 end). Alone or on
/// a straight run it ends there. Where another conveyor crosses the end
/// point, the corner square goes to the one that carries on: a belt feeding
/// another one's start stops at that belt's side, the belt taking goods
/// from another's end runs through; at a T it stops at the side.
fn conveyor_ext(o, e) {
let p = o.p;
let u = e * p.w / 2.0;
let pt = world_of(o, u, 0.0);
let zt = o.z + belt_z(p, u);
for c in objects_on(["conveyor"], pt[0], pt[1], 0.05) {
if c.id == o.id || along_world_x(c.rot) == along_world_x(o.rot) { continue; }
if (c.z + belt_z(c.p, c.u) - zt).abs() > 0.3 { continue; }
let at_start = (c.u + c.p.w / 2.0).abs() < 0.05;
let at_end = (c.u - c.p.w / 2.0).abs() < 0.05;
let half = c.p.d / 2.0;
if !at_start && !at_end { return -half; }
if e > 0.0 && at_start { return -half; }
if e < 0.0 && at_end { return half; }
// two ends or two starts meet: the one along world x runs through
return if along_world_x(o.rot) { half } else { -half };
}
0.0
}
/// What reaches conveyor o at its start: #{ rate, item, running, from, ph }
/// (rate in goods per second; ph: seconds into the loop at which goods land,
/// so a belt after a robot arm runs in step with it) from the producer whose
/// output side it starts at, a robot arm putting goods on it, or else the
/// conveyor ending there (followed back up to 12 belts and arms); () when
/// nothing feeds it.
fn conveyor_feed(o, hops) {
if hops > 12 { return (); }
let p = o.p;
let a = world_of(o, -p.w / 2.0 - 0.15, 0.0);
for m in objects_at(["producer"], a[0], a[1]) {
if (m.z - o.z).abs() > 0.5 { continue; }
if local_of(m, a[0], a[1])[0] > m.p.w / 2.0 - 1.0 {
return #{ rate: m.p.rate / 60.0, item: m.p.product, running: m.p.running, from: m.id, ph: 0.0 };
}
}
let s = world_of(o, -p.w / 2.0, 0.0);
for m in objects_of("robot_arm") {
let dx = m.x - s[0];
let dy = m.y - s[1];
if dx * dx + dy * dy > (m.p.reach + 1.0) * (m.p.reach + 1.0) { continue; }
let t = arm_targets(m, hops + 1);
if t.put.belt == o.id {
return #{ rate: 1.0 / t.cycle, item: t.kind, running: t.running, from: m.id, ph: t.ph + t.cycle * 9.0 / 16.0 };
}
}
let zs = o.z + belt_z(p, -p.w / 2.0);
for c in objects_of("conveyor") {
if c.id == o.id { continue; }
let e = world_of(c, c.p.w / 2.0, 0.0);
let lim = fmax(p.d, c.p.d) / 2.0 + 0.05;
if (e[0] - s[0]).abs() > lim || (e[1] - s[1]).abs() > lim { continue; }
if (c.z + belt_z(c.p, c.p.w / 2.0) - zs).abs() > 0.3 { continue; }
let f = conveyor_feed(c, hops + 1);
if type_of(f) == "map" {
return #{ rate: f.rate, item: f.item, running: f.running && c.p.running, from: c.id, ph: f.ph };
}
return #{ rate: c.p.speed / c.p.gap, item: c.p.item, running: c.p.running, from: c.id, ph: 0.0 };
}
()
}
fn meta_conveyor() {
#{ label: "Conveyor", category: "Industry", icon: "⇉", shape: "line", directed: true,
help: "Carries goods along local +x (drawn from start to end; turn it 180° to reverse). Started at a producer's output it carries what the producer makes, as fast as it makes them; a conveyor starting where another ends carries on with its goods.",
props: [
#{ key: "w", type: "float", def: 6.0, min: 1.0, max: 30.0, step: 0.5, label: "Length (m)" },
#{ key: "d", type: "float", def: 0.8, min: 0.3, max: 2.5, step: 0.05, label: "Belt width (m)" },
#{ key: "h", type: "float", def: 0.8, min: 0.2, max: 4.0, step: 0.05, label: "Height (m)" },
#{ key: "rise", type: "float", def: 0.0, min: -6.0, max: 6.0, step: 0.1, label: "Rise (m)" },
#{ key: "running", type: "bool", def: true, label: "Running" },
#{ key: "speed", type: "float", def: 0.5, min: 0.05, max: 3.0, step: 0.05, label: "Speed (m/s)" },
#{ key: "item", type: "choice", def: "carton", choices: goods_kinds() + ["none"], label: "Goods" },
#{ key: "gap", type: "float", def: 1.5, min: 0.5, max: 20.0, step: 0.1, label: "Spacing (m)" },
] }
}
fn prefab_conveyor(o) {
let p = o.p;
let x0 = -p.w / 2.0 - conveyor_ext(o, -1.0);
let x1 = p.w / 2.0 + conveyor_ext(o, 1.0);
if x1 - x0 < 0.1 { return; }
let W = p.d / 2.0;
let th = 0.14;
let za = belt_z(p, x0);
let zb = belt_z(p, x1);
// the side turned to the viewer (-1: -y), drawn after the goods
let ny = if vis(0.0, -1.0) { -1.0 } else { 1.0 };
let s = sprite();
// legs in pairs, at most 2.5 m apart; the far row first
let n = fmax(1.0, ((x1 - x0) / 2.5).ceiling()).to_int();
for side in [-ny, ny] {
let y = side * (W - 0.06);
for i in 0..(n + 1) {
let u = fmin(fmax(x0 + i * (x1 - x0) / n, x0 + 0.08), x1 - 0.08);
let zl = belt_z(p, u) - th;
if zl > 0.05 { s.box(u - 0.04, y - 0.04, u + 0.04, y + 0.04, 0.0, zl, "convleg"); }
}
}
// the drive motor under the head end, on the -y side
let motor = |q| q.box(x1 - 0.55, -W - 0.32, x1 - 0.12, -W, zb - th - 0.36, zb - th + 0.04, "motor");
if ny > 0.0 && zb - th > 0.45 { motor.call(s); }
// frame and belt
s.prism([[x0, -W], [x1, -W], [x1, W], [x0, W]], [za - th, zb - th, zb - th, za - th], [za, zb, zb, za], "conv", #{ top: "belt" });
let rail = |q, y0, y1| q.prism([[x0, y0], [x1, y0], [x1, y1], [x0, y1]], [za, zb, zb, za], [za + 0.07, zb + 0.07, zb + 0.07, za + 0.07], "conv");
let far = if ny < 0.0 { [W - 0.04, W] } else { [-W, -W + 0.04] };
let near = if ny < 0.0 { [-W, -W + 0.04] } else { [W - 0.04, W] };
rail.call(s, far[0], far[1]);
// goods: what the feed brings (or the belt's own choice), spaced speed / rate
let feed = conveyor_feed(o, 0);
let fed = type_of(feed) == "map";
let kind = if fed { feed.item } else { p.item };
let rate = if fed { feed.rate } else { p.speed / p.gap };
let moving = p.running && (!fed || feed.running);
let stopped = if p.running { "" } else { " (stopped)" };
info_row("Speed", `${num1(p.speed)} m/s${stopped}`);
if kind != "none" && rate > 0.0 {
let k = fmin(1.0, (p.d - 0.1) / 0.5);
let hl = goods_half(kind) * k;
let want = p.speed / rate;
let gap = fmax(want, 2.0 * hl + 0.12);
let src = if fed { ` (from ${feed.from})` } else { "" };
info_row("Carries", `${num1(rate * 60.0)} ${kind}s a minute, ${num1(gap)} m apart${src}`);
if want < gap - 1e-6 { info_row("Backs up", "goods come faster than this belt can space them"); }
let lift = (p.rise / p.w).abs() * hl + 0.01;
let u0 = x0 + hl + 0.06;
let span = x1 - hl - 0.06 - u0;
let cnt = (span / gap).floor().to_int();
let step = if cnt < 1 { span } else { gap };
cnt = if cnt < 1 { 1 } else { cnt };
if span > 0.0 {
// back to front along the belt
let order = [];
for i in 0..cnt { order.push(i); }
if !vis(1.0, 0.0) { order.reverse(); }
if moving {
let per = step / p.speed;
let v = dv(step, 0.0, p.rise * step / p.w);
// the loop starts when goods land (ph seconds in)
let ph = if fed { feed.ph } else { 0.0 };
let late = ((per - ph) % per + per) % per;
let at = `animation-duration:${per}s;animation-delay:-${late}s`;
s.begin("a ride", `--dx:${v[0]}px;--dy:${v[1]}px;${at}`);
for i in order {
let u = u0 + i * step;
let fade = if cnt == 1 { "rideio" } else if i == 0 { "ridein" } else if i == cnt - 1 { "rideout" } else { "" };
if fade != "" { s.begin("a " + fade, at); }
s.goods(kind, u, 0.0, belt_z(p, u) + lift, k);
if fade != "" { s.end(); }
}
s.end();
} else {
for i in order {
let u = u0 + (i + 0.5) * step;
if u < x1 - hl - 0.05 { s.goods(kind, u, 0.0, belt_z(p, u) + lift, k); }
}
}
}
}
rail.call(s, near[0], near[1]);
if ny < 0.0 && zb - th > 0.45 { motor.call(s); }
}
fn meta_producer() {
#{ label: "Producer", category: "Industry", icon: "⚙", shape: "rect",
help: "A press that makes one item every 60 / rate seconds and puts it out on its +x side. A conveyor starting there carries the goods away, spaced by its speed and this rate.",
props: [
#{ key: "rate", type: "float", def: 12.0, min: 0.5, max: 240.0, step: 0.5, label: "Rate (per min)" },
#{ key: "product", type: "choice", def: "carton", choices: goods_kinds(), label: "Product" },
#{ key: "running", type: "bool", def: true, label: "Running" },
#{ key: "w", type: "float", def: 3.2, min: 1.6, max: 20.0, step: 0.1, label: "Length (m)" },
#{ key: "d", type: "float", def: 2.2, min: 1.2, max: 12.0, step: 0.1, label: "Width (m)" },
#{ key: "h", type: "float", def: 2.6, min: 1.6, max: 8.0, step: 0.1, label: "Height (m)" },
#{ key: "out", type: "float", def: 0.8, min: 0.2, max: 4.0, step: 0.05, label: "Output height" },
#{ key: "colour", type: "choice", def: "mach1", choices: ["mach1", "mach2", "mach3", "mach4"], label: "Colour" },
] }
}
fn prefab_producer(o) {
let p = o.p;
let L = p.w / 2.0;
let D = p.d / 2.0;
let t = fmin(p.out, p.h - 1.2);
let per = 60.0 / p.rate;
let mat = p.colour;
// the press sits over the table at the output end, the control cabinet behind it
let cx = -L + fmin(1.0, p.w * 0.3);
let px = (cx + L) / 2.0;
let ct = p.h - 0.35;
let k = fmin(1.0, (fmin(L - cx, p.d) - 0.4) / 0.6);
// the ram hangs under the crown and comes down to just above the item
let rh = fmin(0.3, (ct - t) * 0.25);
let rest = ct - 0.04 - rh;
let stroke = fmax(0.0, rest - (t + goods_h(p.product) * k) - 0.05);
let stopped = if p.running { "" } else { " (stopped)" };
info_row("Makes", `${num1(p.rate)} ${p.product}s a minute, one every ${num1(per)} s${stopped}`);
let s = sprite();
s.box(-L, -D, L, D, 0.0, t, mat);
s.box(-L, -D, cx, D, t, p.h, mat);
s.circle(cx - 0.25, -D + 0.25, p.h + 0.02, 1.2, if p.running { "led ledg" } else { "led ledr" });
// columns: the far pair, then the ram over the item, then the near pair
let ny = if vis(0.0, -1.0) { -1.0 } else { 1.0 };
let post = |q, y| {
let yc = y * (D - 0.12);
q.box(cx + 0.02, yc - 0.1, cx + 0.22, yc + 0.1, t, ct, "steel");
q.box(L - 0.22, yc - 0.1, L - 0.02, yc + 0.1, t, ct, "steel");
};
post.call(s, -ny);
s.goods(p.product, px, 0.0, t, k);
if p.running {
let v = dv(0.0, 0.0, -stroke);
s.begin("a ram", `--dx:${v[0]}px;--dy:${v[1]}px;animation-duration:${per}s`);
}
// the rod runs up into the hydraulic cylinder on the crown
s.cyl(px, 0.0, 0.07, rest + rh, p.h - 0.05 + stroke, "steel");
s.box(px - 0.32, -0.32, px + 0.32, 0.32, rest, rest + rh, "press");
if p.running { s.end(); }
post.call(s, ny);
s.box(cx, -D, L, D, ct, p.h, mat);
s.cyl(px, 0.0, 0.2, p.h, p.h + stroke + 0.15, "press");
}
// ---------------------------------------------------------- robot arms
fn v3(a, b, k) { [a[0] + b[0] * k, a[1] + b[1] * k, a[2] + b[2] * k] }
/// Angle (radians) of the local plan point (x, y) seen from the origin.
fn plan_angle(x, y) {
let r = (x * x + y * y).sqrt();
if r < 1e-9 { return 0.0; }
let a = clamp(x / r, -1.0, 1.0).acos();
if y < 0.0 { -a } else { a }
}
fn side_angle(side) {
if side == "+y" { PI() / 2.0 } else if side == "-x" { PI() } else if side == "-y" { -PI() / 2.0 } else { 0.0 }
}
/// Where robot arm o picks and puts goods: #{ pick, put } each #{ r, a, z,
/// belt } (distance, angle and height in o's frame; the belt id or ""), the
/// kind of goods, its cycle (s: Cycle, else one per item coming in) and
/// whether it runs (it waits while the line before it stands).
fn arm_targets(o, hops) {
let p = o.p;
let pick = ();
let put = ();
let kind = "carton";
let rate = 0.0;
let fed = true;
let ph = 0.0;
let best = 1e9;
if p.pick == "auto" {
for c in objects_of("conveyor") {
let e = world_of(c, c.p.w / 2.0 - 0.35, 0.0);
let l = local_of(o, e[0], e[1]);
let d = (l[0] * l[0] + l[1] * l[1]).sqrt();
if d < 0.6 || d > p.reach + 0.4 || d >= best { continue; }
best = d;
pick = #{ r: d, a: plan_angle(l[0], l[1]), z: c.z + belt_z(c.p, c.p.w / 2.0) - o.z, belt: c.id };
let f = if hops > 12 { () } else { conveyor_feed(c, hops + 1) };
if type_of(f) == "map" { kind = f.item; rate = f.rate; fed = f.running && c.p.running; ph = f.ph; } else { kind = c.p.item; rate = c.p.speed / c.p.gap; fed = c.p.running; ph = 0.0; }
}
}
if type_of(pick) != "map" {
let side = if p.pick == "auto" { "-x" } else { p.pick };
pick = #{ r: p.reach * 0.75, a: side_angle(side), z: 0.8, belt: "" };
kind = p.goods;
}
if kind == "none" { kind = p.goods; }
best = 1e9;
if p.put == "auto" {
for c in objects_of("conveyor") {
if c.id == pick.belt { continue; }
let e = world_of(c, -c.p.w / 2.0 + 0.35, 0.0);
let l = local_of(o, e[0], e[1]);
let d = (l[0] * l[0] + l[1] * l[1]).sqrt();
if d < 0.6 || d > p.reach + 0.4 || d >= best { continue; }
best = d;
put = #{ r: d, a: plan_angle(l[0], l[1]), z: c.z + belt_z(c.p, -c.p.w / 2.0) - o.z, belt: c.id };
}
}
if type_of(put) != "map" {
let a = if p.put == "auto" { pick.a + PI() / 2.0 } else { side_angle(p.put) };
put = #{ r: p.reach * 0.75, a: a, z: 0.14, belt: "" };
}
let cycle = if p.cycle > 0.0 { p.cycle } else if rate > 0.0 { 1.0 / rate } else { 4.0 };
#{ pick: pick, put: put, kind: kind, cycle: clamp(cycle, 1.5, 120.0), running: p.running && fed, ph: ph }
}
/// One pose of the arm (links al long, shoulder 1.2 m up): the gripper's tip
/// at distance r, angle a, height zt, holding an item of kind unless kind is
/// "". The parts are drawn outer ones last when the arm points at the viewer.
fn arm_pose(r, a, zt, kind, k, mat, al) {
let zs = 1.2;
let a1 = al;
let a2 = al;
let wr = r;
let wz = zt + 0.32;
let dz = wz - zs;
let d = fmin((wr * wr + dz * dz).sqrt(), a1 + a2 - 0.01);
let phi = if d < 1e-6 { 0.0 } else { let q = clamp(wr / d, -1.0, 1.0).acos(); if dz < 0.0 { -q } else { q } };
let al = clamp((a1 * a1 + d * d - a2 * a2) / (2.0 * a1 * d), -1.0, 1.0).acos();
let sh = phi + al;
let er = a1 * sh.cos();
let ez = zs + a1 * sh.sin();
let ca = a.cos();
let sa = a.sin();
let n = [-sa, ca, 0.0];
let S = [0.0, 0.0, zs];
let E = [ca * er, sa * er, ez];
let W = [ca * wr, sa * wr, wz];
let T = [ca * wr, sa * wr, zt];
let parts = [0, 1, 2];
if !vis(ca, sa) { parts.reverse(); }
for part in parts {
if part == 0 {
this.beam(S, E, n, 0.26, mat);
this.beam(v3(E, n, -0.17), v3(E, n, 0.17), [ca, sa, 0.0], 0.3, "robotb");
} else if part == 1 {
this.beam(E, W, n, 0.2, mat);
} else {
if kind != "" { this.goods(kind, T[0], T[1], zt - goods_h(kind) * k, k); }
this.beam(W, T, n, 0.12, "robotb");
this.beam(v3(T, n, -0.16), v3(T, n, 0.16), [ca, sa, 0.0], 0.07, "grip");
}
}
this
}
fn meta_robot_arm() {
#{ label: "Robot arm", category: "Industry", icon: "⟟",
help: "Picks goods off the end of the nearest conveyor in its reach and puts them on the start of another one, or on a pallet beside it (Pick from / Put on: auto, or a side of the arm). With Cycle 0 it keeps pace with the goods.",
props: [
#{ key: "reach", type: "float", def: 2.0, min: 1.0, max: 4.0, step: 0.1, label: "Reach (m)" },
#{ key: "pick", type: "choice", def: "auto", choices: ["auto", "+x", "+y", "-x", "-y"], label: "Pick from" },
#{ key: "put", type: "choice", def: "auto", choices: ["auto", "+x", "+y", "-x", "-y"], label: "Put on" },
#{ key: "cycle", type: "float", def: 0.0, min: 0.0, max: 60.0, step: 0.5, label: "Cycle (s)" },
#{ key: "goods", type: "choice", def: "carton", choices: goods_kinds(), label: "Goods" },
#{ key: "running", type: "bool", def: true, label: "Running" },
#{ key: "colour", type: "choice", def: "robot", choices: ["robot", "mach2", "mach3", "mach1"], label: "Colour" },
] }
}
fn prefab_robot_arm(o) {
let p = o.p;
let t = arm_targets(o, 0);
let run = t.running;
let k = 0.9;
let gh = goods_h(t.kind) * k;
let pk = t.pick;
let pt = t.put;
if pt.belt == "" { pt.z = 0.14 + gh; }
let cycle = t.cycle;
let al = (p.reach + 0.6) / 2.0;
let da = pt.a - pk.a;
while da > PI() { da -= 2.0 * PI(); }
while da < -PI() { da += 2.0 * PI(); }
let pick_from = if pk.belt == "" { "its " + p.pick + " side" } else { pk.belt };
let put_on = if pt.belt == "" { "a pallet" } else { pt.belt };
let stopped = if p.running { if run { "" } else { " (waiting: the line before it stands)" } } else { " (stopped)" };
info_row("Moves", `${t.kind}s from ${pick_from} to ${put_on}`);
info_row("Cycle", `${num1(cycle)} s${stopped}`);
// the poses over one cycle: [turn from pick to put (0..1), lift (0..1), holding]
let F = [[0.0, 0.0, true], [0.0, 0.5, true], [0.0, 1.0, true], [0.2, 1.0, true],
[0.45, 1.0, true], [0.7, 1.0, true], [0.9, 1.0, true], [1.0, 1.0, true],
[1.0, 0.4, true], [1.0, 0.0, false], [1.0, 0.6, false], [0.8, 1.0, false],
[0.55, 1.0, false], [0.3, 1.0, false], [0.1, 1.0, false], [0.0, 0.5, false]];
let N = F.len();
let ptx = pt.r * pt.a.cos();
let pty = pt.r * pt.a.sin();
// poses pointing away from the viewer go behind the base, the others in front
let behind = [];
let front = [];
for i in 0..N {
if !run && i != 2 { continue; }
let te = F[i][0] * F[i][0] * (3.0 - 2.0 * F[i][0]);
let a = pk.a + da * te;
if vis(a.cos(), a.sin()) { front.push(i); } else { behind.push(i); }
}
let s = sprite();
// a pallet with a layer of goods where it puts them, when not on a belt
if pt.belt == "" {
s.box(ptx - 0.6, pty - 0.5, ptx + 0.6, pty + 0.5, 0.0, 0.14, "crate");
let layer = [[ptx - 0.3, pty - 0.25], [ptx + 0.3, pty - 0.25], [ptx - 0.3, pty + 0.25], [ptx + 0.3, pty + 0.25]];
layer.sort(|a, b| if depth(a[0], a[1]) < depth(b[0], b[1]) { -1 } else { 1 });
for q in layer { s.goods(t.kind, q[0], q[1], 0.14, k * 0.85); }
}
for side in [0, 1] {
if side == 1 {
s.box(-0.45, -0.45, 0.45, 0.45, 0.0, 0.25, "robotb");
s.cyl(0.0, 0.0, 0.33, 0.25, 0.95, p.colour);
s.cyl(0.0, 0.0, 0.27, 0.95, 1.2, "robotb");
}
for i in if side == 0 { behind } else { front } {
let f = F[i];
let te = f[0] * f[0] * (3.0 - 2.0 * f[0]);
let a = pk.a + da * te;
let r = pk.r + (pt.r - pk.r) * te;
let z = pk.z + (pt.z - pk.z) * te;
if run {
let cls = if i == 0 { "a flip pose0" } else { "a flip" };
// pose 0 (the grip) when the last item on the pick belt goes (t.ph seconds in)
let delay = (((N - i) % N) * cycle / N - t.ph % cycle + cycle) % cycle;
s.begin(cls, `animation-duration:${cycle}s;animation-delay:-${delay}s`);
}
// put down on a pallet: the item stays there until the next one comes
if !f[2] && pt.belt == "" { s.goods(t.kind, ptx, pty, pt.z - gh, k); }
s.arm_pose(r, a, z + gh + f[1] * 0.5, if f[2] { t.kind } else { "" }, k, p.colour, al);
if run { s.end(); }
}
}
}