feat: show default node/element names in table
Some checks failed
CI / lint (push) Has been cancelled
CI / test (macos-latest, 3.10) (push) Has been cancelled
CI / test (macos-latest, 3.11) (push) Has been cancelled
CI / test (macos-latest, 3.12) (push) Has been cancelled
CI / test (ubuntu-latest, 3.10) (push) Has been cancelled
CI / test (ubuntu-latest, 3.11) (push) Has been cancelled
CI / test (ubuntu-latest, 3.12) (push) Has been cancelled
CI / test (windows-latest, 3.10) (push) Has been cancelled
CI / test (windows-latest, 3.11) (push) Has been cancelled
CI / test (windows-latest, 3.12) (push) Has been cancelled
Some checks failed
CI / lint (push) Has been cancelled
CI / test (macos-latest, 3.10) (push) Has been cancelled
CI / test (macos-latest, 3.11) (push) Has been cancelled
CI / test (macos-latest, 3.12) (push) Has been cancelled
CI / test (ubuntu-latest, 3.10) (push) Has been cancelled
CI / test (ubuntu-latest, 3.11) (push) Has been cancelled
CI / test (ubuntu-latest, 3.12) (push) Has been cancelled
CI / test (windows-latest, 3.10) (push) Has been cancelled
CI / test (windows-latest, 3.11) (push) Has been cancelled
CI / test (windows-latest, 3.12) (push) Has been cancelled
This commit is contained in:
parent
8f1aae4370
commit
13bb5d1d91
8 changed files with 172 additions and 21 deletions
|
|
@ -35,6 +35,7 @@ from otko.core.geometry import (
|
|||
DispBeamColumn,
|
||||
ElasticBeamColumn,
|
||||
Element,
|
||||
ElementBase,
|
||||
ForceBeamColumn,
|
||||
GridLine,
|
||||
GridSystem,
|
||||
|
|
@ -114,6 +115,7 @@ __all__ = [
|
|||
"ensure_default_truss_material",
|
||||
"ensure_default_timeseries_and_pattern",
|
||||
"Element",
|
||||
"ElementBase",
|
||||
"TrussElement",
|
||||
"CorotTrussElement",
|
||||
"ElasticBeamColumn",
|
||||
|
|
|
|||
|
|
@ -16,11 +16,31 @@ class Entity(BaseModel):
|
|||
"""Base class for every persisted domain object."""
|
||||
|
||||
model_config = ConfigDict(
|
||||
frozen=False, # individual setters allowed; we lock at the Project boundary
|
||||
extra="forbid", # unknown JSON keys are an error, not a silent ignore
|
||||
frozen=False, # individual setters allowed; we lock at the Project boundary
|
||||
extra="forbid", # unknown JSON keys are an error, not a silent ignore
|
||||
validate_assignment=True,
|
||||
populate_by_name=True,
|
||||
)
|
||||
|
||||
id: PositiveInt = Field(..., description="Unique tag within its kind. Used as the OpenSees tag.")
|
||||
id: PositiveInt = Field(
|
||||
..., description="Unique tag within its kind. Used as the OpenSees tag."
|
||||
)
|
||||
name: str = Field(default="", description="Optional human-readable label.")
|
||||
|
||||
@property
|
||||
def default_name(self) -> str:
|
||||
"""Fallback label used when ``name`` is blank.
|
||||
|
||||
Display-only — reading it never writes back into ``name`` and it
|
||||
is never persisted. Subclasses override this with their prefix
|
||||
(e.g. ``N<id>`` for nodes, ``E<id>`` for elements).
|
||||
"""
|
||||
return ""
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
"""Explicit ``name`` stripped, or ``default_name`` when blank.
|
||||
|
||||
Display-only fallback — reading this never mutates ``name``.
|
||||
"""
|
||||
return self.name.strip() or self.default_name
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from otko.core.geometry.elements import (
|
|||
DispBeamColumn,
|
||||
ElasticBeamColumn,
|
||||
Element,
|
||||
ElementBase,
|
||||
ForceBeamColumn,
|
||||
QuadElement,
|
||||
TrussElement,
|
||||
|
|
@ -25,6 +26,7 @@ from otko.core.geometry.node import Node
|
|||
__all__ = [
|
||||
"Node",
|
||||
"Element",
|
||||
"ElementBase",
|
||||
"TrussElement",
|
||||
"CorotTrussElement",
|
||||
"ElasticBeamColumn",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,21 @@ from pydantic import Field, PositiveFloat, PositiveInt
|
|||
from otko.core._base import Entity
|
||||
|
||||
|
||||
class TrussElement(Entity):
|
||||
class ElementBase(Entity):
|
||||
"""Shared base for all structural elements.
|
||||
|
||||
Carries the display-only ``E<id>`` fallback label so every element
|
||||
type (including Quad) shows the same default name on the canvas
|
||||
and in the table without persisting it into ``name``.
|
||||
"""
|
||||
|
||||
@property
|
||||
def default_name(self) -> str:
|
||||
"""Fallback label for unnamed elements (``E<id>``)."""
|
||||
return f"E{self.id}"
|
||||
|
||||
|
||||
class TrussElement(ElementBase):
|
||||
"""Two-node truss with axial stiffness only — ``element truss``."""
|
||||
|
||||
type: Literal["Truss"] = "Truss"
|
||||
|
|
@ -27,7 +41,7 @@ class TrussElement(Entity):
|
|||
rho: float = Field(default=0.0, ge=0.0, description="Mass per unit length.")
|
||||
|
||||
|
||||
class CorotTrussElement(Entity):
|
||||
class CorotTrussElement(ElementBase):
|
||||
"""Co-rotational truss for large displacements — ``element corotTruss``."""
|
||||
|
||||
type: Literal["CorotTruss"] = "CorotTruss"
|
||||
|
|
@ -37,7 +51,7 @@ class CorotTrussElement(Entity):
|
|||
rho: float = Field(default=0.0, ge=0.0)
|
||||
|
||||
|
||||
class ElasticBeamColumn(Entity):
|
||||
class ElasticBeamColumn(ElementBase):
|
||||
"""Linear-elastic frame element — ``element elasticBeamColumn``.
|
||||
|
||||
Pure-section style: provide section_id; OpenSees pulls EA, EI from
|
||||
|
|
@ -60,7 +74,7 @@ class ElasticBeamColumn(Entity):
|
|||
)
|
||||
|
||||
|
||||
class ForceBeamColumn(Entity):
|
||||
class ForceBeamColumn(ElementBase):
|
||||
"""Force-based fibre frame element — ``element forceBeamColumn``."""
|
||||
|
||||
type: Literal["ForceBeamColumn"] = "ForceBeamColumn"
|
||||
|
|
@ -72,7 +86,7 @@ class ForceBeamColumn(Entity):
|
|||
tolerance: float = Field(default=1e-12, gt=0.0)
|
||||
|
||||
|
||||
class DispBeamColumn(Entity):
|
||||
class DispBeamColumn(ElementBase):
|
||||
"""Displacement-based fibre frame element — ``element dispBeamColumn``."""
|
||||
|
||||
type: Literal["DispBeamColumn"] = "DispBeamColumn"
|
||||
|
|
@ -82,7 +96,7 @@ class DispBeamColumn(Entity):
|
|||
geom_transf: Literal["Linear", "PDelta", "Corotational"] = "Linear"
|
||||
|
||||
|
||||
class ZeroLengthElement(Entity):
|
||||
class ZeroLengthElement(ElementBase):
|
||||
"""Two coincident nodes connected by uniaxial materials per DOF.
|
||||
|
||||
Foundation building block for plastic hinges and isolators.
|
||||
|
|
@ -91,7 +105,9 @@ class ZeroLengthElement(Entity):
|
|||
type: Literal["ZeroLength"] = "ZeroLength"
|
||||
nodes: tuple[PositiveInt, PositiveInt]
|
||||
material_ids: tuple[PositiveInt, ...] = Field(..., min_length=1)
|
||||
dofs: tuple[int, ...] = Field(..., min_length=1, description="DOF directions, 1-indexed (1..6).")
|
||||
dofs: tuple[int, ...] = Field(
|
||||
..., min_length=1, description="DOF directions, 1-indexed (1..6)."
|
||||
)
|
||||
do_rayleigh: bool = Field(
|
||||
default=False,
|
||||
description=(
|
||||
|
|
@ -104,7 +120,7 @@ class ZeroLengthElement(Entity):
|
|||
)
|
||||
|
||||
|
||||
class ZeroLengthSectionElement(Entity):
|
||||
class ZeroLengthSectionElement(ElementBase):
|
||||
"""Two coincident nodes connected by a full :class:`Section` —
|
||||
OpenSees ``element zeroLengthSection``.
|
||||
|
||||
|
|
@ -121,10 +137,12 @@ class ZeroLengthSectionElement(Entity):
|
|||
|
||||
type: Literal["ZeroLengthSection"] = "ZeroLengthSection"
|
||||
nodes: tuple[PositiveInt, PositiveInt]
|
||||
section_id: PositiveInt = Field(..., description="Section attached to the two coincident nodes.")
|
||||
section_id: PositiveInt = Field(
|
||||
..., description="Section attached to the two coincident nodes."
|
||||
)
|
||||
|
||||
|
||||
class BeamWithHingesElement(Entity):
|
||||
class BeamWithHingesElement(ElementBase):
|
||||
"""Force-based beam with lumped plasticity at both ends —
|
||||
``element beamWithHinges``.
|
||||
|
||||
|
|
@ -156,7 +174,7 @@ class BeamWithHingesElement(Entity):
|
|||
geom_transf: Literal["Linear", "PDelta", "Corotational"] = "Linear"
|
||||
|
||||
|
||||
class QuadElement(Entity):
|
||||
class QuadElement(ElementBase):
|
||||
"""Four-node 2D quadrilateral (plane-stress / plane-strain) —
|
||||
``element quad`` / ``bbarQuad`` / ``enhancedQuad``.
|
||||
|
||||
|
|
@ -179,7 +197,8 @@ class QuadElement(Entity):
|
|||
description="Surface pressure applied over the element (force / area).",
|
||||
)
|
||||
rho: float = Field(
|
||||
default=0.0, ge=0.0,
|
||||
default=0.0,
|
||||
ge=0.0,
|
||||
description="Mass density override (kip·s²/in⁴). Leave 0 to use material rho.",
|
||||
)
|
||||
b1: float = Field(
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ class Node(Entity):
|
|||
mass: Mass6 = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
restraint: Restraint6 = (False, False, False, False, False, False)
|
||||
|
||||
@property
|
||||
def default_name(self) -> str:
|
||||
"""Fallback label for unnamed nodes (``N<id>``)."""
|
||||
return f"N{self.id}"
|
||||
|
||||
@property
|
||||
def is_restrained(self) -> bool:
|
||||
"""True if any DOF is fixed."""
|
||||
|
|
|
|||
|
|
@ -1140,9 +1140,7 @@ class ModelRenderer:
|
|||
if self._node_pd is None or len(project.nodes) == 0:
|
||||
return None
|
||||
pts = np.asarray(self._node_pd.points)
|
||||
labels = [
|
||||
(node.name.strip() if node.name.strip() else f"N{node.id}") for node in project.nodes
|
||||
]
|
||||
labels = [node.display_name for node in project.nodes]
|
||||
return self._plotter.add_point_labels(
|
||||
pts,
|
||||
labels,
|
||||
|
|
@ -1170,7 +1168,7 @@ class ModelRenderer:
|
|||
if i is None or j is None:
|
||||
continue
|
||||
centers.append((pts[i] + pts[j]) / 2.0)
|
||||
labels.append(el.name.strip() if el.name.strip() else f"E{el.id}")
|
||||
labels.append(el.display_name)
|
||||
if not centers:
|
||||
return None
|
||||
return self._plotter.add_point_labels(
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ class TableDock(QWidget):
|
|||
table.setRowCount(len(nodes))
|
||||
for r, n in enumerate(nodes):
|
||||
self._put(table, r, 0, str(n.id), editable=False, data=n.id)
|
||||
self._put(table, r, 1, n.name, data=n.id)
|
||||
self._put(table, r, 1, n.display_name, data=n.id)
|
||||
for c, v in enumerate(n.coords, start=2):
|
||||
self._put(table, r, c, f"{v:g}", data=n.id)
|
||||
for c, v in enumerate(n.mass, start=5):
|
||||
|
|
@ -506,6 +506,12 @@ class TableDock(QWidget):
|
|||
nid = self._row_key(table, row)
|
||||
node = project.node(nid)
|
||||
if col == 1:
|
||||
if not node.name.strip() and item.text().strip() == node.display_name:
|
||||
# Confirmed the displayed fallback unchanged — not a real
|
||||
# rename, so leave the stored name blank instead of
|
||||
# persisting "N<id>" (and keep it off the undo stack).
|
||||
self.refresh()
|
||||
return
|
||||
updated = self._dry_run(node, {"name": item.text()})
|
||||
self._dispatch(UpdateNodeFieldsCommand(vm, nid, {"name": updated.name}))
|
||||
elif 2 <= col <= 4:
|
||||
|
|
@ -537,7 +543,7 @@ class TableDock(QWidget):
|
|||
table.setRowCount(len(elements))
|
||||
for r, el in enumerate(elements):
|
||||
self._put(table, r, 0, str(el.id), editable=False, data=el.id)
|
||||
self._put(table, r, 1, el.name, data=el.id)
|
||||
self._put(table, r, 1, el.display_name, data=el.id)
|
||||
self._put(table, r, 2, el.type, editable=False, data=el.id)
|
||||
self._put(table, r, 3, ", ".join(str(n) for n in el.nodes), data=el.id)
|
||||
self._put(
|
||||
|
|
@ -583,6 +589,12 @@ class TableDock(QWidget):
|
|||
eid = self._row_key(table, row)
|
||||
el = project.element(eid)
|
||||
if col == 1:
|
||||
if not el.name.strip() and item.text().strip() == el.display_name:
|
||||
# Confirmed the displayed fallback unchanged — not a real
|
||||
# rename, so leave the stored name blank instead of
|
||||
# persisting "E<id>" (and keep it off the undo stack).
|
||||
self.refresh()
|
||||
return
|
||||
updated = self._dry_run(el, {"name": item.text()})
|
||||
self._dispatch(UpdateElementFieldsCommand(vm, eid, {"name": updated.name}))
|
||||
elif col == 3:
|
||||
|
|
|
|||
93
tests/unit/test_display_name.py
Normal file
93
tests/unit/test_display_name.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
"""Unit tests for the display-only ``display_name`` fallback on entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from otko.core import (
|
||||
BeamWithHingesElement,
|
||||
CorotTrussElement,
|
||||
DispBeamColumn,
|
||||
ElasticBeamColumn,
|
||||
ElementBase,
|
||||
ForceBeamColumn,
|
||||
Node,
|
||||
QuadElement,
|
||||
TrussElement,
|
||||
ZeroLengthElement,
|
||||
ZeroLengthSectionElement,
|
||||
)
|
||||
from otko.core._base import Entity
|
||||
|
||||
_ALL_ELEMENT_TYPES = (
|
||||
TrussElement,
|
||||
CorotTrussElement,
|
||||
ElasticBeamColumn,
|
||||
ForceBeamColumn,
|
||||
DispBeamColumn,
|
||||
ZeroLengthElement,
|
||||
ZeroLengthSectionElement,
|
||||
BeamWithHingesElement,
|
||||
QuadElement,
|
||||
)
|
||||
|
||||
|
||||
class TestEntityBase:
|
||||
def test_base_fallback_is_empty(self) -> None:
|
||||
e = Entity(id=1)
|
||||
assert e.default_name == ""
|
||||
assert e.display_name == ""
|
||||
|
||||
def test_display_name_is_not_persisted(self) -> None:
|
||||
assert "display_name" not in Entity(id=1).model_dump()
|
||||
assert "default_name" not in Entity(id=1).model_dump()
|
||||
assert "display_name" not in Node(id=1, coords=(0, 0, 0)).model_dump()
|
||||
|
||||
|
||||
class TestNodeDisplayName:
|
||||
def test_blank_name_falls_back(self) -> None:
|
||||
n = Node(id=1, coords=(0, 0, 0))
|
||||
assert n.display_name == "N1"
|
||||
|
||||
def test_explicit_name_is_returned(self) -> None:
|
||||
n = Node(id=1, name="Support A", coords=(0, 0, 0))
|
||||
assert n.display_name == "Support A"
|
||||
|
||||
def test_whitespace_only_name_falls_back(self) -> None:
|
||||
n = Node(id=2, name=" ", coords=(0, 0, 0))
|
||||
assert n.display_name == "N2"
|
||||
|
||||
def test_explicit_name_is_stripped(self) -> None:
|
||||
n = Node(id=1, name=" A1 ", coords=(0, 0, 0))
|
||||
assert n.display_name == "A1"
|
||||
|
||||
def test_display_only_never_writes_back(self) -> None:
|
||||
n = Node(id=1, coords=(0, 0, 0))
|
||||
assert n.display_name == "N1"
|
||||
assert n.name == ""
|
||||
|
||||
|
||||
class TestElementDisplayName:
|
||||
def test_all_element_types_share_element_base(self) -> None:
|
||||
for cls in _ALL_ELEMENT_TYPES:
|
||||
assert issubclass(cls, ElementBase)
|
||||
|
||||
def test_blank_truss_name_falls_back(self) -> None:
|
||||
t = TrussElement(id=1, nodes=(1, 2), area=0.01, material_id=1)
|
||||
assert t.display_name == "E1"
|
||||
|
||||
def test_explicit_truss_name_is_returned(self) -> None:
|
||||
t = TrussElement(id=1, name="Chord", nodes=(1, 2), area=0.01, material_id=1)
|
||||
assert t.display_name == "Chord"
|
||||
|
||||
def test_whitespace_only_truss_name_falls_back(self) -> None:
|
||||
t = TrussElement(id=3, name=" ", nodes=(1, 2), area=0.01, material_id=1)
|
||||
assert t.display_name == "E3"
|
||||
|
||||
def test_blank_quad_name_falls_back(self) -> None:
|
||||
q = QuadElement(id=1, nodes=(1, 2, 3, 4), thickness=0.1, material_id=1)
|
||||
assert q.display_name == "E1"
|
||||
|
||||
def test_display_only_never_writes_back(self) -> None:
|
||||
t = TrussElement(id=1, nodes=(1, 2), area=0.01, material_id=1)
|
||||
assert t.display_name == "E1"
|
||||
assert t.name == ""
|
||||
assert "display_name" not in t.model_dump()
|
||||
Loading…
Reference in a new issue