ZoneX Layout Builder
Prototype
Tool · Web

ZoneX Layout Builder

2025

Figma-style top-down venue editor for feeding crowd-simulation engines. A Next.js 16 / React 19 app with a zustand store draws 21 element kinds — boundaries and fences, main and side stages, general and VIP zones, walkways, stairs, seating blocks, static and breakable barricades, entrance/exit/emergency/turnstile gates, restrooms, water stations and food stalls — plus a pen tool for freehand boundaries and walkways. Elements carry configurable metadata and support drag, resize handles and rotation, and layouts export as JSON for the simulator or SVG for documentation. Two things are modelled rather than usable: visibility and locking exist on five layers in the store but the panel that would drive them is not mounted, and the renderers handle bezier segments while the pen only collects bare points, so every curve is a polyline until handle editing exists.

Built with
Next.jsNext.js
ReactReact
TypeScriptTypeScript
zustandzustand
CSS ModulesCSS Modules
SVGSVG
Project Details

STATUS
Prototype
YEAR

2025

TYPE

Tool · Web

TAGS
Editor
Canvas
Simulation
Design Tool

A crowd simulator needs a venue described in numbers: how many people a gate passes per minute, what pressure a barricade fails at, how wide a walkway is in metres. Drawing that venue in a general design tool gives you a picture instead. ZoneX is the authoring step in front of the simulator, and the JSON it emits is the actual deliverable; the canvas exists to make that file easy to produce.

The obvious alternative is to skip the editor and hand-write the JSON, which works right up until a venue has forty elements and somebody needs to move the VIP zone three metres left. The other alternative is to draw the venue in Figma and transcribe it, which produces two artefacts that disagree the moment one of them changes. An editor whose native save format is the simulator's input format has neither problem, and that constraint is what shapes everything below: the element list is not a shape palette, it is an enumeration of things the simulator knows how to model.

The export is the product

Every element type carries a schema of simulation parameters, declared once in a 352-line element config and surfaced automatically in the inspector. Gates differ by throughput, not just by colour: an entrance defaults to 60 people per minute across 2 lanes, an exit to 80 across 3, an emergency exit to 120, a turnstile to 20. A general admission zone carries a capacity and a density limit in people per square metre. A breakable barricade carries a strength, a break pressure of 5,000 N and a maximum force of 8,000 N. Walkways have a width in metres and a flow capacity; restrooms and food stalls have service rates and queue capacities. Zones also carry an allowedRoles array, so access restriction is part of the layout rather than a separate config.

The config is the single source of truth for four separate things at once, which is why it is worth the length. Each entry declares the element's fill, stroke, dash pattern and default footprint for the canvas; its layer category, which is what decides where the element lands rather than any manual assignment; its defaultMeta, which is what a freshly placed element serialises as; and its metadataFields, which is a small form schema - key, label, type, unit, min, max, step, and options for enumerations. The inspector reads that list and renders a number input, a text box, a checkbox or a select accordingly. Adding a parameter to the simulator means adding one object to the config; no component changes.

What the editor does

  • 21 element kinds across structure, zones, gates, barriers and amenities, each with its own fill, stroke, dash pattern and default footprint.
  • Five layers (structure, zones, barriers, amenities, gates) held in the store as ordered records that are visibility-toggled, lockable and reorderable, with z-index driving paint order.
  • A custom SVG canvas at 1200x700 with a 40px grid, hand-rolled rather than built on a canvas library.
  • Drag, resize and rotate: eight resize handles with a minimum size clamp of 20px, and rotation computed from the atan2 angle between the cursor and the element centre, normalised into 0-360 and rounded to whole degrees.
  • A pen-style curve tool that collects points on click, finishes on double-click and cancels on Escape, storing each vertex as a CurvePoint with optional incoming and outgoing bezier handles.
  • Zoom and pan clamped between 0.1x and 5x, panned with middle-mouse or space-drag, with click coordinates run back through the inverse transform so placement stays accurate at any zoom.
  • Keyboard editing: arrow keys nudge by 1px, Shift-arrow by 10px, both clamped to the canvas bounds; Delete removes; Escape deselects.
  • Grid snapping on Ctrl-click, rounding placement to the nearest 40px cell.
  • A live tool readout across the top of the canvas that changes with state - naming the element about to be placed, or counting the points collected so far while the curve tool is open.
  • Two exports: JSON carrying canvas dimensions, every element with its metadata, and a version stamp for the simulator; SVG for documentation.

Hit-testing without a diagramming library

The interaction layer is the part that would normally be somebody else's dependency. Each element renders as an SVG <g> carrying translate(x y) rotate(deg cx cy), and pointer maths runs through createSVGPoint and getScreenCTM().inverse() to convert screen coordinates into canvas coordinates. Drag stores the element's origin and the pointer's origin on mousedown, then applies the delta on every move with the result clamped to the canvas rectangle. Resize does the same but branches on which of the eight handles was grabbed: the four corner cases adjust width, height and origin together, so dragging a north-west handle grows the box up and left rather than moving it. Rotation takes the angle from the element's centre to the cursor at grab time, subtracts it from the live angle, and adds the difference to the stored rotation.

Placement is the one path that has to undo the view transform by hand. A click on the SVG gives a point in unscaled canvas space, and the elements live inside a zoomed and panned group, so the handler subtracts pan and divides by zoom before deciding where the new element goes - and applies grid snapping after that, so a snapped placement is snapped in venue coordinates rather than screen ones.

Honest scope

This is a prototype built in a single sitting, and the two exports are not equally finished. The JSON path is complete and is the one that matters. The SVG path renders each element as a rectangle with its short label, so curved boundaries and walkways do not survive that route, and the layer table is not yet written into either file. There is also no undo stack, no multi-select, and no persistence - the store is plain zustand with no storage middleware, so a refresh empties the canvas.

The layer model is further along in the store than it is on screen. Visibility, locking and reorder actions all exist, and a getVisibleElements selector filters by them, but the panel that would drive them is not mounted in the app shell and the canvas renders the raw element list, so toggling a layer has nowhere to happen yet. The curve tool has the same shape of gap in the other direction: the data model and both renderers handle cubic and quadratic bezier segments, but the pen only ever collects bare points, so in practice every curve is a polyline until a handle-editing UI exists. Finished curves are also always filed as curve-boundary regardless of which curve tool produced them, and are stamped with a capitalised layer name that does not match the lowercase layer ids - which is exactly the bug the unmounted panel would have surfaced immediately.

Two more rough edges worth naming. The grid is painted outside the zoom-and-pan group, so it stays fixed while the venue moves over it. And drag and resize read the pointer through the SVG's own CTM rather than the transformed group's, so they are correct at 100% and drift as you zoom - the click handler compensates for the transform and the drag handler does not.

The interaction model underneath is real, though: hit-testing, transform inversion, resize and rotation maths are all written directly against SVG rather than delegated to a diagramming library, and the parameter schema is complete enough that a simulator could be written against the exported file today.

Project Details

STATUS
Prototype
YEAR

2025

TYPE

Tool · Web

TAGS
Editor
Canvas
Simulation
Design Tool