Commit Graph

23 Commits

Author SHA1 Message Date
rob 20327ee9d3 Highlight the selected feature in the 3D scene (cyan)
Clicking a feature in the Joinery tab now highlights it in the viewport with a
cyan ghost so you can see which mortise/tenon it is; browsing candidates in the
"Fit to…" dialog highlights each one as you select it (restored to the active
feature on cancel). Reuses the overlay mechanism with a kind ("edit"=red pending
vs "highlight"=cyan); controller.highlight_feature drives it.

84 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 09:56:49 -03:00
rob 774ddd3480 GUI: assembly subtree + connection right-click menu
Parts tab is now a QTreeWidget: connected boards group under an "⛓ Assembly"
node (expandable) showing their members; standalone boards stay top-level. A
board with features is flagged ⊕. Multi-select still drives controller.selected
(selecting an assembly node selects its members).

Right-click menu: Back off connections (explode), Re-fit connections (assemble),
Break this board's connections / Break all. Controller wrappers: explode/
assemble/break_connections/groups.

83 tests pass; GUI imports clean (live tree behavior needs a display to verify).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 09:39:48 -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 6f829a2c50 Add "Fit to mate" — size a mortise to a tenon (and vice versa)
In the Joinery tab, a tenon/mortise shows a "Fit to mortise…/tenon…" button
that opens a dialog listing the complementary features on other boards; picking
one resizes the active feature to mate:
- mortise = tenon cross-section + 1/32" clearance, pocket slightly deeper;
- tenon = mortise opening − 1/32" clearance, tongue reaching the pocket bottom.
controller.fit_feature + features_of_kind; commits + re-renders.

71 tests pass (fit mortise->tenon dims).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 16:17:08 -03:00
rob aabf289562 Cut list accounts for protruding tenons
A tenon adds length beyond the board's end, so the real piece you cut is longer
than length_in. cutlist.cut_length() now adds end-tenon protrusions to the cut
length used by the cut list, board-feet, and the buy-list (subtractive features
like mortises/holes don't change the stock you buy, so they're ignored). The
cut list notes when lengths include tenons.

70 tests pass (end-tenon extends cut length; cut features don't).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 16:04:20 -03:00
rob 70f8e9f0a2 Live red preview + Apply for feature editing
Adjusting a feature's fields was abstract and gave unclear feedback. Now:
- Dragging any field (Face/Along/Across/Width/Height/Depth/Diameter) shows a
  live translucent RED ghost of the pending feature over the committed one —
  a cheap pyvista box/cylinder (viewer.feature_preview_mesh), no re-tessellation,
  so it updates instantly.
- An Apply button commits the pending edit (controller.set_preview /
  apply_preview, preview_changed -> viewport.set_preview red overlay).
- Per-kind hint text + per-field tooltips explain what each parameter does.

68 tests pass (preview-then-apply, preview-mesh builds). Verified by render:
committed mortise + red ghost of a moved/resized pending edit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:39:00 -03:00
rob d0e40cdcbc Show real edges on featured boards (fix flat/edgeless look)
Featured (tessellated) boards were drawn with show_edges off to avoid triangle
noise, which left them looking flat and edgeless. Now overlay only the true
geometric edges via pyvista extract_feature_edges (corners, hole rims, chamfer
bevels, mortise walls) — crisp like plain boards, no mesh noise. Selected
boards get yellow edges. Applied in both the standalone viewer and the GUI
viewport. Verified by render: hole, mortise, tenon, and chamfer all read clearly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 14:32:42 -03:00
rob 9cbff4ec78 Add GUI Joinery panel (Phase B) + chamfers
GUI feature panel (gui/feature_panel.py), a "Joinery" tab beside Parts:
- Add buttons (Tenon/Mortise/Hole/Slot/Chamfer) drop a sensibly-sized feature
  on the selected board (add-with-default), then edit fields (face, along,
  across, width, height, depth, diameter) live; a feature list to pick which to
  edit; Delete. controller.active_feature tracks the one being edited, with
  size defaults derived from the board (controller._feature_defaults).

Chamfers (edge bevels):
- New EDGE_KINDS={"chamfer"}; geometry._apply_chamfer selects the edges around a
  face and bevels them with build123d chamfer(), clamped + try/except so an
  over-sized bevel can't crash the build. Verified: end + top-edge chamfers
  render and reduce volume.

66 tests pass (added chamfer volume + oversize-fallback). Verified GUI imports;
live window still needs a real display.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 13:54:04 -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 417bf39d09 Add multi-select + numberpad control panel
Multi-select:
- Ctrl+click in the 3D view (viewport.picked carries an additive flag) and
  Ctrl/Shift in the parts list (ExtendedSelection) build controller.selected.
- Group ops (move_selected/rotate_selected/stand/lay/sand/delete) apply to every
  selected board in ONE undo step via new scene.batch() context manager.
- Voice "move these 4 inches in +y" works: the selected ids are fed into the
  interpreter prompt, which expands to one call per selected board.

Numberpad panel (gui/numpad.py):
- Buttons laid out like a numpad: 4/6/8/2 move X/Y, +/- move Z, 7/9 yaw, 1/3
  tilt, 0 front, . iso, 5 fit. Configurable move-step and angle-step.
- The physical numpad keys do the same — MainWindow.keyPressEvent forwards
  KeypadModifier keys to the panel (unless typing in the command box).

Scene: batch() coalesces checkpoints so a group action is a single undo.
56 tests passing (added batch, toggle-multiselect, group-move-undo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:47:39 -03:00
rob 9d21816542 Flush-by-default joins (corner alignment)
Boards now align to A's reference corner when they butt — top faces level and
one side flush — instead of B floating centered on A. The flush step snaps B's
+faces onto A's +faces along A's cross-section axes, skipping the axis B extends
along so the butt contact is preserved. Equal-size flat joints are unchanged;
mixed sizes (e.g. a 1x8 shelf on a 2x4) now line up cleanly (tops level).

Test: a 1x8 joined to a 2x4 sits tops-flush (center z=0.375), not centered.
53 tests passing; verified with a render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:21:55 -03:00
rob 7d01144143 Fix stuck "thinking…" — background task GC dropped the done signal
The QRunnable was auto-deleted the instant its work finished, destroying its
signals object before Qt delivered the queued result to the UI thread, so the
"done" callback never fired and the command bar stayed disabled. Now tasks keep
a strong reference (autoDelete off) until the result is delivered, then drop it.

Also: the WoodShop reply summary is now logged to the transcript on success
(previously computed but never shown), and the error path re-enables input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:15:22 -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 b24e65548e Fix woodshop-view not closing
The live-update loop spun forever and ignored window close, so the viewport
window couldn't be dismissed. Now it detects closure (close flag via q/Escape
key events, plotter._closed, or render_window going away) and breaks the loop,
then calls plotter.close() to tear down cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:26:14 -03:00
rob 391bbcb3f9 Real butt-joint geometry (faces, not centerlines)
Boards now connect like real lumber: B's end butts flush against A's surface,
offset from A's centerline by A's cross-section half-extent in B's approach
direction (width/thickness, whichever B faces). Previously B's center landed on
A's centerline, so boards interpenetrated and shared centerlines.

- Added Part.local_frame() (length/width/thickness world axes via composed
  rotation matrices, matching geometry/viewer).
- join() computes the surface-contact offset; handles perpendicular T/L joints
  and vertical legs (leg base butts the rail face).
- Tests: butt joint meets surface not centerline; example sentence updated;
  vertical-leg join still correct. 45 passing.

Default alignment is B centered on A at the attach point. Not yet: joinery cuts
and flush-outer-face alignment options.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 02:31:20 -03:00
rob 17e7554ff1 Add `woodshop render <file.png>` for headless viewing
Saves an off-screen PNG of the scene (labels, grid, isometric) so the model can
be inspected without an interactive GUI window — useful over SSH or when
woodshop-view can't open a display. 44 tests passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 02:15:17 -03:00
rob 7b5c58902c Harden command parsing (review fix)
interpret() now extracts the FIRST balanced [...] array and tolerates code
fences / trailing prose, instead of a greedy [.*] that could swallow trailing
bracketed text and fail to parse. Falls back gracefully to a spoken apology.

Added regression tests for trailing brackets, fenced objects, and garbage.
44 tests passing; edge cases (angle 0, offset 0, negative moves, unknown
stock) verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 01:55:12 -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
rob a688623caf Add PoC core: scene model, operations, geometry, viewer
Voice-driven woodworking modeler core (the woodshop-specific half;
voice/AI plumbing will reuse existing CmdForge tools).

- scene.py: Part/Joint/Scene model, place/join/sand/delete/undo, JSON
  persistence (atomic), selection + undo stack
- lumber.py: nominal->actual dimensional lumber table
- units.py: parse "6 ft" / "3 ft 6 in" / "10 inches" to inches
- cli.py: `woodshop` CLI (place/join/sand/delete/undo/export/status)
- geometry.py: build123d solids + STL/STEP export
- viewer.py: live pyvista viewport watching scene.json
- tests: 20 passing, including the canonical example sentence
- pyproject: woodshop + woodshop-view entry points, [viewer] extra

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 01:15:01 -03:00
rob 70591ad6fe Initial project setup
Created by development-hub/new-project script
2026-05-29 00:59:35 -03:00