"""Qt tree-model helpers for the Buffalo Panel GUI."""
# pyright: reportUnknownMemberType=false
# pyright: reportUnknownVariableType=false
from __future__ import annotations
from typing import Any
import numpy as np
from PyQt6 import QtCore, QtGui
from buffalo_panel.app.gui.internal.config_templates import (
build_airfoil_parameter_mapping,
)
[docs]
def model_to_data(
parent: QtGui.QStandardItem | None,
metadata_role: int,
) -> dict[str, Any] | list[Any]:
"""Serialize a tree-model branch back into nested Python data."""
if parent is None:
return {}
is_list = _item_represents_list(parent, metadata_role)
container: Any = [] if is_list else {}
for row in range(parent.rowCount()):
key_item = parent.child(row, 0)
val_item = parent.child(row, 1)
if key_item is None:
continue
key = key_item.data(QtCore.Qt.ItemDataRole.UserRole)
if key_item.hasChildren():
value = model_to_data(key_item, metadata_role)
elif val_item is not None:
value = _coerce_scalar_value(key, val_item.text())
else:
value = {}
if is_list:
container.append(value)
else:
container[key] = value
return container
[docs]
def convert_angle_fields(
root_item: QtGui.QStandardItem | None,
*,
to_rad: bool,
) -> None:
"""Convert freestream and placement angle fields in-place."""
if root_item is None:
return
factor = np.deg2rad(1.0) if to_rad else np.rad2deg(1.0)
def traverse(parent_item: QtGui.QStandardItem) -> None:
for row in range(parent_item.rowCount()):
key_item = parent_item.child(row, 0)
val_item = parent_item.child(row, 1)
if key_item is None:
continue
key = key_item.data(QtCore.Qt.ItemDataRole.UserRole)
parent = key_item.parent()
parent_key = (
parent.data(QtCore.Qt.ItemDataRole.UserRole)
if parent is not None
else None
)
if val_item is not None:
is_angle = (key == "alpha" and parent_key == "freestream") or (
key == "rotation" and parent_key == "placement"
)
if is_angle:
try:
value = float(val_item.text())
except ValueError:
pass
else:
val_item.setText(str(value * factor))
if key_item.hasChildren():
traverse(key_item)
traverse(root_item)
[docs]
def sync_airfoil_definition_rows(
airfoil_item: QtGui.QStandardItem,
airfoil_type: str,
) -> None:
"""Replace airfoil parameter rows to match the selected airfoil type."""
rows_to_remove: list[int] = []
for row in range(airfoil_item.rowCount()):
child_key_item = airfoil_item.child(row, 0)
if child_key_item is None:
continue
if child_key_item.data(QtCore.Qt.ItemDataRole.UserRole) != "type":
rows_to_remove.append(row)
for row in reversed(rows_to_remove):
airfoil_item.removeRow(row)
for key, value in build_airfoil_parameter_mapping(airfoil_type).items():
key_item = QtGui.QStandardItem(key)
key_item.setData(key, QtCore.Qt.ItemDataRole.UserRole)
key_item.setEditable(False)
value_item = QtGui.QStandardItem(str(value))
airfoil_item.appendRow([key_item, value_item])
def _item_represents_list(
item: QtGui.QStandardItem,
metadata_role: int,
) -> bool:
meta = item.data(metadata_role)
meta_mapping = meta if isinstance(meta, dict) else None
if meta_mapping is not None and meta_mapping.get("value_kind") == "list":
return True
if item.rowCount() == 0:
return False
first_key_item = item.child(0, 0)
if first_key_item is None:
return False
first_key = str(first_key_item.data(QtCore.Qt.ItemDataRole.UserRole))
return first_key.startswith("[") and first_key.endswith("]")
def _coerce_scalar_value(key: Any, value_text: str) -> Any:
"""Convert GUI text back into a scalar config value."""
if key == "designation":
return value_text
normalized = value_text.lower()
if normalized == "true":
return True
if normalized == "false":
return False
if value_text == "" or normalized == "none":
return None
try:
return float(value_text) if "." in value_text else int(value_text)
except ValueError:
return value_text