Skip to content

Commit 494263d

Browse files
authored
Merge branch 'main' into maint/seed-part-index-positionally
2 parents 5eb4237 + 0bdbf3f commit 494263d

25 files changed

Lines changed: 2611 additions & 100 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update email address to from ansys to synopsys

‎doc/changelog.d/53.added.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Remote rendering 3.1d - route client per-part mutations through the server triggers

‎doc/changelog.d/54.added.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Remote rendering 3.1e - source per-part state from registry on save and restore it on load

‎doc/styles/Vocab/ANSYS/accept.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ VM
4343
Autoscaling
4444
hps
4545
Synopsys
46+
synopsys
4647
repId
4748
ue
4849
te

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ build-system-version = "1.7.0"
1111
name = "ansys-visor-viewer"
1212
version = "1.0.0_beta"
1313
description = "\"VISOR 3D visualization web component framework\""
14-
authors = ["VISOR Team <visor@ansys.com>"]
14+
authors = ["VISOR Team <visor@synopsys.com>"]
1515
readme = "README.rst"
1616
license = "Ansys License"
1717
repository = "https://github.com/ansys/visor"

‎src/ansys/visor/viewer/models/common/visor_variable_state.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
from typing import List, Tuple
44

5-
from pydantic import BaseModel, ConfigDict, Field
5+
from pydantic import BaseModel, ConfigDict, Field, field_serializer
6+
7+
from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType
68

79

810
class VisorVariableState(BaseModel):
@@ -15,11 +17,23 @@ class VisorVariableState(BaseModel):
1517
a unique identifier for the variable: the variable name, type (POINT/CELL), and number of components, and computes
1618
the ranges across all datasets/parts that contain that variable.
1719
20+
``array_name``, ``type`` and ``num_components`` are those same three identifying properties, carried
21+
explicitly (rather than only opaquely inside ``id``) so a consumer can act on them without parsing the
22+
client-built identifier.
23+
1824
In the future, the backend can own this, but for now we can treat this as passthrough data,
1925
as the id value is stable across sessions.
2026
"""
2127
model_config = ConfigDict(populate_by_name=True)
2228

2329
id: str = Field(...)
30+
array_name: str = Field(..., alias="arrayName")
31+
type: VisorVtkVariableType = Field(...)
32+
num_components: int = Field(..., alias="numComponents")
2433
magnitude_range: Tuple[float, float] | None = Field(default=None, alias="magnitudeRange")
2534
ranges: List[Tuple[float, float]] = Field(default_factory=list)
35+
36+
@field_serializer("type")
37+
def _serialize_type(self, value: VisorVtkVariableType) -> str:
38+
"""Emit the wire value (e.g. "POINT"/"CELL") for both dict-mode and JSON-mode dumps."""
39+
return value.value

‎src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py‎

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,61 @@
11
"""Model for persisted scene state (cross-session)."""
22

3-
from typing import Dict
3+
from typing import Any, Dict, Mapping
44

5-
from pydantic import BaseModel, ConfigDict, Field
5+
from pydantic import BaseModel, ConfigDict, Field, field_validator
66

77
from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState
88
from ansys.visor.viewer.models.common.visor_cross_section_state import VisorCrossSectionState
99
from ansys.visor.viewer.models.common.visor_variable_state import VisorVariableState
1010
from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState
1111

12+
#: Separator used by the client when it mints a variable identifier. See
13+
#: ``VisorSpectrumManager.tryAddSpectrumInfo``, which builds the identifier as
14+
#: ``` `${type}::${name}::${numComponents}` ```.
15+
_IDENTIFIER_SEPARATOR = "::"
16+
17+
def derive_variable_fields_from_identifier(identifier: Any) -> Dict[str, Any] | None:
18+
"""Recover ``type``, ``array_name`` and ``num_components`` from a variable identifier.
19+
20+
This is the inverse of the client's creation of the variable ID, and the only place the
21+
identifier is parsed. The array name is in the middle field and is user data, so it may
22+
contain the separator: the split is anchored at both ends rather than left to right, and
23+
``"POINT::stress::yy::3"`` yields the array name ``"stress::yy"``.
24+
25+
Returns ``None`` when the identifier does not spell out three usable fields, meaning
26+
"cannot be derived" rather than "derived to a default". Callers leave such an
27+
entry untouched so ordinary validation reports the missing fields.
28+
"""
29+
if not isinstance(identifier, str):
30+
return None
31+
32+
# Strip out the last field, which is the number of components.
33+
# e.g. "POINT::stress::yy::3".rpartition("::") -> ("POINT::stress::yy", "::", "3")
34+
head, separator, num_components_str = identifier.rpartition(_IDENTIFIER_SEPARATOR)
35+
if not separator:
36+
return None
37+
38+
try:
39+
num_components = int(num_components_str)
40+
except (TypeError, ValueError):
41+
return None
42+
43+
# Strip out the first field, which is the variable type. The remainder is the array name,
44+
# which may contain the separator.
45+
# e.g. "POINT::stress::yy".partition("::") -> ("POINT", "::", "stress::yy")
46+
variable_type, separator, array_name = head.partition(_IDENTIFIER_SEPARATOR)
47+
if not separator:
48+
return None
49+
50+
if not variable_type or not array_name:
51+
return None
52+
53+
return {
54+
"type": variable_type,
55+
"array_name": array_name,
56+
"num_components": num_components,
57+
}
58+
1259

1360
class PersistedSceneState(BaseModel):
1461
"""Persisted (cross-session) scene state.
@@ -32,3 +79,56 @@ class PersistedSceneState(BaseModel):
3279
dataset_states: Dict[str, "PersistedDatasetState"] = Field(default_factory=dict)
3380
variable_states: Dict[str, "VisorVariableState"] = Field(default_factory=dict)
3481
model_config = ConfigDict(arbitrary_types_allowed=True)
82+
83+
@field_validator("variable_states", mode="before")
84+
@classmethod
85+
def _derive_missing_variable_identity_fields(cls, value: Any) -> Any:
86+
"""Fill absent identity fields on read, from the variable identifier.
87+
88+
``array_name``, ``type``, and ``num_components`` are required and stay
89+
required: the model is shared with ``RuntimeSceneState.spectrum_states``,
90+
so relaxing them would also relax the save-path coercion in
91+
``VisorSaveStateResponse._coerce_app_state``. Tolerance for older save
92+
files lives here, on the container, and applies to the ingest boundary only.
93+
"""
94+
if not isinstance(value, Mapping):
95+
return value
96+
97+
out: Dict[Any, Any] = {}
98+
for key, entry in value.items():
99+
out[key] = cls._derive_missing_identity_fields_for_entry(key, entry)
100+
return out
101+
102+
@staticmethod
103+
def _derive_missing_identity_fields_for_entry(key: Any, entry: Any) -> Any:
104+
"""Return *entry* with any absent identity fields derived from its identifier."""
105+
if isinstance(entry, VisorVariableState) or not isinstance(entry, Mapping):
106+
return entry
107+
108+
# A value that is present but null counts as present and is left
109+
# to fail validation.
110+
missing = [
111+
field
112+
for field, alias in (
113+
("array_name", "arrayName"),
114+
("type", "type"),
115+
("num_components", "numComponents"),
116+
)
117+
if field not in entry and alias not in entry
118+
]
119+
if not missing:
120+
return entry
121+
122+
identifier = entry.get("id")
123+
if not isinstance(identifier, str) or not identifier:
124+
identifier = key
125+
126+
derived = derive_variable_fields_from_identifier(identifier)
127+
if derived is None:
128+
return entry
129+
130+
filled = dict(entry)
131+
for field in missing:
132+
filled[field] = derived[field]
133+
return filled
134+

0 commit comments

Comments
 (0)