PinballEditor
Build your own pinball table while you play: move parts, grow branches, and share tables.
What it does
Press E, or click EDIT TABLE bottom left. A panel opens on the right, with a Parts tab and a Tables tab. The game keeps running, so you can move a slingshot, launch a ball at it, and move it again.
- Drag a part on the table to move it. It stays on the wood. It will not go through the outer walls or into the shooter lane; push it against a wall and it stops flush.
- Drag from the list to add a part: pop bumper, slingshots, flippers, drop target, spinner, rollover lamp, saucer, rubber post, wall, one-way gate, ramp. Let go back over the list and it is not added.
- ⌘ + arrows nudge the picked part 0.05 units a press. A unit is 32 mm, so that is 1.6 mm. Hold Option for ten times as much.
- ⌘ ⇧ + ← / → turn it 1° a press (Option: 15°). The buttons turn it 15°.
- Delete takes it off the table. Undo puts one part back. Reset table puts them all back.
- Mouse wheel scrolls up and down a tall table. Drag a part to the top or bottom edge and the table scrolls too.
- Esc lets go of the part. E closes.
Lamps and halos painted under a part go with it.
Changes are saved as you make them, in the table you are on (see Tables below), and come back when the table loads.
Branches
Drag Branch from the list to just off an edge of the table. A new floor grows there, flush against that edge, with the bottom every floor needs: walls, two flippers on their guides, two slingshots, and a trough under the flippers.
Two wire rails join it to the floor it grew from. A mouth just inside the parent's edge lifts the ball up and drops it in at the top of the branch. When a ball drains past the branch's flippers it rolls to the point of the trough, and the second rail carries it back down to the parent. Only the main table's drain loses a ball.
Drop a branch onto another branch and it grows off that one, so the table becomes a tree. Branches never overlap, and none hangs below the main table's bottom. Drag a branch's floor to move it round its parent's edges; everything on it comes too, its own branches included. Delete on a branch takes the whole subtree.
The camera follows the ball sideways onto a branch.
Tables
The Tables tab works like git branches.
- New starts a table as the scene ships it. Branch off copies the table you are on into a new one of your own.
- Export saves the table as a file. With a branch picked, Export branch saves just that branch and everything on it. Import opens either kind: a table becomes a new table, and a branch grows onto the picked branch, or onto the main table.
- Find tables searches the tables other people shared. Play opens one. It is read-only until you change something; then it becomes your own branch of it, and theirs stays as it was. Branch off makes your own copy straight away.
- Share link copies a link that opens your table.
Every table is kept in this browser. With apiBase set, each change is also copied up to the games server a couple of seconds later. The server has no accounts: the browser that made a table holds its edit key, and only that key can change it.
How a move works
Every part is scene JSON in table coordinates. A bumper is a MeshPrimitive position. A slingshot is three corners. A wall is a list of points, a ramp two ends, a rail a path. So moving a part is one transform: turn every coordinate it holds about the part's centre, then shift it. A flipper turns by its restAngle and flipAngle, and a saucer turns its kick-out.
When you let go, the part is made again: its entity is removed and a new one with the same name is made from the moved JSON. Each component's System builds it fresh, meshes and bodies and all, the same way it does from a scene. While you drag, nothing is rebuilt: the part's meshes slide along with the pointer.
Use it in a scene
A scene is just data — a list of entities, each with its components. At startup the SceneLoader turns this JSON into a live world; edit the file and reload to rebuild it.
Build it from scratch with the bjs CLI:
- 1Scaffold a project
World, renderer, and dev server — ready to run.
- 2Install dependencies
- 3Add PinballEditor
Copies its source into src/ so the scene resolves.
First time? Run bjs login once.
- 4Paste the scene into src/scenes/arcade-room.ts and run
SceneLoader builds the world from the JSON; reload to rebuild.
Props
scene(string, default""). The scene it edits; each table records it as its base. Empty means the last scene loaded.toggleKey(string, default"KeyE"). The key that opens and closes it.startOpen(boolean, defaultfalse).nudge(number, default0.05). Units per ⌘ + arrow press.rotateStep(number, default1). Degrees per ⌘ ⇧ + arrow press.locked(string[]). Entities it never moves: the outer walls, the lane, the lane gate, the plunger rod.storagePrefix(string, default"pinball-editor:"). Tables are kept under<prefix>tables.apiBase(string, default""). Where shared tables live, e.g."/api/pinball/tables". Empty keeps tables in this browser only.urlParam(string, default"table").?table=<id>opens a shared table.importFrom(string[]). Older scene names whose saved layout becomes your first table.
Events
- Emits
pinballEditor.opened { open },pinballEditor.changed { name, op },pinballEditor.tableOpened { key, name, serverId, readOnly }. - Asks
pinballScrollCamera.hold { z }to scroll the table,pinballScrollCamera.boundswith the tree's size, andpinballPlayfield.repaintafter a part with paint under it moves. - Listens to
pinballEditor.toggle { open? }.
The parts list only offers parts whose component the scene has loaded.





















