Line 4: a factory in one file
The route through the talk
One page, no build, nothing from elsewhere
6,9836,983
~2,400~2,400
00
One page, no build, nothing from elsewhere
6,9836,983
The rules of a factory page
Accessibility is part of the work
Physical correctness first
Every thing is a box
function B(x0, y0, z0, x1, y1, z1, c, extra) {
const b = { x0, y0, z0, x1, y1, z1, c, tag: TAG };
if (extra) {
Object.assign(b, extra);
// the render cache recognises a box by position, size, colour, a, ns and stripes; a box
// carrying anything else that might affect drawing is never cached, just drawn live
for (const k in extra) if (k !== 'a' && k !== 'ns' && k !== 'stripes') { b.nocache = true; break; }
}
boxes.push(b);
return b;
}
A hexagon is three slabs
When no order is possible
Draw only what changed
4545
32 px32 px
Two arms, one belt
- Each takes a whole layer and sets it on belt A1, a row in each lane
- Neither moves into the other: a 3D check of every part, 5 cm to spare
- Only one works over the belt and the rack at a time
- Each picks the most downstream pallet it can reach first
One arm over the shared floor
// The floor both depalletizers reach over: the belt where the layers go down and the rack beside
// it. Only one works over it at a time: a move that takes the head over it waits until the other
// has gone (sharedBy). With heads a layer wide, their paths to the belt and the rack cross, and
// waiting step by step for each other there left both stuck. (The lowest slot on the leg, which
// they both pick from too, isn't in it: what they go for there is reserved, and one holding a
// layer over it while the belt is full would keep the other from the rack.)
const SHARED = [[2.4, 1.6, 4.9, 5.0]];
const inShared = p => SHARED.some(([x0, y0, x1, y1]) => p[0] > x0 && p[0] < x1 && p[1] > y0 && p[1] < y1);
const overShared = (arm, a, b) => { for (let k = 0; k <= 1.0001; k += 0.1) if (inShared(turnLerp(arm, a, b, k))) return true; return false; };
When each waits for the other
const stuck = () => {
if (!(o.waiting && arm.waiting)) return false;
if (!sim.armYield) {
const who = going(arm) !== going(o) ? (going(arm) ? o : arm) : arm.zoneWait !== !!o.zoneWait ? (arm.zoneWait ? arm : o) : Math.random() < 0.5 ? arm : o;
sim.armYield = { arm: who, t: sim.t }; sim.armDeadlocks = (sim.armDeadlocks || 0) + 1;
}
return sim.armYield.arm === arm;
};
A rule of the line
Book the floor before you move
The reservation table
const RES = new Map(); // mover -> { list, x0, y0, x1, y1, t0, t1, l }
const FOREVER = 1e9;
const RES_PAD = 0.15; // s either side of every window: setting off a frame late, rounding
const PIECE = 0.25, LEG_SLACK = 0.04, LATE = 0.3, HORIZON = 300, MAX_LEGS = 10;
const levelsAt = z => z < 0.5 ? 1 : z > 1.3 ? 2 : 3;
The reservation table
function resHit(m, b, m2) {
let hit = null;
for (const [who, r] of RES) {
if (who === m || who === m2 || !(r.l & b.l) || r.t1 <= b.t0 || b.t1 <= r.t0 || !boxesMeet(r, b)) continue;
for (const q of r.list) if ((q.l & b.l) && q.t0 < b.t1 && b.t0 < q.t1 && boxesMeet(q, b) && (!hit || q.t1 > hit.t1)) hit = { who, t0: q.t0, t1: q.t1, q };
}
return hit;
}
When a plan runs into someone
- Someone idle steps aside, waits a moment, and goes back
- Someone busy: after 3 s, go round them, or another way
- Failing both, go round firmly: the idle step aside on arrival
- Failing all, walk as far as you can and try again
- Lifts nose to nose: the one with less far to go backs off
~3%~3%
Ten seconds of a run, at 4×
Faults, and who mends them
| Level | Share | Who | Repair |
|---|---|---|---|
| 1 | 50% | the operator or the tech | 3–8 s |
| 2 | 35% | the tech | 8–20 s |
| 3 | 15% | the tech and a contractor | a call-out, then 20–45 s |
Estimates
A monitor, not a constraint
Flag, never prevent
- Lifts against each other and against people on foot
- People and robots against each other, on the same level
- Feet against the treads of the stairs
- A jam check too: nobody with work still for over a minute
OEE = Availability × Performance × Quality
Two samples, one window
function oeeFigures(from, to) {
const planned = to.t - from.t;
if (planned < 30) return null;
const run = Math.max(0, planned - (to.stop - from.stop));
const total = to.total - from.total, scrap = to.scrap - from.scrap;
const A = run / planned;
const P = run > 0 ? IDEAL_CYCLE * total / run : 0;
const Q = total > 0 ? Math.max(0, total - scrap) / total : 1;
return { A, P, Q, oee: A * P * Q };
}
85%85%
Settings that start at world class
let faultScale = 8;
function setFaultScale(v) {
const k = v / faultScale; faultScale = v;
for (const m of [...Object.values(machines), ...robots, ...forklifts]) if (!m.down && m.nextFail !== undefined && m.nextFail > sim.t) m.nextFail = sim.t + (m.nextFail - sim.t) * k;
}
$('faultRate').addEventListener('change', e => setFaultScale(parseFloat(e.target.value)));
85%85%
53%53%
Widgets painted an hour, commit by commit
| commit | Widgets painted an hour |
|---|---|
| Before layer heads | 2517 |
| Layer heads | 3032 |
| Supplies keep pace | 4475 |
| World-class settings | 6851 |
- Means over sixteen seeds × 8 hours at 4×, from the commit messages of rellen/iso-factories.
What needs attention, first
A canvas with words
| Part | Markup |
|---|---|
| The view | a canvas with role="img" and a written description |
| The figures | output elements |
| The controls | real buttons and fieldsets, a visible focus ring, 44 px to hit |
| Machines & faults | a table: the keyboard route to every action |
| Reduced motion | the page starts paused |
Where each part goes
| Part | Goes to | Because |
|---|---|---|
| Simulation | a Rust crate | typed, fast, deterministic, builds for both hosts |
| Drawing | TypeScript | it is already JavaScript |
| Serving, viewers, history | Elixir and Phoenix | supervision, fan-out, persistence |
Why not the simulation in Elixir
Streaming the factory
| Raw JSON | Binary | Compressed WebSocket | |
|---|---|---|---|
| 60 updates a second | 65 | 17 | 11 |
| 20 a second, smoothed | 38 | 8.6 | 5.6 |
- From docs/porting.md: 2.4–5.6 KB/s at 20 updates a second, compressed.
Decisions to make
- One shared factory, one per viewer, or each browser running its own?
- Keep a static GitHub Pages build alongside, through WebAssembly?
- Is the claude.ai artifact still a target?
- Does the single-file page stay as the reference until the port matches it?
Open the page, and watch Line 4 run
Line 4: a factory in one file
The route through the talk
The route through the talk
The route through the talk
The route through the talk
The route through the talk
The route through the talk
The route through the talk
The route through the talk
One page, no build, nothing from elsewhere
6,9836,983
~2,400~2,400
00
One page, no build, nothing from elsewhere
6,9836,983
~2,400~2,400
00
One page, no build, nothing from elsewhere
6,9836,983
~2,400~2,400
00
One page, no build, nothing from elsewhere
6,9836,983
The rules of a factory page
Accessibility is part of the work
Physical correctness first
One page, no build, nothing from elsewhere
6,9836,983
The rules of a factory page
Accessibility is part of the work
Physical correctness first
One page, no build, nothing from elsewhere
6,9836,983
The rules of a factory page
Accessibility is part of the work
Physical correctness first
Every thing is a box
function B(x0, y0, z0, x1, y1, z1, c, extra) {
const b = { x0, y0, z0, x1, y1, z1, c, tag: TAG };
if (extra) {
Object.assign(b, extra);
// the render cache recognises a box by position, size, colour, a, ns and stripes; a box
// carrying anything else that might affect drawing is never cached, just drawn live
for (const k in extra) if (k !== 'a' && k !== 'ns' && k !== 'stripes') { b.nocache = true; break; }
}
boxes.push(b);
return b;
}
Every thing is a box
function B(x0, y0, z0, x1, y1, z1, c, extra) {
const b = { x0, y0, z0, x1, y1, z1, c, tag: TAG };
if (extra) {
Object.assign(b, extra);
// the render cache recognises a box by position, size, colour, a, ns and stripes; a box
// carrying anything else that might affect drawing is never cached, just drawn live
for (const k in extra) if (k !== 'a' && k !== 'ns' && k !== 'stripes') { b.nocache = true; break; }
}
boxes.push(b);
return b;
}
A hexagon is three slabs
When no order is possible
Draw only what changed
4545
32 px32 px
Draw only what changed
4545
32 px32 px
Two arms, one belt
- Each takes a whole layer and sets it on belt A1, a row in each lane
- Neither moves into the other: a 3D check of every part, 5 cm to spare
- Only one works over the belt and the rack at a time
- Each picks the most downstream pallet it can reach first
Two arms, one belt
- Each takes a whole layer and sets it on belt A1, a row in each lane
- Neither moves into the other: a 3D check of every part, 5 cm to spare
- Only one works over the belt and the rack at a time
- Each picks the most downstream pallet it can reach first
Two arms, one belt
- Each takes a whole layer and sets it on belt A1, a row in each lane
- Neither moves into the other: a 3D check of every part, 5 cm to spare
- Only one works over the belt and the rack at a time
- Each picks the most downstream pallet it can reach first
Two arms, one belt
- Each takes a whole layer and sets it on belt A1, a row in each lane
- Neither moves into the other: a 3D check of every part, 5 cm to spare
- Only one works over the belt and the rack at a time
- Each picks the most downstream pallet it can reach first
One arm over the shared floor
// The floor both depalletizers reach over: the belt where the layers go down and the rack beside
// it. Only one works over it at a time: a move that takes the head over it waits until the other
// has gone (sharedBy). With heads a layer wide, their paths to the belt and the rack cross, and
// waiting step by step for each other there left both stuck. (The lowest slot on the leg, which
// they both pick from too, isn't in it: what they go for there is reserved, and one holding a
// layer over it while the belt is full would keep the other from the rack.)
const SHARED = [[2.4, 1.6, 4.9, 5.0]];
const inShared = p => SHARED.some(([x0, y0, x1, y1]) => p[0] > x0 && p[0] < x1 && p[1] > y0 && p[1] < y1);
const overShared = (arm, a, b) => { for (let k = 0; k <= 1.0001; k += 0.1) if (inShared(turnLerp(arm, a, b, k))) return true; return false; };
One arm over the shared floor
// The floor both depalletizers reach over: the belt where the layers go down and the rack beside
// it. Only one works over it at a time: a move that takes the head over it waits until the other
// has gone (sharedBy). With heads a layer wide, their paths to the belt and the rack cross, and
// waiting step by step for each other there left both stuck. (The lowest slot on the leg, which
// they both pick from too, isn't in it: what they go for there is reserved, and one holding a
// layer over it while the belt is full would keep the other from the rack.)
const SHARED = [[2.4, 1.6, 4.9, 5.0]];
const inShared = p => SHARED.some(([x0, y0, x1, y1]) => p[0] > x0 && p[0] < x1 && p[1] > y0 && p[1] < y1);
const overShared = (arm, a, b) => { for (let k = 0; k <= 1.0001; k += 0.1) if (inShared(turnLerp(arm, a, b, k))) return true; return false; };
When each waits for the other
const stuck = () => {
if (!(o.waiting && arm.waiting)) return false;
if (!sim.armYield) {
const who = going(arm) !== going(o) ? (going(arm) ? o : arm) : arm.zoneWait !== !!o.zoneWait ? (arm.zoneWait ? arm : o) : Math.random() < 0.5 ? arm : o;
sim.armYield = { arm: who, t: sim.t }; sim.armDeadlocks = (sim.armDeadlocks || 0) + 1;
}
return sim.armYield.arm === arm;
};
A rule of the line
When each waits for the other
const stuck = () => {
if (!(o.waiting && arm.waiting)) return false;
if (!sim.armYield) {
const who = going(arm) !== going(o) ? (going(arm) ? o : arm) : arm.zoneWait !== !!o.zoneWait ? (arm.zoneWait ? arm : o) : Math.random() < 0.5 ? arm : o;
sim.armYield = { arm: who, t: sim.t }; sim.armDeadlocks = (sim.armDeadlocks || 0) + 1;
}
return sim.armYield.arm === arm;
};
A rule of the line
When each waits for the other
const stuck = () => {
if (!(o.waiting && arm.waiting)) return false;
if (!sim.armYield) {
const who = going(arm) !== going(o) ? (going(arm) ? o : arm) : arm.zoneWait !== !!o.zoneWait ? (arm.zoneWait ? arm : o) : Math.random() < 0.5 ? arm : o;
sim.armYield = { arm: who, t: sim.t }; sim.armDeadlocks = (sim.armDeadlocks || 0) + 1;
}
return sim.armYield.arm === arm;
};
A rule of the line
When each waits for the other
const stuck = () => {
if (!(o.waiting && arm.waiting)) return false;
if (!sim.armYield) {
const who = going(arm) !== going(o) ? (going(arm) ? o : arm) : arm.zoneWait !== !!o.zoneWait ? (arm.zoneWait ? arm : o) : Math.random() < 0.5 ? arm : o;
sim.armYield = { arm: who, t: sim.t }; sim.armDeadlocks = (sim.armDeadlocks || 0) + 1;
}
return sim.armYield.arm === arm;
};
A rule of the line
Book the floor before you move
The reservation table
const RES = new Map(); // mover -> { list, x0, y0, x1, y1, t0, t1, l }
const FOREVER = 1e9;
const RES_PAD = 0.15; // s either side of every window: setting off a frame late, rounding
const PIECE = 0.25, LEG_SLACK = 0.04, LATE = 0.3, HORIZON = 300, MAX_LEGS = 10;
const levelsAt = z => z < 0.5 ? 1 : z > 1.3 ? 2 : 3;
The reservation table
function resHit(m, b, m2) {
let hit = null;
for (const [who, r] of RES) {
if (who === m || who === m2 || !(r.l & b.l) || r.t1 <= b.t0 || b.t1 <= r.t0 || !boxesMeet(r, b)) continue;
for (const q of r.list) if ((q.l & b.l) && q.t0 < b.t1 && b.t0 < q.t1 && boxesMeet(q, b) && (!hit || q.t1 > hit.t1)) hit = { who, t0: q.t0, t1: q.t1, q };
}
return hit;
}
The reservation table
function resHit(m, b, m2) {
let hit = null;
for (const [who, r] of RES) {
if (who === m || who === m2 || !(r.l & b.l) || r.t1 <= b.t0 || b.t1 <= r.t0 || !boxesMeet(r, b)) continue;
for (const q of r.list) if ((q.l & b.l) && q.t0 < b.t1 && b.t0 < q.t1 && boxesMeet(q, b) && (!hit || q.t1 > hit.t1)) hit = { who, t0: q.t0, t1: q.t1, q };
}
return hit;
}
The reservation table
function resHit(m, b, m2) {
let hit = null;
for (const [who, r] of RES) {
if (who === m || who === m2 || !(r.l & b.l) || r.t1 <= b.t0 || b.t1 <= r.t0 || !boxesMeet(r, b)) continue;
for (const q of r.list) if ((q.l & b.l) && q.t0 < b.t1 && b.t0 < q.t1 && boxesMeet(q, b) && (!hit || q.t1 > hit.t1)) hit = { who, t0: q.t0, t1: q.t1, q };
}
return hit;
}
When a plan runs into someone
- Someone idle steps aside, waits a moment, and goes back
- Someone busy: after 3 s, go round them, or another way
- Failing both, go round firmly: the idle step aside on arrival
- Failing all, walk as far as you can and try again
- Lifts nose to nose: the one with less far to go backs off
~3%~3%
When a plan runs into someone
- Someone idle steps aside, waits a moment, and goes back
- Someone busy: after 3 s, go round them, or another way
- Failing both, go round firmly: the idle step aside on arrival
- Failing all, walk as far as you can and try again
- Lifts nose to nose: the one with less far to go backs off
~3%~3%
When a plan runs into someone
- Someone idle steps aside, waits a moment, and goes back
- Someone busy: after 3 s, go round them, or another way
- Failing both, go round firmly: the idle step aside on arrival
- Failing all, walk as far as you can and try again
- Lifts nose to nose: the one with less far to go backs off
~3%~3%
When a plan runs into someone
- Someone idle steps aside, waits a moment, and goes back
- Someone busy: after 3 s, go round them, or another way
- Failing both, go round firmly: the idle step aside on arrival
- Failing all, walk as far as you can and try again
- Lifts nose to nose: the one with less far to go backs off
~3%~3%
When a plan runs into someone
- Someone idle steps aside, waits a moment, and goes back
- Someone busy: after 3 s, go round them, or another way
- Failing both, go round firmly: the idle step aside on arrival
- Failing all, walk as far as you can and try again
- Lifts nose to nose: the one with less far to go backs off
~3%~3%
When a plan runs into someone
- Someone idle steps aside, waits a moment, and goes back
- Someone busy: after 3 s, go round them, or another way
- Failing both, go round firmly: the idle step aside on arrival
- Failing all, walk as far as you can and try again
- Lifts nose to nose: the one with less far to go backs off
~3%~3%
Ten seconds of a run, at 4×
Faults, and who mends them
| Level | Share | Who | Repair |
|---|---|---|---|
| 1 | 50% | the operator or the tech | 3–8 s |
| 2 | 35% | the tech | 8–20 s |
| 3 | 15% | the tech and a contractor | a call-out, then 20–45 s |
Estimates
Faults, and who mends them
| Level | Share | Who | Repair |
|---|---|---|---|
| 1 | 50% | the operator or the tech | 3–8 s |
| 2 | 35% | the tech | 8–20 s |
| 3 | 15% | the tech and a contractor | a call-out, then 20–45 s |
Estimates
Faults, and who mends them
| Level | Share | Who | Repair |
|---|---|---|---|
| 1 | 50% | the operator or the tech | 3–8 s |
| 2 | 35% | the tech | 8–20 s |
| 3 | 15% | the tech and a contractor | a call-out, then 20–45 s |
Estimates
Faults, and who mends them
| Level | Share | Who | Repair |
|---|---|---|---|
| 1 | 50% | the operator or the tech | 3–8 s |
| 2 | 35% | the tech | 8–20 s |
| 3 | 15% | the tech and a contractor | a call-out, then 20–45 s |
Estimates
Faults, and who mends them
| Level | Share | Who | Repair |
|---|---|---|---|
| 1 | 50% | the operator or the tech | 3–8 s |
| 2 | 35% | the tech | 8–20 s |
| 3 | 15% | the tech and a contractor | a call-out, then 20–45 s |
Estimates
A monitor, not a constraint
Flag, never prevent
- Lifts against each other and against people on foot
- People and robots against each other, on the same level
- Feet against the treads of the stairs
- A jam check too: nobody with work still for over a minute
A monitor, not a constraint
Flag, never prevent
- Lifts against each other and against people on foot
- People and robots against each other, on the same level
- Feet against the treads of the stairs
- A jam check too: nobody with work still for over a minute
A monitor, not a constraint
Flag, never prevent
- Lifts against each other and against people on foot
- People and robots against each other, on the same level
- Feet against the treads of the stairs
- A jam check too: nobody with work still for over a minute
A monitor, not a constraint
Flag, never prevent
- Lifts against each other and against people on foot
- People and robots against each other, on the same level
- Feet against the treads of the stairs
- A jam check too: nobody with work still for over a minute
OEE = Availability × Performance × Quality
Two samples, one window
function oeeFigures(from, to) {
const planned = to.t - from.t;
if (planned < 30) return null;
const run = Math.max(0, planned - (to.stop - from.stop));
const total = to.total - from.total, scrap = to.scrap - from.scrap;
const A = run / planned;
const P = run > 0 ? IDEAL_CYCLE * total / run : 0;
const Q = total > 0 ? Math.max(0, total - scrap) / total : 1;
return { A, P, Q, oee: A * P * Q };
}
85%85%
Two samples, one window
function oeeFigures(from, to) {
const planned = to.t - from.t;
if (planned < 30) return null;
const run = Math.max(0, planned - (to.stop - from.stop));
const total = to.total - from.total, scrap = to.scrap - from.scrap;
const A = run / planned;
const P = run > 0 ? IDEAL_CYCLE * total / run : 0;
const Q = total > 0 ? Math.max(0, total - scrap) / total : 1;
return { A, P, Q, oee: A * P * Q };
}
85%85%
Two samples, one window
function oeeFigures(from, to) {
const planned = to.t - from.t;
if (planned < 30) return null;
const run = Math.max(0, planned - (to.stop - from.stop));
const total = to.total - from.total, scrap = to.scrap - from.scrap;
const A = run / planned;
const P = run > 0 ? IDEAL_CYCLE * total / run : 0;
const Q = total > 0 ? Math.max(0, total - scrap) / total : 1;
return { A, P, Q, oee: A * P * Q };
}
85%85%
Settings that start at world class
let faultScale = 8;
function setFaultScale(v) {
const k = v / faultScale; faultScale = v;
for (const m of [...Object.values(machines), ...robots, ...forklifts]) if (!m.down && m.nextFail !== undefined && m.nextFail > sim.t) m.nextFail = sim.t + (m.nextFail - sim.t) * k;
}
$('faultRate').addEventListener('change', e => setFaultScale(parseFloat(e.target.value)));
85%85%
53%53%
Settings that start at world class
let faultScale = 8;
function setFaultScale(v) {
const k = v / faultScale; faultScale = v;
for (const m of [...Object.values(machines), ...robots, ...forklifts]) if (!m.down && m.nextFail !== undefined && m.nextFail > sim.t) m.nextFail = sim.t + (m.nextFail - sim.t) * k;
}
$('faultRate').addEventListener('change', e => setFaultScale(parseFloat(e.target.value)));
85%85%
53%53%
Widgets painted an hour, commit by commit
| commit | Widgets painted an hour |
|---|---|
| Before layer heads | 2517 |
| Layer heads | 3032 |
| Supplies keep pace | 4475 |
| World-class settings | 6851 |
- Means over sixteen seeds × 8 hours at 4×, from the commit messages of rellen/iso-factories.
What needs attention, first
A canvas with words
| Part | Markup |
|---|---|
| The view | a canvas with role="img" and a written description |
| The figures | output elements |
| The controls | real buttons and fieldsets, a visible focus ring, 44 px to hit |
| Machines & faults | a table: the keyboard route to every action |
| Reduced motion | the page starts paused |
A canvas with words
| Part | Markup |
|---|---|
| The view | a canvas with role="img" and a written description |
| The figures | output elements |
| The controls | real buttons and fieldsets, a visible focus ring, 44 px to hit |
| Machines & faults | a table: the keyboard route to every action |
| Reduced motion | the page starts paused |
A canvas with words
| Part | Markup |
|---|---|
| The view | a canvas with role="img" and a written description |
| The figures | output elements |
| The controls | real buttons and fieldsets, a visible focus ring, 44 px to hit |
| Machines & faults | a table: the keyboard route to every action |
| Reduced motion | the page starts paused |
A canvas with words
| Part | Markup |
|---|---|
| The view | a canvas with role="img" and a written description |
| The figures | output elements |
| The controls | real buttons and fieldsets, a visible focus ring, 44 px to hit |
| Machines & faults | a table: the keyboard route to every action |
| Reduced motion | the page starts paused |
A canvas with words
| Part | Markup |
|---|---|
| The view | a canvas with role="img" and a written description |
| The figures | output elements |
| The controls | real buttons and fieldsets, a visible focus ring, 44 px to hit |
| Machines & faults | a table: the keyboard route to every action |
| Reduced motion | the page starts paused |
Where each part goes
| Part | Goes to | Because |
|---|---|---|
| Simulation | a Rust crate | typed, fast, deterministic, builds for both hosts |
| Drawing | TypeScript | it is already JavaScript |
| Serving, viewers, history | Elixir and Phoenix | supervision, fan-out, persistence |
Why not the simulation in Elixir
Where each part goes
| Part | Goes to | Because |
|---|---|---|
| Simulation | a Rust crate | typed, fast, deterministic, builds for both hosts |
| Drawing | TypeScript | it is already JavaScript |
| Serving, viewers, history | Elixir and Phoenix | supervision, fan-out, persistence |
Why not the simulation in Elixir
Where each part goes
| Part | Goes to | Because |
|---|---|---|
| Simulation | a Rust crate | typed, fast, deterministic, builds for both hosts |
| Drawing | TypeScript | it is already JavaScript |
| Serving, viewers, history | Elixir and Phoenix | supervision, fan-out, persistence |
Why not the simulation in Elixir
Where each part goes
| Part | Goes to | Because |
|---|---|---|
| Simulation | a Rust crate | typed, fast, deterministic, builds for both hosts |
| Drawing | TypeScript | it is already JavaScript |
| Serving, viewers, history | Elixir and Phoenix | supervision, fan-out, persistence |
Why not the simulation in Elixir
Where each part goes
| Part | Goes to | Because |
|---|---|---|
| Simulation | a Rust crate | typed, fast, deterministic, builds for both hosts |
| Drawing | TypeScript | it is already JavaScript |
| Serving, viewers, history | Elixir and Phoenix | supervision, fan-out, persistence |
Why not the simulation in Elixir
Streaming the factory
| Raw JSON | Binary | Compressed WebSocket | |
|---|---|---|---|
| 60 updates a second | 65 | 17 | 11 |
| 20 a second, smoothed | 38 | 8.6 | 5.6 |
- From docs/porting.md: 2.4–5.6 KB/s at 20 updates a second, compressed.
Decisions to make
- One shared factory, one per viewer, or each browser running its own?
- Keep a static GitHub Pages build alongside, through WebAssembly?
- Is the claude.ai artifact still a target?
- Does the single-file page stay as the reference until the port matches it?
Decisions to make
- One shared factory, one per viewer, or each browser running its own?
- Keep a static GitHub Pages build alongside, through WebAssembly?
- Is the claude.ai artifact still a target?
- Does the single-file page stay as the reference until the port matches it?
Decisions to make
- One shared factory, one per viewer, or each browser running its own?
- Keep a static GitHub Pages build alongside, through WebAssembly?
- Is the claude.ai artifact still a target?
- Does the single-file page stay as the reference until the port matches it?
Decisions to make
- One shared factory, one per viewer, or each browser running its own?
- Keep a static GitHub Pages build alongside, through WebAssembly?
- Is the claude.ai artifact still a target?
- Does the single-file page stay as the reference until the port matches it?
Open the page, and watch Line 4 run
Sources
22. Widgets painted an hour, commit by commit
- Means over sixteen seeds × 8 hours at 4×, from the commit messages of rellen/iso-factories.
26. Streaming the factory
- From docs/porting.md: 2.4–5.6 KB/s at 20 updates a second, compressed.