Attach a photo (📎 button, drag-drop, paste, or an image URL) and the driver
hands it to claude -p, which reads the image (its Read tool sees images) and
emits the usual tool-call JSON to build a simplified, buildable interpretation
in dimensional lumber — no API keys, same claude -p pipe.
- driver: interpret(image_path=) prepends a reference-photo directive with the
image's absolute path; find_image_url() + fetch_image() download a linked
image to a temp file; woodshop-talk --image (path or URL) for CLI/voice.
- controller.run_command(image_path=) passthrough.
- command bar: 📎 attach (file picker), drag-drop image, Ctrl+V paste image,
and image-URL-in-text detection; downloads run off the UI thread; an image
chip shows/clears the attachment.
- tests: URL detection, image directive in prompt, fetch_image temp write,
controller passthrough, command-bar attach + default-text smoke. 216 pass.
Honest limit: the live image round-trip needs a real display/model call to
verify — wired + unit-tested, please confirm it sees the photo on your machine.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The driver interpreted each utterance in isolation (schemas + scene +
utterance only), so when WoodShop asked a clarifying question and the user
replied "yes", the next turn had no record of what was proposed and fell
back to "not sure what you'd like me to do".
- driver.interpret/handle now accept a rolling (utterance, reply) history;
SYSTEM prompt gains a "Recent conversation" section instructing the model
to execute the previously-proposed calls on affirmation.
- CLI main() keeps a history list across the loop.
- GUI Controller keeps a bounded self._history and threads it through
run_command, appending each turn.
- tests: history render/window, prompt inclusion, handle + controller append.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Joinery tab feature list now has a context menu: right-click a mortise/tenon
to "Break this connection" (only shown when it's connected) or "Delete feature".
controller.break_feature_connection breaks just the connection(s) that feature
is part of, in one undo step.
86 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>