feat: 完善 STEP/B-Rep 一级关系参数化编辑

This commit is contained in:
2026-08-07 18:08:32 +08:00
parent eef9efcc1e
commit 12250603dd
26 changed files with 4254 additions and 1270 deletions
+390 -138
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import math
import os
from pathlib import Path
import sys
@@ -25,7 +26,15 @@ if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from step_editor.widgets import NoWheelComboBox
from step_editor.window_state import WindowStateMixin
from step_editor.window_actions import WindowActionMixin
from step_editor.window_core import WindowCoreMixin
from step_editor.window_state import (
PROPERTY_CURRENT_COLUMN,
PROPERTY_LABEL_COLUMN,
PROPERTY_SCOPE_COLUMN,
PROPERTY_TARGET_COLUMN,
WindowStateMixin,
)
class _StatusBar:
@@ -33,7 +42,7 @@ class _StatusBar:
pass
class _PropertyCardProbe(QWidget, WindowStateMixin):
class _PropertyTableProbe(QWidget, WindowStateMixin):
def __init__(self) -> None:
super().__init__()
self.model = object()
@@ -42,30 +51,58 @@ class _PropertyCardProbe(QWidget, WindowStateMixin):
self.load_in_progress = False
self.property_editor_updating = False
self.property_table_expanded = False
self.property_table_collapsed_rows = 4
self.property_table_collapsed_rows = 5
self.property_table_min_visible_rows = 5
self.property_editor_selected_row = None
self.property_command_active_key = ""
self.property_command_buttons = {}
self.property_editor_specs = []
self.selected_kind = "face"
self.selected_kind = "feature"
self.selected_part_id = None
self.selected_solid_id = None
self.selected_face_id = 0
self.selected_edge_id = None
self.current_info_values = {"area": 100.0}
self.property_table = QTableWidget(0, 5)
self.current_info_values = self._plane_info()
layout = QVBoxLayout(self)
self.object_edit_box = self
self.property_table = QTableWidget(0, 4)
self.property_table.setHorizontalHeaderLabels(["尺寸参数", "当前值", "建模意图", "目标值"])
layout.addWidget(self.property_table)
self.property_card_scroll = QScrollArea()
self.property_card_container = QWidget()
self.property_card_layout = QVBoxLayout(self.property_card_container)
self.property_card_scroll.setWidget(self.property_card_container)
layout.addWidget(self.property_card_scroll)
self.property_expand_button = QPushButton()
layout.addWidget(self.property_expand_button)
self.property_command_summary_label = QLabel()
self.property_command_bar = QFrame()
self.property_command_layout = QHBoxLayout(self.property_command_bar)
self.property_command_help_label = QLabel()
self.current_capability_headline = QLabel()
self.apply_property_button = QPushButton()
@staticmethod
def _plane_info() -> dict[str, object]:
return {
"area": 100.0,
"area_center": (5.0, 5.0, 0.0),
"bbox_center": (5.0, 5.0, 0.0),
"bbox_diagonal": 14.1421356237,
"local_face_width": 10.0,
"local_face_height": 10.0,
"plane_origin": (0.0, 0.0, 0.0),
"push_pull_outward_direction": (0.0, 0.0, 1.0),
"normal": (0.0, 0.0, 1.0),
}
def _selected_action_info(self) -> dict[str, object]:
return {
"area": 100.0,
**self.current_info_values,
"surface": "plane",
"push_pull_status": "ready",
"first_level_boundary_edge_count": 4,
@@ -80,6 +117,82 @@ class _PropertyCardProbe(QWidget, WindowStateMixin):
return _StatusBar()
class _ActionMessageProbe(WindowActionMixin):
pass
class _TimerProbe:
def stop(self) -> None:
pass
class _RenderWindowProbe:
def Render(self) -> None:
pass
class _StatusBarProbe:
def __init__(self) -> None:
self.messages: list[str] = []
def showMessage(self, message: str) -> None:
self.messages.append(str(message))
class _MouseSelectionProbe(WindowCoreMixin):
def __init__(self) -> None:
self.pointer_button_down = False
self.left_button_press_position = None
self.left_button_press_camera_state = None
self.left_button_press_target = None
self.left_button_dragged = False
self.left_click_drag_threshold_px = 6
self.left_click_camera_tolerance = 1e-7
self.camera_interaction_active = False
self.pending_hover_position = None
self.last_hover_pick_position = None
self.hover_timer = _TimerProbe()
self.camera_state = (0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 10.0, 30.0)
self.load_in_progress = False
self.operation_in_progress = False
self.scan_in_progress = False
self.model = object()
self.selected_kind = None
self.render_window = _RenderWindowProbe()
self.status_bar = _StatusBarProbe()
self.pick_targets: list[dict[str, object] | None] = [
{"kind": "face", "target_id": 1, "pick_position": (0.0, 0.0, 0.0)},
{"kind": "face", "target_id": 1, "pick_position": (0.0, 0.0, 0.0)},
]
self.selected_targets: list[dict[str, object]] = []
self.hover_clear_count = 0
self.camera_end_count = 0
def _camera_state_signature(self):
return tuple(self.camera_state)
def _current_selection_mode(self) -> str:
return "Face"
def _pick_selection_target(self, _mode: str, _x: int, _y: int) -> dict[str, object] | None:
if self.pick_targets:
return self.pick_targets.pop(0)
return None
def _select_pick_target(self, target: dict[str, object]) -> None:
self.selected_targets.append(dict(target))
def statusBar(self) -> _StatusBarProbe:
return self.status_bar
def _clear_hover(self, render: bool = True) -> None:
self.hover_clear_count += 1
def _end_camera_interaction(self) -> None:
self.camera_interaction_active = False
self.camera_end_count += 1
class _TopLevelPropertyLabelProbe(QObject):
def __init__(self) -> None:
super().__init__()
@@ -101,152 +214,291 @@ def _assert(condition: bool, message: str) -> None:
raise AssertionError(message)
def _row_by_label(probe: _PropertyTableProbe, label: str) -> int:
for row in range(probe.property_table.rowCount()):
item = probe.property_table.item(row, PROPERTY_LABEL_COLUMN)
if item is not None and item.text() == label:
return row
labels = [
probe.property_table.item(row, PROPERTY_LABEL_COLUMN).text()
for row in range(probe.property_table.rowCount())
if probe.property_table.item(row, PROPERTY_LABEL_COLUMN) is not None
]
raise AssertionError(f"{label!r} row was not found; labels={labels}")
def _assert_property_table_editor(probe: _PropertyTableProbe) -> None:
_assert(
not WindowCoreMixin._should_suppress_transient_tooltip(probe, probe.property_table),
"property table tooltip events should not be suppressed",
)
_assert(
not WindowCoreMixin._should_suppress_transient_tooltip(probe, probe.property_table.viewport()),
"property table viewport tooltip events should not be suppressed",
)
headers = [
probe.property_table.horizontalHeaderItem(column).text()
for column in range(probe.property_table.columnCount())
]
_assert(headers == ["尺寸参数", "当前值", "建模意图", "目标值"], f"unexpected table headers: {headers}")
_assert(probe.property_table.rowCount() == len(probe.property_editor_specs), "table row count should match specs")
header_height = int(probe.property_table.horizontalHeader().height())
frame = int(probe.property_table.frameWidth()) * 2
default_row_height = max(int(probe.property_table.verticalHeader().defaultSectionSize()), 22)
expected_five_row_height = header_height + frame + default_row_height * 5 + 8
_assert(
probe.property_table.minimumHeight() >= expected_five_row_height,
"feature parameter table should reserve enough height for five default rows",
)
_assert(not getattr(probe, "property_card_rows", {}), "property card rows should not be built in table mode")
_assert(not probe.property_card_scroll.isVisible(), "property card scroll area should stay hidden")
_assert(probe.property_card_scroll.maximumHeight() == 0, "property card scroll area should not reserve height")
_assert(not probe.property_command_buttons, "legacy command buttons should not be shown in the parameter table")
_assert(not probe.property_command_bar.isVisible(), "legacy command bar should be hidden")
table_labels = [
probe.property_table.item(row, PROPERTY_LABEL_COLUMN).text()
for row in range(probe.property_table.rowCount())
if probe.property_table.item(row, PROPERTY_LABEL_COLUMN) is not None
]
actionable_labels = [str(spec.get("label", "")) for _row, spec in probe._actionable_property_rows()]
for label in ("面内长度", "面内宽度", "中心", "偏移"):
_assert(label in actionable_labels, f"feature parameter table did not expose {label}")
for legacy_label in ("面积", "U向尺寸", "V向尺寸", "偏移变换"):
_assert(legacy_label not in actionable_labels, f"{legacy_label} should not be exposed as an editable parameter")
_assert(legacy_label not in table_labels, f"{legacy_label} should not appear in the feature parameter table")
for diagnostic_label in ("建模形式", "推荐操作", "一级关系", "关联探测"):
_assert(diagnostic_label not in table_labels, f"{diagnostic_label} should stay out of the feature parameter table")
target_row = _row_by_label(probe, "面内长度")
target_widget = probe.property_table.cellWidget(target_row, PROPERTY_TARGET_COLUMN)
scope_widget = probe.property_table.cellWidget(target_row, PROPERTY_SCOPE_COLUMN)
_assert(isinstance(target_widget, QLineEdit), "editable table row should have a target editor")
_assert(isinstance(scope_widget, NoWheelComboBox), "editable table row should have a modeling-intent combo")
_assert(
not probe.property_table.findChildren(QPushButton),
"feature parameter table should not contain per-row apply buttons",
)
target_widget.setText("12")
probe._update_property_apply_state()
changed = probe._changed_property_rows()
_assert(any(row == target_row for row, _spec, _text in changed), "table target edit was not detected")
_assert(probe.apply_property_button.isEnabled(), "single parametric modeling button should enable for one changed row")
probe.toggle_property_table_expanded()
_assert(probe.property_table_expanded, "property table expand toggle failed")
_assert("收起" in probe.property_expand_button.text(), "expanded table button should offer to collapse")
preserved_editor = probe.property_table.cellWidget(target_row, PROPERTY_TARGET_COLUMN)
_assert(isinstance(preserved_editor, QLineEdit), "target editor disappeared after table expand")
_assert(preserved_editor.text().strip() == "12", "target value was not preserved after table expand")
def _assert_diagnostics_stay_out_of_parameter_table(probe: _PropertyTableProbe) -> None:
long_context = "已按“相邻特征”沿共享边拓扑探测当前特征及 3 个局部关联特征;关联尺寸可在同一参数表中直接修改。"
probe.current_info_values = {
**probe._plane_info(),
"feature_context_note": long_context,
"feature_detection_level": "相邻特征",
"associated_feature_count": 3,
}
probe._refresh_property_editor()
QApplication.processEvents()
table_labels = [
probe.property_table.item(row, PROPERTY_LABEL_COLUMN).text()
for row in range(probe.property_table.rowCount())
if probe.property_table.item(row, PROPERTY_LABEL_COLUMN) is not None
]
for diagnostic_label in ("建模形式", "推荐操作", "一级关系", "关联探测"):
_assert(diagnostic_label not in table_labels, f"{diagnostic_label} should not be shown as a feature parameter")
def _assert_mouse_selection_guards() -> None:
mouse_probe = _MouseSelectionProbe()
mouse_probe._handle_left_button_press(20, 20)
mouse_probe._handle_left_button_release(20, 20)
_assert(
[target.get("target_id") for target in mouse_probe.selected_targets] == [1],
"plain left click should still select",
)
mouse_probe = _MouseSelectionProbe()
mouse_probe._handle_left_button_press(20, 20)
mouse_probe._update_left_button_drag_state(40, 20)
mouse_probe._handle_left_button_release(40, 20)
_assert(not mouse_probe.selected_targets, "left-button drag should not select a face on release")
mouse_probe = _MouseSelectionProbe()
mouse_probe._handle_left_button_press(20, 20)
mouse_probe.camera_state = (0.5, 0.0, 9.8, 0.0, 0.0, 0.0, 0.02, 1.0, 0.0, 1.0, 9.8, 30.0)
mouse_probe._handle_left_button_release(22, 21)
_assert(
not mouse_probe.selected_targets,
"left-button camera rotation should not select a face even when the cursor lands on the model",
)
mouse_probe = _MouseSelectionProbe()
mouse_probe.pick_targets = [None, {"kind": "face", "target_id": 1, "pick_position": (0.0, 0.0, 0.0)}]
mouse_probe._handle_left_button_press(20, 20)
mouse_probe._handle_left_button_release(20, 20)
_assert(
not mouse_probe.selected_targets,
"left-button press on the background should not select a face on release",
)
mouse_probe = _MouseSelectionProbe()
mouse_probe.pick_targets = [
{"kind": "face", "target_id": 1, "pick_position": (0.0, 0.0, 0.0)},
{"kind": "face", "target_id": 2, "pick_position": (0.0, 0.0, 0.0)},
]
mouse_probe._handle_left_button_press(20, 20)
mouse_probe._handle_left_button_release(20, 20)
_assert(
not mouse_probe.selected_targets,
"left-button press/release on different faces should not change selection",
)
def _assert_quick_blind_depth_spec() -> None:
blind_probe = _PropertyTableProbe()
blind_probe.selected_kind = "feature"
blind_probe.selected_face_id = 6
quick_blind_info = {
"surface": "cylinder",
"diameter": 4.0,
"radius": 2.0,
"axis_point": (0.0, 0.0, 0.0),
"axis": (0.0, 0.0, 1.0),
"angular_span": math.tau,
"feature_guess": "hole/groove candidate",
"feature_type": "圆柱孔候选",
"confidence": "medium",
"cylinder_end_type": "blind",
"hole_depth_estimate": 6.0,
"depth_status": "ready",
"recognition_ready_actions": "孔/槽/圆柱直径;盲孔/盲槽深度;封堵孔/槽",
}
blind_specs, _blind_used = blind_probe._editable_property_specs(quick_blind_info)
blind_feature_specs = blind_probe._feature_property_specs(blind_specs, quick_blind_info)
blind_depth_specs = [
spec for spec in blind_feature_specs if str(spec.get("key", "")) == "hole_depth_estimate"
]
_assert(blind_depth_specs, "quick blind hole depth estimate should be visible in feature parameters")
blind_depth_spec = blind_depth_specs[0]
blind_effective_depth = blind_probe._effective_property_spec(blind_depth_spec)
_assert(bool(blind_effective_depth.get("enabled")), "quick blind hole depth should be editable")
_assert(
str(blind_effective_depth.get("action", "")) == "resize_hole_depth",
f"quick blind hole depth should use local depth edit first: {blind_effective_depth}",
)
_assert(
"重新确认底面" in str(blind_effective_depth.get("enabled_tip", "")),
"quick blind hole depth should explain execution-time bottom-face confirmation",
)
owning_mode = dict(blind_depth_spec.get("scope_modes", {})).get("owning", {})
_assert(
not bool(owning_mode.get("enabled")),
"quick blind hole without explicit bottom faces should not expose owning-scale depth as editable",
)
def _assert_user_facing_failure_messages() -> None:
action_probe = _ActionMessageProbe()
illegal_context = {
"operation_name": "测试修改",
"target": "Face 1",
"parameters": {"resize_status": "blocked", "resize_blockers": "目标值必须大于 0。"},
}
illegal_blocker = action_probe._edit_preflight_blocker(illegal_context)
_assert(illegal_blocker is not None, "blocked edit plan should be stopped before worker startup")
_assert(illegal_blocker[0] == "当前操作不合法", "illegal blocked edit should be classified clearly")
english_illegal_context = {
"operation_name": "调整槽宽",
"target": "Face 3",
"parameters": {"resize_status": "blocked", "resize_blockers": "Target slot value must be greater than 0."},
}
english_illegal_blocker = action_probe._edit_preflight_blocker(english_illegal_context)
_assert(
english_illegal_blocker is not None and english_illegal_blocker[0] == "当前操作不合法",
"english geometry blockers should also be classified as illegal operations",
)
risk_blocker = action_probe._plan_preflight_blocker(
{"status": "blocked", "risk": "blocked", "message": "目标壳体厚度会让几何风险过高。"}
)
_assert(risk_blocker is not None and risk_blocker[0] == "风险过高", "blocked high-risk plans should be classified")
recognition_blocker = action_probe._plan_preflight_blocker(
{"status": "blocked", "message": "当前平面没有识别到相对壳体平面。"}
)
_assert(
recognition_blocker is not None and recognition_blocker[0] == "识别不足",
"blocked recognition failures should be classified",
)
unsupported_blocker = action_probe._plan_preflight_blocker(
{"status": "blocked", "message": "当前版本只对简单圆锥解析重建开放这类修改。"}
)
_assert(
unsupported_blocker is not None and unsupported_blocker[0] == "暂未实现",
"blocked unsupported capability plans should be classified",
)
auxiliary_context = {
"operation_name": "测试修改",
"target": "Face 4",
"parameters": {"quick_plan_status": "blocked", "resize_status": "ready", "resize_blockers": ""},
}
_assert(action_probe._edit_preflight_blocker(auxiliary_context) is None, "auxiliary statuses should not block ready edits")
deferred_context = {
"operation_name": "复杂 Face 修改",
"target": "Face 2",
"parameters": {"ui_deferred_model_plan": True, "message": "当前对象需要完整一级关系计划。"},
}
deferred_blocker = action_probe._edit_preflight_blocker(deferred_context)
_assert(deferred_blocker is not None, "deferred model plan should be stopped before slow worker startup")
_assert(deferred_blocker[0] == "暂未实现", "deferred model plan should be classified as unsupported")
title, user_message = action_probe._user_facing_edit_failure_message(
"隔离子进程执行失败;主程序没有崩溃,原模型保持不变。 当前版本暂不支持复杂链式圆角。",
deferred_context,
)
_assert(title == "不能修改", "edit failure dialog title should be user-facing")
_assert("隔离子进程" not in user_message and "子进程" not in user_message, "failure message should not expose isolation details")
_assert("暂未实现" in user_message, "unsupported failure should state that the operation is not implemented yet")
_empty_title, empty_message = action_probe._user_facing_edit_failure_message(
"隔离子进程执行失败;主程序没有崩溃,原模型保持不变。",
deferred_context,
)
_assert("隔离子进程" not in empty_message and "子进程" not in empty_message, "empty internal failure should stay user-facing")
def main() -> int:
app = QApplication.instance() or QApplication([])
top_level_label_probe = _TopLevelPropertyLabelProbe()
app.installEventFilter(top_level_label_probe)
probe = _PropertyCardProbe()
probe = _PropertyTableProbe()
probe.show()
QApplication.processEvents()
probe._refresh_property_editor()
QApplication.processEvents()
_assert(
not top_level_label_probe.shown_labels,
f"property card labels were shown as transient top-level windows: {top_level_label_probe.shown_labels}",
f"property labels were shown as transient top-level windows: {top_level_label_probe.shown_labels}",
)
actionable_labels = [str(spec.get("label", "")) for _row, spec in probe._actionable_property_rows()]
_assert("面积" in actionable_labels, "modifiable feature list did not expose the editable area row")
_assert(not probe.property_command_buttons, "legacy command buttons should not be shown in the modifiable-feature panel")
_assert(not probe.property_command_bar.isVisible(), "legacy command bar should be hidden")
_assert("可修改项" in probe.property_command_summary_label.text(), "modifiable-feature summary is missing")
rows = getattr(probe, "property_card_rows", {})
_assert(rows, "property card rows were not built")
_assert_property_table_editor(probe)
_assert("当前支持" in probe.current_capability_headline.text(), "software progress panel did not show supported areas")
_assert("优先:" not in probe.current_capability_headline.text(), "software progress panel should not show priority copy")
_assert(
not any(isinstance(widgets.get("target_editor"), QLineEdit) for widgets in rows.values()),
"modifiable feature rows should be compact until the user expands one",
)
_assert(
all(str(widgets.get("status_label").text()) == "可修改" for widgets in rows.values()),
"compact modifiable rows should end with the editable status label",
)
_assert(
all(not str(widgets.get("current_value").toolTip()) for widgets in rows.values() if widgets.get("current_value") is not None),
"compact modifiable rows should not show click-triggered tooltips",
"矩形槽口袋" in probe.current_capability_headline.toolTip()
and "多台阶矩形凸台顶层" in probe.current_capability_headline.toolTip(),
"software progress tooltip should name newly supported prismatic feature edits",
)
target_row = probe._actionable_property_rows()[0][0]
probe._select_property_card_row(target_row)
rows = getattr(probe, "property_card_rows", {})
editable_rows = [row for row, widgets in rows.items() if isinstance(widgets.get("action_button"), QPushButton)]
target_rows = [row for row, widgets in rows.items() if isinstance(widgets.get("target_editor"), QLineEdit)]
scope_rows = [row for row, widgets in rows.items() if isinstance(widgets.get("scope_combo"), NoWheelComboBox)]
_assert_diagnostics_stay_out_of_parameter_table(probe)
_assert_mouse_selection_guards()
_assert_quick_blind_depth_spec()
_assert_user_facing_failure_messages()
_assert(editable_rows, "editable property card button is missing")
_assert(target_rows, "property card target editor is missing")
_assert(scope_rows, "property card modeling-intent combo is missing")
probe._toggle_property_card_row(target_row)
QApplication.processEvents()
rows = getattr(probe, "property_card_rows", {})
_assert(probe.property_editor_selected_row is None, "clicking an expanded row should collapse it")
_assert(
not any(isinstance(widgets.get("target_editor"), QLineEdit) for widgets in rows.values()),
"collapsed modifiable rows should return to compact display",
)
_assert(
not any(editor.isVisible() for editor in probe.property_card_container.findChildren(QLineEdit)),
"collapsed modifiable rows should not leave stale target editors visible",
)
_assert(
not any(
label.isVisible() and "建模意图:" in label.text()
for label in probe.property_card_container.findChildren(QLabel)
),
"collapsed modifiable rows should not leave stale expanded hints visible",
)
probe._select_property_card_row(target_row)
rows = getattr(probe, "property_card_rows", {})
target_rows = [row for row, widgets in rows.items() if isinstance(widgets.get("target_editor"), QLineEdit)]
_assert(target_rows, "target editor is missing after re-expanding the row")
rows[target_rows[0]]["target_editor"].setText("144")
probe._update_property_apply_state()
changed = probe._changed_property_rows()
button = rows[target_rows[0]]["action_button"]
_assert(changed, "card target edit was not detected")
_assert(bool(button.property("changed")), "card button did not enter changed state")
_assert(button.isEnabled(), "card button should be enabled for a valid changed target")
probe.toggle_property_table_expanded()
_assert(probe.property_table_expanded, "property card expand toggle failed")
_assert(len(probe.property_card_rows) > len(rows), "expanded property card list did not reveal more rows")
expanded_editor = probe.property_card_rows[target_rows[0]].get("target_editor")
_assert(isinstance(expanded_editor, QLineEdit), "expanded target editor is missing")
_assert(expanded_editor.text().strip() == "144", "target value was not preserved after card rebuild")
long_context = "已按“相邻特征”沿共享边拓扑探测当前特征及 3 个局部关联特征;关联尺寸可在同一参数表中直接修改。"
probe.property_editor_specs = [
{
"key": "feature_context_note",
"label": "关联探测",
"current_text": long_context,
"current_raw": long_context,
"target_text": "",
"editable": False,
"enabled": False,
"status_text": "说明",
"span_value_columns": True,
"pin_top": True,
},
{
"key": "associated_face_center",
"label": "相邻平面 Face 1580 · 中心",
"current_text": "(-118.585, -23.3, -269.561)",
"current_raw": "(-118.585, -23.3, -269.561)",
"target_text": "(-118.585, -23.3, -269.561)",
"editable": True,
"enabled": True,
"action": "move_face_center",
"scope_text": "局部重建",
"status_text": "可修改",
},
]
probe.property_table_expanded = True
probe.property_editor_selected_row = None
probe._rebuild_property_cards()
QApplication.processEvents()
context_value = next(
(
label
for label in probe.property_card_rows[0]["card"].findChildren(QLabel)
if label.objectName() == "propertyCardValue"
),
None,
)
_assert(isinstance(context_value, QLabel), "long associated detection note is missing")
_assert(context_value.wordWrap(), "long associated detection note should wrap in diagnostics view")
_assert("关联尺寸可在同一参数表中直接修改" in context_value.text(), "associated detection note lost its trailing text")
associated_widgets = probe.property_card_rows[1]
associated_card = associated_widgets["card"]
associated_title = next(
(
label
for label in associated_card.findChildren(QLabel)
if label.objectName() == "propertyCardTitle"
),
None,
)
_assert(isinstance(associated_title, QLabel), "long associated row title is missing")
_assert(associated_title.wordWrap(), "long associated row title should use a two-line compact layout")
_assert(associated_card.height() > 24, "long associated row should be taller than a one-line compact row")
associated_value = associated_widgets.get("current_value")
_assert(isinstance(associated_value, QLabel), "long associated row value is missing")
_assert("(-118.585" in associated_value.text(), "long associated row value was hidden by the title")
print("property card editor UI ok")
print("property table editor UI ok")
if QApplication.instance() is app:
app.quit()
return 0