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/144.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[Remote rendering 3.4] server-owned UI panel and theme state
61 changes: 61 additions & 0 deletions src/ansys/visor/viewer/app/trame/local_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
SetBoundingBoxVisibilityPayload,
SetCrossSectionVisibilityPayload,
SetEdgesVisiblePayload,
SetPanelTopLeftPanelCollapsedPayload,
SetPanelTopRightLegendCollapsedPayload,
SetPanelTopRightPanelCollapsedPayload,
SetPanelTopRightTabIndexPayload,
SetProjectionPayload,
SyncCrossSectionPlanePayload,
)
Expand Down Expand Up @@ -76,6 +80,14 @@ def sync_cross_section_plane(
self, origin: List[float], normal: List[float]
) -> None: ...

def set_panel_top_left_panel_collapsed(self, collapsed: bool) -> None: ...

def set_panel_top_right_panel_collapsed(self, collapsed: bool) -> None: ...

def set_panel_top_right_legend_collapsed(self, collapsed: bool) -> None: ...

def set_panel_top_right_tab_index(self, tab_index: int) -> None: ...


# ----------------------------------------------------------------------
# Trigger payload models
Expand Down Expand Up @@ -209,6 +221,10 @@ class LocalApp:
set_bounding_box_visibility: shows or hides the bounding-box outline
set_projection: sets parallel or perspective projection on the camera record
sync_cross_section_plane: records a settled cross-section plane reported by the frontend
set_panel_top_left_panel_collapsed: records whether the top-left panel is collapsed
set_panel_top_right_panel_collapsed: records whether the top-right panel is collapsed
set_panel_top_right_legend_collapsed: records whether the top-right legend is collapsed
set_panel_top_right_tab_index: records which top-right tab is active
set_only_cookie: sets a cookie on the server (note: Trame server only allows a single cookie header)
Protected Methods:
_cleanup(): Cleans up the active actor in the visualization pipeline.
Expand Down Expand Up @@ -504,6 +520,10 @@ def sync_camera(self, payload) -> None:
# ``set_projection`` lives here too: it is delivered the same way, but
# it writes the camera record's projection field rather than a toggle
# of its own.
#
# The last four carry UI panel layout, not widget state: no renderer
# depends on them, so each coordinator just writes one store field.
# Echoes are idempotent for the same reason as the toggles'.
# ------------------------------------------------------------------

@trigger("set_cross_section_visibility")
Expand Down Expand Up @@ -567,6 +587,47 @@ def sync_cross_section_plane(self, payload) -> None:
return
api.sync_cross_section_plane(payload.origin, payload.normal)

@trigger("set_panel_top_left_panel_collapsed")
@parse_payload(SetPanelTopLeftPanelCollapsedPayload)
def set_panel_top_left_panel_collapsed(self, payload) -> None:
"""Frontend -> Backend: the top-left panel reports its collapsed state."""
api = self._mutation_api("set_panel_top_left_panel_collapsed", payload)
if api is None:
return
api.set_panel_top_left_panel_collapsed(payload.collapsed)

@trigger("set_panel_top_right_panel_collapsed")
@parse_payload(SetPanelTopRightPanelCollapsedPayload)
def set_panel_top_right_panel_collapsed(self, payload) -> None:
"""Frontend -> Backend: the top-right panel reports its collapsed state."""
api = self._mutation_api("set_panel_top_right_panel_collapsed", payload)
if api is None:
return
api.set_panel_top_right_panel_collapsed(payload.collapsed)

@trigger("set_panel_top_right_legend_collapsed")
@parse_payload(SetPanelTopRightLegendCollapsedPayload)
def set_panel_top_right_legend_collapsed(self, payload) -> None:
"""Frontend -> Backend: the top-right legend reports its collapsed state."""
api = self._mutation_api("set_panel_top_right_legend_collapsed", payload)
if api is None:
return
api.set_panel_top_right_legend_collapsed(payload.collapsed)

@trigger("set_panel_top_right_tab_index")
@parse_payload(SetPanelTopRightTabIndexPayload)
def set_panel_top_right_tab_index(self, payload) -> None:
"""Frontend -> Backend: the top-right panel reports its active tab.

The index is forwarded verbatim and is never range-checked here; the
client's ``selectTab`` is the guard, so a future third tab is a UI
change rather than a validation failure at this boundary.
"""
api = self._mutation_api("set_panel_top_right_tab_index", payload)
if api is None:
return
api.set_panel_top_right_tab_index(payload.tab_index)

def set_only_cookie(self, key: str, value: str):
"""
Sets a cookie on the server. NOTE: there is a limitation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@
carries two three-component vectors rather than a boolean. It lives here
rather than beside the per-part models for the same reason the four above
do: it is scene-wide and carries no ``nodeId``.

The four ``SetPanelTopLeft*`` / ``SetPanelTopRight*`` models carry UI panel
layout, not widget state, but are here on the same terms: scene-wide, no
``nodeId``, one absolute field each. Their echoes are idempotent like a
toggle's.

``tab_index`` is deliberately unbounded: the client only ever sends ``0``
or ``1``, and ``Panel_TopRight_Util.selectTab`` already ignores anything
else, so the bound lives there, not here.
"""

from typing import List
Expand Down Expand Up @@ -65,3 +74,36 @@ class SyncCrossSectionPlanePayload(BaseModel):
origin: List[float] = Field(min_length=3, max_length=3)
normal: List[float] = Field(min_length=3, max_length=3)


class SetPanelTopLeftPanelCollapsedPayload(BaseModel):
"""Payload of the ``set_panel_top_left_panel_collapsed`` trigger."""

model_config = ConfigDict(populate_by_name=True)

collapsed: bool


class SetPanelTopRightPanelCollapsedPayload(BaseModel):
"""Payload of the ``set_panel_top_right_panel_collapsed`` trigger."""

model_config = ConfigDict(populate_by_name=True)

collapsed: bool


class SetPanelTopRightLegendCollapsedPayload(BaseModel):
"""Payload of the ``set_panel_top_right_legend_collapsed`` trigger."""

model_config = ConfigDict(populate_by_name=True)

collapsed: bool


class SetPanelTopRightTabIndexPayload(BaseModel):
"""Payload of the ``set_panel_top_right_tab_index`` trigger."""

model_config = ConfigDict(populate_by_name=True)

tab_index: int = Field(alias="tabIndex")


12 changes: 8 additions & 4 deletions src/ansys/visor/viewer/models/runtime/scene/runtime_app_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class RuntimeAppState(BaseModel):
@classmethod
def from_components(
cls,
dark_mode: bool,
ui: VisorUIState,
unit: str | None,
dataset_states: Dict[int, RuntimeDatasetState],
orthographic_enabled: bool | None = None,
Expand All @@ -41,8 +41,12 @@ def from_components(
camera: VisorCameraState | None = None,
variable_states: Dict[str, VisorVariableState] | None = None,
) -> "RuntimeAppState":
"""Construct a RuntimeAppState from the given components."""
ui_state = VisorUIState(dark_theme=dark_mode)
"""Construct a RuntimeAppState from the given components.

``ui`` is taken whole and stored as-is: the server owns every one of
its fields (theme plus the four panel-layout fields) and assembles
them in one place, so this constructor never reaches into it.
"""
runtime_scene_state = RuntimeSceneState(
unit=unit,
camera=camera,
Expand All @@ -55,6 +59,6 @@ def from_components(
spectrum_states=variable_states or {},
)
return cls(
ui=ui_state,
ui=ui,
scene=runtime_scene_state,
)
12 changes: 9 additions & 3 deletions src/ansys/visor/viewer/models/runtime/visor_scene_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pydantic import BaseModel, ConfigDict, Field

from ansys.visor.viewer.models.common.visor_ui_state import VisorUIState
from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import RuntimeDatasetState
from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState
from ansys.visor.viewer.models.runtime.vtk.renderer_annotation import RendererAnnotation
Expand All @@ -26,7 +27,7 @@ class VisorSceneDetails(BaseModel):
@classmethod
def from_components(
cls,
dark_mode: bool,
ui: VisorUIState,
unit: str | None,
dataset_states: Dict[int, RuntimeDatasetState],
scene_graph_state: SceneGraphNodeInfo | None = None,
Expand All @@ -36,13 +37,18 @@ def from_components(
edges_enabled: bool | None = None,
bounding_box_enabled: bool | None = None,
) -> "VisorSceneDetails":
"""Construct an instance from components."""
"""Construct an instance from components.

The UI record is forwarded whole to
:meth:`RuntimeAppState.from_components`; this class does not build one
and does not read any field of it.
"""
vtk_info = RuntimeVTKInfo(
scene_graph=scene_graph_state,
renderer_annotation=renderer_annotation,
)
app_state = RuntimeAppState.from_components(
dark_mode=dark_mode,
ui=ui,
unit=unit,
dataset_states=dataset_states,
orthographic_enabled=orthographic_enabled,
Expand Down
Loading
Loading