# Follow one orange through a juice factory

Brix is a UI concept for an orange juice line, built in three.js. It runs a simulated shift you can scrub through, cuts any machine open like a drawing, and keeps the tables a plant team works in beside the 3D, reading the same numbers.

> Can a 3D view of a running plant share one screen with the tables its team already works in, without the two ever disagreeing?

Status: Prototype · Disciplines: Design engineering, Product design, Interaction design · Built with: three.js, React Three Fiber, TypeScript, TanStack Table, Zustand · Topics: 3D interfaces, Manufacturing, Traceability · Year: 2026

Source: https://www.ahmedamr.com/lab/brix

*A live prototype runs at this point on the page.*

Brix is an orange juice factory you can open in a browser tab. Fourteen
stations run from the weighbridge where the fruit trucks come in to the dock
where the pallets leave, and a shift is running on all of them: oranges on the
belts, juice in the pipes, a robot stacking packs, forklifts putting pallets
away. Press Run the line above, or open it full screen, which is the size it
was made for.

Screens for a plant tend to come in two kinds. One is a 3D model of the
place, which shows where everything is and is lovely in a demo. The other is
the screens the plant runs on: tables of truck receipts, tank movements, lots
and pallets, where the work gets done and nothing is drawn. Brix puts both on
one screen, and both read from the same numbers.

It is a UI concept. No plant stands behind it and no data comes in from one.
The shift is simulated, the growers and customers are made up, and every
number in it is a sample.

## One shift, read at any minute

Everything in Brix comes from one model of a shift, worked out minute by
minute when the page loads. Twenty tonnes an hour of Valencia oranges go
through four extractors, which gives about 9,800 litres of juice an hour into
storage tanks. Blend tanks take turns feeding a pasteurizer and a filler rated
at 12,000 one-litre bottles an hour. Trucks arrive on a schedule, tanks fill
and drain, the lab releases blends, and pallets go out on trailers.

The interface keeps no numbers of its own. The dot over a machine, the
reading inside it, a row in a table and the bottle count at the bottom of the
screen all ask the model for the same thing at the same shift time. Because
the whole shift exists before you look at it, the timeline can jump to any
minute and everything on screen still agrees. Drag it back an hour and the
tanks are at their old levels, and every pallet made since has left the
warehouse table.

Three things go wrong during the shift, each a different kind of trouble. A
gasket on the pipe rack lets go, and the only sign is juice that leaves the
extractors and never reaches the finisher. A bottle falls over at the filler's
infeed, and the stop runs both ways: the blow molder backs up behind it, and
the labeler, packer and robot run dry one after another. The pasteurizer's
steam valve sticks shut, and juice comes out of it below its safe
temperature. Each plays out on a fixed script from the minute it starts, so it
looks the same live, scrubbed or replayed.

## Cut open like a drawing

Pick a machine and it opens the way a section opens on an engineering
drawing. A plane slices through the casing, the half nearest you goes, and the
walls the plane passes through are filled with a hatch. Inside, numbered pins
walk through what the machine does, and round instrument tags show the live
readings where the sensors would sit.

<div className="bleed">
  <LayerSwitch
    hidePanelLabel
    layers={[
      {
        id: "closed",
        label: "Closed",
        body: (
          <Figure
            bleed={false}
            id="brix-cut-closed"
            title="The tank farm and pipe rack, closed"
            description="The middle of the line with every casing whole."
            expand
          />
        ),
      },
      {
        id: "open",
        label: "Cut open",
        body: (
          <Figure
            bleed={false}
            id="brix-cut-open"
            title="The same view, cut open"
            description="The same view with every machine sectioned and its walls hatched."
            expand
          />
        ),
      },
    ]}
  />
</div>

Cutting the shape is the easy half. three.js can clip any material against a
plane, and nothing on the far side of it is drawn. What that leaves is a shell
with no walls: through the cut you see the inside of the far side, and the
edge nearest you is a line with no thickness. It reads as a broken model.

Filling the cut uses the stencil buffer, a hidden layer that keeps a whole
number for every pixel on the screen. Picture the line from your eye through
one pixel, starting where it meets the plane and running on into the scene. If
that starting point is in open air, the line goes into the casing and comes
out of it the same number of times, so it crosses as many surfaces facing you
as facing away. If the point is inside a wall, the line has to leave the wall
before anything else, so it crosses one more surface facing away. The
difference is zero in the air and one in the wall.

So each casing is drawn twice more, invisibly, clipped by the same plane, with
depth testing off so that every surface along the line is counted. Surfaces
facing away add one to their pixels; surfaces facing you take one away. A flat
square lying on the plane then draws its hatch only where the count is not
zero, which is the wall, and puts the count back to zero as it draws, so the
next machine's cut starts clean. This is the method in three.js's own
clipping example, and the sample below is it cut down to its parts.

**Count the walls, then hatch them** · `section-cut.ts`

```ts

// What stays is the side the normal points to.
const plane = new THREE.Plane(new THREE.Vector3(0, 0, -1), 0);

function countingPass(side: THREE.Side, op: THREE.StencilOp) {
  return new THREE.MeshBasicMaterial({
    side,
    clippingPlanes: [plane],
    colorWrite: false, // draws nothing you can see
    depthWrite: false,
    depthTest: false, // counts every surface along the line
    stencilWrite: true,
    stencilFunc: THREE.AlwaysStencilFunc,
    stencilFail: op,
    stencilZFail: op,
    stencilZPass: op,
  });
}

// Surfaces facing away add one; surfaces facing you take one away.
const away = countingPass(THREE.BackSide, THREE.IncrementWrapStencilOp);
const toward = countingPass(THREE.FrontSide, THREE.DecrementWrapStencilOp);

// The square on the plane: hatch where the count isn't zero, then reset it.
// Brix draws a hatch here with a small shader; a flat colour shows the idea.
const cap = new THREE.MeshBasicMaterial({
  color: 0x4ea7fc,
  stencilWrite: true,
  stencilRef: 0,
  stencilFunc: THREE.NotEqualStencilFunc,
  stencilFail: THREE.ReplaceStencilOp,
  stencilZFail: THREE.ReplaceStencilOp,
  stencilZPass: THREE.ReplaceStencilOp,
});
```

The two counting passes and the cap go on the casing's meshes and the square,
with render orders that draw them in that sequence, and the renderer needs
`localClippingEnabled` switched on and a context created with a stencil
buffer.

The cost is in the modelling. The count only comes out right when every casing
is a closed solid, an outer skin and an inner skin a wall's thickness apart,
with no gap anywhere along the cut. So the tanks, the extractors and the
pasteurizer are all built that way, in code, along with the rest of the plant.
Brix loads no model files and no textures. The surfaces, from the epoxy floor
and the brushed steel to the pine of the pallets, are painted by a worker
thread when the page opens, as near-white detail that each material tints with
its own colour.

## Follow one orange

Thousands of oranges go through the plant during a shift, and none of them is
anyone in particular. Follow one orange picks one off a truck and keeps the
camera on it until it leaves on another.

The ride is a script of beats, and each beat moves the orange along a path the
plant already uses, on the same clock as everything else: the belt the other
fruit is riding, the pipes, the robot, the forklifts. Where the orange would
only be waiting, in a bin with 28 tonnes of other fruit or in a tank while the
lab checks the blend, the shift clock jumps ahead and the screen says by how
much.

Once it is squeezed there is nothing left to see, because from the extractor
on the juice is never in the open. So its share, about 90 millilitres, is
drawn through the steel as an orange slug, the way a tracer shows up on an
X-ray, and the camera follows it into a tank, through the pasteurizer and over
to the filler. After that it is a bottle, one of six in a pack, one of 105
packs on a pallet, and one of 33 pallets on a trailer.

<Figure
  id="brix-ride-pipes"
  title="The juice in the pipes"
  description="The followed orange's juice traced through the pipes past the finisher, with its record open."
  stage
  expand
/>

The panel in the middle is the orange's record, and it fills in as the ride
goes: picked at an orchard, tipped from a truck that carried 19.6 tonnes at 12.1
°Brix, squeezed in cup 3 of extractor EX-402, then screened, spun, stripped of
air, stored and blended. Juice mixes in a tank, so from storage on the record
can only say which tank, and in what window of time. Every line in it is read
off the same shift model as the plant around it.

## The tables stay

The 3D view shows where things are. Counting them, sorting them and finding
the one with a problem still happens in a table, so Brix keeps the tables.

Each stage of the line has one: truck receipts, the extraction log, tank
movements, the pasteurizer's log, filling lots, pallets. Beside them sit the
records the line exists to serve, customer orders, work orders and the lab's
batch results. They open as a sheet along the bottom of the screen, under the
plant or over all of it.

<Figure
  id="brix-records"
  title="Pallets under the plant"
  description="The pallets table open under the 3D view, with one pallet's record beside it."
  stage
  expand
/>

A row exists only once the thing it records has happened, so the tables move
with the timeline too. Point at a row and the plant shows you where it is.
Open it and the record sits beside the table, with a button to find it in 3D
and another to rewind the shift to the minute it was made. A pallet traces back
through its filling lots to the blend, and from the blend to the trucks of fruit
that went into it.

People can add to the records as well: book a truck, log a hand reading at
the pasteurizer, take an order, plan a work order, put a lot or a pallet on
hold. Each of these shows a notice with an undo, except the pasteurizer
reading, which is a food-safety record and stays once it is written.

## When the pasteurizer runs cold

Every drop of juice has to reach 92 °C and stay there for 30 seconds before it
can go near a bottle. A thermometer at the end of the holding tube checks it,
and under 90 °C a valve sends the juice back round instead of on to the
filler. In food-safety terms that is a critical control point, and the
pasteurizer is the line's first.

When the steam valve sticks, the temperature falls through that limit, the
valve starts sending juice back, and the filler runs out of juice to fill.
The alarms follow ISA-18.2, the standard for alarm management in the process
industries:
raised, acknowledged, back to normal. Each priority has a shape as well as a
colour, so it still reads without the colour.

The question anyone on that shift would ask is whether the juice going into
bottles is safe, and Ask the line takes it in those words. It answers the way
an operator would find out. It opens the pasteurizer, reads the thermometer at
the outlet, checks the shift against the limit, and writes its answer from the
numbers at that moment, with links to the readings it used.

<Figure
  id="brix-ask"
  title="Asking about the pasteurizer"
  description="Ask the line answering whether the pasteurizer is safe while its steam valve is stuck."
/>

There is no language model in it. Each question Ask knows is a short script of
those steps, and only the wording around the numbers is written ahead of
time; the numbers come from the shift when you ask. It knows a handful of
questions, about the stops, the pasteurizer, where a bottle came from and
whether the plan will be met, and asked anything else it lists those.

## One shape for the interface

The first versions of Brix had the panels a control room screen is expected
to have: a bar across the top, a column of tools, a minimap, a strip of stages
along the bottom and a panel down the right. Next to the plant they were a lot
of words, and the layout sat too close to the reference concepts I had
started from.

I mocked up three layouts over real renders and chose the one that keeps the
least on screen. The plant now takes the whole window, and the interface is
one object at the bottom of it, which I call the island. At rest it says one
thing, bottles made against the day's plan. Point at it and it opens into the
controls: the timeline, play and speed, the line's state, the six stages, the
tables, Ask, and a menu for the rest. Pick a machine or a record and it grows
into a card; open the tables and it becomes the sheet. It is the same object
changing shape each time, so nothing you were looking at moves somewhere you
have to look for it. Alerts dock on top of it, whatever shape it is in.

Over the line each machine is a dot in its state's colour, and pointing at one
opens its name and its live number. Colour is kept for things that mean
something. Orange is juice, blue is the cut and the safety checks, and green,
yellow and red are machine states. The rest is graphite greys and hairlines in
the manner of Raycast and Linear. Two earlier looks, a light control-room grey
and a navy with glowing orange, were both dropped on the way.

The island's first motion took 520 milliseconds with a 4% overshoot, and
switching the sheet between half and full screen felt slow and glitchy. It now
grows in 380 milliseconds on a spring that overshoots by about 1.5%, shrinks
in 260 with none, and the contents fade in a beat after the frame starts
moving. Only the frame animates: its size goes straight to its style rather
than through React, so the table inside is not rendered again on every frame
of the move.

## Where it went wrong

Early on, every material in the plant began to shimmer whenever the
camera moved. With the camera still, one frame was identical to the next, so a
screenshot showed nothing at all. Finding it took a probe: turn the camera by
about a twentieth of a pixel each frame, read the pixels back every frame, and
flag the ones that jump. Bigger steps flag every edge that moves. Then each
material was swapped for a flat colour of its own, so every flagged pixel
could be traced to the material it belonged to.

The worst of it was the floor. The coloured zones on it sat 3 millimetres
above the hall floor, too close for the depth buffer to tell apart from the
overview's distance, so the two traded places as the view moved. They are a
centimetre apart now, and the camera's near plane moves out to about 2.5
metres at the overview and comes back to half a metre for close-ups. The
ambient occlusion pass, which darkens corners, was running on too few samples,
and its grain crawled over every machine; more samples cost nothing measurable.
And on braces turned at exactly 45°, the texture flipped between two
orientations from one pixel to the next, until the tie went the same way
every time.

Two findings from the hunt hold for any three.js scene. When the lighting
comes from `scene.environment`, the `envMapIntensity` of a standard material
with no map of its own is overwritten on every draw, and only
`scene.environmentIntensity` changes anything. And SMAA, a smoothing pass, made the shimmer worse on top of 4x
multisampling. It also loads its lookup textures after a delay, so a
measurement taken right after switching it on shows it doing nothing.

Some shimmer is still there, on the thinnest parts at the overview: pipes,
rails and floor stripes crawl a little along their edges. The scene already
uses as much multisampling as the GPU allows, and rendering at a higher
resolution would remove it at a real cost in frame rate.

## Further reading

- [Clipping with stencil caps](https://threejs.org/examples/#webgl_clipping_stencil),
  the three.js example. The method behind the section cuts.
- [camera-controls](https://github.com/yomotsu/camera-controls), Yomotsu.
  The camera under every move in Brix, the ride's included.
