Commit Graph

10 Commits

Author SHA1 Message Date
rob b284b58229 Add angled end cuts: miter + bevel
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>
2026-05-31 00:12:48 -03:00
rob 970b88bc7b Portability + consistency polish (Codex review)
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>
2026-05-30 21:56:28 -03:00
rob e4f9cedf4a Add plywood (sheet stock)
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>
2026-05-30 12:33:49 -03:00
rob fad56f4fc3 Track connections as assemblies (back-off / break / re-fit)
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>
2026-05-30 09:35:34 -03:00
rob e35020382d Add auto-assembly (Make connection) + feature rotation
"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>
2026-05-29 17:15:51 -03:00
rob a0072e6271 Add joinery features (parametric boolean tenon/mortise/hole/slot)
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>
2026-05-29 13:27:57 -03:00
rob e9422aa133 Add unified desktop studio (woodshop / woodshop-gui)
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>
2026-05-29 11:05:39 -03:00
rob 892a376669 Polish viewport, add named projects, concise voice summaries, docs
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>
2026-05-29 01:50:07 -03:00
rob 914c86303f Add 3D orientation, richer operations, and cut list
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>
2026-05-29 01:42:33 -03:00
rob fa03ee71d3 Add voice/conversational loop reusing CmdForge tools
- 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>
2026-05-29 01:28:36 -03:00