295 lines
11 KiB
Python
295 lines
11 KiB
Python
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())
|