Three quality levers for photo-to-build:
- Multiple references at once: interpret/handle/run_command take image_paths
(list); the directive lists every file and tells the model they're different
views/details of one piece. Command bar accumulates attachments (📎 / drag /
paste, getOpenFileNames) with a chip + clear.
- Better guidance: the build directive now walks the model through it — decide
overall dimensions, then count & place legs/rails/top/shelves, keep flush &
square, then joinery.
- Render-feedback loop: woodshop.scenerender renders the scene from front/side/
iso in an isolated subprocess (GL-crash safe); driver.critique() shows the AI
the reference + those renders and returns corrective tool calls (or 'LGTM…');
controller.refine_to_match(rounds) applies them, stopping when satisfied. A
"🔄 Match photo" button runs a round using the retained reference.
viewer.render_to_file gains a view (front/side/top/iso).
tests: multi-image directive, critique prompt, refine loop applies/stops/handles
no-render, command-bar multi-attach + match-button gating. Verified real
front/iso scene renders work via the subprocess. 227 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends "build something like this" beyond photos:
- driver.resolve_reference(source) routes any path/URL: image/PDF → a path
claude -p reads directly; STL/STEP/OBJ → render_mesh() renders an isometric
PNG (pyvista; STEP via build123d→STL) and reports the bounding box; a normal
web URL → fetch_web_text() pulls the page's visible text.
- interpret(reference_text=) injects guide/render-dims text alongside any image
directive; handle() + controller.run_command() + woodshop-talk --ref pass it.
- command bar: picker/drag-drop accept images + .pdf + 3D files; any pasted URL
is resolved; resolution (download/render/fetch) runs off the UI thread.
- find_image_url→find_reference_url (any URL); fetch_image→fetch_url (generic).
- tests: URL detect, image+reference-text directives, fetch_url, web-text strip,
resolve_reference routing per kind, real STL render (skips without GL). 220 pass.
3D render gives the model EXACT proportions (+ bbox) instead of a 2D guess.
Honest limit: render needs the viewer stack + working off-screen GL on your box;
the live model round-trip still wants your eyes to confirm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>