Robot arms pass goods from belt to belt; native beam primitive
Deploy web Studio / deploy (push) Successful in 1m3s

- 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.
This commit is contained in:
grabowski committed 2026-10-24 22:05:46 +07:00
1 parent 18dd324543
commit f49798583e
14 files changed
+460 -31

No files matched your search

+89
View File
@@ -643,6 +643,95 @@ fn conveyors_carry_what_the_producer_makes() {
assert_eq!(row("belt3", "Speed"), "0.8 m/s");
}
fn robot_doc(belt1_running: bool) -> Doc {
let mut doc = Doc::default();
for (id, kind, x, y, rot, props) in [
("press1", "producer", -6.0, -4.0, 0.0, json!({"rate": 15})),
(
"belt1",
"conveyor",
-1.4,
-4.0,
0.0,
json!({"w": 6, "running": belt1_running}),
),
("robot1", "robot_arm", 3.2, -4.0, 0.0, json!({})),
("belt2", "conveyor", 3.2, 1.0, 90.0, json!({"w": 6})),
(
"robot2",
"robot_arm",
3.2,
5.8,
0.0,
json!({"colour": "mach2"}),
),
(
"robot3",
"robot_arm",
-6.0,
5.0,
0.0,
json!({"running": false, "pick": "+x", "put": "-y", "goods": "drum"}),
),
] {
let mut o = obj(id, kind, x, y, props);
o.rot = rot;
doc.objects.push(o);
}
doc
}
#[test]
fn robot_arms_pass_goods_from_belt_to_belt() {
let r = Runtime::new(&Bundle::default()).unwrap();
let doc = robot_doc(true);
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");
assert!(!out.html.contains("NaN"));
// two running arms with 16 poses each, one shown at a time; the stopped one stands still
assert_eq!(
out.html.matches("iso-a iso-flip").count(),
32,
"view {view}"
);
assert_eq!(
out.html.matches("iso-flip iso-pose0").count(),
2,
"view {view}"
);
}
let b = r.build("site", 1, &cfg(), &doc).unwrap();
let out = render(&b.scene, 1, &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()
};
assert_eq!(row("robot1", "Moves"), "cartons from belt1 to belt2");
assert_eq!(row("robot1", "Cycle"), "4 s");
// the belt after the arm carries its goods at its pace, starting its loop when they land
assert_eq!(
row("belt2", "Carries"),
"15 cartons a minute, 2 m apart (from robot1)"
);
assert!(out
.html
.contains("animation-duration:4.0s;animation-delay:-1.75s"));
assert_eq!(row("robot2", "Moves"), "cartons from belt2 to a pallet");
assert_eq!(row("robot3", "Cycle"), "4 s (stopped)");
// a stopped belt before it: the arm waits
let b = r.build("site", 1, &cfg(), &robot_doc(false)).unwrap();
let out = render(&b.scene, 1, &RenderOpts::default());
assert_eq!(out.html.matches("iso-a iso-flip").count(), 0);
assert!(out.info["robot1"].rows[1].1.contains("waiting"));
}
#[test]
fn site_walls_join_at_corners() {
let r = Runtime::new(&Bundle::default()).unwrap();