12 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
WoodShop - Voice-driven conversational 3D woodworking & furniture modeler. Speak (or type) commands like "place a 6 foot 2x4, sand it, attach a 2 foot 2x4 at 90 degrees 10 inches from the end" and watch the model build in a live 3D viewport — Holodeck-style.
Architecture
Design principle: reuse existing CmdForge tools for everything that isn't woodshop-specific; don't reinvent voice/AI plumbing.
woodshop-talk (driver.py) ── the conversational loop
│ dictate ............... speech→text (CmdForge tool, reused)
│ pa-load-tools ......... wood-* → Claude schemas (reused)
│ claude -p ............. interpret utterance → JSON tool calls (reused provider)
│ pa-execute-tool ....... dispatch each wood-* tool (reused)
│ read-aloud ........... speak confirmation (reused)
▼
scene.json ← single source of truth (parts, joints, selection, undo stack)
▲ │ writes
│ reads/mutates ▼
wood-* CmdForge tools woodshop-view (viewer.py)
(place/join/sand/delete/undo) watches scene.json → live pyvista 3D
thin wrappers over `woodshop` CLI
Only woodshop-specific code lives in this repo: the scene model
(scene.py), nominal→actual lumber table (lumber.py), length parsing
(units.py), the woodshop CLI (cli.py), build123d geometry + STL/STEP
export (geometry.py), the pyvista viewport (viewer.py), and the driver
(driver.py). The driver uses Claude (not pa-tool-loop, which hard-wires a
small local model) for reliable structured tool-calling.
Entry points
| Command | Purpose |
|---|---|
woodshop (no args) / woodshop-gui |
The unified desktop studio (viewport + parts panel + command bar) |
woodshop <op> |
CLI ops: place, join, stand, lay, rotate, move, trim, copy, rename, sand, delete, select, undo, redo, clear, status, cutlist, export, render, save, open, projects |
woodshop-view |
Standalone live 3D viewport (watches scene.json; labels, grid, isometric) |
woodshop-talk |
Standalone conversational driver (--voice for mic, --once "..." for one command) |
The studio (src/woodshop/gui/) is a thin PySide6 shell over the same Scene +
operations + interpreter:
controller.py— one in-memoryScene; buttons/menus call typed methods, voice/typed commands go throughdriver.interpretand are applied viaexecute_call, which reuses the CLI command functions (no behavioral drift). Every mutation saves to disk and emitschanged.viewport.py— embeddedpyvistaqt.QtInteractor; click a board to select.panels.py— parts list (ExtendedSelection: Ctrl/Shift multi-select) + selected-part inspector (editable length/yaw/tilt) + quick-action buttons.command_bar.py— text + push-to-talk + transcript, with slow work (LLM/dictate/TTS) on aQThreadPool(workers.py).numpad.py— a numberpad control panel (2/4/6/8 move, 1/3/7/9 rotate, +/− raise/lower, 0/. front/iso, 5 fit) that also responds to the physical numpad keys (MainWindow.keyPressEvent forwards them when not typing).- Spatial feedback:
scene.spatial_summary()(each board's world bounding box viaPart.bbox()+ flagged interpenetrations) is fed into the interpreter prompt, so the AI can reason about where boards are — e.g. "position the left side flush against p2" becomes a computedwood-move, and it can fix overlaps. - Manual add: the Parts tab has a stock dropdown + length + "Add board" to
place a board instantly without the AI (
controller.place). - Multi-selection:
controller.selectedis a list driven by 3D Ctrl+click (viewport.pickedcarries an additive flag) and list multi-select. Group ops (move_selected/rotate_selected/stand/lay/sand/delete) apply to all selected in one undo step viascene.batch(). Voice "move these" works because the selected ids are fed into the interpreter prompt.
Scene file location: $WOODSHOP_SCENE or ~/.local/share/woodshop/scene.json.
Named projects: ~/.local/share/woodshop/projects/<slug>.json.
Stock is dimensional lumber (lumber.NOMINAL_TO_ACTUAL, fixed cross-section) or
plywood sheet stock (ply-3/4, ply-1/2, …): fixed thickness, but a width
given per-panel — place(stock, length, width_in=) requires a width for plywood.
The cut list reports plywood in sq-ft and buys it in 4×8 sheets (lumber stays
board-feet / 8' sticks).
Parts have full 3D orientation (yaw_deg/tilt_deg/roll_deg) so legs and
uprights stand vertically. Parts can be referred to by id (p1) or by a name
set with rename. Each part also carries material + finish (raw/sanded/
clear/stain/paint) + finish_color, which drive the viewer color and cost.
Shop output & inventory subsystems (all deterministic; AI only narrates):
cutplan.py is the keystone — kerf-aware cutting-stock nesting into a JSON-
serializable CutPlan (rough-vs-final sanding allowance, batch quantity, owned-
offcut reuse, grouped by (stock, material)). prices.py = editable Kent-NB price
book + material-aware cost; estimate.py = full quote (consumables + labor +
suggested price); inventory.py = shop-wide event-sourced stock/offcut/build
ledger; instructions.py/jigs.py = build steps + jig suggestions. The GUI
Cut List & BOM window (gui/bom_window.py) and Inventory window
(gui/inventory_window.py) render these. cutlist.py is a quick text summary.
CmdForge tools (the documented command vocabulary)
wood-place, wood-join, wood-sand, wood-delete, wood-undo live in
~/.cmdforge/<name>/ and wrap the woodshop CLI. Regenerate them with
python scripts/gen_wood_tools.py if their schemas change (set CMDFORGE_PY to
point at your cmdforge interpreter; needs PyYAML). The generated wrappers resolve
woodshop at runtime (PATH, else python -m woodshop), so they're portable. The
arg descriptions ARE the LLM's documentation, so keep them clear.
Setup
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[viewer,dev]" # viewer extra pulls build123d + pyvista
pytest # 200+ tests
Known limitations / next steps
- Joins are flush butt joints: B's end sits flush against A's surface, and B is aligned to A's reference corner (top faces level + one side flush) rather than centered. The flush corner is fixed (A's +width/+thick side; no per-join choice of which corner / centered).
- Joinery features (
Featureon eachPart) are parametric ops applied ingeometry.part_solid:tenonfuses a protruding tongue;mortise/hole/slot/dado/rabbetcut a box/cylinder into a face;chamferbevels the edges around a face via build123dchamfer()(handled specially —_apply_chamfer, with a try/except fallback for over-sized bevels). The viewport tessellates featured boards (plain boards stay fast pyvista boxes). Edit paths: CLIfeature/feature-edit/feature-delete/features; voicewood-feature/wood-feature-delete; GUI Joinery tab (feature_panel.py) — add-with-default, then dragging a field shows a live red preview ghost (viewer.feature_preview_mesh— a cheap box/cylinder, no re-tessellation) of the pending change over the committed feature; Apply commits it (controller.set_preview/apply_preview,preview_changedsignal →viewport.set_preview). Per-kind hints + field tooltips explain the parameters.controller.active_featureis the one being edited. A Fit to mate… button (controller.fit_feature) resizes a mortise to a chosen tenon (or vice versa) — pocket = tongue + clearance (1/32"), pocket slightly deeper; a dialog lists the complementary features (features_of_kind). The dialog's Make connection checkbox callscontroller.make_connection→scene.connect(anchor, moving), which moves/orients the moving feature's board so the tenon seats into the mortise (faces meet, axes aligned). Connect builds the target world rotation from the feature frames and decomposes it to the board's yaw/tilt/roll viamatrix_to_ypr(inverse ofPart.local_frame's Rz·Ry(-tilt)·Rx(roll));Part.feature_world_framegives each feature's world point/normal/u/v. Features also haverotation_deg(spin about the face normal) to line up cross-sections. CLIconnect; voicewood-connect. - Connections / assemblies:
connectRECORDS aConnection; connected boards form an assembly (scene.groups(), connection-graph union-find). They stay separate boards (not fused — cut list & disassembly keep working). Ops:scene.explode(d)backs each moving board off along its joint axis,assemble()re-seats (reverse),disconnect(cid/part)breaks (pieces stay put)._seat()is the shared mate math;connect()then_drag_group()applies the same rigid transform to the moving board's existing sub-assembly (minus the anchor's group) so connected boards travel together. The GUI Fit dialog lets you pick which board repositions (make_connection(move_self=)). CLIconnections/disconnect/explode/ assemble; voicewood-explode/assemble/disconnect. The GUI Parts tab is a QTreeWidget grouping connected boards under an assembly node, with a right-click menu (back off / re-fit / break). Not yet: countersinks, click-a-face-to-place, per-assembly naming/rename. - Latency ~7–13s per utterance (one
claude -pcall). - Voice path (
--voice) reusesdictate; the driver loop is hardened against failures but the mic path isn't exercised in the unit tests. - Auto-placement of parts in a multi-step "build a table" request depends on the LLM choosing good offsets; geometry is correct but corners may need nudging.
⚠️ CRITICAL: Updating Todos, Milestones, and Goals
DO NOT edit todos.md, milestones.md, or goals.md files directly.
These files are managed by Development Hub which has file watchers and sync logic. Direct edits will be overwritten or cause conflicts.
Use the devhub-tasks CLI instead:
# Status overview
devhub-tasks status woodshop
# Add todos
devhub-tasks todo add woodshop "Task description" --priority high --milestone M1
# Complete todos (by text match or ID number)
devhub-tasks todo complete woodshop "Task description"
devhub-tasks todo complete woodshop 3
# List todos
devhub-tasks todo list woodshop
# Add milestones
devhub-tasks milestone add woodshop M2 --name "Milestone Name" --target "March 2026"
# Complete milestones (also completes linked todos)
devhub-tasks milestone complete woodshop M1
# Goals
devhub-tasks goal add woodshop "Goal description" --priority high
devhub-tasks goal complete woodshop "Goal description"
Use --json flag for machine-readable output. Run devhub-tasks --help for full documentation.
Files you CAN edit directly: overview.md, architecture.md, README.md, and any other docs.
Development Commands
# Install for development
pip install -e ".[dev]"
# Run tests
pytest
# Run a single test
pytest tests/test_file.py::test_name
Architecture
TODO: Describe the project architecture
Key Modules
TODO: List key modules and their purposes
Key Paths
- Source code:
src/woodshop/ - Tests:
tests/ - Documentation:
docs/(symlink to project-docs)
Documentation
Documentation lives in docs/ (symlink to centralized docs system).
Before updating docs, read docs/updating-documentation.md for full details on visibility rules and procedures.
Quick reference:
- Edit files in
docs/folder - Use
public: truefrontmatter for public-facing docs - Use
<!-- PRIVATE_START -->/<!-- PRIVATE_END -->to hide sections - Deploy:
~/PycharmProjects/project-docs/scripts/build-public-docs.sh woodshop --deploy
Do NOT create documentation files directly in this repository.