Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
38c5e9e
add per-part write path to the dataset registry
LKasianAnsys Aug 28, 2026
ac71d50
remove story/increment reference in comment
LKasianAnsys Aug 28, 2026
ee0b843
chore: adding changelog file 50.added.md [dependabot-skip]
pyansys-ci-bot Aug 28, 2026
ff688aa
chore: adding changelog file 50.added.md [dependabot-skip]
pyansys-ci-bot Aug 28, 2026
ccc2f0f
implement visibility, opacity and diffuse-color applies on the local …
LKasianAnsys Aug 28, 2026
c62294f
move selection and color-variable applies onto VtkNodePipeline
LKasianAnsys Aug 28, 2026
361c20e
chore: adding changelog file 51.added.md [dependabot-skip]
pyansys-ci-bot Aug 28, 2026
0c50362
move remaining three vtk calls to node pipeline
LKasianAnsys Aug 31, 2026
174ee07
register per-part trigger handlers and serialize VTK access with a lock
LKasianAnsys Aug 28, 2026
34b27f0
replace hand-written payloads in LocalApp with Pydantic models and de…
LKasianAnsys Aug 28, 2026
77f9a9d
comment out wasm flush
LKasianAnsys Aug 28, 2026
44233a4
chore: adding changelog file 52.added.md [dependabot-skip]
pyansys-ci-bot Aug 28, 2026
85b6630
remove TODOs and update comment on flush_wasm_state
LKasianAnsys Aug 31, 2026
42efe36
route client per-part mutations through the server triggers
LKasianAnsys Aug 28, 2026
a5a38ce
chore: adding changelog file 53.added.md [dependabot-skip]
pyansys-ci-bot Aug 28, 2026
f16d5de
prettier fixes
LKasianAnsys Aug 28, 2026
5d5530c
update regression test reference to match updated behaviour
LKasianAnsys Aug 31, 2026
47fc0a0
Revert "update regression test reference to match updated behaviour"
LKasianAnsys Aug 31, 2026
707ffb1
reorder regression tests to get clean initial baseline
LKasianAnsys Aug 31, 2026
42e6a3d
reorder regression test color by order to end up with magnitude
LKasianAnsys Aug 31, 2026
786211f
Merge branch 'main' into feat/3.1a-registry-per-part-write-path
LKasianAnsys Aug 31, 2026
3cc3e94
Merge branch 'feat/3.1a-registry-per-part-write-path' into feat/3.1b-…
LKasianAnsys Aug 31, 2026
0477060
Merge branch 'feat/3.1b-per-part-apply-logic' into feat/3.1c-triggers…
LKasianAnsys Aug 31, 2026
ef4700b
Merge branch 'feat/3.1c-triggers-lock-and-payload-validation' into fe…
LKasianAnsys Aug 31, 2026
8cfb507
add arrayName, type, numComponents to variable state
LKasianAnsys Aug 28, 2026
49949da
source per-part state from the registry on save and restore it on load
LKasianAnsys Aug 28, 2026
92b19b5
chore: adding changelog file 54.added.md [dependabot-skip]
pyansys-ci-bot Aug 31, 2026
a4b5ade
bump saved state schema version to 1.1 in regression test
LKasianAnsys Aug 31, 2026
22ee534
Merge branch 'feat/3.1d-client-routes-mutations-to-triggers' into fea…
LKasianAnsys Aug 31, 2026
c06f708
add field validator to derive new variable fields from id, version ba…
LKasianAnsys Sep 1, 2026
bd3b79c
clean up docstrings
LKasianAnsys Sep 2, 2026
34d04bc
clean up docstrings
LKasianAnsys Sep 2, 2026
07bb4a4
Merge branch 'main' into feat/3.1e-registry-sourced-save-and-restore
LKasianAnsys Sep 3, 2026
45831a0
pre-commit fixes
LKasianAnsys Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/54.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remote rendering 3.1e - source per-part state from registry on save and restore it on load
16 changes: 15 additions & 1 deletion src/ansys/visor/viewer/models/common/visor_variable_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from typing import List, Tuple

from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, field_serializer

from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType


class VisorVariableState(BaseModel):
Expand All @@ -15,11 +17,23 @@ class VisorVariableState(BaseModel):
a unique identifier for the variable: the variable name, type (POINT/CELL), and number of components, and computes
the ranges across all datasets/parts that contain that variable.

``array_name``, ``type`` and ``num_components`` are those same three identifying properties, carried
explicitly (rather than only opaquely inside ``id``) so a consumer can act on them without parsing the
client-built identifier.

In the future, the backend can own this, but for now we can treat this as passthrough data,
as the id value is stable across sessions.
"""
model_config = ConfigDict(populate_by_name=True)

id: str = Field(...)
array_name: str = Field(..., alias="arrayName")
type: VisorVtkVariableType = Field(...)
num_components: int = Field(..., alias="numComponents")
magnitude_range: Tuple[float, float] | None = Field(default=None, alias="magnitudeRange")
ranges: List[Tuple[float, float]] = Field(default_factory=list)

@field_serializer("type")
def _serialize_type(self, value: VisorVtkVariableType) -> str:
"""Emit the wire value (e.g. "POINT"/"CELL") for both dict-mode and JSON-mode dumps."""
return value.value
104 changes: 102 additions & 2 deletions src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,61 @@
"""Model for persisted scene state (cross-session)."""

from typing import Dict
from typing import Any, Dict, Mapping

from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, field_validator

from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState
from ansys.visor.viewer.models.common.visor_cross_section_state import VisorCrossSectionState
from ansys.visor.viewer.models.common.visor_variable_state import VisorVariableState
from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState

#: Separator used by the client when it mints a variable identifier. See
#: ``VisorSpectrumManager.tryAddSpectrumInfo``, which builds the identifier as
#: ``` `${type}::${name}::${numComponents}` ```.
_IDENTIFIER_SEPARATOR = "::"

def derive_variable_fields_from_identifier(identifier: Any) -> Dict[str, Any] | None:
"""Recover ``type``, ``array_name`` and ``num_components`` from a variable identifier.

This is the inverse of the client's creation of the variable ID, and the only place the
identifier is parsed. The array name is in the middle field and is user data, so it may
contain the separator: the split is anchored at both ends rather than left to right, and
``"POINT::stress::yy::3"`` yields the array name ``"stress::yy"``.

Returns ``None`` when the identifier does not spell out three usable fields, meaning
"cannot be derived" rather than "derived to a default". Callers leave such an
entry untouched so ordinary validation reports the missing fields.
"""
if not isinstance(identifier, str):
return None

# Strip out the last field, which is the number of components.
# e.g. "POINT::stress::yy::3".rpartition("::") -> ("POINT::stress::yy", "::", "3")
head, separator, num_components_str = identifier.rpartition(_IDENTIFIER_SEPARATOR)
if not separator:
return None

try:
num_components = int(num_components_str)
except (TypeError, ValueError):
return None

# Strip out the first field, which is the variable type. The remainder is the array name,
# which may contain the separator.
# e.g. "POINT::stress::yy".partition("::") -> ("POINT", "::", "stress::yy")
variable_type, separator, array_name = head.partition(_IDENTIFIER_SEPARATOR)
if not separator:
return None

if not variable_type or not array_name:
return None

return {
"type": variable_type,
"array_name": array_name,
"num_components": num_components,
}


class PersistedSceneState(BaseModel):
"""Persisted (cross-session) scene state.
Expand All @@ -32,3 +79,56 @@ class PersistedSceneState(BaseModel):
dataset_states: Dict[str, "PersistedDatasetState"] = Field(default_factory=dict)
variable_states: Dict[str, "VisorVariableState"] = Field(default_factory=dict)
model_config = ConfigDict(arbitrary_types_allowed=True)

@field_validator("variable_states", mode="before")
@classmethod
def _derive_missing_variable_identity_fields(cls, value: Any) -> Any:
"""Fill absent identity fields on read, from the variable identifier.

``array_name``, ``type``, and ``num_components`` are required and stay
required: the model is shared with ``RuntimeSceneState.spectrum_states``,
so relaxing them would also relax the save-path coercion in
``VisorSaveStateResponse._coerce_app_state``. Tolerance for older save
files lives here, on the container, and applies to the ingest boundary only.
"""
if not isinstance(value, Mapping):
return value

out: Dict[Any, Any] = {}
for key, entry in value.items():
out[key] = cls._derive_missing_identity_fields_for_entry(key, entry)
return out

@staticmethod
def _derive_missing_identity_fields_for_entry(key: Any, entry: Any) -> Any:
"""Return *entry* with any absent identity fields derived from its identifier."""
if isinstance(entry, VisorVariableState) or not isinstance(entry, Mapping):
return entry

# A value that is present but null counts as present and is left
# to fail validation.
missing = [
field
for field, alias in (
("array_name", "arrayName"),
("type", "type"),
("num_components", "numComponents"),
)
if field not in entry and alias not in entry
]
if not missing:
return entry

identifier = entry.get("id")
if not isinstance(identifier, str) or not identifier:
identifier = key

derived = derive_variable_fields_from_identifier(identifier)
if derived is None:
return entry

filled = dict(entry)
for field in missing:
filled[field] = derived[field]
return filled

Loading
Loading