Skip to content

Commit 3be1afd

Browse files
committed
serialize framework's roots rather than render window alone
1 parent 0248dd4 commit 3be1afd

3 files changed

Lines changed: 106 additions & 22 deletions

File tree

‎src/ansys/visor/viewer/renderer/local_renderer.py‎

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -324,19 +324,49 @@ def serialize_camera_state(self) -> None:
324324
content, and the client fetches the pre-write camera and applies it
325325
over the one just installed.
326326
327-
The render window's id is passed, not the camera's own. It is the
328-
form the framework itself reproduces -- ``LocalView.update`` resolves
329-
``[self._render_window, *registered]`` to ids and hands those to
330-
``UpdateStatesFromObjects`` -- and the camera sits inside the render
331-
window's dependency closure, which is why ``get_status`` can name the
332-
camera's id at all when building ``ignore_ids``.
327+
The camera's own id is not passed; the render window's is, because
328+
the camera sits inside the render window's dependency closure, which
329+
is why ``get_status`` can name the camera's id at all when building
330+
``ignore_ids``.
331+
332+
**But the render window alone is not the root list the framework
333+
uses.** ``LocalView.update`` resolves ``[self._render_window,
334+
*registered]`` -- the render window *plus* every object handed to
335+
``register_vtk_object`` -- and hands all of those ids to
336+
``UpdateStatesFromObjects``. Here that is the interactor, the
337+
picker, and the orientation / cross-section / bounding-box widget
338+
objects. They are registered separately precisely because they are
339+
**not** reachable from the render window's dependency closure; that
340+
is also why ``ObjectManagerAPI.get_status`` has to union
341+
``GetAllDependencies`` over ``_widgets[root_id]`` on top of the
342+
render window's own closure before it can describe the scene to a
343+
client.
344+
345+
Serialising from the render window alone therefore re-roots the
346+
object manager's state store on a strictly narrower set and leaves
347+
those widget subtrees unrefreshed. A client that is already
348+
connected does not notice -- it cached those states at its own first
349+
load -- but a *freshly* connecting client builds its entire fetch
350+
list from what ``get_status`` reports, so the ids it needs to resolve
351+
(``WasmRendererAnnotation.widgets``) are simply absent and its scene
352+
construction fails. Passing the same root list ``LocalView.update``
353+
does closes that hole.
354+
355+
``api.get_all_ids(render_window_id)`` is the id-space spelling of
356+
that list: ``[root_id, *self._widgets[root_id]]``, which is exactly
357+
``[self._render_window, *registered]`` resolved to ids. It is the
358+
same call ``LocalView.save`` uses, and it degrades to ``[root_id]``
359+
if nothing was ever registered.
333360
334361
No ``js_call``: that lives in ``LocalView.update``, not in the object
335362
manager, so this serialises without pushing and without re-opening
336-
the rebuild race ``_apply_runtime_state_to_render`` refuses.
363+
the rebuild race ``_apply_runtime_state_to_render`` refuses. Widening
364+
the root list changes *what* is serialised, never *whether* the
365+
client is notified.
337366
"""
367+
render_window_id = self._object_manager.GetId(self._render_window)
338368
self._object_manager.UpdateStatesFromObjects(
339-
[self._object_manager.GetId(self._render_window)]
369+
self._local_view.api.get_all_ids(render_window_id)
340370
)
341371

342372
# ------------------------------------------------------------------

‎tests/unit/renderer/test_local_renderer.py‎

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,22 @@ def SetParallelScale(self, value): # noqa: N802
191191
# therefore fails if production names the renderer, the interactor, the picker
192192
# or the active camera, instead of coinciding with whatever it named.
193193
#
194-
# Both are far outside the small-integer range a real object manager hands out
195-
# in a freshly initialised scene, so a literal arriving from anywhere other
196-
# than here is visible on sight.
194+
# ``REGISTERED_WASM_IDS`` stands for the separately registered objects -- the
195+
# interactor, the picker and the widget objects -- that ``LocalView.update``
196+
# carries alongside the render window and that ``api.get_all_ids`` appends to
197+
# the root it is handed. Kept as its own literal so that an implementation
198+
# passing the render window *alone* fails rather than coincides: that
199+
# narrowing is exactly what leaves a freshly connecting client without the
200+
# widget states it has to resolve.
201+
#
202+
# All three are far outside the small-integer range a real object manager
203+
# hands out in a freshly initialised scene, so a literal arriving from
204+
# anywhere other than here is visible on sight.
197205
# ---------------------------------------------------------------------------
198206

199207
RENDER_WINDOW_WASM_ID = 8150001
200208
WRONG_OBJECT_WASM_ID = 8150999
209+
REGISTERED_WASM_IDS = [8150101, 8150102, 8150103]
201210

202211

203212

@@ -224,6 +233,15 @@ def renderer():
224233
render_window = MagicMock(name="render_window")
225234
interactor = MagicMock(name="interactor")
226235
local_view = MagicMock(name="local_view")
236+
# serialize_camera_state roots on api.get_all_ids(render_window_id).
237+
# Stubbed as a faithful function of the root it is handed -- the real
238+
# ObjectManagerAPI.get_all_ids returns [root_id, *widgets[root_id]] --
239+
# so the identity keying of GetId still reaches the assertion: a
240+
# production that rooted on the wrong object puts that object's id at
241+
# the head of this list, not the render window's.
242+
local_view.api.get_all_ids.side_effect = (
243+
lambda root_id: [root_id, *REGISTERED_WASM_IDS]
244+
)
227245
orientation_widget = MagicMock(name="orientation_widget")
228246
cross_section_widget = MagicMock(name="cross_section_widget")
229247
bounding_box_widget = MagicMock(name="bounding_box_widget")
@@ -741,13 +759,25 @@ def test_sync_camera_stores_the_object_without_copying(self, renderer):
741759
def test_serialize_camera_state_updates_states_from_the_render_window_id(
742760
self, renderer
743761
):
744-
"""The re-serialise names the render window, and nothing else.
762+
"""The re-serialise names the render window and the registered objects.
763+
764+
That is the root list ``LocalView.update`` uses --
765+
``[self._render_window, *registered]`` -- of which
766+
``api.get_all_ids(root_id)`` is the id-space spelling,
767+
``[root_id, *widgets[root_id]]``.
745768
746769
The id source is keyed on object identity, so every object other than
747770
the render window resolves to a different, equally distinctive
748-
literal. Asserting ``RENDER_WINDOW_WASM_ID`` therefore fails if the
749-
implementation names ``_vtk_renderer``, the interactor, or the active
750-
camera, rather than coinciding with them. Asserting against
771+
literal, and the fixture's ``get_all_ids`` stub is a faithful
772+
function of the root it is handed. Asserting
773+
``[RENDER_WINDOW_WASM_ID, *REGISTERED_WASM_IDS]`` therefore fails if
774+
the implementation roots on ``_vtk_renderer``, the interactor, or the
775+
active camera, rather than coinciding with them -- that object's id
776+
arrives at the head of the list. It fails equally if the
777+
implementation drops the registered objects and passes the render
778+
window alone: that narrowing re-roots the object manager's state
779+
store on less than the scene, and starves a freshly connecting client
780+
of the widget states it has to resolve. Asserting against
751781
``GetId(renderer._render_window)`` -- the attribute production reads
752782
-- would pass in all of those cases and pin nothing.
753783
@@ -763,7 +793,7 @@ def test_serialize_camera_state_updates_states_from_the_render_window_id(
763793
renderer.serialize_camera_state()
764794

765795
renderer._object_manager.UpdateStatesFromObjects.assert_called_with(
766-
[RENDER_WINDOW_WASM_ID]
796+
[RENDER_WINDOW_WASM_ID, *REGISTERED_WASM_IDS]
767797
)
768798

769799
def test_serialize_camera_state_does_not_notify_the_client(self, renderer):

‎tests/unit/vtk/scene/test_base.py‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,19 @@ def array_dataset() -> vtkPolyData:
162162
# rather than coincides -- if the re-serialisation names the renderer, the
163163
# interactor, the picker or the active camera instead of the render window.
164164
#
165+
# REGISTERED_WASM_IDS stands for the separately registered objects -- the
166+
# interactor, the picker and the widget objects -- that LocalView.update
167+
# carries alongside the render window and that api.get_all_ids appends to the
168+
# root it is handed. Kept as its own literal so that a re-serialisation
169+
# passing the render window alone fails rather than coincides.
170+
#
165171
# Asserting against the id source's own answer for the attribute production
166172
# reads would pin nothing: it would pass whichever object production named.
167173
# ---------------------------------------------------------------------------
168174

169175
RENDER_WINDOW_WASM_ID = 8150001
170176
WRONG_OBJECT_WASM_ID = 8150999
177+
REGISTERED_WASM_IDS = [8150101, 8150102, 8150103]
171178

172179

173180
@pytest.fixture
@@ -183,6 +190,12 @@ def renderer():
183190
identity, the render window resolves to RENDER_WINDOW_WASM_ID and every
184191
other object to WRONG_OBJECT_WASM_ID. That is what lets the ordering test
185192
below assert which object the re-serialisation named.
193+
194+
``api.get_all_ids`` is seeded alongside it, since the re-serialisation
195+
roots on ``api.get_all_ids(render_window_id)``. It is stubbed as a
196+
faithful function of the root it is handed -- the real
197+
``ObjectManagerAPI.get_all_ids`` returns ``[root_id, *widgets[root_id]]``
198+
-- so the identity keying above still reaches the assertion.
186199
"""
187200
mock_server = MagicMock()
188201
mock_server.state = {}
@@ -209,6 +222,9 @@ def renderer():
209222
if obj is r._render_window
210223
else WRONG_OBJECT_WASM_ID
211224
)
225+
r._local_view.api.get_all_ids.side_effect = (
226+
lambda root_id: [root_id, *REGISTERED_WASM_IDS]
227+
)
212228
return r
213229

214230

@@ -1134,7 +1150,9 @@ def test_apply_state_syncs_the_camera_under_the_lock_before_the_render_step(scen
11341150
``("serialize", <ids>)`` for the re-serialisation -- the tuple is the
11351151
recording format, not the argument -- and ``"bridge"`` for the delegated
11361152
render step. ``<ids>`` is asserted as the list production passes, since
1137-
``UpdateStatesFromObjects`` takes a sequence.
1153+
``UpdateStatesFromObjects`` takes a sequence: the render window at the
1154+
head, keyed on identity, followed by the separately registered objects
1155+
that ``LocalView.update`` carries alongside it.
11381156
11391157
Ordered between the two: after the write, because re-serialising before
11401158
it would publish the pre-load camera; before the render step, because the
@@ -1161,7 +1179,7 @@ def _sync(camera_state):
11611179

11621180
assert order == [
11631181
"camera",
1164-
("serialize", [RENDER_WINDOW_WASM_ID]),
1182+
("serialize", [RENDER_WINDOW_WASM_ID, *REGISTERED_WASM_IDS]),
11651183
"bridge",
11661184
]
11671185
assert observed["depth"] >= 1
@@ -1202,8 +1220,8 @@ def test_apply_state_serializes_the_camera_under_the_lock(scene):
12021220
# then ``self._renderer.serialize_camera_state()``, both inside
12031221
# ``_vtk_lock``. Same shape as the ``sync_camera`` trigger section above: the
12041222
# re-serialisation runs after the reset, is spied on
1205-
# ``_object_manager.UpdateStatesFromObjects`` with the render-window id, and
1206-
# the lock is held (depth >= 1) at both points.
1223+
# ``_object_manager.UpdateStatesFromObjects`` with the render-window id at the
1224+
# head of the root list, and the lock is held (depth >= 1) at both points.
12071225
#
12081226
# Own literals are unnecessary here -- reset_camera takes no camera argument,
12091227
# only the scene-graph bounds -- so what is pinned is order and lock depth,
@@ -1221,7 +1239,10 @@ def test_reset_camera_serializes_after_the_reset_with_the_render_window_id(scene
12211239
The spy appends ``"reset"`` for the reset call and the two-tuple
12221240
``("serialize", <ids>)`` for the re-serialisation; the tuple is the
12231241
recording format, not the argument. ``<ids>`` is asserted as the list
1224-
production passes, since ``UpdateStatesFromObjects`` takes a sequence.
1242+
production passes, since ``UpdateStatesFromObjects`` takes a sequence:
1243+
the render window at the head, keyed on identity, followed by the
1244+
separately registered objects that ``LocalView.update`` carries
1245+
alongside it.
12251246
"""
12261247
order = []
12271248
real_reset = scene._renderer.reset_camera
@@ -1237,7 +1258,10 @@ def _reset(bounds):
12371258

12381259
scene.reset_camera()
12391260

1240-
assert order == ["reset", ("serialize", [RENDER_WINDOW_WASM_ID])]
1261+
assert order == [
1262+
"reset",
1263+
("serialize", [RENDER_WINDOW_WASM_ID, *REGISTERED_WASM_IDS]),
1264+
]
12411265

12421266

12431267
def test_reset_camera_holds_the_lock_across_both_halves(scene):

0 commit comments

Comments
 (0)