Robot arms pass goods from belt to belt; native beam primitive
Deploy web Studio / deploy (push) Successful in 1m3s
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:
1 parent
18dd324543
commit
f49798583e
14 files changed
+460
-31
No files matched your search
@@ -40,7 +40,7 @@ isokit build C:/dev/myproject/site-view # dist/index.html, no server needed
|
||||
|
||||
Try the demos: `isokit studio examples/demo` (a works site),
|
||||
`isokit studio examples/house` (a furnished house built from parts) and
|
||||
`isokit studio examples/factory` (presses and conveyors).
|
||||
`isokit studio examples/factory` (presses, conveyors and robot arms).
|
||||
|
||||
### Studio (for everyone)
|
||||
|
||||
@@ -196,8 +196,10 @@ storey.
|
||||
Production lines: a Producer (category Industry) makes goods at a set rate
|
||||
and puts them out on its +x side; Conveyors drawn from there carry them on,
|
||||
round corners and up inclines, each at its own speed, so the gap between goods
|
||||
follows rate and speed. Tooltips show the numbers. `examples/factory` (Factory
|
||||
demo in the web Studio) has a few lines.
|
||||
follows rate and speed. A Robot arm placed past a belt's end picks the goods
|
||||
off and puts them on the next belt or a pallet, in step with them. Tooltips
|
||||
show the numbers. `examples/factory` (Factory demo in the web Studio) has a
|
||||
few lines.
|
||||
|
||||
Animations (fans, smoke, fish, traffic) step on about ten times a second by
|
||||
default ("light"): running CSS animations repaint the whole drawing every frame,
|
||||
@@ -231,7 +233,7 @@ studio/ the Studio GUI (served by `isokit studio`, and by the web
|
||||
web/ web Studio shell: WASM backend for studio.js, projects, folders, zip, code editor
|
||||
examples/demo demo project (works site)
|
||||
examples/house house built from parts (walls, windows, doors, floors, furniture, roof)
|
||||
examples/factory production yard: producers and conveyors
|
||||
examples/factory production yard: producers, conveyors, robot arms
|
||||
deploy/Caddyfile the web Studio's vhost on the buildfor.life web server (s1)
|
||||
```
|
||||
|
||||
|
||||
@@ -870,6 +870,35 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beams_show_three_faces_from_any_side() {
|
||||
for (a, b, n) in [
|
||||
([0.0, 0.0, 1.0], [4.0, 0.0, 1.0], [0.0, 1.0, 0.0]),
|
||||
([0.0, 0.0, 1.0], [0.0, -4.0, 1.0], [1.0, 0.0, 0.0]),
|
||||
([0.0, 0.0, 0.0], [0.0, 0.0, 3.0], [1.0, 0.0, 0.0]),
|
||||
([0.0, 0.0, 0.0], [2.0, 1.0, 3.0], [-1.0, 2.0, 0.0]),
|
||||
] {
|
||||
for rot in 0..4u8 {
|
||||
let mut sc = Scene::new();
|
||||
let mut s = Sprite::new("arm", "");
|
||||
s.beam(a, b, n, 0.4, "iso-robot");
|
||||
sc.add_sprite("main", s);
|
||||
let html = render(&sc, rot, &RenderOpts::default()).html;
|
||||
// three faces; two when one side is seen edge-on (an oblique bar)
|
||||
let faces = html.matches("iso-robot ").count();
|
||||
let oblique = a[0] != b[0] && a[2] != b[2];
|
||||
assert!(
|
||||
faces == 3 || (oblique && faces == 2),
|
||||
"{a:?}->{b:?} rot {rot}: {faces} faces"
|
||||
);
|
||||
assert!(
|
||||
html.contains("iso-robot iso-t"),
|
||||
"{a:?}->{b:?} rot {rot}: no top"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn faces_cull_and_shade_by_normal() {
|
||||
let mut sc = Scene::new();
|
||||
|
||||
@@ -388,6 +388,63 @@ impl Sprite {
|
||||
})
|
||||
}
|
||||
|
||||
/// A square bar `w` wide from `a` to `b` (robot arms, trusses): six faces,
|
||||
/// culled and shaded by their normals like [`Sprite::face`]. `n` is a
|
||||
/// side direction across the bar (any direction not along it).
|
||||
pub fn beam(&mut self, a: P3, b: P3, n: P3, w: f64, mat: &str) -> &mut Self {
|
||||
let sub = |p: P3, q: P3| [p[0] - q[0], p[1] - q[1], p[2] - q[2]];
|
||||
let cross = |p: P3, q: P3| {
|
||||
[
|
||||
p[1] * q[2] - p[2] * q[1],
|
||||
p[2] * q[0] - p[0] * q[2],
|
||||
p[0] * q[1] - p[1] * q[0],
|
||||
]
|
||||
};
|
||||
let unit = |p: P3| {
|
||||
let l = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
|
||||
if l < 1e-12 {
|
||||
None
|
||||
} else {
|
||||
Some([p[0] / l, p[1] / l, p[2] / l])
|
||||
}
|
||||
};
|
||||
let (Some(u), Some(v0)) = (unit(sub(b, a)), unit(cross(sub(b, a), n))) else {
|
||||
return self;
|
||||
};
|
||||
// (u, n, v): a right-handed square frame, so every face below winds
|
||||
// counter-clockwise seen from outside
|
||||
let n = cross(v0, u);
|
||||
let h = w / 2.0;
|
||||
let c = |e: P3, sn: f64, sv: f64| {
|
||||
[
|
||||
e[0] + (n[0] * sn + v0[0] * sv) * h,
|
||||
e[1] + (n[1] * sn + v0[1] * sv) * h,
|
||||
e[2] + (n[2] * sn + v0[2] * sv) * h,
|
||||
]
|
||||
};
|
||||
let k = [
|
||||
c(a, 1.0, 1.0),
|
||||
c(a, -1.0, 1.0),
|
||||
c(a, -1.0, -1.0),
|
||||
c(a, 1.0, -1.0),
|
||||
c(b, 1.0, 1.0),
|
||||
c(b, -1.0, 1.0),
|
||||
c(b, -1.0, -1.0),
|
||||
c(b, 1.0, -1.0),
|
||||
];
|
||||
for f in [
|
||||
[1, 5, 4, 0],
|
||||
[3, 7, 6, 2],
|
||||
[3, 0, 4, 7],
|
||||
[1, 2, 6, 5],
|
||||
[3, 2, 1, 0],
|
||||
[4, 5, 6, 7],
|
||||
] {
|
||||
self.face(f.iter().map(|&i| k[i]).collect(), mat, None, false);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// 3D polygons as one path with a fixed class.
|
||||
pub fn poly(&mut self, polys: Vec<Vec<P3>>, cls: &str, id: Option<String>) -> &mut Self {
|
||||
let polys: Vec<Vec<P3>> = polys
|
||||
|
||||
@@ -27,7 +27,9 @@ meet, so for a plot boundary put each piece's ends exactly on the next piece's e
|
||||
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). \
|
||||
The gap between goods is speed / rate, so set the rate on the producer and the speeds on the belts.";
|
||||
The gap between goods is speed / rate, so set the rate on the producer and the speeds on the belts. \
|
||||
A robot_arm (props.reach, default 2 m) 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 beside it; place it 1.5 to 2 m past a belt's end.";
|
||||
|
||||
fn tool(name: &str, description: &str, props: Value, required: &[&str]) -> Value {
|
||||
json!({ "name": name, "description": description,
|
||||
|
||||
@@ -297,6 +297,14 @@ pub fn register_sprite(e: &mut Engine) {
|
||||
ok(s)
|
||||
},
|
||||
);
|
||||
e.register_fn(
|
||||
"beam",
|
||||
|s: &mut SpriteH, a: D, b: D, nn: D, w: D, mat: &str| -> RR<SpriteH> {
|
||||
s.0.borrow_mut()
|
||||
.beam(p3(&a)?, p3(&b)?, p3(&nn)?, n(&w)?, &classes(mat));
|
||||
ok(s)
|
||||
},
|
||||
);
|
||||
e.register_fn("face", |s: &mut SpriteH, p: D, mat: &str| -> RR<SpriteH> {
|
||||
s.0.borrow_mut().face(pts3(&p)?, &classes(mat), None, false);
|
||||
ok(s)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// isokit/industry: conveyors and producers (machines that make goods at a
|
||||
// set rate). 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.
|
||||
// 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"] }
|
||||
|
||||
@@ -67,10 +69,12 @@ fn conveyor_ext(o, e) {
|
||||
0.0
|
||||
}
|
||||
|
||||
/// What reaches conveyor o at its start: #{ rate, item, running, from }
|
||||
/// (rate in goods per second) from the producer whose output side it starts
|
||||
/// at, or else from the conveyor ending there (followed back up to 12 belts);
|
||||
/// () when nothing feeds it.
|
||||
/// 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;
|
||||
@@ -78,10 +82,19 @@ fn conveyor_feed(o, hops) {
|
||||
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 };
|
||||
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; }
|
||||
@@ -91,9 +104,9 @@ fn conveyor_feed(o, hops) {
|
||||
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 };
|
||||
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 };
|
||||
return #{ rate: c.p.speed / c.p.gap, item: c.p.item, running: c.p.running, from: c.id, ph: 0.0 };
|
||||
}
|
||||
()
|
||||
}
|
||||
@@ -173,11 +186,15 @@ fn prefab_conveyor(o) {
|
||||
if moving {
|
||||
let per = step / p.speed;
|
||||
let v = dv(step, 0.0, p.rise * step / p.w);
|
||||
s.begin("a ride", `--dx:${v[0]}px;--dy:${v[1]}px;animation-duration:${per}s`);
|
||||
// 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, `animation-duration:${per}s`); }
|
||||
if fade != "" { s.begin("a " + fade, at); }
|
||||
s.goods(kind, u, 0.0, belt_z(p, u) + lift, k);
|
||||
if fade != "" { s.end(); }
|
||||
}
|
||||
@@ -251,3 +268,192 @@ fn prefab_producer(o) {
|
||||
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(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -74,7 +74,7 @@ Each phase ends with tests, a Studio/viewer check in Edge, commit and push.
|
||||
1. ~~Web performance~~ (done, see below)
|
||||
2. ~~Conveyors~~ (done, see below)
|
||||
3. ~~Producers with adjustable production speed~~ (done, see below)
|
||||
4. Robotic arms
|
||||
4. ~~Robotic arms~~ (done, see below)
|
||||
5. Rounded streets: curved road segments
|
||||
6. Sidewalks along streets
|
||||
7. Roundabouts
|
||||
@@ -103,6 +103,11 @@ height, rise, speed; corners join without overlap). A conveyor starting at a pro
|
||||
at another conveyor's end, carries those goods, spaced by speed / rate; tooltips show the numbers.
|
||||
`examples/factory`, Factory demo in the web Studio.
|
||||
|
||||
Robot arm: picks goods off the end of the nearest belt in its reach and puts them on the start of
|
||||
another belt or on a pallet, one move per item (or a set cycle), waiting while the line before it
|
||||
stands; the belt after it runs in step. Drawn as a 16-pose flipbook (inverse kinematics, links as
|
||||
the new native `beam` primitive) shown by a stepped CSS animation.
|
||||
|
||||
## Phase 2: richer editing
|
||||
|
||||
- [x] Interior prefabs (see Build your own house); a building `rooms` prop is still open
|
||||
|
||||
+14
-2
@@ -83,6 +83,7 @@ shaded automatically by face direction.
|
||||
| `cyl(cx, cy, r, z0, z1, mat[, opts])`, `frust(cx, cy, r0, r1, z0, z1, mat[, sides[, opts]])` | round things |
|
||||
| `slope(x0, x1, y0, y1, za, zb, th, mat)` | ramp / stair flight |
|
||||
| `face(pts3, mat[, #{id, two_sided}])` | any planar polygon, shaded and culled by its normal (roof slopes, gables) |
|
||||
| `beam(a, b, n, w, mat)` | square bar `w` wide between 3D points a and b, `n` a direction across it (robot arms, trusses) |
|
||||
| `poly(polys3, cls[, id])` | 3D polygons as one path (solar modules) |
|
||||
| `flat(poly or polys, z, cls[, id])` | flat polygon(s); later ones cut holes |
|
||||
| `open(a, b, [[u0, u1, z0, z1], …], cls[, id])` | windows / doors on the face a→b (outward normal on the right) |
|
||||
@@ -122,8 +123,8 @@ cobble, brick) and the site `wall` use it; `house_wall` has its own version
|
||||
|
||||
## Production lines
|
||||
|
||||
`producer` and `conveyor` (stdlib/industry.rhai) work together through the
|
||||
document, like house parts:
|
||||
`producer`, `conveyor` and `robot_arm` (stdlib/industry.rhai) work together
|
||||
through the document, like house parts:
|
||||
|
||||
- A producer makes one of its `product` goods every 60 / `rate` seconds (its
|
||||
ram goes down once per item) and puts them out on its local +x side.
|
||||
@@ -140,6 +141,17 @@ document, like house parts:
|
||||
- The goods ride a group that moves one gap per loop (`iso-ride`), so their
|
||||
drawing order never changes; the first fades in, the last fades out.
|
||||
Tooltips show speed, rate and where the goods come from (`info_row`).
|
||||
- A robot arm picks goods off the end of the nearest conveyor within `reach`
|
||||
(+0.4 m) 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 of the arm). It
|
||||
makes one move per item coming in (`cycle` 0) or every `cycle` seconds, and
|
||||
waits while the line before it stands. The belt it puts on carries its goods
|
||||
and starts its loop when they land, so arm and belts stay in step.
|
||||
- The arm is a flipbook: 16 poses (two-link inverse kinematics, links drawn
|
||||
with `beam`), each shown for a sixteenth of the cycle by a stepped CSS
|
||||
animation (`iso-flip`); a rotation cannot turn a 3D arm in this projection.
|
||||
Poses pointing away from the viewer are drawn before the base, the others
|
||||
after it.
|
||||
|
||||
## House parts
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# isokit factory demo: producers making goods at a set rate, conveyors carrying them.
|
||||
# isokit factory demo: producers making goods at a set rate, conveyors carrying them,
|
||||
# robot arms passing them on.
|
||||
# Open it with `isokit studio examples/factory`.
|
||||
title = "isokit factory"
|
||||
note = "Presses make goods at a set rate; conveyors carry them at their own speed, so the gap between the goods follows rate and speed. Tap a press or a belt for its numbers. Drag to pan, Ctrl + wheel or pinch to zoom."
|
||||
note = "Presses make goods at a set rate; conveyors carry them at their own speed, so the gap between the goods follows rate and speed; robot arms pass them on. Tap a press, belt or robot for its numbers. Drag to pan, Ctrl + wheel or pinch to zoom."
|
||||
default_level = "site"
|
||||
default_rotation = 1
|
||||
prelude = []
|
||||
|
||||
@@ -7,20 +7,22 @@
|
||||
{"id": "press-a", "type": "producer", "x": -14, "y": -4, "props": {"rate": 20, "product": "carton", "colour": "mach1"}, "name": "Carton press", "tag": "Cartons 20/min"},
|
||||
{"id": "belt-a1", "type": "conveyor", "x": -8.4, "y": -4, "rot": 0, "props": {"w": 8}, "name": "Carton belt 1"},
|
||||
{"id": "belt-a2", "type": "conveyor", "x": -4.4, "y": -1, "rot": 90, "props": {"w": 6}, "name": "Carton belt 2"},
|
||||
{"id": "belt-a3", "type": "conveyor", "x": -0.4, "y": 2, "rot": 0, "props": {"w": 8, "speed": 0.8}, "name": "Carton belt 3"},
|
||||
{"id": "belt-a3", "type": "conveyor", "x": -1.4, "y": 2, "rot": 0, "props": {"w": 6, "speed": 0.8}, "name": "Carton belt 3"},
|
||||
{"id": "robot-a", "type": "robot_arm", "x": 3.2, "y": 2, "name": "Palletiser", "tag": "Palletiser"},
|
||||
{"id": "press-b", "type": "producer", "x": 14, "y": -4, "rot": 180, "props": {"rate": 10, "product": "drum", "colour": "mach2", "d": 2.6}, "name": "Drum filler", "tag": "Drums 10/min"},
|
||||
{"id": "belt-b1", "type": "conveyor", "x": 9.4, "y": -4, "rot": 180, "props": {"w": 6, "d": 1.0, "speed": 0.4}, "name": "Drum belt"},
|
||||
{"id": "belt-b2", "type": "conveyor", "x": 6.4, "y": 0, "rot": 90, "props": {"w": 8, "d": 1.0, "speed": 0.4, "rise": 1.4}, "name": "Drum incline"},
|
||||
{"id": "belt-b2", "type": "conveyor", "x": 6.4, "y": -1, "rot": 90, "props": {"w": 6, "d": 1.0, "speed": 0.4, "rise": 0.8}, "name": "Drum incline"},
|
||||
{"id": "robot-b", "type": "robot_arm", "x": 6.4, "y": 3.8, "props": {"reach": 2.4, "colour": "mach2"}, "name": "Loading robot"},
|
||||
{"id": "belt-load", "type": "conveyor", "x": 11.5, "y": 5.2, "rot": 0, "props": {"w": 7, "d": 1.0, "speed": 0.6, "rise": 1.0}, "name": "Loading belt"},
|
||||
{"id": "truck", "type": "truck", "x": 16.6, "y": 5.2, "rot": 90, "props": {"len": 9}, "name": "Truck at the dock"},
|
||||
{"id": "press-c", "type": "producer", "x": -14, "y": 6, "props": {"rate": 6, "product": "part", "colour": "mach4", "running": false}, "name": "Parts press", "tag": "Stopped", "info": "Waiting for a tool change"},
|
||||
{"id": "belt-c1", "type": "conveyor", "x": -9.9, "y": 6, "rot": 0, "props": {"w": 5}, "name": "Parts belt"},
|
||||
{"id": "belt-load", "type": "conveyor", "x": 6.5, "y": 8.5, "rot": 0, "props": {"w": 7, "item": "sack", "gap": 1.1, "speed": 0.6, "rise": 1.0}, "name": "Loading belt"},
|
||||
{"id": "truck", "type": "truck", "x": 13.2, "y": 8.5, "rot": 90, "props": {"len": 9}, "name": "Truck at the dock"},
|
||||
{"id": "office", "type": "shed", "x": -5.5, "y": 9.2, "props": {"w": 6, "d": 3.5, "h": 3}, "name": "Shift office"},
|
||||
{"id": "cont-1", "type": "container", "x": 16.8, "y": 0.2, "rot": 90, "props": {"size": "20ft", "stack": 2}, "name": "Empty drums"},
|
||||
{"id": "cont-1", "type": "container", "x": 12.5, "y": 9.8, "rot": 0, "props": {"size": "20ft", "stack": 2}, "name": "Empty drums"},
|
||||
{"id": "cont-2", "type": "container", "x": -16.6, "y": 1.5, "rot": 90, "props": {"size": "20ft", "colour": "truckbox"}, "name": "Carton blanks"},
|
||||
{"id": "op-a", "type": "person", "x": -14.2, "y": -6.2},
|
||||
{"id": "op-c", "type": "person", "x": -11.8, "y": 7.8, "props": {"cls": "hivis"}},
|
||||
{"id": "op-load", "type": "person", "x": 2.4, "y": 9.6},
|
||||
{"id": "op-load", "type": "person", "x": 10.0, "y": 7.2},
|
||||
{"id": "tree-1", "type": "tree", "x": -20, "y": 13},
|
||||
{"id": "tree-2", "type": "tree", "x": -15, "y": 14},
|
||||
{"id": "tree-3", "type": "tree", "x": 19, "y": 14},
|
||||
|
||||
+10
-2
@@ -379,11 +379,19 @@ with sync_playwright() as p:
|
||||
pg.locator('#right input[type="number"][data-prop="rate"]').press("Enter")
|
||||
pg.wait_for_timeout(900)
|
||||
check("30 cartons a minute, 1 m apart" in rows().get("Carries", ""), "a faster producer packs the belt closer: " + rows().get("Carries", "?"))
|
||||
# a robot arm at the belt's end picks the goods off and stacks them on a pallet, one per item
|
||||
pg.evaluate("__studio.op({op: 'add', type: 'robot_arm', x: 19.2, y: -4.5})")
|
||||
pg.wait_for_timeout(900)
|
||||
rid = doc()["objects"][-1]["id"]
|
||||
rrows = dict(pg.evaluate(f"(__studio.view.info('{rid}') || {{}}).rows || []"))
|
||||
check(rrows.get("Moves") == f"cartons from {cv['id']} to a pallet" and rrows.get("Cycle") == "2 s" and
|
||||
pg.evaluate(f"document.querySelectorAll('.iso-svg [data-obj=\"{rid}\"] .iso-flip').length") == 16,
|
||||
f"a robot arm at the belt's end moves its goods to a pallet in step with them: {rrows}")
|
||||
pg.screenshot(path=str(OUT / "studio-conveyor.jpg"), type="jpeg", quality=80)
|
||||
for _ in range(3):
|
||||
for _ in range(4):
|
||||
pg.locator("#undo").click()
|
||||
pg.wait_for_timeout(500)
|
||||
check(len(doc()["objects"]) == n4, "undo takes the producer and the belt back")
|
||||
check(len(doc()["objects"]) == n4, "undo takes the robot, the producer and the belt back")
|
||||
pg.evaluate("__studio.select(null)")
|
||||
pg.wait_for_timeout(300)
|
||||
|
||||
|
||||
@@ -321,6 +321,9 @@ try:
|
||||
f"Factory demo opens a production yard ({nf} objects, goods riding {rides} belts, no warnings)")
|
||||
carries = pg.evaluate("() => Object.fromEntries((__studio.view.info('belt-a3') || {rows: []}).rows)['Carries'] || ''")
|
||||
check("20 cartons a minute" in carries and "(from belt-a2)" in carries, "goods come down the line: " + carries)
|
||||
loads = pg.evaluate("() => Object.fromEntries((__studio.view.info('belt-load') || {rows: []}).rows)['Carries'] || ''")
|
||||
flips = pg.evaluate("() => document.querySelectorAll('.iso-svg .iso-flip').length")
|
||||
check("(from robot-b)" in loads and flips == 32, f"robot arms pass the goods on ({flips} poses): " + loads)
|
||||
shot(pg, "web-factory.jpg")
|
||||
|
||||
# ---------------------------------------------------------------- folder project (OPFS stands in for a picked folder)
|
||||
|
||||
@@ -122,6 +122,10 @@ html[data-theme=light] .iso-svg .iso-glint { display: inline; }
|
||||
.iso-svg .iso-rideout { animation: iso-fadeout 2s linear infinite; }
|
||||
.iso-svg .iso-rideio { animation: iso-fadeio 2s linear infinite; }
|
||||
.iso-svg .iso-ram { animation: iso-ram 2s ease-in-out infinite; }
|
||||
/* robot arms: a flipbook of 16 poses, each shown for a sixteenth of the cycle (delays set the turn);
|
||||
unanimated (reduced motion, ghosted) only the first */
|
||||
.iso-svg .iso-flip { visibility: hidden; animation: iso-flip16 4s step-end infinite; }
|
||||
.iso-svg .iso-flip.iso-pose0 { visibility: visible; }
|
||||
.iso-svg .iso-park { display: none; }
|
||||
/* paused off-screen (iso-off), when stopped (iso-still), with animation off and during pan/zoom gestures */
|
||||
.iso-off .iso-a, .iso-still .iso-a, .iso-off .iso-drive, .iso-still .iso-drive, .iso-off .iso-ledr, .iso-off .iso-ledo, .iso-still .iso-ledr, .iso-still .iso-ledo,
|
||||
@@ -142,6 +146,7 @@ html[data-theme=light] .iso-svg .iso-glint { display: inline; }
|
||||
@keyframes iso-fadein { 0% { opacity: 0; } 35%, 100% { opacity: 1; } }
|
||||
@keyframes iso-fadeout { 0%, 65% { opacity: 1; } 100% { opacity: 0; } }
|
||||
@keyframes iso-fadeio { 0%, 100% { opacity: 0; } 30%, 70% { opacity: 1; } }
|
||||
@keyframes iso-flip16 { 0% { visibility: visible; } 6.25%, 100% { visibility: hidden; } }
|
||||
@keyframes iso-ram { 0%, 20%, 100% { transform: translate(0, 0); } 45%, 55% { transform: translate(var(--dx), var(--dy)); } }
|
||||
@keyframes iso-move { from { transform: translate(0, 0); } to { transform: translate(var(--dx), var(--dy)); } }
|
||||
@keyframes iso-shuttle {
|
||||
|
||||
Reference in new issue
Block a user