@@ -48,7 +48,7 @@ class VisorSceneBase(ABC):
4848
4949 * :meth:`_get_runtime_state_async` — wasm path does a frontend round-trip;
5050 RCA/headless paths build state server-side.
51- * :meth:`_apply_runtime_state_to_render ` — wasm path calls a JS
51+ * :meth:`_push_runtime_state ` — wasm path calls a JS
5252 ``set_state``; RCA path pushes camera onto ``vtkCamera``; headless
5353 is a no-op.
5454
@@ -117,7 +117,7 @@ async def _get_runtime_state_async(self, timeout: float) -> "RuntimeAppState":
117117 """
118118
119119 @abstractmethod
120- def _apply_runtime_state_to_render (self , runtime_app_state : "RuntimeAppState" ) -> None :
120+ def _push_runtime_state (self , runtime_app_state : "RuntimeAppState" ) -> None :
121121 """
122122 Push a runtime app state onto the renderer / frontend after the
123123 shared per-part state has already been restored.
@@ -174,14 +174,14 @@ def apply_state(self, state: PersistedViewerStateV1):
174174 """
175175 Apply a saved viewer state.
176176
177- Shared work ( per-part state restoration) is done here; the
178- renderer-specific final step is delegated to
179- :meth:`_apply_runtime_state_to_render` .
177+ One ``_restore_*`` step per state class, each making the server's own
178+ copy of that class match the loaded state: its stored state, and the
179+ VTK objects that the state drives .
180180
181- Holds ``_vtk_lock`` for the whole body: the delegated step mutates
182- VTK and pushes to the frontend. The critical section deliberately
183- spans the outbound bridge call and the flush that follows it — the
184- unit the lock protects is the compound sequence, not the VTK work .
181+ The renderer-speific delivery step is delegated to :meth:`_push_runtime_state`,
182+ and runs last, once every record above it has been written.
183+
184+ Holds ``_vtk_lock`` for the whole body, including the delegated render step .
185185 """
186186 with self ._vtk_lock :
187187 # Apply UI settings
@@ -190,9 +190,14 @@ def apply_state(self, state: PersistedViewerStateV1):
190190 # Transform the frontend PersistedViewerStateV1 -> RuntimeAppState
191191 runtime_app_state = self ._state_mapper .persisted_to_runtime (state )
192192
193- self ._restore_part_states_from_runtime (runtime_app_state )
193+ # One call per state class: updates the server's stored state and its VTK objects.
194+ self ._restore_part_states (runtime_app_state )
195+ self ._restore_camera_state (runtime_app_state )
196+ # TODO: restore widget state, UI state, and variable states when they are synced back to the server.
194197
195- self ._apply_runtime_state_to_render (runtime_app_state )
198+ # The server's copy is now current; deliver it to the rendering backend.
199+ # wasm: set_state() to the browser; RCA: a rendered frame; headless: no-op.
200+ self ._push_runtime_state (runtime_app_state )
196201
197202 # Note: There is intentionally no wasm flush here: the bridge call is fire-and-forget, so a flush
198203 # at this point races the client's rebuild against a half-written object graph.
@@ -393,6 +398,7 @@ def reset_camera(self):
393398 return
394399
395400 self ._renderer .reset_camera (self ._scene_graph .bounds )
401+ self ._renderer .serialize_camera_state ()
396402
397403 def pick_geometry (self , actor_wasm_id , cell_id , mode , world_x , world_y , world_z ) -> dict :
398404 """
@@ -518,7 +524,7 @@ def clear_part_color_variable(self, node_id: int) -> None:
518524 return
519525 self ._renderer .clear_color_variable (node_id )
520526
521- def _restore_part_states_from_runtime (self , runtime_app_state : "RuntimeAppState" ) -> None :
527+ def _restore_part_states (self , runtime_app_state : "RuntimeAppState" ) -> None :
522528 """
523529 Restore per-part state from a runtime app state, on the load path.
524530
@@ -538,7 +544,7 @@ def _restore_part_states_from_runtime(self, runtime_app_state: "RuntimeAppState"
538544 dataset = self ._dataset_registry .datasets .get (dataset_id )
539545 if dataset is None :
540546 logger .warning (
541- "_restore_part_states_from_runtime : dataset %s is not registered; "
547+ "_restore_part_states : dataset %s is not registered; "
542548 "its part state was not applied to the pipeline." , dataset_id
543549 )
544550 continue
@@ -554,6 +560,22 @@ def _restore_part_states_from_runtime(self, runtime_app_state: "RuntimeAppState"
554560 part_id , part_state , variable_states , variables_by_part .get (part_id )
555561 )
556562
563+ def _restore_camera_state (self , runtime_app_state : "RuntimeAppState" ) -> None :
564+ """
565+ Restore the camera state from a runtime app state, on the load path.
566+
567+ Write the loaded camera to the record and the pipeline camera, so a client rebuilt
568+ from server state (refresh) gets it. Must precede the render step. The re-serialize
569+ is required: the server advertises the camera's live MTime but serves its cached state,
570+ so without it a client fetches the pre-load camera. A state with no camera leaves both
571+ alone.
572+
573+ Callers must hold ``_vtk_lock``.
574+ """
575+ if runtime_app_state .scene .camera is not None :
576+ self ._renderer .sync_camera (runtime_app_state .scene .camera )
577+ self ._renderer .serialize_camera_state ()
578+
557579 def _restore_one_part_state (
558580 self ,
559581 part_id : int ,
0 commit comments