Files

295 lines
11 KiB
Python
Raw Permalink Normal View History

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())