feat: 完善 STEP/B-Rep 一级关系参数化编辑
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user