Skip to content

Commit d148063

Browse files
authored
feat: [Remote rendering 3.4] server-owned UI panel and theme state (#144)
1 parent 3cbf2d6 commit d148063

15 files changed

Lines changed: 1201 additions & 26 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[Remote rendering 3.4] server-owned UI panel and theme state

‎src/ansys/visor/viewer/app/trame/local_app.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
SetBoundingBoxVisibilityPayload,
1818
SetCrossSectionVisibilityPayload,
1919
SetEdgesVisiblePayload,
20+
SetPanelTopLeftPanelCollapsedPayload,
21+
SetPanelTopRightLegendCollapsedPayload,
22+
SetPanelTopRightPanelCollapsedPayload,
23+
SetPanelTopRightTabIndexPayload,
2024
SetProjectionPayload,
2125
SyncCrossSectionPlanePayload,
2226
)
@@ -76,6 +80,14 @@ def sync_cross_section_plane(
7680
self, origin: List[float], normal: List[float]
7781
) -> None: ...
7882

83+
def set_panel_top_left_panel_collapsed(self, collapsed: bool) -> None: ...
84+
85+
def set_panel_top_right_panel_collapsed(self, collapsed: bool) -> None: ...
86+
87+
def set_panel_top_right_legend_collapsed(self, collapsed: bool) -> None: ...
88+
89+
def set_panel_top_right_tab_index(self, tab_index: int) -> None: ...
90+
7991

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

509529
@trigger("set_cross_section_visibility")
@@ -567,6 +587,47 @@ def sync_cross_section_plane(self, payload) -> None:
567587
return
568588
api.sync_cross_section_plane(payload.origin, payload.normal)
569589

590+
@trigger("set_panel_top_left_panel_collapsed")
591+
@parse_payload(SetPanelTopLeftPanelCollapsedPayload)
592+
def set_panel_top_left_panel_collapsed(self, payload) -> None:
593+
"""Frontend -> Backend: the top-left panel reports its collapsed state."""
594+
api = self._mutation_api("set_panel_top_left_panel_collapsed", payload)
595+
if api is None:
596+
return
597+
api.set_panel_top_left_panel_collapsed(payload.collapsed)
598+
599+
@trigger("set_panel_top_right_panel_collapsed")
600+
@parse_payload(SetPanelTopRightPanelCollapsedPayload)
601+
def set_panel_top_right_panel_collapsed(self, payload) -> None:
602+
"""Frontend -> Backend: the top-right panel reports its collapsed state."""
603+
api = self._mutation_api("set_panel_top_right_panel_collapsed", payload)
604+
if api is None:
605+
return
606+
api.set_panel_top_right_panel_collapsed(payload.collapsed)
607+
608+
@trigger("set_panel_top_right_legend_collapsed")
609+
@parse_payload(SetPanelTopRightLegendCollapsedPayload)
610+
def set_panel_top_right_legend_collapsed(self, payload) -> None:
611+
"""Frontend -> Backend: the top-right legend reports its collapsed state."""
612+
api = self._mutation_api("set_panel_top_right_legend_collapsed", payload)
613+
if api is None:
614+
return
615+
api.set_panel_top_right_legend_collapsed(payload.collapsed)
616+
617+
@trigger("set_panel_top_right_tab_index")
618+
@parse_payload(SetPanelTopRightTabIndexPayload)
619+
def set_panel_top_right_tab_index(self, payload) -> None:
620+
"""Frontend -> Backend: the top-right panel reports its active tab.
621+
622+
The index is forwarded verbatim and is never range-checked here; the
623+
client's ``selectTab`` is the guard, so a future third tab is a UI
624+
change rather than a validation failure at this boundary.
625+
"""
626+
api = self._mutation_api("set_panel_top_right_tab_index", payload)
627+
if api is None:
628+
return
629+
api.set_panel_top_right_tab_index(payload.tab_index)
630+
570631
def set_only_cookie(self, key: str, value: str):
571632
"""
572633
Sets a cookie on the server. NOTE: there is a limitation

‎src/ansys/visor/viewer/models/runtime/requests/widget_state_payloads.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
1818
carries two three-component vectors rather than a boolean. It lives here
1919
rather than beside the per-part models for the same reason the four above
2020
do: it is scene-wide and carries no ``nodeId``.
21+
22+
The four ``SetPanelTopLeft*`` / ``SetPanelTopRight*`` models carry UI panel
23+
layout, not widget state, but are here on the same terms: scene-wide, no
24+
``nodeId``, one absolute field each. Their echoes are idempotent like a
25+
toggle's.
26+
27+
``tab_index`` is deliberately unbounded: the client only ever sends ``0``
28+
or ``1``, and ``Panel_TopRight_Util.selectTab`` already ignores anything
29+
else, so the bound lives there, not here.
2130
"""
2231

2332
from typing import List
@@ -65,3 +74,36 @@ class SyncCrossSectionPlanePayload(BaseModel):
6574
origin: List[float] = Field(min_length=3, max_length=3)
6675
normal: List[float] = Field(min_length=3, max_length=3)
6776

77+
78+
class SetPanelTopLeftPanelCollapsedPayload(BaseModel):
79+
"""Payload of the ``set_panel_top_left_panel_collapsed`` trigger."""
80+
81+
model_config = ConfigDict(populate_by_name=True)
82+
83+
collapsed: bool
84+
85+
86+
class SetPanelTopRightPanelCollapsedPayload(BaseModel):
87+
"""Payload of the ``set_panel_top_right_panel_collapsed`` trigger."""
88+
89+
model_config = ConfigDict(populate_by_name=True)
90+
91+
collapsed: bool
92+
93+
94+
class SetPanelTopRightLegendCollapsedPayload(BaseModel):
95+
"""Payload of the ``set_panel_top_right_legend_collapsed`` trigger."""
96+
97+
model_config = ConfigDict(populate_by_name=True)
98+
99+
collapsed: bool
100+
101+
102+
class SetPanelTopRightTabIndexPayload(BaseModel):
103+
"""Payload of the ``set_panel_top_right_tab_index`` trigger."""
104+
105+
model_config = ConfigDict(populate_by_name=True)
106+
107+
tab_index: int = Field(alias="tabIndex")
108+
109+

‎src/ansys/visor/viewer/models/runtime/scene/runtime_app_state.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class RuntimeAppState(BaseModel):
3030
@classmethod
3131
def from_components(
3232
cls,
33-
dark_mode: bool,
33+
ui: VisorUIState,
3434
unit: str | None,
3535
dataset_states: Dict[int, RuntimeDatasetState],
3636
orthographic_enabled: bool | None = None,
@@ -41,8 +41,12 @@ def from_components(
4141
camera: VisorCameraState | None = None,
4242
variable_states: Dict[str, VisorVariableState] | None = None,
4343
) -> "RuntimeAppState":
44-
"""Construct a RuntimeAppState from the given components."""
45-
ui_state = VisorUIState(dark_theme=dark_mode)
44+
"""Construct a RuntimeAppState from the given components.
45+
46+
``ui`` is taken whole and stored as-is: the server owns every one of
47+
its fields (theme plus the four panel-layout fields) and assembles
48+
them in one place, so this constructor never reaches into it.
49+
"""
4650
runtime_scene_state = RuntimeSceneState(
4751
unit=unit,
4852
camera=camera,
@@ -55,6 +59,6 @@ def from_components(
5559
variable_states=variable_states or {},
5660
)
5761
return cls(
58-
ui=ui_state,
62+
ui=ui,
5963
scene=runtime_scene_state,
6064
)

‎src/ansys/visor/viewer/models/runtime/visor_scene_details.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from pydantic import BaseModel, ConfigDict, Field
66

7+
from ansys.visor.viewer.models.common.visor_ui_state import VisorUIState
78
from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import RuntimeDatasetState
89
from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState
910
from ansys.visor.viewer.models.runtime.vtk.renderer_annotation import RendererAnnotation
@@ -26,7 +27,7 @@ class VisorSceneDetails(BaseModel):
2627
@classmethod
2728
def from_components(
2829
cls,
29-
dark_mode: bool,
30+
ui: VisorUIState,
3031
unit: str | None,
3132
dataset_states: Dict[int, RuntimeDatasetState],
3233
scene_graph_state: SceneGraphNodeInfo | None = None,
@@ -36,13 +37,18 @@ def from_components(
3637
edges_enabled: bool | None = None,
3738
bounding_box_enabled: bool | None = None,
3839
) -> "VisorSceneDetails":
39-
"""Construct an instance from components."""
40+
"""Construct an instance from components.
41+
42+
The UI record is forwarded whole to
43+
:meth:`RuntimeAppState.from_components`; this class does not build one
44+
and does not read any field of it.
45+
"""
4046
vtk_info = RuntimeVTKInfo(
4147
scene_graph=scene_graph_state,
4248
renderer_annotation=renderer_annotation,
4349
)
4450
app_state = RuntimeAppState.from_components(
45-
dark_mode=dark_mode,
51+
ui=ui,
4652
unit=unit,
4753
dataset_states=dataset_states,
4854
orthographic_enabled=orthographic_enabled,

0 commit comments

Comments
 (0)