610 lines
25 KiB
Python
610 lines
25 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
|
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
|
|
from PySide6.QtCore import QEvent, QObject
|
|
from PySide6.QtWidgets import (
|
|
QApplication,
|
|
QCheckBox,
|
|
QFrame,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QLineEdit,
|
|
QPushButton,
|
|
QScrollArea,
|
|
QTableWidget,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
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_actions import WindowActionMixin
|
|
from step_editor.window_core import WindowCoreMixin
|
|
from step_editor.window_state import (
|
|
PROPERTY_CURRENT_COLUMN,
|
|
PROPERTY_INPUT_COLUMN,
|
|
PROPERTY_LABEL_COLUMN,
|
|
PROPERTY_SCOPE_COLUMN,
|
|
PROPERTY_TABLE_HEADERS,
|
|
PROPERTY_TARGET_COLUMN,
|
|
WindowStateMixin,
|
|
)
|
|
|
|
|
|
class _StatusBar:
|
|
def showMessage(self, _text: str) -> None:
|
|
pass
|
|
|
|
|
|
class _PropertyTableProbe(QWidget, WindowStateMixin):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.model = object()
|
|
self.operation_in_progress = False
|
|
self.scan_in_progress = False
|
|
self.load_in_progress = False
|
|
self.property_editor_updating = False
|
|
self.property_table_expanded = False
|
|
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 = "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 = self._plane_info()
|
|
|
|
layout = QVBoxLayout(self)
|
|
self.object_edit_box = self
|
|
self.property_table = QTableWidget(0, len(PROPERTY_TABLE_HEADERS))
|
|
self.property_table.setHorizontalHeaderLabels(list(PROPERTY_TABLE_HEADERS))
|
|
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()
|
|
self.export_parameters_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 {
|
|
**self.current_info_values,
|
|
"surface": "plane",
|
|
"push_pull_status": "ready",
|
|
"first_level_boundary_edge_count": 4,
|
|
"first_level_boundary_vertex_count": 4,
|
|
"first_level_adjacent_face_count": 4,
|
|
}
|
|
|
|
def _set_control_state(self, widget, enabled: bool, _enabled_tip: str, _disabled_tip: str) -> None:
|
|
widget.setEnabled(enabled)
|
|
|
|
def statusBar(self) -> _StatusBar:
|
|
return _StatusBar()
|
|
|
|
|
|
class _ActionMessageProbe(WindowActionMixin):
|
|
pass
|
|
|
|
|
|
class _ParameterExportActionProbe(WindowActionMixin):
|
|
def __init__(self, output_path: Path) -> None:
|
|
self.output_path = output_path
|
|
self.status_bar = _StatusBarProbe()
|
|
self.info_text = ""
|
|
self.export_state_updates = 0
|
|
|
|
def _parameter_export_output_path(self) -> Path:
|
|
return self.output_path
|
|
|
|
def _selected_parameter_export_rows(self) -> list[dict[str, str]]:
|
|
return [
|
|
{
|
|
"name": "面内长度",
|
|
"displayName": "面内长度",
|
|
"type": "number",
|
|
"ioRole": "input",
|
|
"default": "151",
|
|
},
|
|
{
|
|
"name": "偏移",
|
|
"displayName": "偏移",
|
|
"type": "number",
|
|
"ioRole": "input",
|
|
"default": "57.5",
|
|
},
|
|
]
|
|
|
|
def _update_parameter_export_state(self) -> None:
|
|
self.export_state_updates += 1
|
|
|
|
def statusBar(self) -> _StatusBarProbe:
|
|
return self.status_bar
|
|
|
|
def set_plain_info(self, text: str) -> None:
|
|
self.info_text = text
|
|
|
|
|
|
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__()
|
|
self.shown_labels: list[str] = []
|
|
|
|
def eventFilter(self, watched, event):
|
|
if (
|
|
event.type() == QEvent.Type.Show
|
|
and isinstance(watched, QLabel)
|
|
and watched.isWindow()
|
|
and watched.objectName().startswith("propertyCard")
|
|
):
|
|
self.shown_labels.append(f"{type(watched).__name__}:{watched.objectName()}:{watched.text()}")
|
|
return False
|
|
|
|
|
|
def _assert(condition: bool, message: str) -> None:
|
|
if not condition:
|
|
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 == list(PROPERTY_TABLE_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}")
|
|
_assert("中心" not in actionable_labels, "center should be temporarily hidden from editable parameters")
|
|
_assert("中心" not in table_labels, "center should not appear in the feature parameter table")
|
|
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)
|
|
input_checkbox = probe._property_input_checkbox(target_row)
|
|
_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(isinstance(input_checkbox, QCheckBox), "editable table row should have an input-parameter checkbox")
|
|
_assert(not input_checkbox.isChecked(), "input-parameter checkbox should be unchecked by default")
|
|
_assert(
|
|
probe.property_table.cellWidget(target_row, PROPERTY_INPUT_COLUMN) is not None,
|
|
"input-parameter checkbox should be hosted in the input column",
|
|
)
|
|
_assert(not probe.export_parameters_button.isEnabled(), "parameter export button should start disabled")
|
|
_assert(
|
|
not probe.property_table.findChildren(QPushButton),
|
|
"feature parameter table should not contain per-row apply buttons",
|
|
)
|
|
|
|
input_checkbox.setChecked(True)
|
|
QApplication.processEvents()
|
|
selected_rows = probe._selected_parameter_export_rows()
|
|
_assert(probe.export_parameters_button.isEnabled(), "parameter export button should enable after a row is checked")
|
|
_assert(
|
|
selected_rows == [
|
|
{
|
|
"name": "面内长度",
|
|
"displayName": "面内长度",
|
|
"type": "number",
|
|
"ioRole": "input",
|
|
"default": "10",
|
|
}
|
|
],
|
|
f"unexpected parameter export payload: {selected_rows}",
|
|
)
|
|
|
|
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 _assert_parameter_export_action() -> None:
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
output_path = Path(temp_dir) / "data.json"
|
|
probe = _ParameterExportActionProbe(output_path)
|
|
probe.export_selected_parameters()
|
|
payload = json.loads(output_path.read_text(encoding="utf-8"))
|
|
_assert(
|
|
payload == [
|
|
{
|
|
"name": "面内长度",
|
|
"displayName": "面内长度",
|
|
"type": "number",
|
|
"ioRole": "input",
|
|
"default": "151",
|
|
},
|
|
{
|
|
"name": "偏移",
|
|
"displayName": "偏移",
|
|
"type": "number",
|
|
"ioRole": "input",
|
|
"default": "57.5",
|
|
},
|
|
],
|
|
f"parameter export action wrote unexpected JSON: {payload}",
|
|
)
|
|
_assert(probe.status_bar.messages and "已导出 2 个输入参数" in probe.status_bar.messages[-1], "parameter export status should mention exported count")
|
|
_assert("data.json" in probe.info_text and "参数数量:2" in probe.info_text, "parameter export info panel summary should be useful")
|
|
|
|
|
|
def main() -> int:
|
|
app = QApplication.instance() or QApplication([])
|
|
top_level_label_probe = _TopLevelPropertyLabelProbe()
|
|
app.installEventFilter(top_level_label_probe)
|
|
|
|
probe = _PropertyTableProbe()
|
|
probe.show()
|
|
QApplication.processEvents()
|
|
probe._refresh_property_editor()
|
|
QApplication.processEvents()
|
|
_assert(
|
|
not top_level_label_probe.shown_labels,
|
|
f"property labels were shown as transient top-level windows: {top_level_label_probe.shown_labels}",
|
|
)
|
|
_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(
|
|
"矩形槽口袋" in probe.current_capability_headline.toolTip()
|
|
and "多台阶矩形凸台顶层" in probe.current_capability_headline.toolTip(),
|
|
"software progress tooltip should name newly supported prismatic feature edits",
|
|
)
|
|
|
|
_assert_diagnostics_stay_out_of_parameter_table(probe)
|
|
_assert_mouse_selection_guards()
|
|
_assert_quick_blind_depth_spec()
|
|
_assert_user_facing_failure_messages()
|
|
_assert_parameter_export_action()
|
|
|
|
print("property table editor UI ok")
|
|
if QApplication.instance() is app:
|
|
app.quit()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|