Right-click a feature to break its connection
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>
This commit is contained in:
parent
5e8a1c7926
commit
3e7375344e
|
|
@ -301,6 +301,23 @@ class Controller(QObject):
|
||||||
self._do(lambda: self.scene.disconnect(part=part_id) if part_id
|
self._do(lambda: self.scene.disconnect(part=part_id) if part_id
|
||||||
else self.scene.disconnect())
|
else self.scene.disconnect())
|
||||||
|
|
||||||
|
def feature_connection_ids(self, fid: str) -> list[str]:
|
||||||
|
return [c.id for c in self.scene.connections if fid in (c.anchor, c.moving)]
|
||||||
|
|
||||||
|
def break_feature_connection(self, fid: str) -> None:
|
||||||
|
"""Break the connection(s) that this specific feature is part of."""
|
||||||
|
cids = self.feature_connection_ids(fid)
|
||||||
|
if not cids:
|
||||||
|
return
|
||||||
|
|
||||||
|
def op():
|
||||||
|
with self.scene.batch():
|
||||||
|
for cid in cids:
|
||||||
|
self.scene.disconnect(cid=cid)
|
||||||
|
return f"Broke {len(cids)} connection(s) on {fid}."
|
||||||
|
|
||||||
|
self._do(op)
|
||||||
|
|
||||||
def groups(self) -> list[list[str]]:
|
def groups(self) -> list[list[str]]:
|
||||||
return self.scene.groups()
|
return self.scene.groups()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ from __future__ import annotations
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtWidgets import (QCheckBox, QComboBox, QDialog, QDialogButtonBox,
|
from PySide6.QtWidgets import (QCheckBox, QComboBox, QDialog, QDialogButtonBox,
|
||||||
QDoubleSpinBox, QFormLayout, QGridLayout, QHBoxLayout,
|
QDoubleSpinBox, QFormLayout, QGridLayout, QHBoxLayout,
|
||||||
QLabel, QListWidget, QListWidgetItem, QMessageBox,
|
QLabel, QListWidget, QListWidgetItem, QMenu,
|
||||||
QPushButton, QVBoxLayout, QWidget)
|
QMessageBox, QPushButton, QVBoxLayout, QWidget)
|
||||||
|
|
||||||
from ..scene import FACES
|
from ..scene import FACES
|
||||||
from .controller import Controller
|
from .controller import Controller
|
||||||
|
|
@ -33,6 +33,8 @@ class FeaturePanel(QWidget):
|
||||||
|
|
||||||
self.list = QListWidget()
|
self.list = QListWidget()
|
||||||
self.list.itemSelectionChanged.connect(self._on_row)
|
self.list.itemSelectionChanged.connect(self._on_row)
|
||||||
|
self.list.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
|
self.list.customContextMenuRequested.connect(self._feat_menu)
|
||||||
root.addWidget(self.list, 1)
|
root.addWidget(self.list, 1)
|
||||||
|
|
||||||
self.hint = QLabel("")
|
self.hint = QLabel("")
|
||||||
|
|
@ -136,6 +138,19 @@ class FeaturePanel(QWidget):
|
||||||
if items:
|
if items:
|
||||||
self.c.select_feature(items[0].data(Qt.UserRole))
|
self.c.select_feature(items[0].data(Qt.UserRole))
|
||||||
|
|
||||||
|
def _feat_menu(self, pos) -> None:
|
||||||
|
item = self.list.itemAt(pos)
|
||||||
|
if not item:
|
||||||
|
return
|
||||||
|
fid = item.data(Qt.UserRole)
|
||||||
|
self.c.select_feature(fid)
|
||||||
|
menu = QMenu(self)
|
||||||
|
if self.c.feature_connection_ids(fid):
|
||||||
|
menu.addAction("Break this connection",
|
||||||
|
lambda: self.c.break_feature_connection(fid))
|
||||||
|
menu.addAction("Delete feature", self.c.delete_active_feature)
|
||||||
|
menu.exec(self.list.viewport().mapToGlobal(pos))
|
||||||
|
|
||||||
def _preview(self) -> None:
|
def _preview(self) -> None:
|
||||||
"""Live: show a red ghost of the pending values (no commit until Apply)."""
|
"""Live: show a red ghost of the pending values (no commit until Apply)."""
|
||||||
if self._loading or not self.c.active_feature:
|
if self._loading or not self.c.active_feature:
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,17 @@ def test_highlight_feature(tmp_path):
|
||||||
assert c.preview is None
|
assert c.preview is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_break_feature_connection(tmp_path):
|
||||||
|
c = _controller(tmp_path)
|
||||||
|
c.place("2x4", 24); c.add_feature("mortise") # f1 on p1
|
||||||
|
c.place("2x4", 12); c.add_feature("tenon") # f2 on p2
|
||||||
|
c.scene.connect("f1", "f2")
|
||||||
|
assert c.feature_connection_ids("f1") == ["c1"]
|
||||||
|
c.break_feature_connection("f1")
|
||||||
|
assert c.scene.connections == []
|
||||||
|
assert c.feature_connection_ids("f1") == []
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_tool_is_safe(tmp_path):
|
def test_unknown_tool_is_safe(tmp_path):
|
||||||
c = _controller(tmp_path)
|
c = _controller(tmp_path)
|
||||||
assert "unknown" in c.execute_call("wood-bogus", {}).lower()
|
assert "unknown" in c.execute_call("wood-bogus", {}).lower()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue