From 38c5e9e4cbe52c8693b11608cd850385716845e1 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Fri, 28 Aug 2026 12:11:51 -0700 Subject: [PATCH 01/28] add per-part write path to the dataset registry --- .../viewer/vtk/datasets/visor_dataset.py | 14 +- .../vtk/datasets/visor_dataset_registry.py | 180 ++++++++++++++++- .../datasets/test_visor_dataset_registry.py | 190 ++++++++++++++++++ 3 files changed, 382 insertions(+), 2 deletions(-) diff --git a/src/ansys/visor/viewer/vtk/datasets/visor_dataset.py b/src/ansys/visor/viewer/vtk/datasets/visor_dataset.py index 8ba4b8d3..b91ff35a 100644 --- a/src/ansys/visor/viewer/vtk/datasets/visor_dataset.py +++ b/src/ansys/visor/viewer/vtk/datasets/visor_dataset.py @@ -88,7 +88,19 @@ def list_variables(self) -> List[VisorPartVariables]: return result def set_state(self, new_state: PersistedDatasetState) -> None: - """Set the state of the dataset.""" + """ + Set the state of the dataset from persisted, name-keyed part state. + + This is the persisted-state conversion path: it converts + new_state.parts (keyed by part name) into this dataset's runtime + state (keyed by part ID) via persisted_to_runtime_state. + + Note: a second, id-keyed replacement path also exists, on the + registry rather than here: VisorDatasetRegistry.replace_part_states + replaces a dataset's .state directly with an already-runtime, + id-keyed RuntimeDatasetState, without going through this method or + its name-to-id conversion. + """ self.state = self.persisted_to_runtime_state(new_state.parts) def mark_clean(self) -> None: diff --git a/src/ansys/visor/viewer/vtk/datasets/visor_dataset_registry.py b/src/ansys/visor/viewer/vtk/datasets/visor_dataset_registry.py index 0b7e4d90..1ada79a5 100644 --- a/src/ansys/visor/viewer/vtk/datasets/visor_dataset_registry.py +++ b/src/ansys/visor/viewer/vtk/datasets/visor_dataset_registry.py @@ -6,7 +6,10 @@ from ansys.visor.viewer.core.metadata import ExtendedMetadata from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger from ansys.visor.viewer.core.visor_types import VisorDatasetType -from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import RuntimeDatasetState +from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import ( + RuntimeDatasetState, + RuntimePartProperties, +) from ansys.visor.viewer.vtk.datasets.visor_dataset import VisorDataset from ansys.visor.viewer.vtk.variables.visor_part_variables import VisorPartVariables from ansys.visor.viewer.vtk.variables.visor_variable_update import VisorVariableUpdate @@ -106,6 +109,181 @@ def remove(self, dataset_id: int) -> None: if dataset_id in self.datasets: self.datasets.pop(dataset_id) + # ------------------------------------------------------------------ + # Per-part state: write path + # ------------------------------------------------------------------ + + def find_dataset_id_for_part(self, part_id: int) -> int | None: + """ + Find the ID of the dataset that owns the given part. + + Args: + part_id (int): The scene-graph node ID identifying the part. + + Returns: + int | None: The dataset ID that owns the part, or None if no + dataset in the registry has this part_id in its PartIndex. + """ + for dataset_id, dataset in self.datasets.items(): + if part_id in dataset.part_index.part_ids: + return dataset_id + return None + + def get_part_state(self, part_id: int) -> RuntimePartProperties | None: + """ + Get the current runtime state record for a single part. + + This is a read-only lookup: unlike the setters, it does not upsert + a record for a part that has no state yet. + + Args: + part_id (int): The scene-graph node ID identifying the part. + + Returns: + RuntimePartProperties | None: The live state record for the + part, or None if part_id resolves to no dataset, or the owning + dataset has no recorded state for it. + """ + dataset_id = self.find_dataset_id_for_part(part_id) + if dataset_id is None: + return None + return self.datasets[dataset_id].state.part_states.get(part_id) + + def _get_or_create_part_state(self, part_id: int) -> RuntimePartProperties | None: + """ + Resolve the live part-state record for part_id, upserting if needed. + + If the owning dataset has no part_states entry yet for part_id (e.g. + a freshly added dataset with no persisted part state), a + RuntimePartProperties(id=part_id) record is created and inserted + into the dataset's part_states dict first. This is mandatory: a + freshly added dataset has an empty part_states dict, so without + this upsert every setter would silently no-op for it. + + Returns: + RuntimePartProperties | None: The live state record, or None if + part_id resolves to no dataset. + """ + dataset_id = self.find_dataset_id_for_part(part_id) + if dataset_id is None: + return None + dataset = self.datasets[dataset_id] + part_state = dataset.state.part_states.get(part_id) + if part_state is None: + part_state = RuntimePartProperties(id=part_id) + dataset.state.part_states[part_id] = part_state + return part_state + + def set_part_visibility(self, part_id: int, visible: bool) -> bool: + """ + Set whether a part is visible. + + Returns: + bool: True when the record was written, False when part_id + resolves to no dataset. Never raises. + """ + part_state = self._get_or_create_part_state(part_id) + if part_state is None: + return False + part_state.visible = visible + return True + + def set_part_opacity(self, part_id: int, opacity: float) -> bool: + """ + Set a part's opacity. + + Returns: + bool: True when the record was written, False when part_id + resolves to no dataset. Never raises. + """ + part_state = self._get_or_create_part_state(part_id) + if part_state is None: + return False + part_state.opacity = opacity + return True + + def set_part_diffuse_color(self, part_id: int, diffuse_rgb: list[float] | None) -> bool: + """ + Set a part's custom diffuse colour, or clear it with None. + + Returns: + bool: True when the record was written, False when part_id + resolves to no dataset. Never raises. + """ + part_state = self._get_or_create_part_state(part_id) + if part_state is None: + return False + part_state.diffuse_rgb = diffuse_rgb + return True + + def set_part_selected(self, part_id: int, selected: bool) -> bool: + """ + Set whether a part is selected. + + Returns: + bool: True when the record was written, False when part_id + resolves to no dataset. Never raises. + """ + part_state = self._get_or_create_part_state(part_id) + if part_state is None: + return False + part_state.selected = selected + return True + + def set_part_color_variable(self, part_id: int, variable_id: str, component: int | None) -> bool: + """ + Set the variable a part is coloured by, and its component. + + variable_id and component are set together, atomically, in this one + call, mirroring clear_part_color_variable's atomic clear. + + Returns: + bool: True when the record was written, False when part_id + resolves to no dataset. Never raises. + """ + part_state = self._get_or_create_part_state(part_id) + if part_state is None: + return False + part_state.spectrum_id = variable_id + part_state.spectrum_component = component + return True + + def clear_part_color_variable(self, part_id: int) -> bool: + """ + Clear the variable a part is coloured by. + + Sets spectrum_id and spectrum_component to None together, in one + call — the compound class is only ever set or cleared atomically, + never field by field. + + Returns: + bool: True when the record was written, False when part_id + resolves to no dataset. Never raises. + """ + part_state = self._get_or_create_part_state(part_id) + if part_state is None: + return False + part_state.spectrum_id = None + part_state.spectrum_component = None + return True + + def replace_part_states(self, dataset_states: Dict[int, RuntimeDatasetState]) -> None: + """ + Replace the runtime state of registered datasets in bulk. + + For each dataset ID present in both dataset_states and the + registry, that dataset's .state is replaced directly with the + supplied RuntimeDatasetState (already-runtime, id-keyed input). + Dataset IDs in dataset_states that are not present in the registry + are skipped silently; processing continues for the remaining + entries. Never raises. + """ + for dataset_id, runtime_state in dataset_states.items(): + dataset = self.datasets.get(dataset_id) + if dataset is None: + continue + dataset.state = runtime_state + def _get_unique_dataset_name(self, name: str) -> str: """ Rename the dataset by appending a suffix to ensure uniqueness. diff --git a/tests/unit/vtk/datasets/test_visor_dataset_registry.py b/tests/unit/vtk/datasets/test_visor_dataset_registry.py index d55ee2d6..15296e9e 100644 --- a/tests/unit/vtk/datasets/test_visor_dataset_registry.py +++ b/tests/unit/vtk/datasets/test_visor_dataset_registry.py @@ -3,6 +3,10 @@ import pytest +from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import ( + RuntimeDatasetState, + RuntimePartProperties, +) from ansys.visor.viewer.vtk.datasets.visor_dataset_registry import VisorDatasetRegistry @@ -23,6 +27,19 @@ def make_mock_dataset(name="ds", info_dict=None, parts=None, variables=None): return mock +def make_part_dataset(dataset_id, part_ids, part_states=None): + """ + Build a dataset stand-in with a real PartIndex.part_ids list and a real + RuntimeDatasetState (not MagicMock), so the per-part write path can + mutate and be read back through actual object identity. + """ + dataset = MagicMock() + dataset.part_index = MagicMock() + dataset.part_index.part_ids = list(part_ids) + dataset.state = RuntimeDatasetState(id=dataset_id, part_states=part_states or {}) + return dataset + + def test_count_returns_number_of_datasets(registry): """Verify that count returns the number of registered datasets.""" assert registry.count == 0 @@ -180,3 +197,176 @@ def test_update_unit_matches_keeps_unit(registry): registry._update_unit(metadata) assert registry.unit == "m" + + +# ================================================================== # +# Per-part write path (Story 3.1, increment I1) +# ================================================================== # + +def test_find_dataset_id_for_part_selects_between_multiple_datasets(registry): + """Verify the correct dataset id is returned when several datasets are registered.""" + ds_a = make_part_dataset(dataset_id=1, part_ids=[10, 11]) + ds_b = make_part_dataset(dataset_id=2, part_ids=[20, 21]) + registry.datasets = {1: ds_a, 2: ds_b} + + assert registry.find_dataset_id_for_part(20) == 2 + assert registry.find_dataset_id_for_part(11) == 1 + + +def test_find_dataset_id_for_part_not_found_returns_none(registry): + """Verify an unknown part_id resolves to no dataset.""" + ds_a = make_part_dataset(dataset_id=1, part_ids=[10, 11]) + registry.datasets = {1: ds_a} + + assert registry.find_dataset_id_for_part(999999) is None + + +def test_get_part_state_returns_existing_record(registry): + """Verify get_part_state returns the recorded state for a known part.""" + existing = RuntimePartProperties(id=10, opacity=0.7) + ds_a = make_part_dataset(dataset_id=1, part_ids=[10], part_states={10: existing}) + registry.datasets = {1: ds_a} + + result = registry.get_part_state(10) + assert result is existing + assert result.opacity == 0.7 + + +def test_get_part_state_known_dataset_missing_part_returns_none(registry): + """Verify get_part_state does not upsert: a part with no record returns None.""" + ds_a = make_part_dataset(dataset_id=1, part_ids=[10]) + registry.datasets = {1: ds_a} + + assert registry.get_part_state(10) is None + assert ds_a.state.part_states == {} + + +def test_get_part_state_unknown_part_id_returns_none(registry): + """Verify get_part_state returns None, without raising, for an unknown part_id.""" + ds_a = make_part_dataset(dataset_id=1, part_ids=[10]) + registry.datasets = {1: ds_a} + + assert registry.get_part_state(999999) is None + + +def test_set_part_visibility_mutates_record_and_rejects_unknown_part(registry): + """Verify set_part_visibility writes True/False, and False + no raise for unknown part_id.""" + ds_a = make_part_dataset(dataset_id=1, part_ids=[10], part_states={10: RuntimePartProperties(id=10)}) + registry.datasets = {1: ds_a} + + assert registry.set_part_visibility(10, False) is True + assert registry.get_part_state(10).visible is False + + assert registry.set_part_visibility(999999, True) is False + + +def test_set_part_opacity_mutates_record_and_rejects_unknown_part(registry): + """Verify set_part_opacity writes the value, and False + no raise for unknown part_id.""" + ds_a = make_part_dataset(dataset_id=1, part_ids=[10], part_states={10: RuntimePartProperties(id=10)}) + registry.datasets = {1: ds_a} + + assert registry.set_part_opacity(10, 0.4) is True + assert registry.get_part_state(10).opacity == 0.4 + + assert registry.set_part_opacity(999999, 0.4) is False + + +def test_set_part_diffuse_color_mutates_and_clears(registry): + """Verify set_part_diffuse_color writes an RGB list, clears with None, and rejects unknown parts.""" + ds_a = make_part_dataset( + dataset_id=1, part_ids=[10], + part_states={10: RuntimePartProperties(id=10, diffuse_rgb=[1.0, 0.0, 0.0])}, + ) + registry.datasets = {1: ds_a} + + assert registry.set_part_diffuse_color(10, [0.0, 1.0, 0.0]) is True + assert registry.get_part_state(10).diffuse_rgb == [0.0, 1.0, 0.0] + + assert registry.set_part_diffuse_color(10, None) is True + assert registry.get_part_state(10).diffuse_rgb is None + + assert registry.set_part_diffuse_color(999999, [1.0, 1.0, 1.0]) is False + + +def test_set_part_selected_mutates_record_and_rejects_unknown_part(registry): + """Verify set_part_selected writes the value, and False + no raise for unknown part_id.""" + ds_a = make_part_dataset(dataset_id=1, part_ids=[10], part_states={10: RuntimePartProperties(id=10)}) + registry.datasets = {1: ds_a} + + assert registry.set_part_selected(10, True) is True + assert registry.get_part_state(10).selected is True + + assert registry.set_part_selected(999999, True) is False + + +def test_set_part_color_variable_sets_id_and_component_together(registry): + """Verify set_part_color_variable sets spectrum_id and spectrum_component in one call.""" + seed = RuntimePartProperties(id=10) + assert seed.spectrum_id is None + assert seed.spectrum_component is None + ds_a = make_part_dataset(dataset_id=1, part_ids=[10], part_states={10: seed}) + registry.datasets = {1: ds_a} + + assert registry.set_part_color_variable(10, "POINT::pressure::1", 2) is True + + result = registry.get_part_state(10) + assert result.spectrum_id == "POINT::pressure::1" + assert result.spectrum_component == 2 + + assert registry.set_part_color_variable(999999, "POINT::x::1", 0) is False + + +def test_clear_part_color_variable_clears_id_and_component_together(registry): + """Verify clear_part_color_variable clears spectrum_id and spectrum_component in one call.""" + seed = RuntimePartProperties(id=10, spectrum_id="POINT::pressure::1", spectrum_component=2) + ds_a = make_part_dataset(dataset_id=1, part_ids=[10], part_states={10: seed}) + registry.datasets = {1: ds_a} + + assert registry.clear_part_color_variable(10) is True + + result = registry.get_part_state(10) + assert result.spectrum_id is None + assert result.spectrum_component is None + + assert registry.clear_part_color_variable(999999) is False + + +def test_setter_upserts_part_state_when_part_id_known_but_absent_from_part_states(registry): + """ + Verify the upsert path: a part_id present in PartIndex.part_ids but absent + from part_states gets a RuntimePartProperties(id=part_id) created on first + write, rather than silently no-op-ing. + """ + ds_a = make_part_dataset(dataset_id=1, part_ids=[10], part_states={}) + registry.datasets = {1: ds_a} + assert ds_a.state.part_states == {} + + assert registry.set_part_opacity(10, 0.6) is True + + created = ds_a.state.part_states.get(10) + assert created is not None + assert isinstance(created, RuntimePartProperties) + assert created.id == 10 + assert created.opacity == 0.6 + + +def test_replace_part_states_replaces_known_and_skips_unknown_without_aborting(registry): + """ + Verify replace_part_states replaces the state of a known dataset id and + silently skips an unknown dataset id in the same call, without aborting + partway (the known replacement still lands). + """ + ds_a = make_part_dataset(dataset_id=1, part_ids=[10]) + original_state = ds_a.state + registry.datasets = {1: ds_a} + + new_state_for_known = RuntimeDatasetState(id=1, part_states={10: RuntimePartProperties(id=10, opacity=0.9)}) + new_state_for_unknown = RuntimeDatasetState(id=999, part_states={}) + + registry.replace_part_states({1: new_state_for_known, 999: new_state_for_unknown}) + + assert ds_a.state is new_state_for_known + assert ds_a.state is not original_state + assert 999 not in registry.datasets + + From ac71d50afe0ec18df6e323b1d079a49cf318e00b Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Fri, 28 Aug 2026 12:44:15 -0700 Subject: [PATCH 02/28] remove story/increment reference in comment --- tests/unit/vtk/datasets/test_visor_dataset_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/vtk/datasets/test_visor_dataset_registry.py b/tests/unit/vtk/datasets/test_visor_dataset_registry.py index 15296e9e..19047df9 100644 --- a/tests/unit/vtk/datasets/test_visor_dataset_registry.py +++ b/tests/unit/vtk/datasets/test_visor_dataset_registry.py @@ -200,7 +200,7 @@ def test_update_unit_matches_keeps_unit(registry): # ================================================================== # -# Per-part write path (Story 3.1, increment I1) +# Per-part write path # ================================================================== # def test_find_dataset_id_for_part_selects_between_multiple_datasets(registry): From ee0b843048017803824b01dd20354cff01e7082d Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:46:24 +0000 Subject: [PATCH 03/28] chore: adding changelog file 50.added.md [dependabot-skip] --- doc/changelog.d/50.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/50.added.md diff --git a/doc/changelog.d/50.added.md b/doc/changelog.d/50.added.md new file mode 100644 index 00000000..947c8ced --- /dev/null +++ b/doc/changelog.d/50.added.md @@ -0,0 +1 @@ +Remote rendering 3.1 - add per-part write path to the dataset registry From ff688aa122dd96fe2b38e8ea8dc8d951e7cc4457 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 28 Aug 2026 20:02:36 +0000 Subject: [PATCH 04/28] chore: adding changelog file 50.added.md [dependabot-skip] --- doc/changelog.d/50.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changelog.d/50.added.md b/doc/changelog.d/50.added.md index 947c8ced..86f36f04 100644 --- a/doc/changelog.d/50.added.md +++ b/doc/changelog.d/50.added.md @@ -1 +1 @@ -Remote rendering 3.1 - add per-part write path to the dataset registry +Remote rendering 3.1a - add per-part write path to the dataset registry From ccc2f0f78fa4df1ad843f126276b25c4185925a7 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Fri, 28 Aug 2026 12:18:46 -0700 Subject: [PATCH 05/28] implement visibility, opacity and diffuse-color applies on the local renderer --- .../visor/viewer/renderer/local_renderer.py | 39 +++- tests/unit/renderer/test_local_renderer.py | 167 ++++++++++++++++-- 2 files changed, 191 insertions(+), 15 deletions(-) diff --git a/src/ansys/visor/viewer/renderer/local_renderer.py b/src/ansys/visor/viewer/renderer/local_renderer.py index 4d07003a..b334fa3d 100644 --- a/src/ansys/visor/viewer/renderer/local_renderer.py +++ b/src/ansys/visor/viewer/renderer/local_renderer.py @@ -158,15 +158,48 @@ def deregister_all(self) -> None: # ------------------------------------------------------------------ def apply_visibility(self, node_id: int, visible: bool) -> None: - """No-op in Story 1.2. Phase 3 populates.""" + """See :meth:`IRenderer.apply_visibility`. + + Mutates the actor itself, not its property. An unknown *node_id* + is a logged no-op, never a raise. + """ + pipe = self._pipelines.get(node_id) + if pipe is None: + logger.debug( + "apply_visibility: no pipeline for node %s; skipping.", node_id + ) + return + pipe.actor.SetVisibility(1 if visible else 0) def apply_opacity(self, node_id: int, opacity: float) -> None: - """No-op in Story 1.2. Phase 3 populates.""" + """See :meth:`IRenderer.apply_opacity`. + + Mutates the actor's property. An unknown *node_id* is a logged + no-op, never a raise. + """ + pipe = self._pipelines.get(node_id) + if pipe is None: + logger.debug( + "apply_opacity: no pipeline for node %s; skipping.", node_id + ) + return + pipe.actor.GetProperty().SetOpacity(opacity) def apply_diffuse_color( self, node_id: int, r: float, g: float, b: float ) -> None: - """No-op in Story 1.2. Phase 3 populates.""" + """See :meth:`IRenderer.apply_diffuse_color`. + + Mutates the actor property's diffuse colour only. An unknown + *node_id* is a logged no-op, never a raise. + """ + pipe = self._pipelines.get(node_id) + if pipe is None: + logger.debug( + "apply_diffuse_color: no pipeline for node %s; skipping.", node_id + ) + return + pipe.actor.GetProperty().SetDiffuseColor(r, g, b) def apply_edge_visibility(self, node_id: int, edge_visible: bool) -> None: """No-op in Story 1.2. Phase 3 populates.""" diff --git a/tests/unit/renderer/test_local_renderer.py b/tests/unit/renderer/test_local_renderer.py index c0bd33ae..038959ee 100644 --- a/tests/unit/renderer/test_local_renderer.py +++ b/tests/unit/renderer/test_local_renderer.py @@ -6,8 +6,9 @@ 1. Node lifecycle -- register_node / deregister_node (the non-trivial seam). 2. Interface conformance -- NullRenderer and VisorLocalRenderer both satisfy IRenderer's abstract contract. -3. Per-part visual mutations -- one focused test per property; - tests assert they accept their arguments and return None. +3. Per-part visual mutations -- visibility, opacity and diffuse colour + mutate the resolved pipeline's VTK objects; the remaining methods are + still no-ops and are asserted to accept their arguments and return None. 4. Camera round-trip -- reset_camera, sync_camera / get_camera_state. 5. Render / flush delegation. 6. pick_geometry -- vertex, edge, face modes. @@ -18,6 +19,7 @@ from unittest.mock import MagicMock, patch import pytest +from vtkmodules.vtkRenderingCore import vtkActor from ansys.visor.viewer.renderer.base import IRenderer from ansys.visor.viewer.renderer.local_renderer import VisorLocalRenderer @@ -92,6 +94,20 @@ def GetNextActor(self): # noqa: N802 return a +class _DummyPipeline: + """VtkNodePipeline stand-in holding a *real* vtkActor. + + The actor is real so that a test can read the mutated value back off + the VTK object (``GetVisibility``, ``GetProperty().GetOpacity()``, + ``GetProperty().GetDiffuseColor()``) rather than merely recording that + some call was made -- a MagicMock would pass even if the production + code mutated the wrong object or the wrong field. + """ + + def __init__(self): + self.actor = vtkActor() + + # --------------------------------------------------------------------------- # Fixture: VisorLocalRenderer with all VTK infrastructure mocked # --------------------------------------------------------------------------- @@ -336,16 +352,7 @@ def test_deregister_all_empties_registry_and_detaches_all_actors(self, renderer) # =========================================================================== class TestPerPartMutations: - """Each method accepts its contract arguments and returns None without raising.""" - - def test_apply_visibility(self, renderer): - assert renderer.apply_visibility(1, True) is None - - def test_apply_opacity(self, renderer): - assert renderer.apply_opacity(1, 0.5) is None - - def test_apply_diffuse_color(self, renderer): - assert renderer.apply_diffuse_color(1, 1.0, 0.0, 0.0) is None + """The methods still un-implemented accept their arguments and return None.""" def test_apply_edge_visibility(self, renderer): assert renderer.apply_edge_visibility(1, False) is None @@ -369,6 +376,142 @@ def test_refresh_color_variable_range(self, renderer): ) +# =========================================================================== +# 3b. Inline apply bodies: visibility, opacity, diffuse colour +# +# Each mutation is read back off a real vtkActor, and each miss branch +# (unknown node id) is asserted separately. +# =========================================================================== + +class TestInlineApplyBodies: + + # ------------------------------------------------------------------ + # visibility -- mutates the actor itself + # ------------------------------------------------------------------ + + def test_apply_visibility_shows_actor(self, renderer): + """apply_visibility(True) leaves the actor's visibility flag set.""" + pipe = _DummyPipeline() + pipe.actor.SetVisibility(0) + renderer._pipelines[4] = pipe + + renderer.apply_visibility(4, True) + + assert pipe.actor.GetVisibility() == 1 + + def test_apply_visibility_hides_actor(self, renderer): + """apply_visibility(False) leaves the actor's visibility flag clear.""" + pipe = _DummyPipeline() + pipe.actor.SetVisibility(1) + renderer._pipelines[4] = pipe + + renderer.apply_visibility(4, False) + + assert pipe.actor.GetVisibility() == 0 + + def test_apply_visibility_unknown_node_id_is_logged_no_op(self, renderer): + """An unregistered node id logs at debug, does not raise, mutates nothing.""" + pipe = _DummyPipeline() + pipe.actor.SetVisibility(1) + renderer._pipelines[4] = pipe + + with patch( + "ansys.visor.viewer.renderer.local_renderer.logger" + ) as mock_logger: + renderer.apply_visibility(9999, False) # must not raise + + mock_logger.debug.assert_called_once() + assert pipe.actor.GetVisibility() == 1 + + # ------------------------------------------------------------------ + # opacity -- mutates the actor's property + # ------------------------------------------------------------------ + + def test_apply_opacity_sets_property_opacity(self, renderer): + """apply_opacity writes the requested value onto the actor property.""" + pipe = _DummyPipeline() + renderer._pipelines[4] = pipe + + renderer.apply_opacity(4, 0.25) + + assert pipe.actor.GetProperty().GetOpacity() == pytest.approx(0.25) + + def test_apply_opacity_does_not_touch_visibility_or_diffuse_color(self, renderer): + """Opacity lands on the property's opacity field and nothing else.""" + pipe = _DummyPipeline() + pipe.actor.SetVisibility(0) + pipe.actor.GetProperty().SetDiffuseColor(0.25, 0.5, 0.75) + renderer._pipelines[4] = pipe + + renderer.apply_opacity(4, 0.25) + + assert pipe.actor.GetVisibility() == 0 + assert pipe.actor.GetProperty().GetDiffuseColor() == pytest.approx( + (0.25, 0.5, 0.75) + ) + + def test_apply_opacity_unknown_node_id_is_logged_no_op(self, renderer): + """An unregistered node id logs at debug, does not raise, mutates nothing.""" + pipe = _DummyPipeline() + pipe.actor.GetProperty().SetOpacity(0.75) + renderer._pipelines[4] = pipe + + with patch( + "ansys.visor.viewer.renderer.local_renderer.logger" + ) as mock_logger: + renderer.apply_opacity(9999, 0.25) # must not raise + + mock_logger.debug.assert_called_once() + assert pipe.actor.GetProperty().GetOpacity() == pytest.approx(0.75) + + # ------------------------------------------------------------------ + # diffuse colour -- mutates the actor property's DiffuseColor + # ------------------------------------------------------------------ + + def test_apply_diffuse_color_sets_property_diffuse_color(self, renderer): + """apply_diffuse_color writes r, g, b onto the property's diffuse colour.""" + pipe = _DummyPipeline() + renderer._pipelines[4] = pipe + + renderer.apply_diffuse_color(4, 1.0, 0.0, 0.0) + + assert pipe.actor.GetProperty().GetDiffuseColor() == pytest.approx( + (1.0, 0.0, 0.0) + ) + + def test_apply_diffuse_color_does_not_touch_ambient_color_or_opacity( + self, renderer + ): + """SetDiffuseColor, not SetColor or SetAmbientColor, and opacity is left alone.""" + pipe = _DummyPipeline() + pipe.actor.GetProperty().SetAmbientColor(0.25, 0.5, 0.75) + pipe.actor.GetProperty().SetOpacity(0.75) + renderer._pipelines[4] = pipe + + renderer.apply_diffuse_color(4, 1.0, 0.0, 0.0) + + assert pipe.actor.GetProperty().GetAmbientColor() == pytest.approx( + (0.25, 0.5, 0.75) + ) + assert pipe.actor.GetProperty().GetOpacity() == pytest.approx(0.75) + + def test_apply_diffuse_color_unknown_node_id_is_logged_no_op(self, renderer): + """An unregistered node id logs at debug, does not raise, mutates nothing.""" + pipe = _DummyPipeline() + pipe.actor.GetProperty().SetDiffuseColor(0.25, 0.5, 0.75) + renderer._pipelines[4] = pipe + + with patch( + "ansys.visor.viewer.renderer.local_renderer.logger" + ) as mock_logger: + renderer.apply_diffuse_color(9999, 1.0, 0.0, 0.0) # must not raise + + mock_logger.debug.assert_called_once() + assert pipe.actor.GetProperty().GetDiffuseColor() == pytest.approx( + (0.25, 0.5, 0.75) + ) + + # =========================================================================== # 4. Camera # =========================================================================== From c62294fcc98389e9adeab99d62f74ee61326658b Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Fri, 28 Aug 2026 12:19:37 -0700 Subject: [PATCH 06/28] move selection and color-variable applies onto VtkNodePipeline --- .../visor/viewer/renderer/local_renderer.py | 49 +++- src/ansys/visor/viewer/vtk/node_pipeline.py | 97 ++++++++ tests/unit/renderer/test_local_renderer.py | 127 ++++++++-- tests/unit/vtk/test_node_pipeline.py | 226 +++++++++++++++++- 4 files changed, 481 insertions(+), 18 deletions(-) diff --git a/src/ansys/visor/viewer/renderer/local_renderer.py b/src/ansys/visor/viewer/renderer/local_renderer.py index b334fa3d..47913992 100644 --- a/src/ansys/visor/viewer/renderer/local_renderer.py +++ b/src/ansys/visor/viewer/renderer/local_renderer.py @@ -207,7 +207,21 @@ def apply_edge_visibility(self, node_id: int, edge_visible: bool) -> None: def apply_selected( self, node_id: int, selected: bool, diffuse_rgb: list ) -> None: - """No-op in Story 1.2. Phase 3 populates.""" + """See :meth:`IRenderer.apply_selected`. + + Resolves the pipeline and delegates to + :meth:`VtkNodePipeline.set_selected`. *diffuse_rgb* is passed + through as given; supplying a default when the part has no stored + colour is the coordinator's job, not the renderer's. An unknown + *node_id* is a logged no-op, never a raise. + """ + pipe = self._pipelines.get(node_id) + if pipe is None: + logger.debug( + "apply_selected: no pipeline for node %s; skipping.", node_id + ) + return + pipe.set_selected(selected, diffuse_rgb) def apply_color_variable( self, @@ -219,10 +233,39 @@ def apply_color_variable( min_val: float, max_val: float, ) -> None: - """No-op in Story 1.2. Phase 3 populates.""" + """See :meth:`IRenderer.apply_color_variable`. + + Resolves the pipeline and delegates to + :meth:`VtkNodePipeline.set_color_variable`. *array_type* must + already be a :class:`VisorVtkVariableType`; it is parsed at the + trigger boundary, never here, and the pipeline compares it by + identity, so any other value is a logged no-op there. + *spectrum_id* is not forwarded -- it is stored opaquely by the + registry and is not needed to configure the mapper. An unknown + *node_id* is a logged no-op, never a raise. + """ + pipe = self._pipelines.get(node_id) + if pipe is None: + logger.debug( + "apply_color_variable: no pipeline for node %s; skipping.", node_id + ) + return + pipe.set_color_variable(array_type, array_name, component, min_val, max_val) def clear_color_variable(self, node_id: int) -> None: - """No-op in Story 1.2. Phase 3 populates.""" + """See :meth:`IRenderer.clear_color_variable`. + + Resolves the pipeline and delegates to + :meth:`VtkNodePipeline.clear_color_variable`. An unknown + *node_id* is a logged no-op, never a raise. + """ + pipe = self._pipelines.get(node_id) + if pipe is None: + logger.debug( + "clear_color_variable: no pipeline for node %s; skipping.", node_id + ) + return + pipe.clear_color_variable() def refresh_color_variable_range( self, diff --git a/src/ansys/visor/viewer/vtk/node_pipeline.py b/src/ansys/visor/viewer/vtk/node_pipeline.py index 7adeb5ad..1e97dcc1 100644 --- a/src/ansys/visor/viewer/vtk/node_pipeline.py +++ b/src/ansys/visor/viewer/vtk/node_pipeline.py @@ -27,6 +27,7 @@ from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper from ansys.visor.viewer.core.visor_colors import VisorColors +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger logger = VisorDefaultLogger(__name__) @@ -105,6 +106,102 @@ def update_input( algorithm = algorithm_filter(algorithm) self.mapper.SetInputConnection(algorithm.GetOutputPort()) + # ------------------------------------------------------------------ + # Per-part visual mutations + # + # These bodies live here rather than on the renderer because each is + # more than one VTK call, and the colour-variable body additionally + # reads a pipeline-internal object (``base_algorithm``). Identity + # resolution (node id -> pipeline) and the miss branch stay on the + # renderer, which is the only holder of the id -> pipeline map. + # ------------------------------------------------------------------ + + def set_selected(self, selected: bool, diffuse_rgb: list[float]) -> None: + """Apply or remove the selection highlight on this part. + + Parameters + ---------- + selected: + Target state. Absolute, never a toggle. + diffuse_rgb: + The part's diffuse colour, re-applied unconditionally on both + branches -- selection changes the ambient/diffuse lighting + terms, it does not replace the part's colour. + """ + prop = self.actor.GetProperty() + if selected: + prop.SetAmbientColor(0 / 255, 62 / 255, 111 / 255) + prop.SetDiffuse(0.5) + prop.SetAmbient(0.5) + else: + prop.SetDiffuse(1.0) + prop.SetAmbient(0.0) + prop.SetDiffuseColor(*diffuse_rgb) + + def set_color_variable( + self, + association: VisorVtkVariableType, + array_name: str, + component: int, + min_val: float, + max_val: float, + ) -> None: + """Colour this part by a scalar array, over an explicit range. + + Configures the mapper only. No lookup table is authored here: the + table belongs to a later story, and until then a reference resolves + against the client-held default table. + + ``association`` is compared by identity against + :class:`VisorVtkVariableType`; it is never parsed, upper-cased or + string-compared. A value that is neither member, and an array name + that does not exist on the input, are both logged no-ops that + mutate nothing. + """ + in_data = self.base_algorithm.GetInput() + if association is VisorVtkVariableType.POINT: + field = in_data.GetPointData() + elif association is VisorVtkVariableType.CELL: + field = in_data.GetCellData() + else: + logger.warning( + "set_color_variable: association %r is not a VisorVtkVariableType; " + "skipping.", + association, + ) + return + + if field.GetArray(array_name) is None: + logger.warning( + "set_color_variable: array %r not found for association %s; skipping.", + array_name, + association, + ) + return + + mapper = self.mapper + if association is VisorVtkVariableType.POINT: + mapper.SetScalarModeToUsePointFieldData() + else: + mapper.SetScalarModeToUseCellFieldData() + mapper.SelectColorArray(array_name) + mapper.SetArrayComponent(component) + mapper.SetScalarRange(min_val, max_val) + mapper.SetColorModeToMapScalars() + mapper.SetScalarVisibility(True) + # Makes the mapper honour the range set above rather than the + # lookup table's own range. + mapper.SetUseLookupTableScalarRange(0) + + def clear_color_variable(self) -> None: + """Stop colouring this part by a scalar array. + + One call. Does not touch a lookup table and does not restore a + diffuse colour -- the part's colour is whatever was last applied + to it. + """ + self.mapper.SetScalarVisibility(False) + # ------------------------------------------------------------------ # Private helpers # ------------------------------------------------------------------ diff --git a/tests/unit/renderer/test_local_renderer.py b/tests/unit/renderer/test_local_renderer.py index 038959ee..8fc9bfb2 100644 --- a/tests/unit/renderer/test_local_renderer.py +++ b/tests/unit/renderer/test_local_renderer.py @@ -7,8 +7,10 @@ 2. Interface conformance -- NullRenderer and VisorLocalRenderer both satisfy IRenderer's abstract contract. 3. Per-part visual mutations -- visibility, opacity and diffuse colour - mutate the resolved pipeline's VTK objects; the remaining methods are - still no-ops and are asserted to accept their arguments and return None. + mutate the resolved pipeline's VTK objects directly; selection and + colour variable resolve the pipeline and delegate to VtkNodePipeline + (their VTK effects are asserted in tests/unit/vtk/test_node_pipeline.py); + edge visibility and the colour-variable range refresh stay no-ops. 4. Camera round-trip -- reset_camera, sync_camera / get_camera_state. 5. Render / flush delegation. 6. pick_geometry -- vertex, edge, face modes. @@ -21,6 +23,7 @@ import pytest from vtkmodules.vtkRenderingCore import vtkActor +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType from ansys.visor.viewer.renderer.base import IRenderer from ansys.visor.viewer.renderer.local_renderer import VisorLocalRenderer from ansys.visor.viewer.renderer.null_renderer import NullRenderer @@ -102,10 +105,18 @@ class _DummyPipeline: ``GetProperty().GetDiffuseColor()``) rather than merely recording that some call was made -- a MagicMock would pass even if the production code mutated the wrong object or the wrong field. + + The three delegated mutations are MagicMocks, because from this module + the behaviour under test is *that the renderer resolved and delegated*. + Their VTK effects are asserted against real objects in + ``tests/unit/vtk/test_node_pipeline.py``. """ def __init__(self): self.actor = vtkActor() + self.set_selected = MagicMock(name="set_selected") + self.set_color_variable = MagicMock(name="set_color_variable") + self.clear_color_variable = MagicMock(name="clear_color_variable") # --------------------------------------------------------------------------- @@ -352,22 +363,15 @@ def test_deregister_all_empties_registry_and_detaches_all_actors(self, renderer) # =========================================================================== class TestPerPartMutations: - """The methods still un-implemented accept their arguments and return None.""" + """The two permanently un-implemented methods accept their arguments. + + Both stay no-ops beyond this story: edge visibility is a global display + toggle, and the colour-variable range is not held per part. + """ def test_apply_edge_visibility(self, renderer): assert renderer.apply_edge_visibility(1, False) is None - def test_apply_selected(self, renderer): - assert renderer.apply_selected(1, True, [1.0, 0.0, 0.0]) is None - - def test_apply_color_variable(self, renderer): - assert ( - renderer.apply_color_variable(1, "sp-1", "POINT", "pressure", -1, 0.0, 1.0) - is None - ) - - def test_clear_color_variable(self, renderer): - assert renderer.clear_color_variable(1) is None def test_refresh_color_variable_range(self, renderer): assert ( @@ -512,6 +516,101 @@ def test_apply_diffuse_color_unknown_node_id_is_logged_no_op(self, renderer): ) +# =========================================================================== +# 3c. Delegated apply bodies: selection, colour variable +# +# The VTK effects of these live on VtkNodePipeline and are asserted in +# tests/unit/vtk/test_node_pipeline.py. What is asserted here is that +# the renderer resolved the pipeline and delegated with the arguments +# it was given, and that an unknown node id is a logged no-op. +# =========================================================================== + +class TestDelegatedApplyBodies: + + # ------------------------------------------------------------------ + # apply_selected + # ------------------------------------------------------------------ + + def test_apply_selected_delegates_to_pipeline(self, renderer): + """Selection state and the given colour are passed straight through.""" + pipe = _DummyPipeline() + renderer._pipelines[4] = pipe + + renderer.apply_selected(4, True, [1.0, 0.0, 0.0]) + + pipe.set_selected.assert_called_once_with(True, [1.0, 0.0, 0.0]) + + def test_apply_selected_unknown_node_id_is_logged_no_op(self, renderer): + """An unregistered node id logs at debug, does not raise, delegates nothing.""" + pipe = _DummyPipeline() + renderer._pipelines[4] = pipe + + with patch( + "ansys.visor.viewer.renderer.local_renderer.logger" + ) as mock_logger: + renderer.apply_selected(9999, True, [1.0, 0.0, 0.0]) # must not raise + + mock_logger.debug.assert_called_once() + pipe.set_selected.assert_not_called() + + # ------------------------------------------------------------------ + # apply_color_variable + # ------------------------------------------------------------------ + + def test_apply_color_variable_delegates_with_given_arguments(self, renderer): + """The association is forwarded unchanged; spectrum_id is not forwarded.""" + pipe = _DummyPipeline() + renderer._pipelines[4] = pipe + + renderer.apply_color_variable( + 4, "sp-1", VisorVtkVariableType.POINT, "pressure", 1, 0.0, 7.5 + ) + + pipe.set_color_variable.assert_called_once_with( + VisorVtkVariableType.POINT, "pressure", 1, 0.0, 7.5 + ) + + def test_apply_color_variable_unknown_node_id_is_logged_no_op(self, renderer): + """An unregistered node id logs at debug, does not raise, delegates nothing.""" + pipe = _DummyPipeline() + renderer._pipelines[4] = pipe + + with patch( + "ansys.visor.viewer.renderer.local_renderer.logger" + ) as mock_logger: + renderer.apply_color_variable( + 9999, "sp-1", VisorVtkVariableType.POINT, "pressure", 1, 0.0, 7.5 + ) # must not raise + + mock_logger.debug.assert_called_once() + pipe.set_color_variable.assert_not_called() + + # ------------------------------------------------------------------ + # clear_color_variable + # ------------------------------------------------------------------ + + def test_clear_color_variable_delegates_to_pipeline(self, renderer): + pipe = _DummyPipeline() + renderer._pipelines[4] = pipe + + renderer.clear_color_variable(4) + + pipe.clear_color_variable.assert_called_once_with() + + def test_clear_color_variable_unknown_node_id_is_logged_no_op(self, renderer): + """An unregistered node id logs at debug, does not raise, delegates nothing.""" + pipe = _DummyPipeline() + renderer._pipelines[4] = pipe + + with patch( + "ansys.visor.viewer.renderer.local_renderer.logger" + ) as mock_logger: + renderer.clear_color_variable(9999) # must not raise + + mock_logger.debug.assert_called_once() + pipe.clear_color_variable.assert_not_called() + + # =========================================================================== # 4. Camera # =========================================================================== diff --git a/tests/unit/vtk/test_node_pipeline.py b/tests/unit/vtk/test_node_pipeline.py index 8443ca7a..96115c51 100644 --- a/tests/unit/vtk/test_node_pipeline.py +++ b/tests/unit/vtk/test_node_pipeline.py @@ -1,8 +1,9 @@ """Unit tests for VtkNodePipeline.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest +from vtkmodules.vtkCommonCore import vtkFloatArray from vtkmodules.vtkCommonDataModel import vtkPlane, vtkPolyData, vtkUnstructuredGrid from vtkmodules.vtkFiltersCore import vtkAppendPolyData from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter @@ -10,6 +11,7 @@ from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper from ansys.visor.viewer.core.visor_colors import VisorColors +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType from ansys.visor.viewer.vtk.node_pipeline import VtkNodePipeline @@ -26,6 +28,38 @@ def unstructured_dataset() -> vtkUnstructuredGrid: return vtkUnstructuredGrid() +@pytest.fixture +def array_dataset() -> vtkPolyData: + """Sphere output carrying one named point array and one named cell array. + + Each array is sized from the dataset's own counts -- the point array to + ``GetNumberOfPoints()`` and the cell array to ``GetNumberOfCells()`` -- so + the arrays are valid for the association they are attached to. For the + default vtkSphereSource that is 50 points and 96 cells. + """ + src = vtkSphereSource() + src.Update() + dataset = src.GetOutput() + + pressure = vtkFloatArray() + pressure.SetName("pressure") + pressure.SetNumberOfComponents(1) + pressure.SetNumberOfTuples(dataset.GetNumberOfPoints()) + for i in range(dataset.GetNumberOfPoints()): + pressure.SetTuple1(i, float(i)) + dataset.GetPointData().AddArray(pressure) + + temperature = vtkFloatArray() + temperature.SetName("temperature") + temperature.SetNumberOfComponents(1) + temperature.SetNumberOfTuples(dataset.GetNumberOfCells()) + for i in range(dataset.GetNumberOfCells()): + temperature.SetTuple1(i, float(i)) + dataset.GetCellData().AddArray(temperature) + + return dataset + + # --------------------------------------------------------------------------- # from_dataset # --------------------------------------------------------------------------- @@ -132,3 +166,193 @@ def algorithm_filter(base): assert pipe.mapper.GetInputConnection(0, 0).GetProducer() is wrapper +# --------------------------------------------------------------------------- +# set_selected +# +# The selection highlight colour is ambient RGB 0, 62, 111 out of 255. The +# expected values below are written as decimal literals so that the test does +# not restate the production expression. +# --------------------------------------------------------------------------- + +SELECTION_AMBIENT_COLOR = (0.0, 0.24313725490196078, 0.43529411764705883) + + +def test_set_selected_true_sets_selection_ambient_color_and_lighting(poly_dataset): + pipe = VtkNodePipeline.from_dataset(poly_dataset) + + pipe.set_selected(True, [0.1, 0.2, 0.3]) + + prop = pipe.actor.GetProperty() + assert prop.GetAmbientColor() == pytest.approx(SELECTION_AMBIENT_COLOR) + assert prop.GetDiffuse() == pytest.approx(0.5) + assert prop.GetAmbient() == pytest.approx(0.5) + + +def test_set_selected_true_still_applies_the_given_diffuse_color(poly_dataset): + """Selecting changes the lighting terms; it does not replace the colour.""" + pipe = VtkNodePipeline.from_dataset(poly_dataset) + + pipe.set_selected(True, [1.0, 0.0, 0.0]) + + assert pipe.actor.GetProperty().GetDiffuseColor() == pytest.approx((1.0, 0.0, 0.0)) + + +def test_set_selected_false_restores_lighting_defaults(poly_dataset): + pipe = VtkNodePipeline.from_dataset(poly_dataset) + pipe.set_selected(True, [1.0, 0.0, 0.0]) + + pipe.set_selected(False, [0.0, 1.0, 0.0]) + + prop = pipe.actor.GetProperty() + assert prop.GetDiffuse() == pytest.approx(1.0) + assert prop.GetAmbient() == pytest.approx(0.0) + assert prop.GetDiffuseColor() == pytest.approx((0.0, 1.0, 0.0)) + + +def test_set_selected_false_leaves_ambient_color_untouched(poly_dataset): + """Deselecting resets the ambient *term*, not the ambient colour.""" + pipe = VtkNodePipeline.from_dataset(poly_dataset) + # Seeded ambient colour: a distinctive value neither branch writes. + pipe.actor.GetProperty().SetAmbientColor(0.11, 0.22, 0.33) + + pipe.set_selected(False, [0.0, 1.0, 0.0]) + + assert pipe.actor.GetProperty().GetAmbientColor() == pytest.approx( + (0.11, 0.22, 0.33) + ) + + +# --------------------------------------------------------------------------- +# set_color_variable +# --------------------------------------------------------------------------- + +def test_set_color_variable_point_uses_point_field_data_and_selects_array( + array_dataset, +): + pipe = VtkNodePipeline.from_dataset(array_dataset) + + pipe.set_color_variable(VisorVtkVariableType.POINT, "pressure", 0, 0.0, 7.5) + + assert pipe.mapper.GetScalarModeAsString() == "UsePointFieldData" + assert pipe.mapper.GetArrayName() == "pressure" + + +def test_set_color_variable_cell_uses_cell_field_data(array_dataset): + pipe = VtkNodePipeline.from_dataset(array_dataset) + + pipe.set_color_variable(VisorVtkVariableType.CELL, "temperature", 0, 0.0, 7.5) + + assert pipe.mapper.GetScalarModeAsString() == "UseCellFieldData" + assert pipe.mapper.GetArrayName() == "temperature" + + +def test_set_color_variable_sets_the_array_component(array_dataset): + """Component selection lives on the mapper, not on a lookup table.""" + pipe = VtkNodePipeline.from_dataset(array_dataset) + + pipe.set_color_variable(VisorVtkVariableType.POINT, "pressure", 1, 0.0, 7.5) + + assert pipe.mapper.GetArrayComponent() == 1 + + +def test_set_color_variable_sets_exact_scalar_range(array_dataset): + """The mapper honours the passed range, not a table's own range. + + If SetUseLookupTableScalarRange(0) were omitted the mapper would defer to + the lookup table and the range read back would not be what was passed. + """ + pipe = VtkNodePipeline.from_dataset(array_dataset) + + pipe.set_color_variable(VisorVtkVariableType.POINT, "pressure", 0, 0.0, 7.5) + + assert pipe.mapper.GetScalarRange() == pytest.approx((0.0, 7.5)) + assert pipe.mapper.GetUseLookupTableScalarRange() == 0 + + +def test_set_color_variable_enables_scalar_visibility_and_map_scalars(array_dataset): + pipe = VtkNodePipeline.from_dataset(array_dataset) + # Seeded scalar visibility: OFF, so an enabling call is visible. + pipe.mapper.SetScalarVisibility(False) + + pipe.set_color_variable(VisorVtkVariableType.POINT, "pressure", 0, 0.0, 7.5) + + assert pipe.mapper.GetScalarVisibility() == 1 + assert pipe.mapper.GetColorModeAsString() == "MapScalars" + + +def test_set_color_variable_unknown_point_array_warns_and_mutates_nothing( + array_dataset, +): + pipe = VtkNodePipeline.from_dataset(array_dataset) + # Seeded state: scalar visibility OFF and a sentinel array name. If the + # body wrongly proceeded, both would change. + pipe.mapper.SetScalarVisibility(False) + pipe.mapper.SelectColorArray("seeded-sentinel-array") + + with patch("ansys.visor.viewer.vtk.node_pipeline.logger") as mock_logger: + pipe.set_color_variable(VisorVtkVariableType.POINT, "no-such-array", 0, 0.0, 7.5) + + mock_logger.warning.assert_called_once() + assert pipe.mapper.GetScalarVisibility() == 0 + assert pipe.mapper.GetArrayName() == "seeded-sentinel-array" + + +def test_set_color_variable_unknown_cell_array_warns_and_mutates_nothing(array_dataset): + pipe = VtkNodePipeline.from_dataset(array_dataset) + # Seeded state, as above. "pressure" exists but only as a *point* array, + # so the cell-side lookup must miss. + pipe.mapper.SetScalarVisibility(False) + pipe.mapper.SelectColorArray("seeded-sentinel-array") + + with patch("ansys.visor.viewer.vtk.node_pipeline.logger") as mock_logger: + pipe.set_color_variable(VisorVtkVariableType.CELL, "pressure", 0, 0.0, 7.5) + + mock_logger.warning.assert_called_once() + assert pipe.mapper.GetScalarVisibility() == 0 + assert pipe.mapper.GetArrayName() == "seeded-sentinel-array" + + +def test_set_color_variable_non_enum_association_warns_and_mutates_nothing( + array_dataset, +): + """A bare string is not a VisorVtkVariableType and must not pick a branch.""" + pipe = VtkNodePipeline.from_dataset(array_dataset) + # Seeded state: scalar visibility OFF and a sentinel array name. + pipe.mapper.SetScalarVisibility(False) + pipe.mapper.SelectColorArray("seeded-sentinel-array") + + with patch("ansys.visor.viewer.vtk.node_pipeline.logger") as mock_logger: + pipe.set_color_variable("POINT", "pressure", 0, 0.0, 7.5) + + mock_logger.warning.assert_called_once() + assert pipe.mapper.GetScalarVisibility() == 0 + assert pipe.mapper.GetArrayName() == "seeded-sentinel-array" + + +# --------------------------------------------------------------------------- +# clear_color_variable +# --------------------------------------------------------------------------- + +def test_clear_color_variable_disables_scalar_visibility(array_dataset): + pipe = VtkNodePipeline.from_dataset(array_dataset) + pipe.set_color_variable(VisorVtkVariableType.POINT, "pressure", 0, 0.0, 7.5) + assert pipe.mapper.GetScalarVisibility() == 1 + + pipe.clear_color_variable() + + assert pipe.mapper.GetScalarVisibility() == 0 + + +def test_clear_color_variable_leaves_diffuse_color_untouched(array_dataset): + """Clearing does not restore or replace the part's diffuse colour.""" + pipe = VtkNodePipeline.from_dataset(array_dataset) + # Seeded diffuse colour: distinctive, and not the default mesh colour. + pipe.actor.GetProperty().SetDiffuseColor(0.11, 0.22, 0.33) + + pipe.clear_color_variable() + + assert pipe.actor.GetProperty().GetDiffuseColor() == pytest.approx( + (0.11, 0.22, 0.33) + ) + + From 361c20ec373e9d8755db50808e25f4d70d0f3aa9 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:51:28 +0000 Subject: [PATCH 07/28] chore: adding changelog file 51.added.md [dependabot-skip] --- doc/changelog.d/51.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/51.added.md diff --git a/doc/changelog.d/51.added.md b/doc/changelog.d/51.added.md new file mode 100644 index 00000000..d9507670 --- /dev/null +++ b/doc/changelog.d/51.added.md @@ -0,0 +1 @@ +Remote rendering 3.1b - implement per-part apply logic on the renderer and node pipeline From 0c503624220fca2ade48f6f713052b8a271dc6d2 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Mon, 31 Aug 2026 07:14:07 -0700 Subject: [PATCH 08/28] move remaining three vtk calls to node pipeline --- .../visor/viewer/renderer/local_renderer.py | 21 ++- src/ansys/visor/viewer/vtk/node_pipeline.py | 43 +++++ tests/unit/renderer/test_local_renderer.py | 163 +++++------------- tests/unit/vtk/test_node_pipeline.py | 80 +++++++++ 4 files changed, 177 insertions(+), 130 deletions(-) diff --git a/src/ansys/visor/viewer/renderer/local_renderer.py b/src/ansys/visor/viewer/renderer/local_renderer.py index 47913992..e0dd8670 100644 --- a/src/ansys/visor/viewer/renderer/local_renderer.py +++ b/src/ansys/visor/viewer/renderer/local_renderer.py @@ -160,8 +160,9 @@ def deregister_all(self) -> None: def apply_visibility(self, node_id: int, visible: bool) -> None: """See :meth:`IRenderer.apply_visibility`. - Mutates the actor itself, not its property. An unknown *node_id* - is a logged no-op, never a raise. + Resolves the pipeline and delegates to + :meth:`VtkNodePipeline.set_visibility`. An unknown *node_id* is a + logged no-op, never a raise. """ pipe = self._pipelines.get(node_id) if pipe is None: @@ -169,13 +170,14 @@ def apply_visibility(self, node_id: int, visible: bool) -> None: "apply_visibility: no pipeline for node %s; skipping.", node_id ) return - pipe.actor.SetVisibility(1 if visible else 0) + pipe.set_visibility(visible) def apply_opacity(self, node_id: int, opacity: float) -> None: """See :meth:`IRenderer.apply_opacity`. - Mutates the actor's property. An unknown *node_id* is a logged - no-op, never a raise. + Resolves the pipeline and delegates to + :meth:`VtkNodePipeline.set_opacity`. An unknown *node_id* is a + logged no-op, never a raise. """ pipe = self._pipelines.get(node_id) if pipe is None: @@ -183,15 +185,16 @@ def apply_opacity(self, node_id: int, opacity: float) -> None: "apply_opacity: no pipeline for node %s; skipping.", node_id ) return - pipe.actor.GetProperty().SetOpacity(opacity) + pipe.set_opacity(opacity) def apply_diffuse_color( self, node_id: int, r: float, g: float, b: float ) -> None: """See :meth:`IRenderer.apply_diffuse_color`. - Mutates the actor property's diffuse colour only. An unknown - *node_id* is a logged no-op, never a raise. + Resolves the pipeline and delegates to + :meth:`VtkNodePipeline.set_diffuse_color`. An unknown *node_id* + is a logged no-op, never a raise. """ pipe = self._pipelines.get(node_id) if pipe is None: @@ -199,7 +202,7 @@ def apply_diffuse_color( "apply_diffuse_color: no pipeline for node %s; skipping.", node_id ) return - pipe.actor.GetProperty().SetDiffuseColor(r, g, b) + pipe.set_diffuse_color(r, g, b) def apply_edge_visibility(self, node_id: int, edge_visible: bool) -> None: """No-op in Story 1.2. Phase 3 populates.""" diff --git a/src/ansys/visor/viewer/vtk/node_pipeline.py b/src/ansys/visor/viewer/vtk/node_pipeline.py index 1e97dcc1..fa611e0b 100644 --- a/src/ansys/visor/viewer/vtk/node_pipeline.py +++ b/src/ansys/visor/viewer/vtk/node_pipeline.py @@ -127,6 +127,9 @@ def set_selected(self, selected: bool, diffuse_rgb: list[float]) -> None: The part's diffuse colour, re-applied unconditionally on both branches -- selection changes the ambient/diffuse lighting terms, it does not replace the part's colour. + + Note: :meth:`set_diffuse_color` also writes ``SetDiffuseColor``; + this method is not a substitute for it and vice versa. """ prop = self.actor.GetProperty() if selected: @@ -138,6 +141,46 @@ def set_selected(self, selected: bool, diffuse_rgb: list[float]) -> None: prop.SetAmbient(0.0) prop.SetDiffuseColor(*diffuse_rgb) + def set_visibility(self, visible: bool) -> None: + """Show or hide this part. + + Mutates the actor itself, not its property. + + Parameters + ---------- + visible: + Target state. Absolute, never a toggle. + """ + self.actor.SetVisibility(1 if visible else 0) + + def set_opacity(self, opacity: float) -> None: + """Set this part's opacity. + + Mutates the actor's property. + + Parameters + ---------- + opacity: + The opacity value to apply, passed through unchanged. + """ + self.actor.GetProperty().SetOpacity(opacity) + + def set_diffuse_color(self, r: float, g: float, b: float) -> None: + """Set this part's diffuse colour. + + Mutates the actor property's diffuse colour only. + + Parameters + ---------- + r, g, b: + The diffuse colour components, passed through unchanged. + + Note: :meth:`set_selected` also writes ``SetDiffuseColor`` on both + of its branches; this method is not a substitute for it and vice + versa. + """ + self.actor.GetProperty().SetDiffuseColor(r, g, b) + def set_color_variable( self, association: VisorVtkVariableType, diff --git a/tests/unit/renderer/test_local_renderer.py b/tests/unit/renderer/test_local_renderer.py index 8fc9bfb2..ac4c9266 100644 --- a/tests/unit/renderer/test_local_renderer.py +++ b/tests/unit/renderer/test_local_renderer.py @@ -6,11 +6,11 @@ 1. Node lifecycle -- register_node / deregister_node (the non-trivial seam). 2. Interface conformance -- NullRenderer and VisorLocalRenderer both satisfy IRenderer's abstract contract. -3. Per-part visual mutations -- visibility, opacity and diffuse colour - mutate the resolved pipeline's VTK objects directly; selection and - colour variable resolve the pipeline and delegate to VtkNodePipeline - (their VTK effects are asserted in tests/unit/vtk/test_node_pipeline.py); - edge visibility and the colour-variable range refresh stay no-ops. +3. Per-part visual mutations -- visibility, opacity, diffuse colour, + selection and colour variable all resolve the pipeline and delegate to + VtkNodePipeline (their VTK effects are asserted in + tests/unit/vtk/test_node_pipeline.py); edge visibility and the + colour-variable range refresh stay no-ops. 4. Camera round-trip -- reset_camera, sync_camera / get_camera_state. 5. Render / flush delegation. 6. pick_geometry -- vertex, edge, face modes. @@ -21,7 +21,6 @@ from unittest.mock import MagicMock, patch import pytest -from vtkmodules.vtkRenderingCore import vtkActor from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType from ansys.visor.viewer.renderer.base import IRenderer @@ -97,27 +96,6 @@ def GetNextActor(self): # noqa: N802 return a -class _DummyPipeline: - """VtkNodePipeline stand-in holding a *real* vtkActor. - - The actor is real so that a test can read the mutated value back off - the VTK object (``GetVisibility``, ``GetProperty().GetOpacity()``, - ``GetProperty().GetDiffuseColor()``) rather than merely recording that - some call was made -- a MagicMock would pass even if the production - code mutated the wrong object or the wrong field. - - The three delegated mutations are MagicMocks, because from this module - the behaviour under test is *that the renderer resolved and delegated*. - Their VTK effects are asserted against real objects in - ``tests/unit/vtk/test_node_pipeline.py``. - """ - - def __init__(self): - self.actor = vtkActor() - self.set_selected = MagicMock(name="set_selected") - self.set_color_variable = MagicMock(name="set_color_variable") - self.clear_color_variable = MagicMock(name="clear_color_variable") - # --------------------------------------------------------------------------- # Fixture: VisorLocalRenderer with all VTK infrastructure mocked @@ -381,42 +359,33 @@ def test_refresh_color_variable_range(self, renderer): # =========================================================================== -# 3b. Inline apply bodies: visibility, opacity, diffuse colour +# 3b. Delegated apply bodies: visibility, opacity, diffuse colour, +# selection, colour variable # -# Each mutation is read back off a real vtkActor, and each miss branch -# (unknown node id) is asserted separately. +# The VTK effects of these live on VtkNodePipeline and are asserted in +# tests/unit/vtk/test_node_pipeline.py. What is asserted here is that +# the renderer resolved the pipeline and delegated with the arguments +# it was given, and that an unknown node id is a logged no-op. # =========================================================================== -class TestInlineApplyBodies: +class TestDelegatedApplyBodies: # ------------------------------------------------------------------ - # visibility -- mutates the actor itself + # apply_visibility # ------------------------------------------------------------------ - def test_apply_visibility_shows_actor(self, renderer): - """apply_visibility(True) leaves the actor's visibility flag set.""" - pipe = _DummyPipeline() - pipe.actor.SetVisibility(0) + def test_apply_visibility_delegates_to_pipeline(self, renderer): + """The visibility flag is passed straight through.""" + pipe = MagicMock(name="pipeline") renderer._pipelines[4] = pipe renderer.apply_visibility(4, True) - assert pipe.actor.GetVisibility() == 1 - - def test_apply_visibility_hides_actor(self, renderer): - """apply_visibility(False) leaves the actor's visibility flag clear.""" - pipe = _DummyPipeline() - pipe.actor.SetVisibility(1) - renderer._pipelines[4] = pipe - - renderer.apply_visibility(4, False) - - assert pipe.actor.GetVisibility() == 0 + pipe.set_visibility.assert_called_once_with(True) def test_apply_visibility_unknown_node_id_is_logged_no_op(self, renderer): - """An unregistered node id logs at debug, does not raise, mutates nothing.""" - pipe = _DummyPipeline() - pipe.actor.SetVisibility(1) + """An unregistered node id logs at debug, does not raise, delegates nothing.""" + pipe = MagicMock(name="pipeline") renderer._pipelines[4] = pipe with patch( @@ -425,39 +394,24 @@ def test_apply_visibility_unknown_node_id_is_logged_no_op(self, renderer): renderer.apply_visibility(9999, False) # must not raise mock_logger.debug.assert_called_once() - assert pipe.actor.GetVisibility() == 1 + pipe.set_visibility.assert_not_called() # ------------------------------------------------------------------ - # opacity -- mutates the actor's property + # apply_opacity # ------------------------------------------------------------------ - def test_apply_opacity_sets_property_opacity(self, renderer): - """apply_opacity writes the requested value onto the actor property.""" - pipe = _DummyPipeline() + def test_apply_opacity_delegates_to_pipeline(self, renderer): + """The opacity value is passed straight through.""" + pipe = MagicMock(name="pipeline") renderer._pipelines[4] = pipe renderer.apply_opacity(4, 0.25) - assert pipe.actor.GetProperty().GetOpacity() == pytest.approx(0.25) - - def test_apply_opacity_does_not_touch_visibility_or_diffuse_color(self, renderer): - """Opacity lands on the property's opacity field and nothing else.""" - pipe = _DummyPipeline() - pipe.actor.SetVisibility(0) - pipe.actor.GetProperty().SetDiffuseColor(0.25, 0.5, 0.75) - renderer._pipelines[4] = pipe - - renderer.apply_opacity(4, 0.25) - - assert pipe.actor.GetVisibility() == 0 - assert pipe.actor.GetProperty().GetDiffuseColor() == pytest.approx( - (0.25, 0.5, 0.75) - ) + pipe.set_opacity.assert_called_once_with(0.25) def test_apply_opacity_unknown_node_id_is_logged_no_op(self, renderer): - """An unregistered node id logs at debug, does not raise, mutates nothing.""" - pipe = _DummyPipeline() - pipe.actor.GetProperty().SetOpacity(0.75) + """An unregistered node id logs at debug, does not raise, delegates nothing.""" + pipe = MagicMock(name="pipeline") renderer._pipelines[4] = pipe with patch( @@ -466,43 +420,24 @@ def test_apply_opacity_unknown_node_id_is_logged_no_op(self, renderer): renderer.apply_opacity(9999, 0.25) # must not raise mock_logger.debug.assert_called_once() - assert pipe.actor.GetProperty().GetOpacity() == pytest.approx(0.75) + pipe.set_opacity.assert_not_called() # ------------------------------------------------------------------ - # diffuse colour -- mutates the actor property's DiffuseColor + # apply_diffuse_color # ------------------------------------------------------------------ - def test_apply_diffuse_color_sets_property_diffuse_color(self, renderer): - """apply_diffuse_color writes r, g, b onto the property's diffuse colour.""" - pipe = _DummyPipeline() + def test_apply_diffuse_color_delegates_to_pipeline(self, renderer): + """r, g, b are passed straight through.""" + pipe = MagicMock(name="pipeline") renderer._pipelines[4] = pipe renderer.apply_diffuse_color(4, 1.0, 0.0, 0.0) - assert pipe.actor.GetProperty().GetDiffuseColor() == pytest.approx( - (1.0, 0.0, 0.0) - ) - - def test_apply_diffuse_color_does_not_touch_ambient_color_or_opacity( - self, renderer - ): - """SetDiffuseColor, not SetColor or SetAmbientColor, and opacity is left alone.""" - pipe = _DummyPipeline() - pipe.actor.GetProperty().SetAmbientColor(0.25, 0.5, 0.75) - pipe.actor.GetProperty().SetOpacity(0.75) - renderer._pipelines[4] = pipe - - renderer.apply_diffuse_color(4, 1.0, 0.0, 0.0) - - assert pipe.actor.GetProperty().GetAmbientColor() == pytest.approx( - (0.25, 0.5, 0.75) - ) - assert pipe.actor.GetProperty().GetOpacity() == pytest.approx(0.75) + pipe.set_diffuse_color.assert_called_once_with(1.0, 0.0, 0.0) def test_apply_diffuse_color_unknown_node_id_is_logged_no_op(self, renderer): - """An unregistered node id logs at debug, does not raise, mutates nothing.""" - pipe = _DummyPipeline() - pipe.actor.GetProperty().SetDiffuseColor(0.25, 0.5, 0.75) + """An unregistered node id logs at debug, does not raise, delegates nothing.""" + pipe = MagicMock(name="pipeline") renderer._pipelines[4] = pipe with patch( @@ -511,21 +446,7 @@ def test_apply_diffuse_color_unknown_node_id_is_logged_no_op(self, renderer): renderer.apply_diffuse_color(9999, 1.0, 0.0, 0.0) # must not raise mock_logger.debug.assert_called_once() - assert pipe.actor.GetProperty().GetDiffuseColor() == pytest.approx( - (0.25, 0.5, 0.75) - ) - - -# =========================================================================== -# 3c. Delegated apply bodies: selection, colour variable -# -# The VTK effects of these live on VtkNodePipeline and are asserted in -# tests/unit/vtk/test_node_pipeline.py. What is asserted here is that -# the renderer resolved the pipeline and delegated with the arguments -# it was given, and that an unknown node id is a logged no-op. -# =========================================================================== - -class TestDelegatedApplyBodies: + pipe.set_diffuse_color.assert_not_called() # ------------------------------------------------------------------ # apply_selected @@ -533,7 +454,7 @@ class TestDelegatedApplyBodies: def test_apply_selected_delegates_to_pipeline(self, renderer): """Selection state and the given colour are passed straight through.""" - pipe = _DummyPipeline() + pipe = MagicMock(name="pipeline") renderer._pipelines[4] = pipe renderer.apply_selected(4, True, [1.0, 0.0, 0.0]) @@ -542,7 +463,7 @@ def test_apply_selected_delegates_to_pipeline(self, renderer): def test_apply_selected_unknown_node_id_is_logged_no_op(self, renderer): """An unregistered node id logs at debug, does not raise, delegates nothing.""" - pipe = _DummyPipeline() + pipe = MagicMock(name="pipeline") renderer._pipelines[4] = pipe with patch( @@ -559,7 +480,7 @@ def test_apply_selected_unknown_node_id_is_logged_no_op(self, renderer): def test_apply_color_variable_delegates_with_given_arguments(self, renderer): """The association is forwarded unchanged; spectrum_id is not forwarded.""" - pipe = _DummyPipeline() + pipe = MagicMock(name="pipeline") renderer._pipelines[4] = pipe renderer.apply_color_variable( @@ -572,7 +493,7 @@ def test_apply_color_variable_delegates_with_given_arguments(self, renderer): def test_apply_color_variable_unknown_node_id_is_logged_no_op(self, renderer): """An unregistered node id logs at debug, does not raise, delegates nothing.""" - pipe = _DummyPipeline() + pipe = MagicMock(name="pipeline") renderer._pipelines[4] = pipe with patch( @@ -590,7 +511,7 @@ def test_apply_color_variable_unknown_node_id_is_logged_no_op(self, renderer): # ------------------------------------------------------------------ def test_clear_color_variable_delegates_to_pipeline(self, renderer): - pipe = _DummyPipeline() + pipe = MagicMock(name="pipeline") renderer._pipelines[4] = pipe renderer.clear_color_variable(4) @@ -599,7 +520,7 @@ def test_clear_color_variable_delegates_to_pipeline(self, renderer): def test_clear_color_variable_unknown_node_id_is_logged_no_op(self, renderer): """An unregistered node id logs at debug, does not raise, delegates nothing.""" - pipe = _DummyPipeline() + pipe = MagicMock(name="pipeline") renderer._pipelines[4] = pipe with patch( diff --git a/tests/unit/vtk/test_node_pipeline.py b/tests/unit/vtk/test_node_pipeline.py index 96115c51..5a03955a 100644 --- a/tests/unit/vtk/test_node_pipeline.py +++ b/tests/unit/vtk/test_node_pipeline.py @@ -222,6 +222,86 @@ def test_set_selected_false_leaves_ambient_color_untouched(poly_dataset): ) +# --------------------------------------------------------------------------- +# set_visibility +# --------------------------------------------------------------------------- + +def test_set_visibility_true_shows_actor(poly_dataset): + """set_visibility(True) leaves the actor's visibility flag set.""" + pipe = VtkNodePipeline.from_dataset(poly_dataset) + pipe.actor.SetVisibility(0) + + pipe.set_visibility(True) + + assert pipe.actor.GetVisibility() == 1 + + +def test_set_visibility_false_hides_actor(poly_dataset): + """set_visibility(False) leaves the actor's visibility flag clear.""" + pipe = VtkNodePipeline.from_dataset(poly_dataset) + pipe.actor.SetVisibility(1) + + pipe.set_visibility(False) + + assert pipe.actor.GetVisibility() == 0 + + +# --------------------------------------------------------------------------- +# set_opacity +# --------------------------------------------------------------------------- + +def test_set_opacity_sets_property_opacity(poly_dataset): + """set_opacity writes the requested value onto the actor property.""" + pipe = VtkNodePipeline.from_dataset(poly_dataset) + + pipe.set_opacity(0.25) + + assert pipe.actor.GetProperty().GetOpacity() == pytest.approx(0.25) + + +def test_set_opacity_does_not_touch_visibility_or_diffuse_color(poly_dataset): + """Opacity lands on the property's opacity field and nothing else.""" + pipe = VtkNodePipeline.from_dataset(poly_dataset) + pipe.actor.SetVisibility(0) + pipe.actor.GetProperty().SetDiffuseColor(0.25, 0.5, 0.75) + + pipe.set_opacity(0.25) + + assert pipe.actor.GetVisibility() == 0 + assert pipe.actor.GetProperty().GetDiffuseColor() == pytest.approx( + (0.25, 0.5, 0.75) + ) + + +# --------------------------------------------------------------------------- +# set_diffuse_color +# --------------------------------------------------------------------------- + +def test_set_diffuse_color_sets_property_diffuse_color(poly_dataset): + """set_diffuse_color writes r, g, b onto the property's diffuse colour.""" + pipe = VtkNodePipeline.from_dataset(poly_dataset) + + pipe.set_diffuse_color(1.0, 0.0, 0.0) + + assert pipe.actor.GetProperty().GetDiffuseColor() == pytest.approx( + (1.0, 0.0, 0.0) + ) + + +def test_set_diffuse_color_does_not_touch_ambient_color_or_opacity(poly_dataset): + """SetDiffuseColor, not SetColor or SetAmbientColor, and opacity is left alone.""" + pipe = VtkNodePipeline.from_dataset(poly_dataset) + pipe.actor.GetProperty().SetAmbientColor(0.25, 0.5, 0.75) + pipe.actor.GetProperty().SetOpacity(0.75) + + pipe.set_diffuse_color(1.0, 0.0, 0.0) + + assert pipe.actor.GetProperty().GetAmbientColor() == pytest.approx( + (0.25, 0.5, 0.75) + ) + assert pipe.actor.GetProperty().GetOpacity() == pytest.approx(0.75) + + # --------------------------------------------------------------------------- # set_color_variable # --------------------------------------------------------------------------- From 174ee07fb6360598f7705369abe52e32eb5650eb Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Fri, 28 Aug 2026 12:23:40 -0700 Subject: [PATCH 09/28] register per-part trigger handlers and serialize VTK access with a lock --- src/ansys/visor/viewer/app/trame/local_app.py | 144 ++++ src/ansys/visor/viewer/app/visor_vtk_local.py | 4 + src/ansys/visor/viewer/vtk/scene/base.py | 357 +++++++--- tests/unit/app/test_local_app.py | 281 ++++++++ tests/unit/app/test_visor_vtk_local.py | 82 +++ tests/unit/vtk/scene/test_base.py | 662 +++++++++++++++++- tests/unit/vtk/scene/test_local_scene.py | 5 + 7 files changed, 1439 insertions(+), 96 deletions(-) create mode 100644 tests/unit/app/test_local_app.py diff --git a/src/ansys/visor/viewer/app/trame/local_app.py b/src/ansys/visor/viewer/app/trame/local_app.py index 8d2b545c..b865b9b3 100644 --- a/src/ansys/visor/viewer/app/trame/local_app.py +++ b/src/ansys/visor/viewer/app/trame/local_app.py @@ -1,11 +1,13 @@ import urllib.parse from logging import Logger +from typing import List, Optional, Protocol from trame.app.core import Server from trame.decorators import TrameApp, trigger from vtk import vtkObject from ansys.visor.viewer.config import settings +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger logger = VisorDefaultLogger(__name__) @@ -14,6 +16,37 @@ vtkObject.GlobalWarningDisplayOff() +class ScenePartStateApi(Protocol): + """Structural type of the per-part coordinator surface LocalApp calls. + + Typing only: there is no ``runtime_checkable`` decoration and no + ``isinstance`` check anywhere against it. Declaring it here rather than + importing the scene keeps this module free of any scene type, so the + injected object remains LocalApp's only route to the scene. + """ + + def set_part_visibility(self, node_id: int, visible: bool) -> None: ... + + def set_part_opacity(self, node_id: int, opacity: float) -> None: ... + + def set_part_diffuse_color(self, node_id: int, diffuse_rgb: Optional[List[float]]) -> None: ... + + def set_part_selected(self, node_id: int, selected: bool) -> None: ... + + def set_part_color_variable( + self, + node_id: int, + variable_id: str, + association: VisorVtkVariableType, + array_name: str, + component: int, + min_val: float, + max_val: float, + ) -> None: ... + + def clear_part_color_variable(self, node_id: int) -> None: ... + + @TrameApp() class LocalApp: @@ -29,6 +62,12 @@ class LocalApp: 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 + set_part_visibility: sets whether one part is visible + set_part_opacity: sets one part's opacity + set_part_diffuse_color: sets or clears one part's custom diffuse colour + set_part_selected: selects or deselects one part + set_part_color_variable: colours one part by a scalar variable + clear_part_color_variable: stops colouring one part by a scalar variable 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. @@ -41,6 +80,7 @@ def __init__( standalone: bool = True, trame_logger: Logger | None = None, pick_geometry=None, + scene_part_state_api: ScenePartStateApi | None = None, ): self.server = server # Callable to get the scene details in JSON format @@ -49,6 +89,10 @@ def __init__( 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 ScenePartStateApi). The one + # production construction site always supplies it; it is optional so + # that the class stays constructible without a scene. + self._scene_part_state_api = scene_part_state_api # logger for logging trame server lifecycle info self.__trame_logger = trame_logger @@ -154,6 +198,106 @@ def perf_report_server_update(self, payload: dict): f" | handler={payload.get('handlerMs', 0):.2f}ms" ) + # ------------------------------------------------------------------ + # Per-part visual state triggers + # + # Frontend -> Backend. Each takes a single ``payload: dict`` argument and + # returns ``None``; each delegates to the identically-named method on the + # injected coordinator. Every payload carries the absolute target value, + # never a toggle or a delta, so a message the client suppresses as + # redundant is indistinguishable from one that set a value a part already + # had. Required payload keys are read directly: a missing key raises + # KeyError on the trame event-loop thread, which is accepted because the + # only caller is the client written against this contract. + # ------------------------------------------------------------------ + + def _part_state_api(self, trigger_name: str) -> ScenePartStateApi | None: + """Return the injected coordinator, or ``None`` after logging.""" + if self._scene_part_state_api is None: + logger.debug("%s: no scene part-state API injected; ignoring.", trigger_name) + return None + return self._scene_part_state_api + + @trigger("set_part_visibility") + def set_part_visibility(self, payload: dict) -> None: + """Frontend -> Backend: set whether one part is visible.""" + api = self._part_state_api("set_part_visibility") + if api is None: + return + api.set_part_visibility(payload["nodeId"], payload["visible"]) + + @trigger("set_part_opacity") + def set_part_opacity(self, payload: dict) -> None: + """Frontend -> Backend: set one part's opacity.""" + api = self._part_state_api("set_part_opacity") + if api is None: + return + api.set_part_opacity(payload["nodeId"], payload["opacity"]) + + @trigger("set_part_diffuse_color") + def set_part_diffuse_color(self, payload: dict) -> None: + """Frontend -> Backend: set one part's custom diffuse colour. + + ``diffuseRgb`` of ``None`` clears the custom colour and is forwarded + as ``None``; no default colour is substituted here. + """ + api = self._part_state_api("set_part_diffuse_color") + if api is None: + return + api.set_part_diffuse_color(payload["nodeId"], payload["diffuseRgb"]) + + @trigger("set_part_selected") + def set_part_selected(self, payload: dict) -> None: + """Frontend -> Backend: select or deselect one part. + + No colour crosses this trigger: the server reads the part's stored + diffuse colour from its own record. + """ + api = self._part_state_api("set_part_selected") + if api is None: + return + api.set_part_selected(payload["nodeId"], payload["selected"]) + + @trigger("set_part_color_variable") + def set_part_color_variable(self, payload: dict) -> None: + """Frontend -> Backend: colour one part by a scalar variable. + + ``association`` is parsed here, at the boundary, into a + :class:`VisorVtkVariableType` by exact value lookup -- never + upper-cased, never passed on as a bare string. A value that is not a + member is a logged no-op, matching the posture the pipeline takes on an + unknown array name. ``variableId`` is forwarded verbatim and is never + parsed by the server. + """ + api = self._part_state_api("set_part_color_variable") + if api is None: + return + try: + association = VisorVtkVariableType(payload["association"]) + except ValueError: + logger.warning( + "set_part_color_variable: %r is not a VisorVtkVariableType; ignoring.", + payload["association"], + ) + return + api.set_part_color_variable( + payload["nodeId"], + payload["variableId"], + association, + payload["arrayName"], + payload["component"], + payload["min"], + payload["max"], + ) + + @trigger("clear_part_color_variable") + def clear_part_color_variable(self, payload: dict) -> None: + """Frontend -> Backend: stop colouring one part by a scalar variable.""" + api = self._part_state_api("clear_part_color_variable") + if api is None: + return + api.clear_part_color_variable(payload["nodeId"]) + def set_only_cookie(self, key: str, value: str): """ Sets a cookie on the server. NOTE: there is a limitation diff --git a/src/ansys/visor/viewer/app/visor_vtk_local.py b/src/ansys/visor/viewer/app/visor_vtk_local.py index 154aa70e..d61b4483 100644 --- a/src/ansys/visor/viewer/app/visor_vtk_local.py +++ b/src/ansys/visor/viewer/app/visor_vtk_local.py @@ -52,5 +52,9 @@ def _initialize_rendering(self, standalone: bool, trame_log_dir: str | None) -> world_x, world_y, world_z: self._scene.pick_geometry(actor_wasm_id, cell_id, mode, world_x, world_y, world_z), + # The scene satisfies LocalApp's ScenePartStateApi protocol + # structurally: the six per-part coordinator methods carry exactly + # the names and signatures the triggers call. + scene_part_state_api=self._scene, ) diff --git a/src/ansys/visor/viewer/vtk/scene/base.py b/src/ansys/visor/viewer/vtk/scene/base.py index efcadb0a..344b198a 100644 --- a/src/ansys/visor/viewer/vtk/scene/base.py +++ b/src/ansys/visor/viewer/vtk/scene/base.py @@ -1,6 +1,7 @@ """VTK scene management for Visor Viewer.""" import json +import threading from abc import ABC, abstractmethod from typing import TYPE_CHECKING, List @@ -8,6 +9,8 @@ from ansys.visor.viewer.core.metadata import ExtendedMetadata from ansys.visor.viewer.core.perf_timer import PerfTimer +from ansys.visor.viewer.core.visor_colors import VisorColors +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger from ansys.visor.viewer.core.visor_types import VisorDatasetType from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1 @@ -74,6 +77,16 @@ def __init__( self._scene_graph = None self._pipelines = {} + # Serialises every server-side VTK mutation and wasm push. + # + # @trigger handlers run on the trame server's daemon background-thread + # event loop, while the VTK objects they mutate are created and also + # mutated from the caller's (notebook/main) thread. Re-entrant because + # the locked paths nest: finalize_scene -> populate_scene -> + # update_widgets, finalize_scene -> render, and the per-part + # coordinator methods -> apply -> flush. + self._vtk_lock = threading.RLock() + self._dataset_registry = self._initialize_dataset_registry() if renderer is not None: @@ -151,16 +164,20 @@ def apply_state(self, state: PersistedViewerStateV1): Shared work (per-part state restoration) is done here; the renderer-specific final step is delegated to :meth:`_apply_runtime_state_to_render`. + + Holds ``_vtk_lock`` for the whole body: the delegated step mutates + VTK and pushes to the frontend. """ - # Apply UI settings - self.dark_mode = state.ui.dark_theme + with self._vtk_lock: + # Apply UI settings + self.dark_mode = state.ui.dark_theme - # Transform the frontend PersistedViewerStateV1 -> RuntimeAppState - runtime_app_state = self._state_mapper.persisted_to_runtime(state) + # Transform the frontend PersistedViewerStateV1 -> RuntimeAppState + runtime_app_state = self._state_mapper.persisted_to_runtime(state) - # Renderer-specific: flush VTK window and notify frontend (wasm), or - # push camera to vtkCamera (RCA), or no-op (headless). - self._apply_runtime_state_to_render(runtime_app_state) + # Renderer-specific: flush VTK window and notify frontend (wasm), or + # push camera to vtkCamera (RCA), or no-op (headless). + self._apply_runtime_state_to_render(runtime_app_state) def get_scene_details(self) -> VisorSceneDetails: """Return the VisorState.""" @@ -185,11 +202,15 @@ def handle_save_state_response(self, request_id: int, response: dict) -> None: self._frontend_bridge.resolve_save_state_response(request_id, response) def clear(self): - """Remove all actors from the renderer and reset the scene.""" - if self._scene_graph is not None: - self._renderer.deregister_all() - self._dataset_registry.clear() - self._scene_graph = None + """Remove all actors from the renderer and reset the scene. + + Holds ``_vtk_lock``: deregistering actors mutates the VTK renderer. + """ + with self._vtk_lock: + if self._scene_graph is not None: + self._renderer.deregister_all() + self._dataset_registry.clear() + self._scene_graph = None def populate_scene(self): """Update widgets to reflect the current scene contents. @@ -197,121 +218,158 @@ def populate_scene(self): Actor attach is done per-leaf inside :meth:`add_dataset` via :meth:`IRenderer.register_node`, so this method only refreshes the widget bounds and count. + + Holds ``_vtk_lock``: the widget refresh mutates VTK widget objects. """ - if self._scene_graph is None: - self._initialize_scene_graph() - self.update_widgets() + with self._vtk_lock: + if self._scene_graph is None: + self._initialize_scene_graph() + self.update_widgets() def finalize_scene(self, skip_reset_camera: bool = False): - """Populate the scene, reset the camera if applicable, then render.""" - self.populate_scene() + """Populate the scene, reset the camera if applicable, then render. + + Holds ``_vtk_lock`` across all three steps; the nested acquisitions in + :meth:`populate_scene`, :meth:`reset_camera` and :meth:`render` are + re-entrant on the same thread. + """ + with self._vtk_lock: + self.populate_scene() - if not skip_reset_camera: - # if there is one or zero datasets in the scene, reset the camera - if self._dataset_registry.count <= 1: - self.reset_camera() + if not skip_reset_camera: + # if there is one or zero datasets in the scene, reset the camera + if self._dataset_registry.count <= 1: + self.reset_camera() - self.render() + self.render() def update_widgets(self): - """Reset the widgets to fit the scene graph bounds.""" - if self._scene_graph is None: - msg = "Scene graph has not been initialized, cannot reset widgets and camera." - logger.error(msg) - raise RuntimeError(msg) + """Reset the widgets to fit the scene graph bounds. + + Holds ``_vtk_lock``: the widget bounds update mutates the + cross-section representation/plane and the bounding-box outline. + """ + with self._vtk_lock: + if self._scene_graph is None: + msg = "Scene graph has not been initialized, cannot reset widgets and camera." + logger.error(msg) + raise RuntimeError(msg) - self._update_widget_bounds() - self._update_actor_count() + self._update_widget_bounds() + self._update_actor_count() def add_dataset(self, input: VisorDatasetType, metadata: ExtendedMetadata) -> int: - """Set the input dataset and metadata for the scene graph.""" - if input is None: - msg = "Input dataset is None, cannot load dataset." - logger.error(msg) - raise ValueError(msg) + """Set the input dataset and metadata for the scene graph. - # Ensure unique dataset name - dataset_name = self._dataset_registry.get_sanitized_metadata_name(metadata) + Holds ``_vtk_lock``: node registration attaches actors to the VTK + renderer. + """ + with self._vtk_lock: + if input is None: + msg = "Input dataset is None, cannot load dataset." + logger.error(msg) + raise ValueError(msg) - # Initialize the scene graph if it does not exist - if self._scene_graph is None: - self._initialize_scene_graph() + # Ensure unique dataset name + dataset_name = self._dataset_registry.get_sanitized_metadata_name(metadata) - # Load the dataset into the scene graph - dataset_id = self._scene_graph.load_dataset(input, dataset_name) + # Initialize the scene graph if it does not exist + if self._scene_graph is None: + self._initialize_scene_graph() - # Register each leaf's pipeline with the renderer. - subtree = self._scene_graph.get_descendant_node(dataset_id, include_self=True) - if subtree is not None: - for leaf in subtree.get_descendant_part_nodes(include_self=True): - self._renderer.register_node(leaf, leaf.dataset) + # Load the dataset into the scene graph + dataset_id = self._scene_graph.load_dataset(input, dataset_name) - # Seed PartIndex with scene-graph node IDs so that part_id == scene-graph node ID, - # which is the contract the frontend relies on to apply per-part state (opacity etc.). - part_name_to_id = self._scene_graph.get_part_name_to_id_map(dataset_id) + # Register each leaf's pipeline with the renderer. + subtree = self._scene_graph.get_descendant_node(dataset_id, include_self=True) + if subtree is not None: + for leaf in subtree.get_descendant_part_nodes(include_self=True): + self._renderer.register_node(leaf, leaf.dataset) - self._dataset_registry.add(dataset_id, dataset_name, input, part_name_to_id, metadata) + # Seed PartIndex with scene-graph node IDs so that part_id == scene-graph node ID, + # which is the contract the frontend relies on to apply per-part state (opacity etc.). + part_name_to_id = self._scene_graph.get_part_name_to_id_map(dataset_id) - return dataset_id + self._dataset_registry.add(dataset_id, dataset_name, input, part_name_to_id, metadata) + + return dataset_id def remove_dataset(self, dataset_id: int): - """Remove a dataset from the scene graph.""" - if self._scene_graph is None: - msg = "Scene graph has not been initialized, cannot load dataset." - logger.error(msg) - raise RuntimeError(msg) + """Remove a dataset from the scene graph. - # Deregister each leaf's pipeline from the renderer before dropping - # the subtree from the scene graph. - node = self._scene_graph.get_descendant_node(dataset_id) - if node is None: - msg = f"Dataset with id {dataset_id} not found in scene graph." - logger.error(msg) - raise ValueError(msg) + Holds ``_vtk_lock``: node deregistration detaches actors from the VTK + renderer. + """ + with self._vtk_lock: + if self._scene_graph is None: + msg = "Scene graph has not been initialized, cannot load dataset." + logger.error(msg) + raise RuntimeError(msg) - for leaf in node.get_descendant_part_nodes(include_self=True): - self._renderer.deregister_node(leaf.id) + # Deregister each leaf's pipeline from the renderer before dropping + # the subtree from the scene graph. + node = self._scene_graph.get_descendant_node(dataset_id) + if node is None: + msg = f"Dataset with id {dataset_id} not found in scene graph." + logger.error(msg) + raise ValueError(msg) - # Remove dataset from the scene graph - self._scene_graph.remove_dataset(dataset_id) + for leaf in node.get_descendant_part_nodes(include_self=True): + self._renderer.deregister_node(leaf.id) - # Unregister the dataset - self._dataset_registry.remove(dataset_id) + # Remove dataset from the scene graph + self._scene_graph.remove_dataset(dataset_id) + + # Unregister the dataset + self._dataset_registry.remove(dataset_id) def list_variables_for_dataset(self, dataset_id: int) -> List[VisorPartVariables]: return self._dataset_registry.list_variables(dataset_id) def update_variables_for_dataset(self, dataset_id: int, variables: List[VisorVariableUpdate]) -> None: - """Update the variables for the dataset in the scene graph.""" - timer = PerfTimer("update_variables_for_dataset", logger, dataset=dataset_id) + """Update the variables for the dataset in the scene graph. - with timer.phase("dataset_registry.update"): - self._dataset_registry.update_variables(dataset_id, variables) + Holds ``_vtk_lock``: the registry update writes into VTK arrays in + place and the render pushes to wasm. + """ + with self._vtk_lock: + timer = PerfTimer("update_variables_for_dataset", logger, dataset=dataset_id) - # Refresh cached variable metadata on part nodes without re-wiring - # the VTK pipeline (which would call SetInputConnection + mark the - # mapper Modified, causing unnecessary re-serialisation of geometry - # that has not changed). - dataset_node = self._scene_graph.get_descendant_node(dataset_id) - with timer.phase("update_descendant_parts"): - dataset_node.refresh_descendant_variable_metadata(include_self=True) + with timer.phase("dataset_registry.update"): + self._dataset_registry.update_variables(dataset_id, variables) - with timer.phase("render"): - self.render() + # Refresh cached variable metadata on part nodes without re-wiring + # the VTK pipeline (which would call SetInputConnection + mark the + # mapper Modified, causing unnecessary re-serialisation of geometry + # that has not changed). + dataset_node = self._scene_graph.get_descendant_node(dataset_id) + with timer.phase("update_descendant_parts"): + dataset_node.refresh_descendant_variable_metadata(include_self=True) - timer.log() + with timer.phase("render"): + self.render() + + timer.log() def render(self): - """Delegate to the renderer backend.""" - self._renderer.render() + """Delegate to the renderer backend. + + Holds ``_vtk_lock``: renders the VTK window and pushes to wasm. + """ + with self._vtk_lock: + self._renderer.render() def reset_camera(self): - """Reset the camera to fit the scene graph bounds.""" - if self._scene_graph is None: - logger.debug("Scene graph not initialized; skipping camera reset.") - return + """Reset the camera to fit the scene graph bounds. + + Holds ``_vtk_lock``: mutates the VTK renderer's camera. + """ + with self._vtk_lock: + if self._scene_graph is None: + logger.debug("Scene graph not initialized; skipping camera reset.") + return - self._renderer.reset_camera(self._scene_graph.bounds) + self._renderer.reset_camera(self._scene_graph.bounds) def pick_geometry(self, actor_wasm_id, cell_id, mode, world_x, world_y, world_z) -> dict: """ @@ -322,6 +380,125 @@ def pick_geometry(self, actor_wasm_id, cell_id, mode, world_x, world_y, world_z) actor_wasm_id, cell_id, mode, (world_x, world_y, world_z) ) + # ========================================================================= + # Per-part visual state — coordinator surface + # + # Each method does both halves of its trigger, in this order and all under + # ``_vtk_lock``: write the registry record, apply to the server's VTK + # pipeline, then push the mutated state to the wasm client with + # ``flush_wasm_state()``. An unresolvable node id is a logged no-op at + # every layer: nothing is applied and nothing is flushed. + # + # Every value that arrives here is absolute, never relative: the caller + # always supplies the target value, never a toggle or a delta. + # ========================================================================= + + def set_part_visibility(self, node_id: int, visible: bool) -> None: + """Set whether the part identified by *node_id* is visible.""" + with self._vtk_lock: + if not self._dataset_registry.set_part_visibility(node_id, visible): + logger.debug("set_part_visibility: no dataset owns node %s; skipping.", node_id) + return + self._renderer.apply_visibility(node_id, visible) + self._renderer.flush_wasm_state() + + def set_part_opacity(self, node_id: int, opacity: float) -> None: + """Set the opacity of the part identified by *node_id*.""" + with self._vtk_lock: + if not self._dataset_registry.set_part_opacity(node_id, opacity): + logger.debug("set_part_opacity: no dataset owns node %s; skipping.", node_id) + return + self._renderer.apply_opacity(node_id, opacity) + self._renderer.flush_wasm_state() + + def set_part_diffuse_color(self, node_id: int, diffuse_rgb: list[float] | None) -> None: + """ + Set the custom diffuse colour of the part identified by *node_id*, or + clear it with ``None``. + + The store records the absence as absence: ``diffuse_rgb=None`` is + written through as ``None``. The pipeline needs a concrete colour, so + the apply falls back to :attr:`VisorColors.DefaultMeshColor` — "no + custom colour" means "the scene-wide default". + """ + with self._vtk_lock: + if not self._dataset_registry.set_part_diffuse_color(node_id, diffuse_rgb): + logger.debug("set_part_diffuse_color: no dataset owns node %s; skipping.", node_id) + return + applied_rgb = ( + diffuse_rgb if diffuse_rgb is not None else list(VisorColors.DefaultMeshColor) + ) + self._renderer.apply_diffuse_color( + node_id, applied_rgb[0], applied_rgb[1], applied_rgb[2] + ) + self._renderer.flush_wasm_state() + + def set_part_selected(self, node_id: int, selected: bool) -> None: + """ + Select or deselect the part identified by *node_id*. + + No colour crosses the trigger for this class: the server reads the + part's stored ``diffuse_rgb`` from its own record and falls back to + :attr:`VisorColors.DefaultMeshColor` when it is ``None``. The record + is guaranteed to exist here — the setter above returned ``True``, + which means it either found the record or upserted one — so + ``get_part_state`` cannot return ``None`` at this point. + """ + with self._vtk_lock: + if not self._dataset_registry.set_part_selected(node_id, selected): + logger.debug("set_part_selected: no dataset owns node %s; skipping.", node_id) + return + stored_rgb = self._dataset_registry.get_part_state(node_id).diffuse_rgb + diffuse_rgb = ( + stored_rgb if stored_rgb is not None else list(VisorColors.DefaultMeshColor) + ) + self._renderer.apply_selected(node_id, selected, diffuse_rgb) + self._renderer.flush_wasm_state() + + def set_part_color_variable( + self, + node_id: int, + variable_id: str, + association: VisorVtkVariableType, + array_name: str, + component: int, + min_val: float, + max_val: float, + ) -> None: + """ + Colour the part identified by *node_id* by a scalar variable. + + *variable_id* is stored opaquely and is never parsed here; the + association and array name arrive as explicit arguments. *association* + is already a :class:`VisorVtkVariableType` — it is parsed at the + trigger boundary, never derived from a string here. The range travels + as a parameter only and is not persisted per part. + """ + with self._vtk_lock: + if not self._dataset_registry.set_part_color_variable(node_id, variable_id, component): + logger.debug("set_part_color_variable: no dataset owns node %s; skipping.", node_id) + return + self._renderer.apply_color_variable( + node_id, variable_id, association, array_name, component, min_val, max_val + ) + self._renderer.flush_wasm_state() + + def clear_part_color_variable(self, node_id: int) -> None: + """ + Stop colouring the part identified by *node_id* by a scalar variable. + + The variable reference is cleared atomically in the store (id and + component together), matching the atomic set. + """ + with self._vtk_lock: + if not self._dataset_registry.clear_part_color_variable(node_id): + logger.debug( + "clear_part_color_variable: no dataset owns node %s; skipping.", node_id + ) + return + self._renderer.clear_color_variable(node_id) + self._renderer.flush_wasm_state() + # ------------------------------------------------------------------ # Internal helpers # ------------------------------------------------------------------ diff --git a/tests/unit/app/test_local_app.py b/tests/unit/app/test_local_app.py new file mode 100644 index 00000000..d71e1315 --- /dev/null +++ b/tests/unit/app/test_local_app.py @@ -0,0 +1,281 @@ +"""Unit tests for LocalApp's per-part visual-state triggers. + +Coverage targets +---------------- +1. Each of the six triggers unpacks its payload and delegates to the + identically-named method on the injected coordinator object, returning + ``None``. +2. ``association`` is parsed at this boundary into a ``VisorVtkVariableType`` + member -- by exact value lookup, never by upper-casing -- and a value that + is not a member is a logged no-op that delegates nothing. +3. ``variableId`` crosses opaquely: forwarded byte-identically, never parsed. +4. ``diffuseRgb`` of ``None`` is forwarded as ``None`` (a clear), and + ``set_part_selected`` carries no colour at all. +5. With no coordinator injected, every trigger is a logged no-op. + +The coordinator is a single MagicMock standing in for the injected object; +what each trigger does with the values it forwards is asserted in +tests/unit/vtk/scene/test_base.py against the registry and the VTK objects. +""" + +from unittest.mock import MagicMock, patch + +import pytest + +from ansys.visor.viewer.app.trame.local_app import LocalApp +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType + +TRIGGER_NAMES = [ + "set_part_visibility", + "set_part_opacity", + "set_part_diffuse_color", + "set_part_selected", + "set_part_color_variable", + "clear_part_color_variable", +] + +# One payload per trigger, written out literally rather than built from the +# production code's key names. +PAYLOADS = { + "set_part_visibility": {"nodeId": 7, "visible": False}, + "set_part_opacity": {"nodeId": 7, "opacity": 0.25}, + "set_part_diffuse_color": {"nodeId": 7, "diffuseRgb": [1.0, 0.0, 0.0]}, + "set_part_selected": {"nodeId": 7, "selected": True}, + "set_part_color_variable": { + "nodeId": 7, + "variableId": "POINT::pressure::1", + "association": "POINT", + "arrayName": "pressure", + "component": 0, + "min": 0.0, + "max": 49.0, + }, + "clear_part_color_variable": {"nodeId": 7}, +} + + +class MockController: + """Minimal trame controller stand-in that records added handlers.""" + + def __init__(self): + self.handlers = {} + self.add_call_count = 0 + + def add(self, event): + self.add_call_count += 1 + + def decorator(fn): + self.handlers[event] = fn + return fn + + return decorator + + +@pytest.fixture +def mock_server(): + """Provide a mock Trame server.""" + server = MagicMock() + server.controller = MockController() + server.http_headers.set_header = MagicMock() + server.name = "TestServer" + server._www = None + return server + + +@pytest.fixture +def api(): + """Stand-in for the injected per-part coordinator object.""" + return MagicMock(name="scene_part_state_api") + + +@pytest.fixture +def app(mock_server, api): + """LocalApp with a coordinator injected.""" + return LocalApp( + server=mock_server, + get_scene_details_json=MagicMock(), + handle_save_state_response=MagicMock(), + standalone=True, + scene_part_state_api=api, + ) + + +@pytest.fixture +def app_without_api(mock_server): + """LocalApp with no coordinator injected.""" + return LocalApp( + server=mock_server, + get_scene_details_json=MagicMock(), + handle_save_state_response=MagicMock(), + standalone=True, + ) + + +# =========================================================================== +# Delegation +# =========================================================================== + +def test_set_part_visibility_delegates_payload_values(app, api): + """nodeId and visible reach the coordinator unchanged.""" + app.set_part_visibility({"nodeId": 7, "visible": False}) + + api.set_part_visibility.assert_called_once_with(7, False) + + +def test_set_part_opacity_delegates_payload_values(app, api): + """nodeId and opacity reach the coordinator unchanged.""" + app.set_part_opacity({"nodeId": 7, "opacity": 0.25}) + + api.set_part_opacity.assert_called_once_with(7, 0.25) + + +def test_set_part_diffuse_color_delegates_payload_values(app, api): + """nodeId and diffuseRgb reach the coordinator unchanged.""" + app.set_part_diffuse_color({"nodeId": 7, "diffuseRgb": [1.0, 0.0, 0.0]}) + + api.set_part_diffuse_color.assert_called_once_with(7, [1.0, 0.0, 0.0]) + + +def test_set_part_diffuse_color_forwards_none_as_a_clear(app, api): + """A None colour is forwarded as None; no default is substituted here.""" + app.set_part_diffuse_color({"nodeId": 7, "diffuseRgb": None}) + + api.set_part_diffuse_color.assert_called_once_with(7, None) + + +def test_set_part_selected_delegates_payload_values(app, api): + """nodeId and selected reach the coordinator unchanged.""" + app.set_part_selected({"nodeId": 7, "selected": True}) + + api.set_part_selected.assert_called_once_with(7, True) + + +def test_set_part_selected_carries_no_colour(app, api): + """The selection trigger passes exactly two arguments -- no colour.""" + app.set_part_selected({"nodeId": 7, "selected": True}) + + call = api.set_part_selected.call_args + assert len(call.args) == 2 + assert call.kwargs == {} + + +def test_set_part_color_variable_delegates_payload_values(app, api): + """Every colour-variable field reaches the coordinator in contract order.""" + app.set_part_color_variable( + { + "nodeId": 7, + "variableId": "POINT::pressure::1", + "association": "POINT", + "arrayName": "pressure", + "component": 0, + "min": 0.0, + "max": 49.0, + } + ) + + api.set_part_color_variable.assert_called_once_with( + 7, "POINT::pressure::1", VisorVtkVariableType.POINT, "pressure", 0, 0.0, 49.0 + ) + + +def test_set_part_color_variable_parses_point_association_to_the_enum_member(app, api): + """'POINT' arrives as the POINT member itself, not as a string.""" + app.set_part_color_variable( + { + "nodeId": 7, + "variableId": "POINT::pressure::1", + "association": "POINT", + "arrayName": "pressure", + "component": 0, + "min": 0.0, + "max": 1.0, + } + ) + + forwarded = api.set_part_color_variable.call_args.args[2] + assert forwarded is VisorVtkVariableType.POINT + + +def test_set_part_color_variable_parses_cell_association_to_the_enum_member(app, api): + """'CELL' arrives as the CELL member itself, not as a string.""" + app.set_part_color_variable( + { + "nodeId": 7, + "variableId": "CELL::temperature::1", + "association": "CELL", + "arrayName": "temperature", + "component": 0, + "min": 0.0, + "max": 1.0, + } + ) + + forwarded = api.set_part_color_variable.call_args.args[2] + assert forwarded is VisorVtkVariableType.CELL + + +@pytest.mark.parametrize("association", ["point", "cell", "NODE", "", None]) +def test_set_part_color_variable_non_member_association_is_a_logged_no_op( + app, api, association +): + """A non-member association warns and delegates nothing -- no upper-casing.""" + with patch("ansys.visor.viewer.app.trame.local_app.logger") as mock_logger: + app.set_part_color_variable( + { + "nodeId": 7, + "variableId": "POINT::pressure::1", + "association": association, + "arrayName": "pressure", + "component": 0, + "min": 0.0, + "max": 1.0, + } + ) + + api.set_part_color_variable.assert_not_called() + assert mock_logger.warning.call_count == 1 + + +def test_set_part_color_variable_forwards_variable_id_byte_identically(app, api): + """variableId crosses opaquely: not split, not rebuilt, not normalised.""" + variable_id = "CELL::Von Mises::3" + app.set_part_color_variable( + { + "nodeId": 7, + "variableId": variable_id, + "association": "CELL", + "arrayName": "Von Mises", + "component": 2, + "min": -1.5, + "max": 1.5, + } + ) + + assert api.set_part_color_variable.call_args.args[1] == "CELL::Von Mises::3" + + +def test_clear_part_color_variable_delegates_payload_values(app, api): + """nodeId reaches the coordinator unchanged.""" + app.clear_part_color_variable({"nodeId": 7}) + + api.clear_part_color_variable.assert_called_once_with(7) + + +# =========================================================================== +# Return value and the not-injected guard +# =========================================================================== + +@pytest.mark.parametrize("name", TRIGGER_NAMES) +def test_trigger_returns_none(app, name): + """No trigger returns the coordinator's return value.""" + assert getattr(app, name)(PAYLOADS[name]) is None + + +@pytest.mark.parametrize("name", TRIGGER_NAMES) +def test_trigger_is_a_logged_no_op_when_no_coordinator_injected(app_without_api, name): + """With nothing injected, each trigger logs and returns without raising.""" + with patch("ansys.visor.viewer.app.trame.local_app.logger") as mock_logger: + assert getattr(app_without_api, name)(PAYLOADS[name]) is None + + assert mock_logger.debug.call_count == 1 + diff --git a/tests/unit/app/test_visor_vtk_local.py b/tests/unit/app/test_visor_vtk_local.py index a65221e1..434f7fb5 100644 --- a/tests/unit/app/test_visor_vtk_local.py +++ b/tests/unit/app/test_visor_vtk_local.py @@ -517,3 +517,85 @@ def test_load_state_passes_correct_metadata_to_add_dataset(tmp_path, iface): iface._file_io.build_metadata_for_load_state.assert_called_once_with("mesh", "mm", ds_state) iface._file_io.read_snapshot.assert_called_once_with(str(snapshot)) iface._scene.add_dataset.assert_called_once_with(mock_data, built_meta) + + +# ================================================================== # +# LocalApp injection boundary +# ================================================================== # + +PART_STATE_API_METHODS = [ + "set_part_visibility", + "set_part_opacity", + "set_part_diffuse_color", + "set_part_selected", + "set_part_color_variable", + "clear_part_color_variable", +] + + +@pytest.fixture +def local_app_call(): + """Build a VisorVTK with LocalApp patched, and return the LocalApp call. + + The scene is patched too, so the returned call records exactly what + _initialize_rendering passed to LocalApp. + """ + with patch("ansys.visor.viewer.app.visor_vtk.TrameServerManager") as mock_mgr, \ + patch("ansys.visor.viewer.app.visor_vtk_local.LocalApp") as mock_local_app, \ + patch("ansys.visor.viewer.app.visor_vtk_local.VisorLocalScene") as mock_scene: + mock_mgr_inst = MagicMock() + mock_mgr_inst.running = True + mock_mgr_inst._server = MagicMock() + mock_mgr.return_value = mock_mgr_inst + mock_scene_inst = MagicMock() + mock_scene_inst.datasets = {} + mock_scene.return_value = mock_scene_inst + + instance = VisorVTK() + + return mock_local_app.call_args, mock_scene_inst, instance + + +def test_local_app_receives_the_scene_as_the_part_state_api(local_app_call): + """The scene itself is injected, not a wrapper or a set of lambdas.""" + call, scene, instance = local_app_call + + assert call.kwargs["scene_part_state_api"] is scene + assert call.kwargs["scene_part_state_api"] is instance._scene + + +def test_local_app_still_receives_the_pre_existing_arguments(local_app_call): + """The boundary is extended, not broken: the earlier arguments survive.""" + call, scene, instance = local_app_call + + server, get_scene_details_json, handle_save_state_response, standalone = call.args + assert server is instance.server + assert callable(get_scene_details_json) + assert callable(handle_save_state_response) + assert standalone is True + assert callable(call.kwargs["pick_geometry"]) + assert "trame_logger" in call.kwargs + + +def test_the_pre_existing_lambdas_still_delegate_to_the_scene(local_app_call): + """The three original callables are untouched and still reach the scene.""" + call, scene, _instance = local_app_call + + _server, get_scene_details_json, handle_save_state_response, _standalone = call.args + get_scene_details_json() + handle_save_state_response(1, {"ok": True}) + call.kwargs["pick_geometry"](2, 3, "vertex", 0.0, 1.0, 2.0) + + scene.get_scene_details_json.assert_called_once_with() + scene.handle_save_state_response.assert_called_once_with(1, {"ok": True}) + scene.pick_geometry.assert_called_once_with(2, 3, "vertex", 0.0, 1.0, 2.0) + + +@pytest.mark.parametrize("name", PART_STATE_API_METHODS) +def test_local_scene_satisfies_the_part_state_protocol(name): + """VisorLocalScene structurally provides every method the triggers call.""" + from ansys.visor.viewer.vtk.scene.local_scene import VisorLocalScene + + assert callable(getattr(VisorLocalScene, name)) + + diff --git a/tests/unit/vtk/scene/test_base.py b/tests/unit/vtk/scene/test_base.py index 39a3dc76..d17214c8 100644 --- a/tests/unit/vtk/scene/test_base.py +++ b/tests/unit/vtk/scene/test_base.py @@ -1,14 +1,42 @@ -"""Tests for VisorSceneBase as an abstract class. +"""Tests for VisorSceneBase. -These tests only assert the abstract contract of VisorSceneBase: - 1. Direct instantiation raises TypeError. - 2. Exactly the expected set of abstract methods is declared. +Two groups: -Behavioural coverage of shared logic lives in test_local_scene.py, exercised -through the concrete VisorLocalScene subclass. +1. The abstract contract: direct instantiation raises TypeError, and exactly + the expected set of abstract methods is declared. +2. The per-part coordinator surface and the VTK lock. For each of the six + coordinator methods the two halves are asserted **separately** -- the + registry record (the store write) and the VTK object (the pipeline apply) + -- because either half can silently do nothing while the other succeeds. + The lock is asserted around every coordinator method and around all ten + existing public entry points that mutate VTK or push to wasm; a lock added + only to the trigger side would serialise nothing while looking correct. + +Behavioural coverage of the remaining shared logic lives in +test_local_scene.py, exercised through the concrete VisorLocalScene subclass. """ +import threading +from unittest.mock import MagicMock, patch + +import pytest +from vtkmodules.vtkCommonCore import vtkFloatArray +from vtkmodules.vtkCommonDataModel import vtkPolyData +from vtkmodules.vtkFiltersSources import vtkSphereSource + +from ansys.visor.viewer.core.visor_colors import VisorColors +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType +from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import ( + RuntimeDatasetState, + RuntimePartProperties, +) +from ansys.visor.viewer.renderer.local_renderer import VisorLocalRenderer +from ansys.visor.viewer.vtk.datasets.visor_dataset_registry import VisorDatasetRegistry +from ansys.visor.viewer.vtk.node_pipeline import VtkNodePipeline from ansys.visor.viewer.vtk.scene.base import VisorSceneBase +NODE_ID = 7 +UNKNOWN_NODE_ID = 999999 + def test_cannot_instantiate_directly(): """VisorSceneBase is abstract and must raise TypeError on direct instantiation.""" @@ -26,3 +54,625 @@ def test_abstract_methods(): "_apply_runtime_state_to_render", } + +# =========================================================================== +# Test doubles +# =========================================================================== + +class _ConcreteScene(VisorSceneBase): + """Smallest concrete VisorSceneBase: both abstract hooks are inert.""" + + async def _get_runtime_state_async(self, timeout: float): + return MagicMock(name="runtime_app_state") + + def _apply_runtime_state_to_render(self, runtime_app_state) -> None: + return None + + +class _LockSpy: + """Re-entrant lock that records its own acquisition depth. + + Wraps a real RLock so nesting still behaves, and exposes ``depth`` so a + test can assert the lock was *held* at the moment some inner call ran, + rather than merely acquired at some point. + """ + + def __init__(self): + self._lock = threading.RLock() + self.enter_count = 0 + self.exit_count = 0 + self.depth = 0 + self.max_depth = 0 + + def __enter__(self): + self._lock.acquire() + self.enter_count += 1 + self.depth += 1 + self.max_depth = max(self.max_depth, self.depth) + return self + + def __exit__(self, exc_type, exc, tb): + self.depth -= 1 + self.exit_count += 1 + self._lock.release() + return False + + +def _make_part_dataset(dataset_id: int, part_ids, part_states=None): + """Dataset stand-in with a real PartIndex.part_ids and a real state object. + + Real (not MagicMock) state, so a registry write can be read back through + object identity rather than through a recorded call. + """ + dataset = MagicMock() + dataset.part_index = MagicMock() + dataset.part_index.part_ids = list(part_ids) + dataset.state = RuntimeDatasetState(id=dataset_id, part_states=part_states or {}) + return dataset + + +@pytest.fixture +def array_dataset() -> vtkPolyData: + """Sphere output carrying one named point array and one named cell array.""" + src = vtkSphereSource() + src.Update() + dataset = src.GetOutput() + + pressure = vtkFloatArray() + pressure.SetName("pressure") + pressure.SetNumberOfComponents(1) + pressure.SetNumberOfTuples(dataset.GetNumberOfPoints()) + for i in range(dataset.GetNumberOfPoints()): + pressure.SetTuple1(i, float(i)) + dataset.GetPointData().AddArray(pressure) + + temperature = vtkFloatArray() + temperature.SetName("temperature") + temperature.SetNumberOfComponents(1) + temperature.SetNumberOfTuples(dataset.GetNumberOfCells()) + for i in range(dataset.GetNumberOfCells()): + temperature.SetTuple1(i, float(i)) + dataset.GetCellData().AddArray(temperature) + + return dataset + + +@pytest.fixture +def renderer(): + """Real VisorLocalRenderer with every VTK sub-system patched out. + + Real, so the apply bodies under test actually run against real VTK + objects; the sub-systems are patched so no render window, interactor, + LocalView or widget is created. + """ + mock_server = MagicMock() + mock_server.state = {} + + with ( + patch.object(VisorLocalRenderer, "_initialize_vtk_renderer", return_value=MagicMock()), + patch.object(VisorLocalRenderer, "_initialize_render_window", return_value=MagicMock()), + patch.object( + VisorLocalRenderer, "_initialize_render_window_interactor", return_value=MagicMock() + ), + patch.object(VisorLocalRenderer, "_initialize_local_view", return_value=MagicMock()), + patch.object(VisorLocalRenderer, "_initialize_orientation_widget", return_value=MagicMock()), + patch.object( + VisorLocalRenderer, "_initialize_cross_section_widget", return_value=MagicMock() + ), + patch.object( + VisorLocalRenderer, "_initialize_bounding_box_widget", return_value=MagicMock() + ), + ): + return VisorLocalRenderer(mock_server) + + +@pytest.fixture +def pipeline(renderer, array_dataset): + """A real VtkNodePipeline registered under NODE_ID.""" + pipe = VtkNodePipeline.from_dataset(array_dataset) + renderer._pipelines[NODE_ID] = pipe + return pipe + + +@pytest.fixture +def registry(): + """Registry holding one dataset that owns NODE_ID, with no part state yet.""" + reg = VisorDatasetRegistry() + reg.datasets = {1: _make_part_dataset(1, [NODE_ID])} + return reg + + +@pytest.fixture +def scene(renderer, registry, pipeline): + """Concrete scene wired to the real renderer, pipeline and registry.""" + s = _ConcreteScene(MagicMock(name="server"), dark_mode=False, renderer=renderer) + s._dataset_registry = registry + s._renderer.flush_wasm_state = MagicMock(name="flush_wasm_state") + return s + + +def _spy_flush(scene, read_back): + """Replace flush_wasm_state with a spy recording lock depth and VTK state. + + ``read_back`` is called at flush time and its value recorded, so a test + can assert the pipeline apply had *already* happened when the flush ran. + A flush ordered before the apply would push pre-mutation state, and the + client's own reapply would hide that in the browser. + """ + record = {"calls": 0, "depth": None, "value": None} + + def _flush(): + record["calls"] += 1 + record["depth"] = scene._vtk_lock.depth + record["value"] = read_back() + + scene._renderer.flush_wasm_state = _flush + return record + + +# =========================================================================== +# set_part_visibility +# =========================================================================== + +def test_set_part_visibility_writes_the_registry_record(scene, registry): + """Store half: the record carries the requested visibility.""" + scene.set_part_visibility(NODE_ID, False) + + assert registry.get_part_state(NODE_ID).visible is False + + +def test_set_part_visibility_applies_to_the_vtk_actor(scene, pipeline): + """Apply half: the actor's visibility is off.""" + scene.set_part_visibility(NODE_ID, False) + + assert pipeline.actor.GetVisibility() == 0 + + +def test_set_part_visibility_flushes_under_the_lock_after_the_apply(scene, pipeline): + """Lock held, and the actor already mutated, when the flush runs.""" + scene._vtk_lock = _LockSpy() + record = _spy_flush(scene, lambda: pipeline.actor.GetVisibility()) + + scene.set_part_visibility(NODE_ID, False) + + assert record["calls"] == 1 + assert record["depth"] >= 1 + assert record["value"] == 0 + assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count + assert scene._vtk_lock.depth == 0 + + +# =========================================================================== +# set_part_opacity +# =========================================================================== + +def test_set_part_opacity_writes_the_registry_record(scene, registry): + """Store half: the record carries the requested opacity.""" + scene.set_part_opacity(NODE_ID, 0.25) + + assert registry.get_part_state(NODE_ID).opacity == 0.25 + + +def test_set_part_opacity_applies_to_the_vtk_property(scene, pipeline): + """Apply half: the actor property's opacity is the requested value.""" + scene.set_part_opacity(NODE_ID, 0.25) + + assert pipeline.actor.GetProperty().GetOpacity() == pytest.approx(0.25) + + +def test_set_part_opacity_flushes_under_the_lock_after_the_apply(scene, pipeline): + """Lock held, and the property already mutated, when the flush runs.""" + scene._vtk_lock = _LockSpy() + record = _spy_flush(scene, lambda: pipeline.actor.GetProperty().GetOpacity()) + + scene.set_part_opacity(NODE_ID, 0.25) + + assert record["calls"] == 1 + assert record["depth"] >= 1 + assert record["value"] == pytest.approx(0.25) + assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count + + +# =========================================================================== +# set_part_diffuse_color +# =========================================================================== + +def test_set_part_diffuse_color_writes_the_registry_record(scene, registry): + """Store half: the record carries the requested colour.""" + scene.set_part_diffuse_color(NODE_ID, [1.0, 0.0, 0.0]) + + assert registry.get_part_state(NODE_ID).diffuse_rgb == [1.0, 0.0, 0.0] + + +def test_set_part_diffuse_color_applies_to_the_vtk_property(scene, pipeline): + """Apply half: the actor property's diffuse colour is the requested one.""" + scene.set_part_diffuse_color(NODE_ID, [1.0, 0.0, 0.0]) + + assert pipeline.actor.GetProperty().GetDiffuseColor() == pytest.approx((1.0, 0.0, 0.0)) + + +def test_set_part_diffuse_color_none_clears_the_registry_record(scene, registry): + """Store half of a clear: absence is stored as absence, not as a colour.""" + registry.set_part_diffuse_color(NODE_ID, [1.0, 0.0, 0.0]) + + scene.set_part_diffuse_color(NODE_ID, None) + + assert registry.get_part_state(NODE_ID).diffuse_rgb is None + + +def test_set_part_diffuse_color_none_applies_the_default_mesh_colour(scene, pipeline): + """Apply half of a clear: the pipeline falls back to the default colour.""" + scene.set_part_diffuse_color(NODE_ID, [1.0, 0.0, 0.0]) + + scene.set_part_diffuse_color(NODE_ID, None) + + assert pipeline.actor.GetProperty().GetDiffuseColor() == pytest.approx( + tuple(VisorColors.DefaultMeshColor) + ) + + +def test_set_part_diffuse_color_flushes_under_the_lock_after_the_apply(scene, pipeline): + """Lock held, and the colour already applied, when the flush runs.""" + scene._vtk_lock = _LockSpy() + record = _spy_flush(scene, lambda: pipeline.actor.GetProperty().GetDiffuseColor()) + + scene.set_part_diffuse_color(NODE_ID, [1.0, 0.0, 0.0]) + + assert record["calls"] == 1 + assert record["depth"] >= 1 + assert record["value"] == pytest.approx((1.0, 0.0, 0.0)) + assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count + + +# =========================================================================== +# set_part_selected +# =========================================================================== + +def test_set_part_selected_writes_the_registry_record(scene, registry): + """Store half: the record carries the requested selection state.""" + scene.set_part_selected(NODE_ID, True) + + assert registry.get_part_state(NODE_ID).selected is True + + +def test_set_part_selected_applies_the_highlight_to_the_vtk_property(scene, pipeline): + """Apply half: the selection lighting terms are on the actor property.""" + scene.set_part_selected(NODE_ID, True) + + prop = pipeline.actor.GetProperty() + assert prop.GetAmbient() == pytest.approx(0.5) + assert prop.GetDiffuse() == pytest.approx(0.5) + assert prop.GetAmbientColor() == pytest.approx((0.0, 62 / 255, 111 / 255)) + + +def test_set_part_selected_uses_the_stored_diffuse_colour(scene, registry, pipeline): + """The colour comes from the server's own record, not from the caller.""" + registry.set_part_diffuse_color(NODE_ID, [0.25, 0.5, 0.75]) + + scene.set_part_selected(NODE_ID, True) + + assert pipeline.actor.GetProperty().GetDiffuseColor() == pytest.approx((0.25, 0.5, 0.75)) + + +def test_set_part_selected_falls_back_to_the_default_mesh_colour(scene, pipeline): + """With no stored colour, the default constant is used.""" + scene.set_part_selected(NODE_ID, True) + + assert pipeline.actor.GetProperty().GetDiffuseColor() == pytest.approx( + tuple(VisorColors.DefaultMeshColor) + ) + + +def test_set_part_selected_flushes_under_the_lock_after_the_apply(scene, pipeline): + """Lock held, and the highlight already applied, when the flush runs.""" + scene._vtk_lock = _LockSpy() + record = _spy_flush(scene, lambda: pipeline.actor.GetProperty().GetAmbient()) + + scene.set_part_selected(NODE_ID, True) + + assert record["calls"] == 1 + assert record["depth"] >= 1 + assert record["value"] == pytest.approx(0.5) + assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count + + +# =========================================================================== +# set_part_color_variable +# =========================================================================== + +def test_set_part_color_variable_writes_the_registry_record(scene, registry): + """Store half: variable id and component are recorded together.""" + scene.set_part_color_variable( + NODE_ID, "POINT::pressure::1", VisorVtkVariableType.POINT, "pressure", 0, 0.0, 49.0 + ) + + state = registry.get_part_state(NODE_ID) + assert state.spectrum_id == "POINT::pressure::1" + assert state.spectrum_component == 0 + + +def test_set_part_color_variable_applies_to_the_vtk_mapper(scene, pipeline): + """Apply half: the mapper selects the array and honours the given range.""" + scene.set_part_color_variable( + NODE_ID, "POINT::pressure::1", VisorVtkVariableType.POINT, "pressure", 0, 0.0, 49.0 + ) + + mapper = pipeline.mapper + assert mapper.GetScalarVisibility() == 1 + assert mapper.GetArrayName() == "pressure" + assert mapper.GetArrayComponent() == 0 + assert mapper.GetScalarRange() == pytest.approx((0.0, 49.0)) + + +def test_set_part_color_variable_applies_a_cell_association(scene, pipeline): + """Apply half, cell branch: the cell array is selected.""" + scene.set_part_color_variable( + NODE_ID, "CELL::temperature::1", VisorVtkVariableType.CELL, "temperature", 0, 0.0, 95.0 + ) + + assert pipeline.mapper.GetArrayName() == "temperature" + assert pipeline.mapper.GetScalarVisibility() == 1 + + +def test_set_part_color_variable_flushes_under_the_lock_after_the_apply(scene, pipeline): + """Lock held, and the mapper already configured, when the flush runs.""" + scene._vtk_lock = _LockSpy() + record = _spy_flush(scene, lambda: pipeline.mapper.GetArrayName()) + + scene.set_part_color_variable( + NODE_ID, "POINT::pressure::1", VisorVtkVariableType.POINT, "pressure", 0, 0.0, 49.0 + ) + + assert record["calls"] == 1 + assert record["depth"] >= 1 + assert record["value"] == "pressure" + assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count + + +# =========================================================================== +# clear_part_color_variable +# =========================================================================== + +def test_clear_part_color_variable_clears_the_registry_record(scene, registry): + """Store half: both fields are cleared together.""" + registry.set_part_color_variable(NODE_ID, "POINT::pressure::1", 0) + + scene.clear_part_color_variable(NODE_ID) + + state = registry.get_part_state(NODE_ID) + assert state.spectrum_id is None + assert state.spectrum_component is None + + +def test_clear_part_color_variable_disables_scalar_visibility_on_the_mapper(scene, pipeline): + """Apply half: scalar colouring is off on the mapper.""" + scene.set_part_color_variable( + NODE_ID, "POINT::pressure::1", VisorVtkVariableType.POINT, "pressure", 0, 0.0, 49.0 + ) + + scene.clear_part_color_variable(NODE_ID) + + assert pipeline.mapper.GetScalarVisibility() == 0 + + +def test_clear_part_color_variable_flushes_under_the_lock_after_the_apply(scene, pipeline): + """Lock held, and scalar visibility already off, when the flush runs.""" + scene._vtk_lock = _LockSpy() + record = _spy_flush(scene, lambda: pipeline.mapper.GetScalarVisibility()) + + scene.clear_part_color_variable(NODE_ID) + + assert record["calls"] == 1 + assert record["depth"] >= 1 + assert record["value"] == 0 + assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count + + +# =========================================================================== +# Unknown node id: logged no-op at every layer +# =========================================================================== + +COORDINATOR_CALLS = { + "set_part_visibility": (UNKNOWN_NODE_ID, False), + "set_part_opacity": (UNKNOWN_NODE_ID, 0.25), + "set_part_diffuse_color": (UNKNOWN_NODE_ID, [1.0, 0.0, 0.0]), + "set_part_selected": (UNKNOWN_NODE_ID, True), + "set_part_color_variable": ( + UNKNOWN_NODE_ID, + "POINT::pressure::1", + VisorVtkVariableType.POINT, + "pressure", + 0, + 0.0, + 49.0, + ), + "clear_part_color_variable": (UNKNOWN_NODE_ID,), +} + + +@pytest.mark.parametrize("name", list(COORDINATOR_CALLS)) +def test_unknown_node_id_is_a_logged_no_op(scene, registry, pipeline, name): + """No raise, no store write, no VTK mutation and no flush.""" + scene._renderer.flush_wasm_state = MagicMock(name="flush_wasm_state") + pipeline.actor.SetVisibility(1) + pipeline.actor.GetProperty().SetOpacity(1.0) + + with patch("ansys.visor.viewer.vtk.scene.base.logger") as mock_logger: + assert getattr(scene, name)(*COORDINATOR_CALLS[name]) is None + + assert mock_logger.debug.call_count == 1 + assert registry.get_part_state(UNKNOWN_NODE_ID) is None + assert pipeline.actor.GetVisibility() == 1 + assert pipeline.actor.GetProperty().GetOpacity() == pytest.approx(1.0) + scene._renderer.flush_wasm_state.assert_not_called() + + +def test_seeded_part_state_is_mutated_in_place(scene, registry): + """An existing record is mutated in place, not replaced.""" + seeded = RuntimePartProperties(id=NODE_ID) + registry.datasets[1].state.part_states[NODE_ID] = seeded + + scene.set_part_opacity(NODE_ID, 0.25) + + assert registry.get_part_state(NODE_ID) is seeded + + +# =========================================================================== +# The ten existing public entry points hold the lock +# =========================================================================== + +@pytest.fixture +def mocked_scene(renderer): + """Scene whose scene graph, registry and renderer are all MagicMocks. + + Lets the ten existing entry points run to completion without real VTK, so + the assertion is purely about the lock. + """ + s = _ConcreteScene(MagicMock(name="server"), dark_mode=False, renderer=renderer) + s._scene_graph = MagicMock(name="scene_graph") + s._dataset_registry = MagicMock(name="dataset_registry") + s._dataset_registry.count = 1 + s._renderer = MagicMock(name="renderer") + s._state_mapper = MagicMock(name="state_mapper") + s._vtk_lock = _LockSpy() + return s + + +def _depth_probe(mocked_scene, owner, attribute): + """Record the lock depth observed inside a delegated call.""" + observed = {} + + def _side_effect(*args, **kwargs): + observed["depth"] = mocked_scene._vtk_lock.depth + return MagicMock() + + getattr(owner, attribute).side_effect = _side_effect + return observed + + +def test_clear_holds_the_lock(mocked_scene): + """clear() deregisters actors with the lock held.""" + observed = _depth_probe(mocked_scene, mocked_scene._renderer, "deregister_all") + + mocked_scene.clear() + + assert observed["depth"] >= 1 + assert mocked_scene._vtk_lock.enter_count == mocked_scene._vtk_lock.exit_count + + +def test_add_dataset_holds_the_lock(mocked_scene): + """add_dataset() registers nodes with the lock held.""" + subtree = mocked_scene._scene_graph.get_descendant_node.return_value + subtree.get_descendant_part_nodes.return_value = [MagicMock(name="leaf")] + observed = _depth_probe(mocked_scene, mocked_scene._renderer, "register_node") + + mocked_scene.add_dataset(MagicMock(name="input"), MagicMock(name="metadata")) + + assert observed["depth"] >= 1 + assert mocked_scene._vtk_lock.enter_count == mocked_scene._vtk_lock.exit_count + + +def test_remove_dataset_holds_the_lock(mocked_scene): + """remove_dataset() deregisters nodes with the lock held.""" + node = mocked_scene._scene_graph.get_descendant_node.return_value + node.get_descendant_part_nodes.return_value = [MagicMock(name="leaf")] + observed = _depth_probe(mocked_scene, mocked_scene._renderer, "deregister_node") + + mocked_scene.remove_dataset(1) + + assert observed["depth"] >= 1 + assert mocked_scene._vtk_lock.enter_count == mocked_scene._vtk_lock.exit_count + + +def test_update_variables_for_dataset_holds_the_lock(mocked_scene): + """update_variables_for_dataset() updates arrays with the lock held.""" + observed = _depth_probe(mocked_scene, mocked_scene._dataset_registry, "update_variables") + + mocked_scene.update_variables_for_dataset(1, []) + + assert observed["depth"] >= 1 + assert mocked_scene._vtk_lock.enter_count == mocked_scene._vtk_lock.exit_count + + +def test_apply_state_holds_the_lock(mocked_scene): + """apply_state() runs the renderer-specific step with the lock held.""" + observed = {} + mocked_scene._apply_runtime_state_to_render = lambda state: observed.update( + depth=mocked_scene._vtk_lock.depth + ) + + mocked_scene.apply_state(MagicMock(name="persisted_state")) + + assert observed["depth"] >= 1 + assert mocked_scene._vtk_lock.enter_count == mocked_scene._vtk_lock.exit_count + + +def test_render_holds_the_lock(mocked_scene): + """render() renders and pushes to wasm with the lock held.""" + observed = _depth_probe(mocked_scene, mocked_scene._renderer, "render") + + mocked_scene.render() + + assert observed["depth"] >= 1 + assert mocked_scene._vtk_lock.enter_count == mocked_scene._vtk_lock.exit_count + + +def test_update_widgets_holds_the_lock(mocked_scene): + """update_widgets() mutates the widget VTK objects with the lock held.""" + observed = _depth_probe(mocked_scene, mocked_scene._renderer, "update_bounds") + + mocked_scene.update_widgets() + + assert observed["depth"] >= 1 + assert mocked_scene._vtk_lock.enter_count == mocked_scene._vtk_lock.exit_count + + +def test_populate_scene_holds_the_lock(mocked_scene): + """populate_scene() reaches the widget update with the lock held.""" + observed = _depth_probe(mocked_scene, mocked_scene._renderer, "update_actor_count") + + mocked_scene.populate_scene() + + assert observed["depth"] >= 1 + assert mocked_scene._vtk_lock.enter_count == mocked_scene._vtk_lock.exit_count + + +def test_finalize_scene_holds_the_lock(mocked_scene): + """finalize_scene() reaches the render with the lock held.""" + observed = _depth_probe(mocked_scene, mocked_scene._renderer, "render") + + mocked_scene.finalize_scene() + + assert observed["depth"] >= 1 + assert mocked_scene._vtk_lock.enter_count == mocked_scene._vtk_lock.exit_count + + +def test_reset_camera_holds_the_lock(mocked_scene): + """reset_camera() mutates the VTK camera with the lock held.""" + observed = _depth_probe(mocked_scene, mocked_scene._renderer, "reset_camera") + + mocked_scene.reset_camera() + + assert observed["depth"] >= 1 + assert mocked_scene._vtk_lock.enter_count == mocked_scene._vtk_lock.exit_count + + +def test_nested_entry_points_reacquire_the_lock(mocked_scene): + """finalize_scene -> populate_scene / render nests; the RLock allows it.""" + mocked_scene.finalize_scene() + + assert mocked_scene._vtk_lock.max_depth >= 2 + assert mocked_scene._vtk_lock.depth == 0 + assert mocked_scene._vtk_lock.enter_count == mocked_scene._vtk_lock.exit_count + + +def test_the_scene_lock_is_reentrant(): + """The scene's own lock can be acquired twice on one thread.""" + scene = _ConcreteScene(MagicMock(name="server"), renderer=MagicMock(name="renderer")) + + with scene._vtk_lock: + with scene._vtk_lock: + assert scene._vtk_lock is not None + + + diff --git a/tests/unit/vtk/scene/test_local_scene.py b/tests/unit/vtk/scene/test_local_scene.py index 6ae337ea..5f86c071 100644 --- a/tests/unit/vtk/scene/test_local_scene.py +++ b/tests/unit/vtk/scene/test_local_scene.py @@ -1,4 +1,5 @@ import json +import threading from types import SimpleNamespace from unittest.mock import MagicMock, patch @@ -720,6 +721,10 @@ def __init__(self): # do not call super().__init__ to avoid VTK setup # set only the attributes used by finalize_scene self._dataset_registry = SimpleNamespace(count=0) + # DummyScene skips super().__init__, so it mirrors VisorSceneBase.__init__ state by + # hand; any state added to the base constructor must be mirrored here too. RLock, + # not Lock, to match production re-entrancy. + self._vtk_lock = threading.RLock() self._populate_called = False self._reset_called = False self._render_called = False From 34b27f00b06c50d822de72aefa904cabc4cbdc14 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Fri, 28 Aug 2026 12:27:03 -0700 Subject: [PATCH 10/28] replace hand-written payloads in LocalApp with Pydantic models and decorator --- src/ansys/visor/viewer/app/trame/local_app.py | 202 ++++++++++++++---- tests/unit/app/test_local_app.py | 175 +++++++++++++++ 2 files changed, 335 insertions(+), 42 deletions(-) diff --git a/src/ansys/visor/viewer/app/trame/local_app.py b/src/ansys/visor/viewer/app/trame/local_app.py index b865b9b3..23abd24f 100644 --- a/src/ansys/visor/viewer/app/trame/local_app.py +++ b/src/ansys/visor/viewer/app/trame/local_app.py @@ -1,7 +1,9 @@ +import functools import urllib.parse from logging import Logger from typing import List, Optional, Protocol +from pydantic import BaseModel, Field, ValidationError from trame.app.core import Server from trame.decorators import TrameApp, trigger from vtk import vtkObject @@ -47,6 +49,111 @@ def set_part_color_variable( def clear_part_color_variable(self, node_id: int) -> None: ... +# ---------------------------------------------------------------------- +# Trigger payload models +# +# One model per per-part trigger. Field names are snake_case; the +# camelCase wire keys the client sends are carried as pydantic aliases. +## ---------------------------------------------------------------------- + + +class SetPartVisibilityPayload(BaseModel): + """Payload of the ``set_part_visibility`` trigger.""" + + node_id: int = Field(alias="nodeId") + visible: bool + + +class SetPartOpacityPayload(BaseModel): + """Payload of the ``set_part_opacity`` trigger.""" + + node_id: int = Field(alias="nodeId") + opacity: float = Field(ge=0.0, le=1.0) + + +class SetPartDiffuseColorPayload(BaseModel): + """Payload of the ``set_part_diffuse_color`` trigger.""" + + node_id: int = Field(alias="nodeId") + diffuse_rgb: Optional[List[float]] = Field(min_length=3, max_length=3, alias="diffuseRgb") + + +class SetPartSelectedPayload(BaseModel): + """Payload of the ``set_part_selected`` trigger. + + No colour crosses this trigger: the server reads the part's stored + diffuse colour from its own record. + """ + + node_id: int = Field(alias="nodeId") + selected: bool + + +class SetPartColorVariablePayload(BaseModel): + """Payload of the ``set_part_color_variable`` trigger.""" + + node_id: int = Field(alias="nodeId") + variable_id: str = Field(alias="variableId") + association: VisorVtkVariableType + array_name: str = Field(alias="arrayName") + component: int + min_val: float = Field(alias="min") + max_val: float = Field(alias="max") + + +class ClearPartColorVariablePayload(BaseModel): + """Payload of the ``clear_part_color_variable`` trigger.""" + + node_id: int = Field(alias="nodeId") + + +def parse_payload(model: type[BaseModel]): + """Validate a trigger payload into *model*, or make the call a logged no-op. + + The wrapped handler receives the parsed model in place of the raw + ``dict``. A payload that does not validate never reaches the handler + body: it is logged at warning and the trigger returns ``None``. + + Posture. This applies the same logged-no-op posture the whole per-part + path uses for an unresolvable node id, extended to a malformed payload. + The reasoning transfers because it is about the *thread*, not about the + kind of badness: trigger handlers run on trame's daemon event-loop + thread, where a raise surfaces to no caller who can act on it. Before + this decorator the handlers indexed their payloads directly and a + missing key raised ``KeyError`` there. + + ``model_validate``, never ``model(**payload)``. A payload that is not + a mapping at all -- a bare string, a number, a list -- raises + ``TypeError`` from ``**`` but a well-formed ``ValidationError`` from + ``model_validate``. Only the latter lets one guard catch every + malformed shape instead of most of them. + + Decorator order. ``@trigger(...)`` goes **outermost**, above this one. + Both orders happen to work: trame's ``@trigger`` stamps + ``_trame_trigger_names`` on whatever function it is handed, and + ``functools.wraps`` copies ``__dict__`` outward, so an inner + ``@trigger`` is still found by ``TrameApp``'s registration loop. + Outermost is the order whose correctness does not depend on that + copying behaviour, so it is the one that is correct by design rather + than by accident. + """ + + def decorate(handler): + @functools.wraps(handler) + def wrapper(self, payload): + try: + parsed = model.model_validate(payload) + except ValidationError as exc: + logger.warning( + "%s: invalid payload; ignoring. %s", handler.__name__, exc + ) + return None + return handler(self, parsed) + + return wrapper + + return decorate + @TrameApp() class LocalApp: @@ -201,14 +308,20 @@ def perf_report_server_update(self, payload: dict): # ------------------------------------------------------------------ # Per-part visual state triggers # - # Frontend -> Backend. Each takes a single ``payload: dict`` argument and - # returns ``None``; each delegates to the identically-named method on the - # injected coordinator. Every payload carries the absolute target value, - # never a toggle or a delta, so a message the client suppresses as - # redundant is indistinguishable from one that set a value a part already - # had. Required payload keys are read directly: a missing key raises - # KeyError on the trame event-loop thread, which is accepted because the - # only caller is the client written against this contract. + # Frontend -> Backend. Each takes a single ``payload: dict`` argument + # and returns ``None``; each delegates to the identically-named method + # on the injected coordinator. Every payload carries the absolute + # target value, never a toggle or a delta, so a message the client + # suppresses as redundant is indistinguishable from one that set a + # value a part already had. + # + # Payloads are validated at this boundary by ``@parse_payload``, which + # hands the handler a parsed model instead of the raw dict. A payload + # that does not validate -- a missing key, a wrong-typed value, an + # association that is not an enum member, a diffuse colour that is not + # three components, an opacity outside [0, 1], or a payload that is not + # a mapping at all -- is a logged no-op and never reaches a handler + # body. # ------------------------------------------------------------------ def _part_state_api(self, trigger_name: str) -> ScenePartStateApi | None: @@ -219,35 +332,45 @@ def _part_state_api(self, trigger_name: str) -> ScenePartStateApi | None: return self._scene_part_state_api @trigger("set_part_visibility") - def set_part_visibility(self, payload: dict) -> None: + @parse_payload(SetPartVisibilityPayload) + def set_part_visibility(self, payload) -> None: """Frontend -> Backend: set whether one part is visible.""" api = self._part_state_api("set_part_visibility") if api is None: return - api.set_part_visibility(payload["nodeId"], payload["visible"]) + api.set_part_visibility(payload.node_id, payload.visible) @trigger("set_part_opacity") - def set_part_opacity(self, payload: dict) -> None: - """Frontend -> Backend: set one part's opacity.""" + @parse_payload(SetPartOpacityPayload) + def set_part_opacity(self, payload) -> None: + """Frontend -> Backend: set one part's opacity. + + An opacity outside ``[0.0, 1.0]`` fails validation and is a logged + no-op; it does not reach VTK to be clamped. + """ api = self._part_state_api("set_part_opacity") if api is None: return - api.set_part_opacity(payload["nodeId"], payload["opacity"]) + api.set_part_opacity(payload.node_id, payload.opacity) @trigger("set_part_diffuse_color") - def set_part_diffuse_color(self, payload: dict) -> None: + @parse_payload(SetPartDiffuseColorPayload) + def set_part_diffuse_color(self, payload) -> None: """Frontend -> Backend: set one part's custom diffuse colour. ``diffuseRgb`` of ``None`` clears the custom colour and is forwarded - as ``None``; no default colour is substituted here. + as ``None``; no default colour is substituted here. An absent key, + or a colour that is not exactly three components, is a logged + no-op -- nothing is delegated, so nothing is written to the store. """ api = self._part_state_api("set_part_diffuse_color") if api is None: return - api.set_part_diffuse_color(payload["nodeId"], payload["diffuseRgb"]) + api.set_part_diffuse_color(payload.node_id, payload.diffuse_rgb) @trigger("set_part_selected") - def set_part_selected(self, payload: dict) -> None: + @parse_payload(SetPartSelectedPayload) + def set_part_selected(self, payload) -> None: """Frontend -> Backend: select or deselect one part. No colour crosses this trigger: the server reads the part's stored @@ -256,47 +379,42 @@ def set_part_selected(self, payload: dict) -> None: api = self._part_state_api("set_part_selected") if api is None: return - api.set_part_selected(payload["nodeId"], payload["selected"]) + api.set_part_selected(payload.node_id, payload.selected) @trigger("set_part_color_variable") - def set_part_color_variable(self, payload: dict) -> None: + @parse_payload(SetPartColorVariablePayload) + def set_part_color_variable(self, payload) -> None: """Frontend -> Backend: colour one part by a scalar variable. - ``association`` is parsed here, at the boundary, into a - :class:`VisorVtkVariableType` by exact value lookup -- never - upper-cased, never passed on as a bare string. A value that is not a - member is a logged no-op, matching the posture the pipeline takes on an - unknown array name. ``variableId`` is forwarded verbatim and is never - parsed by the server. + ``association`` is resolved at this boundary into a + :class:`VisorVtkVariableType` by the payload model, which matches by + exact value -- never upper-cased, never passed on as a bare string. + A value that is not a member fails validation and is a logged + no-op, matching the posture the pipeline takes on an unknown array + name. ``variableId`` is forwarded verbatim and is never parsed by + the server. """ api = self._part_state_api("set_part_color_variable") if api is None: return - try: - association = VisorVtkVariableType(payload["association"]) - except ValueError: - logger.warning( - "set_part_color_variable: %r is not a VisorVtkVariableType; ignoring.", - payload["association"], - ) - return api.set_part_color_variable( - payload["nodeId"], - payload["variableId"], - association, - payload["arrayName"], - payload["component"], - payload["min"], - payload["max"], + payload.node_id, + payload.variable_id, + payload.association, + payload.array_name, + payload.component, + payload.min_val, + payload.max_val, ) @trigger("clear_part_color_variable") - def clear_part_color_variable(self, payload: dict) -> None: + @parse_payload(ClearPartColorVariablePayload) + def clear_part_color_variable(self, payload) -> None: """Frontend -> Backend: stop colouring one part by a scalar variable.""" api = self._part_state_api("clear_part_color_variable") if api is None: return - api.clear_part_color_variable(payload["nodeId"]) + api.clear_part_color_variable(payload.node_id) def set_only_cookie(self, key: str, value: str): """ diff --git a/tests/unit/app/test_local_app.py b/tests/unit/app/test_local_app.py index d71e1315..25ea4498 100644 --- a/tests/unit/app/test_local_app.py +++ b/tests/unit/app/test_local_app.py @@ -12,6 +12,18 @@ 4. ``diffuseRgb`` of ``None`` is forwarded as ``None`` (a clear), and ``set_part_selected`` carries no colour at all. 5. With no coordinator injected, every trigger is a logged no-op. +6. A malformed payload is a logged no-op and never reaches a handler body: + a missing required key, a wrong-typed value, a payload that is not a + mapping at all, a ``diffuseRgb`` that is not exactly three components, + or an opacity outside ``[0.0, 1.0]``. +7. An unknown extra key is ignored, not rejected: the rest of the payload + is still forwarded. +8. All six trigger names are still registered with the trame server after + the payload decorator wraps the handlers, and the registered callable + still accepts a raw ``dict``. +9. The two logged-no-op paths stay distinct: an invalid payload logs + ``warning`` and no ``debug``; a missing coordinator logs ``debug`` and + no ``warning``. The coordinator is a single MagicMock standing in for the injected object; what each trigger does with the values it forwards is asserted in @@ -279,3 +291,166 @@ def test_trigger_is_a_logged_no_op_when_no_coordinator_injected(app_without_api, assert mock_logger.debug.call_count == 1 + +# =========================================================================== +# Payload validation +# =========================================================================== + +# One required key per trigger, removed to make an otherwise valid payload +# malformed. Written out per trigger rather than derived from the models. +MISSING_KEY = { + "set_part_visibility": "visible", + "set_part_opacity": "opacity", + "set_part_diffuse_color": "diffuseRgb", + "set_part_selected": "selected", + "set_part_color_variable": "arrayName", + "clear_part_color_variable": "nodeId", +} + + +@pytest.mark.parametrize("name", TRIGGER_NAMES) +def test_missing_required_key_is_a_logged_no_op(app, api, name): + """A payload short one required key delegates nothing and warns once.""" + payload = {k: v for k, v in PAYLOADS[name].items() if k != MISSING_KEY[name]} + + with patch("ansys.visor.viewer.app.trame.local_app.logger") as mock_logger: + assert getattr(app, name)(payload) is None + + getattr(api, name).assert_not_called() + assert mock_logger.warning.call_count == 1 + + +def test_wrong_typed_bool_value_is_a_logged_no_op(app, api): + """2 is not a bool: pydantic accepts 0 and 1 only, so this fails.""" + with patch("ansys.visor.viewer.app.trame.local_app.logger") as mock_logger: + assert app.set_part_visibility({"nodeId": 7, "visible": 2}) is None + + api.set_part_visibility.assert_not_called() + assert mock_logger.warning.call_count == 1 + + +def test_wrong_typed_int_value_is_a_logged_no_op(app, api): + """A non-numeric string is not coerced to int: 'x' fails nodeId.""" + with patch("ansys.visor.viewer.app.trame.local_app.logger") as mock_logger: + assert app.set_part_visibility({"nodeId": "x", "visible": False}) is None + + api.set_part_visibility.assert_not_called() + assert mock_logger.warning.call_count == 1 + + +@pytest.mark.parametrize("payload", ["nope", 42, None, [1, 2, 3]]) +def test_non_dict_payload_is_a_logged_no_op_not_a_type_error(app, api, payload): + """A payload that is not a mapping is caught, not raised as TypeError. + + This is why the decorator uses ``model_validate`` rather than + ``Model(**payload)``: the latter raises TypeError on a non-mapping, + which would escape the guard and land on the trame daemon thread. + """ + with patch("ansys.visor.viewer.app.trame.local_app.logger") as mock_logger: + assert app.set_part_visibility(payload) is None + + api.set_part_visibility.assert_not_called() + assert mock_logger.warning.call_count == 1 + + +def test_unknown_extra_key_is_forwarded_not_rejected(app, api): + """A key this server does not know about is dropped, not an error.""" + app.set_part_visibility({"nodeId": 7, "visible": False, "someFutureKey": 1}) + + api.set_part_visibility.assert_called_once_with(7, False) + + +def test_set_part_diffuse_color_absent_key_is_a_validation_failure(app, api): + """An absent diffuseRgb is malformed -- distinct from an explicit null.""" + with patch("ansys.visor.viewer.app.trame.local_app.logger") as mock_logger: + assert app.set_part_diffuse_color({"nodeId": 7}) is None + + api.set_part_diffuse_color.assert_not_called() + assert mock_logger.warning.call_count == 1 + + +def test_set_part_diffuse_color_two_element_colour_is_a_logged_no_op(app, api): + """The torn write: a short colour must not reach the coordinator. + + The coordinator writes the store first and only then indexes the colour + at [0], [1] and [2] to apply it. A two-element list therefore left the + store holding a malformed colour, the VTK object untouched, and an + IndexError on the trame daemon thread. Rejecting it here means nothing + is delegated, so nothing is written. + """ + with patch("ansys.visor.viewer.app.trame.local_app.logger") as mock_logger: + assert app.set_part_diffuse_color({"nodeId": 7, "diffuseRgb": [1.0, 0.0]}) is None + + api.set_part_diffuse_color.assert_not_called() + assert mock_logger.warning.call_count == 1 + + +def test_set_part_opacity_out_of_range_is_a_logged_no_op(app, api): + """An opacity outside [0.0, 1.0] never reaches VTK to be clamped.""" + with patch("ansys.visor.viewer.app.trame.local_app.logger") as mock_logger: + assert app.set_part_opacity({"nodeId": 7, "opacity": 5.0}) is None + + api.set_part_opacity.assert_not_called() + assert mock_logger.warning.call_count == 1 + + +# =========================================================================== +# Trigger registration survives decoration +# =========================================================================== + +def _registered_triggers(mock_server): + """Recover {trigger name: registered callable} from the mock server. + + TrameApp's wrapped __init__ registers each handler as + ``server.trigger(name)(fn)``, so the i-th name passed to ``trigger`` + pairs with the i-th function passed to its return value. + """ + names = [call.args[0] for call in mock_server.trigger.call_args_list] + functions = [call.args[0] for call in mock_server.trigger.return_value.call_args_list] + return dict(zip(names, functions)) + + +@pytest.mark.parametrize("name", TRIGGER_NAMES) +def test_trigger_name_is_still_registered_after_decoration(app, mock_server, name): + """Wrapping the handlers does not lose the @trigger registration.""" + assert name in _registered_triggers(mock_server) + + +def test_the_registered_callable_still_accepts_a_raw_dict(app, api, mock_server): + """Dispatch reaches the coordinator through the wrapper, not past it.""" + registered = _registered_triggers(mock_server) + + registered["set_part_visibility"]({"nodeId": 7, "visible": False}) + + api.set_part_visibility.assert_called_once_with(7, False) + + +# =========================================================================== +# The warning and debug paths stay distinct +# =========================================================================== +# +# Note: the parse now runs before the API lookup, so a malformed payload +# reaching a LocalApp with no coordinator injected logs warning, where it +# would previously have logged debug. Both are logged no-ops. + +@pytest.mark.parametrize("name", TRIGGER_NAMES) +def test_invalid_payload_logs_warning_and_not_debug(app, api, name): + """The validation no-op is a warning and is not confused with the other.""" + with patch("ansys.visor.viewer.app.trame.local_app.logger") as mock_logger: + assert getattr(app, name)({"junk": True}) is None + + getattr(api, name).assert_not_called() + assert mock_logger.warning.call_count == 1 + assert mock_logger.debug.call_count == 0 + + +@pytest.mark.parametrize("name", TRIGGER_NAMES) +def test_missing_coordinator_logs_debug_and_not_warning(app_without_api, name): + """The not-injected no-op is a debug and is not confused with the other.""" + with patch("ansys.visor.viewer.app.trame.local_app.logger") as mock_logger: + assert getattr(app_without_api, name)(PAYLOADS[name]) is None + + assert mock_logger.debug.call_count == 1 + assert mock_logger.warning.call_count == 0 + + From 77f9a9dceb262adc83ec1a7ddddc03ccab562d43 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Fri, 28 Aug 2026 12:28:21 -0700 Subject: [PATCH 11/28] comment out wasm flush --- src/ansys/visor/viewer/vtk/scene/base.py | 18 ++++++---- tests/unit/vtk/scene/test_base.py | 42 ++++++++++++++---------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/ansys/visor/viewer/vtk/scene/base.py b/src/ansys/visor/viewer/vtk/scene/base.py index 344b198a..c5b157f7 100644 --- a/src/ansys/visor/viewer/vtk/scene/base.py +++ b/src/ansys/visor/viewer/vtk/scene/base.py @@ -400,7 +400,8 @@ def set_part_visibility(self, node_id: int, visible: bool) -> None: logger.debug("set_part_visibility: no dataset owns node %s; skipping.", node_id) return self._renderer.apply_visibility(node_id, visible) - self._renderer.flush_wasm_state() + # TODO: uncomment when adding round trips + # self._renderer.flush_wasm_state() def set_part_opacity(self, node_id: int, opacity: float) -> None: """Set the opacity of the part identified by *node_id*.""" @@ -409,7 +410,8 @@ def set_part_opacity(self, node_id: int, opacity: float) -> None: logger.debug("set_part_opacity: no dataset owns node %s; skipping.", node_id) return self._renderer.apply_opacity(node_id, opacity) - self._renderer.flush_wasm_state() + # TODO: uncomment when adding round trips + # self._renderer.flush_wasm_state() def set_part_diffuse_color(self, node_id: int, diffuse_rgb: list[float] | None) -> None: """ @@ -431,7 +433,8 @@ def set_part_diffuse_color(self, node_id: int, diffuse_rgb: list[float] | None) self._renderer.apply_diffuse_color( node_id, applied_rgb[0], applied_rgb[1], applied_rgb[2] ) - self._renderer.flush_wasm_state() + # TODO: uncomment when adding round trips + # self._renderer.flush_wasm_state() def set_part_selected(self, node_id: int, selected: bool) -> None: """ @@ -453,7 +456,8 @@ def set_part_selected(self, node_id: int, selected: bool) -> None: stored_rgb if stored_rgb is not None else list(VisorColors.DefaultMeshColor) ) self._renderer.apply_selected(node_id, selected, diffuse_rgb) - self._renderer.flush_wasm_state() + # TODO: uncomment when adding round trips + # self._renderer.flush_wasm_state() def set_part_color_variable( self, @@ -481,7 +485,8 @@ def set_part_color_variable( self._renderer.apply_color_variable( node_id, variable_id, association, array_name, component, min_val, max_val ) - self._renderer.flush_wasm_state() + # TODO: uncomment when adding round trips + # self._renderer.flush_wasm_state() def clear_part_color_variable(self, node_id: int) -> None: """ @@ -497,7 +502,8 @@ def clear_part_color_variable(self, node_id: int) -> None: ) return self._renderer.clear_color_variable(node_id) - self._renderer.flush_wasm_state() + # TODO: uncomment when adding round trips + # self._renderer.flush_wasm_state() # ------------------------------------------------------------------ # Internal helpers diff --git a/tests/unit/vtk/scene/test_base.py b/tests/unit/vtk/scene/test_base.py index d17214c8..922a0e13 100644 --- a/tests/unit/vtk/scene/test_base.py +++ b/tests/unit/vtk/scene/test_base.py @@ -235,9 +235,10 @@ def test_set_part_visibility_flushes_under_the_lock_after_the_apply(scene, pipel scene.set_part_visibility(NODE_ID, False) - assert record["calls"] == 1 - assert record["depth"] >= 1 - assert record["value"] == 0 + # TODO: change "calls" to 1 and uncomment "depth" and "value" when we add round trips + assert record["calls"] == 0 + # assert record["depth"] >= 1 + # assert record["value"] == 0 assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count assert scene._vtk_lock.depth == 0 @@ -267,9 +268,10 @@ def test_set_part_opacity_flushes_under_the_lock_after_the_apply(scene, pipeline scene.set_part_opacity(NODE_ID, 0.25) - assert record["calls"] == 1 - assert record["depth"] >= 1 - assert record["value"] == pytest.approx(0.25) + # TODO: change "calls" to 1 and uncomment "depth" and "value" when we add round trips + assert record["calls"] == 0 + # assert record["depth"] >= 1 + # assert record["value"] == pytest.approx(0.25) assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count @@ -318,9 +320,10 @@ def test_set_part_diffuse_color_flushes_under_the_lock_after_the_apply(scene, pi scene.set_part_diffuse_color(NODE_ID, [1.0, 0.0, 0.0]) - assert record["calls"] == 1 - assert record["depth"] >= 1 - assert record["value"] == pytest.approx((1.0, 0.0, 0.0)) + # TODO: change "calls" to 1 and uncomment "depth" and "value" when we add round trips + assert record["calls"] == 0 + # assert record["depth"] >= 1 + # assert record["value"] == pytest.approx((1.0, 0.0, 0.0)) assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count @@ -370,9 +373,10 @@ def test_set_part_selected_flushes_under_the_lock_after_the_apply(scene, pipelin scene.set_part_selected(NODE_ID, True) - assert record["calls"] == 1 - assert record["depth"] >= 1 - assert record["value"] == pytest.approx(0.5) + # TODO: change "calls" to 1 and uncomment "depth" and "value" when we add round trips + assert record["calls"] == 0 + # assert record["depth"] >= 1 + # assert record["value"] == pytest.approx(0.5) assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count @@ -423,9 +427,10 @@ def test_set_part_color_variable_flushes_under_the_lock_after_the_apply(scene, p NODE_ID, "POINT::pressure::1", VisorVtkVariableType.POINT, "pressure", 0, 0.0, 49.0 ) - assert record["calls"] == 1 - assert record["depth"] >= 1 - assert record["value"] == "pressure" + # TODO: change "calls" to 1 and uncomment "depth" and "value" when we add round trips + assert record["calls"] == 0 + # assert record["depth"] >= 1 + # assert record["value"] == "pressure" assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count @@ -462,9 +467,10 @@ def test_clear_part_color_variable_flushes_under_the_lock_after_the_apply(scene, scene.clear_part_color_variable(NODE_ID) - assert record["calls"] == 1 - assert record["depth"] >= 1 - assert record["value"] == 0 + # TODO: change "calls" to 1 and uncomment "depth" and "value" when we add round trips + assert record["calls"] == 0 + # assert record["depth"] >= 1 + # assert record["value"] == 0 assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count From 44233a44ffec0fada08add76548c6d44d610a069 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 28 Aug 2026 20:02:19 +0000 Subject: [PATCH 12/28] chore: adding changelog file 52.added.md [dependabot-skip] --- doc/changelog.d/52.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/52.added.md diff --git a/doc/changelog.d/52.added.md b/doc/changelog.d/52.added.md new file mode 100644 index 00000000..9d38fdcb --- /dev/null +++ b/doc/changelog.d/52.added.md @@ -0,0 +1 @@ +Remote rendering 3.1c - register per-part triggers, serialize VTK access with lock, trigger payload validation From 85b66307d832883bd986093545aaeaa56b6d577b Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Mon, 31 Aug 2026 08:02:08 -0700 Subject: [PATCH 13/28] remove TODOs and update comment on flush_wasm_state --- src/ansys/visor/viewer/vtk/scene/base.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/ansys/visor/viewer/vtk/scene/base.py b/src/ansys/visor/viewer/vtk/scene/base.py index c5b157f7..a7cbd706 100644 --- a/src/ansys/visor/viewer/vtk/scene/base.py +++ b/src/ansys/visor/viewer/vtk/scene/base.py @@ -385,9 +385,11 @@ def pick_geometry(self, actor_wasm_id, cell_id, mode, world_x, world_y, world_z) # # Each method does both halves of its trigger, in this order and all under # ``_vtk_lock``: write the registry record, apply to the server's VTK - # pipeline, then push the mutated state to the wasm client with - # ``flush_wasm_state()``. An unresolvable node id is a logged no-op at - # every layer: nothing is applied and nothing is flushed. + # pipeline. Nothing is pushed to the client from here. The client applies + # its own change, and a push at this layer rebuilds the client, which + # re-delivers state and fires further triggers. Presenting a server-originated + # change is the renderer's, since it is not mode-agnostic. + # An unresolvable node id is a logged no-op at the apply layer. # # Every value that arrives here is absolute, never relative: the caller # always supplies the target value, never a toggle or a delta. @@ -400,8 +402,6 @@ def set_part_visibility(self, node_id: int, visible: bool) -> None: logger.debug("set_part_visibility: no dataset owns node %s; skipping.", node_id) return self._renderer.apply_visibility(node_id, visible) - # TODO: uncomment when adding round trips - # self._renderer.flush_wasm_state() def set_part_opacity(self, node_id: int, opacity: float) -> None: """Set the opacity of the part identified by *node_id*.""" @@ -410,8 +410,6 @@ def set_part_opacity(self, node_id: int, opacity: float) -> None: logger.debug("set_part_opacity: no dataset owns node %s; skipping.", node_id) return self._renderer.apply_opacity(node_id, opacity) - # TODO: uncomment when adding round trips - # self._renderer.flush_wasm_state() def set_part_diffuse_color(self, node_id: int, diffuse_rgb: list[float] | None) -> None: """ @@ -433,8 +431,6 @@ def set_part_diffuse_color(self, node_id: int, diffuse_rgb: list[float] | None) self._renderer.apply_diffuse_color( node_id, applied_rgb[0], applied_rgb[1], applied_rgb[2] ) - # TODO: uncomment when adding round trips - # self._renderer.flush_wasm_state() def set_part_selected(self, node_id: int, selected: bool) -> None: """ @@ -456,8 +452,6 @@ def set_part_selected(self, node_id: int, selected: bool) -> None: stored_rgb if stored_rgb is not None else list(VisorColors.DefaultMeshColor) ) self._renderer.apply_selected(node_id, selected, diffuse_rgb) - # TODO: uncomment when adding round trips - # self._renderer.flush_wasm_state() def set_part_color_variable( self, @@ -485,8 +479,6 @@ def set_part_color_variable( self._renderer.apply_color_variable( node_id, variable_id, association, array_name, component, min_val, max_val ) - # TODO: uncomment when adding round trips - # self._renderer.flush_wasm_state() def clear_part_color_variable(self, node_id: int) -> None: """ @@ -502,8 +494,6 @@ def clear_part_color_variable(self, node_id: int) -> None: ) return self._renderer.clear_color_variable(node_id) - # TODO: uncomment when adding round trips - # self._renderer.flush_wasm_state() # ------------------------------------------------------------------ # Internal helpers From 42efe367c72ae78d255fd86587f8ae782f64d3db Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Fri, 28 Aug 2026 12:29:34 -0700 Subject: [PATCH 14/28] route client per-part mutations through the server triggers --- src/ansys/visor/visor-client/src/App.tsx | 3 +- .../VisorSceneGraphPartTriggers.test.tsx | 315 +++++++++++++ .../WasmRendererPartTriggers.test.tsx | 445 ++++++++++++++++++ .../visor-client/src/renderer/IRenderer.ts | 46 ++ .../visor-client/src/renderer/NullRenderer.ts | 35 ++ .../visor-client/src/renderer/WasmRenderer.ts | 110 ++++- .../src/state/VisorSceneGraph.tsx | 18 +- 7 files changed, 966 insertions(+), 6 deletions(-) create mode 100644 src/ansys/visor/visor-client/src/jest-tests/VisorSceneGraphPartTriggers.test.tsx create mode 100644 src/ansys/visor/visor-client/src/jest-tests/WasmRendererPartTriggers.test.tsx diff --git a/src/ansys/visor/visor-client/src/App.tsx b/src/ansys/visor/visor-client/src/App.tsx index 2fb10ec6..bb2a8769 100644 --- a/src/ansys/visor/visor-client/src/App.tsx +++ b/src/ansys/visor/visor-client/src/App.tsx @@ -128,7 +128,8 @@ function App() { } const renderer = await WasmRenderer.createAsync( wasmView.current.vtkScene, - requireWasmAnnotation(sceneDetails.vtkInfo.rendererAnnotation) + requireWasmAnnotation(sceneDetails.vtkInfo.rendererAnnotation), + wasmView.current.trameTriggerAsync ); const newFrontend = new VisorFrontend(renderer, sceneDetails.vtkInfo.sceneGraph); if (visorArgs.current.darkMode != null) { diff --git a/src/ansys/visor/visor-client/src/jest-tests/VisorSceneGraphPartTriggers.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/VisorSceneGraphPartTriggers.test.tsx new file mode 100644 index 00000000..28754931 --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/VisorSceneGraphPartTriggers.test.tsx @@ -0,0 +1,315 @@ +import { CreateVisorSceneGraph, VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; +import VisorVtkDataArray from '../state/appstate/vtkInfo/VisorVtkDataArray.tsx'; +import type { IRenderer } from '../renderer/IRenderer'; + +/** + * The scene graph's six user actions, each of which must now do two things: + * apply to the client's own objects, as it always did, and send the matching + * mutation to the server. + * + * Every action is covered by a *pair* of tests, one per half. That is not + * redundancy: a single test asserting "both happened" would still pass if the + * two were fused, and the failure this pairing exists to catch is a silently + * dropped send, which the render cannot reveal because the client is still + * painting the same result itself. + * + * Expected values here are hand-written literals. In particular the + * colour-variable descriptor is written out rather than read back from the + * spectrum, so the test cannot agree with the code by making the same mistake. + */ + +const ROOT_ID = 0; +const PART_A_ID = 1; +const PART_B_ID = 2; + +const VARIABLE_ID = 'POINT::pressure::1'; + +function makePressureArray(): VisorVtkDataArray { + return new VisorVtkDataArray({ + indexForType: 0, + type: 'POINT', + name: 'pressure', + numComponents: 1, + magnitudeRange: [0, 10], + ranges: [[2, 8]], + }); +} + +function makeRendererDouble() { + return { + // The per-part applies that already existed. + setVisibilityAsync: jest.fn(async () => undefined), + setSelectedAsync: jest.fn(async () => undefined), + setOpacityAsync: jest.fn(async () => undefined), + setDiffuseColorRgbAsync: jest.fn(async () => undefined), + resetDiffuseColorAsync: jest.fn(async () => undefined), + setColorVariableAsync: jest.fn(async () => undefined), + clearColorVariableAsync: jest.fn(async () => undefined), + setEdgeVisibilityAsync: jest.fn(async () => undefined), + setScalarRangeAsync: jest.fn(async () => undefined), + // The per-part sends added for the server path. + sendPartVisibilityAsync: jest.fn(async () => undefined), + sendPartOpacityAsync: jest.fn(async () => undefined), + sendPartDiffuseColorAsync: jest.fn(async () => undefined), + sendPartSelectedAsync: jest.fn(async () => undefined), + sendPartColorVariableAsync: jest.fn(async () => undefined), + sendClearPartColorVariableAsync: jest.fn(async () => undefined), + }; +} + +type RendererDouble = ReturnType; + +function makeGraph(renderer: RendererDouble): VisorSceneNodeExtended { + return CreateVisorSceneGraph( + { + id: ROOT_ID, + dataArrays: [], + name: '', + isGroupNode: true, + isActorNode: false, + nodeType: 'root', + diffuseColor: [1, 1, 1], + bounds: [], + children: [ + { + id: PART_A_ID, + dataArrays: [makePressureArray()], + name: 'part-a', + isGroupNode: false, + isActorNode: true, + nodeType: 'vtkUnstructuredGrid', + diffuseColor: [1, 1, 1], + bounds: [], + children: [], + }, + { + id: PART_B_ID, + dataArrays: [makePressureArray()], + name: 'part-b', + isGroupNode: false, + isActorNode: true, + nodeType: 'vtkUnstructuredGrid', + diffuseColor: [1, 1, 1], + bounds: [], + children: [], + }, + ], + }, + undefined, + renderer as unknown as IRenderer + ); +} + +function setUp() { + const renderer = makeRendererDouble(); + const graph = makeGraph(renderer); + const partA = graph.descendantActorNodesOrSelfDictionary[PART_A_ID]; + return { renderer, graph, partA }; +} + +describe('hide / show', () => { + test('sends set_part_visibility for the part', async () => { + const { renderer, partA } = setUp(); + + await partA.setVisibilityAsync(false); + + expect(renderer.sendPartVisibilityAsync).toHaveBeenCalledWith(PART_A_ID, false); + }); + + test('still applies visibility to the client renderer', async () => { + const { renderer, partA } = setUp(); + + await partA.setVisibilityAsync(false); + + expect(renderer.setVisibilityAsync).toHaveBeenCalledWith(PART_A_ID, false); + }); +}); + +describe('opacity', () => { + test('sends set_part_opacity for the part', async () => { + const { renderer, partA } = setUp(); + + await partA.setOpacityAsync(0.25); + + expect(renderer.sendPartOpacityAsync).toHaveBeenCalledWith(PART_A_ID, 0.25); + }); + + test('still applies opacity to the client renderer', async () => { + const { renderer, partA } = setUp(); + + await partA.setOpacityAsync(0.25); + + expect(renderer.setOpacityAsync).toHaveBeenCalledWith(PART_A_ID, 0.25); + }); +}); + +describe('custom colour, set by rgb', () => { + test('sends the normalised colour', async () => { + const { renderer, partA } = setUp(); + + await partA.setDiffuseColorRgbAsync(0.25, 0.5, 0.75); + + expect(renderer.sendPartDiffuseColorAsync).toHaveBeenCalledWith( + PART_A_ID, + [0.25, 0.5, 0.75] + ); + }); + + test('still applies the colour to the client renderer', async () => { + const { renderer, partA } = setUp(); + + await partA.setDiffuseColorRgbAsync(0.25, 0.5, 0.75); + + expect(renderer.setDiffuseColorRgbAsync).toHaveBeenCalledWith( + PART_A_ID, + 0.25, + 0.5, + 0.75, + false + ); + }); +}); + +describe('custom colour, set by hex', () => { + test('sends the normalised colour', async () => { + const { renderer, partA } = setUp(); + + await partA.setDiffuseColorHexAsync('#ff0000'); + + expect(renderer.sendPartDiffuseColorAsync).toHaveBeenCalledWith(PART_A_ID, [1, 0, 0]); + }); + + test('still applies the colour to the client renderer', async () => { + const { renderer, partA } = setUp(); + + await partA.setDiffuseColorHexAsync('#ff0000'); + + expect(renderer.setDiffuseColorRgbAsync).toHaveBeenCalledWith( + PART_A_ID, + 1, + 0, + 0, + false + ); + }); +}); + +describe('custom colour, reset', () => { + test('sends a null colour, not the default colour value', async () => { + const { renderer, partA } = setUp(); + + await partA.resetDiffuseColorAsync(); + + expect(renderer.sendPartDiffuseColorAsync).toHaveBeenCalledWith(PART_A_ID, null); + }); + + test('still applies the reset to the client renderer', async () => { + const { renderer, partA } = setUp(); + + await partA.resetDiffuseColorAsync(); + + expect(renderer.resetDiffuseColorAsync).toHaveBeenCalledWith( + PART_A_ID, + [1, 1, 1], + false + ); + }); +}); + +describe('colour by variable', () => { + test('sends the descriptor for the part', async () => { + const { renderer, partA } = setUp(); + + await partA.setColorVariableAsync(VARIABLE_ID, 0); + + expect(renderer.sendPartColorVariableAsync).toHaveBeenCalledWith(PART_A_ID, { + spectrumId: 'POINT::pressure::1', + spectrumType: 'POINT', + spectrumName: 'pressure', + component: 0, + min: 2, + max: 8, + }); + }); + + test('still applies the colour variable to the client renderer', async () => { + const { renderer, partA } = setUp(); + + await partA.setColorVariableAsync(VARIABLE_ID, 0); + + expect(renderer.setColorVariableAsync).toHaveBeenCalledWith(PART_A_ID, { + spectrumId: 'POINT::pressure::1', + spectrumType: 'POINT', + spectrumName: 'pressure', + component: 0, + min: 2, + max: 8, + }); + }); +}); + +describe('clear colour by variable', () => { + test('sends clear_part_color_variable for the part', async () => { + const { renderer, partA } = setUp(); + + await partA.clearColorVariableAsync(); + + expect(renderer.sendClearPartColorVariableAsync).toHaveBeenCalledWith(PART_A_ID); + }); + + test('still clears the colour variable on the client renderer', async () => { + const { renderer, partA } = setUp(); + + await partA.clearColorVariableAsync(); + + expect(renderer.clearColorVariableAsync).toHaveBeenCalledWith(PART_A_ID); + }); +}); + +describe('selection', () => { + test('sends set_part_selected for the part, with no colour', async () => { + const { renderer, partA } = setUp(); + + await partA.setSelectedAsync(true); + + expect(renderer.sendPartSelectedAsync).toHaveBeenCalledWith(PART_A_ID, true); + }); + + test('still applies the selection to the client renderer, with its colour', async () => { + const { renderer, partA } = setUp(); + + await partA.setSelectedAsync(true); + + expect(renderer.setSelectedAsync).toHaveBeenCalledWith(PART_A_ID, true, [1, 1, 1]); + }); +}); + +describe('group fan-out', () => { + test('a group action sends for exactly the actor nodes beneath it', async () => { + // Asserted as the set of node ids the sender saw, not as a count: the + // design is deliberately insensitive to how many messages an action + // produces, so a count would pin the wrong property. + const { renderer, graph } = setUp(); + + await graph.setVisibilityAsync(false); + + const ids = renderer.sendPartVisibilityAsync.mock.calls.map( + (call) => (call as unknown as [number, boolean])[0] + ); + expect(new Set(ids)).toEqual(new Set([PART_A_ID, PART_B_ID])); + expect(ids).not.toContain(ROOT_ID); + }); + + test('a group action still applies to exactly the actor nodes beneath it', async () => { + const { renderer, graph } = setUp(); + + await graph.setVisibilityAsync(false); + + const ids = renderer.setVisibilityAsync.mock.calls.map( + (call) => (call as unknown as [number, boolean])[0] + ); + expect(new Set(ids)).toEqual(new Set([PART_A_ID, PART_B_ID])); + expect(ids).not.toContain(ROOT_ID); + }); +}); + diff --git a/src/ansys/visor/visor-client/src/jest-tests/WasmRendererPartTriggers.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererPartTriggers.test.tsx new file mode 100644 index 00000000..e3b6dfbd --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererPartTriggers.test.tsx @@ -0,0 +1,445 @@ +import { WasmRenderer } from '../renderer/WasmRenderer'; +import type { WasmRendererAnnotation } from '../renderer/RendererAnnotation'; +import type VtkScene from '../wasm/VtkScene'; +import type { TrameTriggerSender } from '../renderer/IRenderer'; + +/** + * WasmRenderer's per-part *send* surface: the six methods that carry a + * per-part mutation to the matching server trigger. + * + * Two things are pinned here, and they are pinned by separate tests on + * purpose: + * + * 1. Each send calls the injected sender with the trigger name and the + * exact payload the server's model expects. Every expected payload is a + * hand-written literal; nothing here rebuilds a payload the way the code + * under test builds it. + * + * 2. Each existing per-part apply still mutates the wasm object it always + * mutated. A test that only checked the render would still pass if the + * send were dropped, and a test that only checked the send would still + * pass if the apply were broken, so neither is allowed to stand in for + * the other. + * + * Also pinned: with no sender injected the sends are no-ops that do not + * throw, and a sender that rejects does not propagate out of a send. + */ + +const NODE_ID = 7; +const ACTOR_ID = 101; +const PROPERTY_ID = 102; +const MAPPER_ID = 103; +const ORIENTATION_WIDGET_ID = 201; +const PLANE_ID = 202; +const PLANE_WIDGET_ID = 203; +const PLANE_REPRESENTATION_ID = 204; +const BOUNDING_BOX_ALGORITHM_ID = 205; +const BOUNDING_BOX_OUTLINE_ID = 206; +const BOUNDING_BOX_AXES_ID = 207; + +function makeAnnotation(): WasmRendererAnnotation { + return { + rendererKind: 'wasm', + nodes: { + [String(NODE_ID)]: { + actorId: ACTOR_ID, + propertyId: PROPERTY_ID, + mapperId: MAPPER_ID, + }, + }, + widgets: { + orientationWidgetId: ORIENTATION_WIDGET_ID, + crossSectionPlaneId: PLANE_ID, + crossSectionPlaneWidgetId: PLANE_WIDGET_ID, + crossSectionPlaneRepresentationId: PLANE_REPRESENTATION_ID, + boundingBoxAlgorithmId: BOUNDING_BOX_ALGORITHM_ID, + boundingBoxOutlineActorId: BOUNDING_BOX_OUTLINE_ID, + boundingBoxAxesActorId: BOUNDING_BOX_AXES_ID, + }, + }; +} + +function makeFakeWasmObjects() { + const lut = { + SetHueRange: jest.fn(async () => undefined), + SetVectorModeToMagnitude: jest.fn(async () => undefined), + }; + const actor = { + SetVisibility: jest.fn(async () => undefined), + }; + const property = { + SetAmbientColor: jest.fn(async () => undefined), + SetAmbient: jest.fn(async () => undefined), + SetDiffuse: jest.fn(async () => undefined), + SetDiffuseColor: jest.fn(async () => undefined), + SetOpacity: jest.fn(async () => undefined), + SetEdgeColor: jest.fn(async () => undefined), + EdgeVisibilityOn: jest.fn(async () => undefined), + EdgeVisibilityOff: jest.fn(async () => undefined), + }; + const mapper = { + SetScalarModeToUsePointFieldData: jest.fn(async () => undefined), + SetScalarModeToUseCellFieldData: jest.fn(async () => undefined), + SetScalarRange: jest.fn(async () => undefined), + SetColorModeToMapScalars: jest.fn(async () => undefined), + ColorByArrayComponent: jest.fn(async () => undefined), + SetScalarVisibility: jest.fn(async () => undefined), + CreateDefaultLookupTable: jest.fn(async () => undefined), + GetLookupTable: jest.fn(async () => lut), + }; + const widget = { + observe: jest.fn(), + On: jest.fn(async () => undefined), + Off: jest.fn(async () => undefined), + GetOrigin: jest.fn(async () => [0, 0, 0]), + GetNormal: jest.fn(async () => [0, 0, 1]), + SetOrigin: jest.fn(async () => undefined), + SetNormal: jest.fn(async () => undefined), + }; + return { lut, actor, property, mapper, widget }; +} + +async function makeRenderer(sender: TrameTriggerSender | null) { + const objects = makeFakeWasmObjects(); + const scene = { + canvasDiv: document.createElement('div'), + render: jest.fn(), + clearObserversAndEventListeners: jest.fn(), + getVtkObject: (wasmId: number) => { + switch (wasmId) { + case ACTOR_ID: + return objects.actor; + case PROPERTY_ID: + return objects.property; + case MAPPER_ID: + return objects.mapper; + default: + return objects.widget; + } + }, + }; + const renderer = await WasmRenderer.createAsync( + scene as unknown as VtkScene, + makeAnnotation(), + sender + ); + return { renderer, ...objects }; +} + +/** A sender that records its calls and resolves. */ +function makeSender() { + return jest.fn(async () => undefined) as unknown as jest.Mock & + TrameTriggerSender; +} + +describe('WasmRenderer per-part sends: trigger name and payload', () => { + test('sendPartVisibilityAsync sends set_part_visibility with nodeId and visible', async () => { + const sender = makeSender(); + const { renderer } = await makeRenderer(sender); + + await renderer.sendPartVisibilityAsync(7, false); + + expect(sender).toHaveBeenCalledWith('set_part_visibility', { + nodeId: 7, + visible: false, + }); + }); + + test('sendPartOpacityAsync sends set_part_opacity with nodeId and opacity', async () => { + const sender = makeSender(); + const { renderer } = await makeRenderer(sender); + + await renderer.sendPartOpacityAsync(7, 0.25); + + expect(sender).toHaveBeenCalledWith('set_part_opacity', { + nodeId: 7, + opacity: 0.25, + }); + }); + + test('sendPartDiffuseColorAsync sends set_part_diffuse_color with the colour', async () => { + const sender = makeSender(); + const { renderer } = await makeRenderer(sender); + + await renderer.sendPartDiffuseColorAsync(7, [0.25, 0.5, 0.75]); + + expect(sender).toHaveBeenCalledWith('set_part_diffuse_color', { + nodeId: 7, + diffuseRgb: [0.25, 0.5, 0.75], + }); + }); + + test('sendPartDiffuseColorAsync sends an explicit null diffuseRgb key for a reset', async () => { + // The key must be present and null. The server model gives diffuseRgb + // no default, so an omitted key is a rejected message, not a clear. + const sender = makeSender(); + const { renderer } = await makeRenderer(sender); + + await renderer.sendPartDiffuseColorAsync(7, null); + + expect(sender).toHaveBeenCalledWith('set_part_diffuse_color', { + nodeId: 7, + diffuseRgb: null, + }); + const payload = sender.mock.calls[0][1] as Record; + expect(Object.prototype.hasOwnProperty.call(payload, 'diffuseRgb')).toBe(true); + }); + + test('sendPartSelectedAsync sends set_part_selected with nodeId and selected', async () => { + const sender = makeSender(); + const { renderer } = await makeRenderer(sender); + + await renderer.sendPartSelectedAsync(7, true); + + expect(sender).toHaveBeenCalledWith('set_part_selected', { + nodeId: 7, + selected: true, + }); + }); + + test('sendPartSelectedAsync carries no colour', async () => { + const sender = makeSender(); + const { renderer } = await makeRenderer(sender); + + await renderer.sendPartSelectedAsync(7, true); + + const payload = sender.mock.calls[0][1] as Record; + expect(Object.keys(payload).sort()).toEqual(['nodeId', 'selected']); + }); + + test('sendPartColorVariableAsync maps the descriptor onto the wire keys', async () => { + const sender = makeSender(); + const { renderer } = await makeRenderer(sender); + + await renderer.sendPartColorVariableAsync(7, { + spectrumId: 'POINT::pressure::1', + spectrumType: 'POINT', + spectrumName: 'pressure', + component: 0, + min: 2, + max: 8, + }); + + expect(sender).toHaveBeenCalledWith('set_part_color_variable', { + nodeId: 7, + variableId: 'POINT::pressure::1', + association: 'POINT', + arrayName: 'pressure', + component: 0, + min: 2, + max: 8, + }); + }); + + test('sendPartColorVariableAsync sends CELL as the association for a cell array', async () => { + const sender = makeSender(); + const { renderer } = await makeRenderer(sender); + + await renderer.sendPartColorVariableAsync(7, { + spectrumId: 'CELL::temperature::3', + spectrumType: 'CELL', + spectrumName: 'temperature', + component: -1, + min: -1.5, + max: 4.5, + }); + + expect(sender).toHaveBeenCalledWith('set_part_color_variable', { + nodeId: 7, + variableId: 'CELL::temperature::3', + association: 'CELL', + arrayName: 'temperature', + component: -1, + min: -1.5, + max: 4.5, + }); + }); + + test('sendPartColorVariableAsync forwards variableId verbatim without parsing it', async () => { + // An id that would not survive being split and rebuilt: it is carried + // as an opaque token, and the association and array name travel in + // their own fields precisely so that nothing has to decompose it. + const sender = makeSender(); + const { renderer } = await makeRenderer(sender); + + await renderer.sendPartColorVariableAsync(7, { + spectrumId: 'not::a::parseable::id::at::all', + spectrumType: 'POINT', + spectrumName: 'pressure', + component: 0, + min: 0, + max: 1, + }); + + const payload = sender.mock.calls[0][1] as Record; + expect(payload.variableId).toBe('not::a::parseable::id::at::all'); + expect(payload.association).toBe('POINT'); + expect(payload.arrayName).toBe('pressure'); + }); + + test('sendClearPartColorVariableAsync sends clear_part_color_variable with only nodeId', async () => { + const sender = makeSender(); + const { renderer } = await makeRenderer(sender); + + await renderer.sendClearPartColorVariableAsync(7); + + expect(sender).toHaveBeenCalledWith('clear_part_color_variable', { + nodeId: 7, + }); + }); +}); + +describe('WasmRenderer per-part applies still mutate their wasm objects', () => { + test('setVisibilityAsync still sets visibility on the actor', async () => { + const { renderer, actor } = await makeRenderer(makeSender()); + + await renderer.setVisibilityAsync(NODE_ID, false); + + expect(actor.SetVisibility).toHaveBeenCalledWith(0); + }); + + test('setOpacityAsync still sets opacity on the property', async () => { + const { renderer, property } = await makeRenderer(makeSender()); + + await renderer.setOpacityAsync(NODE_ID, 0.25); + + expect(property.SetOpacity).toHaveBeenCalledWith(0.25); + }); + + test('setDiffuseColorRgbAsync still sets the diffuse colour on the property', async () => { + const { renderer, property } = await makeRenderer(makeSender()); + + await renderer.setDiffuseColorRgbAsync(NODE_ID, 0.25, 0.5, 0.75, false); + + expect(property.SetDiffuseColor).toHaveBeenCalledWith(0.25, 0.5, 0.75); + }); + + test('setSelectedAsync still applies the selection tint and lighting', async () => { + const { renderer, property } = await makeRenderer(makeSender()); + + await renderer.setSelectedAsync(NODE_ID, true, [1, 0, 0]); + + expect(property.SetAmbientColor).toHaveBeenCalledWith([0 / 255, 62 / 255, 111 / 255]); + expect(property.SetDiffuse).toHaveBeenCalledWith(0.5); + expect(property.SetAmbient).toHaveBeenCalledWith(0.5); + expect(property.SetDiffuseColor).toHaveBeenCalledWith(1, 0, 0); + }); + + test('setColorVariableAsync still configures the mapper and the default table', async () => { + const { renderer, mapper, lut } = await makeRenderer(makeSender()); + + await renderer.setColorVariableAsync(NODE_ID, { + spectrumId: 'POINT::pressure::1', + spectrumType: 'POINT', + spectrumName: 'pressure', + component: 0, + min: 2, + max: 8, + }); + + expect(mapper.SetScalarModeToUsePointFieldData).toHaveBeenCalled(); + expect(mapper.SetScalarRange).toHaveBeenCalledWith(2, 8); + expect(mapper.SetColorModeToMapScalars).toHaveBeenCalled(); + expect(mapper.ColorByArrayComponent).toHaveBeenCalledWith('pressure', 0); + expect(mapper.SetScalarVisibility).toHaveBeenCalledWith(1); + expect(mapper.CreateDefaultLookupTable).toHaveBeenCalled(); + expect(lut.SetHueRange).toHaveBeenCalledWith(0.667, 0.0); + expect(lut.SetVectorModeToMagnitude).toHaveBeenCalled(); + }); + + test('clearColorVariableAsync still turns scalar visibility off', async () => { + const { renderer, mapper } = await makeRenderer(makeSender()); + + await renderer.clearColorVariableAsync(NODE_ID); + + expect(mapper.SetScalarVisibility).toHaveBeenCalledWith(0); + }); +}); + +describe('WasmRenderer sends when no sender is injected', () => { + test('all six sends are no-ops that do not throw', async () => { + const { renderer } = await makeRenderer(null); + + await expect(renderer.sendPartVisibilityAsync(7, true)).resolves.toBeUndefined(); + await expect(renderer.sendPartOpacityAsync(7, 0.5)).resolves.toBeUndefined(); + await expect( + renderer.sendPartDiffuseColorAsync(7, [1, 0, 0]) + ).resolves.toBeUndefined(); + await expect(renderer.sendPartSelectedAsync(7, true)).resolves.toBeUndefined(); + await expect( + renderer.sendPartColorVariableAsync(7, { + spectrumId: 'POINT::pressure::1', + spectrumType: 'POINT', + spectrumName: 'pressure', + component: 0, + min: 2, + max: 8, + }) + ).resolves.toBeUndefined(); + await expect(renderer.sendClearPartColorVariableAsync(7)).resolves.toBeUndefined(); + }); + + test('a send touches no wasm object', async () => { + const { renderer, actor, property, mapper } = await makeRenderer(null); + + await renderer.sendPartVisibilityAsync(NODE_ID, false); + await renderer.sendPartOpacityAsync(NODE_ID, 0.25); + await renderer.sendPartSelectedAsync(NODE_ID, true); + + expect(actor.SetVisibility).not.toHaveBeenCalled(); + expect(property.SetOpacity).not.toHaveBeenCalled(); + expect(property.SetAmbientColor).not.toHaveBeenCalled(); + expect(mapper.SetScalarVisibility).not.toHaveBeenCalled(); + }); +}); + +describe('WasmRenderer sends when the sender rejects', () => { + test('the rejection does not propagate out of the send', async () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); + const sender = jest.fn(async () => { + throw new Error('socket closed'); + }) as unknown as TrameTriggerSender; + const { renderer } = await makeRenderer(sender); + + await expect(renderer.sendPartOpacityAsync(7, 0.25)).resolves.toBeUndefined(); + + consoleError.mockRestore(); + }); + + test('the failure is logged with the trigger name and the node id', async () => { + // This log line is the only signal a failed send produces: the render + // looks identical either way, so the message must name what failed and + // for which part, under a fixed prefix that can be grepped. + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); + const sender = jest.fn(async () => { + throw new Error('socket closed'); + }) as unknown as TrameTriggerSender; + const { renderer } = await makeRenderer(sender); + + await renderer.sendPartOpacityAsync(7, 0.25); + + expect(consoleError).toHaveBeenCalledTimes(1); + const message = String(consoleError.mock.calls[0][0]); + expect(message).toContain('[VISOR] per-part trigger send failed:'); + expect(message).toContain("trigger='set_part_opacity'"); + expect(message).toContain('nodeId=7'); + + consoleError.mockRestore(); + }); + + test('a non-Error rejection is caught too', async () => { + // The catch is unnarrowed on purpose: any rejection, of any shape, is + // a failed send. + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); + const sender = jest.fn(async () => { + throw 'not an Error'; + }) as unknown as TrameTriggerSender; + const { renderer } = await makeRenderer(sender); + + await expect(renderer.sendPartVisibilityAsync(7, true)).resolves.toBeUndefined(); + expect(consoleError).toHaveBeenCalledTimes(1); + + consoleError.mockRestore(); + }); +}); + diff --git a/src/ansys/visor/visor-client/src/renderer/IRenderer.ts b/src/ansys/visor/visor-client/src/renderer/IRenderer.ts index de07a0ac..2df10d0c 100644 --- a/src/ansys/visor/visor-client/src/renderer/IRenderer.ts +++ b/src/ansys/visor/visor-client/src/renderer/IRenderer.ts @@ -35,6 +35,23 @@ export type ColorVariableDescriptor = Readonly<{ max: number; }>; +/** + * The transport a renderer uses to send a per-part mutation to the server. + * + * This is the existing trame trigger call surface, nothing new: + * `RemoteVtkScene.trameTriggerAsync(name, ...args)`, which forwards one + * positional argument -- the payload object -- to the named server trigger. + * It is *injected* into the concrete renderer at construction rather than + * imported by it, so that this module stays types-only and compiles to an + * empty module: a value import here would be inherited by every importer, + * including the wasm transport, and would drag the scene graph and React + * into module graphs that must not have them. + * + * A renderer constructed without a sender performs no send. It does not + * throw and does not queue. + */ +export type TrameTriggerSender = (triggerName: string, payload: unknown) => Promise; + /** Result returned by pickGeometryAsync. Discriminated by `mode`. */ export type PickGeometryResult = | { found: false } @@ -140,6 +157,35 @@ export interface IRenderer { clearColorVariableAsync(nodeId: NodeId): Promise; setScalarRangeAsync(nodeId: NodeId, min: number, max: number): Promise; + // ---- Per-part mutations routed to the server ----------------------------- + /** + * These six carry a per-part mutation to the matching server trigger. They + * are *not* a second way to render: they neither read nor touch any wasm + * object, and they are called in addition to -- never instead of -- the + * per-part apply methods above, which keep applying to the client's own + * objects for now. + * + * Each payload is absolute, never relative: it carries the target value, + * so a send that is suppressed upstream, duplicated, or reordered is + * harmless. `nodeId` is always a part (actor) node; a group node fans out + * to its actor descendants before any of these is called. + * + * A send never rejects. With no sender injected it is a no-op; with one, + * a transport failure is logged and swallowed, because these run inside UI + * handlers whose behaviour must not change. + */ + sendPartVisibilityAsync(nodeId: NodeId, visible: boolean): Promise; + sendPartOpacityAsync(nodeId: NodeId, opacity: number): Promise; + /** `null` clears the custom colour; absence of a colour is absence, not a default. */ + sendPartDiffuseColorAsync(nodeId: NodeId, diffuseRgb: readonly number[] | null): Promise; + /** Carries no colour: the server reads the part's stored colour from its own record. */ + sendPartSelectedAsync(nodeId: NodeId, selected: boolean): Promise; + sendPartColorVariableAsync( + nodeId: NodeId, + descriptor: ColorVariableDescriptor + ): Promise; + sendClearPartColorVariableAsync(nodeId: NodeId): Promise; + // ---- View-level widgets (state is renderer-owned; see arch rule (a)) --- setCrossSectionVisibilityAsync(visible?: boolean): Promise; isCrossSectionVisible(): boolean; // cached bool, sync diff --git a/src/ansys/visor/visor-client/src/renderer/NullRenderer.ts b/src/ansys/visor/visor-client/src/renderer/NullRenderer.ts index 6711cf5e..07fffac7 100644 --- a/src/ansys/visor/visor-client/src/renderer/NullRenderer.ts +++ b/src/ansys/visor/visor-client/src/renderer/NullRenderer.ts @@ -166,6 +166,41 @@ export class NullRenderer implements IRenderer { // no-op } + // ---- Per-part mutations routed to the server ----------------------------- + // A renderer with no server behind it sends nothing. These are no-ops for + // the same reason every other method here is: the null renderer has no + // transport, exactly as it has no wasm objects. + + async sendPartVisibilityAsync(_nodeId: NodeId, _visible: boolean): Promise { + // no-op + } + + async sendPartOpacityAsync(_nodeId: NodeId, _opacity: number): Promise { + // no-op + } + + async sendPartDiffuseColorAsync( + _nodeId: NodeId, + _diffuseRgb: readonly number[] | null + ): Promise { + // no-op + } + + async sendPartSelectedAsync(_nodeId: NodeId, _selected: boolean): Promise { + // no-op + } + + async sendPartColorVariableAsync( + _nodeId: NodeId, + _descriptor: ColorVariableDescriptor + ): Promise { + // no-op + } + + async sendClearPartColorVariableAsync(_nodeId: NodeId): Promise { + // no-op + } + async setCrossSectionVisibilityAsync(_visible?: boolean): Promise { // no-op } diff --git a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts index 91245546..0cb6fb98 100644 --- a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts +++ b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts @@ -4,6 +4,7 @@ import { IRenderer, NodeId, PickGeometryResult, + TrameTriggerSender, VisorCameraState, } from './IRenderer'; import VtkScene from '../wasm/VtkScene'; @@ -24,8 +25,13 @@ const SELECTION_TINT_RGB: Readonly = [0 / 255, 62 / 255, 111 / 255]; * this — nothing imports `WasmRenderer` yet. */ export class WasmRenderer implements IRenderer { - private constructor(vtkScene: VtkScene, annotation: WasmRendererAnnotation) { + private constructor( + vtkScene: VtkScene, + annotation: WasmRendererAnnotation, + triggerSender: TrameTriggerSender | null + ) { this.#vtkScene = vtkScene; + this.#triggerSender = triggerSender; const wasmPlaneId = annotation.widgets.crossSectionPlaneId; const wasmPlaneRepId = annotation.widgets.crossSectionPlaneRepresentationId; @@ -66,12 +72,14 @@ export class WasmRenderer implements IRenderer { static async createAsync( vtkScene: VtkScene, - annotation: WasmRendererAnnotation + annotation: WasmRendererAnnotation, + triggerSender: TrameTriggerSender | null = null ): Promise { - return new WasmRenderer(vtkScene, annotation); + return new WasmRenderer(vtkScene, annotation, triggerSender); } readonly #vtkScene: VtkScene; + readonly #triggerSender: TrameTriggerSender | null; readonly #actorIdToNodeId = new Map(); readonly #nodeIdToHandles = new Map(); readonly #crossSectionWidget: CrossSectionWidget; @@ -343,6 +351,102 @@ export class WasmRenderer implements IRenderer { await wasmMapper.SetScalarRange(min, max); } + // ---- Per-part mutations routed to the server ----------------------------- + /** + * Send one per-part payload to its server trigger. + * + * No sender injected -> no-op. This is the state every non-browser context + * is in, and it is deliberate rather than defensive: the renderer is + * constructible without a transport. + * + * A rejection is logged and swallowed, never rethrown. These sends run + * inside UI handlers that behaved a certain way before this call existed, + * and both stacks currently apply the same mutation independently, so a + * transport failure must not change what the user sees or break an + * interaction. The catch is deliberately unnarrowed: any rejection, of any + * shape, is a failed send. The log line below is therefore the *only* + * signal that a send failed -- the render will look correct either way -- + * so its prefix is fixed and greppable, and it names both the trigger and + * the node so a failure can be attributed without a debugger. + */ + async #sendTriggerAsync( + triggerName: string, + nodeId: NodeId, + payload: Record + ): Promise { + if (this.#triggerSender == null) { + return; + } + try { + await this.#triggerSender(triggerName, payload); + } catch (err) { + console.error( + `[VISOR] per-part trigger send failed: trigger='${triggerName}' nodeId=${nodeId}`, + err + ); + } + } + + async sendPartVisibilityAsync(nodeId: NodeId, visible: boolean): Promise { + await this.#sendTriggerAsync('set_part_visibility', nodeId, { + nodeId, + visible, + }); + } + + async sendPartOpacityAsync(nodeId: NodeId, opacity: number): Promise { + await this.#sendTriggerAsync('set_part_opacity', nodeId, { + nodeId, + opacity, + }); + } + + async sendPartDiffuseColorAsync( + nodeId: NodeId, + diffuseRgb: readonly number[] | null + ): Promise { + // The key is always present: `diffuseRgb: null` is how a reset is + // expressed. Omitting it is a different message, and not a valid one. + await this.#sendTriggerAsync('set_part_diffuse_color', nodeId, { + nodeId, + diffuseRgb, + }); + } + + async sendPartSelectedAsync(nodeId: NodeId, selected: boolean): Promise { + // No colour crosses this trigger by design; the server reads the + // part's stored colour from its own record. + await this.#sendTriggerAsync('set_part_selected', nodeId, { + nodeId, + selected, + }); + } + + async sendPartColorVariableAsync( + nodeId: NodeId, + descriptor: ColorVariableDescriptor + ): Promise { + // `variableId` is the client-built opaque token, forwarded verbatim; + // nothing on either side parses it. The association travels as its own + // typed 'POINT'|'CELL' field, and the array name as its own field, + // precisely so that no one has to. + await this.#sendTriggerAsync('set_part_color_variable', nodeId, { + nodeId, + variableId: descriptor.spectrumId, + association: descriptor.spectrumType, + arrayName: descriptor.spectrumName, + component: descriptor.component, + min: descriptor.min, + max: descriptor.max, + }); + } + + async sendClearPartColorVariableAsync(nodeId: NodeId): Promise { + await this.#sendTriggerAsync('clear_part_color_variable', nodeId, { + nodeId, + }); + } + // ---- View-level widgets ------------------------------------------------- async setCrossSectionVisibilityAsync(visible?: boolean): Promise { await this.#crossSectionWidget.setVisibilityAsync(visible); diff --git a/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx b/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx index bfe8275f..9f9e8ab3 100644 --- a/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx +++ b/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx @@ -160,6 +160,10 @@ export const CreateVisorSceneGraph = (() => { customDiffuseColor.rgbNormalized, _selected ); + // A reset is "no custom colour", sent as an explicit null -- + // not as the default colour's value, and not by omitting the + // key. Absence is absence. + await renderer!.sendPartDiffuseColorAsync(nodeId, null); }, async setDiffuseColorHexAsync(hex) { customDiffuseColor.setHex(hex); @@ -171,6 +175,7 @@ export const CreateVisorSceneGraph = (() => { rgbNormalized[2], _selected ); + await renderer!.sendPartDiffuseColorAsync(nodeId, rgbNormalized); }, async setDiffuseColorRgbAsync(r, g, b) { customDiffuseColor.setRgb(r, g, b); @@ -182,6 +187,7 @@ export const CreateVisorSceneGraph = (() => { rgbNormalized[2], _selected ); + await renderer!.sendPartDiffuseColorAsync(nodeId, rgbNormalized); }, async clearColorVariableAsync() { _spectrumId = null; @@ -189,6 +195,7 @@ export const CreateVisorSceneGraph = (() => { _spectrumMin = -1; _spectrumMax = -1; await renderer!.clearColorVariableAsync(nodeId); + await renderer!.sendClearPartColorVariableAsync(nodeId); }, async setColorVariableAsync(id, component) { if (_spectrumId === id && _spectrumComponent === component) { @@ -210,14 +217,16 @@ export const CreateVisorSceneGraph = (() => { _spectrumComponent = component; _spectrumMin = min; _spectrumMax = max; - await renderer!.setColorVariableAsync(nodeId, { + const descriptor = { spectrumId: id, spectrumType: spectrum.type, spectrumName: spectrum.name, component, min, max, - }); + }; + await renderer!.setColorVariableAsync(nodeId, descriptor); + await renderer!.sendPartColorVariableAsync(nodeId, descriptor); }, async setScalarRangeAsync(min, max) { await renderer!.setScalarRangeAsync(nodeId, min, max); @@ -236,6 +245,7 @@ export const CreateVisorSceneGraph = (() => { return; } await renderer!.setVisibilityAsync(nodeId, visible); + await renderer!.sendPartVisibilityAsync(nodeId, visible); }, async setSelectedAsync(selected) { if (_selected === selected) { @@ -255,6 +265,9 @@ export const CreateVisorSceneGraph = (() => { _selected, customDiffuseColor.rgbNormalized ); + // The trigger carries no colour: the server reads the part's + // stored colour from its own record. + await renderer!.sendPartSelectedAsync(nodeId, _selected); }, async setEdgeVisibilityAsync(edgeVisibility: boolean) { if (_edgeVisibility === edgeVisibility) { @@ -287,6 +300,7 @@ export const CreateVisorSceneGraph = (() => { return; } await renderer!.setOpacityAsync(nodeId, opacity); + await renderer!.sendPartOpacityAsync(nodeId, opacity); }, get spectrumId() { return _spectrumId; From a5a38ce43587ce75b5db33c5095841464b9aa785 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 28 Aug 2026 20:11:21 +0000 Subject: [PATCH 15/28] chore: adding changelog file 53.added.md [dependabot-skip] --- doc/changelog.d/53.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/53.added.md diff --git a/doc/changelog.d/53.added.md b/doc/changelog.d/53.added.md new file mode 100644 index 00000000..ed090c66 --- /dev/null +++ b/doc/changelog.d/53.added.md @@ -0,0 +1 @@ +Remote rendering 3.1d - route client per-part mutations through the server triggers From f16d5dee66c2f76db3d56d96792d273d8f65afb1 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Fri, 28 Aug 2026 13:15:48 -0700 Subject: [PATCH 16/28] prettier fixes --- .../VisorSceneGraphPartTriggers.test.tsx | 15 ++------------- .../jest-tests/WasmRendererPartTriggers.test.tsx | 8 ++------ .../visor/visor-client/src/renderer/IRenderer.ts | 5 +---- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/ansys/visor/visor-client/src/jest-tests/VisorSceneGraphPartTriggers.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/VisorSceneGraphPartTriggers.test.tsx index 28754931..7f8078d9 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/VisorSceneGraphPartTriggers.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/VisorSceneGraphPartTriggers.test.tsx @@ -184,13 +184,7 @@ describe('custom colour, set by hex', () => { await partA.setDiffuseColorHexAsync('#ff0000'); - expect(renderer.setDiffuseColorRgbAsync).toHaveBeenCalledWith( - PART_A_ID, - 1, - 0, - 0, - false - ); + expect(renderer.setDiffuseColorRgbAsync).toHaveBeenCalledWith(PART_A_ID, 1, 0, 0, false); }); }); @@ -208,11 +202,7 @@ describe('custom colour, reset', () => { await partA.resetDiffuseColorAsync(); - expect(renderer.resetDiffuseColorAsync).toHaveBeenCalledWith( - PART_A_ID, - [1, 1, 1], - false - ); + expect(renderer.resetDiffuseColorAsync).toHaveBeenCalledWith(PART_A_ID, [1, 1, 1], false); }); }); @@ -312,4 +302,3 @@ describe('group fan-out', () => { expect(ids).not.toContain(ROOT_ID); }); }); - diff --git a/src/ansys/visor/visor-client/src/jest-tests/WasmRendererPartTriggers.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererPartTriggers.test.tsx index e3b6dfbd..26714ad2 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/WasmRendererPartTriggers.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererPartTriggers.test.tsx @@ -128,8 +128,7 @@ async function makeRenderer(sender: TrameTriggerSender | null) { /** A sender that records its calls and resolves. */ function makeSender() { - return jest.fn(async () => undefined) as unknown as jest.Mock & - TrameTriggerSender; + return jest.fn(async () => undefined) as unknown as jest.Mock & TrameTriggerSender; } describe('WasmRenderer per-part sends: trigger name and payload', () => { @@ -362,9 +361,7 @@ describe('WasmRenderer sends when no sender is injected', () => { await expect(renderer.sendPartVisibilityAsync(7, true)).resolves.toBeUndefined(); await expect(renderer.sendPartOpacityAsync(7, 0.5)).resolves.toBeUndefined(); - await expect( - renderer.sendPartDiffuseColorAsync(7, [1, 0, 0]) - ).resolves.toBeUndefined(); + await expect(renderer.sendPartDiffuseColorAsync(7, [1, 0, 0])).resolves.toBeUndefined(); await expect(renderer.sendPartSelectedAsync(7, true)).resolves.toBeUndefined(); await expect( renderer.sendPartColorVariableAsync(7, { @@ -442,4 +439,3 @@ describe('WasmRenderer sends when the sender rejects', () => { consoleError.mockRestore(); }); }); - diff --git a/src/ansys/visor/visor-client/src/renderer/IRenderer.ts b/src/ansys/visor/visor-client/src/renderer/IRenderer.ts index 2df10d0c..5e18cb7f 100644 --- a/src/ansys/visor/visor-client/src/renderer/IRenderer.ts +++ b/src/ansys/visor/visor-client/src/renderer/IRenderer.ts @@ -180,10 +180,7 @@ export interface IRenderer { sendPartDiffuseColorAsync(nodeId: NodeId, diffuseRgb: readonly number[] | null): Promise; /** Carries no colour: the server reads the part's stored colour from its own record. */ sendPartSelectedAsync(nodeId: NodeId, selected: boolean): Promise; - sendPartColorVariableAsync( - nodeId: NodeId, - descriptor: ColorVariableDescriptor - ): Promise; + sendPartColorVariableAsync(nodeId: NodeId, descriptor: ColorVariableDescriptor): Promise; sendClearPartColorVariableAsync(nodeId: NodeId): Promise; // ---- View-level widgets (state is renderer-owned; see arch rule (a)) --- From 5d5530c09243dfa66211e6c6c28c81b630fae173 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Mon, 31 Aug 2026 08:17:22 -0700 Subject: [PATCH 17/28] update regression test reference to match updated behaviour --- .../e2e02_tensor_initial_render_baseline.png | Bin 33657 -> 147976 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/references/e2e02_tensor_initial_render_baseline.png b/tests/references/e2e02_tensor_initial_render_baseline.png index 40e86e1a32b1e1ac84590fe9a3382ef540406973..9532476ce46c90f8480e78063131b5272878a76c 100644 GIT binary patch literal 147976 zcmc$`WmHvd7d4Ecgi;a$Qc_X^($Y#J2ug=^OG`Hhh=9bQ8)=d54(XDV?rxBV0|(w~ z>wQ1Z_v8EZj`56_;g91!+r6(?Ypyxxx-jsKoFwMGNB5ABkT9jC#1xQ_?mR_8LVk>P z8(c9VFl0wU!aRCgZa=KVQN(TkLy`AHTuSsGe~+ZSL90jh-ous&83% zTw-y=s$VzRPmSb^PoKTZ_FTUaP;OLs|BUDv7yzUQQ6t}5;7i@a=HD1Q ze)*2|JaQsCY|L5Z<1tq@CP?{Wo@hZBbw3v@GhHm=VoOuk6+vgEBGV!4<9O50H&|lcHpMNgzED{lkAX^SL z{KkRa`{D21+;iq&P&m_~?z&$y+^wvvI+~HxXzjlnZ;usJ&3x!>X%VTK&bsktKMnx@ z3Mns5NPv3Jb-eDxRgiD>iRLOyw2Ex(TZa+VN@vpu;&-rL(- zZrgkdb&#?jY2wL-aOgG$4{@BWeqOs_9|rU|yalX*=Y*(a$8;~fy(FA&ohJD)O2@7H zJJpWNS~Z+t({wL)x?yhnO%#XqYhwq9x?T(J*xkUF3jms0N5_Bwdl2MdIs&JN&}-QQ$ctn)x^eV#U7AX?MYw7RcwQ_&XwB zgx_frG8wuJ`{^%W-9jY--#p_bO7YQxRZ>4xWb&>+F|h^LkdPeRI>OHGApiM{Qhh22S7v0Hn0k>>AeQ54Lx9d8s&EFvAQ!n~VQas_C z_wV0FC9@yYoI8vW#TGoiIvTiHrnu=ss`h<>3*{nGF^Nd%L7=?*l42_Sm(6qc;mDNZ zv8u)sp|cw7U9WB#?Zfy1_fLH*z7$uFC%c9uDLm#c=VEUaSI%gotG|1fX}`agXze-C zN^Trxu~Y(v)=sEXZ#Bzz278NdIa7U5J|Z`5UeuuCKDk}gl#(LE!_x`97O9!hW#4Vy z?Z;ih8N{BG*N^+!zklTjIlO<**kleix0ATAdMPNFrFMgNzy&L0#x(rX?E?OoBF~fF zo69DiFJ65Odk*`;mt~S=vmd8vDVwfVS#_?K2!&TDu_^T)XL%lHPGF^WU2)H^Z|WJYlYyVefvv+rw0p z`L6w_zHVlf<#Ml%HPO40qj&nZLRG+(LH<_x`PA=*eZ)AJ!Zjw(l~n{Ph5ew1Jl`f~ zt#RG+={dagsHtMT)_9YYiw>T~gDh4L23l8WfK5lq`dZAD&)Z5_mx>cz^tmpB54 zu&|`$mJzF(X|5g4nZL zIjNrTTd1VJ>(>RtKE1D+b<|v)3j=fax;gWbZ!PeG=XfQY(@vUeA0@E5FW$#b8w{qf zZF}H#-b$h3elJ=5dUC+CQcHX5biwmWw|sEXt$DB++Lu#h1wQtCvl(HY{UBOY%RQPd zXHzTpZNrdFFRgD*_d^8^i7wBL9ot5oe)@r`rrPn z^;d_zsW(^aP3waDl&jS{rM*H|i`X2B2jbFH?fymXt4tu~<=JgIUp5u7dR_GHk=I@v zrQRg{K~9s8c5(+TBy+z%-Dj=;4J||J^>NNfbKl&_uC>>t`he@hkozN_{L>fr9RosI*gAFJPmWny$a-+ zwa4p~(;Rs-=<#qjZM&!2N`=33w=v4?<8;cyfyj7x{6U4lK|^uz??NX8wPlj?Quoo( zwrj&2Tc^Mo>&+p0`TF&luwUiA%YfU^sbb2Z(|}8VNxAKU4=S1C#E99l*1jw1bo=D# zho^8Y1^2bRnr*FpIAY&x-l}OsoVE0|oV-hq;zqT=O1S8bC!FIk;vD3SxMVX=)5Yr^ z`MIk#o_U*AOv5LJkB>*E17M4TOcNjS^9pMjrE%GDQ1(n44??U51MkeA-7K+Qf3))4 zHP>+)&C?bS`uvF-4}y^WSE zjB%SgO5to2zWUv2X5i|YfZ`KXI5AwrF(7cn7UqJ1xxVMI?=}+W>EaT<=g}of&VHRW zLl^N_>q9rK&JBFL8wPNDpVVo_R^D|ua>w^i?w$Zg5DUw^cb&Q~2Tg~CqN0SOq?NTpi*5TVlUM+YjzxzytB6oEg zFMhrT*|2?cAslv5*W?CoLQHq6xC_**qSGZ^wNhL!tP9^vI*e<%6}inO?jnH-H8$XR zI+7`Pd@?1-Gk+Dp>UnS^Np@lZN3!5A*~JxUd6S#-2m_@?e5JzU4507ku?q|3O2Vww z88XE=633P_{3Xe$cUP+Bq7SzF!u938JnA4`>Uo=_Fnmalh}{PDW#h{pIg#^Fr+VK| znW?WdvDcy<5D*q~yd{2mUE}j<-9Gs?Ai3v@cg_rXHpLL6RhPu3k6&f{?CJbk^-Ul+BA!d=z+*Y2DaSk_Tv>1% z?B7n)^|quZVtd;u+^@~{c(lm#0vmuvueRe8GV-c9*WP%OM!&gWcZZyC90kqN`$2hQ zMLO1J;W@+oJ*{1`U3@cw_L)LEde~*t@*Z1?yPwuwEUm>*Ny(%kfDs1y?6s{q4-gU= z*@@Kt&^rQ$ukLY{z*?L^hrBqnD&G8l9S2KcURMT>5=PJ(52Y#}5rC2=G{RF4A1!(k zf3XTuAJnPn!YZrTt+6gr*NwD14~DZqVGu;7*qgU3_Z!aU0Ie8TR@I6&VTg{LJuZgx z?hyzhH;AvQn|m!yV?{*L7}v_IQ`H@%VS7wu#d>yIrP_8dZR%GYCA+*R@tpv%IDcB{ z1lBw!#c8fC_|ZzwFjVP5!bo^-^IWbSX+vF71fE{1!3UlFG-zGCyU7Nh%88|%!B5}8&4}ZF=*9kM65$=L#%X;62?g#C-ea9 z_lpy1*J%&!;KlaX5CyMsx;_Z=%Cs9Ce`kl8{N2&PzhA$Ou5R;+E4jZgX<7TgU{Le# zv?&h51NNG3v;c|dJ!{za0Nf_$^XVsrO;emp+>aZmX;6cyJjp zTquU}uEkM@2}C$5giA_}LcBHt&y7&JpPJ&L3vizprFCy)EL|`~0I#%9cAi(+$y;i= z@wKBtMIhy@etqc8GoA9A>UNcjT%-9j-Jyl6uir`8 zDNJ1^KVmxsiEGaJr~nbT&(ZI^s&3M7m3nhAt8+O;00HXRbdfT1VZ&)Gz|~c#K7(9H z9g%Zmn?9J?);^o7J|L|$Sg`MV8xcL9ofsuPnBwn$7alDPqVzXny6!vQODgcf>lnFI zuai8T**!C?iHhpQPb3@e9amQZ$rfMHqOpp1CsrWJx#Owy$pn?cT`C9fi&xJ`%4s=$ zkSXc@*ps2bB#G&Luba!Gt|dStp3i2gUV{`CHF2BVnk+Ty1rQsIj4e>yNV>QiFtb*> zU6?`WT>Y`Njn(n7LXHd3RP8EFe;rDUQlgwGd%9=auM$ zOzzFd9AeKn40kpw^K}5mt5(X{Ey@9cQlX{K)N`kmRkI&@nE|=y zq@}^|=Kn_mH}7Guk`unOxrMB|uG`5{{f{n2B6v*sXFY*3x%*c|fI%q!UtPQq3gP?L z3;6G&iuJ)opb-l1tYdg5ClQ1x{MF>mg>9ys7_HtuT0XhCELsyOlBAu>6vex^>_ixi zAi1GQKx#<$%`5S@_7LS?@WnR%J629m#ScTQ-9=xy++5D5-dcFEbO}iVS6BF+QxpGR zt@X77uvmy_2Y?#Y81|9n%||TsVQfBue87$0w#beKoqS63IWMZJXSq=rE z0E)&x!w$5h_z{V$M6)7F0mn(ceUsM&&w~gW&`LL#1=b$hIyWae@`5Kyr4K5Rz5RB| z2lc#DAoBoeM`^}&yTCrCp4~pvZsS#MZY~4LSj_GdN}wEh=A4%S;qw9unP}DD-%swm z;B#}4dPDq)Qt*j%1eCvkIEQWO0g<1a0tCxEz?M?^%-<{EtxeJzr;{eBoe&(l4*Hs| zug2xS*~bJ$&f-n;O~=DBX8b|vm!P0Pyta0h#RKWljwb*e1jqx)^;vA* zmGB&4#AbPb5C0?1Y-qk;`=eF0Yi<8%_+^S7@scn2&*t?p|GL_%6WVIKFa&@`R5vqE5>Y(FfW$p`8oEaatC zsBrpnIOaV<8^BOOwl$W|CSIsp@TpmA`+q;uE3sO z%@4uJMlNGsqfmhUUI;Ck<({6RM znR>kmlye9?u{Nw*c+Tdz;V#8ogHvw|^YP?PN>6~98y9@Se#!$ocY87glOU?7Vc{<8 zSL?{e#(np7enM?$O0!lFX!boRrt3|Q4pH~Z288T^kp?*^ki0zRuMAKk?L578baYgi zX`-j+laj&{vtv>`ecW@gANVo0vh^=g?qCRP1Kl`YHz!UP%by%tcLVnQ96sK3{S{U4 zb4NJfK{LQKp*fp__7U2Cvtuc8kw-@WRuUbD<4yTsujw)%!^R7k63Gb(d`Ep86i0EL z?>tUMqpIA20HY56`q}r7OghW}oDoQD>rHhUUZC#zl8KwH;oGUd{pbV$Evg3rBAL^B zvu1t$#H$W&u(9m1%A~^!2qBNMB`I`Dd4Mq}&LoI%`!HujweTW{k0l`-6fwRVH* ztd+BIs%AY!CSxf;=s1mM#s+yBdE#1Y)?P}m`GpQA8I}E=@PI+j9%(Fr-_1R~AnoKw ztj~xCoKaSZj@zp0SD7!gBBPGK?w~^PsY|#df4w4aYI^+x%(@LvzR&3z%=z-XQ`mFT zDE9748&HxUxd>?dvX)HR$QWIcO$(5PI#ZrKiYX%Kt&{tG~AKCmcRlW5+FM`GeOg+Bod5DNGFtDmA zN)L(P&V)Md2RT9~3~*gzd}tPYjNiM@j>nE9zelc#y#H{rNt52Bu*$8h>++jt zmloK%b<${e4ehJlH$asb5k`!-0sae|;rm8<&`vX~es#Oq8MyvJRSAiMC?1$|_))U^ zVfQgUJVshBO2M!YUtEhmcgTwEp*G$)+`fR}#lTGhjLi)%Q`mG^kR+Rk(qK^Q`jJErrqpSJgu2JBmS znb6I}z|8<4O){(vQCRokT5~`$c=p?+h*rKji;x^HIRVy=tC`s*6s3y+6jp8`*C3PD zK(2gF_B)#}2txp#dJYWFY@vpTg2fEUP>Bq{$UHOqmxcQP7-72dGm%(M|C!tZ)*@i< z^G<^3E+*HDAzkC{p~`n_lxQKhGA7lA3Xxwlws#%)6yv^U{ZK?A{%R z!n!@adlI`5tPb>!nZmsn7cTIlYz|^IC5ZA9$pd#DI94kCGy9AJAa-tYyx?^FOF4@t ziF3!YE%aiec9Ft7(S(^ZPdH39vo9aqzka6o9KG~ls~2+lg`CG;&EWr;ln$wp#<4|KZxzQ z8m(Fxa%PH6bH~E#4?ApjG^~P!AB4*d;-m|OJ>b98C9 zmt6k%DWph39jBc=YHLN5+O-dW>AaRLV>SWpoqK;-=bp2`fH^15{gA26mFl_xD*PU* z2}4Np-TSxzN*}Y)6T`a~c%WK{eNwcha-uMx#4FZ93Hi03MX??(iWPr#&}YxH(%Y>G zwD{PFN1*51qAo)SG@9 zeqrMzVx{UonZg}S6o*6lmniFEOtVmym6bs*Hu6_l(#~-muZHB!zlGMDZ(2Llp^GxW z9oWV6^2g>v?(4!$?4gq!}te9+Q3Oq8#wLKH3Ln z2;1LzJTb@#4`(W>kLW0cS{>_aYp1hLI#Z$-9Z(WL7NMe|;zcVXKU6W6LwL09esbv% zK@w0;7u5~~o5u<@#iZ8_Li%bCV60n;6No%*_k;FWZC3`Du3+tBs=woGzGGp6elY}^ z2n(pf&zq#_dd&ucKH03rn+UD3h1CH5I{@7Ci17O$1p&8%`;s4jg$UQbEXY;XV;ztS z%13utSy`ca9pJZP3c1ocj*~`^7Y6)+9CS)xY*f-ngS_h`bK5BaJ}KQC(BI5YpVouf znf#q|e%onOIK&d=0)*Fj2Y6Sb6n9eW03wv7BAzBQq^71)9JRH#w?nB9tU7MXn}67c z-16*WG(fG_GD7*bfMX$yX`0b54_>-xyS|70V z@)FzSj;5>LCIKLdA^$oRR6j3(z@hVh<(nF8cQQ_nvIA{L*0UNfaoUoUgZ1WtfzW;>N^j( z%^92tKm`>w$!X5zZy0+=PxP;<;lBxYt6w50ItLST1Ar6ZW6_#x+kfH5|6}Smc*Qxy z?eN9Nkk975ZXjFTu~o=>4-TmWuj_#uz5lFZYWf+V`7``;2cf|Kg5O*MvH_ZB!UlOO z!28^QkQh>OX?u=PgQ>FO;uoz5R6rmay-;PBWiqC> zL1k-3#(rl@r^aCgJS|A~KqaN?4D0y<5}zDYaOr}mKHwgt_xyz2V@0$v@p+HT!^Nvb2=3KVe{>!~;XtnQQ;tx%7(asLMJ!Jy@Hs2RNPyepVGr-05*7HR_A$`df~HGUrJId_l{SOKDLk(Keh-e0I4AbzesYlY{~0nu zL=ViLqwmv$=%|G{Do~H{k2eS8*&ju-=uc=hRlW&YAt#WYF$fck`AGdQX69?*9)d*r z{AmXqlz_}}$bcS9fj|aF3JhED0bR`IRjeu3&4;F5b(=ihMEr73n0ya-^F*6_Q>nBXT3<+ zd)77_E7_(69Rf3HUeKi+9v+5R6pgf;(8WrQz$ybYSIEc*jm$uZGnp70|KoWXsPm)q zp8QfYdvTLa*wFRx`P-*92knGjTZ*YvtLMOly?|sPWM%0-1BN;E9(b_AW1Aj^oIz*A z_q@XNAA8W+Tsdq6+CRE3?PbZJn{z+PLW{@cu>o3gH6M5&wqM0B}ZH6O#Q zX}bJxQ*W{PzfHYiJhL1!%zye!gcshJOaPh&Ej__dL%d)eo$Kg*K=a=~t<4%gCC_h_ zse(6Gme|!9=b-;^J{TbM%X7Ui%6Kva5(19CG~pj`hq?cWs8=(3xFUW1{r~Nukx+ToD35f1rf+5z7*CZ2X&%T9 znNF!&8W38HEi-=e%MJ+P2B7CbKSqc)3j*%mUe=~7sD%rpmvgPlkT@YMMgVj;GXnmh zJ}UaJBXJ&eL6x7=7_8vY{)gDQL6>7i;AaBSb+oj!1UadXhQ?A4f#A6W?u;5>LH~+_ z@_-bn482Du!~OqsQwaQV^E}Sr&}co}8~F0BotL10c6fLQ5dy$qt@^fznXY&6JbBI0 z#VG*Qg-Se2{cjPt@g9(T5x!bph!Y3{5%rL+kmp^+{}%^rvVQ44>#`pC`8?EYprz$D z<^e=7&abiX3HZW1|6%ke7x^EchBIglIfCdBTJT0bOy<1ZxL3Ok`7RxuBfYh5r&H!d z(sQ*3AszL`s|V1LAJ&=`+z$~zPnhDeY8;rPuJo!KfEKNe7vLF?A+jUT0);E^R6c-i zK_O#a4S;9*rUbx~Qd0ID>+HQ}ui=oduFI3nE|776jUeekng~pzKJ8&D0v(f~6n-@k z$2*W~Zc(CwwTfG1$Iu|HZO5ZY?9qAa4@XwHOLJ%ELq0uaU7W5gP;^Pnl{}I4Pww7s z&<^`s(vUhzNnUl4-8G{A%VUs=an1SoAO2I)UWokj!TjFe(@KDoXaDzNPj!dAY|=f) zJ2TCH?rTnYRpzJr1#Jw8)cDT@w0TUHSAH(%x5Sm=Eh_ z_dw{lp){ZB0v9$Z2J5?APNWRl-ELMA7dpv5-d-_);`PRl+pooMthqNdO6*1VbIj+)=}Lcz zxJn{4Ztz-fcYIVOa~hlMYyc;rDhVDHwA;!eR|2;Vf;~WZlTuQQk#^b!A3#E)e_&vw zWNO#ZV}3np{cM;VVwyX)Nlrr-CP{88*G0FS3|rAYm5yf;>sv*PFV{HW-xkTpE|6u3 zV^*oj#+fsD+K0cIWkPX=je#_X1%U#T#k%q<^}%y?9W^9LoT5gKi&m;!zGap#jRx|p zqsWz~27?r&-%82l$Hd7L(&r?5%t&K^p1%~@#HzO+(BXUCE`*39;`J0Q_IsV2rSzgM zc;dtb`QK<6-G0Skj(w$7rn9x4qImf*MxN+g2eRSDwzvYfzWH6c5aqtjzb-B@I( zg;&=?{jJhDiiyyCJS>w zYxn37F!|I<;Gj=1UB4fwE?Mo+(xN9q#^b;b1`NzWDOhxbBGLXp3 zN;B^Lm`WD=UeKr!*JH5~RTjqYbE}vHfNLEo>^X@O-d|12jsL(~vd3()Cc;qvT z0Il)xa7%?y&t?;TfCCKOpBO92CQXYUSkd~KBf4nX2qM}VlNs+zKOt%N+@-tEINhC) z9>FG;UP|$D3bR7_+ug(v*+kC>JYu9ep>UkyUI@&+wE~ZXPJKA$F8E%yx~6ROe(7BA zdgwlT`-cZLV;t4Yo~|AiWnZ{Jma*BU(MZXuz~B!cI_e}wpiyWzVz zx2Z7DoFJy;zob9cNbeaxMmO?IwtQ9*oibfA`FWF^ml$(Tcl^K0c;9BO$#bF4%N-eW zz9!sV0zVCPAm6vp<)QDy?)B5epVKc%C*OSUeyAo|ou*cnrIez~{6jwkx5ZfjFJj^W z_=6iAj4E@#V`AKdN+npO$Jw4TZBAhPT$O z|AcdYyMHR8@1RIXM&6nhmzj`*GIh5ifSkPSX7tHfT)eTdME8{>~+8xY5;$*$+Npj%GxpApFY@CPjADo;DtWc-Bw8139$b7Qg3g-d*iEz+Ny z3t<|A!~K$|<;h8;cG!UIYS@BVL2P4uO|aRtl%S6gcA%KPQ1PVNJHz1nA;f3{wG*B9 zrO92D{AlJELj68-5|wZ`#@Ez#Nt8Ng4zsl+?aNtWj9QaPM8SaYl`_sel zf4u-6hJESOA|*;n5uHRy2E;*xtkjP9mZkPo?n6`y+ew0m^aevJO1%(Vs`A!NO!X-D z+_pAqxeyhN+^n9arxmW!C{mg?)I$DoPDIm_;;_@v}ln8&TKMb!N^QLy4y zcPWe~#r3%g*)6AM|H((Lr51Uwy>rz@76R;AS%=A)ca`}8swr|j87B7%A039&`jtNq z9Q$G=>aM5V_>|mXzKc8R{Xxj^YpbM;L!?PYN|D^-Z05o5Taw+fnRtnuJF+~^X-reJ z50xYRHoR6D4?dS74r_v*83k3)^!oY5D$~eFJ)iFTrC?h2Alu(?IX9r`^R42w7b49p zs1R3jd`ftInbp~Jh?!l}`iZ>EQHaS-q0v7DQ}9u5-Y+#enB&v_Y!U8}s$hagOj3Tt z(uvzN)AV8=Lq-B)P&v7Ll*qqYSL3E{aVym31}M-Lg@>v>U)6j!=Jbf8ds?o1U_!m2 z?KWO;J_{uQ#{(mR5+e=uZK(%g&xK=y*(!dLYnJ-kZSCeXt^R72H%7`Q_|H0#wfLtt zUV(LDz!c18I95}Azq{~yVAFAOXJ?{1#d6I;+HkUKT27EOFVvgtzh#d<<79a!H@w_UsBT=yVtwCgC&Fu1?JJbZ&>SJLR>(9>?q7$RPW;oh?Nm8;7 z?N(4@CSv#*$|c8}KqD4?x=mQ|aY!jRi#dxl@eRuIE2+jGA%1?$+z~RLXLZ|YG_r0p zS2>7T!Yp#L4B8@4sIS285O8VeqW-shVVNSn9{WmEjF55jXOy7|q7l-fFFrXQ+*^!N zCD49*8lrFv+ugs3 z+_gK@m=$U+qMu28tEY$E8D36G97p)wmt|2gIjk6uR?3`fFWevHPT$RAXJwb?m+%`V zqn;@bPHID%DEv<#TMJqRd=!SM}phzdR>w zRgqtQMyk7J(woME28;OUFPC!t3`Oa4dHO0R!{IFPgYp!p+W7Z?{2f~u_J1>!-h*#6 z9Qq)~L9B9Azd~B7F-bh^c-H z3wiSZ>X>+IVL_zJf?7jZ`$XcQ4L4cwWD5D7Dn_OO-9*UyY}kdw@^$_E@@Z|rbYkK8 zJ*~~rM9;8GMQvr~Va(c?4#9|>Tk(EGXHv9cw8w?E8{$5LjJVDvtY4zIzDZ5XDd68z z=y0k^?8j3sY>z0VMai8&<6y)Mt$3iFI8cS_OY{0f<5)9R(?VWKT>Rs6u+|EOH433mggTz~Sl``yEs?E+>zQ6{I` zf?0^ZS^1vDyRy#-4XB?GE#l>(r7A2&)pZ7mChb#-2R@v=ixb?rTKXfg@(9!X!CT|U zB}_CUPru$*q?~M5iFs;Rr`yJin zPO|Nuxf$vh-yI}xJ}JOZzxs`ZjW@tvyP3ItT9QMdQZwNj3A@5IBBvsO_vY=DG_W=l zYU63K3&&VVz*dQ9(T9|k@m5HdQ@7V>j<5NU(EV!qsbIy-M0y5PFy&Rc6*O)UVhUf* z)BN$H@SdME6%UNXf_PL6VZ#{2Byrnm%n0^eDQ*`wj&?22HrEejx-vaa>!2E>`4Y<9 zH=#hUMt)eNjU~UC;u*Hl=5Os&>VC0t$H_!u;Ir~D_PA&QT+sE(u@2RGZsTM^mQ`M4 zBe_8gUP481fVbK1IxX`GL85Pw9BqrexidhM>0Fq9DJ?A$RGY6Pa)`NUOGl)ary2_H z_bdCH$XhC_G|Wiu!12HUGgF`XKp~CFY8p--8UC^o7FWyqR^`DItXQs`H{2Ee^b|w` z5{QOB1^mw`Lluc_)GP}7HCL6*=&mDpp1s^uxWi>FOR%+NYnq;-!ZH?A z^CC`recl9#;RfX&OF}E~N*V3H}B^f_-)D-bA&x|)Bj>kpCfs6v` zU?PJJet=Ry!7*I!^|?<_jjt|ZEQ}oQZJiGh`X;SK|LbS(I?zzeV=$dW3Ew&Vq&DA! zg*Zf8&Q{{47a2KnjNq09D30lg;0nLhPWmq1&h*4K?27H!k}DWhE1=)e(K4?xf{I9Q zI+Mzgswj>_eq(#0^85W+?Q~7Y4ctWM%%2hwvf_;JlJtx-iZsa(^f7FIHcEj)=vsA&+w$!?rqO|!p+mD!1-umfEc>it1U7@PFLIWUxSN` zak)vhZpaxEJ<%F|B9Fxn!_&wN^k@;qtY%~eB0qGQHY5j3IFg?}BaS(W%$_@b$KB!l zIWgvqmFSS9#rvxKNE(99H_F(#46o+mtO}JgoHr1w-)OLyexPY)1%Z?Q62kQs)a;B@ z1wI?rgU5B6B!Pr7Y*Gq)>S4X`w>;rnrx?@bpO~$UG-HBj+L$D%j-S0%egIcT6$*Bs zVIL26U_8v`==W%ymiNL0=LU*RAc?(WYY2b2DcNV-E$A*-UJ>kl6wdHUSV|b)!B&}| zkY|iom{k&1;uA79xa<2mlR|KyZ`$0*fU|?|m6L5D|9XY9RE&&NyP!&KZn@|7 z<*Mc$xA`lP1Xev}##|I?3w;6x>CcmLJkF0kWOCvN@ zsSv@4XtaHgFNdUH3-QK@%pE%TRFt(5Gp((tLriqI#xYwVRbH9|(@F`tgrWH}g{jC< z{hB@4m{ASv^X0pOIK>{Gb`AZv_oW+Nm~+}Xs8ll7*2>D%GA?PORLc07VIPD9(HBsy zREvhS3k6dp2OUTL3{vuI>`}xP4!wocOpXlLTyf6VljV>i{e}nAfFBge%KKgq=zc{G z-z(3c$Vl)ZI-@_PrXU_Kq8q~|bDg+BJIa^V6InH_%^OQBRLSpUG@&*U&Q%ZE!_N ztvs~4$rnAZ%z|c#qlt&QAjm@N{f0^y$3O{V%a%m|)as*k%blO}-@9N*2B-K~YcN*5iv)@j+Z=+@$Oj znMVEfAh){YMWL%Tp8?)4qYAimi7@_qzBi3;8ck{?-B+ibX@<(oq+@8%gAL$g^jvx+ zD<#L368bNJH5Sq&b}Ov4-oTG5+Mu25%*LO_8owm({*;coMzXTB6g_!~2vudNVU3h{ zgzc#MjJ5~|Wx`~(m$Z>io>Zv=({!!Uu1Zyw`uBV)1*fX8FCk*;K4Uw?`!`J{NtZEI zsf~-jT2lvrQ&I((8`4Db5d0v!j9AmK;qy;_31I<2t6z9z{;vHQ5uVj3`Wa#6EuD8B z*Rvrls*6fLp-SR9^|6n)#a^pMH9rfod=QZy!8pJ&amPVj7R6`F;koqPT>r5zZSOx> z-9Nx>_|;DTWK@c^oFi}qb7ZD4N;cCoL(F!*-gx-H9o{stUc1$`T!O2qwf6dZ@7l)%PeUVBTErmhH#eGl^QKIlr7LNufdR200!Q3_yT)W$ zZX~WF(cPlQDEsWf#R)$Y>OR+`qRsEX0v|ov{$?ssYiTu1eIL7fZ&dm-alcHimRKph zB1}YTi^sQlxsk$5=3uGW>O7mdE!f?QsC(!IAESQu@V!G_v8j2P8ny;T%GFR1LU%L(K6+!~u>So8b0=u}eqI{`2LTsz(I8F#ubv?m*abse5eTD=m!T zdiYN8%xaH3`+fE}>o#gKb(om1XPZ8Y1GX|__4_YEL5b z506L;$v^eT8o25ml@@0}Vc54#)vi&M!t=$)jhHXLtD$Z83})z7|F0L&@`@*(nx;=E zE_%m*m9^!I>Fb|cPHLpTv$6!XiI&N&d>+cw4R+Q@3UVxRyy`Fhp(4(RoB!r(mgeYB ze0?~{GscLg*2?<1wUxE`xu2(6&J|WlXa=*?%?9A6R@`%>Bm^$^A<*H6EV3P}O!yB5 zgc>)4*g~~rA%&L7-fa5LE2R_B+$TRwF<$hMb5wJ}^)%8{<8>{jX&taHZ{bcS=nIoq zjq17N2IxBrhO@le>7anly*%9=!oCqQVnX*?REw9c>r+mxw>dm%O z4@ZBMU%zI?nj9`QjbqQNc+u-V9O3ru$r-(75SBy)@sp}j=lIoCwUE0Sjn`Gm>y&GW zIE%6(xoEjk+F!Z)jQ0z^Nqtim3(7KL4HFx_UEpxAHblIBogppW|1k)p9Wt;ny|q&A zv)-I7&tEBw>FC?BhX{Q*`)Xc5(eO@~eV#sxf>VhrQfHHPU^)YPZAyxdDm(J1_CPf3 zmD#VSaq&^_JA(o`-gT+WTqlpa@6?s>iMi4`fDh`63(gESWMi;4{iszf&@+2bFj zgQ7Z7HQ9#kqu3GHg*-}p$s>+V*hZiF+XEP^b4aM}i@u{PQM_#@_ppT~+1RAkp|xyM z${ZuTqw}#iuZkVU($24#;bA6)hWKic2rCXA^dVu=Dq)@X)SF1!{Gt!Ty4uq!V6Osd z>wb=K325Z>>C+N0=qZWOW~*khVY9g%ua`ThGveVYz+RjMeQVEJ8_sus3!BS-Rf%zx zMW4#g#Gc#r`L!aANa`(e0GnBVU{jf;{)&Un!o6n8QqL(n%_X%&&XJwZ&|^5Pv0L!r z^hxU@DWMBxzMm{blTr^{4RM#8;^Ic5RZGUOwLY|!5&0$V?&5qp7SsGeV9p-?idTh& zJUuCB#cA{AKzWB_l7S~&med!2Rjo9Wbd@MGC5VVXk2wvMWSp<1I<_W&-a5|AF?d1b zJ40GFJX_5?>M7=y%;)E`TNk8_`3fJEWD3#yPcS$XF5P66>xyH9pNcUj8Iz&dA+WTFhzUe%zKQ}?>K0rolRkk zpKl&03;$6Xq9k6~E^5C)nMB3(s8fZ^H-rmkOeu6UE~q+Pq}9NTM5^x@?`T-Get0=) zo*+u@ael1g1NGb%jWmT|+V7^jYXs;eS?WE+t?MEp+%4I)!*jn5q_m`lr84zu=VFLx zsApMCCO&_J6_jSZ=#`77Rh|^BCmu?c7T}27|C-C$8{e(^vmZ-Rb7?gTW^%9ld8BU* zL;KnQ-&w&ia}owNz?F}GLY17N;VP2OO5pWL%&5Dm%B`KuSm49@Co%KzBoo5*=$iEN z$Ba@6+1CBYU663?kK;FkJj36k^_S6zh2>j{$5^qbJ8b;uz<*a_ysc}ALiLJd*|M#s zw>tLN0$G|5+gej9BF+ILk+En>@xc)i9f5kT;B5SpykF7UQ&QH(TxM1uDesdEwQG>K zQwLg+1$IgF6i`1Hh*32erOAYCdGbYG{zbw|xUng(Js_8)bCCAn_uzcQZyUvYye=TD;`LN-$dc1DwP@K^0XSuDKU#KUvNv z9u=@TZ4zLg_%&twh<*Pa9#{KRsPORpJH{meiSOl>4Y*&rw(1+|UyFRtQqN+6?X)D9 zG$bs&|8A}!*(LF52K+KF3@z^@Bi3wII=3sl+gW||5J%AHp_70Z-{j{wn2Pu(89r=c zu2srx6Rn>=v0^HvDrxMnVHQ@cHP!~;|5s@)L_ttzp!nWGM)FQh8L)&O4v}cnyJ<$c zL~jNhgwNrpZWAF>ju7!~O49Ve7a!^bo({JD6qQ9N^PLrjyp$ruwc@E(mrES^Y2PaJ zDF@rCuxrbPo2lpC4;4+D_6{NOUwkiS+Lj&XXfqG-R@);A%i8bN458auR_)5g&#$OU zEzQvEN-@*UGWbiW%JXdFO1QT6R%KZgEvyzE+xx~^O<9X5vwW8#Od5l^yHOhVUce%| z6e3;yfyrDDSe0)IKSw#kZ{R{ls59l!v@#z=UANGJl z;e0A}vxWPYNk>=O4FP}=6gIDOdmL+B>RrG614TkaB0dOzGhjTV zzNUP}D`Jp(o@J{DpNC5|@AJ#=mXw)N{?7E4Y>}I7G@c6cH6lMpcWap}AM0iN`0?PY zzaCBzdO`qO6~{n)P<0g*m1^M3i)$FqHDeq#8g-Im;@*`(nA1@bAFMtw|JJTx5H@(& zz@g2av7(Y3RLvB0M;2{_imKN^{lc(uPIfpMHl#wps(ciFHJluB9F$~69uXc=Ttj>q zIl`UtQuyZWpM6JyxXpc=&1_cEvc;P3Wl`0%YOc82>^M5bM%&~@*h6;vaO2IePlK&B z>hoB2u9MaI2;ZaVlZ+k8+;W?_#aDF!JqdzOeg!Ji(L^WbF?TNgblObz4DeTcdV^cK zt>5adXDlstVY;M_GNH5(@xUE z_k5h(5yh+Tm{klOPm*&j$BTuTw?k*Y!{#Nk5T!{-u#)MnNB9H`o$YX0t zG#i=rMsJz;m_%S{&;%T*nW*_wP8?Zg?JwSvq$Jx@)ikO`>28V-in4Vw5r3ZjUExVH z*On&PIJ>!#wPXPZPTvS`(IM9h1?;pT4yWVRz*ljF{rYbuyVR>#KCg}&!QO<*ObmJP z^(W1|=;0m~ptNr1nW>ZFxXP9IQLKj*{K}0{LPWCG&1Pe#v&CA|yLt7dG|t_(zDbLXDM-jM`70daR=i9X-=ALjM^4T;_jO<6KCr5H>^2SS&f*upKbH+3MMWhy=R{<8n!M}~o5`~p z5;dG!(9;!1fsNgll}^mdcn%HZ9gI^&perpE)ugDeQ~r2SL3y`0WvkM)A{Tga5kvqm zWId+`+7z#Q@)ka}+h?_Ca>nn%`WY;o&i%`pg&0M%IO-M}^%pS+Vn*OWc z`ImO2w6v_L0E!Eocy_GxQH_G;70j}$h zY#$i-^j_j_4?&y(!NPTSD9XOlTPNA0K55aXvJ`Fypbi5Q8^aN`a%ZW@??DBtlIEhR zO+WSYF2mfB=a!NN+6Ug&1=~f>b1ujE0IJzxwmh=MZ*1wVf}w#r;>)C8U%Y|>XW9D? zU?tSOt{wP^;9_cMInsgO&LY8(jY6mTP3?6=bt*BTv_^tj)MT2}w?TVu10(Z<3!uF&Q_6-zPAAKK7Oc{zf)%BIKxrDiBB1 zIp6xj37M%a6JtbPmk#;^{)>Ufaf{N{LYJlMTkS!OI}Aj0$@E(f4Qhwb-w#Z~O!)c= zI@L^axniaO{pRK4XpF#PpZkT7N{Fy0QsM&U)HK-kYinAZ2IAWCmm!t#J4ygBMJu+7 zQc__>W@7avYV18iBmVl@wH@(mXhW{Hw&9gtS2Oc$sdyxA>FH=G$!`^ZI-`GXH0tw1#wk?AH3DBJqm0am<;M3H@hq)bgT3Z?f99w&}x$K`S#S zX644H$yN>S$X)YOT_aqEsii_rg_))brX9`Km@KGJH}#5>r0DFL3TktD&7tTt6HUI% z5XN>*NBpGEDtE7t@z6h4gn~HvcW-r+=Te1cAhOiGHmKQg67!Lt;Fk2U)9|!_ofaK6 zUCM&$oC2cCHK|hzcZZZ^zK0j|a)Vlq7hN%-yF=BZCFUZRmPswz&wS3(hsU~AkH3`r z+vfy0IxTzgb1s1>rys>3Tjkf4L@G23qhe+lWVqs`U&(^dT3;@@R5R{S({^Tgg)eX* zf-dY@eFm+JQ@`zcUWJA4B+z$Y^nPpqo=^zR!iM-X88g{z0}O?H-V2~_ zuWLAS9vUjEe|SkbZ54;KNJz~EzF%ErG$AD+RJra^vvW|?E=Y~25ZdzV>E#|qQ&liI zJ5!>q)V&lC^NgD^4nDq@Z-V$hG3h+d)}Qo7lfLh!@6`*8YB8;IL_3=Xr<{iU*w^Yl z4HR9hnTDVxnkbE$ab9$3siz(h*(%|5$UBx#mfIRs{tVPlsNN8+EI3l4oZL_1wrNvI!D!)sb0TgjT%fd%)-567k!hN z(L=`(lAHwg?6DYq_tfE6*NXqM+V$H2$Pf@tAB$NUhfXqn331fswjRl1KGxHz zCX$gC_f>&=I^g%KSTC=Bqtrn=Q=asMs^zP1&c7WcYF$?JnJ!;B zQ|n2E(#%Bind`ymo_RoOFqNujVCRD4{%CB;3`H(Axs6(xx@x1IOQ!j>Z7Fib*&I@} zc3R)$9W&td54DJ2fT!K|Q>ke3RfT48=oL8(n+@dU($gEUq|2H}8P{v8UP}6XFZb3} z6FzTJcy|y#GG>q(FA}W8;yMo;zK-))n&2`L5@*a#;$e-EWvBWqrZcEJhf-On`D02J zEzk zUgV}~PBk4eXB-MdN9H~XT5*sJ{L(N`tnB4LNY#!*Z-WvE1%wkI*fnBm9_m9+Ee*)m zyRWUBM#PG`*DioGUWvMTtHd5&h6GVv`Ri=PbK2J5s8og@pZ=QIoBS4RjoosYK8w^G)3O|C?^uR_m$*7t zrU3KiY0h`>f9itFAoL5co1Vm}a%RsxRVze!{ke-2qLO7~%`5}E%b~WNMHAkQPbeE1 z_H594`AU{~n36f;qq;AB*8cuPfhs&z6`ua#mQk_a?|t*r^v-ZEIhZ%6zLh)Omv99I zrmVSR-&aDFBm2|;c`XJBzoeMBPZA`{)6ao;^3mt+`J!NZ(N3!kP&(Qj51oH=h@#jy zcx|H=Wc4bgJAe@SDl@tP7l#ZK-{T^kN*+v1%nsGn;1s-26PYD<3)fc!4y_Az-d^7s0dQ}l~>loNt z>Ya0pf9kRpa;f~^#x|!>=z-p_y}})T&MSm_6j6YcTyIp@98RyX@8ddArU&KhCn#DBLm_94y>D z(1e?PKh0TPNPJl!z&S^2ykxJ+z_98DWyw&mcX3&(3#u_w5e`6w4OhO&pyiC&Z6o@_ zCbl@WqU&B>5UpquIdKMWiZR8SPG$!*QXS$=I;(#@T;QTu4}4Sk7Z4gk4;t@_5~EGs zPGkxF7FvI@s#rX}7Q}EV`s&s06uBVIvs@{omA~4b+)za8D1?P;UMr9R##etvQ8FBf zUaS2aP~C&^>qb$-7akXN@L*%Pp%83}0E^Ucz-@N*r_WJ~$zaLH{Uha70f zPAYnTqkedj$%MZ94Z7U48DOTR)eb z8eU6m%0KavBlUV`|H};;y=(irZ#TO!Cn_z%xV_MIM~w7l?;4ck^0I`QEP5EgmBjdb1&-nWN6))V4-ic8zMJ=OZKo6$!%GWQsv zN!h3ZS2T6W&{J}ksGoP(uWLEZ@@Vjyz_jBl5Fy~ykP;TDOT~a@r6R479!xaOoo?y_ zcbO`Du+^bK(U>9&OQ1v>JeaK9f0KU%Tvn5j;W3mt^T@&0lw1*0o>W(>q4mOH=1Xq1 zUd`4cg|L?Eh>!Dc4Jj~6K)r!JZkF-82b`!ORh|bFFON{c%H$xOj-t#fV z+JP$bfK^m^%XAf70Krh2c51!j_0F8}A6bce-O|j@=5_Pn8Y=ey?s!_)!r7W!?}4Vk zNEPVLXdXKB@h>H2s`wd(lUG)5K)U`wo#QaT$`$!RYZ;8T$cEhW z(SXtk>)-O1kJ>&c{%8Z&Q*H`lYw4EV4l^UghtWG*{@5YNB^w_nu}i9-6T#bHLo0KH z;|A!csEYb)02Y)4TFm)n+-ddqHRK{)sOAA4Js9I5X)G zQcs!^#P0j-<2`x*yn_^VN{#q)n@hY) z9?zsG>Ok|uIXS~zj+?&r^CxtK&!FLczR@Q)g@42rn4RnWwyzX?hSCgb6?xenK6_BN zc{JlBRidRw5c|8QX~NdsfQ8Gp<)Iz&_V$U}1oKQJ6xwuiBtu%{F;ca1YWSe;&kO4k z2zM)6eNoHqru!K%1tdcX3#d?X|6S9G%h$~cG-2NGg9tElxLy7rb7=lSH~n-7s=XFv z>4O=;6OPAnoJQ}eH}|j)vdAzr?tkokZF(aWK*5tf)goj!2+d9VijvrXiC|ZL-r}06 zemw8jMwc-K04-k6{WW3n@AI~tCBxptunek-TE>nwmIBhs`+^3j{XxISA#HFu53J;; zu=oOcNe}&ZS^42^ZDK})i%|K!CH(^JW7>yZfm49@;BP-uHeWwlXwj5A|6xiYuX{-lV*1 z*W{n!UKMh(kSDUDnOz{$F6<6oP+QGjZ{&GY3hekVzCaTmg0M?-miU{E*x9nLFT=^M zkA7`e=$Yk|2N^a0M|aiJJ6xf9VMB_(D9eE!5|H6m)SZ7SC7lL?V9`sn8M;F#UO3SRlm&H=zXGMlYYESlc)A@MsV$a z?U)1Q_!3U+Ree*w%8J`#?Ta^6O6XF&JXoq8h%GUhi1*%d{5DZ;XcrPX5p_J3SJ zgS@m{k=nxXM^x`*Kd+5T82M8{fK&xe(3QTY0GTrud41K&@OiqXaiWE{O0FHFqS|LgV9F=WmNS zn{R}rP^w{>W29Swyg44M5 zwAlAy%xqgQH<$Nf=?VUS_;HyW%(`Z;eDcd6FJvvEFiqK}O2V19Up-yeD6CIED@gMD zbMn>Ic}NHhQJcOHaqb4vpKaMxleOR@vbr4IDSQLC=1*VYNk^@+IwP%fL^c;fp9G}3 zGw0SUwD+L$Jdb{8_p2TgdysVZD2Bd|eSry=`yXeWajwZQ$w~LJo}|+`VbSNZ+p=N!u%P^LWzt@7C6DH1is@_3dwo4mq!)N< z2$kR4Z64%#`?q^Rh*GR;Oc6E3t3m|C%gT>9oI zu#~sZm;Z1RqB1etY5jHy%%_EoTp}OJM!$CC3uRU`zG_G%XN7TJZpD@E9ylM~!))0e z;g8312QV*9%jOj{>K^?aJxtt}R$~bM)pJ7X+B0w$F1Der9Rug`K?N8b^tB;-`ki}i zd!pZHjts=+5eK(Wv^qso06*>nPo;>x*qEElAQKcVYxFl=JBGUZI z^3v_#0Z&#K)G-gz(a^FU0Jvk_mfJ+4s-&@~|t{=8UW2YlLeiVVhi$pD*6 z%aWMtU50#n+R7D(%MrlCXtZms)DM*bm{UD=YuO4>Y*3rZ84v%JxZiGB_-H;v!-iQW z3zj^?Wl}XF%%O?V7uS&+y!TCxvVR7JIfb0Kpt5DJ300kfX80x=rFUK8?Sh9zkq<_Z zG$32^m=Vu@Co5JA*U?&XrwqPHgv?vPAXgD zhS*1j^mWd6>QcQy-P6;e6W-459!9KzxT-8z(0rC%)kt|SUz0IhTv&0oC>&gGu;0cR zZil}q=yU5?RmQ6PsYCV>B3y-_(8D5F$7cTLlhZmzemOTAwEi)yjE}V)Hlx@|~Q_-KnrF+OsjQr=Pb=Na?)c^DJKL-jOsQ0}3chb4;P>O-S;3 z*rV%io*HGAY88L;?d{N}kBEOoB^eNEZ2Nmpw}%gn*AJh~%ZzD{z~tHFSRTt6CKdGn zSJ?|TGs72~PGv#Q&a%i3AU$DrHwtQhwYG63NXi_VS%>PtX^E;s6V2a?p{g%JWz!aX z>N=FiCVM+!GRF*SEA>1i?Uy{j2Eqjv$OTYcuP=qM06scm!$g!KHC%uuBa7@Y4|(fO z7c6ik0IL2&CWD$N$do8)i8EKGlnrrDit(7`(BU-9lVVR~KFKAs#DyY1D4fRWiOZ7v zjmYTG*oc{KLq?q1Lq^2=Ji^6>YX{GmEsx3>DPXf`IkI!-mu=*LV;8z>>{|LknW_7t zXlrVCZ#K|k^e*(RN*klSI-mbgt8~xoSzW~^TVe;q)5fPQ{i6>2?4YG%+wyR%Kvtxp zJ#lWRZraRgE2DKvpYOe>h11eO!WlktweW9E?&RHg(V2hOLM0W~VC2iTZ6qSdfUMLv z8E6(11wAYM4;bS^5ZkMr0iv7vs?Gtsh(T3oJX}zUbJl7O3Hqlr!aRU6k_|Al`X+k+ zYezD6G6~)I(U)SX$c|Ka)}}|n4u@GM%c)i03%}nJG+6&H`KT@bk1G!$7ygl4vbG;U zL4Odmq%NyM>gC$g#)`kin(bG+Avn$we`O>Hb(JIS%I4V!jQaMxm8^#Q>KX8fnYX{F z6^_+el_(64@ssMwS=1dZ>_>i=>qeixXBnm0iu_hOOKx^;KD8aL?^z*zD&&!j2~Mne z+uI<3cz6S{XZ6^vP#oGCTO+?NcvnYA&HbwZ&kSVKW9a9N$_P>+nzvj4+R1S-q#nN( zY*sy3SB5LmRS+THxt#2Kc4T?z7x9l3&M*&qp+5Y~HeZh0U8;InqR*hYQn$CxitZ zSgxwKKMFd@X8|xL#<2nR=1+jqc(+icf```b!?tryyyLdquZsHAKZ!@uNq65*FF49P zb`~X(G__>7mh78SP{Dh)DF>wM^Dr)%GRvqRmTLMjp+7(Qj}E;UZw#j@J0XwQ+|z2) z2JooPx6>dTRZOY}b%#6lDx%;kcJw~TddJvExSI{vFxa)QKY)gjez9@7KBQ2WCXv-G zLzBj08{{rRr8n?>=WbouAcJd9J9V#LU(YG9h>=FISP?q>p|}qJ2+DBA=!4SaV@~7H zuv`P8s~uPK4YA=zyzhYps4bWTWnxo##c^$3S(%_ws`E&L7f6~b{k<)7?I9Z0)|;j0 zFIb<>fiuwDbhy8N)_~m=2#_C{?L1MUVj$BiPyScvj@`$n=08vem*h-nfZtyNTSp9`-%<&eCY zbNr;9^{j5wagdX~Cd;zGc`2O#iSQ=b?Vc1XrJ=EIcaPnr2({;g>KFfGN%_laFCze6 zl+~I?h-Nd~1x+s@)FPbZDU91C1_3&nKOaeHYg&;B(}QY6O1q7rS($ry|L=>mfkpqv zaY1m z==#JzoQz!`^JYW@JOD%cbqw3FlpXIR(oUUF#vkEW{@v-A<<#RObcfhMwz zV}xwu@R}Cd_xoe`wR-07yWy9-4uVR4vfa&k1+E~pS#J<-2&QB@w9>w%qt}?S$};!< z4n^&oouhm#3*-QlJ0G8)N>*kh3wfYm{l_BKcldHR${~}2PB3SHn~|EoJS2Sgx%8!E zkzGs)XD|a=xQ+vjZ&LNIwAelCZz6p8jdYhDu znwuj#paj?69y?EJ>ZFZim5wZY#Ez?jCu| ze6n>I#F*}A(~UNwek#8&{w|bgE5S}Vq|OgZr3upbjO2b2G;-GqW#A^{GEdAe(Q1N> zoc!6@6tQc-4L*xz6Am#YEyj92qzi z4)@hf*)>XE3j%-hS>WEURdecini=}ucS^SG+_LVx-JF=_r4nX$PIX0gon94=x0dT14ZOMJw#?Az+OMRj>9gdECh6;l=0nc$UDSe2 zu;_8t%9{t?Y+4)1qscw}yrUIZu>Q`4anN9k9ev08T@5MCW8379|HlP5ZVEaWgZ}xM z0wuT?s*)sMCwM2H#pvjzEBB>dqO(@`Xe_I<$~46}b0hu36-z#tbyn2S5B*8_k!rf$ zumD*-d<)5*mSMfOb-S}#!Y=)1PVWJKrvlR(MIotf~cx=rqOi7;uAjQ~Ere622jD6-~{8~2jr+LhA1Fr z{Oj|OG$vOu&4@Mk<&^G+)~a_&hP@jL;e^C6CeBC{q8EenFiV&YsI6Z<#XQ`-C#?_V zzNxB2H+|qB&GE$G_$*$qvc25@jhjrFL5AEEBqBvtfca(#q+$!)&5|frhV!<5o&`G& zu06IAH3vK{LJ~yjTPl1O}@lgb%g%g zDgcuJLI3^3mG9VYppp!)%Z=6Cy6k{bJ^I>1b?6otR>guPKar#_r3vz#SB}?aBOiOp ztFCPBA*JtD1naKcbpuZ_Q8vkLo;l_Ibq(n+oc;XEi=;57P2wC6#@+fm? zd)P1LDqYRd#24MI^C4}o$7%^8X6t(L_j2_&(H^1$N(2Kv58=9LNJK0HnFsV$f;sVn zj)sNN_fC*aityS}6^T?&Ud{Q!=^eIy8-OvHtz4cKHI1GVGWge1_(o%ZnF>KA%ty@= z+5FvDXn;X!@^c$ET$zL0dCe=`$pJTrnGvD}GctB&C@Ckd`D-sYiG|Lae$mkML*i*-*kr%*1u7&=i+K+UFc zWnUtJr(BuFWp1=>R$X?9vbh>sE`>m_BVNUX=%aaM|DDO4vV6tw*@cVLRhh9dwgvRg zPacq9w@gQO2PRp}e|?j)I3mAz)}U%@)bpN}7Q944sfvfptNKr92>?2|FEKlJpE*0m z1T3mL9_mhaSB}E!iaJLYUdw$|P);h(OD}4BTg{+sDX^V=ZfSwY>;I)=_PAOyZ@IZ0 zm+vMt;NfN9?najotY|I4F)E1azg+T@&#RsI-ZoEABq#J|XjkO#LB+*z>C{XKsiw@0D64)cd z$IIU{*bkwphmT4m!|7z|!;RkkSH5j`a#0dmn<09DxVlo&SG!Z^*9zI5+_>>`MLk>! zI1}@Pwm0TkY^eX9jfY@&Yp&TGb+8bFj@fhG_pn#yhSffj_!r9F1Q#-C=~eZ;MSJU$ z=8`%`IFzRF&`i24XmJ2xn!rD^gCB0=F;`QvQrpy5Gp}krj#l*sq0NmRzl7nY4>v4U zdOB)IEB9YkUSvrnAnL6en4INq@nHmcY z+bMa5z{MEQUug*)GX|u~K)F%ITm#O5pE4nqVS1K*^AnDo_9~cY?;J>FQxGmVE)ZpW zlkC#FSya~bm4A>2kHO_8fxjl9?|HowWQa8ILxg&pO3xQV%B7vny_9CX70vt9Et~Sw zU@@-mF=hCNn++vd^U%k~lltxo->Ex%al^{9w~5*e42Jjk?!X|LV3)kI(_)=VQtUm+ z|9?FrTl{BO1cN36(2Xu4KSQAU_XC1$tNFZ{4BmaYL=EzvOJmbxIS9S&?HAFnpwvAR@iz@LB0c* z=Vdx!NB9)qQp5>e6*M=Jcnd0?)kU(3H`wyn`o`PGglw@Y?;2_6w~2fHEA?Jrt%=n+ zxcFXpk>A4%6?h2Fps&9au>nfSk(D(h;8N}+v*&pN9E7=_>Tx{%{h*Mmn~}aOFKX3_ z*s^ObA%+&TDDNvDP3Aus0~atQG9BU4%BYGU#fk*M-twEg0k@Bj&jXKvMepaF^E3^k zadtA$I)Gp#7Ev-<4ks%QFH^2_=Upx` zB3r~P4fHT6OkV&rH?ZiMaXZANm?()~6enp}l`z#4$gWf2XeF#=O*DK!mRDISVIZsD2sju*C{@XPJd_Qcvk1?4eMz z$k>NpsjRZZ6%0;`-;ErU=RUIO%`IubF9!9x(+{yo`76WBeruX{16?VXnK$XHtBFz2 z9;h_W)ArCsy>})Dr6GvL5=#*udYuKDDbEH!@?aHy4uK!v&i!>6#mxI)nGzFgvq&=~ zsYurnq*WSnR7_x(ZNULYc>E@v?(|RBh&%SeZ4xCwedM2{v<>chbW7g~pmV$?Mt*6D z`ihx%7GEOu^*nx6Ck33~AeSY|gTxi}8{WK_?UT^KizfDtcJgSxAp|EPUYw6U6LjWP zGi{Slw86cecNf;gl&ldib-1xMN<&y*i?A#IP#&fIErB=_soWtoM9JeQ5Xd)v9+SZr z{cI^Y-0gfYQ+3m2Y)}FuvZ^nkrD4euvH18Jt^M9_E-KXsTTQ*T)z3wgo30Ls1Z<4I zEsI3OV2c+t4R5*YOI);RZx9^Wzy17f!zfj@i0VQbNx9wAeEX3O@pHnCCXIDP&HnS0 zzQTMN0!yMBbH1y|pN}4=7GvLCFG`&@z{8{-!t+}%qgrCH>6%>M{5zy`)^%kU&_#?t zUHYspL675tReW`8gqcyK$wkpFgf`zVO-nm=GTxL*t7KLCyXU=Nt z-|8tH@Yhof9@HCkLTTp<-X+hcylN=#xKS)Uy>YO6y;KMXp7Pvb#RE`$gDEYU^{(MnWNNxPjd( z=X0~e<59L5fF&8%M#HL{f-Q%$8D|1}WS$pAYNKRcbap4ctz~!1+<7BQ?O@w)P_ON@ zQfMH#IIM%Ku6qmhr5<_@_v*O_KisHd?B+VRZ~O~fP*^!UDUWjMaT5wA!bQCq5?P(g z^wqp*HksU(vLGEJ2SE<;oegl!X&1gFNQAFGwfT0{kTOg-o)j>lq?gJ@J3qjN3+-gz zyB&w0Jpp1G>GTSWm;E~mT2)oQbLLMhem~hxI07P%@kCj@r&)B*itmk>j1Pm`WTQhc zlrEYe26yHo47cWo^!!{85qZy2Pip$<9dOP=6=xTIJM>rC8Lk^-V`j{iNrLS*`yf51 z;jEq?@p}nncj_2PSLHF#w)#~Sf4E`RbjBiHu_Avar}oK>J$GuIe)5#7vZzjri#5dg zhq^&F1ZK#yw*UQY+vh!{)ytC>GIvuM-~1q^_E(+#iyi%5tGO&Uo-P7D)pI9iazvM_ z2`Ho{Ht*hQYs62c4g6LCsBThoIo3sqE~uMO59$At!k3`KIfH6t8CjJK#yN5?3AQ@8 zta)V;8(8!NIU~rKGVA1&eQo*KCxVkE8aoQX7D1q#VxC{14I)oLi1_{Is=Zlge>T1POb0JBxWreJI=U&QNT>Dszuyouz-}^C zHEz>^K{5M5oycaBNR0ROEIOFJacYLVXgi>6kzRl+SgSy2H;c?us^lcJP(m=a5kAsW)67>joqYI{MPNCFWNss(<^)b zgyrbb=WT+mnJ()Z5~{Mpng8Dd?2C{VZ9N|)3*^20DQ{4u{wx=HK&Xkt@MavA(3vo6 zO0U34-^QZn(rAfd(^!6xRL-W$nlPAtqcXA7U$x`WXI_^cWGP~YGbu1)k#F&@SLYFa z>K$2_`4C&JyQeJ&km7v!I53ALflv#Kj+SL`fPC0hYb+Cg%yHy4Ynf2w+Bg%J_A+;= z%H8BS%cFDB3&&v$qO@0kaF?~-OL?~E`uAl?Cl{s27McQMi)#CH`Leu*qnntd!YKmX zHMi?m`wuJ1iNIIJjDLH*TMVq=n==!y|u(B5z9J8hO|`y37u@e z&_I&YeV$obvS-S}mL|L^f` zL!=yVvh^!_o)6}x=6orfm1a*l_S+zO(E3lXU7rV;!mU8ei} z?L;OyrV!>&=NSIE&(Q4%KEBpV3%lX0mgav7tF(Mq^ub<+O+hc~fTs&^_J3SJUbW|+ zCoQz!_C71Yjz46+aOX70<7bK#ZA^A??s>5usaqb5`wX_r`}@&98WG&KRu^T~4OC6y zxO9*{2G;6Na4Gto?!SYMz(_V7inV{Mx{0c)^tlapS?i3IP%PgA;K`+MXU|eSvcjO8 zgb?p=_{60F^1VA#1fK$UYM#KFaa64)P{ z;Di83N<1T|keUi|`Ys_+BE91-pg$aMkfeNJtV*__>87-rxPin>pS_ zZRJA5j`Krm56}WZ*qb%1Aeb+)boV2uJUy(xxc6L?9)j&^(HD0#QJDBMpj2t4MS5dp z^oxvc4Na%L;+)zAq4yr5lZnA$sqDEU#H~n?b3r2{R7!aIlSm!f759|Hr)_qYYz-%n zd$`A|7GtZtM!(a2Mw>AfiHU}P`v(z9ghNW<%`=0fpuWOtxmeO)WY4?ykQ z=8HqA+p_(lbzaM_GNsSY6Xbi3=a ztC3U;B5@-+2!5I&*RKKQbPl6qsvftj+Q8x4nfJ2aNGP)m!rHQFIqo#v_Y@uWB_l)n z3XL&nEZzxZB8UHanQr|e#UQEZeud34A4*@Si?7v2taBI*q#rTlwsaV2 zD4XPb<+>8TTEs5?tWMvb5vH~jQkA@g_O_k8ZNnu!(-p5r_e=}*0~qGzXm?kM)V=bB z6aAMRoFzTd*9Rw^c=!0f^4r!6BOGZ=3^fW~xIV6qsM_T&7?LfVes0&QL%C_>!An-g z1Wx^vg{`t?gGolUCbho^TZ4$)UaWf#8ML)NKQY=Bg}jrm>5E3(I>ea zrV=hbQaoRpijCQ$F`g@IGfP0K?u_LZodE3`5=(tuaZdaPrD7no?%t?;UcrCdWksn_ z_zwB_wiBxcr==foQ+);(LE*37Mh-BZS4agT6nso3x`E3qiN%nDb;oDPG}~@u`tpVZ zbw`6v&fgoef`KaVT>JgHTv{?_bh$<#!SLyIA0S=bLEC)@yw4irb|3nt2k z-KQ*Y1p0{Ye=`MrU*goo2_jY<)7cR#Irna{wd8BJXegS`(jr~0^7?Wg$@G(%mbEZN z3ssxgFhUMEsgkQe$z1I#Rf=o$+gT)`MNihusc(80>w(ZD9n%SHPew=m|!W)G!%Steh|T;qc0`^=k?S_KAgjE z>)_U2Wt5qKXiW}yt_gTf&PIq<|M6@LFGi|99(zQleuOXYCX@vbI*lLqP99F_3#nEw zn}|`<{}Sye!|y&2x}Yaw_1ysuv`J?I4c^AEy<`W~ z?c}&%L%%_PY0bymf|1eyl-sNtdBhhDxrXBl9PtYulB>TbhTTNTV4gpqzH|A=T(cM^ z?~^%IwBKKz9LhAdtCB@0(nq%pjnuZ^um1B@X_vnvIG2;W3lpoY+*}5tUZncN=CkHM zb9UtN8macq#YA+Tuow&7z#};jj%+4AH>M1JU(OTXR0AU?MdU8}+3rno^LaD=p2y5s zq>{&M_?9l9|5K)Vdhq4kdu)hlxjFSvvni+0NTk^~*|FEL=S7|x8cKitdmxsH zi^l(D>H#PD+8@(nC-eE<;5L?u=?KtlH}FeUZ^q}c$@q5W@FH4yVs^M>M)^Z$f~^Lt z^6BV*ak@7|ax_G%b=1=9u~~UZ%_=c+l`d>8e^tg|(+gkD3XSe5eY7r@4c|J#*^(a< zl}IVWKHW3O_;5^?)>h9*$>=&FJP4L5X0xlG%KytvSNwO6GmFzmMED+|gJf6eh7YDP z9q>^}xayVdujm_TrCiJJ!7fUoe(c*98~WHjm{9nBG6mj*O`7ED^kF*Yi8)xGmSBV9 z-xRMvAeiJs5L?b8Zj$Emz%V6VRD3d!5PD1(ZfdFys`P9zAHd~W zq-!D>PuZQ@slWp35|U=GPbNn|>h<$+Bi_53wyFB8;-d@VGy!9%M7eOL6ZCchE+cn= zu2WA3mqTL@!R*wD$}kJ5z-3k2#W_w>mT?8YXw#BzNrVHryS6-djaOy2S6lk`Xj z7z1SG7EGmr$9)M@V(W5Y*GzVPv+FFVPv@!Rh>Cy@J`2#)-epxP!);MGFFcwMsUzZX z@ahzX42^;9;EQ)r*RTC^)tpF54Wj;gXYtjgw(KWQpwiT+&D5JG#XZs$uW!C^X_Aqh zmnvna`oz?rM7EqB0}FHAGo{}`(Kc>B0Iciua_#Xe$_{&u4IaKbOmxY_Ttc>Q%q#E~E_n#Ax`N#4wUDCBykyAf-Dw-qQREALX>{AhADo zGJc<;mUcJtsaAl)pwi6SP%mo`ngah21$C-osgMNPkqe?r#QO2IWXg#0K2@vl<>c2f znNhSU76uyA4SDF8D%7lIy!9TR^|oN}Es+eZiqJ(*_s|jj=0}6RFIhd?hlJ0`QJ|?n zbpa`KsI(J?zcT^~my;#Bj+81J?4zJ2mfGdChPeO7(_2SH{e9oVz5(eNx?u)JItP%J z7-Eo;4y8+4x>00cXohZ7I))ZdN~OD7LO_u25(067p0m&1`+PWW zp`R(Vh%X0mY~vwsHggV7`yz7gmiM6)YAVJbj9Zo_Nxk>c?q*eb%0RD97A2{!k`7>Tf2UxWNCXZiP5+^HF zgZ8qb6(SWlcSmy!w(A4w7+i)Qx`ZxT@ChmzJ3gEk*$(+0Cm<_W(fk^xCmHPFZk}L5 z44pQxs)tFDVbA6``X>6<%P5hRD)yF|7RE($bXs4=!7>(lro}w!CBDTp^bgBOtghH! zl{J@Yn+8y4Du27^D?GA6YvShoNfrGK7o23#Rg>cwl;7{+fLyi;hb(zMT6meDS}**% zE;MrgxTui1ZxLZbkt|r~7E0!5({j+arWs+ZrJQa&Qht5=gmc-yi0e7%6g$E4HLx)4?N=oIa8N1gWb09cnMFJ6vv0$&gc zH#UKSMChv+Nq2%3n0)c9s7O_;BnV3Zvx8()(-Pw3KaLLgouCJo-oRjMf8xdIMGec} z#f;++B!4&TjA`J7+7#A-{gddoFlbnrwl|G$$bhi7Jy`-yk~ZyMfhsVHcEH`253wN@U~bhGC?s%t5g>=a5rxCTVAJkCy&>ZR73nJ(C3#GL%cV_6_L@=O1B1oehv zFCnAjCttkJ!JC)f#SBArZvH=&QkCID=!Llq&=rLQ2-SkM2PH?dzrZ#Q0^TA?AUr=qyh)J)znVu-CZ?BG?rTHuLBbndffUxA=->p*5?q50 z*0q53OJ^6-iG*NI)F(zD_VpME>z&D+gm`ZXnjW5f`lM*3AzQGDGFFUIi9N3OOIkhB5$$WFd*I?)PPdaW?@n!FG0Qw!%iL`$*#`Qb|7 zXFIN)pUt5cj?8~$QqO@s zaEeX{)sv!Q&+;kb+@jc{hN%WZx*_yYj`;M?C=^*HnaAIBaX>zh!n;q7WJZt>RT$8U z5I}L>$fl}yT+Vcj`A3(tGi2MJC#|XldI4IouaoiSU;l`#DNfS!cqOep^F5u!*a_46 zT+#$j&;Ct1BK{Z_cXOsz^`Go%?v5X)D7p#Yfax_$>_xxdMz+syVG18TSQE7tcgRc& zS{ucEUK}cFR%vRQ%yqz0CrRLr*X-FA3iw?tB*%OTA2y$@=pZB*NCD*=eq7+VP~a!C zvT^HQykJS1bV<61Z%%5B1l+ZRkBKUEV>q-#_tw37OF|%D)yl( zQ36uf1NWDy*$K&OmLI9I^FgO*q02xK&+HRr%1GjiZz;JSPn0IF3+sF#4}zfog3L1T z=|1CkMT{t})eIdHR~Sycbz`Luou<`IDq{4IbG?7XQ-dXZUYTM|A{_!le}a+KeE&OP z4_=;WdMgnG!=B*X+hlz zYiL+=eY6*UZu^nfFMQtDS46VnYz&a z?q!@(vM-V44Ed+K;H6;op1F!R)Td&Tcxsc4@=9fmj)~G5V-qqflTARy-cwJC_gF=0PPwp|a&<454<$;^67RNsQCQKC-~vF^1u zS|a3zz{~WY+fiE^mH%B`?&Xj4_=-)$JFj#n>K>yWEs>#g7bj9RK^|8F?b2Z$3nR|6 zi*xO4zl#hPrA^-W8i0RwEABpa{tIwZ+EL}kEq98pzWBgyLh`6QDi=(#R+R^#Uh)WC zq)A3W0$8j|536M2YhVNe9iWhFd|8jC3v{ z`S6SOiyDqqZig;tx(n4_jOz10F9q4%QeAj-v6&;4T4c>EhBp7^G6}6BA?L(5r!#7d z+H5v*49>JzwZ8z=*-|?~YxOW^9%v3JQ6_SM=oxFx2$#@Vc5LBgup_)*-kxV&`%!!O z`18y5;;+w1&wFd@KiBkoF*UYe-*NF3*~N4AO3{M3V_V@YjJtq!PK$!^ z6Y3fvT-~OG7Sy~fjCauKG-g0C`Gwr4DKn6e*i!?ga%v5mxRn0WV6&b^M*}j>EZ7hf zq9#i)AOkh86VJ=?|29E(NcE(#2aE}k^DU^Trl^2)J=4&bS|Hu_6`|tC5s%GS9n~m@;Jvj5@T}>-MvVTuVb*@>vid*sF%Z1hM;8mT0qD;QX`2Sw+k;l(Ru0*_>nNzIV1W6I4ZnQqb_ybhOV zJvf|PeNVk8vRY$TY$A^U9_)~Q5kG~F&9B|fV)7?r(5h^<%>cx>&59=+x^6edN@+b$ ztW;M3fBIT1#HK-W1i}{-;bGM;I`w5>)2hxhit56Eh?ix3jH~JWoYJqh53*)Etg3$A zp975A1t^w?oxuO*y8t+# zR&~XG;7Tp0UhDS;d-!fu5^0e}Z zAw>i6pI3!to)_xKoldRTg2O(?#l?*KOjfgwo`Th;)NW_h^(q6OQ?N0*D%IHm(gjlk zhovuKo>N2Kr5A1?Y~Mx@T^Qw)oJ;z#W!zkqxt7jWtdudO z1!bj#NvK8r%~WI!s$XG(InpI-r>6nhU39RK#7(R z&RMe0c^`8F7Y|2~y5M==0Ob>r!7en{_BK05e*PY5rtu3y>>K)D;fS#e*?~Zw2XB@M z0r)A1#)P;&vj;5fpv4+B0(T)!#sd#J;_Iv-0(85}{eR+G>yn3%b5N)~SBRWCQ~uNU zm<}Nm7OMX%$muaiP+d_bt%S|>%+BCfzqK+htmL#}LPII^r~)`4nI#f?E%C&9nQm=Y zq`oazWpCd2(2mE@t>tg(PPgdkxM0i_O3qvJv*Tos4cmRm^cfIjr>YDdw<84ck zPplw@htL8Dart5SRcPGmvqjlY!o8wob&n}_u&Tmbe93k+iVZT*v+64qE27GRHGqpa zMV;nueVzESENJ-tRkN~fa8O`q%C8km^axL6;|mP2=dlK_) zhE6GCbsavz*G=f&Ewl{T&Uxl5LlLS2 z`Y4`5K?B>(cS|xd17Grb_oy>Je98Y%HKaR;l-%@D+1sApT z7wOl}k(tqf7phyjevxr56cmx3-1@WIJV*NerUvS!WBsle@2w5G#hh|Ru ze8$4guFhyj;)0wBJ00Jdagu@x;Qq-%$>fclz#D_r47@d_`PL4Y$xm~scK(&Z=-$io zuS#rHiruL_4yaIHS-c{DiY`WQHMop8( z?9dNIl0Tjs5)H^8SK|IO=XMyLoV?Wz#3WRy*9 z`?i>KZ#HQYVv0c0W*Lp(#d=a;6XfpH?jAzEGcqqJ@-0vWN{CNcc3-3fzH4y?S4Kl)RT@c$;1B zzYQgpiD9E6TJdccQn(+;Aw6JUUA|`4cG}bOg|FV>wnX9cJGCz*hEgPmuRqps_EcOI z{t0kE48vD@!3&m2b*9is0Jt?oHw{+OV5=En>1f><#G-$uneJ#dYcFdcAN5u%OO#+2 z)t7d}wa}~Hv^;Y-9jp|-;;_%=E2W7wZ?#H1Fwr(mDVppL?s1-~SQ#6vGv4^w#$UB- z3l48nTb2}MJnvet=ifAr$f`BPe>NtsKcrR3W(_t-9h=!>(A)(GZCgDysV}(6k~U?O z;$nn~gt2uRWB7jAI8eU~F&u-(3C61_<_4W|_!P^%R+E_|u`k(bd`*6CK|%;mC4gH` z(X5-0aXS-CDVul&(COxtWgO~<3R+`JYZ`LWytp4xUj8g_CmK8SbHE(7s!RvqV!$rx zH?mmF;kkDdp>ttI#QqM{P6H9wJ87XjdMt{> ztZ1Bj$-WDceKdbSC_iWwRn?=rX^Lr*En;KOGEw*}9-@55Wu{RX$f>UAxytofH7EU( z<}QY-&wAKVt>$BCLo!OJm&fZO{YSC(i$VFHnU+PBoGzBwQV}&y!uUX~jJYkw%rmV6 zpoXgfYepD=c$X{}M7_zBHc6LutQmnDi-?I&Xzs$rYHKVAZtbM$@Pak9+H^mm=M;rBynb{Z1uQ#$CFybJNIaqhldzC3-jGxUe`03C^KK_5M0J_WZBoJrVQxc)H$U&T&23>odc>#k z_xOxmG%pka?!XVXtd_)ej8)SAe8NuuJ;eNoKsbKF6&KdQ2wer!7KTyeB^WSsgcvb0 zm=#pe&jc|FF46Um$N8!5I6-~UC9d7A%sg!0gQl=U7Tc}HqSQn}(vy4qKEL%WnoISJ zFw3v{+J8YS;hLUxHryl5`GwPw)I^^n!C5~c{Xm6G&El;jYbv$)40O{tK*+i=g)IpQ z?6sQBM~SweTxgS_veN-QQpTeaiE6}U= zl)nQ%b%(#7Ds{J;o@q2INBQ%XBT`aCWxkds85-3P%T7CQd3-32C7Tji+L;&Ccs}f^ z%(Ow-9AO;mB|ULx>K+)P?VF+xjhTw{DX|!!{xjv>gPlvSj8}QxRc*;)d#_xT zied?}tvOH8)#H^_N@;Y^5mWz@01iOAk`;FBt>#m$0xY@nVHDP;P4JhXm=u2^=l$Ag z55l{HvNPR7F?cp(7ge*drGf5@x*8tT7V{&Etq{pY&XMy?=>D>TysHm@8(B6x;m$fr z{pmJM?Kl#mJ_LIl@`ox?=5?1aYrq-m-SvE_5U?^EF!_74%zT4-?bLnMh+9D3D+Q=8 zdGn|$r67V0xCO1s4@v0Rny-0|RIgyzC!JsYtU>AVRC!*_#o3{9&+_O$S6n7O<&Ek$ zRC(9N*uBNFP+X`zkDK?NQ&DXHm26`o@S}~48_li%P>p3Q(M3tB<(hlR(#^nCMILX$ z4&1dGp<^oD-8{+q4WwJm`{m~~K~ETw=iWEbxWl=>?RN-#dUE$}R6_N(>qEKC$Od5i zY09j{!k>LtKXwjEM4zM*5JQh312&+YYuusyuS}d-64+~7iHccAxs@1ep`GlEnA5rH zUhtM4x16bjDZ|E}y!x^A{ZO(X1j)56B@ zWwW>diFLB;W6-P%xJu7b@^GS#5ioY!!IughJF123;N?w{Wu1Ulen2#}>fZtq+}dge zsY3{rID1Hl;S()wA(71@HrN!!b1e%;W_s4h)SQU`JMEP!mho4D8nNd#Q|s2FR*7vD z?m~|Nr}lufbab}{&i*#aEDOs2&9kv6cq%SM1beo|F+uVS-soGF@qJ3&^lP@tN`;9s z&*OsE#@b}>8&SqJ2wgJaBf5p={xeW|b40E0)w4|v;MuZI9DpMksAs>K22D+k4P@(g z^Reh+iaHo@hB!*KCIuH2vkJM6KQ`Ac0v%FQEvz$$IH(>kqVG&nIg2Vs6D^V&!X#o&W$_ZitM!v1xqjFwhh?IZg1Lq zC#9AwxK?(adR-w-Nzxuc6vp;YAAps+6Nw1~Z2c;ee?j^9jRh{w(JZ%jH#TyE7fyH=JHB>8%LY5%$>lC@-1|ZyM?l26y!j|;<753qup@z<+A%D zq`8PAseHC~80R}XudJ~UB5KNPQW2c?&k_T9|cD@qu&X~$A9$=4qk zm24*a*KG){uPLLiiLkW?AC9FIv2<3UFw;4s&W|_$dbg-8Uw{n$zK_2*7UB(G#w8C&1AQk~ZLxB9j@;!RxS(IIG{HSPa znvb04)pqJ68Fji}q@vNAGA*e=hrCneIXmt=ltyMyD$54%f)@!A=!iUAY2Hkc40d5? zDlx1x4E$-m%)Lq33=4KcKifWxsY0GVhR*=(Qovu@J7-5oNidjYdtj?qo-2+o%gf<|Pm<{sQ za?349KA*Y%oZX{P%yuFsMRR(n{#~zcSsx3~Gk)$iDDZcYh4$MoSl`g+Z9JCgLqc8; z0VM+azk18!C7*DE!M?O8QTR4px1jdWUJjkD{pjR@T1#ILgES>FF}Nhk9+jD>*Wc zMI0Es;wCdh0d%F=-THM?9m5@p>*zTV*KNDKA8yYPRb0->cm2;*_qk*=9Ja)^jF6BW z{Wj*CZc`a5Gu%a4!o43od|hnM2b7MQH|(ob!#vYiC`<{G0;e-aBW5VV=Si=Zboa|z zkQG!^^yTX&b?N21b!5)|hqlxvGoE~F-QW^_WKM>znpD!q4M-q$(uoAZ(~U650S}1~F$9J_)=b9) z;;RqU4SNX8WeGuJcqlJS3Dj;!(LQ)O{dY-1UKf)Y-?MUd>Y?T=JM2~=uYaf0S>Zou z6}~>hwY*a51NU{Hk6I=7RDq{UJT%DQt4_&9MVa4B%r{8VOoY$(VkN^dInC>AdhD{L~C3iWod%O4juSe3!{ ze(b1ZrpZNAu}m_=;dD^7e5`($9=?AO zv;bG=u^pI)F}YPZD&vXKm!x50=xYCSrB5PlMkHUdBcFf_tNVEZd=j6NIp#F`4fC}J ze0?dS2y793UR`j@H_PIb37xhDdETE?=uZhXKKK?If_LsvV?=%YjWEKut5Z3{L3= z`x3YUZi6iag+)vovPE_09=8lIiM_vVppjLi-$@)8~}WtX12WATOfw(16z?>`lN z2&NVp(DM1tU0N_MnpZ;?E>y}iWL_{SSal5QlYc6VV6o8n3Bh%99vv1Q?ES1-_aPf- zf1Q|%>AKY~!X?!Z+!RS>cj^NGlhunX>tKKcDV}C2@TBhlumBr_UE%=kR{$X3bg1dz z+Hh`IQ<@oiP>=cu>}e#h%BmWO1PmT~&qa~v4#kTFxj7z|Vh~%SDU=dBf(ondGZmzxdz1Kd-%(&VE11 zafPHa^Zy1oe;*w22{amGZNQ>MproVt`T+li3;FY8EQRz?oC9IN-UaY>x9i)p0s|u^ zMuf7=5@2nIO+Ez(SladnJ%|Cn@>~iGhe#c5x>%tRPmS*8GZQbwfQtS`TYFk`t$(vE zSnR57AR4gw{v6ux8J|8wHg(c<$JYTeQ4fFRV%wHgJ1y@?_Cdzhk%Fuo5w-KIhQ_+Q zK!aCX`P_Js|Gnu1{IoA;>fpv7f#o{P<4W(~%Dj8=04msB(FZn{dSo<_Il8AfDRvT} zmtjCq!xI8-yZkjUMer$R6twI$xiJf0BDPI72{d^`oi)k&oSKEru*H@FHj@01Lj|Lw zXv;tnkQIYAPyq-cdypk2qn&!lQ|-CRU7%*$^DpZH3DRZ9VESKJTgc6+PYM-zEk*vy z68bqn{Gc}IA6)E*v_>zCSi84!iEVs{Ryd`vi;F*3oPF0IjF5u3yMciOjBb$37YifC zgpJb`yojJao~1)>21J?t814lRc7x*q@SJIg4qu!e(KjOQWM{NV!DmoU-cEkr9$MO^ zk~UbWWrEnpG1)O&+R<|d{GPP2M97ZD$rF1$F}RZ4)ONznMWrRI7(ib+Gv;;PWm;@9 ztWYjW!EHL`wf9nlGQC567xHwK@gb!sN*^GOFz>ptBBpFjl=<~W{mBNdj`vsj-E_$- z+4!6ilwe&tX`Xy56yg36k`nPdK9Gq@de12~UH>Si?|7YqrfiIV^1NBBX@&WC>gREQ zSbf?spBiu-rELlBysPE8O9B{C|0an~n3X&7In+^*1eCuUadKe4Ou0(3)^w#(;4=pn zw$isclLVVz0iwHx^6XL^+CxT^E#hB3puM%iu5CSuKMFl^PY4qD?x1cMP=gXER;vSs zEziDTwCED_pD?EnKA)t7glKP7foJ8Hc=+<&f^2Ni-bHT2O1LGAMRnIsw~f=-+X;LE zRs~FrmjBMPyocu}vO&m#xO+rumw%_nEGvFIMTAW47gP+F?XG$XC%Gzml+%&OQDAVe zAt5I8=ObfNIi(U-U*E7>@jXeYBxjX&f}(uXD-i`iJ}dM1bb-+8f^aG`zcE6nmAUjL zsdjcoao@6q!6J?T(19OFQm{GkbY`<9J(Z*pvCy|6w(g)2$bAAR8!L@vlVA^L;1mH0emNl2UhnqDnKjy3nsN z%Bxi81mOj6))o1Oz{96Koui#1Mgb?3YeNz>iy%AH#{OOnAt8OC*CR)Ok;GRwV2k>j zWl{yy&pwMV@D494p(}C$M>EH5 zIn@zj6VSc8jI;MH-W6-i5g+~fyqn$EOp(P~N>zbRiz`wacw z8hpS4tLBFWwegMm8nGo+gl0nc#$KmoA-Nh{@y2)L7Z6yK$`3m0^Fhs!?wFWoNW@a}~d#_&Tr{4sM4B=Zwmn1e7#9CyHoqLvpuu5!7jJqn_A9oAE5lx6vWe|}$kv>JSyTJ1A(lh(U%Jan z(o;0Iy%)!n(kv`2=e@%>lZ(AiU_xSRy4L$u@n5xh@LBGp0S!UXh`UaHsWGKz?eIMdcQfE{n zPk47ggmc6WHTVMs`4xBe>^E#en3dTh8e~5Oc~J4ml=s_tZgu|w&!7GA#zWO_Ynz{t zD1QA+tS><|6hI?&tCBq|twdZhT`LFYLI0zpRKcYF4U zBYNBCQ8%rE8F>&0g^b=Y1*>)-g$A%!yfdO9+()&gu_kMquERZz( zrnkCE;bbUJm`Qf*2bhO}_U z9*2iKDZh8pk%Oj;w^+lI$UFEp!al528;SP6r6v`YmGN#|N8>|EtOv7QtorH|2%;Tp zsNjz!pzLT8VWom7^1R@1^1V1>ipY|=J^8^k$nBx=I@%HGDo7*m)(5L(QWShB@`}XK z=%X&~nptr&Ds6J2Q~`hKa;wXQ$F<&{DlSR3o(V~8<68I2<2Y<^;tRAqF`L$${y>3s0Uu_Tr5#+Mth2aIWeo| z3TZ5X?TX1=^6tci4gprQ0G}Ki^Pjsd7=Dytn^QZ3ZO13>box|6SXA`&n&Cbg!S;dN z(13cPHukcjJ*}y-7#flh!-D->1(Tinr2ZE)_h!cUg+&s6d27;z{Jx8AVXpQ~Uhh}jNJeAYq+T#a~>@oSf( zcBfBvjeozew=KXPRw6QNq4=OlKwq>{EPi289z4(kY8-`tb&w>E%He#x?ZypgrPO!n zltYU8y^!o4bN~m2ZFia*)%J>&+L^Gnb2F49M2Y~_YZ61S=%rRBcg=&JeO=M|r;RA}&2+Mgz;;@m*mS>7C|FHzx zJtUgN8+K_WuYe&kCh@QT$XH~ODr847z4Z#{O~y?gFPE+|#)?O>`S7-!E9&ATU)Q*> zMV4vqN?=1?eP19u3&v{HFYeoNPnC1smxd z;=2^2wm=q46uxvF$K<~EL#(%r_qV9~-(UXx3Z*p#(&>4j0wR{?3*Xx4(CSM0n5Kks zUTf_$+=a?zq7bsPm$Vn`G|1tgZSzqxKW_rxX?e^mtB8VR;Bdi#sSHsiIOi_US6~o! zdfN$EC58Ul)F@+5C;nu_lZ;oNSUO)Q=A;$1o-}C4J`}qJu?-NuL!8)pxB7~c{y-$Fzg+rLP(SIlK+0>A+|Sdkj|qcH ze0zRu`62oI8RhyMWM`7si0$h&BoaM~%rph+@YFO?-t&hL-3J5RAT9}|VjPOr^Yk{I z88{WQz1!T6)dUAy(4vpd2U8_7J^zpPVXU*XFaE$ytAbS`D6pI#LI&i_tS+^guyhI3 zoX6Ey$Zb=42&NH2rvON04z!as|>Q;KxTARbL+O zKU@lU7pw@T)Z)ZB>l^?RgxL!49c$Ln&?8wm*+J}*fOpOD{yOZu2)R(Fpu)Ta=3TD8 zO({&;n`b5>_~|4liY~@1q3?*XT>4))8Qw7++u+~5j_dqBA4-Ng%>M?r15J{n_?mdb zSUrYMa z%{x5tTHcc3v`M+A*jdp135`T?-?ia-A^CxuvZXw zATDT-ub!J{m^p|{C_Et0QAkxFZUp5$7DX=$9lgu?{T6oK#v`7j;7ji_{8?2Ui+~1$ zOIzX%v@UV9kq;!iyYpNfA7|1fPf{td5{7^ApWbf06k3e5t^mjya~7xd^w9oRJf@1j#FnDfP(^9`q* zlS-dmzLhWIPFGK&9!YCbVs|2k`!2S9G#i)ysc~K;dE~bGW&V3{Szv_|*rSfuHK24v z@5{^8FEd0g&`nm}r%}{KsOLc!8fybDF^OVC_FzY_J{Pw)pQ&o7y505T@uTUqC&}wp z5jQ&3r9QjGf<(!0(c7J!4Bk`ULK4-EnIJ`7%g%wYh9touGpB5gg3%eIziK)Kl`!oK z2*$*m?8A4XK}DBu5auU!Jqe9lJ=u5MZZvR#dYc6{4)iU5lRYEGuNVCR*Wi|M&a=+i{ zdSacschTef{{k{bUU0U{v+CIT;Ij60`5~8J!9&%J04t>(`o6;QKyh;q1HC5Z7<`qKXROa~>%J?4OAe#`ER3>#JJBj9ju#kt_G_&~zJru}d1@KiY(`%vY(-)5huj?# zY_5z`AOHca4ZQ`j+it&Z9X<}z^NUxk-wMMOFg8J)k?1D~Hqz>ZSOs-gVu5K#?x=Cm z)TNiQsP`RMnS$UCe9}gJt^BpRg|E(_?(ciS+@z zfV#iz*OwuDYKd5g^&Z0OZ@i@lc4=qxieX>7p?dVP))$Fd@>rJQ>aZ7+>3#yYG#p6^ zEuU*Gm3O(m!M*pH_g|41%?0<)t~(PKgQruqHf(|6R$9OTr*xWcEMwZI9T+*Tl zD$SsHK|RT-Vn5P93xY=HqGPd;X+39!gjWF~KWyfD(8O}dl~^xqGFIBa&^VHvAeR)o zkqMWyh);h4)-G!zP-*U>E3yMvC(F{`4&85m2;Mu$-QwLJ*5tFRM~s^$Zs7Bq${fB@ zEtfXj@OZxR{@(@GmYa{gsLGOCpNOSSLnnC7nt-BklIh*w!(p;fx}b&JxiQ@TbR}UE zg>AdGXyyU%yC0pWMC+g-FtsX;Hsm9aWCnLc}qPL{GYi6Q~j*V+Dn7;Wiuj|Y_F zEK7PI_{gLEoRzI^Oqme|4+aE&jq|As80ucNJXFUACpF2pfg3wWCBuGZyn6ug*Zt9j z(F)CvQ!9<=?;t%t@e>T(FY8iBgKuAOrAO_b=2xEI34UbTPkOlD&tUN-b0yer%}9sv zhembbr!QY#U+hu?rB2kw;b8*V94-WCdromLdMeV3u2#~+KbJqn@2(k3ZPI%U)vA$v zcR01xCcA&Bg&Vfy+ZdPznxDnIC(sa0R>HF#Jp0iGn<)=}x zG$oh;5|2DPPIqZU{Aa9@=|u9K*x z;8&>Wj64n!v5P-cL03-z>EOaIQ=mCYubyQe(k7+P9}^Ynwzq?PIPIEytrS)m)&CNA zHvw~q+;|hJJfyf3V3#pT(sjHwl5-S#)jE_WG}vAEt%faPJ1M>HiB#Z^9{Bn~oXvYId1G;n4)V2}-`0sb39m-n%+8cr6^(zmBH1Fyz9L^cmcz zCIF*bYAKFEdzh405mJVyF5~^tNOF_X6%2#Y6B1hOS_?-Plwfp$2|zfb+hWg#z%QhDqEJmHOXMkaO+I z+y2H2)+0@N+P{eRZR?tRE*=E0_@QI=MTuAv8}i1rUZsNgB!SQB8*B$JCOGce)$*m2 zRd-@p5VC7uvXfsX)f)dRQ3Rd;u!LVA z5!P{gI^@CR?(V)!&Zud!;QBwLfd*FVt0Wj)(7UCPw@x9*AuK>(llF? zxt=;PwenHVltnLpD${p+^wXicTwa0)9d4h~@aa{J6g#1Z-}Ye3!?4J&CmM)iZPC~- z%=jG+&K_R8iGfn+Cn*R;Ghd#q6~NBZE^zdFg%Z#NIF|y^eprYx8jKIE;V2Y4s~!tE z?T92XUGkNjF6TWMvz-GMv41yXllw6$1v!&vB~4r@{j0&wQ|;GLSRtzozNTATlx??b zIB;Z({nIy4M!+gd33nM57f%D3v4imGd8c6~x@6Htt<1}3w$k^_D-k}14~# z)vMC1szv}}`a%k%*LuL>I`oqB^(m*3kx+K=G(5#nrAL8hxgBGz6FMA@ZTkWf84VMtG{;bnPo_HntqD3loLUxX>#55-~ zdTb#ut-m0K?|LnnM`T|Bf{x)S_<33M1bKlj*Kr<#RljaNkZq&oEpja3mP*E-oZeynKb<`yAwin@n7$d@=t*GR^ervXwTs%oYtS=U~Xq+ku5oGO#CF-%p}ZeQMc>9gB{6bL{T&lDYW&T^nkem8yb|3G zc8vTp;#o1!W{s&|vs<5e!0nG7sel+aYDb(A)ASKjMsv)kd~*EhQxAzgmSUDpZA(VE zQ9|&-CR5C`2)?k`SsKM28lx&~D`38DLi(Y{C{q4p*NRB-m_GCPL%oM-6qkm$f!3^uq9H);Jz?iS)@BiVyD8yn}Xc>%!KL&T#ZA347UyK^k&=b#d-^&lTXA#dQjII+;CwOKV|Iht4-s{bOH~jvx#=Ovc^sGS4 zIp4u=c7Z&q_zeyJUy=yE=bmfx;evA&lOO)~R7nF+?5#hVnaSKivyMu9Yasi~bW+~G z`td@hr!W-rDYI1yg|O;=xk@#p@oFU??SAV0kH@9*dLfBtype%$wUo$H))pWDCx zZb`bE19hn>Ac31-8dUt1ZRJ?2(kSAqG$?221MA`+jtC1vFeGC5a(xU-YMaH#6E%}5 zIsoE{SlXnizPvRr31R&n*GYn0-t@Aj7i$lC9>m3b)&v=iCv@=6*kg)SCr7uDXnj8LTgeC0?K8ek5`av>FRr5pavu06$?y)EI9WYmnRCPz z9Yv)0L_C?t%xmKbz$jsfPe)D84QClnJa&#xnI?q}UnvUKaqvW*q9&)GL3InMd!J<% zjW9aQ#4FA7%i!(#7?r~^mep1&9^%2Y4vW!Xn>RM%7RI3NRvZ7ZBrzGku^-u3#CC8a z!G!EOnw3Mu<(a|QqpA2$@6Jvoh$zxc0o_+CT&~_9K8cZfqZM-n-9}jgujYSM@ajXt zFn(_A6FMx;Y1Q8#R=yyLEj*MG4l#TMXm*Qz`AkleRlS+_ZU5e@@E#y+u9r})>i@Wa zXSU-TLMw(~e7AsWs&`;x@iHVksUC&Wu|q*T=p(liuci6r8u1Jy{(RH1gd5Pup!g7R;QD(V-K zuW{Qq1QOfCxKM9o0uKGZn?W!r_ewdbjO0W^rFeZDV%=wGr5bPq51;+k$uVUpM)%#? zpX@H5^T*>2Uy11{=D2|hXZzh4-uE>%>a7;+)K+!jG64cUvf3xwv@?a2%KKP!mmCSE zpJ?$TSM!fI99`+vTbuI8dwPK=!L0RehO!w<%RhU(KPzD1OO!36XQkkFUFpG%V8UMr zjARsxX29Ljo`(R-$s!T`J-}6C3QRv$L7dWgb$fVjSFfj3QETCzWSINXu-@aceLu<`i0aQJo;qTVD>m7P5J z(lR4<`3@Zn!fOeP3T;DNfEA90zQoV0nj@DMY6wY zJ+;rgM-5P_`R`zZ7l&e};)q7c(JYo20iJ1<`22fW;=Z}}?AUeJhpM)kxb%8v8ae4v zvm$~Jrzz4Q(r9~dDR-pTvbKMNa!PewS4Xbg@mGmQ$*daM-@Am=;>BoCtCTk3@=36b zjjDo}ccB<<0k~I5zh^^Uo`2{)ysZ1e-IRdQCccO?PyCt?uBW@q#mZ>9MUh~p_ZGb7 z?y#pUp=Zx?144uiHe4)dCa#}+`_cHuutBPTGuma}?AjDnSPlB0ayf}8WQ#Z#T{Fl?ZqTtL7>6|^6+cS*zWrJsnxaOw zA!LuKSiI9()%e|GaF02lSn$9*gvp0lQGEE`YG1t2>+}+w=YyPuni0mh z3bG?xRZ$R?Z*vFUhiQSH1sY!gohJp$X$$$^bVrdT>}l1Lo+CN-*T3!T#=n^2?*W@6 zS*ynL{QlyvF?~3hLRRyba|-Ibp}>g`mU*LOAJqPigPR%_)Hp1J#FP9ohka}zMU!;? zS6RI=9vCoi-^N%9-*ITaQS9Jr;XD67Y`I0ciVye~R{N>-j%vc{frHT1aawEI^Zxh2 zTtoj62vTXEpB>u&V!m+$TGyN}I1*#W}`NCxfeZM&X#rFNcH(fD}P5c%`9gxuN$GD0hw0-i(J&K3??m5LzSCEG5?Ow(q<{T zAo492syq{w1t-e^M|#Rg+NGzlSurPcwLAqh9};A5<|OBr-|arf7Hpyt1(;xGOm?dB9=$m+&+>JUjrO+>4J48=%i>Plg#T;kF} z0oA8bR71_&GJ^}IKWtWVMMOPcPgkfYca>LTK^TCkyNpGfK|nFK%CY~$Cp)oznV{}Y zN)&su+FvwJXKnF@0db`HC1k&V1=u>aAgFLeEkM{DziG6+~Y@k`mi>ayEGKg%smee-YB5J{_Y z?lZq9vtAKOHS;ay_r^=e4E}r#e{SX=tx5v&>7eAsgyabAzDbM%lSZKpyy3~p^oA*F zZ?5t2sD{Yv>J`38JFV@;TO?=cS!`D%`5GW$PYUov_W4zB z+gxRrZ8W_goAa~UQ;Y_o4Tiv?Nl=S1ri2X z9-lU*(q7+sQx&trf>3)Q6AczZ3H3$rRGZ6T?C_z;@o-;qmr2biW)ETd3AAkP{_B^e zkE76cgOs9Y@Y)KqTl2-#HQiGJZ;->oIe0$BhEzVSQksf@ZQcgZnkgX?tX3q@OdEoB zc>iUmj=-)2n-6nCST<62T1xEY+|@3ayg8B7!=E4Io@XUxkY}ae9>Te`mDb&*DPOcl zDIp5+wPULGJamaW_w=-@b3B6l(-WTv_eY;8+6ku!XgUbbj6d4wq1g*n5;^-xDxC6+ zQjyW_93L-o7@Kq{*5sI0E2VU>2Qu03p&kpu2T4KFAsA}B#b`QdJ83t% zwLEBz0lBv>S$r~>DP)R^YTIgEF@I-U!=^8{IF?u3TeMrYrgJ4+oH|x<$@N7<6b--X zO8@c2jJ{d_esD}|dGI0oKb`A|;6LUE|6VJ6HY3#oAOL6#+9O7aJ*7iY$L2hyfAS!| z5Wd?p7;ubN&?(6%2s%Qn?^TKa#%=AQJVod$_a{~L6;dM(En0q1&ZzWv@Tta+VJ#GL z^xMyDy3c*2J+oI(mEv2^lG5}~tpDsm!Wao-NRu$|Pb0*7y=+OrzJt4G4^}i1`af9F$i51+73_CGFQODG6!21N`{@w({#w-q0`!uE}y(j$n6 zuL?w1Qvl(c;*WwSUcU@p+dlcl9guHYEr8E_mZG+; ze=pK5m*A7KyWL~u&UH@1a8LFX;~|mNx2rWkw#-;|f|IM6mOmgLDE4G$>HIs{yuGCi z(Z?qCglbj-)<8rtg&jlb1d+8th7!zmD7xhB(C5yNr*usCXr){Rm*OM!$HdiyfsFgr z5v;2yFoiymuan%n({wiKX!W;7RON(?P24}89;&>)k)q(gEwdz0W{#ow!;X?O}eI!z9R`q9?~Jp>Aba9G@hbFQ0`LoUFK}n zsvK0*ZS28~Me`ATQ9l5a{nMTjZMBp+8uWL8nqRr+mR75+wnbztCjj_OTf2XJE7hA%QkXS4K&IPoSV@sWb&H1)#5&qPD?0I#uFc z_R|__#2vqN(ypHRUQB`=K>;d2YA2+L51X{AOSu}MCLx8#N?*0WpW>9zL&d%8fdSBO znz=klZKMRS-r+2~XLCu3Qh#6WOvLkexrw1Ts(WynnRx8Au02s}X(%K)C2AhRPNl}}&+9+P z6%PY>;bvB56v zMnIJs-;Y3*l+?K=Ro5<9X*T$z|R{dMk{_doj9jwDFoo< zoL6CUFRLcO9h55kQS)Z6)b>1V@;A1SY2lkshZZDGjFmYb7N}eP_u;+zpVOftRN(nv*F*ex=vAH)7j1Eg=Oj zKE{}$#IHI<*UV6#Bv*A?SQ?`6cXc8X>^Az2&)cfHav~oOO~UL@!9MxuZ;Ub534xCQ zjMa>fpJL_J76yu@o+Dfsj=JKqM&vKMnxUy;%Qp_E1EgZ5I@Zr#T>RdjTr;G@w}LL`pq2f`=~8H-=pxqG3u4 zrM$(g1!$`JEwbOauK9OyZ7O~Yt)FAjEzk*bjp8CvjBS26rGI2!P1fmHZ4zenICoF< z|G0n!tHrlg&yPotL)uRHE$=tMjvZo?O;_=BC`U8)Ux{=1Pvt9y|9k~Ta>T1h9b|}! zB6}HRMD4Z>x$wLBsYY|c^tL&jCodooSZ@G~RiCVy`95q7)fmD4D)6`Src=km;t&*{ z9cD-o4VlcOQVL}#vpz!f3}UEywPWC^Xt34rRERkI^x2(p>PIx5u=7zmx)y6gWjJw5_duRY8j&UIWHwGfb!LFzGUmtYX45jiJDz5I6@02P`#z=-ff6%=vs= z={#I{2j^NP_#v5l_R~)zmz6$rs_7+$dquCS4bH0`>_Q8l3lK!^X}x-RVz%fgTRqhL zn}OBG+b7I`vN|)JNOpay8l++E>A;IoTbX#jzCqU;WERm=JD;IQNP!E#8u*PsV>jRl zuUz2zLs+`-&)NI@08k*K3A%2JzPLR->LJM|aj#D+@9FMw{kEfbu%x!3uvs#CZhG}S zUir-~iv{cOXGLw0jh;+?VD(@Rj7v2ho~o}=XL;c9`qOmgwtAW2s4=;kJ1M(skcV{O zv(e;G2n)4abyB{mG(>VcobxW3&js2apZrdDG#99H1C9#TD$w|MdJ+G;DMhrwm!r2s zOMiX_+svAOd9>Rf-D6nUC&HC^hLOs2XJ~;td0NStI=_80JH+%@S7;U#@7vvJSO^0J ztn@UQj8+sSYl3b|)##9iVM6bYt;(lfhhN%2f>g;N^EwxUO@;@)rtWe^Bx7ZTSt zcLi+n5A9z`5<9g?UbD>P!T2I#^eXk@SnvwSsJnqg;5_W?PichZ8s<#D_7%d7KI~WS zJKE(`H)OLF-$&jZwvchy-czKP<@h`J&2U1^^t)qs=L7uK#@0DYDwI({+*)Z>99Ohf zH-;t}oNJf@!5C|*3Zhpj?o9^NCd!~VX0v5{N(>%Nm8^(=N(zM#fP*qw%+yc&6YRly z;@f*E8*vigaWox6a*Xb1O1bs{i+Yiwd){I@cC@*Q3%;bh?aaQmbR5s^p^5x=(5uG+ zCIi3RxL(*O<#fkMWgf~LA+R%{@=P9T7@CYVx;SCqeb0(4*7sOXG*H7<6kA&cd{qHFL5PA?dWw` zn+<>oIcQTw$B&We9=MP^DtNrAiEh=!Fylju7kprWu+!WdPBc`)kp9cSQ@*KpB1Cq>LxT7NWH)%>h`D&yx|gn#-7^mk)k$eQF+?JW`A*${JWX})f%h|!I+ zA>l$^Q2*W9ak>7@`Clc_{9h$7FlvQ^EZ`>}$>@-bqVF-um>t%Z!@%mBXFzP-z7%iy zQL;jiQ~)9*RiwW;)B@}p^8f~tfL6X#+hw0}>2@ZT_qD{=zLnT^#C;5T);Z5^^Jt6t zBL6@ZiJ`INK(e%m)XbnBH8!DMrKYBRZ-oo^$B=ppq_(X3^>t;f%47Qy3o=nB|3Tpw=ER~MPa=Y64SS!)7^RAPXaW!i zkxD*LGBa)Zs3TWldAaY* zd)^^G`H(Z9uc_gSXXx47Ov1XNns=fZ;s>zP!zaMXhhqY=A=bQ^oEq%bjMUV6kk2~{ zlLh)rK^D)uqk0P^C`4BpfA}eSu_>HS3)*A$szQy)=)tgADa@U-3h_$&2q~1$l(O5; z7J6Szqa47-x1gUAPItJb%bWYRVBbC?z3yXDH;ME2XRr5CZ~Sfmg8AJSK=fArVk zwdU10KRB8VAr}DWjZhU%IaYz{&?<2ti*tod(zF{=tpxZqm%=d02AnD6_W`6J9nf38 zk^N^IpunV&vH}fyD-P@mZ?Y*V^2nm9fPj5-yg1)2KW`eN{?BjKDJ)M&qm=5TRNvR&2l@R-4^(*3VER%G z`-(u}+$|c1)rcZK3~z|xUZg7q@=5NT;?7gnx%-b_t2fUo{ZAsp6!X5`aV=KT8@N&Y zqbs(3t+@mvq!O-m_?nh3;(wsw;-!h7O^02qD$WO_nj(9 zHRL52U^Ng@CVm}gzci0EChq91Bwt(1C{J)tyl6Zq1iR!Vg>Tl>lU(owRQ%ZwDPPtYwpM?BA39VC?#?= zx9qNX7Bd@N^q#6ROwQ5tm;IA>{_Cg+Ix*JUrgyq8|9{Y>j%7dEA7%E7HTIRVQ?8mG zI{b0Ap^RhyE_+7y;bb)is`Zc`h^Iic;sduJsnI@C;RDMS(;}ttsznO%&?sQpJk!yB z=>+Cc-7~Y+uF3Tn_J(b(jvyFlD@>%bO&~$}xtN8%T3^#4l_rxzb;ZVF^s=jRjm}Jfu1Mq`!<$S*PMuQ8eJA(&*mle!DpL|cZgva{Av??Q_Dx5L5Y3wHLiTqe< zSIc6yFSo3T8#*_EwieM>^|G$O(ca+YJ*<<`FWoe9_VX+HEPA$ti>HFVUrm#59(*9Z$M_2|`7}x;m%K#W zo{3MNvLpx(>n?IMY28O&fvq^6x0-6|Hht{h%r1W_E&Z-wk7Q4Ae-0kW4!VuNvP_a` ztWhHM>(ZdrN^8F!WoBGalfgM=aCWlx|DK8D#kc z-y>V8e?Q-?JU_ZKKA8p$p&>Xy8!m=KjlB38L%uO>BJZC;ZlH$>_YZ`%zDh|_4JD*r z+T3muyc;M?WvRAkUJ|i)a*<7@4mC4`_CKE5T6cn+fyE=oe4zJ;1G=)RH*6))%>x0` z@=`K|Zp2iIu5aF!K)nXiI%_EG09NHuWYjb1Dt>;j^y=r!@oopTLU2#j_yzKr!T?c- ze|6I*?2&`?quI@2c9vaHrjHip+>B;-y{~s}m%>5@*f^HA9 z|1Je@yu8~FdU<7E`aL%&rpF8F)D0D$!4WQzt~?S$lQ@Nw@PsP#)+JJWDS_EazmbJ%Glp>1>kc_f9mIaK=2@SQ0}XU z0iq8mhG6plm`^uu+Z9ju9`VVB@Q&Z861oiJr$deW=rF&rceS3bGwh*;k+mYL#q9;R zHHg99T=+L%mBlBJL{Y*NPz`R(*=CFDZ4;bh^FphY$ggNw-s?h_Zd{p8pEo2)ead#2x+Ssl?%7^s!j~;-igkL(m2>$O+y!=C6=tJ28-mto5deY5L#)V~0`{9l#lI_xSJB_%r*nJq;{FW=kyQ&pYzil4ok2wP zm4Sxk`lme}?Y-Z5gLWB&Q^XPWT7aKXes8)}SrdIGJK2bXK#63J&gV|L!|Q-B@Y_q} zEo>nul~^mF+dcV_Ef?g~#%b~~K7M4uWM}-z^cC=NvY6vVNYG|LgU7|HX?pa=|8W6d zyHqKNOJVu(bz-O6t{CIt<%I?V_RiEJ$>?lLuJkWY^`XaQrQBiNd0cL5ES73>x#mLy z%1QYSN{Fppk5Mem-#4DrQ_}8k_gZ$w>$lgb>O+zxFiHwfzaI>MP^cw=HM_9~@9u{q z?~YQ05n=U?7|ddrWNr36HJ0})zmhw%-*dx^7Wvf%ZugYV6neH!G8(lJtMk{u_Qn@}5r^ipI?W+m;`I9nF`bAwYb*z9~Isz>( z#+kB9{lb(oYa;#(ukHSdAL_~7*leLA*tx#s=Mt z>QA&J`b(a&=N{>2&aaHf|H8x*`^y|O_k(P45?+C6`lsSbIbDQA@_u)gCTKQ}AG5`& z3#`0KFg5n4D>VhbV+L&vAx@6{#+Ws~JJUk*-q#jqal!(FV(FWqVO zrY`!yQPOMj#GD5o11hfqWJePU03!8#${a9qmbeo#0m|B?sV zy!=T~*$978aAvW#Sf~anbIDH{$}<-gm*E6rN|dUY)z=&cN+ElIK6V{asfi>ZB+#bh zg1**AKkZU8)`5Vl?LR-f>P~wS;+j7K0((lHyalF(Gfv$9uQu5IzuEvDsZJ`;RcJo) zDRMN34J zg3-3+et@!2YN*3r;y#?S|ASU5!DgB_#sjt6p@qH86U{FoQzg$Bn7fKRH4abmv=~=n zgbKwgZ_YXXG;H1;s4%NSQctY2R+qMwFOVu0(6 zKrAcmf=q6WTKv`Lfyc2=;xm?I#D-dfFKmxu&JPFyrtU#5=Fr84c->>b1 zB679HB=#XAZ`~u#AK7cu7Lb)Z=)g}K0$BYi@7I7f(%^g&O(a2t*;-~aHI&!eAd5nw zvF|jHq2>+q@i=O?@xe+~uLc2o)qSK3bP6%(LF*JJtMi5E1>r=t&iX-_Hz+J^!q0@T zdM!UUz}nyrPj1y}tYlxGN=3DK(E(Yp1P@s^%c4f4={$!vC#EaNFd z!|tEUK+dAJq~J;HHMc&d{|6TozwmMAtro%m=*lE{I^NOj2d>C25%uS9@a7;fdL#+b zl@wd_x-I?{8jpIjsFcz)e=nZb!+|wQXcj_)o;V!vcD~JGZybgBvv&_CI%DTJt$58NxNMmZx7Tk^@P%+UpY5ENv_N_!It%-ho;JDwF(Y3|(gItrV6Lnz|sV$&J_!uD7 zrOJ1P%Krg3oeAlGcY2=QKf4cG^lEOS*fHM1$kZschy9`|^@KL93s&(*YsLM&k#fUQ zhl&3)n4~Wo<}8d@wST#Yj_tVXexUV9PP?>cV6SKMR|tMSuQnHcjde$y8kR8%FF?y| zLbj1U>Bb9x`+z&eHrTJaB zi&-(LXVOlx!LH}akc~rG=bt~m>!~LCR!sCvUlkhU392TnQN=4k{B{lQH|DZbmZISL zFTlw_mEEY9^7+KMBw58{AX5D`FpFs&<5o!Q;v2QPt;HmXcZX*#*SE$=%5SD4)dCjE z7nl*3(-lsj>W41_B>_I#%`zRH9KgPbn-Sg1?|+fL3g$3F%ROrc>#x0o#h9wxs<}>m z!7pWY-S6n}V0H<^)V%?5ibZCu0JJ6Np-L1uS~)r}(8FLwUDH3+tG((&OZ=xW5D&qv z$ItjWoF_@cuAAJ?Z|c@O6QQtNWH)NXt8u9BcXw(RrpAYhu&4>V*w9r%DrcKF>xjDRGi=3Sy17?$%d2~O#V1-^Cev@v0QqZ9XM##a-; zW9r$oY)|NLAv?p(;I}L*e~2r#m=--t2zkNx_NM3PKUKpBnMh`5O|jWr?sNKTLt$za za6?5%%Cd3tAPC62`4+EG7Fg;z+3Y2PE+>p66f~)j5fu-#h*vOhN!h;HJpCgOAh2UZ zkI+3Kh!uDY-26#~?g^eb)VJm{+SQdDNAeLRn?mg_>hBJTg8y6uU&j3`#`=BOUjOTC zC8|Z8yuIbZWZC{xLx?&UU(vU4T^YpdhK`eN*7TZ)IR?~P7s=7YX<8oJClH1}QOg+r zcgIu3JRmvr@2gKYDqh{I%3B)V4}G>oFtZfek}Jl!)RtDxT4f|n4KPAnxfi>~mXp5f zd*stxvQN$Rx(j*m92`|MK6=0WHP%5NBw)-jFJ13ER(edlnb)~>*htF%6G6pvN`g`q zwHrwSeu0FCfPo}=fiI^59m!pJn;CK@!j3}VS7Dpc;Rtjq^YYMv3?L_pIhh~)d6E6M z^5}kMKR(8<1X^jr8#Wda*mJ3zI;#Grjj{v1PUJg_%dcXAu;2mz)zTVwRWoiol1?WVEIsedrq|yBFqp$s8>w^g~!ofYu$o=318rB*HK4)c+yULn_>E!mKK;Rp^W|}{E&NYRH!=LCwEbW1k}j4`trHR zdfk4ETTzz?2DKLp9|mhRuDR|;g=~_j-ydqQrc$E5FB$}e6$ZEtfFkC)RQg%oNBoh& zhf?|0q>O%d<{bH84T?D=PjaWsw3NB(GTsI-Z#|xQkd&~@De-+9&wJO<&}81@v2gJq zm|MdB4>F@>F9i5D;Oy9@w|gg=rXh12b*1oe5%-qD*0|{aO<>&~TB%Tf@kk^??0k9M z*|+7Ds^)d`!$_POa99I#EIwNi4w!SFqyi^Bl)958v?);y3CCXf58Ew8pnsO_i~Qsa zxU?oD~%!gtCqEDu+qy^Rc3HAewSdty3;{Hh~b9#%eb?D3v}&qjx%Euy!g3%6y!vwS45qHH-b^LueLB`Y&w<5PurT1H0tnzjWwlG zqQ!{?8wWx2zY>0nX?=B>8gzu{@Td^4wOvshVc-D+kGJ-EC<43#cY-eWidXU-c?mF6 zNfP8H^0kJNWNS9fob(l5l%=vB%4qS)W@JGApD-ETo8sjB3t8{!-~=*6Rp6M@!CY4! zfbMwJwXUCSA%X9R-jal~j7hI(*Io=}!bKfHT*3VLlo6n6cyB%+^qq>4qLEe3NMU&? z8-k!oNZN)msXD2s>?88f?6nYSI*(^XgM<6i-}i7QXk}0Eepc^OoDUszU7HTB?RJQ- z0@ixm=Z{(qUc|0p7Du~bb{IX}8UV>?Ux)l|f`z6&`mrA$WLVt9a7vzLEQ znb6>6RA8&8l(yyZ%FdVoYc>n~!jB&s@JA^ic=*GK58VxB8bq2s_E&%a{7WXl&RiVp zJSVN}d=v*SaRfDty7``m)fDL;M?602GPqu798(%z9jW;s23w!SgE_B!jQmft2>+*9 z7!u#9kiI5mJ*@-?&p%lYmW>dSP@2@8}P1+npqrSAH3eNZ|a zZ7^y4Zn>{!F<7}8U65t_Ms_bwzOKua>E#8;Pv9|^({i}IJhBygadVy=yXeKj?Y4?7 z05LS4GN)-X>ZN6S^Md^00^9(Xb}m1k43Aii+z7X3nJ5}e(>8l;gp*EfP4Ce^?5LYv zZX?ti)^IOI###ezNzX@M0{~NCT?om87^2cOGb~UbtCsxq_&bOoIOPAh05-|eOWYbW z`jj1uutvk@-Sn+R-V1qJoQb}6%12Y6h4o{XC;}ic8@H8&6`?3k8FcTGn=HFrAE;IM70hQJ@VD58h%Oo_AL7bCDjdFcg=y?}M-qvBQ! z8T3+gU+ny2B+2IYjX#t*R*S_h|c9-ba>08c>3R5l| z5Bhr_PWb9TMJVhe=}z-#@X^)pOuSd8Cx@g;MMg==Izn&dhCp_zv(GAxlwu1M50v<5 zUVfe#<=V1e(cqDZn?P+%c12KJuCA>qPfT{Ngd-EhzrTvzPzl|oYDuoa> z9o!a@y2CFWpQmV;H9}-j>J3@aIkJpAXdq$y{aVFB2rkxT2ap@);Nyl_^MD=NIO#4A zcmd)QFf7~I_b;Wxk1q!=3f}CG(37dTdHK5Dx!a&Ol_w)!SqGymhZG5V%S{Wu0q}p0 zza1D0rO)E5Yh4{NU39MdyuQa~LRf%R!JxWEVG0(YKs&2)8*{>RDv@5xO!=FX*ccJz zE4}GK;MNE=iKc!F+mGW;XNScheS8C#naGj-p%&2!S@|`W9rGWHNL{mQDTB^qBqd$r zy5)9%6bs^4eE%`AT*oJ`1TZb=8%O#QT^^)r3xh_ zsoqottq1kQ_wb0l{DpcH@mgc%SoFX&YFs;vzVfGtrhCHQ$`QfA9TJ)EGOlkt*O^)K zicn><)p!_?3_mW|DNLXQIZKz!Bipq43Vrt@=|3B?)<0N6jQ<^ZC}S@Y!UCML)~cdE?^1n3Tg=Yso3H+NL*uWv2LVq94iLt)9 z8p>Xv(4fcbJZeXSQ=%jtQQEl3Aw>vk+1*Q@go6N6AR_PfAn4hNg)cyDV~(oEgoJtz z-dmX`mr%kUuhPHf6QT5#^J5)p?u%s&j~b*1iIn!Dw2gbHP}m*H)s4{scLcZ-i=8GRXp0p9itw>-l-IQiPl- zR#YnD3(Gt1$q=f5{>NrnD*4!^7~<#nu5PPZPTr?)VkQ52^~2N9zOd!2kELGk6Gm98 z^$7nc#1d(eX}q_D2md}{Z*L1C3Ma(NFibS25dBGJn1<_7&Q#4kZ`nsHeU(p(r1*yB za!#unvHK!1uA=3j$%}rhH4(;G)8c#eRJh#ZDO%ixwZf+9 zA$y;U_b{pVI zOPe~sAVKULKKySE?_-bBwGH@=E3i;4CTxP}TV3+lcc~Q$TduK;+qVbmw8$7ic{T(D z;aE@NtqHlGOq=xvWZ&l42lklw7&hZk;}NzYa*-8M zi)y|=|6PP`Ni8#vQNf8L6vb{GM7j8ec|R-O zeLZ@-lF-ZMZ~Du-p_fZ0vLm9oW`AynCliA_+`$4cDGl5L5>zVhDKU)>z# zsnnO!oul^!+L>JDoYu4uePY_4R@RRs{<#{>aZr{TQ~(Eu%gqi)pZ~=B{}l>-qBw1K zpL>62ez-YMR<0;8rLxIxz+bzK{Vn#rw`*-PQBMwbuH|Ee5pIb{JR$?LDJo4BM$=t9 zQ9tu+=lqVgwA8vE8NFY;IGYpsq~Uu(+|yUE_J?pF#{){;1?e-}0+4F@x6aeAY+q;y zeG!!WI=ncGR_rt-G;XD&pwxQKMdd#wjj?|?N1B`HcCDyj|3yvh+c@lj4 z%R2ba`rdw;BZxl5AK!IfwgvT*Dl3lE&yM_ccII+~<4xNlDL<9^Sjg|#5P1@n*uxA) zj@H+|6AUnTie&5aQVX@DJ_4jG@0LHZ#3Ed&qR5=%x-A{rKL6AT6ttf4Y+O>or!@*-0@t7jtqd3 zlFUCH-8e1Yci}Z~2v_xXJ9;qx2p=Q!4nOV*Li7GiTE04W+S^9+sDZHTUbZ(Ma6g=Q zh&Cn6A(7sY_Dt)c3+o9>(NJj1U(N~u+=MQy8`+^?7)q|K8O9mC0 zt)b3~{u+n}Ovc`f_5A};R7?qD4J9WjG1RE2D}a3@J?z^P6>Wfi4QI*~Q*wTL08^>* zSom2c!)7OfGv&fUdB5|%axgK|cI{oG3|;VGvZXgaph-&HlW@1gekCgYu_8QJSyfp zZ{1jy{Yi79FdQQzA_(xqOh`XdIQMav(NrF@^SWQA&KN`r1=!{6 zkJ)y0fEB%A`lOisQw3}N*$L6SIgint4}1)7ddvH(YPIF5E#;plO&F$}Pw_ z2IK5C%7xpD7F%nITE~=vZXFcvQeC@Oe|_|_X|3;K*yg70*Gc7^$8Mc{rr4a4BhX=& zHv1sM(yy|~;t-yqkZ${Wtiw{jn-$#m@Ur?Q@h7pira#tP4^-@K>Z0T#z$|_!pWRQX zPk-O}=q;EzBS`_0m?SP15Nybhvi_5&g^2)(KUSFeEbk5*-IVUcMO&!ym@G2xxeE_9 z+tE5XTYn##eTY%x=DvTyv?=x;^Jl-^e}()3Fb9O+@A}ErIg6eEtRNw>!Vr)Lv4r+F zk*!h#OV`+@_M2+^TynZZ1Xrz?C6C4Mest~)tcZZI7lAJBC4nn~6Bdx*ST6hL!L7Rm7v zt=?8TUB1LSp(0T4R5vfR6R>t5Bd|c8rZUyunQK4(C^^dSd}~$S3C@gr?(#wr@lasi zcIJ|IE5kiEqJy=XCFnBYG&lfKh6lQ;w ze723xka|x5HIgo=8j}?x&sU8uutib)$UP69#YzJjtp6a%u5x>mR`{9(giHHHQ-qc> zpwCAFu=_b^{9BnAy1<6@AszB#4_^t+(WmNSKKXLS^;s3OAfeVPan6+AYlE~4xkASLReqBJ`YhhQ9Gq>0nd_K|U)$xI>(WJinE& zzTN0S{`heo zh4Uqz8zf|y7bPXS206tLTZan}Y^lV`l$}1zQDKFB*?l$hJL9ZpP)m6vp!zb~$0#dS zz9v>{m=IS=_$|`1vL71B{t3pAD)0dQo0N@VxcF!FEL!4EEtv(dGj*#zKKP{pNOSXJ z<&SEz$E<3bT!XAxW&4?8eb~r0669!TSQJPfr}%GpEato^FHP$#d_Wl3 z#nPH_$H`ByfUHT`OYo{QBS#EG_k5~0a&Vy+5Pi_S8MIF~i;4E+oWlZL3lcHF()EM4 z8ZAGV8om@|ssMUeR)w5;9!3WEJP(s!^>L0)den|M|Csu`VAbaPQP*WCupvgiTvgGoBrvB)F`9mRHe2~A=d!@o%F@M72GxvnH)az&B^VGIGOrU zuF`MbN%+nMJb2zz+z2kdGgDdf=gDZbav2BrpI7PsDr` zqnHiu-vOZsRBq<4Ow*&uX9LdI7Z-z=rj}z-(){X=Qd2_nSr9PkoDH0xc(DGSxlj3_ zJ8{-fwX1%wIM=-4ttZ%&Rd67L1<@icG6>4ND>Q3MoH22|wARoT6VJ4Yuhx9r_`bfE zmJhy0#{*DR@XtsR-nP@<2Qm%S|Ko3cOmi@F(4&#ZaqV~w48lnN%8r_jg;S<~W zH_ZHUlSHz|%J(jM0tAPyknUvals=SGuAlY&*hcXuCr2L(om@%4 z6lS?($FWzO!W&2bA5(AP&~*F83rlb0Xe2kf1VKSTa@6RCA<`g9NlN#C(E|xV1W9Qa zAxKJ(P^6KPlul`hcl!L!Iq#pq&A!)t-JfI`oipnYZrfIu&tf?Gz26zNZrorI%+pwn zL7sSsTl>!Rik>dMeZ5 zgg)pw=}_oc{>sCIM#^b5J_c-(Li%V9ZRC>>sz|+-=b{i)F>f@3T4HtI-cGD5XOJLs z4SO`_2Ppb0Yc&v39-oke$!uXYc+7K`50vK$o8wBN@Yb= z7oQhikO2k?Hpp$`g;)^WT@C85{DGY4n0?ii-i~8&(_|;IdOcX?vg`|P47gd5J*g0$ zM)p)m8HIpC=K><|kE{&|fvFIVXTw74eN%>ZiAsbJ+|VY%8aa$qd0N;r1e=>(XX;DS zZPYoWYR2`;^a0LKkizl+?>7}!r)l(ZF$x@hylBek=^7Z37@(o;VlbJGRD)UdF=|#K zx^*FKkGn6s%%cbOgpw6$m{MjRm+J^m_F?@rQVahOzg`QU*dX(%*+(SUe z#I=bPJ}ugTevXMxB9KDxSYeV-#gihYfNQ*2w4_%FrMgFMHMxX9K?V976Ph{n+1HO{ zn{VYX)_qS8^0iM;JB0Y0`Frs{wt>`r{o`{+Od83*fxhUgS%xE?`hF$?e>qrBp51P^ zI=A-Q5tmw`+c%WS7-#-yMMhtyEd|2zKxNz?`5YuNB=|=O21(7+WVywfDo;HwwkgFx zKFvPpUl9s2ZJf9lWakndn6t{nsQsYzM3XyMIUjbQ2nq6p1mwCPUr7X1mXYM__3mg((a-N7nmN~TE)E9a~#)oGR{-(>}_IiY~(H=6-mEu zDix1(Kj%x-ZzwLRPLe%wAQ3FsUE{UYz1M=3KuWrDaCk^|>Zew3E0ZAQ z_mZ!=pQ_1wW+HZEa;fHdRlrC#PhT!@!Ej-s$b@nclUY zK^vN@03kvBBzc)=X&RFPzvjnBFGt?{Q%FAETRJ8V%3b;j7-n58H0}Mu$>)EbmjtEa zd$~$hocJL~?Yv_!l-cAbRs|%`jrv9>2GP32zSb@8cB|c8T}j2GIREhqpZr2UYNd8x z<~E~`;%VF2)%v6B7ln7A_(y$wp@_AJak=L^A1A$dbu`AaoO@MMBQCb|0k#)YI1k*a z5WGH(46;-Vivw>d#~XzL4Itfuc|VRmXJbPX8xIYr`4TqogGBBtiuY_KM}#8U)jthD zZ5xNx*j^_!*klxdZ8v$fgvuX3oS#{9$tT6#W(43Dh&XJHZaYFh{Jr*M{>^2dza%8v z&FRkzlCHhG^oRniABswk58(DgKPN%GN*-pX2f>CR3C>4;+Zqa^B@DNeZt}JYJ$hbI z&fjXz%&sn<$McK;ATbO{kRvY+&8qAQ1C282CbvDMt9ijuXIADmd8x&gQ2mYop#zEU zf7=b<77^n{4_1EKlJe)=)p6DX%>iGQ8QQm|%t4Q9Xfe}DeoO%zy zCYp~k=^a9OoNUA5!=zO(V+yFPh0+hh@?O=Rd{cuA3N@6RQ@`82J73jKW^u#!EGTkt z>89t*y!GP2&l?+Y>+8)|x9XzSfz^T`sN+KpuddXM62fE;q)jflihWCdmk+=TRy4p` zj%u7`6W^Arv0J2@Xk2F#KeVyJGZhS#bLFciuiYB#1bt%viK=+Q6R9DP-09XTh`=C* zg^zKhy5%=?dBv=Kd38{BSLcKVJMaf;v4>9mdeow?2EoGp_kC(M^LgDibK;xbe>=zq zPJC6lWeDCR3f|1YDjh%NZeqvC%>Ty7nr?@G<>+efjLx4VN&rm-8mfhLdsZR}M6q*W z)PQRW5&7QVjuwK-gBTy#>a*#j385yJOBRSj2IZhw@3${GiKds=quTRcYCLkF7efAgC zc77PO*|M7U!=roWC1E)%OFbj`7y%=gj~yqNG8u`NxG!XlZ0ez;g0aXF^)OSNpn?Tx z6&50eTAL84W*xF4R_{z&QP_R!5qBS-o7O4W+uEGlSlK@nY<~fjDZ%i7Kfm6S;bPeQ z=_hw%2s@$TQpK=s+y|F?eD=OI0mn28@uVYM+C-k_^P?PFm3i6 z1;f&3%>|3=HV9ag%+WR@S)k^Wj{=n{Q8G={DLGF8N{;0*lx5+C4D(3#=l8(2sR~Fr z?vl_&TrFTmM7K!AEV`(1q;=0`ifsai-jYjkm;ixDea4V6nZenjYq*MvLw7)|%+5+Z zfRlXukRLG$N%RMmQ4Q43R8%?jVF?S(YuV-~MUezEY|qVa5p#OE6>ELAR#(0WlVd}c*JNt_%PP^FITT%wq9zL2b!a5%;<~H77z}+rs1;%zDezo@q(&Slm zcNT~UcYbakep!tTg`TOgIr(7#G6NxHOy5>kNMaiYWz6Y8?(v$Hbf0Zn1-pMo`%_Xl z1c!A2-^$Hh;zA5pr){a)?UYHEe1luP!V(wo;~ogKR073}AqZij#|vK$XAf*bWPL%4 zNiR79e5=f*H-_ywQLS+HDCW*I%K@m!D&BewTKxUvZsS4^j|Bo~0p<3fte1~ZC_dG} z%5cyPN5fI;lttF-Un-}meTj15S0rTJxi2YJ$XSccCJIFA?Qm?}ObJA6!C!gYY$-#F zwILuUYGrDhqQ^Nzqlv6p5KvJ94BQlgiXo)J3hQB`0bBwdC(pwLn{PCe$q17kirPm# zduZp&y`&d=PSgE}8o!4{BrI?Qd;gb&FHI*agp;KYP}_)SsxA;=}37SjE@H>IIhZ5OU()-+~pj;FiRWBnwp z)DNvTpRXrkO}2&0QRt$Oh?~4O;!`wsqDjs-2z>~}q{t(W?Rrvq<600Fo(Hk;mLQsB zKve)@I0&cxlu)KQYJckSszpRuJNj)6H0<7bk!iC0!rFUZYK}B-tH9W44p&U=Y*pI^ zxy1~aq zTf*Q)yq&|!==HstFi?hIx1wMcdx~$!C$GERh}Uv%<&C@L_qIl&N~U6uA%A`!H6E8z z4yK1~U}J}b`2f0#;LmqNFJ_Jf)^_37YSatUj3lvgl7U+k1))3Pq@9D7#DE`@(#vEh zN|i8zNR$LPN*^i}CAe)=p3`8n#3W8QeSkqjq7Nomva¬7)o864U%n>Tj;-7UsV+ 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⪻B0G)H0-SY=uA!KuCC>%KRSZIRt^o9x|_Y;Wkau87q`SNNDIr zypt`45TyTOO0eUBFI70P!?&$H3qVL-SjfL0y&Ha9gj%*J;&3l? Date: Mon, 31 Aug 2026 08:37:27 -0700 Subject: [PATCH 18/28] Revert "update regression test reference to match updated behaviour" This reverts commit 5d5530c09243dfa66211e6c6c28c81b630fae173. --- .../e2e02_tensor_initial_render_baseline.png | Bin 147976 -> 33657 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/references/e2e02_tensor_initial_render_baseline.png b/tests/references/e2e02_tensor_initial_render_baseline.png index 9532476ce46c90f8480e78063131b5272878a76c..40e86e1a32b1e1ac84590fe9a3382ef540406973 100644 GIT binary patch literal 33657 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⪻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¬7)o864U%n>Tj;-7UsV+ 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 Date: Mon, 31 Aug 2026 08:44:00 -0700 Subject: [PATCH 19/28] reorder regression tests to get clean initial baseline --- .../regressions/test_variable_components.py | 166 +++++++++--------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/tests/e2e/regressions/test_variable_components.py b/tests/e2e/regressions/test_variable_components.py index d30787db..3634b1a9 100644 --- a/tests/e2e/regressions/test_variable_components.py +++ b/tests/e2e/regressions/test_variable_components.py @@ -39,6 +39,89 @@ TENSOR_COMPONENT_LABELS = ["XX", "XY", "XZ", "YX", "YY", "YZ", "ZX", "ZY", "ZZ"] +# =========================================================================== +# Selecting a tensor component colors the part (screenshot diff) +# =========================================================================== + +@pytest.mark.parametrize("visor_asset_spec", [TENSOR_ASSETS], indirect=True) +@pytest.mark.regression +class TestTensorComponentColoring: + """ + Verify that selecting a tensor component updates the rendered + view, confirmed by screenshot baseline comparison. + """ + + def test_initial_render_baseline(self, page: Page, visor_server, baseline_dir, request): + """Take a baseline screenshot of the tensor mesh initial render.""" + target_url = visor_server.url + page.set_viewport_size({"width": 1280, "height": 800}) + page.goto(target_url, wait_until="networkidle") + canvas = wait_for_canvas_alive(page, "tensor-initial") + settle(page, 300) # preserve prior render-settle timing before baseline capture + + result = visual.verify_canvas_against_baseline( + canvas_locator=canvas, + baseline_dir=baseline_dir, + pixel_threshold=2.55, + request=request, + compare_images=compare_images, + test_id="e2e02_tensor_initial_render", + test_suite="regression", + ) + + suite_result = visual.make_test_result(result) + assert suite_result.passed, "Initial render baseline failed\n" + suite_result.summary() + + def test_component_selection_changes_rendering(self, page: Page, visor_server): + """Selecting different tensor components produces visually distinct canvas renders.""" + target_url = visor_server.url + page.set_viewport_size({"width": 1280, "height": 800}) + page.goto(target_url, wait_until="networkidle") + canvas = wait_for_canvas_alive(page, "tensor-coloring") + + # Select the part to activate the variable selection UI + part_row = page.locator("div.visor-tree-view td:nth-child(2)").filter(has_text="tensor_test_sphere") + part_row.wait_for(state="visible", timeout=10_000) + part_row.click() + + # Select the Stress variable + variable_select = page.locator("select").filter(has=page.locator("option[data-name='Stress']")) + variable_select.wait_for(state="visible", timeout=10_000) + variable_select.select_option(value="POINT::Stress::9") + + # Wait for the component dropdown to become visible + component_select = page.locator("label").filter(has_text="Component").locator("select") + component_select.wait_for(state="visible", timeout=10_000) + + # Capture a canvas screenshot per component for the representative subset + components_to_test = ["Magnitude", "XX", "YY", "ZZ"] + screenshots_root = Path("tests/artifacts/regression/screenshots") + screenshots_root.mkdir(parents=True, exist_ok=True) + + stamp = time.strftime("%Y%m%d-%H%M%S") + shot_paths: list[Path] = [] + for comp in components_to_test: + component_select.select_option(label=comp) + settle(page, 500) # Allow renderer time to update + shot_path = screenshots_root / f"e2e04_coloring_{comp}_{stamp}.png" + canvas.screenshot(path=str(shot_path)) + shot_paths.append(shot_path) + + # Assert each consecutive pair of screenshots differs (RMS > threshold), + # confirming that selecting a different component changes the rendering. + min_rms = 1.0 + for i in range(len(shot_paths) - 1): + img_a = Image.open(shot_paths[i]) + img_b = Image.open(shot_paths[i + 1]) + img_a, img_b = ensure_same_size_and_mode(img_a, img_b) + rms, _ = rms_diff(img_a, img_b) + assert rms > min_rms, ( + f"Switching from '{components_to_test[i]}' to '{components_to_test[i + 1]}' " + f"did not change the render (RMS={rms:.3f}, expected > {min_rms}). " + f"Screenshots: {shot_paths[i]}, {shot_paths[i + 1]}" + ) + + # =========================================================================== # Variable dropdown shows all 9 tensor components # =========================================================================== @@ -124,89 +207,6 @@ def test_tensor_component_dropdown_options(self, page: Page, visor_server, basel ) -# =========================================================================== -# Selecting a tensor component colors the part (screenshot diff) -# =========================================================================== - -@pytest.mark.parametrize("visor_asset_spec", [TENSOR_ASSETS], indirect=True) -@pytest.mark.regression -class TestTensorComponentColoring: - """ - Verify that selecting a tensor component updates the rendered - view, confirmed by screenshot baseline comparison. - """ - - def test_initial_render_baseline(self, page: Page, visor_server, baseline_dir, request): - """Take a baseline screenshot of the tensor mesh initial render.""" - target_url = visor_server.url - page.set_viewport_size({"width": 1280, "height": 800}) - page.goto(target_url, wait_until="networkidle") - canvas = wait_for_canvas_alive(page, "tensor-initial") - settle(page, 300) # preserve prior render-settle timing before baseline capture - - result = visual.verify_canvas_against_baseline( - canvas_locator=canvas, - baseline_dir=baseline_dir, - pixel_threshold=2.55, - request=request, - compare_images=compare_images, - test_id="e2e02_tensor_initial_render", - test_suite="regression", - ) - - suite_result = visual.make_test_result(result) - assert suite_result.passed, "Initial render baseline failed\n" + suite_result.summary() - - def test_component_selection_changes_rendering(self, page: Page, visor_server): - """Selecting different tensor components produces visually distinct canvas renders.""" - target_url = visor_server.url - page.set_viewport_size({"width": 1280, "height": 800}) - page.goto(target_url, wait_until="networkidle") - canvas = wait_for_canvas_alive(page, "tensor-coloring") - - # Select the part to activate the variable selection UI - part_row = page.locator("div.visor-tree-view td:nth-child(2)").filter(has_text="tensor_test_sphere") - part_row.wait_for(state="visible", timeout=10_000) - part_row.click() - - # Select the Stress variable - variable_select = page.locator("select").filter(has=page.locator("option[data-name='Stress']")) - variable_select.wait_for(state="visible", timeout=10_000) - variable_select.select_option(value="POINT::Stress::9") - - # Wait for the component dropdown to become visible - component_select = page.locator("label").filter(has_text="Component").locator("select") - component_select.wait_for(state="visible", timeout=10_000) - - # Capture a canvas screenshot per component for the representative subset - components_to_test = ["Magnitude", "XX", "YY", "ZZ"] - screenshots_root = Path("tests/artifacts/regression/screenshots") - screenshots_root.mkdir(parents=True, exist_ok=True) - - stamp = time.strftime("%Y%m%d-%H%M%S") - shot_paths: list[Path] = [] - for comp in components_to_test: - component_select.select_option(label=comp) - settle(page, 500) # Allow renderer time to update - shot_path = screenshots_root / f"e2e04_coloring_{comp}_{stamp}.png" - canvas.screenshot(path=str(shot_path)) - shot_paths.append(shot_path) - - # Assert each consecutive pair of screenshots differs (RMS > threshold), - # confirming that selecting a different component changes the rendering. - min_rms = 1.0 - for i in range(len(shot_paths) - 1): - img_a = Image.open(shot_paths[i]) - img_b = Image.open(shot_paths[i + 1]) - img_a, img_b = ensure_same_size_and_mode(img_a, img_b) - rms, _ = rms_diff(img_a, img_b) - assert rms > min_rms, ( - f"Switching from '{components_to_test[i]}' to '{components_to_test[i + 1]}' " - f"did not change the render (RMS={rms:.3f}, expected > {min_rms}). " - f"Screenshots: {shot_paths[i]}, {shot_paths[i + 1]}" - ) - - # =========================================================================== # Regression — Selecting scalar/vector components still works # =========================================================================== From 42e6a3d52bc4ef8b7051afb65b71aec9240b08ac Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Mon, 31 Aug 2026 09:35:23 -0700 Subject: [PATCH 20/28] reorder regression test color by order to end up with magnitude --- tests/e2e/regressions/test_variable_components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/regressions/test_variable_components.py b/tests/e2e/regressions/test_variable_components.py index 3634b1a9..9cf947da 100644 --- a/tests/e2e/regressions/test_variable_components.py +++ b/tests/e2e/regressions/test_variable_components.py @@ -94,7 +94,7 @@ def test_component_selection_changes_rendering(self, page: Page, visor_server): component_select.wait_for(state="visible", timeout=10_000) # Capture a canvas screenshot per component for the representative subset - components_to_test = ["Magnitude", "XX", "YY", "ZZ"] + components_to_test = ["XX", "YY", "ZZ", "Magnitude"] screenshots_root = Path("tests/artifacts/regression/screenshots") screenshots_root.mkdir(parents=True, exist_ok=True) From 8cfb5072798d69e3789cf01962fa7cb4ac051fca Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Fri, 28 Aug 2026 12:33:16 -0700 Subject: [PATCH 21/28] add arrayName, type, numComponents to variable state --- .../models/common/visor_variable_state.py | 17 ++++++- .../models/persist/persisted_viewer_state.py | 2 +- .../visor/visor-client/src/VisorFrontend.tsx | 3 ++ .../src/state/appstate/VisorSpectrumState.tsx | 44 +++++++++++++++++++ .../unit/models/test_visor_variable_state.py | 26 +++++++++++ 5 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 tests/unit/models/test_visor_variable_state.py diff --git a/src/ansys/visor/viewer/models/common/visor_variable_state.py b/src/ansys/visor/viewer/models/common/visor_variable_state.py index 4034689a..9deb072f 100644 --- a/src/ansys/visor/viewer/models/common/visor_variable_state.py +++ b/src/ansys/visor/viewer/models/common/visor_variable_state.py @@ -2,7 +2,9 @@ from typing import List, Tuple -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_serializer + +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType class VisorVariableState(BaseModel): @@ -15,11 +17,24 @@ class VisorVariableState(BaseModel): a unique identifier for the variable: the variable name, type (POINT/CELL), and number of components, and computes the ranges across all datasets/parts that contain that variable. + ``array_name``, ``type`` and ``num_components`` are those same three identifying properties, carried + explicitly (rather than only opaquely inside ``id``) so a consumer can act on them without parsing the + client-built identifier. They are required: a variable state with no known source array, association, or + component count is not a state this model can represent. + In the future, the backend can own this, but for now we can treat this as passthrough data, as the id value is stable across sessions. """ model_config = ConfigDict(populate_by_name=True) id: str = Field(...) + array_name: str = Field(..., alias="arrayName") + type: VisorVtkVariableType = Field(...) + num_components: int = Field(..., alias="numComponents") magnitude_range: Tuple[float, float] | None = Field(default=None, alias="magnitudeRange") ranges: List[Tuple[float, float]] = Field(default_factory=list) + + @field_serializer("type") + def _serialize_type(self, value: VisorVtkVariableType) -> str: + """Emit the wire value (e.g. "POINT"/"CELL") for both dict-mode and JSON-mode dumps.""" + return value.value diff --git a/src/ansys/visor/viewer/models/persist/persisted_viewer_state.py b/src/ansys/visor/viewer/models/persist/persisted_viewer_state.py index 16161113..be744036 100644 --- a/src/ansys/visor/viewer/models/persist/persisted_viewer_state.py +++ b/src/ansys/visor/viewer/models/persist/persisted_viewer_state.py @@ -25,7 +25,7 @@ class PersistedViewerStateV1(BaseModel): """ model_config = ConfigDict(arbitrary_types_allowed=True) - version: Literal["1.0"] = Field(default="1.0") + version: Literal["1.1"] = Field(default="1.1") ui: VisorUIState = Field(default_factory=VisorUIState) scene: PersistedSceneState = Field(default_factory=PersistedSceneState) diff --git a/src/ansys/visor/visor-client/src/VisorFrontend.tsx b/src/ansys/visor/visor-client/src/VisorFrontend.tsx index a2cc9e19..56860b5f 100644 --- a/src/ansys/visor/visor-client/src/VisorFrontend.tsx +++ b/src/ansys/visor/visor-client/src/VisorFrontend.tsx @@ -260,6 +260,9 @@ export class VisorFrontend { for (const spectrumInfo of spectrumInfos.array) { const spectrumState = new VisorSpectrumState(); spectrumState.setId(spectrumInfo.id.toString()); + spectrumState.setArrayName(spectrumInfo.name); + spectrumState.setType(spectrumInfo.type); + spectrumState.setNumComponents(spectrumInfo.numComponents); const magnitudeRange = spectrumInfo.getRangeInfo(-1); if (magnitudeRange == null) { throw new Error(`range at component ${-1} not found`); diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorSpectrumState.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorSpectrumState.tsx index a9eecd08..c2151b7f 100644 --- a/src/ansys/visor/visor-client/src/state/appstate/VisorSpectrumState.tsx +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorSpectrumState.tsx @@ -1,13 +1,19 @@ import { ensureArray, ensureNumberArray, + ensureNumber, + ensureString, JsonDict, parseState, StateInput, } from './VisorStateCommon.tsx'; +import type { FieldAssociation } from './vtkInfo/VisorVtkDataArray.tsx'; export default class VisorSpectrumState { private _id: string = ''; + private _arrayName: string = ''; + private _type: FieldAssociation | undefined = undefined; + private _numComponents: number = 0; private _magnitudeRange: number[] | undefined = undefined; private _ranges: (number[] | undefined)[] = []; @@ -28,6 +34,36 @@ export default class VisorSpectrumState { } } + get arrayName(): string { + return this._arrayName; + } + + setArrayName(val: string | null | undefined): void { + if (val != null) { + this._arrayName = ensureString(val, 'val'); + } + } + + get type(): FieldAssociation | undefined { + return this._type; + } + + setType(val: FieldAssociation | null | undefined): void { + if (val != null) { + this._type = val; + } + } + + get numComponents(): number { + return this._numComponents; + } + + setNumComponents(val: number | null | undefined): void { + if (val != null) { + this._numComponents = ensureNumber(val, 'val'); + } + } + get magnitudeRange(): number[] | undefined { return this._magnitudeRange; } @@ -83,6 +119,11 @@ export default class VisorSpectrumState { } this.setId(thisId); + this.setArrayName(data.arrayName === undefined ? this._arrayName : data.arrayName); + this.setType(data.type === undefined ? this._type : data.type); + this.setNumComponents( + data.numComponents === undefined ? this._numComponents : data.numComponents + ); this.setMagnitudeRange( data.magnitudeRange === undefined ? this._magnitudeRange : data.magnitudeRange, replace @@ -99,6 +140,9 @@ export default class VisorSpectrumState { toDict(): JsonDict { return { id: this.id, + arrayName: this.arrayName, + type: this.type, + numComponents: this.numComponents, magnitudeRange: this.magnitudeRange, ranges: this.ranges, }; diff --git a/tests/unit/models/test_visor_variable_state.py b/tests/unit/models/test_visor_variable_state.py new file mode 100644 index 00000000..0dd42a72 --- /dev/null +++ b/tests/unit/models/test_visor_variable_state.py @@ -0,0 +1,26 @@ +"""Unit tests for VisorVariableState.""" + +import pytest +from pydantic import ValidationError + +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType +from ansys.visor.viewer.models.common.visor_variable_state import VisorVariableState + +_VALID_KWARGS = { + "id": "POINT::displacement::3", + "array_name": "displacement", + "type": VisorVtkVariableType.POINT, + "num_components": 3, +} + + +@pytest.mark.parametrize("missing_field", ["array_name", "type", "num_components"]) +def test_construction_missing_required_field_raises(missing_field): + """Omitting any one of array_name, type, or num_components must raise, + proving each is required on its own rather than all three being + collectively-but-not-individually required.""" + kwargs = {k: v for k, v in _VALID_KWARGS.items() if k != missing_field} + + with pytest.raises(ValidationError): + VisorVariableState(**kwargs) + From 49949daa85f406b0c68c9550f609025ce77fa7d1 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Fri, 28 Aug 2026 12:33:57 -0700 Subject: [PATCH 22/28] source per-part state from the registry on save and restore it on load --- src/ansys/visor/viewer/vtk/scene/base.py | 269 ++++++++- .../viewer/vtk/scene/visor_state_mapper.py | 7 + tests/integration/test_save_load_state.py | 119 ++++ tests/unit/vtk/scene/test_base.py | 550 +++++++++++++++++- .../unit/vtk/scene/test_visor_state_mapper.py | 45 ++ 5 files changed, 985 insertions(+), 5 deletions(-) diff --git a/src/ansys/visor/viewer/vtk/scene/base.py b/src/ansys/visor/viewer/vtk/scene/base.py index a7cbd706..0903e4fb 100644 --- a/src/ansys/visor/viewer/vtk/scene/base.py +++ b/src/ansys/visor/viewer/vtk/scene/base.py @@ -150,10 +150,36 @@ async def get_state(self, timeout: float) -> PersistedViewerStateV1: Capture the current viewer state and return it as a :class:`PersistedViewerStateV1`. - The renderer-specific part of state capture is handled by - :meth:`_get_runtime_state_async`. + The frontend round trip (:meth:`_get_runtime_state_async`) remains the + only source for everything the browser owns — UI panels, unit, the + display toggles, camera, cross-section and the per-variable ranges. + **Per-part state is not taken from it.** The dataset registry is the + server-side authority for that, so ``scene.dataset_states`` is replaced + wholesale with the registry's own runtime state before the persisted + mapping runs. The browser is no longer consulted for per-part state. + + The registry is read under ``_vtk_lock`` and each dataset state is + deep-copied out of it. The registry hands out its live + ``RuntimeDatasetState`` objects by reference, and the per-part setters + mutate the records they contain in place from the trame daemon thread; + without the snapshot the persist mapper would be an unlocked reader of + records that are being written concurrently. The lock is taken *after* + the ``await`` has completed and released before the mapping, so it is + never held across an ``await``. Named cost: this is the only place a + coroutine acquires ``_vtk_lock``, so a save issued while a long + main-thread VTK call holds the lock waits for that call. It cannot + deadlock: the outbound bridge call made under the lock reaches + ``loop.call_later`` and returns without waiting on the event loop. """ runtime_state = await self._get_runtime_state_async(timeout) + + with self._vtk_lock: + registry_dataset_states = { + dataset_id: dataset_state.model_copy(deep=True) + for dataset_id, dataset_state in self._dataset_registry.runtime_state_dict.items() + } + runtime_state.scene.dataset_states = registry_dataset_states + persisted = self._state_mapper.runtime_to_persisted(runtime_state) return persisted @@ -166,7 +192,9 @@ def apply_state(self, state: PersistedViewerStateV1): :meth:`_apply_runtime_state_to_render`. Holds ``_vtk_lock`` for the whole body: the delegated step mutates - VTK and pushes to the frontend. + VTK and pushes to the frontend. The critical section deliberately + spans the outbound bridge call and the flush that follows it — the + unit the lock protects is the compound sequence, not the VTK work. """ with self._vtk_lock: # Apply UI settings @@ -175,10 +203,18 @@ def apply_state(self, state: PersistedViewerStateV1): # Transform the frontend PersistedViewerStateV1 -> RuntimeAppState runtime_app_state = self._state_mapper.persisted_to_runtime(state) + # Restore per-part state server-side: write the registry, then apply + # every part to this process's VTK pipeline. The browser is not + # asked to do this, and is not trusted to have done it. + self._restore_part_states_from_runtime(runtime_app_state) + # Renderer-specific: flush VTK window and notify frontend (wasm), or # push camera to vtkCamera (RCA), or no-op (headless). self._apply_runtime_state_to_render(runtime_app_state) + # Note: There is intentionally no wasm flush here: the bridge call is fire-and-forget, so a flush + # at this point races the client's rebuild against a half-written object graph. + def get_scene_details(self) -> VisorSceneDetails: """Return the VisorState.""" if self._scene_graph is None: @@ -495,6 +531,233 @@ def clear_part_color_variable(self, node_id: int) -> None: return self._renderer.clear_color_variable(node_id) + def _restore_part_states_from_runtime(self, runtime_app_state: "RuntimeAppState") -> None: + """ + Restore per-part state from a runtime app state, on the load path. + + Two halves, in this order: the registry is replaced wholesale from the + supplied dataset states (``replace_part_states``), then every part is + applied to this process's VTK pipeline through ``IRenderer.apply_*``. + Either half can silently do nothing while the other succeeds, which is + why they are separate steps and separately asserted. + + Takes no lock of its own: it is only ever called from inside + :meth:`apply_state`'s critical section, and acquiring here would read + as though the method were independently safe to call. + + Every failure is a logged no-op, per the posture used at every other + layer: an unregistered dataset id, an unresolvable part, a malformed + colour, a half-set variable reference, an unknown variable identifier, + an unknown array, and an array whose width disagrees with the stored + one are each skipped without raising. + """ + dataset_states = runtime_app_state.scene.dataset_states or {} + variable_states = runtime_app_state.scene.spectrum_states or {} + + # Store half. Unknown dataset ids are skipped by the registry. + self._dataset_registry.replace_part_states(dataset_states) + + # Apply half. The dataset id is resolved once per dataset, from the + # loop key, and the skip is explicit and logged: replace_part_states + # skips unknown ids silently, so without this the store half and the + # apply half could disagree about which datasets exist. + for dataset_id, dataset_state in dataset_states.items(): + dataset = self._dataset_registry.datasets.get(dataset_id) + if dataset is None: + logger.warning( + "_restore_part_states_from_runtime: dataset %s is not registered; " + "its part state was not applied to the pipeline.", dataset_id + ) + continue + + # Per-part variable metadata, keyed by part id — one entry per + # non-empty leaf. Built once per dataset rather than per part. + variables_by_part = { + entry.part_id: entry.variables for entry in dataset.list_variables() + } + + for part_id, part_state in dataset_state.part_states.items(): + self._restore_one_part_state( + part_id, part_state, variable_states, variables_by_part.get(part_id) + ) + + def _restore_one_part_state( + self, + part_id: int, + part_state, + variable_states: dict, + part_variables, + ) -> None: + """ + Apply one restored part record to the pipeline. + + Absent fields are absent: a ``None`` is "this record says nothing about + that property", not "reset it to the default", so nothing is applied + for it. **Recorded divergence:** :meth:`apply_state` does not + guarantee freshly built pipelines — ``load_state`` rebuilds datasets + only when the scene is empty — so a part whose stored ``diffuse_rgb`` + is ``None`` keeps whatever colour the pipeline already carries while + the registry records no custom colour. + + The colour is validated to exactly three elements here; a malformed + colour is a logged no-op and the same guard covers the colour the + selection branch reads. The registry keeps the malformed value it was + loaded with — requiring on load would make the next save silently + rewrite the user's file. + """ + if part_state.visible is not None: + self._renderer.apply_visibility(part_id, part_state.visible) + + if part_state.opacity is not None: + self._renderer.apply_opacity(part_id, part_state.opacity) + + stored_rgb = part_state.diffuse_rgb + valid_rgb = None + if stored_rgb is not None: + if len(stored_rgb) == 3: + valid_rgb = stored_rgb + else: + logger.warning( + "_restore_one_part_state: part %s has a diffuse colour of %s elements, " + "not 3; leaving the pipeline colour unchanged.", part_id, len(stored_rgb) + ) + + if valid_rgb is not None: + self._renderer.apply_diffuse_color(part_id, valid_rgb[0], valid_rgb[1], valid_rgb[2]) + + if part_state.selected is not None: + selection_rgb = ( + valid_rgb if valid_rgb is not None else list(VisorColors.DefaultMeshColor) + ) + self._renderer.apply_selected(part_id, part_state.selected, selection_rgb) + + self._restore_part_color_variable(part_id, part_state, variable_states, part_variables) + + def _restore_part_color_variable( + self, + part_id: int, + part_state, + variable_states: dict, + part_variables, + ) -> None: + """ + Restore one part's colour-variable reference, or clear it. + + The variable reference is a compound value that is only ever set or + cleared atomically, so a half-set pair — an identifier with no + component, or a component with no identifier — is not a state this can + act on and is a logged no-op in both directions. A component with no + identifier in particular must not fall through to the clear branch. + + The range is resolved from the persisted variable entry, whose index + convention is the opposite of the client's internal table: a stored + component of ``-1`` reads ``magnitude_range`` and a component of 0 or + greater reads ``ranges[component]``. The branch is three-way with no + fallthrough — a negative component other than ``-1`` is neither the + magnitude sentinel nor an index, and returns before any mutation. + + The array is resolved against the server's **per-part** variable + metadata and its component count is checked against the stored one: + two parts can carry same-named arrays of different widths, which the + application treats as different quantities, so name and association + alone could colour a part by the wrong quantity with no error anywhere. + """ + variable_id = part_state.spectrum_id + component = part_state.spectrum_component + + if variable_id is None: + if component is not None: + logger.warning( + "_restore_part_color_variable: part %s stores component %s with no variable " + "identifier; not clearing and not applying.", part_id, component + ) + return + self._renderer.clear_color_variable(part_id) + return + + if component is None: + logger.warning( + "_restore_part_color_variable: part %s stores variable '%s' with no component; " + "skipping.", part_id, variable_id + ) + return + + variable_state = variable_states.get(variable_id) + if variable_state is None: + logger.warning( + "_restore_part_color_variable: no variable entry for '%s' (part %s); skipping.", + variable_id, part_id + ) + return + + if component == -1: + value_range = variable_state.magnitude_range + elif component >= 0: + if component >= len(variable_state.ranges): + logger.warning( + "_restore_part_color_variable: component %s is outside the %s stored ranges " + "for '%s' (part %s); skipping.", + component, len(variable_state.ranges), variable_id, part_id + ) + return + value_range = variable_state.ranges[component] + else: + logger.warning( + "_restore_part_color_variable: component %s for '%s' (part %s) is neither the " + "magnitude sentinel (-1) nor a component index; skipping.", + component, variable_id, part_id + ) + return + + if value_range is None: + logger.warning( + "_restore_part_color_variable: no stored range for component %s of '%s' " + "(part %s); skipping.", component, variable_id, part_id + ) + return + + if part_variables is None: + logger.warning( + "_restore_part_color_variable: no variable metadata for part %s; skipping.", + part_id + ) + return + + resolved = next( + ( + variable for variable in part_variables + if variable.name == variable_state.array_name + and variable.type is variable_state.type + ), + None, + ) + if resolved is None: + logger.warning( + "_restore_part_color_variable: array '%s' (%s) not found on part %s; skipping.", + variable_state.array_name, variable_state.type, part_id + ) + return + + if resolved.num_components != variable_state.num_components: + logger.warning( + "_restore_part_color_variable: array '%s' on part %s has %s components, the " + "stored variable has %s; the part does not participate in this variable.", + variable_state.array_name, part_id, + resolved.num_components, variable_state.num_components + ) + return + + min_val, max_val = value_range + self._renderer.apply_color_variable( + part_id, + variable_id, + variable_state.type, + variable_state.array_name, + component, + min_val, + max_val, + ) + # ------------------------------------------------------------------ # Internal helpers # ------------------------------------------------------------------ diff --git a/src/ansys/visor/viewer/vtk/scene/visor_state_mapper.py b/src/ansys/visor/viewer/vtk/scene/visor_state_mapper.py index 6df9034b..0a681481 100644 --- a/src/ansys/visor/viewer/vtk/scene/visor_state_mapper.py +++ b/src/ansys/visor/viewer/vtk/scene/visor_state_mapper.py @@ -73,6 +73,13 @@ def persisted_to_runtime(self, state: PersistedViewerStateV1) -> RuntimeAppState """Convert PersistedViewerStateV1 back to runtime RuntimeAppState. Logic parity with ``VisorScene._persisted_to_runtime``. + + This is **not**, and never has been, the registry-population path. It + builds runtime dataset states and returns them on the RuntimeAppState; + it never assigns its result back onto ``VisorDataset.state``, so no + registry record is written by calling it. + :meth:`VisorSceneBase._restore_part_states_from_runtime` now populates + the registry explicitly, from the state this method returns. """ # UI settings ui_state = state.ui diff --git a/tests/integration/test_save_load_state.py b/tests/integration/test_save_load_state.py index f19be965..415901b4 100644 --- a/tests/integration/test_save_load_state.py +++ b/tests/integration/test_save_load_state.py @@ -29,6 +29,8 @@ tests/files/simple_multiblock.vtkhdf — VTK MultiBlockDataSet """ +import asyncio +import json import os from unittest.mock import MagicMock, patch @@ -41,9 +43,15 @@ from ansys.visor.viewer.app.visor_vtk import VisorVTK from ansys.visor.viewer.core.metadata import ExtendedMetadata +from ansys.visor.viewer.models.common.part_properties import PartProperties from ansys.visor.viewer.models.common.visor_ui_state import VisorUIState from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1 +from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import ( + RuntimeDatasetState, + RuntimePartProperties, +) +from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState from ansys.visor.viewer.vtk.io.file_to_dataset import file_to_dataset from ansys.visor.viewer.vtk.io.visor_file_io import VisorFileIO @@ -370,3 +378,114 @@ def test_stale_snapshot_deleted_after_dataset_removed(self, file_io, tmp_path): assert plate_snap.exists() assert not mesh_snap.exists() + +# ================================================================== # +# Registry-sourced save, and registry restore on load +# ================================================================== # + +class TestRegistrySourcedPartState: + """The server's own registry is the authority for per-part state across a + save/load round trip, with the browser never consulted. + + The two directions are separate tests: the save asserts against the raw + ``visor.json`` (parts keyed by **name**), the load asserts against the + registry. Neither uses the other's output, so a failure names one side. + """ + + def test_saved_visor_json_carries_registry_part_state_keyed_by_name(self, iface, tmp_path): + """save_state writes the registry's per-part state, not the frontend's.""" + data = file_to_dataset(_vtp_path()) + dataset_id = iface._scene.add_dataset(data, ExtendedMetadata(name="plate", unit="m")) + part_id = iface._scene.datasets[dataset_id].part_index.part_ids[0] + + # Mutate through the coordinator, i.e. exactly what a trigger does. + iface._scene.set_part_opacity(part_id, 0.25) + iface._scene.set_part_visibility(part_id, False) + iface._scene.set_part_diffuse_color(part_id, [1.0, 0.0, 0.0]) + + # The browser answers getState with contradictory per-part values for + # the same dataset. A pass therefore proves the file came from the + # registry rather than from the frontend round trip. + frontend_state = RuntimeAppState.from_components( + dark_mode=False, + unit="m", + dataset_states={ + dataset_id: RuntimeDatasetState( + id=dataset_id, + part_states={ + part_id: RuntimePartProperties(id=part_id, opacity=0.99, visible=True) + }, + ) + }, + ) + + async def _frontend_round_trip(timeout: float = 5.0): + return frontend_state + + iface._scene._get_runtime_state_async = _frontend_round_trip + iface._server_manager = MagicMock() + iface._server_manager.running = True + + asyncio.run(iface.save_state(str(tmp_path))) + + with open(os.path.join(str(tmp_path), "visor.json"), "r") as fh: + written = json.load(fh) + + # Parts are keyed by name, not id; a non-composite dataset has one + # part, named after the dataset. + part = written["scene"]["dataset_states"]["plate"]["parts"]["plate"] + assert part["opacity"] == 0.25 + assert part["visible"] is False + assert part["diffuse_rgb"] == [1.0, 0.0, 0.0] + + def test_reloading_the_saved_state_restores_the_registry(self, file_io, iface, tmp_path): + """load_state populates the registry itself, with no browser involved.""" + original = file_to_dataset(_vtp_path()) + snap = file_io.get_persisted_dataset_path(str(tmp_path), "plate") + file_io.write_dataset(snap, original) + + state = PersistedViewerStateV1.from_components( + ui_state=VisorUIState(), + unit="m", + orthographic_enabled=None, + cross_section_enabled=None, + edges_enabled=None, + bounding_box_enabled=None, + datasets={ + "plate": PersistedDatasetState( + serialized_dataset_path=str(snap), + parts={ + "plate": PartProperties( + opacity=0.25, + visible=False, + selected=True, + diffuse_rgb=[1.0, 0.0, 0.0], + color_by="POINT::pressure::1", + color_by_component=0, + ) + }, + ) + }, + ) + file_io.write_state(str(tmp_path), state) + + # No browser: the bridge push and the wasm flush are not exercised + # in-process. The registry restore must not depend on either. + iface._scene._apply_runtime_state_to_render = MagicMock() + iface._scene._renderer.flush_wasm_state = MagicMock() + + assert iface._scene.dataset_count == 0 + iface.load_state(str(tmp_path)) + + dataset = next(iter(iface._scene.datasets.values())) + part_id = dataset.part_index.part_ids[0] + record = iface._scene._dataset_registry.get_part_state(part_id) + + assert record.opacity == 0.25 + assert record.visible is False + assert record.selected is True + assert record.diffuse_rgb == [1.0, 0.0, 0.0] + assert record.spectrum_id == "POINT::pressure::1" + assert record.spectrum_component == 0 + + diff --git a/tests/unit/vtk/scene/test_base.py b/tests/unit/vtk/scene/test_base.py index 922a0e13..feb52ed8 100644 --- a/tests/unit/vtk/scene/test_base.py +++ b/tests/unit/vtk/scene/test_base.py @@ -15,6 +15,8 @@ Behavioural coverage of the remaining shared logic lives in test_local_scene.py, exercised through the concrete VisorLocalScene subclass. """ +import asyncio +import json import threading from unittest.mock import MagicMock, patch @@ -25,17 +27,23 @@ from ansys.visor.viewer.core.visor_colors import VisorColors from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType +from ansys.visor.viewer.models.common.visor_variable_state import VisorVariableState from ansys.visor.viewer.models.runtime.dataset.runtime_dataset_state import ( RuntimeDatasetState, RuntimePartProperties, ) +from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState from ansys.visor.viewer.renderer.local_renderer import VisorLocalRenderer from ansys.visor.viewer.vtk.datasets.visor_dataset_registry import VisorDatasetRegistry from ansys.visor.viewer.vtk.node_pipeline import VtkNodePipeline from ansys.visor.viewer.vtk.scene.base import VisorSceneBase +from ansys.visor.viewer.vtk.variables.visor_part_variables import VisorPartVariables +from ansys.visor.viewer.vtk.variables.visor_variables import VisorVariable NODE_ID = 7 +SECOND_NODE_ID = 8 UNKNOWN_NODE_ID = 999999 +DATASET_ID = 1 def test_cannot_instantiate_directly(): @@ -98,16 +106,19 @@ def __exit__(self, exc_type, exc, tb): return False -def _make_part_dataset(dataset_id: int, part_ids, part_states=None): +def _make_part_dataset(dataset_id: int, part_ids, part_states=None, part_variables=None): """Dataset stand-in with a real PartIndex.part_ids and a real state object. Real (not MagicMock) state, so a registry write can be read back through - object identity rather than through a recorded call. + object identity rather than through a recorded call. ``part_variables`` + is the per-part variable metadata the restore path reads back through + ``VisorDataset.list_variables()`` — one ``VisorPartVariables`` per part. """ dataset = MagicMock() dataset.part_index = MagicMock() dataset.part_index.part_ids = list(part_ids) dataset.state = RuntimeDatasetState(id=dataset_id, part_states=part_states or {}) + dataset.list_variables.return_value = list(part_variables or []) return dataset @@ -540,6 +551,12 @@ def mocked_scene(renderer): s._dataset_registry.count = 1 s._renderer = MagicMock(name="renderer") s._state_mapper = MagicMock(name="state_mapper") + # apply_state now walks the mapped state's dataset_states, so the mapper + # must return a real RuntimeAppState rather than a MagicMock (whose + # dataset_states would be a MagicMock and raise on iteration). + s._state_mapper.persisted_to_runtime.return_value = RuntimeAppState.from_components( + dark_mode=False, unit="m", dataset_states={} + ) s._vtk_lock = _LockSpy() return s @@ -681,4 +698,533 @@ def test_the_scene_lock_is_reentrant(): assert scene._vtk_lock is not None +# =========================================================================== +# Save/load: the registry is the server-side authority for per-part state +# +# The save-path assertions (what get_state hands to the persist mapper) and +# the load-path assertions (the registry write, and the pipeline applies) are +# separate tests: either half can silently do nothing while the other works. +# Every expected value below is a hand-written literal. +# =========================================================================== + +FRONTEND_DATASET_ID = 4242 +VARIABLE_ID = "POINT::pressure::1" + +# The point array the `array_dataset` fixture carries, described the way the +# persisted variable entry describes it. Component 0's range and the +# magnitude range are deliberately different so a test can tell which branch +# the restore took. +PRESSURE_MAGNITUDE_RANGE = (0.0, 49.0) +PRESSURE_COMPONENT_RANGE = (10.0, 20.0) + + +def _pressure_variable() -> VisorVariable: + """Server-side per-part metadata for the fixture's "pressure" point array.""" + return VisorVariable( + index=0, + type=VisorVtkVariableType.POINT, + name="pressure", + num_components=1, + num_points=50, + ranges=[PRESSURE_COMPONENT_RANGE], + magnitude_range=PRESSURE_MAGNITUDE_RANGE, + ) + + +def _variable_state( + array_name="pressure", + var_type=VisorVtkVariableType.POINT, + num_components=1, + magnitude_range=PRESSURE_MAGNITUDE_RANGE, + ranges=(PRESSURE_COMPONENT_RANGE,), +) -> VisorVariableState: + """A persisted variable entry, as I6a stores it.""" + return VisorVariableState( + id=VARIABLE_ID, + array_name=array_name, + type=var_type, + num_components=num_components, + magnitude_range=magnitude_range, + ranges=list(ranges), + ) + + +def _seed_part_variables(registry, variables, part_id=NODE_ID, dataset_id=1): + """Give the registry's dataset per-part variable metadata for *part_id*.""" + registry.datasets[dataset_id].list_variables.return_value = [ + VisorPartVariables(part_id=part_id, part_name="part", variables=list(variables)) + ] + + +def _runtime_state(part_states, variable_states=None, dataset_id=1): + """A real RuntimeAppState carrying the given per-part records.""" + return RuntimeAppState.from_components( + dark_mode=False, + unit="m", + dataset_states={ + dataset_id: RuntimeDatasetState(id=dataset_id, part_states=part_states) + }, + variable_states=variable_states or {}, + ) + + +def _frontend_state(): + """What the browser returns: per-part values for a dataset id of its own. + + Distinguishable on both axes — a dataset id the registry does not have and + an opacity the registry never held — so a test can tell registry-sourced + output from frontend-sourced output. + """ + return RuntimeAppState.from_components( + dark_mode=False, + unit="m", + dataset_states={ + FRONTEND_DATASET_ID: RuntimeDatasetState( + id=FRONTEND_DATASET_ID, + part_states={NODE_ID: RuntimePartProperties(id=NODE_ID, opacity=0.99)}, + ) + }, + ) + + +def _capture_persist_input(scene, frontend_state): + """Wire get_state to *frontend_state* and capture what the mapper receives.""" + captured = {} + + async def _get_runtime_state_async(timeout): + return frontend_state + + def _runtime_to_persisted(runtime_state): + captured["runtime_state"] = runtime_state + return MagicMock(name="persisted") + + scene._get_runtime_state_async = _get_runtime_state_async + scene._state_mapper = MagicMock(name="state_mapper") + scene._state_mapper.runtime_to_persisted.side_effect = _runtime_to_persisted + return captured + + +def _apply(scene, runtime_state): + """Run apply_state with the mapper stubbed to return *runtime_state*.""" + scene._state_mapper = MagicMock(name="state_mapper") + scene._state_mapper.persisted_to_runtime.return_value = runtime_state + return scene.apply_state(MagicMock(name="persisted_state")) + + +class _DepthRecordingRegistry(VisorDatasetRegistry): + """Registry that records the scene's lock depth when it is read.""" + + def __init__(self, scene): + super().__init__() + self._scene = scene + self.depth_at_read = None + + @property + def runtime_state_dict(self): + self.depth_at_read = self._scene._vtk_lock.depth + return VisorDatasetRegistry.runtime_state_dict.fget(self) + + +@pytest.fixture +def second_pipeline(renderer, array_dataset): + """A second real VtkNodePipeline, registered under SECOND_NODE_ID.""" + pipe = VtkNodePipeline.from_dataset(array_dataset) + renderer._pipelines[SECOND_NODE_ID] = pipe + return pipe + + +# --------------------------------------------------------------------------- +# Save path +# --------------------------------------------------------------------------- + +def test_get_state_sources_dataset_states_from_the_registry(scene, registry): + """The persist mapper is handed the registry's per-part state.""" + registry.set_part_opacity(NODE_ID, 0.25) + captured = _capture_persist_input(scene, _frontend_state()) + + asyncio.run(scene.get_state(timeout=1.0)) + + mapped = captured["runtime_state"].scene.dataset_states + assert list(mapped) == [1] + assert mapped[1].part_states[NODE_ID].opacity == 0.25 + + +def test_get_state_discards_the_frontend_dataset_states(scene, registry): + """The browser's per-part state does not survive into the persisted state.""" + registry.set_part_opacity(NODE_ID, 0.25) + captured = _capture_persist_input(scene, _frontend_state()) + + asyncio.run(scene.get_state(timeout=1.0)) + + mapped = captured["runtime_state"].scene.dataset_states + assert FRONTEND_DATASET_ID not in mapped + assert all( + part.opacity != 0.99 + for dataset_state in mapped.values() + for part in dataset_state.part_states.values() + ) + + +def test_get_state_reads_the_registry_under_the_lock(scene): + """The lock is *held* at the moment the registry is read, not merely taken.""" + scene._vtk_lock = _LockSpy() + spy_registry = _DepthRecordingRegistry(scene) + spy_registry.datasets = {1: _make_part_dataset(1, [NODE_ID])} + scene._dataset_registry = spy_registry + _capture_persist_input(scene, _frontend_state()) + + asyncio.run(scene.get_state(timeout=1.0)) + + assert spy_registry.depth_at_read >= 1 + assert scene._vtk_lock.depth == 0 + assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count + + +def test_get_state_snapshots_the_registry_rather_than_referencing_it(scene, registry): + """A write landing after the read does not change what was captured.""" + registry.set_part_opacity(NODE_ID, 0.25) + live_state = registry.datasets[1].state + captured = _capture_persist_input(scene, _frontend_state()) + + asyncio.run(scene.get_state(timeout=1.0)) + registry.set_part_opacity(NODE_ID, 0.75) + + snapshot = captured["runtime_state"].scene.dataset_states[1] + assert snapshot is not live_state + assert snapshot.part_states[NODE_ID] is not live_state.part_states[NODE_ID] + assert snapshot.part_states[NODE_ID].opacity == 0.25 + + +def test_apply_state_pushes_a_json_encodable_runtime_state(scene, registry): + """What the bridge is handed survives JSON encoding. + + An enum-typed field reaching the transport unserialised failed the push + with wslink -32002 at I6a while every gate passed, because nothing in the + suite exercised the push payload. This asserts the payload of the object + apply_state hands to the renderer-specific step, not a model in isolation. + """ + pushed = {} + scene._apply_runtime_state_to_render = lambda state: pushed.update(state=state) + + _apply( + scene, + _runtime_state( + {NODE_ID: RuntimePartProperties(id=NODE_ID, opacity=0.25, spectrum_id=VARIABLE_ID)}, + variable_states={VARIABLE_ID: _variable_state()}, + ), + ) + + encoded = json.dumps(pushed["state"].model_dump(by_alias=True)) + assert '"POINT"' in encoded + + +# --------------------------------------------------------------------------- +# Load path — store half and apply half, separately +# --------------------------------------------------------------------------- + +def test_apply_state_populates_the_registry_from_the_persisted_state(scene, registry): + """Store half: the registry carries the restored record.""" + _apply( + scene, + _runtime_state( + {NODE_ID: RuntimePartProperties(id=NODE_ID, opacity=0.25, visible=False)} + ), + ) + + record = registry.get_part_state(NODE_ID) + assert record.opacity == 0.25 + assert record.visible is False + + +def test_apply_state_applies_every_part_to_the_pipeline( + scene, registry, pipeline, second_pipeline +): + """Apply half: both parts reach their own VTK objects.""" + registry.datasets[1] = _make_part_dataset(1, [NODE_ID, SECOND_NODE_ID]) + pipeline.actor.SetVisibility(1) + second_pipeline.actor.GetProperty().SetOpacity(1.0) + + _apply( + scene, + _runtime_state( + { + NODE_ID: RuntimePartProperties(id=NODE_ID, visible=False), + SECOND_NODE_ID: RuntimePartProperties(id=SECOND_NODE_ID, opacity=0.25), + } + ), + ) + + assert pipeline.actor.GetVisibility() == 0 + assert second_pipeline.actor.GetProperty().GetOpacity() == pytest.approx(0.25) + + +def test_apply_state_does_not_flush_after_the_bridge_call(scene, registry): + """Exactly one flush, and it is ordered after the bridge call returns.""" + order = [] + scene._apply_runtime_state_to_render = lambda state: order.append("bridge") + scene._renderer.flush_wasm_state = lambda: order.append("flush") + + _apply(scene, _runtime_state({NODE_ID: RuntimePartProperties(id=NODE_ID, opacity=0.25)})) + + assert order == ["bridge"] + + +def test_restore_part_states_holds_the_lock(scene, registry, pipeline): + """The lock is held at both halves inside the restore helper itself.""" + scene._vtk_lock = _LockSpy() + observed = {} + real_replace = registry.replace_part_states + + def _replace(dataset_states): + observed["store_depth"] = scene._vtk_lock.depth + return real_replace(dataset_states) + + def _apply_opacity(node_id, opacity): + observed["apply_depth"] = scene._vtk_lock.depth + + registry.replace_part_states = _replace + scene._renderer.apply_opacity = _apply_opacity + + _apply(scene, _runtime_state({NODE_ID: RuntimePartProperties(id=NODE_ID, opacity=0.25)})) + + assert observed["store_depth"] >= 1 + assert observed["apply_depth"] >= 1 + assert scene._vtk_lock.depth == 0 + + +def test_apply_state_unregistered_dataset_id_is_a_logged_skip(scene, pipeline): + """A dataset the registry does not have is skipped explicitly, not silently.""" + pipeline.actor.SetVisibility(1) + + with patch("ansys.visor.viewer.vtk.scene.base.logger") as mock_logger: + _apply( + scene, + _runtime_state( + {NODE_ID: RuntimePartProperties(id=NODE_ID, visible=False)}, + dataset_id=FRONTEND_DATASET_ID, + ), + ) + + assert mock_logger.warning.call_count == 1 + assert pipeline.actor.GetVisibility() == 1 + + +def test_apply_state_unknown_node_is_a_logged_no_op(scene, registry, pipeline): + """A part with no pipeline is a logged no-op, not a raise.""" + pipeline.actor.SetVisibility(1) + + with patch("ansys.visor.viewer.renderer.local_renderer.logger") as mock_logger: + result = _apply( + scene, + _runtime_state( + {UNKNOWN_NODE_ID: RuntimePartProperties(id=UNKNOWN_NODE_ID, visible=False)} + ), + ) + + assert result is None + assert mock_logger.debug.call_count >= 1 + assert pipeline.actor.GetVisibility() == 1 + + +def test_apply_state_short_diffuse_color_is_a_logged_no_op(scene, registry, pipeline): + """A malformed colour leaves the VTK object alone; the record keeps it.""" + pipeline.actor.GetProperty().SetDiffuseColor(0.1, 0.2, 0.3) + + with patch("ansys.visor.viewer.vtk.scene.base.logger") as mock_logger: + _apply( + scene, + _runtime_state( + {NODE_ID: RuntimePartProperties(id=NODE_ID, diffuse_rgb=[1.0, 0.0])} + ), + ) + + assert mock_logger.warning.call_count == 1 + assert pipeline.actor.GetProperty().GetDiffuseColor() == pytest.approx((0.1, 0.2, 0.3)) + assert registry.get_part_state(NODE_ID).diffuse_rgb == [1.0, 0.0] + + +# --------------------------------------------------------------------------- +# Load path — the colour-variable branch +# --------------------------------------------------------------------------- + +def _color_variable_state(component, **variable_kwargs): + """A part coloured by the fixture's point array, at *component*.""" + return _runtime_state( + { + NODE_ID: RuntimePartProperties( + id=NODE_ID, spectrum_id=VARIABLE_ID, spectrum_component=component + ) + }, + variable_states={VARIABLE_ID: _variable_state(**variable_kwargs)}, + ) + + +def _seed_unconfigured_mapper(pipeline): + """Distinctive, non-default mapper state so a no-op is visible as one.""" + pipeline.mapper.SetScalarVisibility(0) + pipeline.mapper.SetScalarRange(11.0, 22.0) + + +def test_apply_state_restores_the_magnitude_range_when_component_is_minus_one( + scene, registry, pipeline +): + """A stored component of -1 reads magnitude_range, not ranges[0].""" + _seed_part_variables(registry, [_pressure_variable()]) + + _apply(scene, _color_variable_state(-1)) + + assert pipeline.mapper.GetArrayName() == "pressure" + assert pipeline.mapper.GetScalarRange() == pytest.approx((0.0, 49.0)) + + +def test_apply_state_restores_the_per_component_range(scene, registry, pipeline): + """A stored component of 0 reads ranges[0], not magnitude_range.""" + _seed_part_variables(registry, [_pressure_variable()]) + + _apply(scene, _color_variable_state(0)) + + assert pipeline.mapper.GetArrayName() == "pressure" + assert pipeline.mapper.GetScalarRange() == pytest.approx((10.0, 20.0)) + + +def test_apply_state_negative_component_other_than_minus_one_is_a_logged_no_op( + scene, registry, pipeline +): + """-2 is neither the magnitude sentinel nor an index: no fallthrough.""" + _seed_part_variables(registry, [_pressure_variable()]) + _seed_unconfigured_mapper(pipeline) + + with patch("ansys.visor.viewer.vtk.scene.base.logger") as mock_logger: + _apply(scene, _color_variable_state(-2)) + + assert mock_logger.warning.call_count == 1 + assert pipeline.mapper.GetScalarVisibility() == 0 + assert pipeline.mapper.GetScalarRange() == pytest.approx((11.0, 22.0)) + + +def test_apply_state_component_beyond_the_stored_ranges_is_a_logged_no_op( + scene, registry, pipeline +): + """An index past the stored ranges applies nothing.""" + _seed_part_variables(registry, [_pressure_variable()]) + _seed_unconfigured_mapper(pipeline) + + with patch("ansys.visor.viewer.vtk.scene.base.logger") as mock_logger: + _apply(scene, _color_variable_state(3)) + + assert mock_logger.warning.call_count == 1 + assert pipeline.mapper.GetScalarVisibility() == 0 + assert pipeline.mapper.GetScalarRange() == pytest.approx((11.0, 22.0)) + + +def test_apply_state_absent_range_is_a_logged_no_op(scene, registry, pipeline): + """A variable entry with no magnitude range applies nothing.""" + _seed_part_variables(registry, [_pressure_variable()]) + _seed_unconfigured_mapper(pipeline) + + with patch("ansys.visor.viewer.vtk.scene.base.logger") as mock_logger: + _apply(scene, _color_variable_state(-1, magnitude_range=None)) + + assert mock_logger.warning.call_count == 1 + assert pipeline.mapper.GetScalarVisibility() == 0 + assert pipeline.mapper.GetScalarRange() == pytest.approx((11.0, 22.0)) + + +def test_apply_state_unknown_variable_identifier_is_a_logged_no_op( + scene, registry, pipeline +): + """A stored identifier with no variable entry applies nothing.""" + _seed_part_variables(registry, [_pressure_variable()]) + _seed_unconfigured_mapper(pipeline) + runtime = _runtime_state( + { + NODE_ID: RuntimePartProperties( + id=NODE_ID, spectrum_id=VARIABLE_ID, spectrum_component=0 + ) + }, + variable_states={}, + ) + + with patch("ansys.visor.viewer.vtk.scene.base.logger") as mock_logger: + _apply(scene, runtime) + + assert mock_logger.warning.call_count == 1 + assert pipeline.mapper.GetScalarVisibility() == 0 + + +def test_apply_state_unknown_array_name_is_a_logged_no_op(scene, registry, pipeline): + """An array the part does not carry applies nothing.""" + _seed_part_variables(registry, [_pressure_variable()]) + _seed_unconfigured_mapper(pipeline) + + with patch("ansys.visor.viewer.vtk.scene.base.logger") as mock_logger: + _apply(scene, _color_variable_state(0, array_name="no_such_array")) + + assert mock_logger.warning.call_count == 1 + assert pipeline.mapper.GetScalarVisibility() == 0 + + +def test_apply_state_array_width_mismatch_is_a_logged_no_op(scene, registry, pipeline): + """Same name and association, different width: a different quantity.""" + _seed_part_variables(registry, [_pressure_variable()]) + _seed_unconfigured_mapper(pipeline) + + with patch("ansys.visor.viewer.vtk.scene.base.logger") as mock_logger: + _apply( + scene, + _color_variable_state( + 0, num_components=3, ranges=((10.0, 20.0), (0.0, 1.0), (0.0, 2.0)) + ), + ) + + assert mock_logger.warning.call_count == 1 + assert pipeline.mapper.GetScalarVisibility() == 0 + + +def test_apply_state_clears_the_color_variable_when_none_is_stored( + scene, registry, pipeline +): + """No stored identifier and no stored component: the clear branch.""" + pipeline.mapper.SetScalarVisibility(1) + + _apply(scene, _runtime_state({NODE_ID: RuntimePartProperties(id=NODE_ID)})) + + assert pipeline.mapper.GetScalarVisibility() == 0 + + +def test_apply_state_variable_id_without_component_is_a_logged_no_op( + scene, registry, pipeline +): + """Half a compound value: an identifier with no component.""" + _seed_part_variables(registry, [_pressure_variable()]) + _seed_unconfigured_mapper(pipeline) + runtime = _runtime_state( + {NODE_ID: RuntimePartProperties(id=NODE_ID, spectrum_id=VARIABLE_ID)}, + variable_states={VARIABLE_ID: _variable_state()}, + ) + + with patch("ansys.visor.viewer.vtk.scene.base.logger") as mock_logger: + _apply(scene, runtime) + + assert mock_logger.warning.call_count == 1 + assert pipeline.mapper.GetScalarVisibility() == 0 + + +def test_apply_state_component_without_variable_id_does_not_clear( + scene, registry, pipeline +): + """The mirror half: a component with no identifier must not fall through.""" + pipeline.mapper.SetScalarVisibility(1) + runtime = _runtime_state( + {NODE_ID: RuntimePartProperties(id=NODE_ID, spectrum_component=0)}, + variable_states={VARIABLE_ID: _variable_state()}, + ) + + with patch("ansys.visor.viewer.vtk.scene.base.logger") as mock_logger: + _apply(scene, runtime) + + assert mock_logger.warning.call_count == 1 + assert pipeline.mapper.GetScalarVisibility() == 1 + + diff --git a/tests/unit/vtk/scene/test_visor_state_mapper.py b/tests/unit/vtk/scene/test_visor_state_mapper.py index 89a07437..f16587e3 100644 --- a/tests/unit/vtk/scene/test_visor_state_mapper.py +++ b/tests/unit/vtk/scene/test_visor_state_mapper.py @@ -11,6 +11,7 @@ class FakeDataset: def __init__(self, name, did): self.name = name self.id = did + self.state = "untouched-sentinel" def runtime_to_persisted_state(self, state): return {"converted": state} @@ -243,3 +244,47 @@ def test_persisted_to_runtime_handles_none_variable_states(monkeypatch): mapper.persisted_to_runtime(state) assert captured["variable_states"] == {} + + +def test_persisted_to_runtime_does_not_write_back_to_dataset_state(monkeypatch): + """The mapper is not, and never has been, the registry-population path. + + It builds runtime dataset states and returns them; it never assigns them + onto ``VisorDataset.state``. That is the gap + ``VisorSceneBase._restore_part_states_from_runtime`` closes, so the + negative is pinned here rather than assumed. + """ + + dataset = FakeDataset(name="dsA", did=10) + registry = FakeRegistry(by_name={"dsA": dataset}) + + monkeypatch.setattr( + "ansys.visor.viewer.vtk.scene.visor_state_mapper.RuntimeAppState.from_components", + lambda **kwargs: kwargs, + ) + + state = type("Persisted", (), { + "ui": type("UI", (), {"dark_theme": False})(), + "scene": type("Scene", (), { + "unit": "m", + "camera": None, + "variable_states": {}, + "dataset_states": { + "dsA": type("DS", (), {"parts": {"p": 9}})() + }, + "cross_section": None, + "orthographic_enabled": False, + "cross_section_enabled": False, + "edges_enabled": False, + "bounding_box_enabled": False, + })(), + })() + + mapper = VisorStateMapper(registry) + result = mapper.persisted_to_runtime(state) + + # The converted state is returned ... + assert result["dataset_states"] == {10: {"rt": {"p": 9}}} + # ... and is not written back onto the dataset the registry holds. + assert dataset.state == "untouched-sentinel" + From 92b19b56d5097e964a2dcfed59d39a0b4eca1d97 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:21:31 +0000 Subject: [PATCH 23/28] chore: adding changelog file 54.added.md [dependabot-skip] --- doc/changelog.d/54.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/54.added.md diff --git a/doc/changelog.d/54.added.md b/doc/changelog.d/54.added.md new file mode 100644 index 00000000..11cc8ac9 --- /dev/null +++ b/doc/changelog.d/54.added.md @@ -0,0 +1 @@ +Remote rendering 3.1e - source per-part state from registry on save and restore it on load From a4b5adec4b3b4eb61f32e04e575df929f39128d4 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Mon, 31 Aug 2026 08:27:42 -0700 Subject: [PATCH 24/28] bump saved state schema version to 1.1 in regression test --- tests/e2e/regressions/test_save_load_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/regressions/test_save_load_state.py b/tests/e2e/regressions/test_save_load_state.py index e180c191..4f8fd822 100644 --- a/tests/e2e/regressions/test_save_load_state.py +++ b/tests/e2e/regressions/test_save_load_state.py @@ -125,7 +125,7 @@ def test_save_load_state_file_content_valid(self, visor_server, page, tmp_path): content = json.loads(state_file.read_text()) # Verify top-level structure - assert content.get("version") == "1.0", "State version should be 1.0" + assert content.get("version") == "1.1", "State version should be 1.1" assert "scene" in content, "State should contain scene data" assert "dataset_states" in content["scene"], "Scene should contain dataset_states" assert len(content["scene"]["dataset_states"]) > 0, ( From c06f7081560b8ec7d6bc69b90f2bade633d0c43e Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Tue, 1 Sep 2026 14:31:49 -0700 Subject: [PATCH 25/28] add field validator to derive new variable fields from id, version back to 1.0 --- .../models/persist/persisted_viewer_state.py | 2 +- .../persist/scene/persisted_scene_state.py | 149 ++++++- tests/e2e/regressions/test_save_load_state.py | 2 +- .../unit/models/test_persisted_scene_state.py | 382 ++++++++++++++++++ .../models/test_visor_save_state_response.py | 61 +++ 5 files changed, 592 insertions(+), 4 deletions(-) create mode 100644 tests/unit/models/test_persisted_scene_state.py diff --git a/src/ansys/visor/viewer/models/persist/persisted_viewer_state.py b/src/ansys/visor/viewer/models/persist/persisted_viewer_state.py index be744036..16161113 100644 --- a/src/ansys/visor/viewer/models/persist/persisted_viewer_state.py +++ b/src/ansys/visor/viewer/models/persist/persisted_viewer_state.py @@ -25,7 +25,7 @@ class PersistedViewerStateV1(BaseModel): """ model_config = ConfigDict(arbitrary_types_allowed=True) - version: Literal["1.1"] = Field(default="1.1") + version: Literal["1.0"] = Field(default="1.0") ui: VisorUIState = Field(default_factory=VisorUIState) scene: PersistedSceneState = Field(default_factory=PersistedSceneState) diff --git a/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py b/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py index be12ddd3..728afff4 100644 --- a/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py +++ b/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py @@ -1,14 +1,84 @@ """Model for persisted scene state (cross-session).""" -from typing import Dict +from typing import Any, Dict, Mapping -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState from ansys.visor.viewer.models.common.visor_cross_section_state import VisorCrossSectionState from ansys.visor.viewer.models.common.visor_variable_state import VisorVariableState from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState +#: Separator used by the client when it mints a variable identifier. See +#: ``VisorSpectrumManager.tryAddSpectrumInfo``, which builds the identifier as +#: ``` `${type}::${name}::${numComponents}` ```. +_IDENTIFIER_SEPARATOR = "::" + +#: Number of fields the client packs into an identifier. +_IDENTIFIER_FIELD_COUNT = 3 + + +def derive_variable_fields_from_identifier(identifier: Any) -> Dict[str, Any] | None: + """Recover ``type``, ``array_name`` and ``num_components`` from a variable identifier. + + The client mints the identifier as ``::::`` + (``VisorSpectrumManager.tsx``), so the three identifying properties are + recoverable from the identifier alone. This function is the permanent, + named inverse of that minting, and is the *only* place the identifier is + taken apart. + + The split is anchored at both ends and driven by the field count, **not** by + a left-to-right split on the separator: the array name is the middle field + and is user data, so it may itself contain ``::``. Two cuts: + + 1. one cut from the right yields the trailing component count, and + 2. one cut from the left of the remainder yields the leading type, + + leaving everything between them as the array name, verbatim and with any + embedded separators intact. ``"POINT::stress::yy::3"`` therefore yields the + array name ``"stress::yy"``, where a left-to-right split would have + truncated it to ``"stress"``. + + Args: + identifier: The persisted variable identifier. + + Returns: + A mapping of the three derived fields, or ``None`` when the identifier + does not spell out three usable fields. ``None`` means "cannot be + derived", never "derived to a default": callers must leave the entry + untouched so that ordinary validation reports the missing fields, rather + than filling in a guess. Structural success is not semantic success, so + an empty type or array name, or a non-integer component count, is + refused even though the separators are all present. + """ + if not isinstance(identifier, str): + return None + + head, separator, trailing = identifier.rpartition(_IDENTIFIER_SEPARATOR) + if not separator: + return None + + variable_type, separator, array_name = head.partition(_IDENTIFIER_SEPARATOR) + if not separator: + return None + + # Structurally there are now three fields. Refuse the ones that are not + # semantically usable rather than admitting a state that names an array + # which cannot exist. + if not variable_type or not array_name: + return None + + try: + num_components = int(trailing) + except (TypeError, ValueError): + return None + + return { + "type": variable_type, + "array_name": array_name, + "num_components": num_components, + } + class PersistedSceneState(BaseModel): """Persisted (cross-session) scene state. @@ -32,3 +102,78 @@ class PersistedSceneState(BaseModel): dataset_states: Dict[str, "PersistedDatasetState"] = Field(default_factory=dict) variable_states: Dict[str, "VisorVariableState"] = Field(default_factory=dict) model_config = ConfigDict(arbitrary_types_allowed=True) + + @field_validator("variable_states", mode="before") + @classmethod + def _derive_missing_variable_identity_fields(cls, value: Any) -> Any: + """Fill absent identity fields on *read*, from the variable identifier. + + ``VisorVariableState.array_name``, ``.type`` and ``.num_components`` are + required and stay required: they are shared with + ``RuntimeSceneState.spectrum_states``, so relaxing them on the model + would also relax the save-path coercion in + ``VisorSaveStateResponse._coerce_app_state``. The tolerance for older + save files therefore lives here, on the container, where it applies to + the ingest boundary only. + + Save files written before those fields existed carry the same three + properties inside the identifier, so they are recovered by + :func:`derive_variable_fields_from_identifier` before coercion. Nothing + downstream of this validator ever sees ``None``: by the time an entry + becomes a ``VisorVariableState`` the fields are populated or validation + has already failed. + + Values that are already ``VisorVariableState`` instances -- which is + every value on the *write* path, where the mapper passes the runtime + dictionary through wholesale -- are handed back untouched. Values that + are not mappings are handed back untouched too, so that Pydantic raises + a well-formed ``ValidationError`` rather than this validator raising + ``TypeError`` while indexing. + """ + if not isinstance(value, Mapping): + return value + + out: Dict[Any, Any] = {} + for key, entry in value.items(): + out[key] = cls._derive_missing_identity_fields_for_entry(key, entry) + return out + + @staticmethod + def _derive_missing_identity_fields_for_entry(key: Any, entry: Any) -> Any: + """Return *entry* with any absent identity fields derived from its identifier.""" + # Already-built models (the write path) and non-mappings are not ours to touch. + if isinstance(entry, VisorVariableState) or not isinstance(entry, Mapping): + return entry + + # ``populate_by_name`` is set on VisorVariableState, so either spelling + # counts as present. A present-but-null value counts as present: only + # genuine absence is tolerated, so an explicit null still fails. + missing = [ + field + for field, alias in ( + ("array_name", "arrayName"), + ("type", "type"), + ("num_components", "numComponents"), + ) + if field not in entry and alias not in entry + ] + if not missing: + return entry + + # The entry carries the identifier as a field of its own; the mapping key + # is a copy of it. Prefer the entry's own value, fall back to the key. + identifier = entry.get("id") + if not isinstance(identifier, str) or not identifier: + identifier = key + + derived = derive_variable_fields_from_identifier(identifier) + if derived is None: + # Undecipherable: leave it alone and let ordinary validation report + # the missing fields. Never fill with a guess. + return entry + + filled = dict(entry) + for field in missing: + filled[field] = derived[field] + return filled + diff --git a/tests/e2e/regressions/test_save_load_state.py b/tests/e2e/regressions/test_save_load_state.py index 4f8fd822..e180c191 100644 --- a/tests/e2e/regressions/test_save_load_state.py +++ b/tests/e2e/regressions/test_save_load_state.py @@ -125,7 +125,7 @@ def test_save_load_state_file_content_valid(self, visor_server, page, tmp_path): content = json.loads(state_file.read_text()) # Verify top-level structure - assert content.get("version") == "1.1", "State version should be 1.1" + assert content.get("version") == "1.0", "State version should be 1.0" assert "scene" in content, "State should contain scene data" assert "dataset_states" in content["scene"], "Scene should contain dataset_states" assert len(content["scene"]["dataset_states"]) > 0, ( diff --git a/tests/unit/models/test_persisted_scene_state.py b/tests/unit/models/test_persisted_scene_state.py new file mode 100644 index 00000000..9e9e2190 --- /dev/null +++ b/tests/unit/models/test_persisted_scene_state.py @@ -0,0 +1,382 @@ +"""Unit tests for the persisted-scene-state identity backfill. + +Two things are under test: + +* :func:`derive_variable_fields_from_identifier`, exercised directly against + hand-written literal identifiers, and +* the ``variable_states`` before-validator, exercised through + ``PersistedViewerStateV1.model_validate_json`` on literal 1.0-shaped + documents. + +Neither the parser's own separator constant nor any client-shaped helper is +imported or reused here: every identifier below is typed out by hand, so a test +cannot agree with the parser by construction. +""" + +import json + +import pytest +from pydantic import ValidationError + +from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType +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.persist.scene.persisted_scene_state import ( + PersistedSceneState, + derive_variable_fields_from_identifier, +) + + +# ------------------------------------------------------------------ +# Helpers +# ------------------------------------------------------------------ + +def _document(variable_states: dict, version: str = "1.0") -> str: + """A save-file document in the 1.0 shape, as JSON text. + + Only the envelope is assembled here; every identifier and every entry is + written out by hand at the call site. + """ + return json.dumps({"version": version, "scene": {"variable_states": variable_states}}) + + +def _entry(identifier: str) -> dict: + """A variable-state entry as saved before the identity fields existed. + + ``id``, ``magnitudeRange`` and ``ranges`` are all a 1.0 entry ever carried. + """ + return { + "id": identifier, + "magnitudeRange": [0.0, 1.0], + "ranges": [[0.0, 1.0]], + } + + +def _load(document: str) -> PersistedViewerStateV1: + return PersistedViewerStateV1.model_validate_json(document) + + +def _load_one(identifier: str, entry: dict | None = None): + """Load a one-entry 1.0 document keyed by *identifier* and return the parsed entry.""" + entry = _entry(identifier) if entry is None else entry + return _load(_document({identifier: entry})).scene.variable_states[identifier] + + +# ------------------------------------------------------------------ +# The derivation, tested directly against hand-written literals +# ------------------------------------------------------------------ + +def test_derives_the_three_fields_from_a_plain_identifier(): + """A plain identifier yields type, array name and component count.""" + assert derive_variable_fields_from_identifier("POINT::pressure::1") == { + "type": "POINT", + "array_name": "pressure", + "num_components": 1, + } + + +def test_derives_when_the_array_name_contains_the_separator(): + """Array names are user data and may contain the separator character. + + A left-to-right split on the separator would truncate ``stress::yy`` to + ``stress``; splitting from the right by field count does not. + """ + assert derive_variable_fields_from_identifier("POINT::stress::yy::3") == { + "type": "POINT", + "array_name": "stress::yy", + "num_components": 3, + } + + +def test_derives_when_the_array_name_contains_several_separators(): + """More than one embedded separator is still kept verbatim.""" + derived = derive_variable_fields_from_identifier("CELL::a::b::c::2") + + assert derived["array_name"] == "a::b::c" + assert derived["type"] == "CELL" + assert derived["num_components"] == 2 + + +def test_same_array_name_different_component_counts_stay_distinct(): + """Two variables sharing an array name are told apart by component count.""" + scalar = derive_variable_fields_from_identifier("POINT::velocity::1") + vector = derive_variable_fields_from_identifier("POINT::velocity::3") + + assert scalar["array_name"] == vector["array_name"] == "velocity" + assert scalar["num_components"] == 1 + assert vector["num_components"] == 3 + assert scalar != vector + + +def test_cell_association_is_derived(): + """The type field is read from the identifier, not assumed to be POINT.""" + assert derive_variable_fields_from_identifier("CELL::volume::1")["type"] == "CELL" + + +@pytest.mark.parametrize("identifier", ["pressure", "POINT::pressure"]) +def test_returns_none_when_the_identifier_has_too_few_parts(identifier): + """Fewer than three fields cannot be derived, and are not guessed at.""" + assert derive_variable_fields_from_identifier(identifier) is None + + +def test_returns_none_when_the_component_count_is_not_an_integer(): + """A non-integer trailing field is refused rather than coerced.""" + assert derive_variable_fields_from_identifier("POINT::pressure::many") is None + + +@pytest.mark.parametrize("identifier", ["POINT::::1", "::pressure::1"]) +def test_returns_none_when_a_field_is_structurally_present_but_empty(identifier): + """Structural success is not semantic success. + + ``POINT::::1`` has three structural parts but an empty array name, and + ``::pressure::1`` an empty type. Filling either would produce a variable + state naming something that cannot exist, which then fails silently + downstream as an unresolvable-array no-op. Both are refused so that the + failure is a loud missing-field error instead. + """ + assert derive_variable_fields_from_identifier(identifier) is None + + +# ------------------------------------------------------------------ +# The fill, through literal 1.0-shaped documents +# ------------------------------------------------------------------ + +def test_v1_0_document_omitting_the_three_fields_parses_and_derives(): + """An old save file loads, and the absent fields are derived on ingest.""" + entry = _load_one("POINT::pressure::1") + + assert entry.array_name == "pressure" + assert entry.type is VisorVtkVariableType.POINT + assert entry.num_components == 1 + # The rest of the entry survives untouched. + assert entry.magnitude_range == (0.0, 1.0) + assert entry.ranges == [(0.0, 1.0)] + + +def test_v1_0_document_with_an_array_name_containing_the_separator_parses(): + """The embedded-separator case survives the whole ingest path, not just the split.""" + entry = _load_one("POINT::stress::yy::3") + + assert entry.array_name == "stress::yy" + assert entry.num_components == 3 + + +def test_v1_0_document_keeps_two_variables_sharing_an_array_name_distinct(): + """Two entries differing only in component count remain two entries.""" + document = _document( + { + "POINT::velocity::1": _entry("POINT::velocity::1"), + "POINT::velocity::3": _entry("POINT::velocity::3"), + } + ) + + variable_states = _load(document).scene.variable_states + + assert len(variable_states) == 2 + assert variable_states["POINT::velocity::1"].num_components == 1 + assert variable_states["POINT::velocity::3"].num_components == 3 + assert ( + variable_states["POINT::velocity::1"].array_name + == variable_states["POINT::velocity::3"].array_name + == "velocity" + ) + + +# ------------------------------------------------------------------ +# What the fill must not do +# ------------------------------------------------------------------ + +def test_present_fields_are_not_overwritten_by_derivation(): + """An explicit value wins over anything the identifier would imply.""" + entry = _entry("POINT::pressure::1") + entry["arrayName"] = "explicitly_named" + entry["type"] = "CELL" + entry["numComponents"] = 9 + + stored = _load_one("POINT::pressure::1", entry) + + assert stored.array_name == "explicitly_named" + assert stored.type is VisorVtkVariableType.CELL + assert stored.num_components == 9 + + +def test_camel_case_aliases_are_recognised_as_present(): + """A field supplied only under its wire alias is not treated as absent.""" + entry = _entry("POINT::pressure::1") + entry["arrayName"] = "supplied_by_alias" + + stored = _load_one("POINT::pressure::1", entry) + + assert stored.array_name == "supplied_by_alias" + # The two that really were absent are still derived. + assert stored.type is VisorVtkVariableType.POINT + assert stored.num_components == 1 + + +def test_snake_case_names_are_recognised_as_present(): + """The same holds for the Python field names, since populate_by_name is set.""" + entry = _entry("POINT::pressure::1") + entry["num_components"] = 7 + + stored = _load_one("POINT::pressure::1", entry) + + assert stored.num_components == 7 + assert stored.array_name == "pressure" + + +# ------------------------------------------------------------------ +# The write path +# ------------------------------------------------------------------ + +def test_already_built_instances_pass_through_untouched(): + """The write path hands the validator models, not dicts, and keeps them. + + The mapper passes the runtime dictionary through wholesale, so the values + arriving here are the very objects the caller built. They must not be + rebuilt, copied, or re-derived from their identifiers. + """ + built = VisorVariableState( + id="POINT::pressure::1", + array_name="pressure", + type=VisorVtkVariableType.POINT, + num_components=1, + ) + + scene = PersistedSceneState(variable_states={"POINT::pressure::1": built}) + + assert scene.variable_states["POINT::pressure::1"] is built + + +def test_instances_are_not_re_derived_from_a_disagreeing_identifier(): + """A model whose fields disagree with its id keeps its fields.""" + built = VisorVariableState( + id="POINT::pressure::1", + array_name="something_else", + type=VisorVtkVariableType.CELL, + num_components=4, + ) + + scene = PersistedSceneState(variable_states={"POINT::pressure::1": built}) + + stored = scene.variable_states["POINT::pressure::1"] + assert stored.array_name == "something_else" + assert stored.type is VisorVtkVariableType.CELL + assert stored.num_components == 4 + + +# ------------------------------------------------------------------ +# Failures stay loud +# ------------------------------------------------------------------ + +def test_unparseable_identifier_raises_validation_error(): + """An identifier that cannot be derived from fails, rather than filling a guess.""" + document = _document({"POINT::pressure": _entry("POINT::pressure")}) + + with pytest.raises(ValidationError) as excinfo: + _load(document) + + # Pydantic reports missing fields under the spelling it validated by, which + # for a document read off the wire is the alias. + reported = {error["loc"][-1] for error in excinfo.value.errors()} + assert {"arrayName", "type", "numComponents"} <= reported + + +def test_empty_array_name_in_the_identifier_raises_validation_error(): + """``POINT::::1`` reaches validation unfilled, and fails there.""" + document = _document({"POINT::::1": _entry("POINT::::1")}) + + with pytest.raises(ValidationError) as excinfo: + _load(document) + + reported = {error["loc"][-1] for error in excinfo.value.errors()} + assert {"arrayName", "type", "numComponents"} <= reported + + +def test_explicit_null_still_fails_rather_than_being_backfilled(): + """A present-but-null field is present, and an invalid value, not an absent one.""" + entry = _entry("POINT::pressure::1") + entry["arrayName"] = None + + with pytest.raises(ValidationError): + _load(_document({"POINT::pressure::1": entry})) + + +def test_non_mapping_entry_raises_validation_error(): + """A malformed entry produces a ValidationError, never a TypeError.""" + document = json.dumps( + {"version": "1.0", "scene": {"variable_states": {"POINT::pressure::1": "not-a-mapping"}}} + ) + + with pytest.raises(ValidationError): + _load(document) + + +def test_non_mapping_variable_states_raises_validation_error(): + """The container itself being malformed is Pydantic's to report.""" + document = json.dumps({"version": "1.0", "scene": {"variable_states": "not-a-mapping"}}) + + with pytest.raises(ValidationError): + _load(document) + + +# ------------------------------------------------------------------ +# Which string the derivation reads +# ------------------------------------------------------------------ + +def test_identifier_is_taken_from_the_entry_id_when_it_disagrees_with_the_key(): + """The entry's own ``id`` is the identifier of record; the key is a copy of it. + + They agree in every file the client writes -- ``VisorSceneState`` keys the + dictionary by ``newState.id`` and re-emits ``id`` inside ``toDict()`` -- so + this pins the precedence rule explicitly rather than letting it go untested + because the two happen to coincide. + """ + entry = _entry("CELL::temperature::3") + + stored = _load_one("POINT::pressure::1", entry) + + assert stored.array_name == "temperature" + assert stored.type is VisorVtkVariableType.CELL + assert stored.num_components == 3 + + +def test_key_and_id_agreeing_derive_the_values_the_identifier_spells_out(): + """The normal case: key equals id, and both spell out the same three fields.""" + stored = _load_one("POINT::stress::yy::3") + + assert stored.id == "POINT::stress::yy::3" + assert stored.array_name == "stress::yy" + assert stored.type is VisorVtkVariableType.POINT + assert stored.num_components == 3 + + +def test_key_is_used_when_the_entry_carries_no_id(): + """With no ``id`` to read, the mapping key stands in for it. + + ``id`` is itself required, so such an entry still fails -- but it must fail + on ``id`` alone, which is what shows the three identity fields were filled + from the key rather than left absent. + """ + document = _document({"POINT::pressure::1": {"magnitudeRange": [0.0, 1.0], "ranges": []}}) + + with pytest.raises(ValidationError) as excinfo: + _load(document) + + reported = {error["loc"][-1] for error in excinfo.value.errors()} + assert reported == {"id"} + + +# ------------------------------------------------------------------ +# The version literal +# ------------------------------------------------------------------ + +def test_version_literal_is_1_0(): + """The persisted schema version is 1.0.""" + assert PersistedViewerStateV1().version == "1.0" + + +def test_a_document_declaring_version_1_1_is_rejected(): + """1.1 was never released as a schema version and must not be accepted.""" + with pytest.raises(ValidationError): + _load(_document({}, version="1.1")) + diff --git a/tests/unit/models/test_visor_save_state_response.py b/tests/unit/models/test_visor_save_state_response.py index 9c4b9983..f0de700c 100644 --- a/tests/unit/models/test_visor_save_state_response.py +++ b/tests/unit/models/test_visor_save_state_response.py @@ -112,3 +112,64 @@ def test_model_dump_contains_fields(monkeypatch): assert data["requestId"] == 5 assert "appState" in data + + +def test_save_path_rejects_a_variable_state_missing_the_identity_fields(): + """The save path must keep raising on a client that stops emitting the fields. + + ``VisorVariableState`` is shared between ``PersistedSceneState.variable_states`` + and ``RuntimeSceneState.spectrum_states``, so making the three identity + fields optional on the model would have relaxed this coercion too. The + tolerance for old save files lives on the persisted container instead, and + this pins the fact that it did not leak here: reads tolerate absence, + writes do not. + """ + payload = { + "requestId": 1, + "appState": { + "scene": { + "spectrumStates": { + "POINT::pressure::1": { + "id": "POINT::pressure::1", + "magnitudeRange": [0.0, 1.0], + "ranges": [[0.0, 1.0]], + } + } + } + }, + } + + with pytest.raises(ValidationError) as excinfo: + VisorSaveStateResponse.model_validate(payload) + + # Reported under the wire alias, which is the spelling validation ran by. + reported = {error["loc"][-1] for error in excinfo.value.errors()} + assert {"arrayName", "type", "numComponents"} <= reported + + +def test_save_path_accepts_a_variable_state_carrying_the_identity_fields(): + """The same payload with the three fields present validates, so the guard is specific.""" + payload = { + "requestId": 1, + "appState": { + "scene": { + "spectrumStates": { + "POINT::pressure::1": { + "id": "POINT::pressure::1", + "arrayName": "pressure", + "type": "POINT", + "numComponents": 1, + "magnitudeRange": [0.0, 1.0], + "ranges": [[0.0, 1.0]], + } + } + } + }, + } + + resp = VisorSaveStateResponse.model_validate(payload) + + stored = resp.app_state.scene.spectrum_states["POINT::pressure::1"] + assert stored.array_name == "pressure" + assert stored.num_components == 1 + From bd3b79c973f2a820234f1de30094f5efbc4e5cb5 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Wed, 2 Sep 2026 08:58:05 -0700 Subject: [PATCH 26/28] clean up docstrings --- .../models/common/visor_variable_state.py | 3 +- .../persist/scene/persisted_scene_state.py | 101 +++++------------- src/ansys/visor/viewer/vtk/scene/base.py | 75 +++++-------- 3 files changed, 53 insertions(+), 126 deletions(-) diff --git a/src/ansys/visor/viewer/models/common/visor_variable_state.py b/src/ansys/visor/viewer/models/common/visor_variable_state.py index 9deb072f..335a23b2 100644 --- a/src/ansys/visor/viewer/models/common/visor_variable_state.py +++ b/src/ansys/visor/viewer/models/common/visor_variable_state.py @@ -19,8 +19,7 @@ class VisorVariableState(BaseModel): ``array_name``, ``type`` and ``num_components`` are those same three identifying properties, carried explicitly (rather than only opaquely inside ``id``) so a consumer can act on them without parsing the - client-built identifier. They are required: a variable state with no known source array, association, or - component count is not a state this model can represent. + client-built identifier. In the future, the backend can own this, but for now we can treat this as passthrough data, as the id value is stable across sessions. diff --git a/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py b/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py index 728afff4..4865f68f 100644 --- a/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py +++ b/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py @@ -14,65 +14,42 @@ #: ``` `${type}::${name}::${numComponents}` ```. _IDENTIFIER_SEPARATOR = "::" -#: Number of fields the client packs into an identifier. -_IDENTIFIER_FIELD_COUNT = 3 - - def derive_variable_fields_from_identifier(identifier: Any) -> Dict[str, Any] | None: """Recover ``type``, ``array_name`` and ``num_components`` from a variable identifier. - The client mints the identifier as ``::::`` - (``VisorSpectrumManager.tsx``), so the three identifying properties are - recoverable from the identifier alone. This function is the permanent, - named inverse of that minting, and is the *only* place the identifier is - taken apart. - - The split is anchored at both ends and driven by the field count, **not** by - a left-to-right split on the separator: the array name is the middle field - and is user data, so it may itself contain ``::``. Two cuts: - - 1. one cut from the right yields the trailing component count, and - 2. one cut from the left of the remainder yields the leading type, - - leaving everything between them as the array name, verbatim and with any - embedded separators intact. ``"POINT::stress::yy::3"`` therefore yields the - array name ``"stress::yy"``, where a left-to-right split would have - truncated it to ``"stress"``. - - Args: - identifier: The persisted variable identifier. - - Returns: - A mapping of the three derived fields, or ``None`` when the identifier - does not spell out three usable fields. ``None`` means "cannot be - derived", never "derived to a default": callers must leave the entry - untouched so that ordinary validation reports the missing fields, rather - than filling in a guess. Structural success is not semantic success, so - an empty type or array name, or a non-integer component count, is - refused even though the separators are all present. + This is the inverse of the client's creation of the variable ID, and the only place the + identifier is parsed. The array name is in the middle field and is user data, so it may + contain the separator: the split is anchored at both ends rather than left to right, and + ``"POINT::stress::yy::3"`` yields the array name ``"stress::yy"``. + + Returns ``None`` when the identifier does not spell out three usable fields, meaning + "cannot be derived" rather than "derived to a default". Callers leave such an + entry untouched so ordinary validation reports the missing fields. """ if not isinstance(identifier, str): return None - head, separator, trailing = identifier.rpartition(_IDENTIFIER_SEPARATOR) + # Strip out the last field, which is the number of components. + # e.g. "POINT::stress::yy::3".rpartition("::") -> ("POINT::stress::yy", "::", "3") + head, separator, num_components_str = identifier.rpartition(_IDENTIFIER_SEPARATOR) if not separator: return None + try: + num_components = int(num_components_str) + except (TypeError, ValueError): + return None + + # Strip out the first field, which is the variable type. The remainder is the array name, + # which may contain the separator. + # e.g. "POINT::stress::yy".partition("::") -> ("POINT", "::", "stress::yy") variable_type, separator, array_name = head.partition(_IDENTIFIER_SEPARATOR) if not separator: return None - # Structurally there are now three fields. Refuse the ones that are not - # semantically usable rather than admitting a state that names an array - # which cannot exist. if not variable_type or not array_name: return None - try: - num_components = int(trailing) - except (TypeError, ValueError): - return None - return { "type": variable_type, "array_name": array_name, @@ -106,29 +83,13 @@ class PersistedSceneState(BaseModel): @field_validator("variable_states", mode="before") @classmethod def _derive_missing_variable_identity_fields(cls, value: Any) -> Any: - """Fill absent identity fields on *read*, from the variable identifier. - - ``VisorVariableState.array_name``, ``.type`` and ``.num_components`` are - required and stay required: they are shared with - ``RuntimeSceneState.spectrum_states``, so relaxing them on the model - would also relax the save-path coercion in - ``VisorSaveStateResponse._coerce_app_state``. The tolerance for older - save files therefore lives here, on the container, where it applies to - the ingest boundary only. - - Save files written before those fields existed carry the same three - properties inside the identifier, so they are recovered by - :func:`derive_variable_fields_from_identifier` before coercion. Nothing - downstream of this validator ever sees ``None``: by the time an entry - becomes a ``VisorVariableState`` the fields are populated or validation - has already failed. - - Values that are already ``VisorVariableState`` instances -- which is - every value on the *write* path, where the mapper passes the runtime - dictionary through wholesale -- are handed back untouched. Values that - are not mappings are handed back untouched too, so that Pydantic raises - a well-formed ``ValidationError`` rather than this validator raising - ``TypeError`` while indexing. + """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.spectrum_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. """ if not isinstance(value, Mapping): return value @@ -141,13 +102,11 @@ def _derive_missing_variable_identity_fields(cls, value: Any) -> Any: @staticmethod def _derive_missing_identity_fields_for_entry(key: Any, entry: Any) -> Any: """Return *entry* with any absent identity fields derived from its identifier.""" - # Already-built models (the write path) and non-mappings are not ours to touch. if isinstance(entry, VisorVariableState) or not isinstance(entry, Mapping): return entry - # ``populate_by_name`` is set on VisorVariableState, so either spelling - # counts as present. A present-but-null value counts as present: only - # genuine absence is tolerated, so an explicit null still fails. + # ``populate_by_name`` is set, so either spelling counts as present, and an + # explicit null counts as present and is left to fail validation. missing = [ field for field, alias in ( @@ -160,16 +119,12 @@ def _derive_missing_identity_fields_for_entry(key: Any, entry: Any) -> Any: if not missing: return entry - # The entry carries the identifier as a field of its own; the mapping key - # is a copy of it. Prefer the entry's own value, fall back to the key. identifier = entry.get("id") if not isinstance(identifier, str) or not identifier: identifier = key derived = derive_variable_fields_from_identifier(identifier) if derived is None: - # Undecipherable: leave it alone and let ordinary validation report - # the missing fields. Never fill with a guess. return entry filled = dict(entry) diff --git a/src/ansys/visor/viewer/vtk/scene/base.py b/src/ansys/visor/viewer/vtk/scene/base.py index 0903e4fb..7276d4c8 100644 --- a/src/ansys/visor/viewer/vtk/scene/base.py +++ b/src/ansys/visor/viewer/vtk/scene/base.py @@ -535,32 +535,18 @@ def _restore_part_states_from_runtime(self, runtime_app_state: "RuntimeAppState" """ Restore per-part state from a runtime app state, on the load path. - Two halves, in this order: the registry is replaced wholesale from the - supplied dataset states (``replace_part_states``), then every part is - applied to this process's VTK pipeline through ``IRenderer.apply_*``. - Either half can silently do nothing while the other succeeds, which is - why they are separate steps and separately asserted. - - Takes no lock of its own: it is only ever called from inside - :meth:`apply_state`'s critical section, and acquiring here would read - as though the method were independently safe to call. - - Every failure is a logged no-op, per the posture used at every other - layer: an unregistered dataset id, an unresolvable part, a malformed - colour, a half-set variable reference, an unknown variable identifier, - an unknown array, and an array whose width disagrees with the stored - one are each skipped without raising. + Replaces the part states of each dataset named in the supplied states, + then applies every part to this process's VTK pipeline through ``IRenderer``. + Datasets the registry does not hold are skipped and logged. Every other failure + is a logged no-op. + + Callers must hold ``_vtk_lock``. """ dataset_states = runtime_app_state.scene.dataset_states or {} variable_states = runtime_app_state.scene.spectrum_states or {} - # Store half. Unknown dataset ids are skipped by the registry. self._dataset_registry.replace_part_states(dataset_states) - # Apply half. The dataset id is resolved once per dataset, from the - # loop key, and the skip is explicit and logged: replace_part_states - # skips unknown ids silently, so without this the store half and the - # apply half could disagree about which datasets exist. for dataset_id, dataset_state in dataset_states.items(): dataset = self._dataset_registry.datasets.get(dataset_id) if dataset is None: @@ -570,7 +556,7 @@ def _restore_part_states_from_runtime(self, runtime_app_state: "RuntimeAppState" ) continue - # Per-part variable metadata, keyed by part id — one entry per + # Per-part variable metadata, keyed by part id: one entry per # non-empty leaf. Built once per dataset rather than per part. variables_by_part = { entry.part_id: entry.variables for entry in dataset.list_variables() @@ -591,18 +577,13 @@ def _restore_one_part_state( """ Apply one restored part record to the pipeline. - Absent fields are absent: a ``None`` is "this record says nothing about - that property", not "reset it to the default", so nothing is applied - for it. **Recorded divergence:** :meth:`apply_state` does not - guarantee freshly built pipelines — ``load_state`` rebuilds datasets - only when the scene is empty — so a part whose stored ``diffuse_rgb`` - is ``None`` keeps whatever colour the pipeline already carries while - the registry records no custom colour. + A ``None`` field means ""this record says nothing about that property", + not "reset it to the default", so nothing is applied for it. - The colour is validated to exactly three elements here; a malformed - colour is a logged no-op and the same guard covers the colour the + The color is validated to exactly three elements here; a malformed + color is a logged no-op and the same guard covers the color the selection branch reads. The registry keeps the malformed value it was - loaded with — requiring on load would make the next save silently + loaded with; requiring on load would make the next save silently rewrite the user's file. """ if part_state.visible is not None: @@ -641,26 +622,18 @@ def _restore_part_color_variable( part_variables, ) -> None: """ - Restore one part's colour-variable reference, or clear it. - - The variable reference is a compound value that is only ever set or - cleared atomically, so a half-set pair — an identifier with no - component, or a component with no identifier — is not a state this can - act on and is a logged no-op in both directions. A component with no - identifier in particular must not fall through to the clear branch. - - The range is resolved from the persisted variable entry, whose index - convention is the opposite of the client's internal table: a stored - component of ``-1`` reads ``magnitude_range`` and a component of 0 or - greater reads ``ranges[component]``. The branch is three-way with no - fallthrough — a negative component other than ``-1`` is neither the - magnitude sentinel nor an index, and returns before any mutation. - - The array is resolved against the server's **per-part** variable - metadata and its component count is checked against the stored one: - two parts can carry same-named arrays of different widths, which the - application treats as different quantities, so name and association - alone could colour a part by the wrong quantity with no error anywhere. + Restore one part's color-variable reference, or clear it. + + The reference is a compound value, set and cleared as a unit, so if + either the identifier or component is missing, it is a logged no-op. + + A stored component of ``-1`` reads ``magnitude_range``; 0 or greater + reads ``ranges[component]``. Any other value is refused. + + The array is resolved against the server's per-part variable metadata, + and its width is checked against the stored component count: two parts + can carry same-named arrays of different widths, which the application + treats as different quantities. """ variable_id = part_state.spectrum_id component = part_state.spectrum_component From 34d04bc21eb08b2e393fe3725cbe067d3b3a97cb Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Wed, 2 Sep 2026 09:11:12 -0700 Subject: [PATCH 27/28] clean up docstrings --- .../persist/scene/persisted_scene_state.py | 4 +-- src/ansys/visor/viewer/vtk/scene/base.py | 32 ++++--------------- .../viewer/vtk/scene/visor_state_mapper.py | 10 +++--- 3 files changed, 13 insertions(+), 33 deletions(-) diff --git a/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py b/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py index 4865f68f..241d5e93 100644 --- a/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py +++ b/src/ansys/visor/viewer/models/persist/scene/persisted_scene_state.py @@ -105,8 +105,8 @@ def _derive_missing_identity_fields_for_entry(key: Any, entry: Any) -> Any: if isinstance(entry, VisorVariableState) or not isinstance(entry, Mapping): return entry - # ``populate_by_name`` is set, so either spelling counts as present, and an - # explicit null counts as present and is left to fail validation. + # A value that is present but null counts as present and is left + # to fail validation. missing = [ field for field, alias in ( diff --git a/src/ansys/visor/viewer/vtk/scene/base.py b/src/ansys/visor/viewer/vtk/scene/base.py index 7276d4c8..d6cb066e 100644 --- a/src/ansys/visor/viewer/vtk/scene/base.py +++ b/src/ansys/visor/viewer/vtk/scene/base.py @@ -150,26 +150,13 @@ async def get_state(self, timeout: float) -> PersistedViewerStateV1: Capture the current viewer state and return it as a :class:`PersistedViewerStateV1`. - The frontend round trip (:meth:`_get_runtime_state_async`) remains the - only source for everything the browser owns — UI panels, unit, the - display toggles, camera, cross-section and the per-variable ranges. - **Per-part state is not taken from it.** The dataset registry is the - server-side authority for that, so ``scene.dataset_states`` is replaced - wholesale with the registry's own runtime state before the persisted - mapping runs. The browser is no longer consulted for per-part state. - - The registry is read under ``_vtk_lock`` and each dataset state is - deep-copied out of it. The registry hands out its live - ``RuntimeDatasetState`` objects by reference, and the per-part setters - mutate the records they contain in place from the trame daemon thread; - without the snapshot the persist mapper would be an unlocked reader of - records that are being written concurrently. The lock is taken *after* - the ``await`` has completed and released before the mapping, so it is - never held across an ``await``. Named cost: this is the only place a - coroutine acquires ``_vtk_lock``, so a save issued while a long - main-thread VTK call holds the lock waits for that call. It cannot - deadlock: the outbound bridge call made under the lock reaches - ``loop.call_later`` and returns without waiting on the event loop. + 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. + + The registry hands out live ``RuntimeDatasetState`` objects that the per-part + setters mutate from teh trame daemon thread, so each one is deep-copied under + ``_vtk_lock``. The lock is taken after the ``await`` and never held across one. """ runtime_state = await self._get_runtime_state_async(timeout) @@ -203,13 +190,8 @@ def apply_state(self, state: PersistedViewerStateV1): # Transform the frontend PersistedViewerStateV1 -> RuntimeAppState runtime_app_state = self._state_mapper.persisted_to_runtime(state) - # Restore per-part state server-side: write the registry, then apply - # every part to this process's VTK pipeline. The browser is not - # asked to do this, and is not trusted to have done it. self._restore_part_states_from_runtime(runtime_app_state) - # Renderer-specific: flush VTK window and notify frontend (wasm), or - # push camera to vtkCamera (RCA), or no-op (headless). self._apply_runtime_state_to_render(runtime_app_state) # Note: There is intentionally no wasm flush here: the bridge call is fire-and-forget, so a flush diff --git a/src/ansys/visor/viewer/vtk/scene/visor_state_mapper.py b/src/ansys/visor/viewer/vtk/scene/visor_state_mapper.py index 0a681481..2732d4f6 100644 --- a/src/ansys/visor/viewer/vtk/scene/visor_state_mapper.py +++ b/src/ansys/visor/viewer/vtk/scene/visor_state_mapper.py @@ -74,12 +74,10 @@ def persisted_to_runtime(self, state: PersistedViewerStateV1) -> RuntimeAppState Logic parity with ``VisorScene._persisted_to_runtime``. - This is **not**, and never has been, the registry-population path. It - builds runtime dataset states and returns them on the RuntimeAppState; - it never assigns its result back onto ``VisorDataset.state``, so no - registry record is written by calling it. - :meth:`VisorSceneBase._restore_part_states_from_runtime` now populates - the registry explicitly, from the state this method returns. + Returns the runtime dataset states on the ``RuntimeAppState``; it does not + assign them to ``VisorDataset.state``. The registry is populated by + :meth:`VisorSceneBase._restore_part_states_from_runtime`. + """ # UI settings ui_state = state.ui From 45831a0f073c6d853d341af2e699d11f000db091 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Thu, 3 Sep 2026 08:12:45 -0700 Subject: [PATCH 28/28] pre-commit fixes --- src/ansys/visor/viewer/vtk/scene/base.py | 2 +- tests/unit/models/test_persisted_scene_state.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ansys/visor/viewer/vtk/scene/base.py b/src/ansys/visor/viewer/vtk/scene/base.py index d6cb066e..d98277f1 100644 --- a/src/ansys/visor/viewer/vtk/scene/base.py +++ b/src/ansys/visor/viewer/vtk/scene/base.py @@ -155,7 +155,7 @@ async def get_state(self, timeout: float) -> PersistedViewerStateV1: own runtime state before the persisted mapping runs. The registry hands out live ``RuntimeDatasetState`` objects that the per-part - setters mutate from teh trame daemon thread, so each one is deep-copied under + 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. """ runtime_state = await self._get_runtime_state_async(timeout) diff --git a/tests/unit/models/test_persisted_scene_state.py b/tests/unit/models/test_persisted_scene_state.py index 9e9e2190..a8548f08 100644 --- a/tests/unit/models/test_persisted_scene_state.py +++ b/tests/unit/models/test_persisted_scene_state.py @@ -26,7 +26,6 @@ derive_variable_fields_from_identifier, ) - # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------