fix: ElasticSectionForm round-trips the SectionShape union

The form still passed flat b/h/d/tw/tf/diameter fields, which
ElasticSection (extra='forbid') rejects, so reading any elastic
section crashed. Rebuild the display-only hint as a PipeShape /
AngleShape / RectShape member instead; an incomplete shape degrades
to None rather than raising.
This commit is contained in:
smillmorel 2026-09-16 13:08:49 -04:00
commit ba3cad4eff

View file

@ -19,8 +19,11 @@ from PySide6.QtWidgets import (
) )
from otko.core import ( from otko.core import (
AngleShape,
ElasticSection, ElasticSection,
FiberSection, FiberSection,
PipeShape,
RectShape,
SectionAggregator, SectionAggregator,
) )
@ -94,14 +97,13 @@ class SectionFormBase(QWidget):
class ElasticSectionForm(SectionFormBase): class ElasticSectionForm(SectionFormBase):
type_label = "Elastic Section" type_label = "Elastic Section"
#: Shape → visible display-only dimensions. ``None`` hides them all. #: Shape kind → visible display-only dimensions (see the core
#: ``SectionShape`` union). ``None`` hides them all.
_SHAPE_DIMS: ClassVar[dict[str | None, tuple[str, ...]]] = { _SHAPE_DIMS: ClassVar[dict[str | None, tuple[str, ...]]] = {
None: (), None: (),
"rect": ("b", "h"), "rect": ("b", "d"),
"I": ("b", "d", "tw", "tf"), "angle": ("b", "d", "t"),
"H": ("b", "d", "tw", "tf"), "pipe": ("od", "t"),
"pipe": ("diameter",),
"circle": ("diameter",),
} }
def __init__(self, parent: QWidget | None = None) -> None: def __init__(self, parent: QWidget | None = None) -> None:
@ -127,14 +129,14 @@ class ElasticSectionForm(SectionFormBase):
self._layout.addRow(QLabel("<i>Extruded-view shape (display only).</i>")) self._layout.addRow(QLabel("<i>Extruded-view shape (display only).</i>"))
self._shape = QComboBox() self._shape = QComboBox()
self._shape.addItem("(none)", None) self._shape.addItem("(none)", None)
for key in ("rect", "I", "H", "pipe", "circle"): for key in ("rect", "angle", "pipe"):
self._shape.addItem(key, key) self._shape.addItem(key, key)
self._shape.currentIndexChanged.connect(self._update_dim_visibility) self._shape.currentIndexChanged.connect(self._update_dim_visibility)
self._layout.addRow("Shape:", self._shape) self._layout.addRow("Shape:", self._shape)
self._dim_spins: dict[str, QDoubleSpinBox] = {} self._dim_spins: dict[str, QDoubleSpinBox] = {}
self._dim_labels: dict[str, QLabel] = {} self._dim_labels: dict[str, QLabel] = {}
for key in ("b", "h", "d", "tw", "tf", "diameter"): for key in ("b", "d", "t", "od"):
spin = _opt_spin() spin = _opt_spin()
lbl = QLabel(f"{key}:") lbl = QLabel(f"{key}:")
self._dim_spins[key] = spin self._dim_spins[key] = spin
@ -160,20 +162,15 @@ class ElasticSectionForm(SectionFormBase):
self._g.setValue(s.G) self._g.setValue(s.G)
if s.J is not None: if s.J is not None:
self._j.setValue(s.J) self._j.setValue(s.J)
idx = self._shape.findData(s.shape) kind = getattr(s.shape, "kind", None) if s.shape is not None else None
idx = self._shape.findData(kind)
self._shape.setCurrentIndex(idx if idx >= 0 else 0) self._shape.setCurrentIndex(idx if idx >= 0 else 0)
for key, spin in self._dim_spins.items(): for key, spin in self._dim_spins.items():
v = getattr(s, key, None) v = getattr(s.shape, key, None) if s.shape is not None else None
spin.setValue(v if v is not None else _UNSET_SENTINEL) spin.setValue(v if v is not None else _UNSET_SENTINEL)
self._update_dim_visibility() self._update_dim_visibility()
def _read_specific(self, sid: int) -> ElasticSection: def _read_specific(self, sid: int) -> ElasticSection:
shape = self._shape.currentData()
visible = set(self._SHAPE_DIMS.get(shape, ()))
dims = {
key: (_opt_value(spin) if key in visible else None)
for key, spin in self._dim_spins.items()
}
return ElasticSection( return ElasticSection(
id=sid, id=sid,
name=self._name_edit.text(), name=self._name_edit.text(),
@ -183,10 +180,38 @@ class ElasticSectionForm(SectionFormBase):
Iy=self._iy.value(), Iy=self._iy.value(),
G=self._g.value(), G=self._g.value(),
J=self._j.value(), J=self._j.value(),
shape=shape, shape=self._read_shape(),
**dims, # type: ignore[arg-type]
) )
def _read_shape(self) -> PipeShape | AngleShape | RectShape | None:
"""Build the display-only shape hint from the form fields.
Dimensions are optional in the UI, so an incomplete shape degrades
to ``None`` instead of raising — the hint is never emitted to
OpenSees and cannot change an analysis result.
"""
def dim(key: str) -> float | None:
return _opt_value(self._dim_spins[key])
kind = self._shape.currentData()
if kind == "rect":
b, d = dim("b"), dim("d")
if b is None or d is None:
return None
return RectShape(b=b, d=d)
if kind == "angle":
b, d, t = dim("b"), dim("d"), dim("t")
if b is None or d is None or t is None:
return None
return AngleShape(b=b, d=d, t=t)
if kind == "pipe":
od, t = dim("od"), dim("t")
if od is None or t is None:
return None
return PipeShape(od=od, t=t)
return None
class FiberSectionSummaryForm(SectionFormBase): class FiberSectionSummaryForm(SectionFormBase):
"""Read-only overview of a :class:`FiberSection`. """Read-only overview of a :class:`FiberSection`.