feat: 完善一级关系编辑 UI 与视图体验
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QPoint, Qt
|
||||
from PySide6.QtTest import QTest
|
||||
from PySide6.QtWidgets import QApplication, QLabel, QLineEdit, QPushButton, 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.app import DEFAULT_MODEL_PATH, StepEditorWindow, _suppress_vtk_output_window
|
||||
from step_editor.widgets import NoWheelComboBox
|
||||
|
||||
|
||||
def _pump(app: QApplication, ms: int = 150) -> None:
|
||||
deadline = time.monotonic() + ms / 1000.0
|
||||
while time.monotonic() < deadline:
|
||||
app.processEvents()
|
||||
time.sleep(0.005)
|
||||
|
||||
|
||||
def _native_grab(widget: QWidget, target: Path) -> None:
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
return
|
||||
screen = app.primaryScreen()
|
||||
if screen is None:
|
||||
return
|
||||
screen.grabWindow(int(widget.winId())).save(str(target))
|
||||
|
||||
|
||||
def _compact_text(text: object, limit: int = 120) -> str:
|
||||
value = str(text or "").replace("\n", " ").strip()
|
||||
if len(value) <= limit:
|
||||
return value
|
||||
return f"{value[: limit - 1]}..."
|
||||
|
||||
|
||||
def _label_overflows(label: QLabel) -> bool:
|
||||
if label.width() <= 0:
|
||||
return False
|
||||
return bool(label.sizeHint().width() > label.width() + 3 and not label.wordWrap())
|
||||
|
||||
|
||||
def _row_snapshot(window: StepEditorWindow) -> list[dict[str, object]]:
|
||||
rows = getattr(window, "property_card_rows", {})
|
||||
result: list[dict[str, object]] = []
|
||||
if not isinstance(rows, dict):
|
||||
return result
|
||||
for row, widgets in rows.items():
|
||||
if not isinstance(widgets, dict):
|
||||
continue
|
||||
card = widgets.get("card")
|
||||
if not isinstance(card, QWidget):
|
||||
continue
|
||||
labels = card.findChildren(QLabel)
|
||||
result.append(
|
||||
{
|
||||
"row": int(row),
|
||||
"height": card.height(),
|
||||
"selected": bool(card.property("selected")),
|
||||
"visible": card.isVisible(),
|
||||
"title": _compact_text(widgets.get("status_label").text() if False else ""),
|
||||
"labels": [
|
||||
{
|
||||
"object": label.objectName(),
|
||||
"text": _compact_text(label.text(), 90),
|
||||
"width": label.width(),
|
||||
"size_hint_width": label.sizeHint().width(),
|
||||
"overflow": _label_overflows(label),
|
||||
}
|
||||
for label in labels
|
||||
if label.isVisible()
|
||||
],
|
||||
"has_editor": isinstance(widgets.get("target_editor"), QLineEdit),
|
||||
"has_scope": isinstance(widgets.get("scope_combo"), NoWheelComboBox),
|
||||
"has_button": isinstance(widgets.get("action_button"), QPushButton),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _actionable_labels(window: StepEditorWindow) -> list[str]:
|
||||
return [str(spec.get("label", "") or "") for _row, spec in window._actionable_property_rows()]
|
||||
|
||||
|
||||
def _find_actionable_target(window: StepEditorWindow) -> dict[str, object] | None:
|
||||
if window.model is None:
|
||||
return None
|
||||
best: dict[str, object] | None = None
|
||||
best_score = -1
|
||||
max_faces = len(window.model.faces)
|
||||
for mode, selector in (("Face", window.select_face), ("Feature", window.select_feature)):
|
||||
window._set_selection_mode(mode)
|
||||
for face_id in range(max_faces):
|
||||
selector(face_id)
|
||||
QApplication.processEvents()
|
||||
labels = _actionable_labels(window)
|
||||
score = len(labels)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best = {"mode": mode, "face_id": face_id, "labels": labels}
|
||||
if score >= 3:
|
||||
return best
|
||||
return best
|
||||
|
||||
|
||||
def _numeric_target_text(text: str) -> str:
|
||||
match = re.search(r"[-+]?\d+(?:\.\d+)?", text)
|
||||
if not match:
|
||||
return "1"
|
||||
value = float(match.group(0))
|
||||
if abs(value) < 1e-9:
|
||||
value = 1.0
|
||||
else:
|
||||
value *= 1.05
|
||||
return f"{value:.6g}"
|
||||
|
||||
|
||||
def _pick_editable_numeric_row(window: StepEditorWindow) -> int | None:
|
||||
for row, spec in window._actionable_property_rows():
|
||||
effective = window._effective_property_spec(spec, row=row)
|
||||
if str(effective.get("value_type", "number")) == "number":
|
||||
return int(row)
|
||||
rows = window._actionable_property_rows()
|
||||
return int(rows[0][0]) if rows else None
|
||||
|
||||
|
||||
def _click_card(window: StepEditorWindow, row: int) -> bool:
|
||||
card = window._property_card_widgets(row).get("card")
|
||||
if not isinstance(card, QWidget):
|
||||
return False
|
||||
QTest.mouseClick(card, Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, QPoint(18, max(3, card.height() // 2)))
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output-dir", default="assets/screenshots/property_panel_usability")
|
||||
parser.add_argument("--model", default=str(DEFAULT_MODEL_PATH))
|
||||
parser.add_argument("--face-id", type=int, default=None)
|
||||
parser.add_argument("--selection-mode", choices=("Face", "Feature"), default=None)
|
||||
parser.add_argument("--detection-level", choices=("current-only", "associated-only", "secondary"), default=None)
|
||||
parser.add_argument("--expand-diagnostics", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
_suppress_vtk_output_window()
|
||||
app = QApplication(["property-panel-usability-probe"])
|
||||
window = StepEditorWindow(Path(args.model), background_load=False)
|
||||
window.resize(1280, 820)
|
||||
window.show()
|
||||
_pump(app, 350)
|
||||
window.load_step(Path(args.model), background=False)
|
||||
_pump(app, 500)
|
||||
_native_grab(window, output_dir / "00_loaded.png")
|
||||
|
||||
if args.detection_level and hasattr(window, "feature_detection_combo"):
|
||||
index = window.feature_detection_combo.findData(args.detection_level)
|
||||
if index >= 0:
|
||||
window.feature_detection_combo.setCurrentIndex(index)
|
||||
_pump(app, 250)
|
||||
|
||||
target = None
|
||||
if args.face_id is not None:
|
||||
mode = args.selection_mode or "Feature"
|
||||
if mode == "Feature":
|
||||
window._set_selection_mode("Feature")
|
||||
window.select_feature(int(args.face_id))
|
||||
else:
|
||||
window._set_selection_mode("Face")
|
||||
window.select_face(int(args.face_id))
|
||||
_pump(app, 350)
|
||||
target = {
|
||||
"mode": mode,
|
||||
"face_id": int(args.face_id),
|
||||
"labels": _actionable_labels(window),
|
||||
}
|
||||
else:
|
||||
target = _find_actionable_target(window)
|
||||
if target is None or not target.get("labels"):
|
||||
summary = {
|
||||
"model": str(Path(args.model)),
|
||||
"error": "no actionable face or feature found",
|
||||
}
|
||||
(output_dir / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"property panel usability probe wrote {output_dir}")
|
||||
return 2
|
||||
|
||||
mode = str(target["mode"])
|
||||
face_id = int(target["face_id"])
|
||||
window._set_selection_mode(mode)
|
||||
if mode == "Feature":
|
||||
window.select_feature(face_id)
|
||||
else:
|
||||
window.select_face(face_id)
|
||||
_pump(app, 350)
|
||||
_native_grab(window, output_dir / "01_compact_actionable.png")
|
||||
|
||||
if args.expand_diagnostics and hasattr(window, "property_expand_button") and window.property_expand_button.isVisible():
|
||||
QTest.mouseClick(
|
||||
window.property_expand_button,
|
||||
Qt.MouseButton.LeftButton,
|
||||
Qt.KeyboardModifier.NoModifier,
|
||||
QPoint(max(3, window.property_expand_button.width() // 2), max(3, window.property_expand_button.height() // 2)),
|
||||
)
|
||||
_pump(app, 250)
|
||||
_native_grab(window, output_dir / "01b_expanded_diagnostics_before_edit.png")
|
||||
|
||||
target_row = _pick_editable_numeric_row(window)
|
||||
if target_row is not None:
|
||||
_click_card(window, target_row)
|
||||
_pump(app, 250)
|
||||
_native_grab(window, output_dir / "02_expanded_first_action.png")
|
||||
expanded_rows = _row_snapshot(window)
|
||||
|
||||
entered_target = ""
|
||||
button_text_after_input = ""
|
||||
selected_widgets = window._property_card_widgets(target_row) if target_row is not None else {}
|
||||
editor = selected_widgets.get("target_editor")
|
||||
if isinstance(editor, QLineEdit):
|
||||
entered_target = _numeric_target_text(editor.text())
|
||||
editor.setFocus()
|
||||
editor.selectAll()
|
||||
QTest.keyClicks(editor, entered_target)
|
||||
_pump(app, 250)
|
||||
button = selected_widgets.get("action_button")
|
||||
if isinstance(button, QPushButton):
|
||||
button_text_after_input = button.text()
|
||||
_native_grab(window, output_dir / "03_after_target_input.png")
|
||||
|
||||
collapsed_after_blank_click = False
|
||||
if target_row is not None:
|
||||
_click_card(window, target_row)
|
||||
_pump(app, 250)
|
||||
collapsed_after_blank_click = getattr(window, "property_editor_selected_row", None) is None
|
||||
_native_grab(window, output_dir / "04_collapsed_again.png")
|
||||
|
||||
if hasattr(window, "property_expand_button") and window.property_expand_button.isVisible():
|
||||
QTest.mouseClick(
|
||||
window.property_expand_button,
|
||||
Qt.MouseButton.LeftButton,
|
||||
Qt.KeyboardModifier.NoModifier,
|
||||
QPoint(max(3, window.property_expand_button.width() // 2), max(3, window.property_expand_button.height() // 2)),
|
||||
)
|
||||
_pump(app, 250)
|
||||
_native_grab(window, output_dir / "05_more_diagnostics.png")
|
||||
|
||||
compact_rows = _row_snapshot(window)
|
||||
overflow_labels = [
|
||||
label
|
||||
for row in compact_rows + expanded_rows
|
||||
for label in row.get("labels", [])
|
||||
if bool(label.get("overflow"))
|
||||
]
|
||||
summary = {
|
||||
"model": str(Path(args.model)),
|
||||
"target": {
|
||||
"mode": mode,
|
||||
"face_id": face_id,
|
||||
"actionable_labels": target.get("labels", []),
|
||||
},
|
||||
"summary_label": getattr(window, "property_command_summary_label", None).text()
|
||||
if hasattr(window, "property_command_summary_label")
|
||||
else "",
|
||||
"target_row": target_row,
|
||||
"entered_target": entered_target,
|
||||
"button_text_after_input": button_text_after_input,
|
||||
"collapsed_after_blank_click": collapsed_after_blank_click,
|
||||
"compact_rows": compact_rows,
|
||||
"expanded_rows": expanded_rows,
|
||||
"overflow_labels": overflow_labels,
|
||||
"selected_row_after_diagnostics": getattr(window, "property_editor_selected_row", None),
|
||||
}
|
||||
(output_dir / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
window.close()
|
||||
_pump(app, 100)
|
||||
app.quit()
|
||||
print(f"property panel usability probe wrote {output_dir}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user