I built Box Grid as a tool that generates a custom grid of boxes for drawers and cabinets and lets you export them for 3D printing. I use it for my own drawers and cabinets and due to the nature of the tool, it has to be robust and reliable. And I found it to be a great learning experience buidling it in the first place, but now after severl month vacant I dove back into it. I wanted to add more features, but more often than not I found myself battling the code in fundamental places, so I decided to rebuild the foundation, without doing a full rewrite.
So in July I started rebuilding the parts of the code that were underlying the whole app, so I could add new features without breaking the existing ones or slowing down the user experience.
What was wrong
Every time the grid rebuilds due to a user action, Three.js replaces the meshes. State, selection, hidden boxes and layout lived next to the renderer or inside it. The store could still say a box was selected, but the new mesh had no highlight. Hide state hung on object IDs that died on redraw. Export could pick up helpers or whatever was on screen. Geometry helpers read the global store, so they were hard to test. Bad combine or invalid sizes failed softly or wiped state.
Moving to a new foundation
Several small changes were made, but the most important ones were the ones that moved the state out of the Three.js scene and into the domain model. Extracting the state had several first order benefits, like resizing no longer wiping combined or hidden topology. But also the model parameters got a single sanitize and clamp path so bad dimensions could not drive broken geometry. Hidden boxes were moved into the grid model itself, so the state was no longer tied to the renderer. And I cleaned up the Three.js lifecycle, disposing geometries and materials, dropping leaked listeners, tearing down the renderer so long sessions do not pile up GPU work and event handlers.
Geometry unit tests were also added for the first time, covering outlines, combine and split, visibility, unsupported shapes, empty or multi-loop groups. These tests gave me some more confidence to make larger structual changes and refactorings. Green tests are a good feeling when changing underlying code
Cleanup and improvements
The biggest structural change was the introduction of a scene view adapter. It owns the Three.js setup, the render loop, raycasting, highlighting, camera controls, resize, animation and GPU cleanup. React hooks are used to bridge lifecycles and to expose the view to the application. Grid combine, split, and visibility, keyboard shortcuts, and layout sync live in application hooks and pure command functions. The page composes state and UI into a single component, but it does not own the renderer. The domain model is the single source of truth for the grid, and the view is a pure function of the domain model used to render it. This makes the whole code base more modular and easier to understand and to build upon.
For example, the combine command now has explicit topology checks before mutation. Disconnected, diagonal only, holed, empty, undersized, out-of-bounds, and unsupported selections are rejected without clearing selection. Outline analysis and mutation sit behind one safe combine API that makes it easier to test. Or the new minimum box size calculation now comes from wall thickness, corner radius, and clearance. The slider minimums now follow that rule: keep the configured max-box size when a valid partition exists and only raise it when no valid partition fits.
Now was also the time to add CI to this project, to make work with agents easier. The whole CI is super simple and only takes a few seconds to typecheck, lint, format, test, and build the project. This raises my confidence when looking at code that was generated and they can fix their own issues until the CI checks pass. I could be sure that the basic app is running and I only need to focus on the changed parts of the PR.
Layouts that survive refresh and sharing
Well, this is technically a new feature, but it was a byproduct of the new foundation. The layout of your drawers and cabinets now survives refresh and are now shareable by URL. This is done with a simple codec that encodes the layout into a hash that is appended to the URL. When the page is loaded, the layout is decoded from the hash and applied to the grid. If there are issues with the hash, the layout is loaded from localStorage and only if that fails, the default layout is applied.
This is a very simple implementation of a feature that will be expanded in the future to support sharing more complex layouts with other users or making a libary of predefined layouts that can be used as a starting point for new projects.
Much of these changes are invisible in the UI. But that is the point. The underlying layer has to be solid before I can add more fun features.