A new 'miter' feature angles a board's end — miter across the width (frame
corners, braces), bevel through the thickness (tilted blade), or both
(compound). Slots into the existing Feature system.
- scene: Feature.miter_deg/bevel_deg; END_KINDS={"miter"}; add_feature forces an
end face and defaults to 45° miter; serialization additive.
- geometry._apply_miter: subtracts a large block rotated about the end (Z=miter,
Y=bevel) so the viewer/export show the real angled end; guarded.
- cut list notes "miter 45° / bevel 15°"; instructions describe the angled end;
jigs suggest a miter sled/gauge for ≥3 repeated angle settings.
- cli feature --miter/--bevel; controller wood-feature passthrough; gen_wood_tools
wood-feature gains --miter/--bevel (re-run it for the voice tools).
- GUI Joinery tab: "+ Miter" button + Miter/Bevel angle fields (live edit/apply).
- tests: default 45°, end-face forcing, JSON roundtrip, cut-list note,
instructions, jig suggestion, and real geometry (volume reduced). 237 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Housekeeping over features, per Codex: consistency and portability now matter
more than another feature.
- driver.scene_summary no longer hardcodes ~/PycharmProjects/.venv/bin/woodshop;
new driver.woodshop_cmd() resolves the CLI portably (PATH, else `python -m
woodshop`). Used by the voice/GUI status path.
- scripts/gen_wood_tools.py: CMDFORGE_PY overridable via env; generated tool
bodies resolve `woodshop` at RUNTIME (shutil.which → python -m woodshop), no
baked-in local path; file-writing moved under main()/__main__ (was running at
import); PyYAML declared under dev deps.
- cutlist.py: drop the misleading "+10% waste" label — shopping already uses the
kerf-aware CutPlan nesting.
- Docs refreshed (README + CLAUDE): real test count, parametric joinery is
modeled, new cutplan/prices/estimate/inventory/colors modules + GUI windows,
portable tool regeneration.
- tests: driver path discovery (PATH + module fallback), generated tool bodies
compile and contain no hardcoded paths. 207 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New shop-packet output: a printable cost estimate driven by the active
CutPlan's buy-counts × a curated, editable price book (HST 15%).
- prices.py: DEFAULT_PRICES seeded with real Kent (New Brunswick) shelf
prices per buy-unit (lumber = 8' stick, plywood = 4x8 sheet); persisted to
$XDG_CONFIG_HOME/woodshop/prices.json (defaults + saved overrides).
estimate() -> CostEstimate (lines/subtotal/tax/total/missing); lumber price
scales with stick length; unknown stock is flagged, never invented.
- BOM window: Cost tab with "Edit prices…" (PriceEditDialog), "Refresh from
Kent…", and Print.
- fetch_kent_prices() + scripts/fetch_kent_prices.py: best-effort refresh.
Kent renders prices client-side (not in HTML), so it tries a static parse
then Playwright if installed — honest that it may need updating.
- tests: estimate math, per-sheet plywood, stick-length scaling, missing-price
flagging, save/load roundtrip, corrupt-file fallback, JSON-LD parse, cost
tab render + price edit persistence. 153 passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plywood is sheet stock — fixed thickness, width cut per-panel. lumber.py adds
ply-1/8…ply-3/4 specs + is_plywood/plywood_thickness and normalizes "3/4
plywood" -> "ply-3/4". scene.place(stock, length, width_in=) requires a width
for plywood (lumber ignores it). Cut list reports plywood in sq-ft and buys it
in 4×8 sheets (lumber stays board-feet / 8' sticks).
Wired through CLI (place --width), voice (wood-place width arg + prompt note),
and the Parts-tab manual add (plywood in the dropdown + a width field enabled
for plywood). Geometry/export/render work unchanged (section = thickness×width).
91 tests pass; verified a plywood top renders as a thin panel and exports to STEP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
connect() now RECORDS a Connection (anchor feature, moving feature) instead of
just moving a board, so connected parts form an assembly:
- scene.groups(): connected-component part groups via the connection graph.
- explode(distance): back each moving board off along its joint axis (exploded
view); assemble(): re-seat all (reverse); disconnect(cid/part): break a
connection — pieces stay in place but become independent.
- _seat() extracted from connect() so re-fit re-runs the mate math.
- delete() drops connections referencing the removed part; clear() resets them;
connections persist in scene.json.
Parts stay SEPARATE boards (not fused) so the cut list and disassembly keep
working — the assembly is a group, not a merge.
CLI: connections / disconnect / explode / assemble; voice: wood-connect/explode/
assemble/disconnect (25 tools). Fit dialog shows part names. 83 tests pass
(records+groups, explode/assemble roundtrip, disconnect keeps position).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Make connection" checkbox in the Fit dialog moves/orients the other board so
its tenon seats into the mortise (faces meet, insertion axes aligned, cross-axes
matched):
- scene.connect(anchor, moving): builds the moving feature's desired world frame
from Part.feature_world_frame, solves R = [dN|dU|dV]·[n|u|v]^T, decomposes to
yaw/tilt/roll via matrix_to_ypr (inverse of local_frame's Rz·Ry(-tilt)·Rx(roll)),
and positions so the contact points coincide. Verified: tenon-board stands and
seats into a top mortise; Euler round-trip exact.
- Feature.rotation_deg: spin a feature about its face normal (geometry rotates
the cut/add solid; preview + connect honor it) so cross-sections line up.
- Shared face_frame/rotation math moved to scene.py (geometry imports it).
- CLI `connect`, `--rotation` on features; voice `wood-connect`; GUI rotation
field + "Make connection" checkbox. 22 wood-* tools.
79 tests pass (ypr round-trip, connect seats tenon, rotated feature cuts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Features as re-editable objects attached to a board, each a boolean op:
- scene.py: Feature dataclass (kind/face/position/size/depth), Part.features,
add_feature/edit_feature/delete_feature/find_feature, serialization + counter.
- geometry.py: part_solid now builds the local board then fuses tenons / cuts
mortise/hole/slot/dado/rabbet via build123d booleans, then places it. _face_frame
maps each board face; holes are oriented cylinders, others oriented boxes.
- viewer.py: featured boards render the tessellated true solid (edges off to
avoid triangle noise); plain boards keep the fast pyvista box.
- cli.py: feature / feature-edit / feature-delete / features commands; status
shows feature kinds. gui/controller: wood-feature(-delete) dispatch.
- 21 wood-* tools (added wood-feature, wood-feature-delete).
64 tests pass (feature model + build123d volume/tessellation checks). Verified
with a render: tenon + mortise + through-hole on one board, and STEP/STL export.
Phase A (model + geometry + CLI/voice). Next: GUI feature panel; chamfers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A single PySide6 window combining the 3D viewport, parts panel, and command
bar — mouse, keyboard, and voice all drive the same scene and the same visible
selection (which resolves the "delete that" ambiguity).
- gui/controller.py: one in-memory Scene; buttons call typed methods, voice/
typed commands go through driver.interpret and apply via execute_call, which
REUSES the CLI command functions (no drift). Saves to disk + emits `changed`.
- gui/viewport.py: embedded pyvistaqt QtInteractor; click-to-select a board;
camera presets; reuses _part_mesh/_PALETTE.
- gui/panels.py: parts list + selected inspector (editable length/yaw/tilt) +
quick actions (stand/lay/rotate90/sand/duplicate/rename/delete).
- gui/command_bar.py + workers.py: text + push-to-talk mic + transcript +
speak toggle; LLM/dictate/TTS run on a QThreadPool so the UI never blocks.
- gui/main_window.py: layout + menus (File open/save/export/render, Edit
undo/redo/clear/delete, View cameras, Build templates + cut list, Help).
- Scene: added select() and redo() (+ _redo stack, CLI select/redo, wood-select/
wood-redo tools). driver.dispatch takes a pluggable executor; interpret takes
scene_text so the GUI feeds its in-memory state.
- Bare `woodshop` launches the studio; 'gui' extra; woodshop-gui entry point.
52 tests (incl. controller); GUI verified by import + offscreen controller
exercise (live VTK window needs a real display, untested headless).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Viewport (woodshop-view): part labels (id/name), dimensioned floor grid in
inches, parallel-projection isometric default, selection highlight, quieter VTK.
Named projects: woodshop save/open/projects (slugified names under
~/.local/share/woodshop/projects/); wood-save/open/projects tools.
Driver: concise spoken summaries (verb+count roll-up so "build a table" speaks
one short line, not 12; queries/clarifications spoken verbatim); per-utterance
errors no longer kill the session; auto-discovers all wood-* tools.
Docs: real README and CLAUDE.md (architecture, full command set, limitations).
17 wood-* tools. 41 tests passing.
Verified end-to-end: "build a coffee table" and "make a bookshelf side frame"
each produce correct multi-board models with cut lists and STEP/STL export.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3D orientation (the key gap): boards now have yaw/tilt/roll, so legs and
uprights can stand vertically. geometry.py and viewer.py apply the full
rotation; join is orientation-aware (vertical boards rest their base on the
target face). Old rotation_deg scenes migrate transparently.
New operations + CLI subcommands + wood-* tools: stand, lay, rotate, move,
trim (cut to length), copy, rename (human aliases, resolvable by name), clear.
Parts resolve by id OR name.
Cut list (cutlist.py): grouped cut list, board-feet (nominal), and an 8'-stick
shopping estimate with waste — the workshop-assistant payoff.
Driver: auto-discovers all wood-* tools (glob), richer prompt that decomposes
"build a table" into place/stand/join/move and labels parts. Verified: one
sentence -> an 8-board table base with a correct cut list.
14 wood-* CmdForge tools regenerated. 36 tests passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- driver.py (woodshop-talk): the conversational loop. Reuses dictate (STT),
pa-load-tools (schemas), claude -p (interpret), pa-execute-tool (dispatch),
read-aloud (TTS). Resolves $N symbols so multi-op utterances can reference
boards placed earlier in the same sentence; tolerates fenced/garbage output.
- wood-* CmdForge tools generator (scripts/gen_wood_tools.py): place/join/sand/
delete/undo wrappers over the woodshop CLI; arg descriptions double as the
LLM's command documentation.
- UX/realism fixes: lenient anchor parsing (end/start/far/near), and joins now
stack board B on A's face in Z instead of interpenetrating centerlines.
- Tests: 25 passing (added anchor, Z-stack, and driver symbol-resolution tests).
- CLAUDE.md: architecture, entry points, setup, known limitations.
Verified end-to-end (typed): the canonical sentence produces the correct 4-op
scene; follow-up commands on a non-empty scene resolve ids correctly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>