Northside Works · Line 4

Line 4: a factory in one file

Line 4, the running factory: belts, robot arms, a paint booth, lifts and the status panel
rellen.github.io/iso-factories · line-4/index.html
Northside Works · Line 4

Line 4: a factory in one file

Line 4, the running factory: belts, robot arms, a paint booth, lifts and the status panel
rellen.github.io/iso-factories · line-4/index.html
Northside Works · Line 4
rellen.github.io/iso-factories · line-4/index.html
2 / 28
Northside Works · Line 4
rellen.github.io/iso-factories · line-4/index.html
2 / 28
Northside Works · Line 4
rellen.github.io/iso-factories · line-4/index.html
2 / 28
Northside Works · Line 4
rellen.github.io/iso-factories · line-4/index.html
2 / 28
Northside Works · Line 4
rellen.github.io/iso-factories · line-4/index.html
2 / 28
Northside Works · Line 4
rellen.github.io/iso-factories · line-4/index.html
2 / 28
Northside Works · Line 4
rellen.github.io/iso-factories · line-4/index.html
2 / 28
Northside Works · Line 4
rellen.github.io/iso-factories · line-4/index.html
2 / 28
Northside Works · Line 4

One page, no build, nothing from elsewhere

6,983

lines in one HTML file

~2,400

boxes drawn each frame

0

libraries, requests or build steps
rellen.github.io/iso-factories · line-4/index.html
3 / 28
Northside Works · Line 4

One page, no build, nothing from elsewhere

6,983

lines in one HTML file

~2,400

boxes drawn each frame

0

libraries, requests or build steps
rellen.github.io/iso-factories · line-4/index.html
3 / 28
Northside Works · Line 4

One page, no build, nothing from elsewhere

6,983

lines in one HTML file

~2,400

boxes drawn each frame

0

libraries, requests or build steps
rellen.github.io/iso-factories · line-4/index.html
3 / 28
Northside Works · Line 4

One page, no build, nothing from elsewhere

6,983

lines in one HTML file

The rules of a factory page

It runs from file://, on GitHub Pages and as a claude.ai artifact, whose content security policy blocks every request to elsewhere.

Accessibility is part of the work

The canvas is an image with a written description, the figures are output elements, and the table of machines is the keyboard route to every action.

Physical correctness first

Nothing passes through anything else. An impossible move is flagged and counted, never hidden.
rellen.github.io/iso-factories · line-4/index.html
4 / 28
Northside Works · Line 4

One page, no build, nothing from elsewhere

6,983

lines in one HTML file

The rules of a factory page

It runs from file://, on GitHub Pages and as a claude.ai artifact, whose content security policy blocks every request to elsewhere.

Accessibility is part of the work

The canvas is an image with a written description, the figures are output elements, and the table of machines is the keyboard route to every action.

Physical correctness first

Nothing passes through anything else. An impossible move is flagged and counted, never hidden.
rellen.github.io/iso-factories · line-4/index.html
4 / 28
Northside Works · Line 4

One page, no build, nothing from elsewhere

6,983

lines in one HTML file

The rules of a factory page

It runs from file://, on GitHub Pages and as a claude.ai artifact, whose content security policy blocks every request to elsewhere.

Accessibility is part of the work

The canvas is an image with a written description, the figures are output elements, and the table of machines is the keyboard route to every action.

Physical correctness first

Nothing passes through anything else. An impossible move is flagged and counted, never hidden.
rellen.github.io/iso-factories · line-4/index.html
4 / 28
Northside Works · Line 4

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;
  }
rellen.github.io/iso-factories · line-4/index.html
5 / 28
Northside Works · Line 4

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;
  }
rellen.github.io/iso-factories · line-4/index.html
5 / 28
Northside Works · Line 4

A hexagon is three slabs

rellen.github.io/iso-factories · line-4/index.html
6 / 28
Northside Works · Line 4

When no order is possible

rellen.github.io/iso-factories · line-4/index.html
7 / 28
Northside Works · Line 4

Draw only what changed

45

frames unchanged before a box is cached

32 px

tiles, redrawn whole
rellen.github.io/iso-factories · line-4/index.html
8 / 28
Northside Works · Line 4

Draw only what changed

45

frames unchanged before a box is cached

32 px

tiles, redrawn whole
rellen.github.io/iso-factories · line-4/index.html
8 / 28
Northside Works · Line 4

Two arms, one belt

Rocky
Bullwinkle
  • 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
rellen.github.io/iso-factories · line-4/index.html
9 / 28
Northside Works · Line 4

Two arms, one belt

Rocky
Bullwinkle
  • 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
rellen.github.io/iso-factories · line-4/index.html
9 / 28
Northside Works · Line 4

Two arms, one belt

Rocky
Bullwinkle
  • 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
rellen.github.io/iso-factories · line-4/index.html
9 / 28
Northside Works · Line 4

Two arms, one belt

Rocky
Bullwinkle
  • 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
rellen.github.io/iso-factories · line-4/index.html
9 / 28
Northside Works · Line 4

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; };
rellen.github.io/iso-factories · line-4/index.html
10 / 28
Northside Works · Line 4

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; };
rellen.github.io/iso-factories · line-4/index.html
10 / 28
Northside Works · Line 4

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

If each is waiting for the other, one moves out of the way, round to its rest, and carries on from there.
rellen.github.io/iso-factories · line-4/index.html
11 / 28
Northside Works · Line 4

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

If each is waiting for the other, one moves out of the way, round to its rest, and carries on from there.
rellen.github.io/iso-factories · line-4/index.html
11 / 28
Northside Works · Line 4

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

If each is waiting for the other, one moves out of the way, round to its rest, and carries on from there.
rellen.github.io/iso-factories · line-4/index.html
11 / 28
Northside Works · Line 4

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

If each is waiting for the other, one moves out of the way, round to its rest, and carries on from there.
rellen.github.io/iso-factories · line-4/index.html
11 / 28
Northside Works · Line 4

Book the floor before you move

rellen.github.io/iso-factories · line-4/index.html
12 / 28
Northside Works · Line 4

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;
rellen.github.io/iso-factories · line-4/index.html
13 / 28
Northside Works · Line 4

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;
  }
rellen.github.io/iso-factories · line-4/index.html
14 / 28
Northside Works · Line 4

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;
  }
rellen.github.io/iso-factories · line-4/index.html
14 / 28
Northside Works · Line 4

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;
  }
rellen.github.io/iso-factories · line-4/index.html
14 / 28
Northside Works · Line 4

When a plan runs into someone

  1. Someone idle steps aside, waits a moment, and goes back
  2. Someone busy: after 3 s, go round them, or another way
  3. Failing both, go round firmly: the idle step aside on arrival
  4. Failing all, walk as far as you can and try again
  5. Lifts nose to nose: the one with less far to go backs off

~3%

of the throughput: the cost of the table
rellen.github.io/iso-factories · line-4/index.html
15 / 28
Northside Works · Line 4

When a plan runs into someone

  1. Someone idle steps aside, waits a moment, and goes back
  2. Someone busy: after 3 s, go round them, or another way
  3. Failing both, go round firmly: the idle step aside on arrival
  4. Failing all, walk as far as you can and try again
  5. Lifts nose to nose: the one with less far to go backs off

~3%

of the throughput: the cost of the table
rellen.github.io/iso-factories · line-4/index.html
15 / 28
Northside Works · Line 4

When a plan runs into someone

  1. Someone idle steps aside, waits a moment, and goes back
  2. Someone busy: after 3 s, go round them, or another way
  3. Failing both, go round firmly: the idle step aside on arrival
  4. Failing all, walk as far as you can and try again
  5. Lifts nose to nose: the one with less far to go backs off

~3%

of the throughput: the cost of the table
rellen.github.io/iso-factories · line-4/index.html
15 / 28
Northside Works · Line 4

When a plan runs into someone

  1. Someone idle steps aside, waits a moment, and goes back
  2. Someone busy: after 3 s, go round them, or another way
  3. Failing both, go round firmly: the idle step aside on arrival
  4. Failing all, walk as far as you can and try again
  5. Lifts nose to nose: the one with less far to go backs off

~3%

of the throughput: the cost of the table
rellen.github.io/iso-factories · line-4/index.html
15 / 28
Northside Works · Line 4

When a plan runs into someone

  1. Someone idle steps aside, waits a moment, and goes back
  2. Someone busy: after 3 s, go round them, or another way
  3. Failing both, go round firmly: the idle step aside on arrival
  4. Failing all, walk as far as you can and try again
  5. Lifts nose to nose: the one with less far to go backs off

~3%

of the throughput: the cost of the table
rellen.github.io/iso-factories · line-4/index.html
15 / 28
Northside Works · Line 4

When a plan runs into someone

  1. Someone idle steps aside, waits a moment, and goes back
  2. Someone busy: after 3 s, go round them, or another way
  3. Failing both, go round firmly: the idle step aside on arrival
  4. Failing all, walk as far as you can and try again
  5. Lifts nose to nose: the one with less far to go backs off

~3%

of the throughput: the cost of the table
rellen.github.io/iso-factories · line-4/index.html
15 / 28
Northside Works · Line 4

Ten seconds of a run, at 4×

Ten seconds of Line 4 at four times its speed
rellen.github.io/iso-factories · line-4/index.html
16 / 28
Northside Works · Line 4

Faults, and who mends them

LevelShareWhoRepair
150%the operator or the tech3–8 s
235%the tech8–20 s
315%the tech and a contractora call-out, then 20–45 s

Estimates

After a look, each fault shows an estimate. The real repair comes in early a quarter of the time, and now and then blows out to more than twice as long.
rellen.github.io/iso-factories · line-4/index.html
17 / 28
Northside Works · Line 4

Faults, and who mends them

LevelShareWhoRepair
150%the operator or the tech3–8 s
235%the tech8–20 s
315%the tech and a contractora call-out, then 20–45 s

Estimates

After a look, each fault shows an estimate. The real repair comes in early a quarter of the time, and now and then blows out to more than twice as long.
rellen.github.io/iso-factories · line-4/index.html
17 / 28
Northside Works · Line 4

Faults, and who mends them

LevelShareWhoRepair
150%the operator or the tech3–8 s
235%the tech8–20 s
315%the tech and a contractora call-out, then 20–45 s

Estimates

After a look, each fault shows an estimate. The real repair comes in early a quarter of the time, and now and then blows out to more than twice as long.
rellen.github.io/iso-factories · line-4/index.html
17 / 28
Northside Works · Line 4

Faults, and who mends them

LevelShareWhoRepair
150%the operator or the tech3–8 s
235%the tech8–20 s
315%the tech and a contractora call-out, then 20–45 s

Estimates

After a look, each fault shows an estimate. The real repair comes in early a quarter of the time, and now and then blows out to more than twice as long.
rellen.github.io/iso-factories · line-4/index.html
17 / 28
Northside Works · Line 4

Faults, and who mends them

LevelShareWhoRepair
150%the operator or the tech3–8 s
235%the tech8–20 s
315%the tech and a contractora call-out, then 20–45 s

Estimates

After a look, each fault shows an estimate. The real repair comes in early a quarter of the time, and now and then blows out to more than twice as long.
rellen.github.io/iso-factories · line-4/index.html
17 / 28
Northside Works · Line 4

A monitor, not a constraint

Flag, never prevent

Impossible moves are logged and counted. A guard that silently stopped them would hide the bug.
  • 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
rellen.github.io/iso-factories · line-4/index.html
18 / 28
Northside Works · Line 4

A monitor, not a constraint

Flag, never prevent

Impossible moves are logged and counted. A guard that silently stopped them would hide the bug.
  • 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
rellen.github.io/iso-factories · line-4/index.html
18 / 28
Northside Works · Line 4

A monitor, not a constraint

Flag, never prevent

Impossible moves are logged and counted. A guard that silently stopped them would hide the bug.
  • 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
rellen.github.io/iso-factories · line-4/index.html
18 / 28
Northside Works · Line 4

A monitor, not a constraint

Flag, never prevent

Impossible moves are logged and counted. A guard that silently stopped them would hide the bug.
  • 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
rellen.github.io/iso-factories · line-4/index.html
18 / 28
Northside Works · Line 4

OEE = Availability × Performance × Quality

rellen.github.io/iso-factories · line-4/index.html
19 / 28
Northside Works · Line 4

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%

OEE at the world-class settings
rellen.github.io/iso-factories · line-4/index.html
20 / 28
Northside Works · Line 4

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%

OEE at the world-class settings
rellen.github.io/iso-factories · line-4/index.html
20 / 28
Northside Works · Line 4

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%

OEE at the world-class settings
rellen.github.io/iso-factories · line-4/index.html
20 / 28
Northside Works · Line 4

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%

OEE at the world-class settings

53%

at 1×, typical breakdowns, typical quality
rellen.github.io/iso-factories · line-4/index.html
21 / 28
Northside Works · Line 4

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%

OEE at the world-class settings

53%

at 1×, typical breakdowns, typical quality
rellen.github.io/iso-factories · line-4/index.html
21 / 28
Northside Works · Line 4

Widgets painted an hour, commit by commit

02000400060008000Before layer headsLayer headsSupplies keep paceWorld-class settingscommitWidgets painted an hour 6851
Widgets painted an hour, as the last commits changed the line
commitWidgets painted an hour
Before layer heads2517
Layer heads3032
Supplies keep pace4475
World-class settings6851
  1. Means over sixteen seeds × 8 hours at 4×, from the commit messages of rellen/iso-factories.
rellen.github.io/iso-factories · line-4/index.html
22 / 28
Northside Works · Line 4

What needs attention, first

The operator screen: the top bar with the line's state and the shift clock, the factory view, the status panel with OEE 95%, 7,641 painted an hour, supplies and the crew, and the controls along the bottom.
rellen.github.io/iso-factories · line-4/index.html
23 / 28
Northside Works · Line 4

A canvas with words

PartMarkup
The viewa canvas with role="img" and a written description
The figuresoutput elements
The controlsreal buttons and fieldsets, a visible focus ring, 44 px to hit
Machines & faultsa table: the keyboard route to every action
Reduced motionthe page starts paused
rellen.github.io/iso-factories · line-4/index.html
24 / 28
Northside Works · Line 4

A canvas with words

PartMarkup
The viewa canvas with role="img" and a written description
The figuresoutput elements
The controlsreal buttons and fieldsets, a visible focus ring, 44 px to hit
Machines & faultsa table: the keyboard route to every action
Reduced motionthe page starts paused
rellen.github.io/iso-factories · line-4/index.html
24 / 28
Northside Works · Line 4

A canvas with words

PartMarkup
The viewa canvas with role="img" and a written description
The figuresoutput elements
The controlsreal buttons and fieldsets, a visible focus ring, 44 px to hit
Machines & faultsa table: the keyboard route to every action
Reduced motionthe page starts paused
rellen.github.io/iso-factories · line-4/index.html
24 / 28
Northside Works · Line 4

A canvas with words

PartMarkup
The viewa canvas with role="img" and a written description
The figuresoutput elements
The controlsreal buttons and fieldsets, a visible focus ring, 44 px to hit
Machines & faultsa table: the keyboard route to every action
Reduced motionthe page starts paused
rellen.github.io/iso-factories · line-4/index.html
24 / 28
Northside Works · Line 4

A canvas with words

PartMarkup
The viewa canvas with role="img" and a written description
The figuresoutput elements
The controlsreal buttons and fieldsets, a visible focus ring, 44 px to hit
Machines & faultsa table: the keyboard route to every action
Reduced motionthe page starts paused
rellen.github.io/iso-factories · line-4/index.html
24 / 28
Northside Works · Line 4

Where each part goes

PartGoes toBecause
Simulationa Rust cratetyped, fast, deterministic, builds for both hosts
DrawingTypeScriptit is already JavaScript
Serving, viewers, historyElixir and Phoenixsupervision, fan-out, persistence

Why not the simulation in Elixir

Every sub-step needs one consistent view of the whole floor. One process per actor turns that into a coordination problem.
rellen.github.io/iso-factories · line-4/index.html
25 / 28
Northside Works · Line 4

Where each part goes

PartGoes toBecause
Simulationa Rust cratetyped, fast, deterministic, builds for both hosts
DrawingTypeScriptit is already JavaScript
Serving, viewers, historyElixir and Phoenixsupervision, fan-out, persistence

Why not the simulation in Elixir

Every sub-step needs one consistent view of the whole floor. One process per actor turns that into a coordination problem.
rellen.github.io/iso-factories · line-4/index.html
25 / 28
Northside Works · Line 4

Where each part goes

PartGoes toBecause
Simulationa Rust cratetyped, fast, deterministic, builds for both hosts
DrawingTypeScriptit is already JavaScript
Serving, viewers, historyElixir and Phoenixsupervision, fan-out, persistence

Why not the simulation in Elixir

Every sub-step needs one consistent view of the whole floor. One process per actor turns that into a coordination problem.
rellen.github.io/iso-factories · line-4/index.html
25 / 28
Northside Works · Line 4

Where each part goes

PartGoes toBecause
Simulationa Rust cratetyped, fast, deterministic, builds for both hosts
DrawingTypeScriptit is already JavaScript
Serving, viewers, historyElixir and Phoenixsupervision, fan-out, persistence

Why not the simulation in Elixir

Every sub-step needs one consistent view of the whole floor. One process per actor turns that into a coordination problem.
rellen.github.io/iso-factories · line-4/index.html
25 / 28
Northside Works · Line 4

Where each part goes

PartGoes toBecause
Simulationa Rust cratetyped, fast, deterministic, builds for both hosts
DrawingTypeScriptit is already JavaScript
Serving, viewers, historyElixir and Phoenixsupervision, fan-out, persistence

Why not the simulation in Elixir

Every sub-step needs one consistent view of the whole floor. One process per actor turns that into a coordination problem.
rellen.github.io/iso-factories · line-4/index.html
25 / 28
Northside Works · Line 4

Streaming the factory

02040608060 updates a second20 a second, smoothedRaw JSON6538Binary178.6Compressed WebSocket115.6
Kilobytes a second for each viewer, upper estimates
Raw JSONBinaryCompressed WebSocket
60 updates a second651711
20 a second, smoothed388.65.6
  1. From docs/porting.md: 2.4–5.6 KB/s at 20 updates a second, compressed.
rellen.github.io/iso-factories · line-4/index.html
26 / 28
Northside Works · Line 4

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?
rellen.github.io/iso-factories · line-4/index.html
27 / 28
Northside Works · Line 4

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?
rellen.github.io/iso-factories · line-4/index.html
27 / 28
Northside Works · Line 4

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?
rellen.github.io/iso-factories · line-4/index.html
27 / 28
Northside Works · Line 4

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?
rellen.github.io/iso-factories · line-4/index.html
27 / 28
Northside Works · Line 4

Open the page, and watch Line 4 run

rellen.github.io/iso-factories
This talk is in the same repository: talks/line-4, a Mix project with Expresso as a dev dependency.
rellen.github.io/iso-factories · line-4/index.html
28 / 28

Sources

22. Widgets painted an hour, commit by commit

  1. Means over sixteen seeds × 8 hours at 4×, from the commit messages of rellen/iso-factories.

26. Streaming the factory

  1. From docs/porting.md: 2.4–5.6 KB/s at 20 updates a second, compressed.

Keys and commands

Overview10 rows
  • ?/The search of the keys and commands. Esc closes it.
  • j→Page DownSpaceSelect the next slide
  • k←Page UpSelect the previous slide
  • ↓Select the slide below
  • ↑Select the slide above
  • HomeSelect the first slide
  • EndSelect the last slide
  • EnterStep 1 of the selected slide
  • Click or tap a slideStep 1 of that slide
  • oEscClose the overview. The step does not change.
Menu6 rows
  • ?/The search of the keys and commands. Esc closes it.
  • j↓Move the cursor to the next step
  • k↑Move the cursor to the previous step
  • EnterThe step of the cursor
  • Click or tap a step or a slideThat step
  • mEscClose the menu. The step does not change.
Present view19 rows

Move through the talk

  • j→↓Page DownSpaceNext step, or the first step of the next slide
  • k←↑Page UpPrevious step, or the last step of the previous slide
  • HomeFirst slide
  • EndStep 1 of the last slide
  • 0 to 9Type a slide number
  • EnterStep 1 of the slide that you typed

Find a slide

  • oOverview of the slides. Only this window shows it.
  • mMenu of the slides and their steps. Only this window shows it.

Change the screen

  • bBlack screen. The next key shows the slide again.
  • dCode in full color until the next step, in the two windows
  • tLight or dark variant of the theme, in the two windows
  • gProgress bar on or off
  • fFull screen on or off

Views and windows

  • sSpeaker view, in a second window
  • pHandout view
  • ?/The search of the keys and commands. Esc closes it.

Mouse and touch

  • Click or tap the right two thirds, or swipe leftNext step
  • Click or tap the left third, or swipe rightPrevious step
  • Click or tap a linkThe slide and the step of the link
Speaker view16 rows

Move through the talk

  • j→↓Page DownSpaceNext step, or the first step of the next slide
  • k←↑Page UpPrevious step, or the last step of the previous slide
  • HomeFirst slide
  • EndStep 1 of the last slide
  • 0 to 9Type a slide number
  • EnterStep 1 of the slide that you typed

Find a slide

  • oOverview of the slides. Only this window shows it.
  • mMenu of the slides and their steps. Only this window shows it.

Change the screen

  • bBlack screen. The next key shows the slide again.
  • dCode in full color until the next step, in the two windows
  • tLight or dark variant of the theme, in the two windows
  • fFull screen on or off

Views and windows

  • rSet the timer to 0:00
  • ?/The search of the keys and commands. Esc closes it.

Mouse and touch

  • Click or tap the right two thirds, or swipe leftNext step
  • Click or tap the left third, or swipe rightPrevious step
Handout view5 rows
  • jNext step. The present view then shows it.
  • kPrevious step. The present view then shows it.
  • pPresent view
  • aEvery step, or the steps of the handout option. A print shows the same.
  • ?/The search of the keys and commands. Esc closes it.

No key matches the search.