Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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/165.maintenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[Remote rendering 3.6a] Cleanups after moving authority to server
9 changes: 0 additions & 9 deletions src/ansys/visor/viewer/app/trame/local_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ class LocalApp:
server (Server): the Trame server instance
Methods:
get_scene_details_json: returns the scene details in JSON format
save_state_response: sends the current app state as payload
pick_geometry: picks the geometry for rendering
perf_report_wasm: reports the performance of the wasm update cycle
perf_report_server_update: reports the performance of the server update cycle
Expand Down Expand Up @@ -235,7 +234,6 @@ def __init__(
self,
server: Server,
get_scene_details_json: callable,
handle_save_state_response: callable,
standalone: bool = True,
trame_logger: Logger | None = None,
pick_geometry=None,
Expand All @@ -244,8 +242,6 @@ def __init__(
self.server = server
# Callable to get the scene details in JSON format
self._get_scene_details_json = get_scene_details_json
# Callable to handle the save state response from the frontend
self._handle_save_state_response = handle_save_state_response
# Callable for sub-geometry picking (optional)
self._pick_geometry = pick_geometry
# Per-part visual state coordinator (see SceneMutationApi). The one
Expand Down Expand Up @@ -310,11 +306,6 @@ def get_visor_scene_details_json(self):
""" returns the scene details in JSON format. """
return self._get_scene_details_json()

@trigger("save_state_response")
def save_state_response(self, request_id: int, payload: dict):
"""Frontend → Backend: the frontend sends the current app state as payload."""
return self._handle_save_state_response(request_id, payload)

@trigger("pick_geometry")
def pick_geometry(self, actor_wasm_id, cell_id, mode: str, world_x: float, world_y: float, world_z: float):
"""Frontend → Backend: perform sub-geometry picking given a WASM actor/cell ID and world-space point."""
Expand Down
20 changes: 5 additions & 15 deletions src/ansys/visor/viewer/app/visor_vtk.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Trame VTKlocal implementation of Visor class"""

import asyncio
import traceback
from abc import abstractmethod
from functools import wraps
Expand Down Expand Up @@ -365,29 +364,20 @@ def update_variables(self, dataset_id: int, variables: List[Dict[str, Any]]) ->
logger.info(f"Variables for dataset {dataset_id} updated successfully")

@require_server_on
async def save_state(self, state_dir: str, timeout: float = 5.0) -> None:
async def save_state(self, state_dir: str) -> None:
"""
Save the current viewer state to a directory.
Requests the runtime state from the frontend, converts it to persisted state,
and writes it as visor.json to state_dir.
Builds the persisted state from the scene's server-side records and
writes it as visor.json to state_dir. No client needs to be connected.

The canonical snapshot path is always stamped onto each dataset's persisted
state before writing, so load_state has one unambiguous place to look
regardless of how the dataset was originally loaded.
"""
try:
state = await self._scene.get_state(timeout=timeout)
except asyncio.TimeoutError as e:
msg = (
"Timed out waiting for the frontend to respond to `getState`.\n"
"This typically means the Trame client is not connected/ready, or JS calls are not being processed.\n"
f"Waited {timeout} seconds."
)
raise RuntimeError(msg) from e
state = self._scene.get_state()

# Stamp the canonical snapshot path for every registered dataset.
# The registry is the source of truth for which datasets are loaded — not
# the frontend state, which only knows about visual/UI properties.
# The registry is the source of truth for which datasets are loaded.
for dataset_name, dataset_state in state.scene.dataset_states.items():
dataset_state.serialized_dataset_path = str(
self._file_io.get_persisted_dataset_path(state_dir, dataset_name)
Expand Down
1 change: 0 additions & 1 deletion src/ansys/visor/viewer/app/visor_vtk_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ def _initialize_rendering(self, standalone: bool, trame_log_dir: str | None) ->
self._trame_app = LocalApp(
self.server,
lambda: self._scene.get_scene_details_json(),
lambda request_id, response: self._scene.handle_save_state_response(request_id, response),
standalone,
trame_logger=get_trame_logger(trame_log_dir),
pick_geometry=lambda
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ 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.variable_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.
required: the model is also built on the save path, by
``VisorVariableRecord.to_variable_state``, so relaxing them would also
relax that construction. Tolerance for older save files lives here, on
the container, and applies to the ingest boundary only.
"""
if not isinstance(value, Mapping):
return value
Expand Down

This file was deleted.

This file was deleted.

70 changes: 26 additions & 44 deletions src/ansys/visor/viewer/vtk/scene/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import threading
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING, Dict, List
from typing import Dict, List

from trame_server import Server

Expand All @@ -24,6 +24,7 @@
from ansys.visor.viewer.models.common.visor_variable_state import VisorVariableState
from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1
from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import RuntimePartProperties
from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState
from ansys.visor.viewer.models.runtime.visor_scene_details import VisorSceneDetails
from ansys.visor.viewer.renderer.base import IRenderer
from ansys.visor.viewer.vtk.datasets.visor_dataset import VisorDataset
Expand All @@ -34,9 +35,6 @@
from ansys.visor.viewer.vtk.variables.visor_part_variables import VisorPartVariables
from ansys.visor.viewer.vtk.variables.visor_variable_update import VisorVariableUpdate

if TYPE_CHECKING:
from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState

logger = VisorDefaultLogger(__name__)


Expand Down Expand Up @@ -81,19 +79,20 @@ class VisorSceneBase(ABC):
implementation (``self._vtk_renderer``): VTK pipeline objects, actor
lifecycle, camera, and scene-state serialisation.

Subclasses must implement two abstract hooks that capture the difference
in *state authority* between rendering backends:
Subclasses must implement one abstract hook, which captures the difference
in *state delivery* between rendering backends:

* :meth:`_get_runtime_state_async` — wasm path does a frontend round-trip;
RCA/headless paths build state server-side.
* :meth:`_push_runtime_state` — wasm path calls a JS
``set_state``; RCA path pushes camera onto ``vtkCamera``; headless
is a no-op.

The saved state is built here, from the server's own records, for every
backend; no backend asks a client for it.

This separation means that adding a new rendering backend requires only:

1. A new :class:`IRenderer` implementation.
2. A new :class:`VisorSceneBase` subclass that overrides the two hooks.
2. A new :class:`VisorSceneBase` subclass that overrides the hook.
"""

_server: Server
Expand Down Expand Up @@ -187,20 +186,9 @@ def __init__(
self._state_mapper = VisorStateMapper(self._dataset_registry)

# =========================================================================
# Abstract hooks — subclasses differ on state authority
# Abstract hook — subclasses differ on state delivery
# =========================================================================

@abstractmethod
async def _get_runtime_state_async(self, timeout: float) -> "RuntimeAppState":
"""
Obtain the current runtime app state.

* Wasm path: round-trip to the React frontend via
:class:`VisorFrontendBridge`.
* RCA / headless paths: build entirely from server-side VTK objects and
the dataset registry — no frontend call.
"""

@abstractmethod
def _push_runtime_state(self, runtime_app_state: "RuntimeAppState") -> None:
"""
Expand Down Expand Up @@ -248,29 +236,25 @@ def list_all_dataset_info(self) -> dict[int, dict]:
"""Convenience: metadata snapshot for UI without parts."""
return self._dataset_registry.list_info()

async def get_state(self, timeout: float) -> PersistedViewerStateV1:
def get_state(self) -> PersistedViewerStateV1:
"""
Capture the current viewer state and return it as a
:class:`PersistedViewerStateV1`.

The frontend round trip remains the only source for everything the browser owns.
Per-part state is not: ``scene.dataset_states`` is replaced with the registry's
own runtime state before the persisted mapping runs.
Build the current viewer state from the server's own records and
return it as a :class:`PersistedViewerStateV1`.

The registry hands out live ``RuntimeDatasetState`` objects that the per-part
setters mutate from the trame daemon thread, so each one is deep-copied under
``_vtk_lock``. The lock is taken after the ``await`` and never held across one.
No client is consulted, so a save works with no browser connected.
The runtime state starts empty and every one of its fields is assigned
below; a field left unassigned here would be saved at its model default.

The camera and the widget toggles are what the browser's reply does not
get to supply. Per-part state comes from the registry, the toggles from
this object's own store, and the camera from the renderer's record; the
reply is consulted for none of the three.
Per-part state comes from the registry. The registry hands out live
``RuntimeDatasetState`` objects that the per-part setters mutate from the
trame daemon thread, so each one is deep-copied under ``_vtk_lock``.

The UI record is the fourth. ``runtime_state.ui`` is replaced
wholesale with a copy of this object's own record.
The widget toggles come from this object's own store, and the UI record
is a copy of this object's own record, so a panel trigger landing after
the call cannot mutate the state already returned.

The camera comes from the renderer's record, which is authoritative, rather than
from the reply or from the pipeline ``vtkCamera``: the pipeline is the
from the pipeline ``vtkCamera``: the pipeline is the
record's projection, and reading it back would re-import whatever drift
VTK introduced -- ``ResetCamera`` rewrites ``clipping_range``. The
assignment is unconditional. A ``None`` record means no camera was ever
Expand All @@ -288,8 +272,11 @@ async def get_state(self, timeout: float) -> PersistedViewerStateV1:
``None`` means. In normal operation there is no ``None`` case to
guard because the renderer seeds the record from its own widget the
first time bounds are pushed.

Variables and unit are the server's: the variable records and the
registry's unit.
"""
runtime_state = await self._get_runtime_state_async(timeout)
runtime_state = RuntimeAppState()

with self._vtk_lock:
registry_dataset_states = {
Expand All @@ -306,7 +293,6 @@ async def get_state(self, timeout: float) -> PersistedViewerStateV1:
runtime_state.scene.edges_enabled = self._edges_enabled
runtime_state.scene.bounding_box_enabled = self._bounding_box_enabled
runtime_state.ui = self._ui_state.model_copy()
# Variables and unit are the server's, never the browser's reply.
runtime_state.scene.variable_states = self._variable_records.model_copy(deep=True).variables
runtime_state.scene.unit = self._dataset_registry.unit
runtime_state.scene.dataset_states = registry_dataset_states
Expand Down Expand Up @@ -419,10 +405,6 @@ def get_scene_details_json(self) -> str:
"""Return the VisorVtkPipelineState as JSON string."""
return json.dumps(self.get_scene_details().model_dump(exclude_none=True, by_alias=True))

def handle_save_state_response(self, request_id: int, response: dict) -> None:
"""Called by LocalApp trigger when the frontend responds."""
self._frontend_bridge.resolve_save_state_response(request_id, response)

def clear(self):
"""Remove all actors from the renderer and reset the scene.

Expand Down
18 changes: 4 additions & 14 deletions src/ansys/visor/viewer/vtk/scene/local_scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ class VisorLocalScene(VisorSceneBase):
"""
Coordinator for a Visor viewer scene using the wasm/LocalView rendering path.

The React frontend is authority for camera state. :meth:`get_state` asks
the frontend for the current camera via :class:`VisorFrontendBridge` and
waits for the response. :meth:`apply_state` pushes the restored state back
to the frontend via a JS ``set_state`` call.
:meth:`get_state` is the base class's server-side build and asks the
frontend for nothing. :meth:`apply_state` pushes the restored state to
the frontend via a JS ``set_state`` call through :class:`VisorFrontendBridge`.

This is the scene used by
:class:`~ansys.visor.viewer.app.visor_vtk_local.VisorVTKLocal`
Expand All @@ -42,14 +41,9 @@ def __init__(self, server: Server, dark_mode: bool = False):
raise RuntimeError(msg) from e

# -------------------------------------------------------------------------
# State authority hooks — wasm: frontend round-trip
# State delivery hook — wasm: JS set_state
# -------------------------------------------------------------------------

async def _get_runtime_state_async(self, timeout: float) -> "RuntimeAppState":
"""Ask the React frontend for the current app state (camera, UI, …)."""
response = await self._frontend_bridge.request_state(timeout=timeout)
return response.app_state

def _push_runtime_state(self, runtime_app_state: "RuntimeAppState") -> None:
"""
Flush the VTK window then push the restored state to the React frontend.
Expand All @@ -70,10 +64,6 @@ def _push_runtime_state(self, runtime_app_state: "RuntimeAppState") -> None:
# Wasm-specific helpers (not part of VisorSceneBase)
# -------------------------------------------------------------------------

def handle_save_state_response(self, request_id: int, response: dict) -> None:
"""Called by LocalApp trigger when the frontend responds."""
self._frontend_bridge.resolve_save_state_response(request_id, response)

def cleanup_state(self) -> None:
"""Remove transient wasm keys from trame server state."""
self._server.state.pop("wasm_ids", None)
Expand Down
Loading
Loading