diff --git a/src/woodshop/gui/controller.py b/src/woodshop/gui/controller.py index f5fc726..db9215a 100644 --- a/src/woodshop/gui/controller.py +++ b/src/woodshop/gui/controller.py @@ -81,7 +81,8 @@ class Controller(QObject): self._schemas: str | None = None self.selected: list[str] = [self.scene.selection] if self.scene.selection else [] self.active_feature: str | None = None # feature currently being edited - self.preview = None # (Part, Feature) pending preview, or None + self.preview = None # (Part, Feature) shown as an overlay, or None + self.preview_kind = "edit" # "edit" (red pending) | "highlight" (cyan) # ----- persistence / notify ---------------------------------------- def save(self) -> None: @@ -328,6 +329,20 @@ class Controller(QObject): if val is not None and hasattr(pending, k): setattr(pending, k, val) self.preview = (part, pending) + self.preview_kind = "edit" + self.preview_changed.emit() + + def highlight_feature(self, fid: str | None) -> None: + """Show a cyan highlight of a feature in the scene (no edit).""" + if not fid: + self.preview = None + else: + try: + part, feat = self.scene.find_feature(fid) + self.preview = (part, copy.copy(feat)) + self.preview_kind = "highlight" + except SceneError: + self.preview = None self.preview_changed.emit() def clear_preview(self) -> None: diff --git a/src/woodshop/gui/feature_panel.py b/src/woodshop/gui/feature_panel.py index 07344ae..1321d53 100644 --- a/src/woodshop/gui/feature_panel.py +++ b/src/woodshop/gui/feature_panel.py @@ -123,8 +123,8 @@ class FeaturePanel(QWidget): self.fit_btn.setEnabled(bool(mate)) self.fit_btn.setText(f"Fit to {mate}…" if mate else "Fit to mate…") self._loading = False - if self.c.preview is not None: # committed state -> drop any stale ghost - self.c.clear_preview() + # Highlight the selected feature in the scene (cyan), or clear if none. + self.c.highlight_feature(feat.id if feat else None) def _on_row(self) -> None: if self._loading: @@ -160,6 +160,9 @@ class FeaturePanel(QWidget): item = QListWidgetItem(label) item.setData(Qt.UserRole, f.id) lst.addItem(item) + # Highlight whichever candidate is selected so the user sees it in 3D. + lst.currentItemChanged.connect( + lambda cur, _prev: self.c.highlight_feature(cur.data(Qt.UserRole)) if cur else None) lst.setCurrentRow(0) lay.addWidget(lst) connect_cb = QCheckBox("Make connection (move && orient the other board to seat the joint)") @@ -168,11 +171,14 @@ class FeaturePanel(QWidget): bb.accepted.connect(dlg.accept) bb.rejected.connect(dlg.reject) lay.addWidget(bb) - if dlg.exec() and lst.currentItem(): + accepted = dlg.exec() + if accepted and lst.currentItem(): target = lst.currentItem().data(Qt.UserRole) self.c.fit_feature(target) # size to match if connect_cb.isChecked(): self.c.make_connection(target) # then assemble + else: + self.c.highlight_feature(feat.id) # restore highlight on cancel _HINTS = { diff --git a/src/woodshop/gui/main_window.py b/src/woodshop/gui/main_window.py index 9115f6c..dbceadd 100644 --- a/src/woodshop/gui/main_window.py +++ b/src/woodshop/gui/main_window.py @@ -57,7 +57,8 @@ class MainWindow(QMainWindow): self.viewport.picked.connect(self._on_pick) self.controller.changed.connect(self._on_changed) self.controller.preview_changed.connect( - lambda: self.viewport.set_preview(self.controller.preview)) + lambda: self.viewport.set_preview(self.controller.preview, + self.controller.preview_kind)) self._build_menus() self._on_changed() # initial render + status diff --git a/src/woodshop/gui/viewport.py b/src/woodshop/gui/viewport.py index 05c655b..6d3dc2d 100644 --- a/src/woodshop/gui/viewport.py +++ b/src/woodshop/gui/viewport.py @@ -95,8 +95,9 @@ class Viewport(QWidget): self.plotter.camera_position = cam # keep the user's viewpoint self.plotter.render() - def set_preview(self, preview) -> None: - """Draw (or clear) the translucent red ghost of a pending feature edit.""" + def set_preview(self, preview, kind="edit") -> None: + """Draw (or clear) a feature overlay: red for a pending edit, cyan to + highlight the selected feature.""" for name in ("preview_face", "preview_edges"): try: self.plotter.remove_actor(name) @@ -104,13 +105,14 @@ class Viewport(QWidget): pass if preview is not None: part, feat = preview + color = "#33ccff" if kind == "highlight" else "red" try: mesh = feature_preview_mesh(part, feat) - self.plotter.add_mesh(mesh, color="red", opacity=0.4, pickable=False, + self.plotter.add_mesh(mesh, color=color, opacity=0.4, pickable=False, reset_camera=False, name="preview_face") edges = mesh.extract_feature_edges() if edges.n_points: - self.plotter.add_mesh(edges, color="red", line_width=3, + self.plotter.add_mesh(edges, color=color, line_width=3, pickable=False, reset_camera=False, name="preview_edges") except Exception: pass diff --git a/tests/test_gui_controller.py b/tests/test_gui_controller.py index 809ae2f..5e377a7 100644 --- a/tests/test_gui_controller.py +++ b/tests/test_gui_controller.py @@ -113,6 +113,17 @@ def test_fit_mortise_to_tenon(tmp_path): assert m.depth_in == 1.5 + 1 / 32 +def test_highlight_feature(tmp_path): + c = _controller(tmp_path) + c.place("2x4", 12) + c.add_feature("mortise") # f1 + c.highlight_feature("f1") + assert c.preview is not None and c.preview_kind == "highlight" + assert c.preview[1].id == "f1" + c.highlight_feature(None) + assert c.preview is None + + def test_unknown_tool_is_safe(tmp_path): c = _controller(tmp_path) assert "unknown" in c.execute_call("wood-bogus", {}).lower()