feat: 完善参数化关系式与SCDM模型修改验证
This commit is contained in:
@@ -80,6 +80,63 @@ def _isolated_hole_resize(model: StepModel, face_id: int, diameter: float, root:
|
||||
return str(response.get("message") or "")
|
||||
|
||||
|
||||
def _assert_isolated_hole_resize_disables_stale_external_regions(
|
||||
model: StepModel,
|
||||
face_id: int,
|
||||
diameter: float,
|
||||
root: Path,
|
||||
) -> None:
|
||||
input_path = root / "face87_stale_input.brep"
|
||||
output_path = root / "face87_stale_output.brep"
|
||||
request_path = root / "face87_stale_resize_request.json"
|
||||
model.export_internal_brep(input_path)
|
||||
request_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"input_path": str(input_path),
|
||||
"output_path": str(output_path),
|
||||
"input_format": "brep",
|
||||
"output_format": "brep",
|
||||
"operation": "resize_cylindrical_hole",
|
||||
"args": [face_id, diameter],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
code = run_request(request_path)
|
||||
response = json.loads(request_path.with_suffix(".response.json").read_text(encoding="utf-8"))
|
||||
_assert(code == 0 and response.get("ok") is True, f"isolated stale-region resize should pass: {response}")
|
||||
message = str(response.get("message") or "")
|
||||
marker = "verified_face="
|
||||
marker_index = message.find(marker)
|
||||
_assert(marker_index >= 0, f"isolated resize should report verified_face: {message}")
|
||||
verified_text = message[marker_index + len(marker):].split(".", 1)[0].strip()
|
||||
verified_face = int(float(verified_text))
|
||||
|
||||
result_model = StepModel.load_internal_brep(output_path)
|
||||
result_model.filename = MODEL_PATH
|
||||
stale_region = result_model.face_region_ids(verified_face)
|
||||
_assert(
|
||||
len(stale_region) > 1,
|
||||
f"test fixture should expose the stale external-region bug before marking: {stale_region}",
|
||||
)
|
||||
|
||||
result_model.mark_external_recognition_stale("isolated-edit-result")
|
||||
current_region = result_model.face_region_ids(verified_face)
|
||||
_assert(
|
||||
current_region == [verified_face],
|
||||
f"isolated edit result should not reuse original STEP Analysis Situs hole groups: {current_region}",
|
||||
)
|
||||
result_model.assign_logical_face_region_exclusive(face_id, current_region)
|
||||
_assert(
|
||||
result_model.face_ids_for_logical_id(face_id) == current_region,
|
||||
f"logical Face {face_id} should only attach to the edited hole, not a neighboring hole: "
|
||||
f"{result_model.face_ids_for_logical_id(face_id)}",
|
||||
)
|
||||
|
||||
|
||||
def _isolated_hole_axis_move(
|
||||
model: StepModel,
|
||||
face_id: int,
|
||||
@@ -331,6 +388,7 @@ def main() -> int:
|
||||
with tempfile.TemporaryDirectory(prefix="icepak_face87_isolated_") as temp_dir:
|
||||
worker_message = _isolated_hole_resize(model, 87, 0.3, Path(temp_dir))
|
||||
_assert("diameter 0.5 -> 0.3" in worker_message, "Face 87 isolated diameter shrink should complete")
|
||||
_assert_isolated_hole_resize_disables_stale_external_regions(model, 87, 0.3, Path(temp_dir))
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="icepak_face85_axis_isolated_") as temp_dir:
|
||||
worker_message = _isolated_hole_axis_move(model, 85, axis_move_target, Path(temp_dir))
|
||||
|
||||
@@ -9,7 +9,9 @@ import tempfile
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PySide6.QtCore import QEvent, QObject, QStringListModel
|
||||
from PySide6.QtCore import QEvent, QObject, Qt, QStringListModel
|
||||
from PySide6.QtGui import QKeyEvent
|
||||
from PySide6.QtTest import QTest
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
@@ -22,6 +24,7 @@ from PySide6.QtWidgets import (
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
@@ -31,6 +34,7 @@ if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from step_editor.widgets import NoWheelComboBox
|
||||
from step_editor.relation_formulas import ObjectParameterRef, parse_relation_formula
|
||||
from step_editor.window_actions import WindowActionMixin
|
||||
from step_editor.window_core import WindowCoreMixin
|
||||
from step_editor.window_state import (
|
||||
@@ -40,6 +44,7 @@ from step_editor.window_state import (
|
||||
PROPERTY_SCOPE_COLUMN,
|
||||
PROPERTY_TABLE_HEADERS,
|
||||
PROPERTY_TARGET_COLUMN,
|
||||
RELATION_FORMULA_OBJECT_COMPLETION_LIMIT,
|
||||
WindowStateMixin,
|
||||
)
|
||||
|
||||
@@ -71,6 +76,7 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
||||
self.selected_solid_id = None
|
||||
self.selected_face_id = 0
|
||||
self.selected_edge_id = None
|
||||
self.selected_pick_position = None
|
||||
self.step_path = PROJECT_ROOT / "assets" / "models" / "probe.step"
|
||||
self.current_info_values = self._plane_info()
|
||||
self.executed_property_actions: list[tuple[str, str]] = []
|
||||
@@ -100,7 +106,14 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
||||
self.relation_formula_input = QLineEdit()
|
||||
self.relation_formula_completer_model = QStringListModel(self)
|
||||
self.relation_formula_completer = QCompleter(self.relation_formula_completer_model, self)
|
||||
self.relation_formula_input.setCompleter(self.relation_formula_completer)
|
||||
self.relation_formula_completer.setMaxVisibleItems(12)
|
||||
self.relation_formula_completer.activated[str].connect(self._on_relation_formula_completion_activated)
|
||||
relation_popup = self.relation_formula_completer.popup()
|
||||
if relation_popup is not None:
|
||||
relation_popup.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
self.relation_formula_completer.setWidget(self.relation_formula_input)
|
||||
self.relation_formula_input.textChanged.connect(self._on_relation_formula_input_changed)
|
||||
layout.addWidget(self.relation_formula_input)
|
||||
self.add_relation_formula_button = QPushButton()
|
||||
self.remove_relation_formula_button = QPushButton()
|
||||
self.relation_formula_list = QListWidget()
|
||||
@@ -136,6 +149,9 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
||||
def _set_control_state(self, widget, enabled: bool, _enabled_tip: str, _disabled_tip: str) -> None:
|
||||
widget.setEnabled(enabled)
|
||||
|
||||
def setTitle(self, title: str) -> None:
|
||||
self.object_edit_title = title
|
||||
|
||||
def resize_face_width_local(self) -> None:
|
||||
self.executed_property_actions.append(("resize_face_width_local", self.face_width_input.text()))
|
||||
self._after_property_edit_finished(success=True)
|
||||
@@ -148,6 +164,81 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
||||
return _StatusBar()
|
||||
|
||||
|
||||
class _RelationFormulaEventProbe(WindowCoreMixin, _PropertyTableProbe):
|
||||
pass
|
||||
|
||||
|
||||
class _GlobalRelationCompletionModel:
|
||||
def __init__(self) -> None:
|
||||
self.faces = [object() for _index in range(100)]
|
||||
self.edges = [object() for _index in range(12)]
|
||||
self.face_logical_ids = list(range(len(self.faces)))
|
||||
self.face_region_call_count = 0
|
||||
|
||||
def face_logical_id(self, face_id: int) -> int:
|
||||
return int(self.face_logical_ids[int(face_id)])
|
||||
|
||||
def face_region_logical_id(self, face_id: int) -> int:
|
||||
self.face_region_call_count += 1
|
||||
return int(face_id)
|
||||
|
||||
def resolve_face_selection_id(self, object_id: int) -> int | None:
|
||||
if 0 <= int(object_id) < len(self.faces):
|
||||
return int(object_id)
|
||||
return None
|
||||
|
||||
def quick_face_info(self, face_id: int) -> dict[str, object]:
|
||||
return {"surface": "cylinder" if int(face_id) in {85, 87} else "plane"}
|
||||
|
||||
|
||||
class _LargeRelationCompletionModel(_GlobalRelationCompletionModel):
|
||||
def __init__(self) -> None:
|
||||
self.faces = [object() for _index in range(10000)]
|
||||
self.edges = [object() for _index in range(1000)]
|
||||
self.face_logical_ids = list(range(len(self.faces)))
|
||||
self.face_region_call_count = 0
|
||||
|
||||
def face_region_logical_id(self, face_id: int) -> int:
|
||||
self.face_region_call_count += 1
|
||||
raise AssertionError("relation formula completion should not call face_region_logical_id")
|
||||
|
||||
|
||||
class _RelationFormulaRemapModel:
|
||||
def __init__(self, face_infos: dict[int, dict[str, object]], face_count: int = 120) -> None:
|
||||
self.faces = [object() for _index in range(face_count)]
|
||||
self.edges = []
|
||||
self.face_logical_ids = list(range(face_count))
|
||||
self.face_part_ids = [0 for _index in range(face_count)]
|
||||
self.face_solid_ids = [0 for _index in range(face_count)]
|
||||
self.face_infos = dict(face_infos)
|
||||
|
||||
def face_logical_id(self, face_id: int) -> int:
|
||||
return int(self.face_logical_ids[int(face_id)])
|
||||
|
||||
def face_region_logical_id(self, face_id: int) -> int:
|
||||
return self.face_logical_id(int(face_id))
|
||||
|
||||
def face_ids_for_logical_id(self, logical_id: int) -> list[int]:
|
||||
return [face_id for face_id, item in enumerate(self.face_logical_ids) if int(item) == int(logical_id)]
|
||||
|
||||
def resolve_face_selection_id(self, object_id: int) -> int | None:
|
||||
matches = self.face_ids_for_logical_id(int(object_id))
|
||||
if matches:
|
||||
return int(matches[0])
|
||||
if 0 <= int(object_id) < len(self.faces):
|
||||
return int(object_id)
|
||||
return None
|
||||
|
||||
def quick_face_info(self, face_id: int) -> dict[str, object]:
|
||||
info = dict(self.face_infos.get(int(face_id), {}))
|
||||
if not info:
|
||||
info = {"surface": "plane", "area_center": (1000.0 + int(face_id), 0.0, 0.0), "area": 1.0}
|
||||
info.setdefault("part_id", 0)
|
||||
info.setdefault("solid_id", 0)
|
||||
info.setdefault("axis", (0.0, 0.0, 1.0))
|
||||
return info
|
||||
|
||||
|
||||
class _ActionMessageProbe(WindowActionMixin):
|
||||
pass
|
||||
|
||||
@@ -463,26 +554,627 @@ def _assert_relation_formula_editor() -> None:
|
||||
completions = set(probe.relation_formula_completer_model.stringList())
|
||||
_assert("Face0.面内长度" in completions, f"relation formula completion missing face length: {completions}")
|
||||
_assert("Face0.面内宽度" in completions, f"relation formula completion missing face width: {completions}")
|
||||
probe.relation_formula_input.setText("F")
|
||||
probe.relation_formula_input.setCursorPosition(1)
|
||||
probe._update_relation_formula_completions()
|
||||
f_completions = set(probe.relation_formula_completer_model.stringList())
|
||||
_assert(
|
||||
"Face" in f_completions or "Face0" in f_completions,
|
||||
f"relation formula should suggest face object tokens after 'F': {f_completions}",
|
||||
)
|
||||
_assert(probe.relation_formula_completer.completionCount() > 0, "relation formula completer should have matches after 'F'")
|
||||
_assert(probe._accept_relation_formula_completion(), "Tab should accept relation formula completion after 'F'")
|
||||
_assert(
|
||||
probe.relation_formula_input.text() in {"Face", "Face0"},
|
||||
f"Tab should replace 'F' with a face completion: {probe.relation_formula_input.text()}",
|
||||
)
|
||||
if probe.relation_formula_input.text() == "Face":
|
||||
probe._update_relation_formula_completions()
|
||||
face_object_completions = set(probe.relation_formula_completer_model.stringList())
|
||||
_assert(
|
||||
"Face0" in face_object_completions,
|
||||
f"relation formula should suggest concrete Face IDs after completing Face: {face_object_completions}",
|
||||
)
|
||||
_assert(probe._accept_relation_formula_completion(), "second Tab should accept a concrete Face ID completion")
|
||||
_assert(
|
||||
probe.relation_formula_input.text() == "Face0",
|
||||
f"second Tab should complete Face to Face0: {probe.relation_formula_input.text()}",
|
||||
)
|
||||
probe._update_relation_formula_completions()
|
||||
exact_object_completions = set(probe.relation_formula_completer_model.stringList())
|
||||
_assert(
|
||||
any(str(item).startswith("Face0.") for item in exact_object_completions),
|
||||
f"concrete object completion should suggest editable parameters: {exact_object_completions}",
|
||||
)
|
||||
event_probe = _RelationFormulaEventProbe()
|
||||
event_probe.show()
|
||||
event_probe._refresh_property_editor()
|
||||
event_probe.relation_formula_input.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
QApplication.processEvents()
|
||||
event_probe.relation_formula_input.setText("F")
|
||||
event_probe.relation_formula_input.setCursorPosition(1)
|
||||
tab_event = QKeyEvent(QEvent.Type.ShortcutOverride, Qt.Key.Key_Tab, Qt.KeyboardModifier.NoModifier)
|
||||
_assert(
|
||||
event_probe.eventFilter(event_probe.relation_formula_input, tab_event),
|
||||
"relation formula input should consume Tab before Qt moves focus",
|
||||
)
|
||||
_assert(
|
||||
event_probe.relation_formula_input.text() in {"Face", "Face0"},
|
||||
f"ShortcutOverride Tab should accept the face completion: {event_probe.relation_formula_input.text()}",
|
||||
)
|
||||
QApplication.processEvents()
|
||||
_assert(event_probe.relation_formula_input.hasFocus(), "relation formula input should keep focus after Tab completion")
|
||||
popup = event_probe.relation_formula_completer.popup()
|
||||
if popup is not None:
|
||||
popup.installEventFilter(event_probe)
|
||||
before_backspace = event_probe.relation_formula_input.text()
|
||||
popup_backspace = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_Backspace, Qt.KeyboardModifier.NoModifier)
|
||||
_assert(
|
||||
event_probe.eventFilter(popup, popup_backspace),
|
||||
"relation formula completer popup should forward Backspace to the input",
|
||||
)
|
||||
_assert(
|
||||
event_probe.relation_formula_input.text() == before_backspace[:-1],
|
||||
f"Backspace should edit the input even if popup receives it: {event_probe.relation_formula_input.text()}",
|
||||
)
|
||||
_assert(event_probe.relation_formula_input.hasFocus(), "relation formula input should keep focus after popup Backspace")
|
||||
global_probe = _PropertyTableProbe()
|
||||
global_probe.model = _GlobalRelationCompletionModel()
|
||||
global_probe.selected_kind = "face"
|
||||
global_probe.selected_face_id = 0
|
||||
global_probe.show()
|
||||
global_probe._refresh_property_editor()
|
||||
global_probe.relation_formula_input.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
QApplication.processEvents()
|
||||
global_probe.model.face_region_call_count = 0
|
||||
global_probe.relation_formula_input.setText("Face")
|
||||
global_probe.relation_formula_input.setCursorPosition(len("Face"))
|
||||
global_probe._update_relation_formula_completions()
|
||||
global_probe._show_relation_formula_completion_popup()
|
||||
QApplication.processEvents()
|
||||
global_face_completion_list = list(global_probe.relation_formula_completer_model.stringList())
|
||||
global_face_completions = set(global_face_completion_list)
|
||||
_assert("Face85" in global_face_completions, f"global Face completion should include Face85: {global_face_completions}")
|
||||
_assert("Face85." not in global_face_completions, f"object completion should not show trailing dot: {global_face_completions}")
|
||||
_assert(
|
||||
global_face_completion_list.index("Face2") < global_face_completion_list.index("Face10") < global_face_completion_list.index("Face85"),
|
||||
f"Face completions should be sorted by numeric ID: {global_face_completion_list[:20]}",
|
||||
)
|
||||
global_probe.relation_formula_input.setText("Face8")
|
||||
global_probe.relation_formula_input.setCursorPosition(len("Face8"))
|
||||
global_probe._update_relation_formula_completions()
|
||||
face8_completion_list = list(global_probe.relation_formula_completer_model.stringList())
|
||||
_assert("Face87" in face8_completion_list, f"Face8 should still suggest longer Face IDs: {face8_completion_list[:20]}")
|
||||
_assert(
|
||||
face8_completion_list[0] != "Face8",
|
||||
f"Face8 completion should prefer longer IDs instead of no-op self completion: {face8_completion_list[:20]}",
|
||||
)
|
||||
_assert(
|
||||
face8_completion_list.index("Face80") < face8_completion_list.index("Face87"),
|
||||
f"Face8 completions should keep numeric order: {face8_completion_list[:20]}",
|
||||
)
|
||||
face8_tab_probe = _PropertyTableProbe()
|
||||
face8_tab_probe.model = _GlobalRelationCompletionModel()
|
||||
face8_tab_probe.selected_kind = "face"
|
||||
face8_tab_probe.selected_face_id = 0
|
||||
face8_tab_probe._refresh_property_editor()
|
||||
face8_tab_probe.relation_formula_input.setText("Face8")
|
||||
face8_tab_probe.relation_formula_input.setCursorPosition(len("Face8"))
|
||||
_assert(face8_tab_probe._accept_relation_formula_completion(), "Tab should accept a longer Face8 completion")
|
||||
_assert(
|
||||
face8_tab_probe.relation_formula_input.text().startswith("Face8")
|
||||
and face8_tab_probe.relation_formula_input.text() != "Face8",
|
||||
f"Tab after Face8 should complete to a longer ID, not stay unchanged: {face8_tab_probe.relation_formula_input.text()}",
|
||||
)
|
||||
face_tab_probe = _RelationFormulaEventProbe()
|
||||
face_tab_probe.model = _GlobalRelationCompletionModel()
|
||||
face_tab_probe.selected_kind = "face"
|
||||
face_tab_probe.selected_face_id = 0
|
||||
face_tab_probe.show()
|
||||
face_tab_probe._refresh_property_editor()
|
||||
face_tab_probe.relation_formula_input.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
QApplication.processEvents()
|
||||
face_tab_probe.relation_formula_input.setText("F")
|
||||
face_tab_probe.relation_formula_input.setCursorPosition(len("F"))
|
||||
face_tab_event = QKeyEvent(QEvent.Type.ShortcutOverride, Qt.Key.Key_Tab, Qt.KeyboardModifier.NoModifier)
|
||||
_assert(face_tab_probe.eventFilter(face_tab_probe.relation_formula_input, face_tab_event), "F + Tab should be consumed")
|
||||
for _index in range(4):
|
||||
QApplication.processEvents()
|
||||
_assert(face_tab_probe.relation_formula_input.text() == "Face", f"F + Tab should complete to Face: {face_tab_probe.relation_formula_input.text()}")
|
||||
_assert("Face87" in set(face_tab_probe.relation_formula_completer_model.stringList()), "Face layer should suggest Face87 after F + Tab")
|
||||
_assert(face_tab_probe.relation_formula_completer.popup().isVisible(), "Face layer popup should open after F + Tab")
|
||||
_assert(
|
||||
face_tab_probe.relation_formula_completer.popup().minimumWidth()
|
||||
>= max(face_tab_probe.relation_formula_input.width(), 320),
|
||||
"Face completion popup should be wide enough to show text",
|
||||
)
|
||||
face_manual_probe = _RelationFormulaEventProbe()
|
||||
face_manual_probe.model = _GlobalRelationCompletionModel()
|
||||
face_manual_probe.selected_kind = "face"
|
||||
face_manual_probe.selected_face_id = 0
|
||||
face_manual_probe.show()
|
||||
face_manual_probe._refresh_property_editor()
|
||||
face_manual_probe.relation_formula_input.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
QApplication.processEvents()
|
||||
face_manual_probe.relation_formula_input.setText("Face")
|
||||
face_manual_probe.relation_formula_input.setCursorPosition(len("Face"))
|
||||
for _index in range(4):
|
||||
QApplication.processEvents()
|
||||
_assert("Face87" in set(face_manual_probe.relation_formula_completer_model.stringList()), "manual Face input should suggest Face87")
|
||||
_assert(face_manual_probe.relation_formula_completer.popup().isVisible(), "manual Face input should open the next completion layer")
|
||||
_assert(
|
||||
face_manual_probe.relation_formula_completer.popup().minimumWidth()
|
||||
>= max(face_manual_probe.relation_formula_input.width(), 320),
|
||||
"manual Face popup should not collapse to cursor width",
|
||||
)
|
||||
global_probe.relation_formula_input.setText("Face87")
|
||||
global_probe.relation_formula_input.setCursorPosition(len("Face87"))
|
||||
global_probe._update_relation_formula_completions()
|
||||
face87_completion_list = list(global_probe.relation_formula_completer_model.stringList())
|
||||
face87_ref = next((item for item in face87_completion_list if str(item).startswith("Face87.")), "")
|
||||
_assert(face87_ref, f"exact Face ID should suggest editable parameters: {face87_completion_list}")
|
||||
face87_radius_ref = next((item for item in face87_completion_list if str(item).endswith("半径")), "")
|
||||
_assert(face87_radius_ref, f"exact Face ID should suggest radius: {face87_completion_list}")
|
||||
radius_probe = _PropertyTableProbe()
|
||||
radius_probe.model = _GlobalRelationCompletionModel()
|
||||
radius_probe.selected_kind = "face"
|
||||
radius_probe.selected_face_id = 0
|
||||
radius_probe.show()
|
||||
radius_probe._refresh_property_editor()
|
||||
radius_probe.relation_formula_input.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
QApplication.processEvents()
|
||||
radius_probe.relation_formula_input.setText("Face87")
|
||||
radius_probe.relation_formula_input.setCursorPosition(len("Face87"))
|
||||
radius_probe._update_relation_formula_completions()
|
||||
radius_probe._show_relation_formula_completion_popup()
|
||||
QApplication.processEvents()
|
||||
radius_completion_list = list(radius_probe.relation_formula_completer_model.stringList())
|
||||
radius_row = radius_completion_list.index(face87_radius_ref)
|
||||
radius_model = radius_probe.relation_formula_completer.model()
|
||||
radius_popup = radius_probe.relation_formula_completer.popup()
|
||||
radius_probe.relation_formula_completer.setCurrentRow(radius_row)
|
||||
radius_popup.setCurrentIndex(radius_model.index(radius_row, 0))
|
||||
_assert(radius_probe._accept_relation_formula_completion(), "Tab should accept the popup-selected radius completion")
|
||||
_assert(
|
||||
radius_probe.relation_formula_input.text() == face87_radius_ref,
|
||||
f"Tab should keep the popup-selected completion, not reset to default: {radius_probe.relation_formula_input.text()}",
|
||||
)
|
||||
global_probe.relation_formula_input.setText(face87_ref)
|
||||
global_probe.relation_formula_input.setCursorPosition(len(face87_ref))
|
||||
global_probe._update_relation_formula_completions()
|
||||
_assert(" = " in set(global_probe.relation_formula_completer_model.stringList()), "complete Face parameter should suggest '='")
|
||||
global_probe._on_relation_formula_completion_activated(" = ")
|
||||
_assert(
|
||||
global_probe.relation_formula_input.text() == f"{face87_ref} = ",
|
||||
f"mouse activation should insert '=' without replacing the whole formula: {global_probe.relation_formula_input.text()}",
|
||||
)
|
||||
global_probe.relation_formula_input.setText(face87_ref)
|
||||
global_probe.relation_formula_input.setCursorPosition(len(face87_ref))
|
||||
global_probe._update_relation_formula_completions()
|
||||
global_probe.relation_formula_input.setText(f"{face87_ref} = F")
|
||||
global_probe.relation_formula_input.setCursorPosition(len(f"{face87_ref} = F"))
|
||||
global_probe._update_relation_formula_completions()
|
||||
rhs_f_completion_list = list(global_probe.relation_formula_completer_model.stringList())
|
||||
_assert(
|
||||
"Face85" in rhs_f_completion_list and "Face87" in rhs_f_completion_list,
|
||||
f"right-hand expression should suggest global Face IDs after F: {rhs_f_completion_list[:20]}",
|
||||
)
|
||||
global_probe.relation_formula_input.setText(f"{face87_ref} = Face8")
|
||||
global_probe.relation_formula_input.setCursorPosition(len(f"{face87_ref} = Face8"))
|
||||
global_probe._update_relation_formula_completions()
|
||||
rhs_face8_completion_list = list(global_probe.relation_formula_completer_model.stringList())
|
||||
_assert("Face87" in rhs_face8_completion_list, f"right-hand Face8 should suggest Face87: {rhs_face8_completion_list[:20]}")
|
||||
global_probe.relation_formula_input.setText(f"{face87_ref} = Face87")
|
||||
global_probe.relation_formula_input.setCursorPosition(len(f"{face87_ref} = Face87"))
|
||||
global_probe._update_relation_formula_completions()
|
||||
rhs_face87_completion_list = list(global_probe.relation_formula_completer_model.stringList())
|
||||
_assert(
|
||||
any(str(item).startswith("Face87.") for item in rhs_face87_completion_list),
|
||||
f"right-hand exact Face ID should suggest parameters: {rhs_face87_completion_list}",
|
||||
)
|
||||
global_probe.relation_formula_input.setText("Edge")
|
||||
global_probe.relation_formula_input.setCursorPosition(len("Edge"))
|
||||
global_probe._update_relation_formula_completions()
|
||||
global_edge_completion_list = list(global_probe.relation_formula_completer_model.stringList())
|
||||
global_edge_completions = set(global_edge_completion_list)
|
||||
_assert("Edge0" in global_edge_completions, f"Edge completion should be available even in Face selection mode: {global_edge_completions}")
|
||||
_assert(
|
||||
global_edge_completion_list.index("Edge2") < global_edge_completion_list.index("Edge10"),
|
||||
f"Edge completions should be sorted by numeric ID: {global_edge_completion_list}",
|
||||
)
|
||||
_assert(
|
||||
global_probe.model.face_region_call_count == 0,
|
||||
f"relation formula typing should not call heavy face_region_logical_id: {global_probe.model.face_region_call_count}",
|
||||
)
|
||||
large_probe = _PropertyTableProbe()
|
||||
large_probe.model = _LargeRelationCompletionModel()
|
||||
large_probe.selected_kind = "face"
|
||||
large_probe.selected_face_id = 0
|
||||
large_probe._refresh_property_editor()
|
||||
large_probe.model.face_region_call_count = 0
|
||||
large_probe.relation_formula_input.setText("F")
|
||||
large_probe.relation_formula_input.setCursorPosition(len("F"))
|
||||
large_probe._update_relation_formula_completions()
|
||||
large_completions = list(large_probe.relation_formula_completer_model.stringList())
|
||||
_assert("Face87" in large_completions, f"large model F completion should still include common Face IDs: {large_completions[:20]}")
|
||||
_assert(
|
||||
len(large_completions) <= RELATION_FORMULA_OBJECT_COMPLETION_LIMIT + 2,
|
||||
f"large model broad completion should be capped: {len(large_completions)}",
|
||||
)
|
||||
_assert(
|
||||
large_probe.model.face_region_call_count == 0,
|
||||
f"large model completion should not call heavy face_region_logical_id: {large_probe.model.face_region_call_count}",
|
||||
)
|
||||
probe.relation_formula_input.setText("Face0.")
|
||||
probe.relation_formula_input.setCursorPosition(len("Face0."))
|
||||
probe._update_relation_formula_completions()
|
||||
face_completions = set(probe.relation_formula_completer_model.stringList())
|
||||
_assert("Face0.面内长度" in face_completions, f"relation formula should suggest parameters after Face0.: {face_completions}")
|
||||
probe.relation_formula_input.setText("Face0.面内宽度")
|
||||
probe.relation_formula_input.setCursorPosition(len("Face0.面内宽度"))
|
||||
probe._update_relation_formula_completions()
|
||||
_assert(" = " in set(probe.relation_formula_completer_model.stringList()), "relation formula should suggest '=' after a target parameter")
|
||||
probe.relation_formula_input.setText("Face0.面内宽度 = Face0.面内长度 * 1.2")
|
||||
probe.add_relation_formula()
|
||||
_assert(len(probe.relation_formula_items) == 1, f"formula was not stored: {probe.relation_formula_items}")
|
||||
_assert(probe.relation_formula_list.count() == 1, "formula list should display the stored formula")
|
||||
probe._update_property_apply_state()
|
||||
_assert(probe.apply_property_button.isEnabled(), "formula targeting current table should enable parametric modeling")
|
||||
probe.apply_current_property_edit()
|
||||
for _index in range(6):
|
||||
QApplication.processEvents()
|
||||
if not getattr(probe, "property_batch_active", False):
|
||||
if not getattr(probe, "relation_formula_replay_active", False):
|
||||
break
|
||||
_assert(
|
||||
probe.executed_property_actions == [("resize_face_height_local", "12")],
|
||||
f"formula should fill target value and execute the existing row action: {probe.executed_property_actions}",
|
||||
f"formula should immediately fill target value and execute the existing row action: {probe.executed_property_actions}",
|
||||
)
|
||||
width_row = _row_by_label(probe, "面内宽度")
|
||||
width_widget = probe.property_table.cellWidget(width_row, PROPERTY_TARGET_COLUMN)
|
||||
_assert(isinstance(width_widget, QLineEdit), "formula target row should still have a target editor")
|
||||
_assert(width_widget.text().strip() == "12", "formula result should be written back to the target value cell")
|
||||
_assert(str(probe.relation_formula_items[0].get("status")) == "applied", "formula should be marked as applied")
|
||||
for _index in range(4):
|
||||
QApplication.processEvents()
|
||||
_assert(probe.relation_formula_input.isEnabled(), "relation formula input should stay enabled after one formula is applied")
|
||||
probe.relation_formula_input.setText("F")
|
||||
_assert(probe.add_relation_formula_button.isEnabled(), "existing formulas should not disable adding another formula")
|
||||
|
||||
|
||||
def _assert_relation_radius_formula_proxy() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe.model = _GlobalRelationCompletionModel()
|
||||
probe.selected_kind = "face"
|
||||
probe.selected_face_id = 87
|
||||
probe.property_table.setRowCount(1)
|
||||
probe.property_editor_specs = [
|
||||
{
|
||||
"key": "diameter",
|
||||
"label": "直径",
|
||||
"current_raw": 1.0,
|
||||
"current_text": "1",
|
||||
"target_text": "1",
|
||||
"editable": True,
|
||||
"enabled": True,
|
||||
"action": "resize_hole",
|
||||
"value_type": "number",
|
||||
"source_face_id": 87,
|
||||
}
|
||||
]
|
||||
probe.property_table.setItem(0, PROPERTY_LABEL_COLUMN, QTableWidgetItem("直径"))
|
||||
target = QLineEdit("1")
|
||||
probe.property_table.setCellWidget(0, PROPERTY_TARGET_COLUMN, target)
|
||||
|
||||
ref = ObjectParameterRef("Face", 87, "半径")
|
||||
visible = probe._relation_visible_spec_for_ref(ref)
|
||||
_assert(visible is not None, "Face radius formula should fall back to an executable diameter row")
|
||||
_assert(
|
||||
abs(float(probe._relation_value_for_ref(ref)) - 0.5) <= 1e-9,
|
||||
f"radius formula should read half of the diameter: {probe._relation_value_for_ref(ref)}",
|
||||
)
|
||||
row = probe._set_relation_target_value(ref, 0.6)
|
||||
_assert(row == 0, f"radius proxy should write to the diameter row: {row}")
|
||||
_assert(target.text() == "1.2", f"radius proxy should convert target radius to diameter: {target.text()}")
|
||||
|
||||
|
||||
def _assert_relation_formula_input_remains_editable_while_replaying() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe.model = _GlobalRelationCompletionModel()
|
||||
probe.selected_kind = "face"
|
||||
probe.selected_face_id = 87
|
||||
probe._refresh_property_editor()
|
||||
probe.operation_in_progress = True
|
||||
probe.relation_formula_replay_active = True
|
||||
probe.relation_formula_replay_queue = []
|
||||
probe.relation_formula_replay_total = 1
|
||||
probe.relation_formula_input.setEnabled(False)
|
||||
probe._update_relation_formula_buttons()
|
||||
_assert(probe.relation_formula_input.isEnabled(), "relation formula input should stay editable while formulas replay")
|
||||
|
||||
probe.relation_formula_input.setText("Face87.半径 = Face85.半径")
|
||||
probe.add_relation_formula()
|
||||
_assert(len(probe.relation_formula_items) == 1, f"formula should be stored while replaying: {probe.relation_formula_items}")
|
||||
_assert(len(probe.relation_formula_replay_queue) == 1, f"formula should be queued while replaying: {probe.relation_formula_replay_queue}")
|
||||
_assert(probe.relation_formula_input.text() == "", "queued formula should clear the input for the next formula")
|
||||
_assert(probe.relation_formula_input.isEnabled(), "relation formula input should remain enabled after queuing")
|
||||
_assert(probe.relation_formula_replay_active, "adding a queued formula should not stop the current replay")
|
||||
|
||||
|
||||
def _assert_relation_formula_input_clickable_after_existing_formula() -> None:
|
||||
probe = _RelationFormulaEventProbe()
|
||||
probe.relation_formula_input.installEventFilter(probe)
|
||||
probe.show()
|
||||
probe._refresh_property_editor()
|
||||
probe.relation_formula_input.setText("Face0.面内宽度 = Face0.面内长度 * 1.2")
|
||||
probe.add_relation_formula()
|
||||
for _index in range(12):
|
||||
QApplication.processEvents()
|
||||
if not getattr(probe, "relation_formula_replay_active", False):
|
||||
break
|
||||
_assert(len(probe.relation_formula_items) == 1, f"formula should exist before click test: {probe.relation_formula_items}")
|
||||
_assert(str(probe.relation_formula_items[0].get("status")) == "applied", "formula should be applied before click test")
|
||||
probe.activateWindow()
|
||||
probe.raise_()
|
||||
QApplication.processEvents()
|
||||
QTest.mouseClick(probe.relation_formula_input, Qt.MouseButton.LeftButton)
|
||||
QApplication.processEvents()
|
||||
_assert(probe.relation_formula_input.isEnabled(), "relation formula input should stay enabled with an existing formula")
|
||||
_assert(probe.relation_formula_input.hasFocus(), "clicking the relation formula input should focus it with an existing formula")
|
||||
probe.relation_formula_input.setText("F")
|
||||
_assert(probe.add_relation_formula_button.isEnabled(), "clickable input should allow adding another formula")
|
||||
|
||||
|
||||
def _assert_relation_formula_input_is_selection_independent() -> None:
|
||||
probe = _RelationFormulaEventProbe()
|
||||
probe.relation_formula_input.installEventFilter(probe)
|
||||
probe.model = _GlobalRelationCompletionModel()
|
||||
probe.selected_kind = None
|
||||
probe.selected_face_id = None
|
||||
probe.selected_edge_id = None
|
||||
probe.relation_formula_items = []
|
||||
probe.relation_formula_input.setEnabled(False)
|
||||
probe._refresh_relation_formula_list()
|
||||
_assert(probe.relation_formula_input.isEnabled(), "relation formula input should be editable without a selected face")
|
||||
_assert(not probe.relation_formula_input.isReadOnly(), "relation formula input should not become read-only without a selected face")
|
||||
_assert(not probe.add_relation_formula_button.isEnabled(), "empty relation formula should not enable the add button")
|
||||
|
||||
probe.show()
|
||||
probe.activateWindow()
|
||||
probe.raise_()
|
||||
QApplication.processEvents()
|
||||
QTest.mouseClick(probe.relation_formula_input, Qt.MouseButton.LeftButton)
|
||||
QApplication.processEvents()
|
||||
|
||||
probe.relation_formula_input.setText("Face8")
|
||||
probe.relation_formula_input.setCursorPosition(len("Face8"))
|
||||
probe._update_relation_formula_completions()
|
||||
object_completions = list(probe.relation_formula_completer_model.stringList())
|
||||
_assert("Face85" in object_completions and "Face87" in object_completions, f"global face completions should not require selection: {object_completions[:20]}")
|
||||
|
||||
probe.relation_formula_input.setText("Face85")
|
||||
probe.relation_formula_input.setCursorPosition(len("Face85"))
|
||||
probe._update_relation_formula_completions()
|
||||
parameter_completions = list(probe.relation_formula_completer_model.stringList())
|
||||
_assert(any(str(item).startswith("Face85.") for item in parameter_completions), f"Face parameter completions should not require selection: {parameter_completions}")
|
||||
|
||||
|
||||
def _assert_relation_formula_input_recovers_after_loading() -> None:
|
||||
probe = _RelationFormulaEventProbe()
|
||||
probe.model = _GlobalRelationCompletionModel()
|
||||
probe.mode_combo = NoWheelComboBox()
|
||||
probe.mode_combo.addItem("Face", "Face")
|
||||
probe.load_thread = None
|
||||
probe.load_refine_thread = None
|
||||
probe.pending_load_path = None
|
||||
probe.load_in_progress = True
|
||||
probe._update_relation_formula_buttons()
|
||||
_assert(not probe.relation_formula_input.isEnabled(), "relation formula input should be disabled while loading")
|
||||
|
||||
probe._end_load_task()
|
||||
_assert(probe.relation_formula_input.isEnabled(), "relation formula input should recover after loading without selecting a face")
|
||||
|
||||
|
||||
def _assert_relation_formula_ids_follow_model_remap() -> None:
|
||||
old_model = _RelationFormulaRemapModel(
|
||||
{
|
||||
85: {"surface": "cylinder", "area_center": (0.5, 1.0, 9.5), "bbox_center": (0.5, 1.0, 9.5), "radius": 0.25, "area": 8.0},
|
||||
87: {"surface": "cylinder", "area_center": (2.0, 1.0, 9.5), "bbox_center": (2.0, 1.0, 9.5), "radius": 0.25, "area": 8.0},
|
||||
}
|
||||
)
|
||||
new_model = _RelationFormulaRemapModel(
|
||||
{
|
||||
90: {"surface": "cylinder", "area_center": (0.5, 1.0, 9.5), "bbox_center": (0.5, 1.0, 9.5), "radius": 0.25, "area": 8.0},
|
||||
92: {"surface": "cylinder", "area_center": (2.0, 1.0, 9.5), "bbox_center": (2.0, 1.0, 9.5), "radius": 0.15, "area": 5.0},
|
||||
}
|
||||
)
|
||||
probe = _PropertyTableProbe()
|
||||
formula_text = "Face87.直径 = Face85.半径"
|
||||
probe.model = old_model
|
||||
formula = parse_relation_formula(formula_text)
|
||||
probe.relation_formula_items = [
|
||||
{
|
||||
"id": 1,
|
||||
"text": formula_text,
|
||||
"enabled": True,
|
||||
"status": "applied",
|
||||
"message": "已修改模型。",
|
||||
"signatures": probe._relation_signatures_for_formula(formula),
|
||||
}
|
||||
]
|
||||
probe.model = new_model
|
||||
note = probe._refresh_relation_formulas_after_model_edit()
|
||||
_assert(
|
||||
probe.relation_formula_items[0]["text"] == "Face92.直径 = Face90.半径",
|
||||
f"relation formula should follow remapped visible Face IDs: {probe.relation_formula_items}",
|
||||
)
|
||||
_assert("Face ID" in note, f"relation remap note should mention updated Face IDs: {note}")
|
||||
|
||||
old_model = _RelationFormulaRemapModel(
|
||||
{
|
||||
85: {"surface": "cylinder", "area_center": (0.5, 1.0, 9.5), "bbox_center": (0.5, 1.0, 9.5), "radius": 0.25, "area": 8.0},
|
||||
87: {"surface": "cylinder", "area_center": (0.5, 1.0, 0.5), "bbox_center": (0.5, 1.0, 0.5), "radius": 0.25, "area": 8.0},
|
||||
}
|
||||
)
|
||||
new_model = _RelationFormulaRemapModel(
|
||||
{
|
||||
85: {"surface": "cylinder", "area_center": (0.5, 1.0, 9.5), "bbox_center": (0.5, 1.0, 9.5), "radius": 0.125, "area": 4.0},
|
||||
11: {"surface": "cylinder", "area_center": (0.5, 1.0, 0.5), "bbox_center": (0.5, 1.0, 0.5), "radius": 0.25, "area": 8.0},
|
||||
}
|
||||
)
|
||||
probe = _PropertyTableProbe()
|
||||
formula_text = "Face85.直径 = Face87.半径"
|
||||
probe.model = old_model
|
||||
formula = parse_relation_formula(formula_text)
|
||||
probe.relation_formula_items = [
|
||||
{
|
||||
"id": 1,
|
||||
"text": formula_text,
|
||||
"enabled": True,
|
||||
"status": "applied",
|
||||
"message": "已修改模型。",
|
||||
"signatures": probe._relation_signatures_for_formula(formula),
|
||||
}
|
||||
]
|
||||
probe.model = new_model
|
||||
probe._refresh_relation_formulas_after_model_edit()
|
||||
_assert(
|
||||
probe.relation_formula_items[0]["text"] == "Face85.直径 = Face11.半径",
|
||||
f"relation formula should remap a changed reference ID even when the target ID is preserved: {probe.relation_formula_items}",
|
||||
)
|
||||
|
||||
old_model = _RelationFormulaRemapModel(
|
||||
{
|
||||
85: {"surface": "cylinder", "area_center": (0.5, 1.0, 9.5), "bbox_center": (0.5, 1.0, 9.5), "radius": 0.25, "area": 8.0},
|
||||
87: {"surface": "cylinder", "area_center": (0.5, 1.0, 0.5), "bbox_center": (0.5, 1.0, 0.5), "radius": 0.25, "area": 8.0},
|
||||
}
|
||||
)
|
||||
new_model = _RelationFormulaRemapModel(
|
||||
{
|
||||
85: {"surface": "cylinder", "area_center": (0.5, 1.0, 0.5), "bbox_center": (0.5, 1.0, 0.5), "radius": 0.5, "area": 16.0},
|
||||
89: {"surface": "cylinder", "area_center": (0.5, 1.0, 9.5), "bbox_center": (0.5, 1.0, 9.5), "radius": 0.125, "area": 4.0},
|
||||
91: {"surface": "cylinder", "area_center": (0.5, 1.0, 0.5), "bbox_center": (0.5, 1.0, 0.5), "radius": 0.25, "area": 8.0},
|
||||
}
|
||||
)
|
||||
new_model.face_logical_ids[89] = 85
|
||||
probe = _PropertyTableProbe()
|
||||
formula_text = "Face85.直径 = Face87.半径"
|
||||
probe.model = old_model
|
||||
formula = parse_relation_formula(formula_text)
|
||||
probe.relation_formula_items = [
|
||||
{
|
||||
"id": 1,
|
||||
"text": formula_text,
|
||||
"enabled": True,
|
||||
"status": "applied",
|
||||
"message": "已修改模型。",
|
||||
"signatures": probe._relation_signatures_for_formula(formula),
|
||||
}
|
||||
]
|
||||
probe.model = new_model
|
||||
probe._refresh_relation_formulas_after_model_edit()
|
||||
_assert(
|
||||
probe.relation_formula_items[0]["text"] == "Face85.直径 = Face91.半径",
|
||||
f"relation remap should prefer the old reference radius over a same-center changed target: {probe.relation_formula_items}",
|
||||
)
|
||||
|
||||
old_model = _RelationFormulaRemapModel(
|
||||
{
|
||||
9: {"surface": "cylinder", "area_center": (0.5, 1.0, 9.5), "bbox_center": (0.5, 1.0, 9.5), "radius": 0.25, "area": 8.0},
|
||||
87: {"surface": "cylinder", "area_center": (0.5, 1.0, 0.5), "bbox_center": (0.5, 1.0, 0.5), "radius": 0.15, "area": 4.8},
|
||||
}
|
||||
)
|
||||
new_model = _RelationFormulaRemapModel(
|
||||
{
|
||||
14: {"surface": "cylinder", "area_center": (0.5, 1.0, 9.5), "bbox_center": (0.5, 1.0, 9.5), "radius": 0.4, "area": 12.8},
|
||||
87: {"surface": "cylinder", "area_center": (0.5, 1.0, 0.5), "bbox_center": (0.5, 1.0, 0.5), "radius": 0.15, "area": 4.8},
|
||||
}
|
||||
)
|
||||
new_model.face_logical_ids[14] = 9
|
||||
probe = _PropertyTableProbe()
|
||||
formula_text = "Face87.直径 = Face9.半径"
|
||||
probe.model = old_model
|
||||
formula = parse_relation_formula(formula_text)
|
||||
probe.relation_formula_items = [
|
||||
{
|
||||
"id": 1,
|
||||
"text": formula_text,
|
||||
"enabled": True,
|
||||
"status": "applied",
|
||||
"message": "已修改模型。",
|
||||
"signatures": probe._relation_signatures_for_formula(formula),
|
||||
}
|
||||
]
|
||||
probe.model = new_model
|
||||
probe._refresh_relation_formulas_after_model_edit(
|
||||
context={"target_kind": "face", "target_id": 9, "target_logical_id": 9}
|
||||
)
|
||||
_assert(
|
||||
probe.relation_formula_items[0]["text"] == "Face87.直径 = Face9.半径",
|
||||
f"edited right-hand dependency should keep its logical Face ID instead of becoming invalid: {probe.relation_formula_items}",
|
||||
)
|
||||
_assert(
|
||||
str(probe.relation_formula_items[0].get("status")) != "invalid",
|
||||
f"edited right-hand dependency should remain valid: {probe.relation_formula_items}",
|
||||
)
|
||||
_assert(
|
||||
getattr(probe, "_last_relation_formula_refresh_affected_ids", []) == [1],
|
||||
f"edited right-hand dependency should mark the formula for reapply: {probe.relation_formula_items}",
|
||||
)
|
||||
|
||||
old_model = _RelationFormulaRemapModel(
|
||||
{
|
||||
9: {"surface": "cylinder", "area_center": (0.5, 1.0, 9.5), "bbox_center": (0.5, 1.0, 9.5), "radius": 0.25, "area": 8.0},
|
||||
87: {"surface": "cylinder", "area_center": (2.0, 1.0, 9.5), "bbox_center": (2.0, 1.0, 9.5), "radius": 0.125, "area": 4.0},
|
||||
}
|
||||
)
|
||||
new_model = _RelationFormulaRemapModel(
|
||||
{
|
||||
1: {"surface": "cylinder", "area_center": (2.0, 1.0, 9.5), "bbox_center": (2.0, 1.0, 9.5), "radius": 0.5, "area": 16.0},
|
||||
9: {"surface": "cylinder", "area_center": (0.5, 1.0, 9.5), "bbox_center": (0.5, 1.0, 9.5), "radius": 0.4, "area": 12.8},
|
||||
11: {"surface": "cylinder", "area_center": (2.0, 1.0, 9.5), "bbox_center": (2.0, 1.0, 9.5), "radius": 0.125, "area": 4.0},
|
||||
}
|
||||
)
|
||||
new_model.face_logical_ids[9] = 9
|
||||
probe = _PropertyTableProbe()
|
||||
formula_text = "Face87.\u76f4\u5f84 = Face9.\u534a\u5f84"
|
||||
probe.model = old_model
|
||||
formula = parse_relation_formula(formula_text)
|
||||
probe.relation_formula_items = [
|
||||
{
|
||||
"id": 1,
|
||||
"text": formula_text,
|
||||
"enabled": True,
|
||||
"status": "applied",
|
||||
"message": "applied",
|
||||
"signatures": probe._relation_signatures_for_formula(formula),
|
||||
}
|
||||
]
|
||||
probe.model = new_model
|
||||
probe._refresh_relation_formulas_after_model_edit(
|
||||
context={"target_kind": "face", "target_id": 9, "target_logical_id": 9}
|
||||
)
|
||||
_assert(
|
||||
probe.relation_formula_items[0]["text"] == "Face11.\u76f4\u5f84 = Face9.\u534a\u5f84",
|
||||
f"unchanged formula target should stay strict while a dependency changes: {probe.relation_formula_items}",
|
||||
)
|
||||
_assert(
|
||||
str(probe.relation_formula_items[0].get("status")) != "invalid",
|
||||
f"strict target remap should keep the dependency formula valid: {probe.relation_formula_items}",
|
||||
)
|
||||
_assert(
|
||||
getattr(probe, "_last_relation_formula_refresh_affected_ids", []) == [1],
|
||||
f"edited dependency should still queue the formula for reapply after target remap: {probe.relation_formula_items}",
|
||||
)
|
||||
|
||||
def _unexpected_restore() -> bool:
|
||||
raise AssertionError("dependency relation replay must not restore the formula base snapshot")
|
||||
|
||||
probe.relation_formula_base_snapshot = {"sentinel": object()}
|
||||
probe._restore_relation_formula_base_snapshot = _unexpected_restore
|
||||
probe._start_relation_formula_reapply(reason="dependency", restore_base=False, formula_ids=[1])
|
||||
_assert(
|
||||
[int(item.get("id", -1)) for item in probe.relation_formula_replay_queue] == [1],
|
||||
f"dependency replay should queue only the affected formula: {probe.relation_formula_replay_queue}",
|
||||
)
|
||||
probe.relation_formula_replay_active = False
|
||||
probe.relation_formula_replay_queue = []
|
||||
|
||||
|
||||
def _assert_mouse_selection_guards() -> None:
|
||||
@@ -724,6 +1416,12 @@ def main() -> int:
|
||||
|
||||
_assert_diagnostics_stay_out_of_parameter_table(probe)
|
||||
_assert_relation_formula_editor()
|
||||
_assert_relation_radius_formula_proxy()
|
||||
_assert_relation_formula_input_remains_editable_while_replaying()
|
||||
_assert_relation_formula_input_clickable_after_existing_formula()
|
||||
_assert_relation_formula_input_is_selection_independent()
|
||||
_assert_relation_formula_input_recovers_after_loading()
|
||||
_assert_relation_formula_ids_follow_model_remap()
|
||||
_assert_mouse_selection_guards()
|
||||
_assert_quick_blind_depth_spec()
|
||||
_assert_user_facing_failure_messages()
|
||||
|
||||
+20
-3
@@ -287,6 +287,16 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.property_command_buttons: dict[str, QPushButton] = {}
|
||||
self.relation_formula_items: list[dict[str, object]] = []
|
||||
self.relation_formula_next_id = 1
|
||||
self.relation_formula_base_snapshot: dict[object, object] | None = None
|
||||
self.relation_formula_replay_queue: list[dict[str, object]] = []
|
||||
self.relation_formula_replay_total = 0
|
||||
self.relation_formula_replay_done = 0
|
||||
self.relation_formula_replay_active = False
|
||||
self.relation_formula_replay_current_id: int | None = None
|
||||
self.relation_formula_replay_callback_seen = False
|
||||
self._relation_formula_replay_running_action = False
|
||||
self._relation_formula_object_label_cache_key: object = None
|
||||
self._relation_formula_object_label_cache: dict[str, object] = {}
|
||||
|
||||
self._build_ui()
|
||||
self._build_vtk()
|
||||
@@ -1292,7 +1302,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.relation_formula_box.setObjectName("relationFormulaBox")
|
||||
help_tip(
|
||||
self.relation_formula_box,
|
||||
"用 FaceID.参数 = 表达式 的形式建立关系式。第一版会先计算公式并回填当前参数表目标值,再执行参数化建模。",
|
||||
"用 FaceID.参数 = 表达式 的形式建立关系式。添加后会立即按当前公式组重新计算并修改模型。",
|
||||
)
|
||||
relation_layout = QVBoxLayout(self.relation_formula_box)
|
||||
relation_layout.setContentsMargins(6, 8, 6, 6)
|
||||
@@ -1305,13 +1315,20 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.relation_formula_input.setPlaceholderText("Face87.直径 = Face85.直径")
|
||||
help_tip(
|
||||
self.relation_formula_input,
|
||||
"示例:Face87.直径 = Face85.直径,或 Face87.位置 = Face85.位置 + (0, 0, -3.5)。输入 Face87. 后会提示当前可用参数。",
|
||||
"示例:Face87.直径 = Face85.直径,或 Face87.位置 = Face85.位置 + (0, 0, -3.5)。输入 Face87. 后会提示可用参数。",
|
||||
)
|
||||
self.relation_formula_completer_model = QStringListModel(self)
|
||||
self.relation_formula_completer = QCompleter(self.relation_formula_completer_model, self)
|
||||
self.relation_formula_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
||||
self.relation_formula_completer.setFilterMode(Qt.MatchFlag.MatchStartsWith)
|
||||
self.relation_formula_completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
|
||||
self.relation_formula_input.setCompleter(self.relation_formula_completer)
|
||||
self.relation_formula_completer.setMaxVisibleItems(12)
|
||||
self.relation_formula_completer.activated[str].connect(self._on_relation_formula_completion_activated)
|
||||
relation_popup = self.relation_formula_completer.popup()
|
||||
if relation_popup is not None:
|
||||
relation_popup.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
relation_popup.installEventFilter(self)
|
||||
self.relation_formula_completer.setWidget(self.relation_formula_input)
|
||||
self.relation_formula_input.installEventFilter(self)
|
||||
self.relation_formula_input.textChanged.connect(self._on_relation_formula_input_changed)
|
||||
self.relation_formula_input.returnPressed.connect(self.add_relation_formula)
|
||||
|
||||
@@ -4808,6 +4808,24 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._editable_feature_candidates_cache.clear()
|
||||
self._cylindrical_feature_candidates_cache.clear()
|
||||
|
||||
def mark_external_recognition_stale(self, reason: str = "modified-topology") -> None:
|
||||
self._topology_refresh_generation = max(int(getattr(self, "_topology_refresh_generation", 0) or 0), 2)
|
||||
self._asitus_hole_regions_attempted = False
|
||||
self._asitus_hole_regions_loading = False
|
||||
self._asitus_hole_region_cache.clear()
|
||||
self._asitus_face_relation_cache.clear()
|
||||
self._asitus_adjacency_relation_cache.clear()
|
||||
self._asitus_geometric_relation_cache.clear()
|
||||
self._asitus_hole_recognition_info.clear()
|
||||
self._asitus_hole_recognition_info.update(
|
||||
{
|
||||
"ok": False,
|
||||
"reason": str(reason or "modified-topology"),
|
||||
"message": "Analysis Situs result ignored because the model topology has changed.",
|
||||
}
|
||||
)
|
||||
self._clear_same_domain_dependent_caches()
|
||||
|
||||
def _install_asitus_relation_summary(self, result: dict[str, object]) -> None:
|
||||
faces = result.get("faces", ())
|
||||
if isinstance(faces, (tuple, list)):
|
||||
|
||||
@@ -8257,6 +8257,10 @@ class WindowActionMixin:
|
||||
new_model.filename = self.step_path
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
new_model.mark_external_recognition_stale("isolated-edit-result")
|
||||
except Exception:
|
||||
pass
|
||||
child_message = str(response.get("message") or "隔离子进程编辑完成。")
|
||||
self._preserve_isolated_face_logical_id(new_model, context, child_message)
|
||||
started = time.perf_counter()
|
||||
@@ -9186,8 +9190,11 @@ class WindowActionMixin:
|
||||
timings["finish_ui"] = time.perf_counter() - finish_started
|
||||
locator_note = self._locate_operation_record(record)
|
||||
relation_note = ""
|
||||
relation_dependency_ids: list[int] = []
|
||||
relation_replay_active = bool(getattr(self, "relation_formula_replay_active", False))
|
||||
if hasattr(self, "_refresh_relation_formulas_after_model_edit"):
|
||||
relation_note = self._refresh_relation_formulas_after_model_edit()
|
||||
relation_note = self._refresh_relation_formulas_after_model_edit(context=context)
|
||||
relation_dependency_ids = list(getattr(self, "_last_relation_formula_refresh_affected_ids", []) or [])
|
||||
if relation_note:
|
||||
locator_note = f"{locator_note}\n{relation_note}" if locator_note else relation_note
|
||||
except Exception as exc:
|
||||
@@ -9220,6 +9227,15 @@ class WindowActionMixin:
|
||||
self.set_plain_info(f"{record.detail}{timing_detail}\n\n{locator_note}")
|
||||
if hasattr(self, "_after_property_edit_finished"):
|
||||
self._after_property_edit_finished(success=True)
|
||||
if (
|
||||
relation_dependency_ids
|
||||
and not relation_replay_active
|
||||
and hasattr(self, "_queue_relation_formula_dependency_reapply")
|
||||
and hasattr(self, "_run_pending_relation_formula_dependency_reapply")
|
||||
):
|
||||
self._queue_relation_formula_dependency_reapply(relation_dependency_ids)
|
||||
if not bool(getattr(self, "property_batch_active", False)):
|
||||
self._run_pending_relation_formula_dependency_reapply()
|
||||
|
||||
@Slot(str)
|
||||
def _fail_edit_action(self, message: str) -> None:
|
||||
|
||||
@@ -179,12 +179,48 @@ class WindowCoreMixin:
|
||||
elif watched is getattr(self, "property_table", None):
|
||||
if event.type() == QEvent.Type.Resize and hasattr(self, "_resize_property_table_columns"):
|
||||
QTimer.singleShot(0, self._resize_property_table_columns)
|
||||
elif watched is self._relation_formula_popup_widget():
|
||||
if event.type() == QEvent.Type.KeyPress and event.key() == Qt.Key.Key_Backspace:
|
||||
editor = getattr(self, "relation_formula_input", None)
|
||||
popup = self._relation_formula_popup_widget()
|
||||
if editor is not None and hasattr(editor, "backspace"):
|
||||
if popup is not None and hasattr(popup, "hide"):
|
||||
popup.hide()
|
||||
if hasattr(editor, "setFocus"):
|
||||
editor.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
editor.backspace()
|
||||
if hasattr(self, "_update_relation_formula_completions"):
|
||||
QTimer.singleShot(0, self._update_relation_formula_completions)
|
||||
event.accept()
|
||||
return True
|
||||
elif watched is getattr(self, "relation_formula_input", None):
|
||||
if event.type() == QEvent.Type.KeyPress and event.key() == Qt.Key.Key_Tab:
|
||||
if event.type() == QEvent.Type.MouseButtonPress:
|
||||
if hasattr(self, "_hide_relation_formula_completion_popup"):
|
||||
self._hide_relation_formula_completion_popup()
|
||||
if hasattr(watched, "setFocus"):
|
||||
watched.setFocus(Qt.FocusReason.MouseFocusReason)
|
||||
if event.type() in {QEvent.Type.ShortcutOverride, QEvent.Type.KeyPress} and event.key() == Qt.Key.Key_Tab:
|
||||
if bool(getattr(self, "_relation_formula_tab_completion_accepted", False)):
|
||||
self._relation_formula_tab_completion_accepted = False
|
||||
event.accept()
|
||||
return True
|
||||
if hasattr(self, "_accept_relation_formula_completion") and self._accept_relation_formula_completion():
|
||||
self._relation_formula_tab_completion_accepted = event.type() == QEvent.Type.ShortcutOverride
|
||||
if self._relation_formula_tab_completion_accepted:
|
||||
QTimer.singleShot(0, lambda: setattr(self, "_relation_formula_tab_completion_accepted", False))
|
||||
event.accept()
|
||||
return True
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def _relation_formula_popup_widget(self):
|
||||
completer = getattr(self, "relation_formula_completer", None)
|
||||
if completer is None or not hasattr(completer, "popup"):
|
||||
return None
|
||||
try:
|
||||
return completer.popup()
|
||||
except RuntimeError:
|
||||
return None
|
||||
|
||||
def _should_suppress_transient_tooltip(self, watched) -> bool:
|
||||
if not isinstance(watched, QWidget):
|
||||
return False
|
||||
@@ -1274,6 +1310,8 @@ class WindowCoreMixin:
|
||||
self.load_in_progress = False
|
||||
self.pending_load_path = None
|
||||
self._update_action_states()
|
||||
if hasattr(self, "_update_relation_formula_buttons"):
|
||||
self._update_relation_formula_buttons()
|
||||
if float(getattr(self, "preview_load_deflection", 0.0) or 0.0) > float(
|
||||
getattr(self, "initial_load_deflection", 0.0) or 0.0
|
||||
):
|
||||
@@ -1337,6 +1375,8 @@ class WindowCoreMixin:
|
||||
self.load_in_progress = False
|
||||
self.pending_load_path = None
|
||||
self._update_action_states()
|
||||
if hasattr(self, "_update_relation_formula_buttons"):
|
||||
self._update_relation_formula_buttons()
|
||||
self._request_thread_quit(self.load_thread)
|
||||
self._request_thread_quit(self.load_refine_thread)
|
||||
|
||||
@@ -1387,6 +1427,8 @@ class WindowCoreMixin:
|
||||
self.redo_stack.clear()
|
||||
self.operation_history.clear()
|
||||
self.redo_history.clear()
|
||||
if hasattr(self, "_clear_relation_formula_runtime_state"):
|
||||
self._clear_relation_formula_runtime_state(clear_items=True)
|
||||
self.clear_diff_preview(render=False)
|
||||
if hasattr(self, "history_list"):
|
||||
self.history_list.clear()
|
||||
|
||||
+935
-69
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user