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