M2 is complete. All four sections (Materials, Constraints, Assemblies, Parts) are editable. Recalculate is pinned to the top bar with a live calc summary (orange when there are unapplied changes; constraint blurs auto-recalc). The height-driven primary-part logic visibly resizes the cabinet as you change caster height, top allowance, etc. Save / Load / New, CSV export, Print Cut List all live. Project name is click-to-rename. Side panels collapse. Color picker on materials updates the 3D view live. Ready for an end-to-end walkthrough via the Getting Started section above. Next: M3 formulas.
A single-file HTML tool for designing shop projects in 3D. You define materials, parts, and constraints; it gives you a parametric 3D view and an accurate cut list. Built for cabinets, workbenches, rolling carts — the everyday shop stuff.
Open it
Double-click index.html in this folder. It opens in your default browser. No build step, no server, no internet needed once Three.js loads.
What you'll see on first load
Mobile Tool Cart36" − 4" − 1" − 0" = 31" → Left Side [Recalculate] · · · Help · Save · Load · New
Grouped by material
Sorted by area, biggest first
Sheet count + board feet
[CSV] [Print] buttons
Status bar — parts count · board feet · sheets · last recalc time
The default Mobile Tool Cart loads with five parts, three materials, and a working set of constraints. The calc summary and Recalculate button live in the top bar so they're always visible.
The view bar
Front / Top / Side are flat orthographic views. 3D is perspective with mouse-drag orbit. Reset snaps the 3D view back to the default angle.
Editing your first part
In the left panel, find the Parts section. Click the ⋮ on any card (e.g. "Left Side") to expand it.
Change the W value from 0.75 to 1.5. Tab to the next field — Tab navigates through inputs natively.
Look at the Recalculate button on the right. It just turned orange with a dot — that means you have unapplied changes.
Click Recalculate. The 3D view updates with the thicker sides, the cut list refreshes, and the button goes back to white.
Left Side
★⋮
W0.75
H31
D24
X0
Y4
Z0
An expanded part card. Always-visible row 1: name, height-driven star, material, ⋮ toggle. Row 2: W / H / D. Below the divider: position, grain, height-driven checkbox, notes, plus Duplicate and Delete.
Recalculate button states. White = clean (nothing to apply). Orange = you've edited something and the 3D view + cut list are stale until you click it.
Adding a new part
At the bottom of the parts list, click + Add Part. A new card appears with default dimensions (12 × 12 × 0.75), expanded for editing, with the name field selected. Type a name, Tab through W/H/D, pick a material from the dropdown.
Duplicating a part
Click ⋮ on any card to expand it, then click Duplicate. A copy is added with "(copy)" appended to the name and offset 2" in X so it doesn't sit on top of the original.
Try the height-driven Recalculate (the centerpiece feature)
This is the trick that makes the tool actually useful. The cabinet's "primary" part (Left Side, marked with ★) auto-resizes to keep your overall target height correct as casters, top thickness, and toe kick change.
Open the Project Constraints section in the left panel.
Find Caster height. Change 4 to 5. Tab away.
The Left Side instantly shrinks by 1" to keep the cabinet at 36" total. The 3D view, cut list, and top-bar calc summary all update.
Now toggle Casters off entirely. Left Side grows by the former caster height. Cabinet still 36" total.
Change Top allowance from 1 to 2. Left Side shrinks by 1".
This same logic works for any cabinet — set your target height, mark the part you want auto-resized as "Height-driven" in its expanded card, and pick it as the Primary part. Done.
Each material is a card with a color swatch, name, and type badge (sheet/lumber). Click the swatch to open a color picker — the 3D view updates live as you drag. Click ⋮ to expand for thickness, type, sheet width, sheet length. Click + Add Material to define a new one.
Deleting a material that's used by parts shows a confirm with what'll happen — those parts get reassigned to the next available material so you don't end up with broken references.
Editing assemblies
Most projects have one assembly (the default "Base Cabinet"). For a multi-part project — say a workbench with separate cabinets — click + Add Assembly, give it a name, set its X/Y/Z position. Then in any part's expanded card, pick the new assembly from the Assembly dropdown. Parts inherit their assembly's position offset so you can move whole sub-assemblies in one go.
Saving and loading projects
Your work doesn't auto-save (deliberately — no localStorage). Use the top bar buttons:
Save downloads the current project as <projectname>.woodshop.json. Drop it anywhere.
Load opens a file picker — pick a .woodshop.json file to restore.
New resets to the default Mobile Tool Cart (with confirm, so you don't lose work).
Click the project name in the top bar to rename. Enter to commit, Escape to cancel.
Exporting the cut list
Top of the right panel:
CSV downloads the cut list as a CSV (Material, Part, Assembly, W, H, D, Grain, Quantity). Opens cleanly in Numbers / Excel.
Print opens your browser's print dialog with a clean print-friendly layout: project name, date, calc summary, cut list grouped by material with sheet/board-foot estimates, and a constraints summary at the bottom. Save as PDF if you don't need paper.
Collapsing the side panels
Each side panel has a thin "rail" at the top with a toggle button. Click ◀ on the left panel (or ▶ on the right) to fold the panel into a 36px-wide strip. The 3D view grows to fill the recovered space. Click again to bring the panel back.
▶
← collapsed left panel; click ▶ to expand
When collapsed, the side panel shows only the rail. The 3D viewport fills the space.
Tips
Tab navigation works between inputs — handy for fast spreadsheet-style editing.
Numeric fields turn red while you type if the value isn't a number; on blur, they revert to the previous valid value.
★ next to a part name means it's the height-driven primary part — its height gets recalculated from the constraints.
Auto-recalc happens when you add, duplicate, or delete a part. Manual Recalculate is needed only after editing dimensions on existing parts.
Visual reference
Quick-reference of the main UI elements. Mockups update as the app changes.
App layout
Project nameHelp · Save · Load · New
Left (320px)
Project panel — collapsible
3D viewport (flex)
Right (380px)
Cut list — collapsible
Status bar
View bar
Active view button highlighted in brown. Reset is styled in muted italic to read as an action, not a view.
Part card — collapsed
Right Side
⋮
W0.75
H31
D24
Part card — expanded (orange border)
Left Side
★⋮
W0.75
H31
D24
X0
Y4
Z0
Recalculate button states
clean — nothing to apply
dirty — you have unapplied edits
Milestone plan
Milestone
Scope
Status
M1
Skeleton that renders. Layout, Three.js scene, view buttons, default cart visible, grid + axis indicator, status bar. No editing.
Bug fix — 3D view "way down": radius was max_dim * 2.5 + 30, putting the camera ~110" away with the cabinet at ~46% of viewport. Now uses the bounding-box diagonal with a tighter multiplier (diag * 1.4 + 10), so the cabinet fills the view properly and sits at the visual center.
Bug fix — caster controls: the height input had no label next to the checkbox, making it unclear what it was. Split into two clearly-labeled rows: "Casters" (checkbox) and "Caster height" (number).
Top-bar Recalculate (UX #4 resolved): the calc summary and Recalculate button are now pinned to the top bar, always visible: 36" − 4" casters − 1" top − 0" toe kick = 31" → Left Side [Recalculate]. Removed from the Cut List header to declutter that panel.
Editable assemblies: add/edit/delete with name + X/Y/Z position. Each part card now has an Assembly dropdown in its expanded body. Deleting a non-empty assembly reassigns its parts to the next available assembly (or creates a default one).
CSV export: "CSV" button in the Cut List header downloads a clean cut list (Material, Part, Assembly, W, H, D, Grain, Quantity) ready for Numbers/Excel.
Print Cut List: "Print" button opens a print-friendly view with header (project name + date + recalc summary), cut list grouped by material with sheet/board-foot summaries, and a constraints table. Uses @media print + a hidden #print-view div — no UI chrome on paper.
New helpers: refreshPartAssemblyDropdowns(), buildPrintViewHTML(), csvEscape().
2026-04-25 · M2 partial
Editable materials — color picker, type, sheet dims, delete with reassign
Materials panel uses the same card pattern as parts. Always visible: color swatch · name · type badge · ⋮ toggle.
Click ⋮ to expand and edit type (sheet/lumber), thickness, sheet width, sheet length. The sheet-dim fields hide automatically when type is lumber.
Color picker is native HTML5 (input[type=color]) and updates the 3D scene live while you drag — instant feedback, the painterly part of designing.
+ Add Material at the bottom appends a new material with sensible defaults, expands it, focuses the name. Part dropdowns refresh automatically.
Delete on a material that's in use shows a confirm with a note like "3 parts use this material and will be reassigned to '3/4 Birch Ply'" — pick the next material as fallback. Cleaner than letting parts go dangling. (Bends the brief, which says "flag an error" — but for DIY users, broken state is worse than auto-reassign.)
If you delete the last material, dependent parts get materialId: null; meshes fall back to gray and the cut list groups them under "?". The user can then add a new material and reassign in the part cards.
New helper: refreshPartMaterialDropdowns() — surgical update of every part card's material <select> options without rebuilding the cards. Called from recalculate() so material name edits propagate to part dropdowns instantly.
Auto-recalc fires on blur of material text/numeric fields (per brief). Color and type changes apply immediately.
2026-04-25 · M2 partial
Save / Load / New + project rename
Save downloads the current project as <projectname>.woodshop.json. Filename is auto-derived from the project name (spaces → hyphens, special chars stripped).
Load opens a file picker, parses the JSON, and replaces the in-memory project. Forgiving of slightly malformed files: missing arrays default to empty; missing constraints default to the default-cart's; mismatched version shows a confirm dialog instead of crashing.
New shows a confirm dialog and resets to the default Mobile Tool Cart.
Project name in the top bar is now click-to-rename: contenteditable on click, Enter commits, Escape reverts. Updates browser tab title too.
applyLoadedProject() mutates the existing project object in place rather than reassigning, so all closures keep working.
New // === IO === section in JS holds save / load / new / rename / sanitize.
This is the first form of persistence — no localStorage (per brief), so saving is the only way to keep work across sessions. The Save button is now prominent in the top bar.
2026-04-25 · M2 partial
Editable constraints panel — Recalculate height-driven logic now visible
Constraints panel is now an editable form: target height/width/depth (nullable), casters toggle + height, top allowance, toe kick, primary part dropdown, calc summary, notes textarea.
Auto-recalc fires on blur of any constraint input (per brief). Toggling casters or picking a different primary recalcs immediately.
The 3D view now visibly responds to constraint changes: change caster height from 4 → 5, Tab away, the Left Side shrinks by 1" to keep total at 36".
New updateConstraintsUI() does surgical updates (calc summary, primary-part dropdown options, caster-height disabled state, primary part's H input) instead of replacing the whole panel — preserves focus while tabbing through inputs and avoids the "browser loses focus mid-Tab" bug.
Toggling Height-driven off on a part that was the primary auto-clears primaryHeightPart, so the dropdown stays consistent.
Constraint listeners are attached once via event delegation on the panel container (no listener stacking on re-render).
Known M2 limitation: turning casters on/off doesn't move part positions — Y values are hardcoded numbers in M2. Once formulas land in M3, parts that reference constraint("casterHeight") in their position will shift correctly.
2026-04-25 · docs
Help link in app + Getting Started + visual mockups
Top bar of index.html now has a Help link that opens DOCS.html in a new tab.
DOCS gained a Getting Started section: what the tool is, how to open it, what you see, your first edit, adding/duplicating parts, collapsing panels, tips.
DOCS gained a Visual reference section: quick-reference mockups of the layout, view bar, part cards (collapsed + expanded), Recalculate states.
"Screenshots" are CSS recreations of the actual UI rather than image files — they update with the docs and don't bloat the repo.
Cross-doc links at the top of DOCS now point to App / Plan / UX / Brief.
2026-04-25 · M2 partial
Editable parts cards + Recalculate dirty state (UX #2, #3 resolved)
Parts panel is now an editable list of cards. Each card always shows: Name (text), Material (dropdown), W / H / D (numeric). Click ⋮ to expand for X / Y / Z, Grain, Height-driven, Notes, Duplicate, Delete.
Tab navigation between fields just works (native browser behavior). Focusing a numeric field highlights it; invalid values turn red until corrected and revert on blur.
Add Part button at the bottom appends a new part with sensible defaults, expands it, and focuses the name input.
Duplicate on each card clones the part (with "(copy)" suffix and a 2" X-offset so it's not on top of the original) — covers UX #3.
Delete with a confirm dialog. If the deleted part was the primary height part, that pointer is cleared.
Recalculate button is now live. Clean state: white "Recalculate". After any field edit: orange "Recalculate ●". Clicking it applies primary-height-driven logic, rebuilds the 3D scene, refreshes the cut list and constraints summary, and clears the dirty state.
Add / duplicate / delete auto-recalc immediately (per brief). Field edits set dirty until you click Recalculate.
Expanded-card state is preserved across re-renders via an expandedCards Set — no card collapses unexpectedly when the list rebuilds.
New section in JS: parts cards live inside UI, recalculate / dirty machinery is at the bottom of UI.
2026-04-25 · M2 partial
Iso → 3D + Reset View (UX #7 resolved)
Replaced the redundant Iso and Orbit buttons with a single 3D button (perspective + drag-orbit) and a separate Reset action.
3D mode preserves the camera angle when you switch away and back; Reset snaps it to the default iso angle.
setView('3d', { reset: true }) is the single entry point for both initial boot and the Reset button.
Internal currentView values are now: 'front' | 'top' | 'side' | '3d'. Drag-orbit only fires when currentView === '3d'.
Bends the brief (which listed 5 view buttons including Iso); spirit preserved — flat views + 3D + a one-click reset.
2026-04-25 · M2 partial
Collapsible side panels (UX #1 resolved)
Each side panel now has a thin "rail" at the top with a collapse toggle (◀ / ▶).
When collapsed, the panel folds to a 36px-wide strip showing only the rail; the 3D viewport flexes to fill the recovered space.
Toggling triggers a viewport resize and refits the ortho frustum so the 3D view stays correct.
CSS is driven by --left-w / --right-w custom properties on :root, swapped by body.left-collapsed / body.right-collapsed classes.
2026-04-25 · M1 complete
Initial skeleton — render-only
Created index.html (~795 lines, single self-contained file, Three.js r128 from CDN).
Three-column layout (320 / flex / 380) with top bar and status bar.
Default Mobile Tool Cart project hardcoded with all five parts, three materials, one assembly, full constraints panel.
Three.js scene: ortho camera for Front/Top/Side, perspective for Iso/Orbit. Camera object swaps; one renderer / one scene.
Custom orbit controls (no OrbitControls import) — drag rotates only in Iso/Orbit, mouse wheel zooms in all views.
Grid floor at Y=0: fine 1" grid + bolder 12" grid, layered.
Axis indicator (X/Y/Z arrows + sprite labels) rendered as bottom-left scissor-test inset, mirrors active camera direction.
Cut list grouped by material, sorted by area desc, quantity-collapsed, sheet-count w/ 15% waste buffer for sheet goods, board feet for lumber.
Section banners in JS: DATA, RESOLVE, RENDER, UI, BOOT.
resolve(value) chokepoint in place — every dim/position read goes through it. M3 will swap in the parser without touching callsites.
Created plan.html with milestone breakdown.
Open UX questions
Questions waiting on user input. Each links to where it would land in the milestone plan.
1. Target screen size? — resolved
Resolved by adding collapsible side panels (2026-04-25). Click the ◀ / ▶ in each rail to fold the panel to a 36px strip; click again to bring it back. The 3D viewport reflows automatically.
2. Parts editing flow — forms, side panel, or inline table? — resolved
Resolved as inline editable cards (2026-04-25). Always-visible row: Name, Material, W, H, D. Expand a card for X, Y, Z, grain, height-driven, notes, plus Duplicate / Delete. Tab navigation works natively.
3. Duplicate-part button? — resolved
Yes. Each expanded card has a Duplicate button. Clones with "(copy)" suffix and a 2" X-offset.
Resolved (2026-04-25) by pinning the calc summary + Recalculate button to the top bar so they're always visible. The constraints panel itself stays in the left column for editing.
5. Formula reference picker?
Typing part("Left Side").height is verbose. A "+" button next to formula fields opening a picker (pick part → pick property → inserts the reference) speeds entry ~3×. Lands in M3.
6. Cut list vs 3D — split or tabs?
When designing, 3D matters most; when prepping for the shop, cut list does. A tab toggle on the right column ("3D | Cut List") would let the active view take all the space. Lands in M2 if chosen.
7. Iso vs Orbit — collapse into one? — resolved
Resolved by replacing Iso + Orbit with a single 3D view button plus a separate Reset action (2026-04-25). 3D preserves the angle across view switches; Reset snaps back to default.
Locked-in decisions
Architectural choices made. Don't relitigate without a strong reason.
Single resolve(value) chokepoint
Every dim and position is read through one function. M1–M2 it returns numbers; M3 swaps in the formula parser without touching any callsite. Why: avoids a rewrite when formulas land.
Data model frozen at brief shape
Materials, Parts, Assemblies, Constraints, top-level Project with version: 1. Any change here breaks save files. Why: save format must round-trip from M2 onward.
One renderer, one scene, swap the camera object
Ortho for Front/Top/Side, perspective for Iso/Orbit. Custom orbit controls — no OrbitControls import (single CDN script tag rule). Why: simplest mental model, no juggling render targets.
No eval(), no browser storage, single HTML file
Hard requirements from the brief. Not negotiable.
Section banners in JS
DATA · RESOLVE · RENDER · UI · IO · BOOT. New code goes in the matching section; cross-section calls go through the documented entry points only.
Known issues / TODO
Severity
Item
Lands in
done
All sections (Materials, Constraints, Assemblies, Parts) editable. Save / Load / New, CSV export, Print Cut List, top-bar Recalculate summary all live.
M2
limitation
Toggling casters on/off doesn't move part positions in M2 (positions are hardcoded numbers). Fully fixed when formulas land in M3 — parts can reference constraint("casterHeight") in their position.y.
M3
deferred
No formula support; default cart's formula strings are pre-resolved to numbers.
M3
deferred
Click-to-select in 3D and parts list highlight not wired up.
M4
deferred
Print stylesheet, CSV export, keyboard shortcuts.
M2 / M4
verify
Side view looks from +X (right side). Confirm this is the wanted convention; some users expect the left side.
M2
verify
Top view "up" is set to −Z so the back of the cabinet is at screen-top. Confirm this matches the user's mental model.
M2
Last updated: 2026-04-25 — M2 done · ready for end-to-end test via Getting Started