From 671e9028ff0ad51da455219d2d9c0e6b478b4e01 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Mon, 28 Sep 2026 07:27:45 -0700 Subject: [PATCH 1/5] rename Spectrum -> Variable --- .../visualizer_ui/top_right_panel.rst | 6 +- .../persist/scene/persisted_scene_state.py | 4 +- .../runtime/dataset/runtime_dataset_state.py | 36 +- .../models/runtime/scene/runtime_app_state.py | 2 +- .../runtime/scene/runtime_scene_state.py | 2 +- src/ansys/visor/viewer/renderer/base.py | 6 +- .../visor/viewer/renderer/local_renderer.py | 6 +- .../visor/viewer/renderer/null_renderer.py | 4 +- .../vtk/datasets/visor_dataset_registry.py | 10 +- src/ansys/visor/viewer/vtk/scene/base.py | 6 +- .../viewer/vtk/scene/visor_state_mapper.py | 2 +- .../visor-client/src/CameraSyncReporter.ts | 2 +- .../visor/visor-client/src/VisorFrontend.tsx | 88 ++-- .../src/aggregate/AggregateSelectionInfo.tsx | 122 +++--- ...tsx => AggregateVariableComponentInfo.tsx} | 150 +++---- ...trumInfo.tsx => AggregateVariableInfo.tsx} | 106 ++--- .../components/ui-panels/Panel_TopRight.tsx | 146 +++---- .../AggregateSelectionInfo.test.tsx | 278 ++++++------- ...> AggregateVariableComponentInfo.test.tsx} | 388 +++++++++--------- ...est.tsx => AggregateVariableInfo.test.tsx} | 300 +++++++------- .../VisorSceneGraphPartTriggers.test.tsx | 14 +- ...test.tsx => VisorVariableManager.test.tsx} | 224 +++++----- .../WasmRendererPartTriggers.test.tsx | 30 +- .../visor-client/src/renderer/IRenderer.ts | 6 +- .../visor-client/src/renderer/WasmRenderer.ts | 10 +- .../src/state/VisorSceneGraph.tsx | 94 ++--- ...umManager.tsx => VisorVariableManager.tsx} | 134 +++--- .../src/state/appstate/VisorPartState.tsx | 40 +- .../src/state/appstate/VisorSceneState.tsx | 52 +-- ...ectrumState.tsx => VisorVariableState.tsx} | 10 +- .../regressions/test_variable_components.py | 2 +- tests/integration/test_save_load_state.py | 4 +- tests/references/dash_snapshot_reference.json | 38 +- tests/unit/models/test_dataset_state.py | 40 +- tests/unit/models/test_runtime_scene_state.py | 14 +- .../models/test_visor_save_state_response.py | 8 +- tests/unit/renderer/test_local_renderer.py | 2 +- .../datasets/test_visor_dataset_registry.py | 18 +- tests/unit/vtk/scene/test_base.py | 18 +- .../unit/vtk/scene/test_visor_state_mapper.py | 4 +- 40 files changed, 1213 insertions(+), 1213 deletions(-) rename src/ansys/visor/visor-client/src/aggregate/{AggregateSpectrumComponentInfo.tsx => AggregateVariableComponentInfo.tsx} (51%) rename src/ansys/visor/visor-client/src/aggregate/{AggregateSpectrumInfo.tsx => AggregateVariableInfo.tsx} (61%) rename src/ansys/visor/visor-client/src/jest-tests/{AggregateSpectrumComponentInfo.test.tsx => AggregateVariableComponentInfo.test.tsx} (53%) rename src/ansys/visor/visor-client/src/jest-tests/{AggregateSpectrumInfo.test.tsx => AggregateVariableInfo.test.tsx} (66%) rename src/ansys/visor/visor-client/src/jest-tests/{VisorSpectrumManager.test.tsx => VisorVariableManager.test.tsx} (70%) rename src/ansys/visor/visor-client/src/state/{VisorSpectrumManager.tsx => VisorVariableManager.tsx} (72%) rename src/ansys/visor/visor-client/src/state/appstate/{VisorSpectrumState.tsx => VisorVariableState.tsx} (93%) diff --git a/doc/source/user_guide/visualizer_ui/top_right_panel.rst b/doc/source/user_guide/visualizer_ui/top_right_panel.rst index fcfd88fd..839912d3 100644 --- a/doc/source/user_guide/visualizer_ui/top_right_panel.rst +++ b/doc/source/user_guide/visualizer_ui/top_right_panel.rst @@ -32,7 +32,7 @@ Use the following fields on the **Part properties** tab: - **Color variable** - - Apply a data array (color variable) to map values to a color spectrum on the selected part. + - Apply a data array (color variable) to map values to a color variable on the selected part. - On this page, the terms *data array*, *color variable*, and *variable* have the same meaning. - Open **Color by variable** to view available variables. The list is the union of variables across selected parts. @@ -79,7 +79,7 @@ Use the following fields on the **Legend settings** tab: - **Max (color variable maximum)** - - Set the upper bound of the visible color spectrum for the current color variable. + - Set the upper bound of the visible color variable for the current color variable. - After you enter a value, press the **Enter** key or click **Apply**. - Click **Reset** to restore the dataset's original maximum. - This value applies globally to the color variable, not only to one part. @@ -87,7 +87,7 @@ Use the following fields on the **Legend settings** tab: - **Min (color variable minimum)** - - Set the lower bound of the visible color spectrum for the current color variable. + - Set the lower bound of the visible color variable for the current color variable. - After you enter a value, press the **Enter** key or click **Apply**. - Click **Reset** to restore the dataset's original minimum. - This value applies globally to the color variable, not only to one part. 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 526ca59d..2e41bcde 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 @@ -10,7 +10,7 @@ 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 +#: ``VisorVariableManager.tryAddVariableInfo``, which builds the identifier as #: ``` `${type}::${name}::${numComponents}` ```. _IDENTIFIER_SEPARATOR = "::" @@ -85,7 +85,7 @@ def _derive_missing_variable_identity_fields(cls, value: Any) -> Any: """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``, + required: the model is shared with ``RuntimeSceneState.variable_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. diff --git a/src/ansys/visor/viewer/models/runtime/dataset/runtime_dataset_state.py b/src/ansys/visor/viewer/models/runtime/dataset/runtime_dataset_state.py index 5012f1d1..78cf2736 100644 --- a/src/ansys/visor/viewer/models/runtime/dataset/runtime_dataset_state.py +++ b/src/ansys/visor/viewer/models/runtime/dataset/runtime_dataset_state.py @@ -12,18 +12,18 @@ class RuntimePartProperties(BaseModel): Frontend-facing model of part properties for runtime state serialization. This class mirrors the fields of ``PartProperties`` using snake_case Python - attribute names, but serializes with camelCase aliases (``spectrumId``, - ``spectrumComponent``, ``diffuseRgb``) for parity with the frontend wire + attribute names, but serializes with camelCase aliases (``variableId``, + ``variableComponent``, ``diffuseRgb``) for parity with the frontend wire format. Callers must pass ``by_alias=True`` when dumping (e.g., in ``get_scene_details_json``) so the emitted keys stay camelCase. Serialization semantics (aligned with the frontend state model): - - ``spectrumId`` is **always included** in the serialized output, even when - ``None``. On the frontend, ``null`` means "no spectrum applied" (a real + - ``variableId`` is **always included** in the serialized output, even when + ``None``. On the frontend, ``null`` means "no variable applied" (a real value), while an absent key means "leave this property unchanged". - All other optional fields (``opacity``, ``visible``, ``selected``, - ``spectrumComponent``, ``diffuseRgb``) are **omitted from the serialized + ``variableComponent``, ``diffuseRgb``) are **omitted from the serialized output when ``None``**, so that the frontend treats them as pass-through / unchanged. @@ -31,9 +31,9 @@ class RuntimePartProperties(BaseModel): opacity: Optional[float]: The opacity of the part, between 0.0 (fully transparent) and 1.0 (fully opaque). visible: Optional[bool]: Whether the part is visible in the scene. selected: Optional[bool]: Whether the part is currently selected by the user. - spectrum_id: Optional[str]: The ID of the variable used to colour this part, - or ``None`` to indicate that no spectrum is applied. Always serialized. - spectrum_component: Optional[int]: If the variable specified by spectrum_id has multiple components, + variable_id: Optional[str]: The ID of the variable used to colour this part, + or ``None`` to indicate that no variable is applied. Always serialized. + variable_component: Optional[int]: If the variable specified by variable_id has multiple components, this specifies which component to use for coloring. """ model_config = ConfigDict(populate_by_name=True) @@ -43,11 +43,11 @@ class RuntimePartProperties(BaseModel): visible: Optional[bool] = Field(default=None) selected: Optional[bool] = Field(default=None) - # spectrum_id=None means "no spectrum applied" — a real, meaningful value. + # variable_id=None means "no variable applied" — a real, meaningful value. # It is always included in serialized output so the frontend can act on it. - spectrum_id: Optional[str] = Field(default=None, alias="spectrumId") - # If the variable with ID spectrum_id has multiple components, this specifies which component to use for coloring. - spectrum_component: Optional[int] = Field(default=None, alias="spectrumComponent") + variable_id: Optional[str] = Field(default=None, alias="variableId") + # If the variable with ID variable_id has multiple components, this specifies which component to use for coloring. + variable_component: Optional[int] = Field(default=None, alias="variableComponent") # If set, this part's color is determined by the specified RGB values (each between 0 and 1). diffuse_rgb: Optional[List[float]] = Field(default=None, alias="diffuseRgb") @@ -56,7 +56,7 @@ class RuntimePartProperties(BaseModel): def _serialize(self, handler: Any, info: Any = None) -> dict: """Custom serializer that implements the frontend undefined-vs-null contract. - ``spectrumId`` is always present in the output (``null`` is meaningful). + ``variableId`` is always present in the output (``null`` is meaningful). All other optional fields are omitted when their value is ``None``, so the frontend interprets them as pass-through / undefined. @@ -67,7 +67,7 @@ def _serialize(self, handler: Any, info: Any = None) -> dict: data: dict = handler(self) keys_to_omit_when_none = { "opacity", "visible", "selected", - "spectrum_component", "spectrumComponent", + "variable_component", "variableComponent", "diffuse_rgb", "diffuseRgb", } return {k: v for k, v in data.items() if not (k in keys_to_omit_when_none and v is None)} @@ -77,8 +77,8 @@ def to_part_properties(self) -> PartProperties: opacity=self.opacity, visible=self.visible, selected=self.selected, - color_by=self.spectrum_id, - color_by_component=self.spectrum_component, + color_by=self.variable_id, + color_by_component=self.variable_component, diffuse_rgb=self.diffuse_rgb, ) @@ -89,8 +89,8 @@ def from_part_properties(cls, id: int, props: PartProperties) -> "RuntimePartPro opacity=props.opacity, visible=props.visible, selected=props.selected, - spectrum_id=props.color_by, - spectrum_component=props.color_by_component, + variable_id=props.color_by, + variable_component=props.color_by_component, diffuse_rgb=props.diffuse_rgb, ) diff --git a/src/ansys/visor/viewer/models/runtime/scene/runtime_app_state.py b/src/ansys/visor/viewer/models/runtime/scene/runtime_app_state.py index 4eb9512d..cca1b430 100644 --- a/src/ansys/visor/viewer/models/runtime/scene/runtime_app_state.py +++ b/src/ansys/visor/viewer/models/runtime/scene/runtime_app_state.py @@ -52,7 +52,7 @@ def from_components( edges_enabled=edges_enabled, bounding_box_enabled=bounding_box_enabled, dataset_states=dataset_states, - spectrum_states=variable_states or {}, + variable_states=variable_states or {}, ) return cls( ui=ui_state, diff --git a/src/ansys/visor/viewer/models/runtime/scene/runtime_scene_state.py b/src/ansys/visor/viewer/models/runtime/scene/runtime_scene_state.py index a0b8bc6a..cefaabcc 100644 --- a/src/ansys/visor/viewer/models/runtime/scene/runtime_scene_state.py +++ b/src/ansys/visor/viewer/models/runtime/scene/runtime_scene_state.py @@ -28,7 +28,7 @@ class RuntimeSceneState(BaseModel): edges_enabled: bool | None = Field(default=None, alias="edgesEnabled") bounding_box_enabled: bool | None = Field(default=None, alias="boundingBoxEnabled") dataset_states: Dict[int, "RuntimeDatasetState"] = Field(default_factory=dict, alias="datasetStates") - spectrum_states: Dict[str, "VisorVariableState"] = Field(default_factory=dict, alias="spectrumStates") + variable_states: Dict[str, "VisorVariableState"] = Field(default_factory=dict, alias="variableStates") @field_validator("dataset_states", mode="before") @classmethod diff --git a/src/ansys/visor/viewer/renderer/base.py b/src/ansys/visor/viewer/renderer/base.py index 5821b150..86346cad 100644 --- a/src/ansys/visor/viewer/renderer/base.py +++ b/src/ansys/visor/viewer/renderer/base.py @@ -121,7 +121,7 @@ def apply_selected( def apply_color_variable( self, node_id: int, - spectrum_id: str, + variable_id: str, array_type: str, array_name: str, component: int, @@ -136,7 +136,7 @@ def apply_color_variable( ---------- node_id: Scene-graph node to update. - spectrum_id: + variable_id: Server-authoritative opaque ID (stored as-is; not parsed here). array_type: ``"POINT"`` or ``"CELL"``. @@ -156,7 +156,7 @@ def clear_color_variable(self, node_id: int) -> None: def refresh_color_variable_range( self, node_id: int, - spectrum_id: str, + variable_id: str, array_type: str, array_name: str, component: int, diff --git a/src/ansys/visor/viewer/renderer/local_renderer.py b/src/ansys/visor/viewer/renderer/local_renderer.py index e2d28c85..522703a8 100644 --- a/src/ansys/visor/viewer/renderer/local_renderer.py +++ b/src/ansys/visor/viewer/renderer/local_renderer.py @@ -226,7 +226,7 @@ def apply_selected( def apply_color_variable( self, node_id: int, - spectrum_id: str, + variable_id: str, array_type: str, array_name: str, component: int, @@ -240,7 +240,7 @@ def apply_color_variable( 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 + *variable_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. """ @@ -270,7 +270,7 @@ def clear_color_variable(self, node_id: int) -> None: def refresh_color_variable_range( self, node_id: int, - spectrum_id: str, + variable_id: str, array_type: str, array_name: str, component: int, diff --git a/src/ansys/visor/viewer/renderer/null_renderer.py b/src/ansys/visor/viewer/renderer/null_renderer.py index 2b4b2065..5a3f3463 100644 --- a/src/ansys/visor/viewer/renderer/null_renderer.py +++ b/src/ansys/visor/viewer/renderer/null_renderer.py @@ -85,7 +85,7 @@ def apply_selected(self, node_id: int, selected: bool, diffuse_rgb: list) -> Non def apply_color_variable( self, node_id: int, - spectrum_id: str, + variable_id: str, array_type: str, array_name: str, component: int, @@ -100,7 +100,7 @@ def clear_color_variable(self, node_id: int) -> None: def refresh_color_variable_range( self, node_id: int, - spectrum_id: str, + variable_id: str, array_type: str, array_name: str, component: int, 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 7311fdc1..ef89fb07 100644 --- a/src/ansys/visor/viewer/vtk/datasets/visor_dataset_registry.py +++ b/src/ansys/visor/viewer/vtk/datasets/visor_dataset_registry.py @@ -245,15 +245,15 @@ def set_part_color_variable(self, part_id: int, variable_id: str, component: int 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 + part_state.variable_id = variable_id + part_state.variable_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 + Sets variable_id and variable_component to None together, in one call — the compound class is only ever set or cleared atomically, never field by field. @@ -264,8 +264,8 @@ def clear_part_color_variable(self, part_id: int) -> bool: 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 + part_state.variable_id = None + part_state.variable_component = None return True def replace_part_states(self, dataset_states: Dict[int, RuntimeDatasetState]) -> None: diff --git a/src/ansys/visor/viewer/vtk/scene/base.py b/src/ansys/visor/viewer/vtk/scene/base.py index 68739357..9c53aa13 100644 --- a/src/ansys/visor/viewer/vtk/scene/base.py +++ b/src/ansys/visor/viewer/vtk/scene/base.py @@ -712,7 +712,7 @@ def _restore_part_states(self, runtime_app_state: "RuntimeAppState") -> None: Callers must hold ``_vtk_lock``. """ dataset_states = runtime_app_state.scene.dataset_states or {} - variable_states = runtime_app_state.scene.spectrum_states or {} + variable_states = runtime_app_state.scene.variable_states or {} self._dataset_registry.replace_part_states(dataset_states) @@ -849,8 +849,8 @@ def _restore_part_color_variable( 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 + variable_id = part_state.variable_id + component = part_state.variable_component if variable_id is None: if component is not None: 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 e22c0ffb..87bcfc46 100644 --- a/src/ansys/visor/viewer/vtk/scene/visor_state_mapper.py +++ b/src/ansys/visor/viewer/vtk/scene/visor_state_mapper.py @@ -46,7 +46,7 @@ def runtime_to_persisted(self, runtime_app_state: RuntimeAppState) -> PersistedV # Scene - camera camera = scene_state.camera # Scene - variables: Pass through as-is since they are already keyed by stable variable identifier - variable_states = scene_state.spectrum_states + variable_states = scene_state.variable_states # Scene - datasets runtime_dataset_states = scene_state.dataset_states persisted_dataset_states = {} diff --git a/src/ansys/visor/visor-client/src/CameraSyncReporter.ts b/src/ansys/visor/visor-client/src/CameraSyncReporter.ts index 347606bb..d60a9cc2 100644 --- a/src/ansys/visor/visor-client/src/CameraSyncReporter.ts +++ b/src/ansys/visor/visor-client/src/CameraSyncReporter.ts @@ -10,7 +10,7 @@ import type { * * This module exists apart from `VisorFrontend` so that it can be tested. A * `VisorFrontend` cannot be constructed under jest -- it needs a real scene - * graph node, the module-global spectrum manager and a renderer that accepts + * graph node, the module-global variable manager and a renderer that accepts * `attachSceneGraph`, and it ends in `Object.freeze` -- so a listener body * written inline there would be pinned by nothing, and the payload shape is * precisely the part no server-side gate can check. `CameraGestureTracker` diff --git a/src/ansys/visor/visor-client/src/VisorFrontend.tsx b/src/ansys/visor/visor-client/src/VisorFrontend.tsx index aa59dcc3..e83be541 100644 --- a/src/ansys/visor/visor-client/src/VisorFrontend.tsx +++ b/src/ansys/visor/visor-client/src/VisorFrontend.tsx @@ -1,10 +1,10 @@ -import { getSpectrumManager, VisorSpectrumCollection } from './state/VisorSpectrumManager.tsx'; +import { getVariableManager, VisorVariableCollection } from './state/VisorVariableManager.tsx'; import { CreateVisorSceneGraph, VisorSceneNodeExtended } from './state/VisorSceneGraph.tsx'; import { getPromiseResolver } from './utils/JsHelpers'; import VisorAppState from './state/appstate/VisorAppState.tsx'; import VisorDatasetState from './state/appstate/VisorDatasetState.tsx'; import VisorPartState from './state/appstate/VisorPartState.tsx'; -import VisorSpectrumState from './state/appstate/VisorSpectrumState.tsx'; +import VisorVariableState from './state/appstate/VisorVariableState.tsx'; import { TreeViewUtil } from './treeview/TreeView.tsx'; import { StateInput } from './state/appstate/VisorStateCommon.tsx'; import VisorVtkSceneNode from './state/appstate/vtkInfo/VisorVtkSceneNode.tsx'; @@ -24,9 +24,9 @@ export class VisorFrontend { sceneGraphNode: VisorVtkSceneNode, triggerSender: TrameTriggerSender ) { - const spectrumManager = getSpectrumManager(); - const sceneGraph = CreateVisorSceneGraph(sceneGraphNode, spectrumManager, renderer); - spectrumManager.finishAddingDataArrayMetadata(); + const variableManager = getVariableManager(); + const sceneGraph = CreateVisorSceneGraph(sceneGraphNode, variableManager, renderer); + variableManager.finishAddingDataArrayMetadata(); renderer.attachSceneGraph(sceneGraph); let treeViewUtilSet: boolean = false; @@ -67,7 +67,7 @@ export class VisorFrontend { const bp: string = ((window as any).__visorArgs?.basePath ?? '').replace(/\/$/, ''); const self = this; - this.globalSpectrumCollection = spectrumManager.globalSpectrumCollection; + this.globalVariableCollection = variableManager.globalVariableCollection; this.#unit = ''; this.darkMode = darkMode; this.render = async () => { @@ -100,16 +100,16 @@ export class VisorFrontend { remover?.(); }; this.defaultActorColor = []; - this.setSpectrumRangeAsync = async (spectrumId, component, min, max) => { - const spectrum = spectrumManager.globalSpectrumCollection.getSpectrum(spectrumId); - if (spectrum == null) { + this.setVariableRangeAsync = async (variableId, component, min, max) => { + const variable = variableManager.globalVariableCollection.getVariable(variableId); + if (variable == null) { return; } - spectrum.setCustomRange(component, min, max); + variable.setCustomRange(component, min, max); for (const actorNode of sceneGraph.descendantActorNodesOrSelfArray) { if ( - actorNode.spectrumId === spectrumId && - actorNode.spectrumComponent == component + actorNode.variableId === variableId && + actorNode.variableComponent == component ) { await actorNode.setScalarRangeAsync(min, max); } @@ -276,34 +276,34 @@ export class VisorFrontend { partState.setDiffuseRgb(partNode.diffuseRgb); partState.setOpacity(partNode.opacity); partState.setSelected(partNode.selected); - partState.setSpectrumId(partNode.spectrumId); - partState.setSpectrumComponent(partNode.spectrumComponent); + partState.setVariableId(partNode.variableId); + partState.setVariableComponent(partNode.variableComponent); datasetState.copyPart(partState); } sceneState.copyDataset(datasetState); } - const spectrumInfos = spectrumManager.globalSpectrumCollection; - 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); + const variableInfos = variableManager.globalVariableCollection; + for (const variableInfo of variableInfos.array) { + const variableState = new VisorVariableState(); + variableState.setId(variableInfo.id.toString()); + variableState.setArrayName(variableInfo.name); + variableState.setType(variableInfo.type); + variableState.setNumComponents(variableInfo.numComponents); + const magnitudeRange = variableInfo.getRangeInfo(-1); if (magnitudeRange == null) { throw new Error(`range at component ${-1} not found`); } - spectrumState.setMagnitudeRange(magnitudeRange.customRange); + variableState.setMagnitudeRange(magnitudeRange.customRange); const ranges: number[][] = []; - for (let i = 0; i < spectrumInfo.numComponents; i++) { - const range = spectrumInfo.getRangeInfo(i); + for (let i = 0; i < variableInfo.numComponents; i++) { + const range = variableInfo.getRangeInfo(i); if (range == null) { throw new Error(`range at component ${i} not found`); } ranges.push(range.customRange); } - spectrumState.setRanges(ranges); - sceneState.copySpectrum(spectrumState); + variableState.setRanges(ranges); + sceneState.copyVariable(variableState); } return appState; }; @@ -482,14 +482,14 @@ export class VisorFrontend { promises.push(promise); } if ( - part_state.spectrumId !== undefined && - part_state.spectrumComponent !== undefined + part_state.variableId !== undefined && + part_state.variableComponent !== undefined ) { let promise; - if (part_state.spectrumId !== null) { + if (part_state.variableId !== null) { promise = node.setColorVariableAsync( - part_state.spectrumId, - part_state.spectrumComponent + part_state.variableId, + part_state.variableComponent ); } else { promise = node.clearColorVariableAsync(); @@ -499,20 +499,20 @@ export class VisorFrontend { } } } - // wait for all the parts to be updated before updating the spectrum ranges + // wait for all the parts to be updated before updating the variable ranges await Promise.all(promises); promises.length = 0; - for (const spectrum_state of sceneState.getSpectrumStates()) { - const idStr = spectrum_state.id; - if (spectrum_state.magnitudeRange !== undefined) { - const range = spectrum_state.magnitudeRange; - const promise = self.setSpectrumRangeAsync(idStr, -1, range[0], range[1]); + for (const variable_state of sceneState.getVariableStates()) { + const idStr = variable_state.id; + if (variable_state.magnitudeRange !== undefined) { + const range = variable_state.magnitudeRange; + const promise = self.setVariableRangeAsync(idStr, -1, range[0], range[1]); promises.push(promise); } - for (let i = 0; i < spectrum_state.ranges.length; i++) { - const range = spectrum_state.ranges[i]; + for (let i = 0; i < variable_state.ranges.length; i++) { + const range = variable_state.ranges[i]; if (range !== undefined) { - const promise = self.setSpectrumRangeAsync(idStr, i, range[0], range[1]); + const promise = self.setVariableRangeAsync(idStr, i, range[0], range[1]); promises.push(promise); } } @@ -559,9 +559,9 @@ export class VisorFrontend { getCameraStateAsync: () => Promise; toggleFullScreenAsync: () => Promise; addCameraChangedListener: (callback: (cameraState: VisorCameraState) => void) => () => void; - globalSpectrumCollection: VisorSpectrumCollection; - setSpectrumRangeAsync: ( - spectrumId: string, + globalVariableCollection: VisorVariableCollection; + setVariableRangeAsync: ( + variableId: string, component: number, min: number, max: number diff --git a/src/ansys/visor/visor-client/src/aggregate/AggregateSelectionInfo.tsx b/src/ansys/visor/visor-client/src/aggregate/AggregateSelectionInfo.tsx index 33196888..83c7cc21 100644 --- a/src/ansys/visor/visor-client/src/aggregate/AggregateSelectionInfo.tsx +++ b/src/ansys/visor/visor-client/src/aggregate/AggregateSelectionInfo.tsx @@ -1,27 +1,27 @@ import { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; -import { AggregateSpectrumInfo } from './AggregateSpectrumInfo.tsx'; +import { AggregateVariableInfo } from './AggregateVariableInfo.tsx'; import { tryParseFloat } from '../utils/JsHelpers'; /** - * Event handlers emitted when aggregate spectrum selections change. + * Event handlers emitted when aggregate variable selections change. */ class AggregateSelectionEvents { /** - * Called when the selected spectrum changes. + * Called when the selected variable changes. * * A value of `undefined` generally represents a mixed selection, while - * `null` represents no selected spectrum. + * `null` represents no selected variable. */ - onSpectrumChange!: ((spectrumInfo: AggregateSpectrumInfo | null | undefined) => void) | null; + onVariableChange!: ((variableInfo: AggregateVariableInfo | null | undefined) => void) | null; /** - * Called when a component of the selected spectrum changes. + * Called when a component of the selected variable changes. * * A value of `undefined` generally represents a mixed selection, while - * `null` represents no selected spectrum. + * `null` represents no selected variable. */ - onSpectrumComponentChange!: - ((spectrumInfo: AggregateSpectrumInfo | null | undefined) => void) | null; + onVariableComponentChange!: + ((variableInfo: AggregateVariableInfo | null | undefined) => void) | null; } /** @@ -52,9 +52,9 @@ export class AggregateSelectionInfo { #displayOpacity: number | null | undefined = null; /** - * Spectrum identifier shared by the selected nodes. + * Variable identifier shared by the selected nodes. */ - #displaySpectrumId: string | null | undefined = null; + #displayVariableId: string | null | undefined = null; /** * Custom diffuse color shared by the selected nodes, represented as a @@ -63,21 +63,21 @@ export class AggregateSelectionInfo { #displayDiffuseColor: string | null | undefined = null; /** - * Available spectra collected from all selected nodes, keyed by spectrum ID. + * Available spectra collected from all selected nodes, keyed by variable ID. */ - #spectrumOptions: Map = new Map(); + #variableOptions: Map = new Map(); /** - * Aggregate information for the currently selected spectrum. + * Aggregate information for the currently selected variable. */ - #currentSpectrumInfo: AggregateSpectrumInfo | null | undefined = null; + #currentVariableInfo: AggregateVariableInfo | null | undefined = null; /** * Event handlers associated with this aggregate selection. */ #events: AggregateSelectionEvents = { - onSpectrumChange: null, - onSpectrumComponentChange: null, + onVariableChange: null, + onVariableComponentChange: null, }; /** @@ -87,8 +87,8 @@ export class AggregateSelectionInfo { * has the same value. Properties are set to `undefined` when the nodes have * differing values. * - * Spectrum options from all supplied nodes are also collected and converted - * into {@link AggregateSpectrumInfo} instances. + * Variable options from all supplied nodes are also collected and converted + * into {@link AggregateVariableInfo} instances. * * @param actorNodes - Scene nodes included in the aggregate selection. * @returns A promise resolving to the initialized aggregate selection. @@ -102,47 +102,47 @@ export class AggregateSelectionInfo { const node = actorNodes[i]; const thisName: string | null = node.name; const thisOpacity: number | null = node.opacity; - const thisSpectrumId: string | null = node.spectrumId; + const thisVariableId: string | null = node.variableId; const thisDiffuseColor: string | null = node.customDiffuseColorHex; - for (const spectrumInfo of node.spectrumCollection.array) { - const idStr = spectrumInfo.id.toString(); + for (const variableInfo of node.variableCollection.array) { + const idStr = variableInfo.id.toString(); - if (!obj.#spectrumOptions.has(idStr)) { - const val = await AggregateSpectrumInfo.getInstanceAsync( + if (!obj.#variableOptions.has(idStr)) { + const val = await AggregateVariableInfo.getInstanceAsync( actorNodes, obj, - spectrumInfo + variableInfo ); - obj.#spectrumOptions.set(idStr, val); + obj.#variableOptions.set(idStr, val); } } if (i === 0) { obj.#displayName = thisName; obj.#displayOpacity = thisOpacity; - obj.#displaySpectrumId = thisSpectrumId; + obj.#displayVariableId = thisVariableId; obj.#displayDiffuseColor = thisDiffuseColor; } else { obj.#displayName !== thisName && (obj.#displayName = undefined); obj.#displayOpacity !== thisOpacity && (obj.#displayOpacity = undefined); - obj.#displaySpectrumId !== thisSpectrumId && (obj.#displaySpectrumId = undefined); + obj.#displayVariableId !== thisVariableId && (obj.#displayVariableId = undefined); obj.#displayDiffuseColor !== thisDiffuseColor && (obj.#displayDiffuseColor = undefined); } } - if (obj.#displaySpectrumId != null) { - obj.#currentSpectrumInfo = obj.#spectrumOptions.get(obj.#displaySpectrumId.toString()); + if (obj.#displayVariableId != null) { + obj.#currentVariableInfo = obj.#variableOptions.get(obj.#displayVariableId.toString()); - if (obj.#currentSpectrumInfo == null) { - console.warn(`invalid spectrumId: '${obj.#displaySpectrumId}'`); + if (obj.#currentVariableInfo == null) { + console.warn(`invalid variableId: '${obj.#displayVariableId}'`); } } else { - obj.#currentSpectrumInfo = obj.#displaySpectrumId; + obj.#currentVariableInfo = obj.#displayVariableId; } return obj; @@ -177,32 +177,32 @@ export class AggregateSelectionInfo { } /** - * Gets the spectrum identifier shared by the selected nodes. + * Gets the variable identifier shared by the selected nodes. * - * @returns The shared spectrum ID, `null` when unset, or `undefined` when + * @returns The shared variable ID, `null` when unset, or `undefined` when * mixed. */ - get displaySpectrumId(): string | null | undefined { - return this.#displaySpectrumId; + get displayVariableId(): string | null | undefined { + return this.#displayVariableId; } /** - * Gets information about the currently selected spectrum. + * Gets information about the currently selected variable. * - * @returns The current spectrum information, `null` when no valid spectrum + * @returns The current variable information, `null` when no valid variable * is selected, or `undefined` when the selection is mixed. */ - get currentSpectrumInfo(): AggregateSpectrumInfo | null | undefined { - return this.#currentSpectrumInfo; + get currentVariableInfo(): AggregateVariableInfo | null | undefined { + return this.#currentVariableInfo; } /** - * Gets all available aggregate spectrum options. + * Gets all available aggregate variable options. * - * @returns A map of spectrum IDs to aggregate spectrum information. + * @returns A map of variable IDs to aggregate variable information. */ - get spectrumOptions(): ReadonlyMap { - return this.#spectrumOptions; + get variableOptions(): ReadonlyMap { + return this.#variableOptions; } /** @@ -238,36 +238,36 @@ export class AggregateSelectionInfo { }; /** - * Selects a spectrum by its numeric or string identifier. + * Selects a variable by its numeric or string identifier. * - * When the identifier is not present in {@link spectrumOptions}, the - * current spectrum and display spectrum ID are set to `null`. Passing + * When the identifier is not present in {@link variableOptions}, the + * current variable and display variable ID are set to `null`. Passing * `null` or `undefined` preserves that value and clears or marks the * selection as mixed, respectively. * - * The `onSpectrumChange` handler is invoked after the selection is updated. + * The `onVariableChange` handler is invoked after the selection is updated. * - * @param id - The spectrum identifier to select, `null` to clear the + * @param id - The variable identifier to select, `null` to clear the * selection, or `undefined` to represent a mixed selection. - * @returns Information about the selected spectrum, `null` when no matching - * spectrum exists, or `undefined` for a mixed selection. + * @returns Information about the selected variable, `null` when no matching + * variable exists, or `undefined` for a mixed selection. */ - setDisplaySpectrumId = ( + setDisplayVariableId = ( id: number | string | null | undefined - ): AggregateSpectrumInfo | null | undefined => { + ): AggregateVariableInfo | null | undefined => { if (id != null) { - this.#currentSpectrumInfo = this.#spectrumOptions.get(id.toString()) ?? null; + this.#currentVariableInfo = this.#variableOptions.get(id.toString()) ?? null; - this.#displaySpectrumId = this.#currentSpectrumInfo?.id ?? null; + this.#displayVariableId = this.#currentVariableInfo?.id ?? null; } else { - this.#currentSpectrumInfo = id; - this.#displaySpectrumId = id; + this.#currentVariableInfo = id; + this.#displayVariableId = id; } - if (this.#events.onSpectrumChange != null) { - this.#events.onSpectrumChange(this.#currentSpectrumInfo); + if (this.#events.onVariableChange != null) { + this.#events.onVariableChange(this.#currentVariableInfo); } - return this.#currentSpectrumInfo; + return this.#currentVariableInfo; }; } diff --git a/src/ansys/visor/visor-client/src/aggregate/AggregateSpectrumComponentInfo.tsx b/src/ansys/visor/visor-client/src/aggregate/AggregateVariableComponentInfo.tsx similarity index 51% rename from src/ansys/visor/visor-client/src/aggregate/AggregateSpectrumComponentInfo.tsx rename to src/ansys/visor/visor-client/src/aggregate/AggregateVariableComponentInfo.tsx index d1a2eeb1..caba91b3 100644 --- a/src/ansys/visor/visor-client/src/aggregate/AggregateSpectrumComponentInfo.tsx +++ b/src/ansys/visor/visor-client/src/aggregate/AggregateVariableComponentInfo.tsx @@ -1,17 +1,17 @@ import { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; -import { VisorSpectrumComponentMetadata } from '../state/VisorSpectrumManager.tsx'; -import { AggregateSpectrumInfo } from './AggregateSpectrumInfo.tsx'; +import { VisorVariableComponentMetadata } from '../state/VisorVariableManager.tsx'; +import { AggregateVariableInfo } from './AggregateVariableInfo.tsx'; import { tryParseFloat } from '../utils/JsHelpers'; /** - * Metadata describing a spectrum component within an aggregate spectrum. + * Metadata describing a variable component within an aggregate variable. */ -type SpectrumComponentMetadata = VisorSpectrumComponentMetadata & {}; +type VariableComponentMetadata = VisorVariableComponentMetadata & {}; /** - * Event callbacks emitted when the component's displayed spectrum range changes. + * Event callbacks emitted when the component's displayed variable range changes. */ -class AggregateSpectrumComponentInfoEvents { +class AggregateVariableComponentInfoEvents { /** * Called when the displayed minimum value changes. * @@ -30,7 +30,7 @@ class AggregateSpectrumComponentInfoEvents { } /** - * Represents the aggregate display-range state for one spectrum component + * Represents the aggregate display-range state for one variable component * across a collection of scene nodes. * * A range value has the following meanings: @@ -40,37 +40,37 @@ class AggregateSpectrumComponentInfoEvents { * - `undefined`: the selected nodes have different values, or range * information is unavailable for at least one node. * - * Instances must be created with {@link AggregateSpectrumComponentInfo.getInstanceAsync}. + * Instances must be created with {@link AggregateVariableComponentInfo.getInstanceAsync}. */ -export class AggregateSpectrumComponentInfo { +export class AggregateVariableComponentInfo { /** * Prevents direct construction. * - * Use {@link AggregateSpectrumComponentInfo.getInstanceAsync} to create + * Use {@link AggregateVariableComponentInfo.getInstanceAsync} to create * and initialize an instance. */ private constructor() {} - /** Aggregate spectrum associated with this component. */ - #spectrumInfo!: AggregateSpectrumInfo; + /** Aggregate variable associated with this component. */ + #variableInfo!: AggregateVariableInfo; - /** Metadata describing this spectrum component. */ - #componentMetadata!: SpectrumComponentMetadata; + /** Metadata describing this variable component. */ + #componentMetadata!: VariableComponentMetadata; /** Shared default minimum across the selected scene nodes. */ - #displaySpectrumDefaultMin: number | null | undefined = null; + #displayVariableDefaultMin: number | null | undefined = null; /** Shared default maximum across the selected scene nodes. */ - #displaySpectrumDefaultMax: number | null | undefined = null; + #displayVariableDefaultMax: number | null | undefined = null; /** Shared custom minimum across the selected scene nodes. */ - #displaySpectrumMin: number | null | undefined = null; + #displayVariableMin: number | null | undefined = null; /** Shared custom maximum across the selected scene nodes. */ - #displaySpectrumMax: number | null | undefined = null; + #displayVariableMax: number | null | undefined = null; /** Event callbacks for changes to the custom display range. */ - #events: AggregateSpectrumComponentInfoEvents = { + #events: AggregateVariableComponentInfoEvents = { onMinChange: null, onMaxChange: null, }; @@ -82,56 +82,56 @@ export class AggregateSpectrumComponentInfo { * An endpoint is set to `undefined` when values differ between nodes or * when a node does not contain range information for this component. * - * @param actorNodes - Scene nodes whose spectrum ranges will be aggregated. - * @param spectrumInfo - Aggregate spectrum containing the component. - * @param componentMetadata - Metadata identifying the spectrum component. + * @param actorNodes - Scene nodes whose variable ranges will be aggregated. + * @param variableInfo - Aggregate variable containing the component. + * @param componentMetadata - Metadata identifying the variable component. * @returns A fully initialized aggregate component information instance. */ static async getInstanceAsync( actorNodes: VisorSceneNodeExtended[], - spectrumInfo: AggregateSpectrumInfo, - componentMetadata: SpectrumComponentMetadata - ): Promise { - const obj = new AggregateSpectrumComponentInfo(); - obj.#spectrumInfo = spectrumInfo; + variableInfo: AggregateVariableInfo, + componentMetadata: VariableComponentMetadata + ): Promise { + const obj = new AggregateVariableComponentInfo(); + obj.#variableInfo = variableInfo; obj.#componentMetadata = componentMetadata; for (let i = 0; i < actorNodes.length; i++) { const node = actorNodes[i]; - const thisSpectrum = node.spectrumCollection.getSpectrum(spectrumInfo.id); - const rangeInfo = thisSpectrum?.getRangeInfo(componentMetadata.id); + const thisVariable = node.variableCollection.getVariable(variableInfo.id); + const rangeInfo = thisVariable?.getRangeInfo(componentMetadata.id); if (rangeInfo == null) { - obj.#displaySpectrumDefaultMin = undefined; - obj.#displaySpectrumDefaultMax = undefined; - obj.#displaySpectrumMin = undefined; - obj.#displaySpectrumMax = undefined; + obj.#displayVariableDefaultMin = undefined; + obj.#displayVariableDefaultMax = undefined; + obj.#displayVariableMin = undefined; + obj.#displayVariableMax = undefined; break; } const { defaultRange, customRange } = rangeInfo; - const thisSpectrumDefaultMin: number | null = defaultRange[0]; - const thisSpectrumDefaultMax: number | null = defaultRange[1]; - const thisSpectrumMin: number | null = customRange[0]; - const thisSpectrumMax: number | null = customRange[1]; + const thisVariableDefaultMin: number | null = defaultRange[0]; + const thisVariableDefaultMax: number | null = defaultRange[1]; + const thisVariableMin: number | null = customRange[0]; + const thisVariableMax: number | null = customRange[1]; if (i === 0) { - obj.#displaySpectrumDefaultMin = thisSpectrumDefaultMin; - obj.#displaySpectrumDefaultMax = thisSpectrumDefaultMax; - obj.#displaySpectrumMin = thisSpectrumMin; - obj.#displaySpectrumMax = thisSpectrumMax; + obj.#displayVariableDefaultMin = thisVariableDefaultMin; + obj.#displayVariableDefaultMax = thisVariableDefaultMax; + obj.#displayVariableMin = thisVariableMin; + obj.#displayVariableMax = thisVariableMax; } else { - obj.#displaySpectrumDefaultMin !== thisSpectrumDefaultMin && - (obj.#displaySpectrumDefaultMin = undefined); + obj.#displayVariableDefaultMin !== thisVariableDefaultMin && + (obj.#displayVariableDefaultMin = undefined); - obj.#displaySpectrumDefaultMax !== thisSpectrumDefaultMax && - (obj.#displaySpectrumDefaultMax = undefined); + obj.#displayVariableDefaultMax !== thisVariableDefaultMax && + (obj.#displayVariableDefaultMax = undefined); - obj.#displaySpectrumMin !== thisSpectrumMin && - (obj.#displaySpectrumMin = undefined); + obj.#displayVariableMin !== thisVariableMin && + (obj.#displayVariableMin = undefined); - obj.#displaySpectrumMax !== thisSpectrumMax && - (obj.#displaySpectrumMax = undefined); + obj.#displayVariableMax !== thisVariableMax && + (obj.#displayVariableMax = undefined); } } @@ -139,24 +139,24 @@ export class AggregateSpectrumComponentInfo { } /** - * Gets the spectrum component identifier. + * Gets the variable component identifier. */ - get id(): SpectrumComponentMetadata['id'] { + get id(): VariableComponentMetadata['id'] { return this.#componentMetadata.id; } /** - * Gets the metadata associated with this spectrum component. + * Gets the metadata associated with this variable component. */ - get metadata(): SpectrumComponentMetadata { + get metadata(): VariableComponentMetadata { return this.#componentMetadata; } /** - * Gets the aggregate spectrum associated with this component. + * Gets the aggregate variable associated with this component. */ - get spectrumInfo(): AggregateSpectrumInfo { - return this.#spectrumInfo; + get variableInfo(): AggregateVariableInfo { + return this.#variableInfo; } /** @@ -165,8 +165,8 @@ export class AggregateSpectrumComponentInfo { * @returns The shared value, `null` when unset, or `undefined` when the * selected nodes do not agree or range information is unavailable. */ - get displaySpectrumDefaultMin(): number | null | undefined { - return this.#displaySpectrumDefaultMin; + get displayVariableDefaultMin(): number | null | undefined { + return this.#displayVariableDefaultMin; } /** @@ -175,8 +175,8 @@ export class AggregateSpectrumComponentInfo { * @returns The shared value, `null` when unset, or `undefined` when the * selected nodes do not agree or range information is unavailable. */ - get displaySpectrumDefaultMax(): number | null | undefined { - return this.#displaySpectrumDefaultMax; + get displayVariableDefaultMax(): number | null | undefined { + return this.#displayVariableDefaultMax; } /** @@ -185,8 +185,8 @@ export class AggregateSpectrumComponentInfo { * @returns The shared value, `null` when unset, or `undefined` when the * selected nodes do not agree or range information is unavailable. */ - get displaySpectrumMin(): number | null | undefined { - return this.#displaySpectrumMin; + get displayVariableMin(): number | null | undefined { + return this.#displayVariableMin; } /** @@ -195,54 +195,54 @@ export class AggregateSpectrumComponentInfo { * @returns The shared value, `null` when unset, or `undefined` when the * selected nodes do not agree or range information is unavailable. */ - get displaySpectrumMax(): number | null | undefined { - return this.#displaySpectrumMax; + get displayVariableMax(): number | null | undefined { + return this.#displayVariableMax; } /** * Gets the event callbacks for display-range changes. */ - get events(): AggregateSpectrumComponentInfoEvents { + get events(): AggregateVariableComponentInfoEvents { return this.#events; } /** * Parses and sets the custom minimum display value. * - * The registered {@link AggregateSpectrumComponentInfoEvents.onMinChange} + * The registered {@link AggregateVariableComponentInfoEvents.onMinChange} * callback is invoked after the value is updated. * * @param min - A numeric value, numeric string, `null`, or `undefined`. * @returns `true` when the parsed value is neither `null` nor `undefined`; * otherwise `false`. */ - setDisplaySpectrumMin = (min: number | string | null | undefined): boolean => { - this.#displaySpectrumMin = tryParseFloat(min); + setDisplayVariableMin = (min: number | string | null | undefined): boolean => { + this.#displayVariableMin = tryParseFloat(min); if (this.#events.onMinChange != null) { - this.#events.onMinChange(this.#displaySpectrumMin); + this.#events.onMinChange(this.#displayVariableMin); } - return this.#displaySpectrumMin != null; + return this.#displayVariableMin != null; }; /** * Parses and sets the custom maximum display value. * - * The registered {@link AggregateSpectrumComponentInfoEvents.onMaxChange} + * The registered {@link AggregateVariableComponentInfoEvents.onMaxChange} * callback is invoked after the value is updated. * * @param max - A numeric value, numeric string, `null`, or `undefined`. * @returns `true` when the parsed value is neither `null` nor `undefined`; * otherwise `false`. */ - setDisplaySpectrumMax = (max: number | string | null | undefined): boolean => { - this.#displaySpectrumMax = tryParseFloat(max); + setDisplayVariableMax = (max: number | string | null | undefined): boolean => { + this.#displayVariableMax = tryParseFloat(max); if (this.#events.onMaxChange != null) { - this.#events.onMaxChange(this.#displaySpectrumMax); + this.#events.onMaxChange(this.#displayVariableMax); } - return this.#displaySpectrumMax != null; + return this.#displayVariableMax != null; }; } diff --git a/src/ansys/visor/visor-client/src/aggregate/AggregateSpectrumInfo.tsx b/src/ansys/visor/visor-client/src/aggregate/AggregateVariableInfo.tsx similarity index 61% rename from src/ansys/visor/visor-client/src/aggregate/AggregateSpectrumInfo.tsx rename to src/ansys/visor/visor-client/src/aggregate/AggregateVariableInfo.tsx index a9bcffe5..003b6dea 100644 --- a/src/ansys/visor/visor-client/src/aggregate/AggregateSpectrumInfo.tsx +++ b/src/ansys/visor/visor-client/src/aggregate/AggregateVariableInfo.tsx @@ -1,29 +1,29 @@ import { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; -import { VisorSpectrumInfo } from '../state/VisorSpectrumManager.tsx'; -import { AggregateSpectrumComponentInfo } from './AggregateSpectrumComponentInfo.tsx'; +import { VisorVariableInfo } from '../state/VisorVariableManager.tsx'; +import { AggregateVariableComponentInfo } from './AggregateVariableComponentInfo.tsx'; import { AggregateSelectionInfo } from './AggregateSelectionInfo.tsx'; /** - * Represents aggregate spectrum information for a collection of selected scene + * Represents aggregate variable information for a collection of selected scene * nodes. * - * The class resolves the available spectrum components and determines whether + * The class resolves the available variable components and determines whether * the selected nodes share a common component to display. * - * Instances must be created with {@link AggregateSpectrumInfo.getInstanceAsync}. + * Instances must be created with {@link AggregateVariableInfo.getInstanceAsync}. */ -export class AggregateSpectrumInfo { +export class AggregateVariableInfo { /** - * Creates an uninitialized aggregate spectrum information object. + * Creates an uninitialized aggregate variable information object. * * @private */ private constructor() {} - /** Metadata for the spectrum represented by this instance. */ - #spectrumInfo!: VisorSpectrumInfo; + /** Metadata for the variable represented by this instance. */ + #variableInfo!: VisorVariableInfo; - /** Selection state associated with this spectrum. */ + /** Selection state associated with this variable. */ #selectionInfo!: AggregateSelectionInfo; /** @@ -39,42 +39,42 @@ export class AggregateSpectrumInfo { * Information for the currently displayed component. * * This follows the same nullability semantics as - * {@link AggregateSpectrumInfo.displayComponentId}. + * {@link AggregateVariableInfo.displayComponentId}. */ - #currentComponentInfo: AggregateSpectrumComponentInfo | null | undefined = null; + #currentComponentInfo: AggregateVariableComponentInfo | null | undefined = null; /** * Available component information, indexed by the component ID converted to * a string. */ - #componentOptions: Map = new Map(); + #componentOptions: Map = new Map(); /** - * Creates and initializes aggregate spectrum information for a collection + * Creates and initializes aggregate variable information for a collection * of scene nodes. * * Component metadata is resolved asynchronously. The selected nodes are then - * inspected to determine whether they share a common spectrum component. + * inspected to determine whether they share a common variable component. * - * When a node's configured component is unavailable for the spectrum, the + * When a node's configured component is unavailable for the variable, the * first component option is used as its fallback. * * @param actorNodes - Scene nodes included in the aggregate selection. - * @param selectionInfo - Selection state that owns this spectrum information. - * @param spectrumInfo - Metadata describing the spectrum and its components. - * @returns A fully initialized aggregate spectrum information instance. + * @param selectionInfo - Selection state that owns this variable information. + * @param variableInfo - Metadata describing the variable and its components. + * @returns A fully initialized aggregate variable information instance. */ static async getInstanceAsync( actorNodes: VisorSceneNodeExtended[], selectionInfo: AggregateSelectionInfo, - spectrumInfo: VisorSpectrumInfo - ): Promise { - const obj = new AggregateSpectrumInfo(); + variableInfo: VisorVariableInfo + ): Promise { + const obj = new AggregateVariableInfo(); obj.#selectionInfo = selectionInfo; - obj.#spectrumInfo = spectrumInfo; + obj.#variableInfo = variableInfo; - for (const componentMetadata of spectrumInfo.componentOptions) { - const val = await AggregateSpectrumComponentInfo.getInstanceAsync( + for (const componentMetadata of variableInfo.componentOptions) { + const val = await AggregateVariableComponentInfo.getInstanceAsync( actorNodes, obj, componentMetadata @@ -84,13 +84,13 @@ export class AggregateSpectrumInfo { for (let i = 0; i < actorNodes.length; i++) { const node = actorNodes[i]; - const spectrum = node.spectrumCollection.getSpectrum(spectrumInfo.id); - let thisComponentId: number | null = node.spectrumComponent; + const variable = node.variableCollection.getVariable(variableInfo.id); + let thisComponentId: number | null = node.variableComponent; - if (spectrum != null) { - if (spectrum.getRangeInfo(node.spectrumComponent) == null) { + if (variable != null) { + if (variable.getRangeInfo(node.variableComponent) == null) { // Default to the first component option. - thisComponentId = spectrum.componentOptions[0].id; + thisComponentId = variable.componentOptions[0].id; } } @@ -118,21 +118,21 @@ export class AggregateSpectrumInfo { } /** - * Gets the spectrum's unique identifier. + * Gets the variable's unique identifier. * - * @returns The spectrum identifier. + * @returns The variable identifier. */ - get id(): VisorSpectrumInfo['id'] { - return this.#spectrumInfo.id; + get id(): VisorVariableInfo['id'] { + return this.#variableInfo.id; } /** - * Gets the underlying spectrum metadata. + * Gets the underlying variable metadata. * - * @returns The spectrum metadata associated with this instance. + * @returns The variable metadata associated with this instance. */ - get metadata(): VisorSpectrumInfo { - return this.#spectrumInfo; + get metadata(): VisorVariableInfo { + return this.#variableInfo; } /** @@ -146,13 +146,13 @@ export class AggregateSpectrumInfo { } /** - * Gets all available spectrum components. + * Gets all available variable components. * * The map is keyed by each component ID converted to a string. * * @returns A map of component IDs to component information. */ - get componentOptions(): Map { + get componentOptions(): Map { return this.#componentOptions; } @@ -163,15 +163,15 @@ export class AggregateSpectrumInfo { * is selected, or `undefined` when the selected nodes do not share a common * component. */ - get currentComponentInfo(): AggregateSpectrumComponentInfo | null | undefined { + get currentComponentInfo(): AggregateVariableComponentInfo | null | undefined { return this.#currentComponentInfo; } /** - * Changes the component displayed for the current spectrum. + * Changes the component displayed for the current variable. * - * The spectrum must be the current spectrum in the associated selection. - * After the value is updated, the selection's spectrum-component-change + * The variable must be the current variable in the associated selection. + * After the value is updated, the selection's variable-component-change * callback is invoked when one is registered. * * @param id - The component ID to display. Numeric and string IDs are @@ -179,16 +179,16 @@ export class AggregateSpectrumInfo { * marks the component state accordingly. * @returns Information for the selected component, `null` when the ID does * not match an available component, or `undefined` when explicitly passed. - * @throws {Error} When there is no current spectrum. - * @throws {Error} When this instance is not the current spectrum. + * @throws {Error} When there is no current variable. + * @throws {Error} When this instance is not the current variable. */ setDisplayComponentId = ( id: number | string | null | undefined - ): AggregateSpectrumComponentInfo | null | undefined => { - if (this.#selectionInfo.currentSpectrumInfo == null) { - throw new Error('component should not be changed when the current spectrum is null'); - } else if (this.#selectionInfo.currentSpectrumInfo !== this) { - throw new Error('component should not be changed on a spectrum that is not current'); + ): AggregateVariableComponentInfo | null | undefined => { + if (this.#selectionInfo.currentVariableInfo == null) { + throw new Error('component should not be changed when the current variable is null'); + } else if (this.#selectionInfo.currentVariableInfo !== this) { + throw new Error('component should not be changed on a variable that is not current'); } if (id != null) { @@ -201,8 +201,8 @@ export class AggregateSpectrumInfo { const events = this.#selectionInfo.events; - if (events.onSpectrumComponentChange != null) { - events.onSpectrumComponentChange(this.#selectionInfo.currentSpectrumInfo); + if (events.onVariableComponentChange != null) { + events.onVariableComponentChange(this.#selectionInfo.currentVariableInfo); } return this.#currentComponentInfo; diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx index 42f45b92..3b8b4ebb 100644 --- a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx +++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx @@ -2,8 +2,8 @@ import { FC, useEffect } from 'react'; import { VisorFrontend } from '../../VisorFrontend.tsx'; import { AwcIcons } from '../AwcIcons'; import { VisorSceneNodeExtended } from '../../state/VisorSceneGraph.tsx'; -import { AggregateSpectrumComponentInfo } from '../../aggregate/AggregateSpectrumComponentInfo.tsx'; -import { AggregateSpectrumInfo } from '../../aggregate/AggregateSpectrumInfo.tsx'; +import { AggregateVariableComponentInfo } from '../../aggregate/AggregateVariableComponentInfo.tsx'; +import { AggregateVariableInfo } from '../../aggregate/AggregateVariableInfo.tsx'; import { Panel_TopRight_Util } from './Panel_TopRight_Util.tsx'; import { AggregateSelectionInfo } from '../../aggregate/AggregateSelectionInfo.tsx'; import { @@ -487,31 +487,31 @@ export const Panel_TopRight: FC<{ const aggregateSelectionInfo = await AggregateSelectionInfo.getInstanceAsync(actorNodes); - aggregateSelectionInfo.events.onSpectrumComponentChange = async (spectrumInfo) => { - await spectrumComponentChangeHandler(spectrumInfo); - await applySpectrumAsync(spectrumInfo?.currentComponentInfo); + aggregateSelectionInfo.events.onVariableComponentChange = async (variableInfo) => { + await variableComponentChangeHandler(variableInfo); + await applyVariableAsync(variableInfo?.currentComponentInfo); }; - aggregateSelectionInfo.events.onSpectrumChange = async (spectrumInfo) => { - await spectrumChangeHandler(spectrumInfo); - await applySpectrumAsync(spectrumInfo?.currentComponentInfo); + aggregateSelectionInfo.events.onVariableChange = async (variableInfo) => { + await variableChangeHandler(variableInfo); + await applyVariableAsync(variableInfo?.currentComponentInfo); }; - await spectrumChangeHandler(aggregateSelectionInfo.currentSpectrumInfo); - await spectrumComponentChangeHandler(aggregateSelectionInfo.currentSpectrumInfo); + await variableChangeHandler(aggregateSelectionInfo.currentVariableInfo); + await variableComponentChangeHandler(aggregateSelectionInfo.currentVariableInfo); - async function spectrumComponentChangeHandler( - spectrumInfo: AggregateSpectrumInfo | null | undefined + async function variableComponentChangeHandler( + variableInfo: AggregateVariableInfo | null | undefined ) { - const componentInfo = spectrumInfo?.currentComponentInfo; - if (spectrumInfo == null || componentInfo == null) { + const componentInfo = variableInfo?.currentComponentInfo; + if (variableInfo == null || componentInfo == null) { legendPanelNoRangeElem.style.removeProperty('display'); legendPanelRangeContainer.style.display = 'none'; legendOverlayElem.style.display = 'none'; legendOverlayElem_alt.style.display = 'none'; - if (spectrumInfo === null) { + if (variableInfo === null) { legendPanelNoRangeElem.innerHTML = `No data array is selected.`; - } else if (spectrumInfo === undefined) { + } else if (variableInfo === undefined) { const msg = `Selected parts must all have the same data array and component`; legendPanelNoRangeElem.innerHTML = `${msg} in order to edit min/max values.`; } else if (componentInfo === null) { @@ -522,21 +522,21 @@ export const Panel_TopRight: FC<{ } } else { componentInfo.events.onMinChange = () => { - updateMinLabel(componentInfo.displaySpectrumMin); - updateLegend(spectrumInfo); + updateMinLabel(componentInfo.displayVariableMin); + updateLegend(variableInfo); }; componentInfo.events.onMaxChange = () => { - updateMaxLabel(componentInfo.displaySpectrumMax); - updateLegend(spectrumInfo); + updateMaxLabel(componentInfo.displayVariableMax); + updateLegend(variableInfo); }; - updateMinLabel(componentInfo.displaySpectrumMin); - updateMaxLabel(componentInfo.displaySpectrumMax); - updateLegend(spectrumInfo); + updateMinLabel(componentInfo.displayVariableMin); + updateMaxLabel(componentInfo.displayVariableMax); + updateLegend(variableInfo); } } - function updateLegend(spectrumInfo: AggregateSpectrumInfo) { - const componentInfo = spectrumInfo.currentComponentInfo; + function updateLegend(variableInfo: AggregateVariableInfo) { + const componentInfo = variableInfo.currentComponentInfo; if (componentInfo == null) { throw new Error('componentInfo should not be null here'); } @@ -544,24 +544,24 @@ export const Panel_TopRight: FC<{ legendPanelRangeContainer.style.removeProperty('display'); legendOverlayElem.style.removeProperty('display'); legendOverlayElem_alt.style.removeProperty('display'); - const spectrumMetadata = spectrumInfo.metadata; + const variableMetadata = variableInfo.metadata; const overlayTitle = `
- + - + - + ${ - spectrumMetadata.numComponents > 1 + variableMetadata.numComponents > 1 ? ` @@ -576,8 +576,8 @@ export const Panel_TopRight: FC<{ legendOverlayTitleContainer.innerHTML = overlayTitle; legendOverlayTitleContainer_alt.innerHTML = overlayTitle; { - const min = componentInfo.displaySpectrumMin; - const max = componentInfo.displaySpectrumMax; + const min = componentInfo.displayVariableMin; + const max = componentInfo.displayVariableMax; const minRounded = typeof min === 'number' ? fixValue(min) @@ -602,47 +602,47 @@ export const Panel_TopRight: FC<{ } } - async function spectrumChangeHandler( - spectrumInfo: AggregateSpectrumInfo | null | undefined + async function variableChangeHandler( + variableInfo: AggregateVariableInfo | null | undefined ) { - await spectrumComponentChangeHandler(spectrumInfo); + await variableComponentChangeHandler(variableInfo); componentSelectElem.options.length = 0; - if (spectrumInfo != null) { + if (variableInfo != null) { constantRgbInputContainer.style.display = 'none'; propertyPanelComponentContainer.style.removeProperty('display'); propertyPanelNoComponentContainer.style.display = 'none'; - if (spectrumInfo.displayComponentId === undefined) { + if (variableInfo.displayComponentId === undefined) { const optionElem = document.createElement('option'); optionElem.value = ''; optionElem.text = ''; componentSelectElem.options.add(optionElem); } - for (const item of spectrumInfo.componentOptions.values()) { + for (const item of variableInfo.componentOptions.values()) { const option = document.createElement('option'); option.value = item.id.toString(); option.text = item.metadata.name; - option.selected = item.id === spectrumInfo.displayComponentId; + option.selected = item.id === variableInfo.displayComponentId; componentSelectElem.options.add(option); } componentSelectElem.disabled = componentSelectElem.options.length === 1; const originalOptionCount = componentSelectElem.options.length; componentSelectElem.onchange = async () => { - if (spectrumInfo.displayComponentId === undefined) { + if (variableInfo.displayComponentId === undefined) { if (originalOptionCount === componentSelectElem.options.length) { if (componentSelectElem.selectedIndex !== 0) { componentSelectElem.options[0].remove(); } } } - spectrumInfo.setDisplayComponentId(componentSelectElem.value); + variableInfo.setDisplayComponentId(componentSelectElem.value); }; } else { propertyPanelComponentContainer.style.display = 'none'; - if (spectrumInfo === null) { + if (variableInfo === null) { constantRgbInputContainer.style.removeProperty('display'); propertyPanelNoComponentContainer.style.display = 'none'; - await clearSpectrumAsync(); - } else if (spectrumInfo === undefined) { + await clearVariableAsync(); + } else if (variableInfo === undefined) { constantRgbInputContainer.style.display = 'none'; propertyPanelNoComponentContainer.style.removeProperty('display'); const msg = `Selected parts must all have the same data array in order to`; @@ -715,21 +715,21 @@ export const Panel_TopRight: FC<{ }; legendPanelApplyRangeButton.onclick = async () => { const componentInfo = - aggregateSelectionInfo.currentSpectrumInfo?.currentComponentInfo; + aggregateSelectionInfo.currentVariableInfo?.currentComponentInfo; if (componentInfo == null) { throw new Error( `min/max should not be edited if the current component info is null or undefined` ); - } else if (componentInfo.setDisplaySpectrumMin(legendPanelMinInput.value)) { - if (componentInfo.setDisplaySpectrumMax(legendPanelMaxInput.value)) { + } else if (componentInfo.setDisplayVariableMin(legendPanelMinInput.value)) { + if (componentInfo.setDisplayVariableMax(legendPanelMaxInput.value)) { const { id: componentId, - spectrumInfo: { id: spectrumId }, - displaySpectrumMin: min, - displaySpectrumMax: max, + variableInfo: { id: variableId }, + displayVariableMin: min, + displayVariableMax: max, } = componentInfo; - await visorState.setSpectrumRangeAsync( - spectrumId, + await visorState.setVariableRangeAsync( + variableId, componentId, min!, max! @@ -741,24 +741,24 @@ export const Panel_TopRight: FC<{ legendPanelMinResetButton.onclick = async (e) => { e.preventDefault(); const componentInfo = - aggregateSelectionInfo.currentSpectrumInfo?.currentComponentInfo; + aggregateSelectionInfo.currentVariableInfo?.currentComponentInfo; if (componentInfo == null) { throw new Error( `min/max should not be edited if the current component info is null or undefined` ); } - updateMinLabel(componentInfo.displaySpectrumDefaultMin); + updateMinLabel(componentInfo.displayVariableDefaultMin); }; legendPanelMaxResetButton.onclick = async (e) => { e.preventDefault(); const componentInfo = - aggregateSelectionInfo.currentSpectrumInfo?.currentComponentInfo; + aggregateSelectionInfo.currentVariableInfo?.currentComponentInfo; if (componentInfo == null) { throw new Error( `min/max should not be edited if the current component info is null or undefined` ); } - updateMaxLabel(componentInfo.displaySpectrumDefaultMax); + updateMaxLabel(componentInfo.displayVariableDefaultMax); }; diffuseColorResetButton.onclick = async (e) => { e.preventDefault(); @@ -800,7 +800,7 @@ export const Panel_TopRight: FC<{ await visorState.render(); } - async function clearSpectrumAsync() { + async function clearVariableAsync() { const promises = []; for (const node of actorNodes) { const promise = node.clearColorVariableAsync(); @@ -810,8 +810,8 @@ export const Panel_TopRight: FC<{ await visorState.render(); } - async function applySpectrumAsync( - componentInfo?: AggregateSpectrumComponentInfo | null, + async function applyVariableAsync( + componentInfo?: AggregateVariableComponentInfo | null, defaultMin?: boolean, defaultMax?: boolean, typing?: boolean @@ -820,23 +820,23 @@ export const Panel_TopRight: FC<{ return; } const { - spectrumInfo, - displaySpectrumDefaultMin, - displaySpectrumDefaultMax, - displaySpectrumMin, - displaySpectrumMax, + variableInfo, + displayVariableDefaultMin, + displayVariableDefaultMax, + displayVariableMin, + displayVariableMax, } = componentInfo; const min = - defaultMin === true ? displaySpectrumDefaultMin : displaySpectrumMin; + defaultMin === true ? displayVariableDefaultMin : displayVariableMin; const max = - defaultMax === true ? displaySpectrumDefaultMax : displaySpectrumMax; + defaultMax === true ? displayVariableDefaultMax : displayVariableMax; updateMinLabel(min, typing); updateMaxLabel(max, typing); - updateLegend(spectrumInfo); + updateLegend(variableInfo); const promises = []; for (const node of actorNodes) { const promise = node.setColorVariableAsync( - spectrumInfo.id, + variableInfo.id, componentInfo.id ); promises.push(promise); @@ -856,7 +856,7 @@ export const Panel_TopRight: FC<{ return optionElem; })() ); - if (aggregateSelectionInfo.displaySpectrumId === undefined) { + if (aggregateSelectionInfo.displayVariableId === undefined) { arr.unshift( (() => { const optionElem = document.createElement('option'); @@ -869,7 +869,7 @@ export const Panel_TopRight: FC<{ return arr; })() ); - aggregateSelectionInfo.spectrumOptions.forEach((item) => { + aggregateSelectionInfo.variableOptions.forEach((item) => { const { id, metadata } = item; const { type, name, fullName, numComponents } = metadata; const optionElem = document.createElement('option'); @@ -877,19 +877,19 @@ export const Panel_TopRight: FC<{ optionElem.value = id.toString(); optionElem.dataset.name = name; optionElem.dataset.type = type; - optionElem.selected = id === aggregateSelectionInfo.displaySpectrumId; + optionElem.selected = id === aggregateSelectionInfo.displayVariableId; variableSelectElem.options.add(optionElem); }); const originalOptionCount = variableSelectElem.options.length; variableSelectElem.onchange = async () => { - if (aggregateSelectionInfo.displaySpectrumId === undefined) { + if (aggregateSelectionInfo.displayVariableId === undefined) { if (originalOptionCount === variableSelectElem.options.length) { if (variableSelectElem.selectedIndex !== 0) { variableSelectElem.options[0].remove(); } } } - aggregateSelectionInfo.setDisplaySpectrumId(variableSelectElem.value); + aggregateSelectionInfo.setDisplayVariableId(variableSelectElem.value); }; } else { propertyPanelBodyElem.style.display = 'none'; diff --git a/src/ansys/visor/visor-client/src/jest-tests/AggregateSelectionInfo.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/AggregateSelectionInfo.test.tsx index 5f4fe9a0..b82838ad 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/AggregateSelectionInfo.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/AggregateSelectionInfo.test.tsx @@ -1,9 +1,9 @@ import { AggregateSelectionInfo } from '../aggregate/AggregateSelectionInfo.tsx'; import type { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; import type { - VisorSpectrumComponentMetadata, - VisorSpectrumInfo, -} from '../state/VisorSpectrumManager.tsx'; + VisorVariableComponentMetadata, + VisorVariableInfo, +} from '../state/VisorVariableManager.tsx'; describe('AggregateSelectionInfo', () => { afterEach(() => { @@ -15,31 +15,31 @@ describe('AggregateSelectionInfo', () => { expect(result.displayName).toBeNull(); expect(result.displayOpacity).toBeNull(); - expect(result.displaySpectrumId).toBeNull(); + expect(result.displayVariableId).toBeNull(); expect(result.displayDiffuseColor).toBeNull(); - expect(result.currentSpectrumInfo).toBeNull(); - expect(result.spectrumOptions.size).toBe(0); + expect(result.currentVariableInfo).toBeNull(); + expect(result.variableOptions.size).toBe(0); }); test('aggregates matching values from all actor nodes', async () => { - const spectrum = createSpectrum(10, [1]); + const variable = createVariable(10, [1]); const actorNodes = [ createActorNode({ name: 'mesh', opacity: 0.5, - spectrumId: '10', - spectrumComponent: 1, + variableId: '10', + variableComponent: 1, customDiffuseColorHex: '#123456', - spectra: [spectrum], + spectra: [variable], }), createActorNode({ name: 'mesh', opacity: 0.5, - spectrumId: '10', - spectrumComponent: 1, + variableId: '10', + variableComponent: 1, customDiffuseColorHex: '#123456', - spectra: [spectrum], + spectra: [variable], }), ]; @@ -47,84 +47,84 @@ describe('AggregateSelectionInfo', () => { expect(result.displayName).toBe('mesh'); expect(result.displayOpacity).toBe(0.5); - expect(result.displaySpectrumId).toBe('10'); + expect(result.displayVariableId).toBe('10'); expect(result.displayDiffuseColor).toBe('#123456'); - expect(result.spectrumOptions.size).toBe(1); - expect(result.spectrumOptions.has('10')).toBe(true); - expect(result.currentSpectrumInfo).toBe(result.spectrumOptions.get('10')); - expect(result.currentSpectrumInfo?.id).toBe('10'); + expect(result.variableOptions.size).toBe(1); + expect(result.variableOptions.has('10')).toBe(true); + expect(result.currentVariableInfo).toBe(result.variableOptions.get('10')); + expect(result.currentVariableInfo?.id).toBe('10'); }); - test('stores each spectrum only once when several nodes expose it', async () => { - const spectrum = createSpectrum(10, [1]); + test('stores each variable only once when several nodes expose it', async () => { + const variable = createVariable(10, [1]); const actorNodes = [ createActorNode({ - spectra: [spectrum], - spectrumId: '10', - spectrumComponent: 1, + spectra: [variable], + variableId: '10', + variableComponent: 1, }), createActorNode({ - spectra: [spectrum], - spectrumId: '10', - spectrumComponent: 1, + spectra: [variable], + variableId: '10', + variableComponent: 1, }), createActorNode({ - spectra: [spectrum], - spectrumId: '10', - spectrumComponent: 1, + spectra: [variable], + variableId: '10', + variableComponent: 1, }), ]; const result = await AggregateSelectionInfo.getInstanceAsync(actorNodes); - expect(result.spectrumOptions.size).toBe(1); - expect([...result.spectrumOptions.keys()]).toEqual(['10']); + expect(result.variableOptions.size).toBe(1); + expect([...result.variableOptions.keys()]).toEqual(['10']); }); - test('collects different spectrum options from the actor nodes', async () => { - const spectrum10 = createSpectrum(10, [1]); - const spectrum20 = createSpectrum(20, [1]); + test('collects different variable options from the actor nodes', async () => { + const variable10 = createVariable(10, [1]); + const variable20 = createVariable(20, [1]); const actorNodes = [ createActorNode({ - spectra: [spectrum10], - spectrumId: '10', - spectrumComponent: 1, + spectra: [variable10], + variableId: '10', + variableComponent: 1, }), createActorNode({ - spectra: [spectrum20], - spectrumId: '10', - spectrumComponent: 1, + spectra: [variable20], + variableId: '10', + variableComponent: 1, }), ]; const result = await AggregateSelectionInfo.getInstanceAsync(actorNodes); - expect([...result.spectrumOptions.keys()]).toEqual(['10', '20']); + expect([...result.variableOptions.keys()]).toEqual(['10', '20']); }); test('uses undefined for values that differ between actor nodes', async () => { - const spectrum10 = createSpectrum(10, [1]); - const spectrum20 = createSpectrum(20, [1]); + const variable10 = createVariable(10, [1]); + const variable20 = createVariable(20, [1]); const actorNodes = [ createActorNode({ name: 'mesh A', opacity: 0.25, - spectrumId: '10', - spectrumComponent: 1, + variableId: '10', + variableComponent: 1, customDiffuseColorHex: '#111111', - spectra: [spectrum10, spectrum20], + spectra: [variable10, variable20], }), createActorNode({ name: 'mesh B', opacity: 0.75, - spectrumId: '20', - spectrumComponent: 1, + variableId: '20', + variableComponent: 1, customDiffuseColorHex: '#222222', - spectra: [spectrum10, spectrum20], + spectra: [variable10, variable20], }), ]; @@ -132,30 +132,30 @@ describe('AggregateSelectionInfo', () => { expect(result.displayName).toBeUndefined(); expect(result.displayOpacity).toBeUndefined(); - expect(result.displaySpectrumId).toBeUndefined(); + expect(result.displayVariableId).toBeUndefined(); expect(result.displayDiffuseColor).toBeUndefined(); - expect(result.currentSpectrumInfo).toBeUndefined(); + expect(result.currentVariableInfo).toBeUndefined(); }); test('preserves values that match while marking only differing values undefined', async () => { - const spectrum = createSpectrum(10, [1]); + const variable = createVariable(10, [1]); const actorNodes = [ createActorNode({ name: 'same name', opacity: 0.25, - spectrumId: '10', - spectrumComponent: 1, + variableId: '10', + variableComponent: 1, customDiffuseColorHex: '#123456', - spectra: [spectrum], + spectra: [variable], }), createActorNode({ name: 'same name', opacity: 0.75, - spectrumId: '10', - spectrumComponent: 1, + variableId: '10', + variableComponent: 1, customDiffuseColorHex: '#123456', - spectra: [spectrum], + spectra: [variable], }), ]; @@ -163,24 +163,24 @@ describe('AggregateSelectionInfo', () => { expect(result.displayName).toBe('same name'); expect(result.displayOpacity).toBeUndefined(); - expect(result.displaySpectrumId).toBe('10'); + expect(result.displayVariableId).toBe('10'); expect(result.displayDiffuseColor).toBe('#123456'); }); - test('warns when the common spectrum ID is not an available option', async () => { + test('warns when the common variable ID is not an available option', async () => { const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); const actorNode = createActorNode({ - spectrumId: '999', - spectrumComponent: 1, - spectra: [createSpectrum(10, [1])], + variableId: '999', + variableComponent: 1, + spectra: [createVariable(10, [1])], }); const result = await AggregateSelectionInfo.getInstanceAsync([actorNode]); - expect(result.displaySpectrumId).toBe('999'); - expect(result.currentSpectrumInfo).toBeUndefined(); - expect(warningSpy).toHaveBeenCalledWith("invalid spectrumId: '999'"); + expect(result.displayVariableId).toBe('999'); + expect(result.currentVariableInfo).toBeUndefined(); + expect(warningSpy).toHaveBeenCalledWith("invalid variableId: '999'"); }); test('setDisplayName updates the displayed name', async () => { @@ -226,116 +226,116 @@ describe('AggregateSelectionInfo', () => { expect(result.displayOpacity).toBeNull(); }); - test('setDisplaySpectrumId selects an available spectrum', async () => { - const spectrum10 = createSpectrum(10, [1]); - const spectrum20 = createSpectrum(20, [1]); + test('setDisplayVariableId selects an available variable', async () => { + const variable10 = createVariable(10, [1]); + const variable20 = createVariable(20, [1]); const result = await AggregateSelectionInfo.getInstanceAsync([ createActorNode({ - spectrumId: '10', - spectrumComponent: 1, - spectra: [spectrum10, spectrum20], + variableId: '10', + variableComponent: 1, + spectra: [variable10, variable20], }), ]); - const selected = result.setDisplaySpectrumId(20); + const selected = result.setDisplayVariableId(20); - expect(selected).toBe(result.spectrumOptions.get('20')); - expect(result.currentSpectrumInfo).toBe(result.spectrumOptions.get('20')); - expect(result.displaySpectrumId).toBe('20'); + expect(selected).toBe(result.variableOptions.get('20')); + expect(result.currentVariableInfo).toBe(result.variableOptions.get('20')); + expect(result.displayVariableId).toBe('20'); }); - test('setDisplaySpectrumId accepts a string ID', async () => { - const spectrum = createSpectrum(10, [1]); + test('setDisplayVariableId accepts a string ID', async () => { + const variable = createVariable(10, [1]); const result = await AggregateSelectionInfo.getInstanceAsync([ createActorNode({ - spectrumId: null, - spectrumComponent: 1, - spectra: [spectrum], + variableId: null, + variableComponent: 1, + spectra: [variable], }), ]); - const selected = result.setDisplaySpectrumId('10'); + const selected = result.setDisplayVariableId('10'); - expect(selected).toBe(result.spectrumOptions.get('10')); - expect(result.displaySpectrumId).toBe('10'); + expect(selected).toBe(result.variableOptions.get('10')); + expect(result.displayVariableId).toBe('10'); }); - test('setDisplaySpectrumId returns null for an unavailable spectrum', async () => { - const spectrum = createSpectrum(10, [1]); + test('setDisplayVariableId returns null for an unavailable variable', async () => { + const variable = createVariable(10, [1]); const result = await AggregateSelectionInfo.getInstanceAsync([ createActorNode({ - spectrumId: '10', - spectrumComponent: 1, - spectra: [spectrum], + variableId: '10', + variableComponent: 1, + spectra: [variable], }), ]); - const selected = result.setDisplaySpectrumId('999'); + const selected = result.setDisplayVariableId('999'); expect(selected).toBeNull(); - expect(result.currentSpectrumInfo).toBeNull(); - expect(result.displaySpectrumId).toBeNull(); + expect(result.currentVariableInfo).toBeNull(); + expect(result.displayVariableId).toBeNull(); }); - test('setDisplaySpectrumId preserves null and undefined', async () => { - const spectrum = createSpectrum(10, [1]); + test('setDisplayVariableId preserves null and undefined', async () => { + const variable = createVariable(10, [1]); const result = await AggregateSelectionInfo.getInstanceAsync([ createActorNode({ - spectrumId: '10', - spectrumComponent: 1, - spectra: [spectrum], + variableId: '10', + variableComponent: 1, + spectra: [variable], }), ]); - expect(result.setDisplaySpectrumId(null)).toBeNull(); - expect(result.currentSpectrumInfo).toBeNull(); - expect(result.displaySpectrumId).toBeNull(); + expect(result.setDisplayVariableId(null)).toBeNull(); + expect(result.currentVariableInfo).toBeNull(); + expect(result.displayVariableId).toBeNull(); - expect(result.setDisplaySpectrumId(undefined)).toBeUndefined(); - expect(result.currentSpectrumInfo).toBeUndefined(); - expect(result.displaySpectrumId).toBeUndefined(); + expect(result.setDisplayVariableId(undefined)).toBeUndefined(); + expect(result.currentVariableInfo).toBeUndefined(); + expect(result.displayVariableId).toBeUndefined(); }); - test('setDisplaySpectrumId invokes onSpectrumChange', async () => { - const spectrum10 = createSpectrum(10, [1]); - const spectrum20 = createSpectrum(20, [1]); + test('setDisplayVariableId invokes onVariableChange', async () => { + const variable10 = createVariable(10, [1]); + const variable20 = createVariable(20, [1]); const result = await AggregateSelectionInfo.getInstanceAsync([ createActorNode({ - spectrumId: '10', - spectrumComponent: 1, - spectra: [spectrum10, spectrum20], + variableId: '10', + variableComponent: 1, + spectra: [variable10, variable20], }), ]); const handler = jest.fn(); - result.events.onSpectrumChange = handler; + result.events.onVariableChange = handler; - const selected = result.setDisplaySpectrumId(20); + const selected = result.setDisplayVariableId(20); expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith(selected); }); - test('setDisplaySpectrumId invokes the event with null for an invalid ID', async () => { - const spectrum = createSpectrum(10, [1]); + test('setDisplayVariableId invokes the event with null for an invalid ID', async () => { + const variable = createVariable(10, [1]); const result = await AggregateSelectionInfo.getInstanceAsync([ createActorNode({ - spectrumId: '10', - spectrumComponent: 1, - spectra: [spectrum], + variableId: '10', + variableComponent: 1, + spectra: [variable], }), ]); const handler = jest.fn(); - result.events.onSpectrumChange = handler; + result.events.onVariableChange = handler; - result.setDisplaySpectrumId(999); + result.setDisplayVariableId(999); expect(handler).toHaveBeenCalledWith(null); }); @@ -343,15 +343,15 @@ describe('AggregateSelectionInfo', () => { test('events initially contain null handlers', async () => { const result = await AggregateSelectionInfo.getInstanceAsync([]); - expect(result.events.onSpectrumChange).toBeNull(); - expect(result.events.onSpectrumComponentChange).toBeNull(); + expect(result.events.onVariableChange).toBeNull(); + expect(result.events.onVariableComponentChange).toBeNull(); }); }); -interface SpectrumFixture { - metadata: VisorSpectrumInfo; +interface VariableFixture { + metadata: VisorVariableInfo; runtime: { - componentOptions: VisorSpectrumComponentMetadata[]; + componentOptions: VisorVariableComponentMetadata[]; getRangeInfo: jest.Mock; }; } @@ -359,27 +359,27 @@ interface SpectrumFixture { interface ActorNodeOptions { name?: string | null; opacity?: number | null; - spectrumId?: string | null; - spectrumComponent?: number; + variableId?: string | null; + variableComponent?: number; customDiffuseColorHex?: string | null; - spectra?: SpectrumFixture[]; + spectra?: VariableFixture[]; } -function createSpectrum(id: number, componentIds: number[]): SpectrumFixture { +function createVariable(id: number, componentIds: number[]): VariableFixture { const componentOptions = componentIds.map((componentId) => ({ id: componentId, name: `Component ${componentId}`, - })) as VisorSpectrumComponentMetadata[]; + })) as VisorVariableComponentMetadata[]; const validIds = new Set(componentIds); const idString = id.toString(); - const metadata: VisorSpectrumInfo = { + const metadata: VisorVariableInfo = { id: idString, type: 'POINT', - name: `spectrum-${idString}`, + name: `variable-${idString}`, shape: componentIds.length === 1 ? 'Scalar' : `Vector${componentIds.length}`, - fullName: `POINT - spectrum-${idString}`, + fullName: `POINT - variable-${idString}`, numComponents: componentIds.length, componentOptions, getRangeInfo: (componentId) => { @@ -416,24 +416,24 @@ function createSpectrum(id: number, componentIds: number[]): SpectrumFixture { function createActorNode({ name = 'mesh', opacity = 1, - spectrumId = null, - spectrumComponent = 0, + variableId = null, + variableComponent = 0, customDiffuseColorHex = '#ffffff', spectra = [], }: ActorNodeOptions = {}): VisorSceneNodeExtended { return { name, opacity, - spectrumId, - spectrumComponent, + variableId, + variableComponent, customDiffuseColorHex, - spectrumCollection: { - array: spectra.map((spectrum) => spectrum.metadata), + variableCollection: { + array: spectra.map((variable) => variable.metadata), - getSpectrum: jest.fn((id: number | string) => { + getVariable: jest.fn((id: number | string) => { const match = spectra.find( - (spectrum) => spectrum.metadata.id.toString() === id.toString() + (variable) => variable.metadata.id.toString() === id.toString() ); return match?.runtime ?? null; diff --git a/src/ansys/visor/visor-client/src/jest-tests/AggregateSpectrumComponentInfo.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/AggregateVariableComponentInfo.test.tsx similarity index 53% rename from src/ansys/visor/visor-client/src/jest-tests/AggregateSpectrumComponentInfo.test.tsx rename to src/ansys/visor/visor-client/src/jest-tests/AggregateVariableComponentInfo.test.tsx index ee60188f..7c19c0e1 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/AggregateSpectrumComponentInfo.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/AggregateVariableComponentInfo.test.tsx @@ -1,315 +1,315 @@ import { AggregateSelectionInfo } from '../aggregate/AggregateSelectionInfo.tsx'; -import { AggregateSpectrumComponentInfo } from '../aggregate/AggregateSpectrumComponentInfo.tsx'; -import type { AggregateSpectrumInfo } from '../aggregate/AggregateSpectrumInfo.tsx'; +import { AggregateVariableComponentInfo } from '../aggregate/AggregateVariableComponentInfo.tsx'; +import type { AggregateVariableInfo } from '../aggregate/AggregateVariableInfo.tsx'; import type { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; import type { - VisorSpectrumCollection, - VisorSpectrumComponentMetadata, - VisorSpectrumInfo, -} from '../state/VisorSpectrumManager.tsx'; + VisorVariableCollection, + VisorVariableComponentMetadata, + VisorVariableInfo, +} from '../state/VisorVariableManager.tsx'; import type { FieldAssociation } from '../state/appstate/vtkInfo/VisorVtkDataArray.tsx'; -describe('AggregateSpectrumComponentInfo', () => { +describe('AggregateVariableComponentInfo', () => { afterEach(() => { jest.restoreAllMocks(); }); - test('exposes its component ID, metadata, and parent spectrum', async () => { - const parentSpectrum = await createParentSpectrum(); - const metadata = getComponentMetadata(parentSpectrum, 0); + test('exposes its component ID, metadata, and parent variable', async () => { + const parentVariable = await createParentVariable(); + const metadata = getComponentMetadata(parentVariable, 0); - const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + const result = await AggregateVariableComponentInfo.getInstanceAsync( [ createActorNode({ - spectrumId: parentSpectrum.id, - spectrumComponent: 0, + variableId: parentVariable.id, + variableComponent: 0, spectra: [ - createSpectrum({ - id: parentSpectrum.id, + createVariable({ + id: parentVariable.id, componentIds: [-1, 0, 1, 2], }), ], }), ], - parentSpectrum, + parentVariable, metadata ); - expect(result).toBeInstanceOf(AggregateSpectrumComponentInfo); + expect(result).toBeInstanceOf(AggregateVariableComponentInfo); expect(result.id).toBe(0); expect(result.metadata).toBe(metadata); - expect(result.spectrumInfo).toBe(parentSpectrum); + expect(result.variableInfo).toBe(parentVariable); }); test('returns null aggregate values when there are no actor nodes', async () => { - const parentSpectrum = await createParentSpectrum(); - const metadata = getComponentMetadata(parentSpectrum, 0); + const parentVariable = await createParentVariable(); + const metadata = getComponentMetadata(parentVariable, 0); - const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + const result = await AggregateVariableComponentInfo.getInstanceAsync( [], - parentSpectrum, + parentVariable, metadata ); - expect(result.displaySpectrumDefaultMin).toBeNull(); - expect(result.displaySpectrumDefaultMax).toBeNull(); - expect(result.displaySpectrumMin).toBeNull(); - expect(result.displaySpectrumMax).toBeNull(); + expect(result.displayVariableDefaultMin).toBeNull(); + expect(result.displayVariableDefaultMax).toBeNull(); + expect(result.displayVariableMin).toBeNull(); + expect(result.displayVariableMax).toBeNull(); }); test('aggregates matching range values from all actor nodes', async () => { - const parentSpectrum = await createParentSpectrum(); - const metadata = getComponentMetadata(parentSpectrum, 0); + const parentVariable = await createParentVariable(); + const metadata = getComponentMetadata(parentVariable, 0); - const firstSpectrum = createSpectrum({ - id: parentSpectrum.id, + const firstVariable = createVariable({ + id: parentVariable.id, componentIds: [-1, 0, 1, 2], ranges: new Map([[0, createRange(-10, 10, -5, 5)]]), }); - const secondSpectrum = createSpectrum({ - id: parentSpectrum.id, + const secondVariable = createVariable({ + id: parentVariable.id, componentIds: [-1, 0, 1, 2], ranges: new Map([[0, createRange(-10, 10, -5, 5)]]), }); - const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + const result = await AggregateVariableComponentInfo.getInstanceAsync( [ createActorNode({ - spectrumId: parentSpectrum.id, - spectrumComponent: 0, - spectra: [firstSpectrum], + variableId: parentVariable.id, + variableComponent: 0, + spectra: [firstVariable], }), createActorNode({ - spectrumId: parentSpectrum.id, - spectrumComponent: 0, - spectra: [secondSpectrum], + variableId: parentVariable.id, + variableComponent: 0, + spectra: [secondVariable], }), ], - parentSpectrum, + parentVariable, metadata ); - expect(result.displaySpectrumDefaultMin).toBe(-10); - expect(result.displaySpectrumDefaultMax).toBe(10); - expect(result.displaySpectrumMin).toBe(-5); - expect(result.displaySpectrumMax).toBe(5); + expect(result.displayVariableDefaultMin).toBe(-10); + expect(result.displayVariableDefaultMax).toBe(10); + expect(result.displayVariableMin).toBe(-5); + expect(result.displayVariableMax).toBe(5); }); test('marks only differing range fields as undefined', async () => { - const parentSpectrum = await createParentSpectrum(); - const metadata = getComponentMetadata(parentSpectrum, 0); + const parentVariable = await createParentVariable(); + const metadata = getComponentMetadata(parentVariable, 0); - const firstSpectrum = createSpectrum({ - id: parentSpectrum.id, + const firstVariable = createVariable({ + id: parentVariable.id, componentIds: [-1, 0, 1, 2], ranges: new Map([[0, createRange(-10, 10, -5, 5)]]), }); - const secondSpectrum = createSpectrum({ - id: parentSpectrum.id, + const secondVariable = createVariable({ + id: parentVariable.id, componentIds: [-1, 0, 1, 2], ranges: new Map([[0, createRange(-20, 10, -15, 5)]]), }); - const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + const result = await AggregateVariableComponentInfo.getInstanceAsync( [ createActorNode({ - spectrumId: parentSpectrum.id, - spectrumComponent: 0, - spectra: [firstSpectrum], + variableId: parentVariable.id, + variableComponent: 0, + spectra: [firstVariable], }), createActorNode({ - spectrumId: parentSpectrum.id, - spectrumComponent: 0, - spectra: [secondSpectrum], + variableId: parentVariable.id, + variableComponent: 0, + spectra: [secondVariable], }), ], - parentSpectrum, + parentVariable, metadata ); - expect(result.displaySpectrumDefaultMin).toBeUndefined(); - expect(result.displaySpectrumDefaultMax).toBe(10); - expect(result.displaySpectrumMin).toBeUndefined(); - expect(result.displaySpectrumMax).toBe(5); + expect(result.displayVariableDefaultMin).toBeUndefined(); + expect(result.displayVariableDefaultMax).toBe(10); + expect(result.displayVariableMin).toBeUndefined(); + expect(result.displayVariableMax).toBe(5); }); - test('sets all range values to undefined when a node has no matching spectrum', async () => { - const parentSpectrum = await createParentSpectrum(); - const metadata = getComponentMetadata(parentSpectrum, 0); + test('sets all range values to undefined when a node has no matching variable', async () => { + const parentVariable = await createParentVariable(); + const metadata = getComponentMetadata(parentVariable, 0); - const matchingSpectrum = createSpectrum({ - id: parentSpectrum.id, + const matchingVariable = createVariable({ + id: parentVariable.id, componentIds: [-1, 0, 1, 2], ranges: new Map([[0, createRange(-10, 10, -5, 5)]]), }); - const unrelatedSpectrum = createSpectrum({ + const unrelatedVariable = createVariable({ id: 'POINT::temperature::1', name: 'temperature', shape: 'Scalar', componentIds: [-1], }); - const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + const result = await AggregateVariableComponentInfo.getInstanceAsync( [ createActorNode({ - spectrumId: parentSpectrum.id, - spectrumComponent: 0, - spectra: [matchingSpectrum], + variableId: parentVariable.id, + variableComponent: 0, + spectra: [matchingVariable], }), createActorNode({ - spectrumId: unrelatedSpectrum.id, - spectrumComponent: -1, - spectra: [unrelatedSpectrum], + variableId: unrelatedVariable.id, + variableComponent: -1, + spectra: [unrelatedVariable], }), ], - parentSpectrum, + parentVariable, metadata ); - expect(result.displaySpectrumDefaultMin).toBeUndefined(); - expect(result.displaySpectrumDefaultMax).toBeUndefined(); - expect(result.displaySpectrumMin).toBeUndefined(); - expect(result.displaySpectrumMax).toBeUndefined(); + expect(result.displayVariableDefaultMin).toBeUndefined(); + expect(result.displayVariableDefaultMax).toBeUndefined(); + expect(result.displayVariableMin).toBeUndefined(); + expect(result.displayVariableMax).toBeUndefined(); }); test('sets all range values to undefined when a component range is missing', async () => { - const parentSpectrum = await createParentSpectrum(); - const metadata = getComponentMetadata(parentSpectrum, 0); + const parentVariable = await createParentVariable(); + const metadata = getComponentMetadata(parentVariable, 0); - const spectrumWithRange = createSpectrum({ - id: parentSpectrum.id, + const variableWithRange = createVariable({ + id: parentVariable.id, componentIds: [-1, 0, 1, 2], ranges: new Map([[0, createRange(-10, 10, -5, 5)]]), }); - const spectrumWithoutRange = createSpectrum({ - id: parentSpectrum.id, + const variableWithoutRange = createVariable({ + id: parentVariable.id, componentIds: [-1, 0, 1, 2], omittedRangeIds: [0], }); - const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + const result = await AggregateVariableComponentInfo.getInstanceAsync( [ createActorNode({ - spectrumId: parentSpectrum.id, - spectrumComponent: 0, - spectra: [spectrumWithRange], + variableId: parentVariable.id, + variableComponent: 0, + spectra: [variableWithRange], }), createActorNode({ - spectrumId: parentSpectrum.id, - spectrumComponent: 0, - spectra: [spectrumWithoutRange], + variableId: parentVariable.id, + variableComponent: 0, + spectra: [variableWithoutRange], }), ], - parentSpectrum, + parentVariable, metadata ); - expect(result.displaySpectrumDefaultMin).toBeUndefined(); - expect(result.displaySpectrumDefaultMax).toBeUndefined(); - expect(result.displaySpectrumMin).toBeUndefined(); - expect(result.displaySpectrumMax).toBeUndefined(); + expect(result.displayVariableDefaultMin).toBeUndefined(); + expect(result.displayVariableDefaultMax).toBeUndefined(); + expect(result.displayVariableMin).toBeUndefined(); + expect(result.displayVariableMax).toBeUndefined(); }); test('stops aggregation after encountering a missing range', async () => { - const parentSpectrum = await createParentSpectrum(); - const metadata = getComponentMetadata(parentSpectrum, 0); + const parentVariable = await createParentVariable(); + const metadata = getComponentMetadata(parentVariable, 0); - const missingRangeSpectrum = createSpectrum({ - id: parentSpectrum.id, + const missingRangeVariable = createVariable({ + id: parentVariable.id, componentIds: [-1, 0, 1, 2], omittedRangeIds: [0], }); - const laterSpectrum = createSpectrum({ - id: parentSpectrum.id, + const laterVariable = createVariable({ + id: parentVariable.id, componentIds: [-1, 0, 1, 2], ranges: new Map([[0, createRange(-100, 100, -50, 50)]]), }); - const laterGetRangeInfoSpy = jest.spyOn(laterSpectrum, 'getRangeInfo'); + const laterGetRangeInfoSpy = jest.spyOn(laterVariable, 'getRangeInfo'); - const result = await AggregateSpectrumComponentInfo.getInstanceAsync( + const result = await AggregateVariableComponentInfo.getInstanceAsync( [ createActorNode({ - spectrumId: parentSpectrum.id, - spectrumComponent: 0, - spectra: [missingRangeSpectrum], + variableId: parentVariable.id, + variableComponent: 0, + spectra: [missingRangeVariable], }), createActorNode({ - spectrumId: parentSpectrum.id, - spectrumComponent: 0, - spectra: [laterSpectrum], + variableId: parentVariable.id, + variableComponent: 0, + spectra: [laterVariable], }), ], - parentSpectrum, + parentVariable, metadata ); - expect(result.displaySpectrumDefaultMin).toBeUndefined(); - expect(result.displaySpectrumDefaultMax).toBeUndefined(); - expect(result.displaySpectrumMin).toBeUndefined(); - expect(result.displaySpectrumMax).toBeUndefined(); + expect(result.displayVariableDefaultMin).toBeUndefined(); + expect(result.displayVariableDefaultMax).toBeUndefined(); + expect(result.displayVariableMin).toBeUndefined(); + expect(result.displayVariableMax).toBeUndefined(); expect(laterGetRangeInfoSpy).not.toHaveBeenCalled(); }); - describe('setDisplaySpectrumMin', () => { + describe('setDisplayVariableMin', () => { test('accepts a number', async () => { const result = await createComponentInfo(); - const success = result.setDisplaySpectrumMin(12.5); + const success = result.setDisplayVariableMin(12.5); expect(success).toBe(true); - expect(result.displaySpectrumMin).toBe(12.5); + expect(result.displayVariableMin).toBe(12.5); }); test('parses a numeric string', async () => { const result = await createComponentInfo(); - const success = result.setDisplaySpectrumMin('12.5'); + const success = result.setDisplayVariableMin('12.5'); expect(success).toBe(true); - expect(result.displaySpectrumMin).toBe(12.5); + expect(result.displayVariableMin).toBe(12.5); }); test('uses parseFloat behavior for partially numeric strings', async () => { const result = await createComponentInfo(); - const success = result.setDisplaySpectrumMin('12.5px'); + const success = result.setDisplayVariableMin('12.5px'); expect(success).toBe(true); - expect(result.displaySpectrumMin).toBe(12.5); + expect(result.displayVariableMin).toBe(12.5); }); test('converts a nonnumeric string to null', async () => { const result = await createComponentInfo(); - const success = result.setDisplaySpectrumMin('not numeric'); + const success = result.setDisplayVariableMin('not numeric'); expect(success).toBe(false); - expect(result.displaySpectrumMin).toBeNull(); + expect(result.displayVariableMin).toBeNull(); }); test('preserves null', async () => { const result = await createComponentInfo(); - const success = result.setDisplaySpectrumMin(null); + const success = result.setDisplayVariableMin(null); expect(success).toBe(false); - expect(result.displaySpectrumMin).toBeNull(); + expect(result.displayVariableMin).toBeNull(); }); test('preserves undefined', async () => { const result = await createComponentInfo(); - const success = result.setDisplaySpectrumMin(undefined); + const success = result.setDisplayVariableMin(undefined); expect(success).toBe(false); - expect(result.displaySpectrumMin).toBeUndefined(); + expect(result.displayVariableMin).toBeUndefined(); }); test('invokes onMinChange with the parsed value', async () => { @@ -318,7 +318,7 @@ describe('AggregateSpectrumComponentInfo', () => { result.events.onMinChange = handler; - result.setDisplaySpectrumMin('25.5'); + result.setDisplayVariableMin('25.5'); expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith(25.5); @@ -330,66 +330,66 @@ describe('AggregateSpectrumComponentInfo', () => { result.events.onMinChange = handler; - result.setDisplaySpectrumMin('invalid'); + result.setDisplayVariableMin('invalid'); expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith(null); }); }); - describe('setDisplaySpectrumMax', () => { + describe('setDisplayVariableMax', () => { test('accepts a number', async () => { const result = await createComponentInfo(); - const success = result.setDisplaySpectrumMax(87.5); + const success = result.setDisplayVariableMax(87.5); expect(success).toBe(true); - expect(result.displaySpectrumMax).toBe(87.5); + expect(result.displayVariableMax).toBe(87.5); }); test('parses a numeric string', async () => { const result = await createComponentInfo(); - const success = result.setDisplaySpectrumMax('87.5'); + const success = result.setDisplayVariableMax('87.5'); expect(success).toBe(true); - expect(result.displaySpectrumMax).toBe(87.5); + expect(result.displayVariableMax).toBe(87.5); }); test('uses parseFloat behavior for partially numeric strings', async () => { const result = await createComponentInfo(); - const success = result.setDisplaySpectrumMax('87.5px'); + const success = result.setDisplayVariableMax('87.5px'); expect(success).toBe(true); - expect(result.displaySpectrumMax).toBe(87.5); + expect(result.displayVariableMax).toBe(87.5); }); test('converts a nonnumeric string to null', async () => { const result = await createComponentInfo(); - const success = result.setDisplaySpectrumMax('not numeric'); + const success = result.setDisplayVariableMax('not numeric'); expect(success).toBe(false); - expect(result.displaySpectrumMax).toBeNull(); + expect(result.displayVariableMax).toBeNull(); }); test('preserves null', async () => { const result = await createComponentInfo(); - const success = result.setDisplaySpectrumMax(null); + const success = result.setDisplayVariableMax(null); expect(success).toBe(false); - expect(result.displaySpectrumMax).toBeNull(); + expect(result.displayVariableMax).toBeNull(); }); test('preserves undefined', async () => { const result = await createComponentInfo(); - const success = result.setDisplaySpectrumMax(undefined); + const success = result.setDisplayVariableMax(undefined); expect(success).toBe(false); - expect(result.displaySpectrumMax).toBeUndefined(); + expect(result.displayVariableMax).toBeUndefined(); }); test('invokes onMaxChange with the parsed value', async () => { @@ -398,7 +398,7 @@ describe('AggregateSpectrumComponentInfo', () => { result.events.onMaxChange = handler; - result.setDisplaySpectrumMax('75.5'); + result.setDisplayVariableMax('75.5'); expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith(75.5); @@ -410,7 +410,7 @@ describe('AggregateSpectrumComponentInfo', () => { result.events.onMaxChange = handler; - result.setDisplaySpectrumMax('invalid'); + result.setDisplayVariableMax('invalid'); expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith(null); @@ -427,18 +427,18 @@ describe('AggregateSpectrumComponentInfo', () => { test('setters replace custom aggregate values without changing defaults', async () => { const result = await createComponentInfo(); - expect(result.displaySpectrumDefaultMin).toBe(-10); - expect(result.displaySpectrumDefaultMax).toBe(10); - expect(result.displaySpectrumMin).toBe(-5); - expect(result.displaySpectrumMax).toBe(5); + expect(result.displayVariableDefaultMin).toBe(-10); + expect(result.displayVariableDefaultMax).toBe(10); + expect(result.displayVariableMin).toBe(-5); + expect(result.displayVariableMax).toBe(5); - result.setDisplaySpectrumMin(-2); - result.setDisplaySpectrumMax(2); + result.setDisplayVariableMin(-2); + result.setDisplayVariableMax(2); - expect(result.displaySpectrumMin).toBe(-2); - expect(result.displaySpectrumMax).toBe(2); - expect(result.displaySpectrumDefaultMin).toBe(-10); - expect(result.displaySpectrumDefaultMax).toBe(10); + expect(result.displayVariableMin).toBe(-2); + expect(result.displayVariableMax).toBe(2); + expect(result.displayVariableDefaultMin).toBe(-10); + expect(result.displayVariableDefaultMax).toBe(10); }); }); @@ -447,7 +447,7 @@ interface RangeInfo { customRange: [number, number]; } -interface CreateSpectrumOptions { +interface CreateVariableOptions { id: string; componentIds: number[]; name?: string; @@ -458,13 +458,13 @@ interface CreateSpectrumOptions { } interface CreateActorNodeOptions { - spectrumId: string | null; - spectrumComponent: number; - spectra: VisorSpectrumInfo[]; + variableId: string | null; + variableComponent: number; + spectra: VisorVariableInfo[]; } -async function createParentSpectrum(): Promise { - const spectrum = createSpectrum({ +async function createParentVariable(): Promise { + const variable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', shape: 'Vector3', @@ -472,50 +472,50 @@ async function createParentSpectrum(): Promise { }); const actorNode = createActorNode({ - spectrumId: spectrum.id, - spectrumComponent: 0, - spectra: [spectrum], + variableId: variable.id, + variableComponent: 0, + spectra: [variable], }); const selection = await AggregateSelectionInfo.getInstanceAsync([actorNode]); - expect(selection.currentSpectrumInfo).not.toBeNull(); - expect(selection.currentSpectrumInfo).not.toBeUndefined(); + expect(selection.currentVariableInfo).not.toBeNull(); + expect(selection.currentVariableInfo).not.toBeUndefined(); - return selection.currentSpectrumInfo!; + return selection.currentVariableInfo!; } -async function createComponentInfo(): Promise { - const parentSpectrum = await createParentSpectrum(); +async function createComponentInfo(): Promise { + const parentVariable = await createParentVariable(); - const metadata = getComponentMetadata(parentSpectrum, 0); + const metadata = getComponentMetadata(parentVariable, 0); - const actorSpectrum = createSpectrum({ - id: parentSpectrum.id, + const actorVariable = createVariable({ + id: parentVariable.id, name: 'velocity', shape: 'Vector3', componentIds: [-1, 0, 1, 2], ranges: new Map([[0, createRange(-10, 10, -5, 5)]]), }); - return AggregateSpectrumComponentInfo.getInstanceAsync( + return AggregateVariableComponentInfo.getInstanceAsync( [ createActorNode({ - spectrumId: parentSpectrum.id, - spectrumComponent: 0, - spectra: [actorSpectrum], + variableId: parentVariable.id, + variableComponent: 0, + spectra: [actorVariable], }), ], - parentSpectrum, + parentVariable, metadata ); } function getComponentMetadata( - spectrumInfo: AggregateSpectrumInfo, + variableInfo: AggregateVariableInfo, componentId: number -): VisorSpectrumComponentMetadata { - const metadata = spectrumInfo.metadata.componentOptions.find((item) => item.id === componentId); +): VisorVariableComponentMetadata { + const metadata = variableInfo.metadata.componentOptions.find((item) => item.id === componentId); if (metadata == null) { throw new Error(`component metadata not found: ${componentId}`); @@ -524,7 +524,7 @@ function getComponentMetadata( return metadata; } -function createSpectrum({ +function createVariable({ id, componentIds, name = 'velocity', @@ -532,7 +532,7 @@ function createSpectrum({ shape = 'Vector3', ranges = new Map(), omittedRangeIds = [], -}: CreateSpectrumOptions): VisorSpectrumInfo { +}: CreateVariableOptions): VisorVariableInfo { const componentOptions = createComponentOptions(componentIds); const rangeState = new Map(); @@ -593,17 +593,17 @@ function createSpectrum({ }; } -function createSpectrumCollection(spectra: VisorSpectrumInfo[]): VisorSpectrumCollection { +function createVariableCollection(spectra: VisorVariableInfo[]): VisorVariableCollection { const array = [...spectra]; - const map = new Map( - array.map((spectrum) => [spectrum.id, spectrum]) + const map = new Map( + array.map((variable) => [variable.id, variable]) ); return { array, - getSpectrum(id: string | null) { + getVariable(id: string | null) { if (id == null) { return null; } @@ -614,21 +614,21 @@ function createSpectrumCollection(spectra: VisorSpectrumInfo[]): VisorSpectrumCo } function createActorNode({ - spectrumId, - spectrumComponent, + variableId, + variableComponent, spectra, }: CreateActorNodeOptions): VisorSceneNodeExtended { return { name: 'mesh', opacity: 1, - spectrumId, - spectrumComponent, + variableId, + variableComponent, customDiffuseColorHex: '#ffffff', - spectrumCollection: createSpectrumCollection(spectra), + variableCollection: createVariableCollection(spectra), } as unknown as VisorSceneNodeExtended; } -function createComponentOptions(componentIds: number[]): VisorSpectrumComponentMetadata[] { +function createComponentOptions(componentIds: number[]): VisorVariableComponentMetadata[] { return componentIds.map((id) => ({ id, name: getComponentName(id), diff --git a/src/ansys/visor/visor-client/src/jest-tests/AggregateSpectrumInfo.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/AggregateVariableInfo.test.tsx similarity index 66% rename from src/ansys/visor/visor-client/src/jest-tests/AggregateSpectrumInfo.test.tsx rename to src/ansys/visor/visor-client/src/jest-tests/AggregateVariableInfo.test.tsx index 2a9ee2ce..de557d8a 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/AggregateSpectrumInfo.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/AggregateVariableInfo.test.tsx @@ -1,20 +1,20 @@ import { AggregateSelectionInfo } from '../aggregate/AggregateSelectionInfo.tsx'; -import { AggregateSpectrumInfo } from '../aggregate/AggregateSpectrumInfo.tsx'; +import { AggregateVariableInfo } from '../aggregate/AggregateVariableInfo.tsx'; import type { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; import type { - VisorSpectrumCollection, - VisorSpectrumComponentMetadata, - VisorSpectrumInfo, -} from '../state/VisorSpectrumManager.tsx'; + VisorVariableCollection, + VisorVariableComponentMetadata, + VisorVariableInfo, +} from '../state/VisorVariableManager.tsx'; import type { FieldAssociation } from '../state/appstate/vtkInfo/VisorVtkDataArray.tsx'; -describe('AggregateSpectrumInfo', () => { +describe('AggregateVariableInfo', () => { afterEach(() => { jest.restoreAllMocks(); }); - test('exposes its spectrum ID and metadata', async () => { - const spectrum = createSpectrum({ + test('exposes its variable ID and metadata', async () => { + const variable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], @@ -22,21 +22,21 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: spectrum.id, - spectrumComponent: 0, - spectra: [spectrum], + variableId: variable.id, + variableComponent: 0, + spectra: [variable], }), ]); - const result = selection.currentSpectrumInfo; + const result = selection.currentVariableInfo; - expect(result).toBeInstanceOf(AggregateSpectrumInfo); + expect(result).toBeInstanceOf(AggregateVariableInfo); expect(result?.id).toBe('POINT::velocity::3'); - expect(result?.metadata).toBe(spectrum); + expect(result?.metadata).toBe(variable); }); test('creates one aggregate component option for each metadata component', async () => { - const spectrum = createSpectrum({ + const variable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], @@ -44,13 +44,13 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: spectrum.id, - spectrumComponent: 0, - spectra: [spectrum], + variableId: variable.id, + variableComponent: 0, + spectra: [variable], }), ]); - const result = selection.currentSpectrumInfo!; + const result = selection.currentVariableInfo!; expect([...result.componentOptions.keys()]).toEqual(['-1', '0', '1', '2']); @@ -61,7 +61,7 @@ describe('AggregateSpectrumInfo', () => { }); test('associates aggregate components with their metadata', async () => { - const spectrum = createSpectrum({ + const variable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], @@ -69,27 +69,27 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: spectrum.id, - spectrumComponent: 1, - spectra: [spectrum], + variableId: variable.id, + variableComponent: 1, + spectra: [variable], }), ]); - const result = selection.currentSpectrumInfo!; + const result = selection.currentVariableInfo!; const component = result.componentOptions.get('1')!; - expect(component.metadata).toBe(spectrum.componentOptions[2]); - expect(component.spectrumInfo).toBe(result); + expect(component.metadata).toBe(variable.componentOptions[2]); + expect(component.variableInfo).toBe(result); }); test('selects the common component used by all actor nodes', async () => { - const firstSpectrum = createSpectrum({ + const firstVariable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], }); - const secondSpectrum = createSpectrum({ + const secondVariable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], @@ -97,31 +97,31 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: firstSpectrum.id, - spectrumComponent: 1, - spectra: [firstSpectrum], + variableId: firstVariable.id, + variableComponent: 1, + spectra: [firstVariable], }), createActorNode({ - spectrumId: secondSpectrum.id, - spectrumComponent: 1, - spectra: [secondSpectrum], + variableId: secondVariable.id, + variableComponent: 1, + spectra: [secondVariable], }), ]); - const result = selection.currentSpectrumInfo!; + const result = selection.currentVariableInfo!; expect(result.displayComponentId).toBe(1); expect(result.currentComponentInfo).toBe(result.componentOptions.get('1')); }); test('uses undefined when actor nodes have different components', async () => { - const firstSpectrum = createSpectrum({ + const firstVariable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], }); - const secondSpectrum = createSpectrum({ + const secondVariable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], @@ -129,25 +129,25 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: firstSpectrum.id, - spectrumComponent: 0, - spectra: [firstSpectrum], + variableId: firstVariable.id, + variableComponent: 0, + spectra: [firstVariable], }), createActorNode({ - spectrumId: secondSpectrum.id, - spectrumComponent: 1, - spectra: [secondSpectrum], + variableId: secondVariable.id, + variableComponent: 1, + spectra: [secondVariable], }), ]); - const result = selection.currentSpectrumInfo!; + const result = selection.currentVariableInfo!; expect(result.displayComponentId).toBeUndefined(); expect(result.currentComponentInfo).toBeUndefined(); }); test('defaults to the first component option when the selected component has no range', async () => { - const spectrum = createSpectrum({ + const variable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], @@ -155,20 +155,20 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: spectrum.id, - spectrumComponent: 999, - spectra: [spectrum], + variableId: variable.id, + variableComponent: 999, + spectra: [variable], }), ]); - const result = selection.currentSpectrumInfo!; + const result = selection.currentVariableInfo!; expect(result.displayComponentId).toBe(-1); expect(result.currentComponentInfo).toBe(result.componentOptions.get('-1')); }); - test("aggregates component ranges from each actor node's spectrum collection", async () => { - const firstSpectrum = createSpectrum({ + test("aggregates component ranges from each actor node's variable collection", async () => { + const firstVariable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], @@ -180,7 +180,7 @@ describe('AggregateSpectrumInfo', () => { ]), }); - const secondSpectrum = createSpectrum({ + const secondVariable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], @@ -194,27 +194,27 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: firstSpectrum.id, - spectrumComponent: 1, - spectra: [firstSpectrum], + variableId: firstVariable.id, + variableComponent: 1, + spectra: [firstVariable], }), createActorNode({ - spectrumId: secondSpectrum.id, - spectrumComponent: 1, - spectra: [secondSpectrum], + variableId: secondVariable.id, + variableComponent: 1, + spectra: [secondVariable], }), ]); - const component = selection.currentSpectrumInfo!.currentComponentInfo!; + const component = selection.currentVariableInfo!.currentComponentInfo!; - expect(component.displaySpectrumDefaultMin).toBe(-20); - expect(component.displaySpectrumDefaultMax).toBe(20); - expect(component.displaySpectrumMin).toBe(-15); - expect(component.displaySpectrumMax).toBe(15); + expect(component.displayVariableDefaultMin).toBe(-20); + expect(component.displayVariableDefaultMax).toBe(20); + expect(component.displayVariableMin).toBe(-15); + expect(component.displayVariableMax).toBe(15); }); test('marks differing component ranges as undefined', async () => { - const firstSpectrum = createSpectrum({ + const firstVariable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0], @@ -224,7 +224,7 @@ describe('AggregateSpectrumInfo', () => { ]), }); - const secondSpectrum = createSpectrum({ + const secondVariable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0], @@ -236,29 +236,29 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: firstSpectrum.id, - spectrumComponent: 0, - spectra: [firstSpectrum], + variableId: firstVariable.id, + variableComponent: 0, + spectra: [firstVariable], }), createActorNode({ - spectrumId: secondSpectrum.id, - spectrumComponent: 0, - spectra: [secondSpectrum], + variableId: secondVariable.id, + variableComponent: 0, + spectra: [secondVariable], }), ]); - const component = selection.currentSpectrumInfo!.currentComponentInfo!; + const component = selection.currentVariableInfo!.currentComponentInfo!; - expect(component.displaySpectrumDefaultMin).toBeUndefined(); - expect(component.displaySpectrumDefaultMax).toBeUndefined(); - expect(component.displaySpectrumMin).toBeUndefined(); - expect(component.displaySpectrumMax).toBe(5); + expect(component.displayVariableDefaultMin).toBeUndefined(); + expect(component.displayVariableDefaultMax).toBeUndefined(); + expect(component.displayVariableMin).toBeUndefined(); + expect(component.displayVariableMax).toBe(5); }); test('warns when a displayed component has range data but no component option', async () => { const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - const spectrum = createSpectrum({ + const variable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1], @@ -272,13 +272,13 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: spectrum.id, - spectrumComponent: 999, - spectra: [spectrum], + variableId: variable.id, + variableComponent: 999, + spectra: [variable], }), ]); - const result = selection.currentSpectrumInfo!; + const result = selection.currentVariableInfo!; expect(result.displayComponentId).toBe(999); expect(result.currentComponentInfo).toBeUndefined(); @@ -286,7 +286,7 @@ describe('AggregateSpectrumInfo', () => { }); test('setDisplayComponentId selects a component by number', async () => { - const spectrum = createSpectrum({ + const variable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], @@ -294,13 +294,13 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: spectrum.id, - spectrumComponent: 0, - spectra: [spectrum], + variableId: variable.id, + variableComponent: 0, + spectra: [variable], }), ]); - const result = selection.currentSpectrumInfo!; + const result = selection.currentVariableInfo!; const selected = result.setDisplayComponentId(2); expect(selected).toBe(result.componentOptions.get('2')); @@ -309,7 +309,7 @@ describe('AggregateSpectrumInfo', () => { }); test('setDisplayComponentId accepts a string ID', async () => { - const spectrum = createSpectrum({ + const variable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], @@ -317,13 +317,13 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: spectrum.id, - spectrumComponent: 0, - spectra: [spectrum], + variableId: variable.id, + variableComponent: 0, + spectra: [variable], }), ]); - const result = selection.currentSpectrumInfo!; + const result = selection.currentVariableInfo!; const selected = result.setDisplayComponentId('1'); expect(selected).toBe(result.componentOptions.get('1')); @@ -331,7 +331,7 @@ describe('AggregateSpectrumInfo', () => { }); test('setDisplayComponentId returns null for an unavailable component', async () => { - const spectrum = createSpectrum({ + const variable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1], @@ -339,13 +339,13 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: spectrum.id, - spectrumComponent: 0, - spectra: [spectrum], + variableId: variable.id, + variableComponent: 0, + spectra: [variable], }), ]); - const result = selection.currentSpectrumInfo!; + const result = selection.currentVariableInfo!; const selected = result.setDisplayComponentId(999); expect(selected).toBeNull(); @@ -354,7 +354,7 @@ describe('AggregateSpectrumInfo', () => { }); test('setDisplayComponentId preserves null', async () => { - const spectrum = createSpectrum({ + const variable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1], @@ -362,13 +362,13 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: spectrum.id, - spectrumComponent: 0, - spectra: [spectrum], + variableId: variable.id, + variableComponent: 0, + spectra: [variable], }), ]); - const result = selection.currentSpectrumInfo!; + const result = selection.currentVariableInfo!; expect(result.setDisplayComponentId(null)).toBeNull(); expect(result.currentComponentInfo).toBeNull(); @@ -376,7 +376,7 @@ describe('AggregateSpectrumInfo', () => { }); test('setDisplayComponentId preserves undefined', async () => { - const spectrum = createSpectrum({ + const variable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1], @@ -384,21 +384,21 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: spectrum.id, - spectrumComponent: 0, - spectra: [spectrum], + variableId: variable.id, + variableComponent: 0, + spectra: [variable], }), ]); - const result = selection.currentSpectrumInfo!; + const result = selection.currentVariableInfo!; expect(result.setDisplayComponentId(undefined)).toBeUndefined(); expect(result.currentComponentInfo).toBeUndefined(); expect(result.displayComponentId).toBeUndefined(); }); - test('setDisplayComponentId invokes onSpectrumComponentChange', async () => { - const spectrum = createSpectrum({ + test('setDisplayComponentId invokes onVariableComponentChange', async () => { + const variable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1], @@ -406,16 +406,16 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: spectrum.id, - spectrumComponent: 0, - spectra: [spectrum], + variableId: variable.id, + variableComponent: 0, + spectra: [variable], }), ]); - const result = selection.currentSpectrumInfo!; + const result = selection.currentVariableInfo!; const handler = jest.fn(); - selection.events.onSpectrumComponentChange = handler; + selection.events.onVariableComponentChange = handler; result.setDisplayComponentId(1); @@ -423,8 +423,8 @@ describe('AggregateSpectrumInfo', () => { expect(handler).toHaveBeenCalledWith(result); }); - test('throws when changing a component while the current spectrum is null', async () => { - const spectrum = createSpectrum({ + test('throws when changing a component while the current variable is null', async () => { + const variable = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1], @@ -432,29 +432,29 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: spectrum.id, - spectrumComponent: 0, - spectra: [spectrum], + variableId: variable.id, + variableComponent: 0, + spectra: [variable], }), ]); - const result = selection.currentSpectrumInfo!; + const result = selection.currentVariableInfo!; - selection.setDisplaySpectrumId(null); + selection.setDisplayVariableId(null); expect(() => { result.setDisplayComponentId(1); - }).toThrow('component should not be changed when the current spectrum is null'); + }).toThrow('component should not be changed when the current variable is null'); }); - test('throws when changing a component on a spectrum that is not current', async () => { - const velocity = createSpectrum({ + test('throws when changing a component on a variable that is not current', async () => { + const velocity = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], }); - const acceleration = createSpectrum({ + const acceleration = createVariable({ id: 'POINT::acceleration::3', name: 'acceleration', componentIds: [-1, 0, 1, 2], @@ -462,27 +462,27 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: velocity.id, - spectrumComponent: 0, + variableId: velocity.id, + variableComponent: 0, spectra: [velocity, acceleration], }), ]); - const nonCurrentSpectrum = selection.spectrumOptions.get(acceleration.id)!; + const nonCurrentVariable = selection.variableOptions.get(acceleration.id)!; expect(() => { - nonCurrentSpectrum.setDisplayComponentId(1); - }).toThrow('component should not be changed on a spectrum that is not current'); + nonCurrentVariable.setDisplayComponentId(1); + }).toThrow('component should not be changed on a variable that is not current'); }); - test('allows component changes after a different spectrum becomes current', async () => { - const velocity = createSpectrum({ + test('allows component changes after a different variable becomes current', async () => { + const velocity = createVariable({ id: 'POINT::velocity::3', name: 'velocity', componentIds: [-1, 0, 1, 2], }); - const acceleration = createSpectrum({ + const acceleration = createVariable({ id: 'POINT::acceleration::3', name: 'acceleration', componentIds: [-1, 0, 1, 2], @@ -490,13 +490,13 @@ describe('AggregateSpectrumInfo', () => { const selection = await createSelection([ createActorNode({ - spectrumId: velocity.id, - spectrumComponent: 0, + variableId: velocity.id, + variableComponent: 0, spectra: [velocity, acceleration], }), ]); - const accelerationInfo = selection.setDisplaySpectrumId(acceleration.id)!; + const accelerationInfo = selection.setDisplayVariableId(acceleration.id)!; expect(() => { accelerationInfo.setDisplayComponentId(2); @@ -514,7 +514,7 @@ interface RangeInfo { customRange: number[]; } -interface CreateSpectrumOptions { +interface CreateVariableOptions { id: string; name: string; componentIds: number[]; @@ -524,9 +524,9 @@ interface CreateSpectrumOptions { } interface CreateActorNodeOptions { - spectrumId: string | null; - spectrumComponent: number; - spectra: VisorSpectrumInfo[]; + variableId: string | null; + variableComponent: number; + spectra: VisorVariableInfo[]; } async function createSelection( @@ -535,14 +535,14 @@ async function createSelection( return AggregateSelectionInfo.getInstanceAsync(actorNodes); } -function createSpectrum({ +function createVariable({ id, name, componentIds, type = 'POINT', shape = 'Vector3', ranges, -}: CreateSpectrumOptions): VisorSpectrumInfo { +}: CreateVariableOptions): VisorVariableInfo { const componentOptions = createComponentOptions(componentIds); const rangeState = new Map(); @@ -602,14 +602,14 @@ function createSpectrum({ }; } -function createSpectrumCollection(spectra: VisorSpectrumInfo[]): VisorSpectrumCollection { +function createVariableCollection(spectra: VisorVariableInfo[]): VisorVariableCollection { const array = [...spectra]; - const map = new Map(array.map((spectrum) => [spectrum.id, spectrum])); + const map = new Map(array.map((variable) => [variable.id, variable])); return { array, - getSpectrum(id: string | null) { + getVariable(id: string | null) { if (id == null) { return null; } @@ -620,21 +620,21 @@ function createSpectrumCollection(spectra: VisorSpectrumInfo[]): VisorSpectrumCo } function createActorNode({ - spectrumId, - spectrumComponent, + variableId, + variableComponent, spectra, }: CreateActorNodeOptions): VisorSceneNodeExtended { return { name: 'mesh', opacity: 1, - spectrumId, - spectrumComponent, + variableId, + variableComponent, customDiffuseColorHex: '#ffffff', - spectrumCollection: createSpectrumCollection(spectra), + variableCollection: createVariableCollection(spectra), } as unknown as VisorSceneNodeExtended; } -function createComponentOptions(componentIds: number[]): VisorSpectrumComponentMetadata[] { +function createComponentOptions(componentIds: number[]): VisorVariableComponentMetadata[] { return componentIds.map((id) => ({ id, name: getComponentName(id), 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 7f8078d9..fe47f3d5 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 @@ -15,7 +15,7 @@ import type { IRenderer } from '../renderer/IRenderer'; * * 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. + * variable, so the test cannot agree with the code by making the same mistake. */ const ROOT_ID = 0; @@ -213,9 +213,9 @@ describe('colour by variable', () => { await partA.setColorVariableAsync(VARIABLE_ID, 0); expect(renderer.sendPartColorVariableAsync).toHaveBeenCalledWith(PART_A_ID, { - spectrumId: 'POINT::pressure::1', - spectrumType: 'POINT', - spectrumName: 'pressure', + variableId: 'POINT::pressure::1', + variableType: 'POINT', + variableName: 'pressure', component: 0, min: 2, max: 8, @@ -228,9 +228,9 @@ describe('colour by variable', () => { await partA.setColorVariableAsync(VARIABLE_ID, 0); expect(renderer.setColorVariableAsync).toHaveBeenCalledWith(PART_A_ID, { - spectrumId: 'POINT::pressure::1', - spectrumType: 'POINT', - spectrumName: 'pressure', + variableId: 'POINT::pressure::1', + variableType: 'POINT', + variableName: 'pressure', component: 0, min: 2, max: 8, diff --git a/src/ansys/visor/visor-client/src/jest-tests/VisorSpectrumManager.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/VisorVariableManager.test.tsx similarity index 70% rename from src/ansys/visor/visor-client/src/jest-tests/VisorSpectrumManager.test.tsx rename to src/ansys/visor/visor-client/src/jest-tests/VisorVariableManager.test.tsx index ecd68eca..5c516c79 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/VisorSpectrumManager.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/VisorVariableManager.test.tsx @@ -1,36 +1,36 @@ import VisorVtkDataArray from '../state/appstate/vtkInfo/VisorVtkDataArray.tsx'; -import { getSpectrumManager, VisorSpectrumInfo } from '../state/VisorSpectrumManager.tsx'; +import { getVariableManager, VisorVariableInfo } from '../state/VisorVariableManager.tsx'; -describe('getSpectrumManager', () => { +describe('getVariableManager', () => { describe('manager lifecycle', () => { test('returns a frozen manager object', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); expect(Object.isFrozen(manager)).toBe(true); expect(typeof manager.addDataArrayMetadata).toBe('function'); expect(typeof manager.finishAddingDataArrayMetadata).toBe('function'); }); - test('throws when globalSpectrumCollection is read before finishing', () => { - const manager = getSpectrumManager(); + test('throws when globalVariableCollection is read before finishing', () => { + const manager = getVariableManager(); - expect(() => manager.globalSpectrumCollection).toThrow( + expect(() => manager.globalVariableCollection).toThrow( 'finishAddingDataArrayMetadata() has not been called yet' ); }); test('creates an empty global collection when no metadata was added', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); manager.finishAddingDataArrayMetadata(); - expect(manager.globalSpectrumCollection.array).toEqual([]); - expect(manager.globalSpectrumCollection.getSpectrum(null)).toBeNull(); - expect(manager.globalSpectrumCollection.getSpectrum('missing')).toBeNull(); + expect(manager.globalVariableCollection.array).toEqual([]); + expect(manager.globalVariableCollection.getVariable(null)).toBeNull(); + expect(manager.globalVariableCollection.getVariable('missing')).toBeNull(); }); test('throws when finishAddingDataArrayMetadata is called twice', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); manager.finishAddingDataArrayMetadata(); @@ -40,21 +40,21 @@ describe('getSpectrumManager', () => { }); test('different managers have independent state', () => { - const firstManager = getSpectrumManager(); - const secondManager = getSpectrumManager(); + const firstManager = getVariableManager(); + const secondManager = getVariableManager(); firstManager.addDataArrayMetadata([createDataArray()]); firstManager.finishAddingDataArrayMetadata(); secondManager.finishAddingDataArrayMetadata(); - expect(firstManager.globalSpectrumCollection.array).toHaveLength(1); - expect(secondManager.globalSpectrumCollection.array).toHaveLength(0); + expect(firstManager.globalVariableCollection.array).toHaveLength(1); + expect(secondManager.globalVariableCollection.array).toHaveLength(0); }); }); describe('addDataArrayMetadata', () => { test('returns an empty frozen collection for an empty array', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); const collection = manager.addDataArrayMetadata([]); @@ -63,8 +63,8 @@ describe('getSpectrumManager', () => { expect(Object.isFrozen(collection.array)).toBe(true); }); - test('creates scalar spectrum metadata', () => { - const manager = getSpectrumManager(); + test('creates scalar variable metadata', () => { + const manager = getVariableManager(); const collection = manager.addDataArrayMetadata([ createDataArray({ @@ -76,15 +76,15 @@ describe('getSpectrumManager', () => { }), ]); - const spectrum = collection.array[0]; + const variable = collection.array[0]; - expect(spectrum.id).toBe('POINT::temperature::1'); - expect(spectrum.type).toBe('POINT'); - expect(spectrum.name).toBe('temperature'); - expect(spectrum.shape).toBe('Scalar'); - expect(spectrum.fullName).toBe('POINT - temperature (Scalar)'); - expect(spectrum.numComponents).toBe(1); - expect(spectrum.componentOptions).toEqual([ + expect(variable.id).toBe('POINT::temperature::1'); + expect(variable.type).toBe('POINT'); + expect(variable.name).toBe('temperature'); + expect(variable.shape).toBe('Scalar'); + expect(variable.fullName).toBe('POINT - temperature (Scalar)'); + expect(variable.numComponents).toBe(1); + expect(variable.componentOptions).toEqual([ { id: -1, name: 'Magnitude', @@ -93,7 +93,7 @@ describe('getSpectrumManager', () => { }); test('creates Vector2 component options', () => { - const spectrum = addSingleSpectrum( + const variable = addSingleVariable( createDataArray({ numComponents: 2, ranges: [ @@ -103,8 +103,8 @@ describe('getSpectrumManager', () => { }) ); - expect(spectrum.shape).toBe('Vector2'); - expect(spectrum.componentOptions).toEqual([ + expect(variable.shape).toBe('Vector2'); + expect(variable.componentOptions).toEqual([ { id: -1, name: 'Magnitude' }, { id: 0, name: 'X' }, { id: 1, name: 'Y' }, @@ -112,7 +112,7 @@ describe('getSpectrumManager', () => { }); test('creates Vector3 component options', () => { - const spectrum = addSingleSpectrum( + const variable = addSingleVariable( createDataArray({ numComponents: 3, ranges: [ @@ -123,8 +123,8 @@ describe('getSpectrumManager', () => { }) ); - expect(spectrum.shape).toBe('Vector3'); - expect(spectrum.componentOptions).toEqual([ + expect(variable.shape).toBe('Vector3'); + expect(variable.componentOptions).toEqual([ { id: -1, name: 'Magnitude' }, { id: 0, name: 'X' }, { id: 1, name: 'Y' }, @@ -133,7 +133,7 @@ describe('getSpectrumManager', () => { }); test('creates Vector4 component options', () => { - const spectrum = addSingleSpectrum( + const variable = addSingleVariable( createDataArray({ numComponents: 4, ranges: [ @@ -145,8 +145,8 @@ describe('getSpectrumManager', () => { }) ); - expect(spectrum.shape).toBe('Vector4'); - expect(spectrum.componentOptions).toEqual([ + expect(variable.shape).toBe('Vector4'); + expect(variable.componentOptions).toEqual([ { id: -1, name: 'Magnitude' }, { id: 0, name: 'X' }, { id: 1, name: 'Y' }, @@ -156,14 +156,14 @@ describe('getSpectrumManager', () => { }); test('creates nine-component tensor labels', () => { - const spectrum = addSingleSpectrum( + const variable = addSingleVariable( createDataArray({ numComponents: 9, ranges: Array.from({ length: 9 }, (_, i) => [-i, i]), }) ); - expect(spectrum.componentOptions).toEqual([ + expect(variable.componentOptions).toEqual([ { id: -1, name: 'Magnitude' }, { id: 0, name: 'XX' }, { id: 1, name: 'XY' }, @@ -178,7 +178,7 @@ describe('getSpectrumManager', () => { }); test('throws for an unsupported component count', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); expect(() => { manager.addDataArrayMetadata([ @@ -197,7 +197,7 @@ describe('getSpectrumManager', () => { }); test('returns spectra in input order', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); const collection = manager.addDataArrayMetadata([ createDataArray({ @@ -212,7 +212,7 @@ describe('getSpectrumManager', () => { }); test('creates separate spectra for different names', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); const collection = manager.addDataArrayMetadata([ createDataArray({ @@ -228,7 +228,7 @@ describe('getSpectrumManager', () => { }); test('creates separate spectra for different types', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); const collection = manager.addDataArrayMetadata([ createDataArray({ @@ -245,7 +245,7 @@ describe('getSpectrumManager', () => { }); test('creates separate spectra for different component counts', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); const collection = manager.addDataArrayMetadata([ createDataArray({ @@ -271,55 +271,55 @@ describe('getSpectrumManager', () => { }); }); - describe('local spectrum collections', () => { - test('looks up a spectrum by ID', () => { - const manager = getSpectrumManager(); + describe('local variable collections', () => { + test('looks up a variable by ID', () => { + const manager = getVariableManager(); const collection = manager.addDataArrayMetadata([createDataArray()]); - const spectrum = collection.array[0]; + const variable = collection.array[0]; - expect(collection.getSpectrum(spectrum.id)).toBe(spectrum); + expect(collection.getVariable(variable.id)).toBe(variable); }); test('returns null for null and unknown IDs', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); const collection = manager.addDataArrayMetadata([createDataArray()]); - expect(collection.getSpectrum(null)).toBeNull(); - expect(collection.getSpectrum('unknown')).toBeNull(); + expect(collection.getVariable(null)).toBeNull(); + expect(collection.getVariable('unknown')).toBeNull(); }); test('returns a frozen collection and array', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); const collection = manager.addDataArrayMetadata([createDataArray()]); expect(Object.isFrozen(collection)).toBe(true); expect(Object.isFrozen(collection.array)).toBe(true); }); - test('spectrum metadata objects are frozen', () => { - const spectrum = addSingleSpectrum(createDataArray()); + test('variable metadata objects are frozen', () => { + const variable = addSingleVariable(createDataArray()); - expect(Object.isFrozen(spectrum)).toBe(true); + expect(Object.isFrozen(variable)).toBe(true); }); }); describe('range information', () => { test('returns the magnitude range for component -1', () => { - const spectrum = addSingleSpectrum( + const variable = addSingleVariable( createDataArray({ magnitudeRange: [0, 10], }) ); - expect(spectrum.getRangeInfo(-1)).toEqual({ + expect(variable.getRangeInfo(-1)).toEqual({ defaultRange: [0, 10], customRange: [0, 10], }); }); test('returns the range for an individual component', () => { - const spectrum = addSingleSpectrum( + const variable = addSingleVariable( createDataArray({ ranges: [ [-1, 1], @@ -329,39 +329,39 @@ describe('getSpectrumManager', () => { }) ); - expect(spectrum.getRangeInfo(0)).toEqual({ + expect(variable.getRangeInfo(0)).toEqual({ defaultRange: [-1, 1], customRange: [-1, 1], }); - expect(spectrum.getRangeInfo(2)).toEqual({ + expect(variable.getRangeInfo(2)).toEqual({ defaultRange: [-3, 3], customRange: [-3, 3], }); }); test('returns null for null, undefined, and out-of-range components', () => { - const spectrum = addSingleSpectrum(createDataArray()); + const variable = addSingleVariable(createDataArray()); - expect(spectrum.getRangeInfo(null)).toBeNull(); - expect(spectrum.getRangeInfo(undefined)).toBeNull(); - expect(spectrum.getRangeInfo(-2)).toBeNull(); - expect(spectrum.getRangeInfo(3)).toBeNull(); - expect(spectrum.getRangeInfo(100)).toBeNull(); + expect(variable.getRangeInfo(null)).toBeNull(); + expect(variable.getRangeInfo(undefined)).toBeNull(); + expect(variable.getRangeInfo(-2)).toBeNull(); + expect(variable.getRangeInfo(3)).toBeNull(); + expect(variable.getRangeInfo(100)).toBeNull(); }); test('returns cloned range arrays', () => { - const spectrum = addSingleSpectrum( + const variable = addSingleVariable( createDataArray({ magnitudeRange: [0, 10], }) ); - const first = spectrum.getRangeInfo(-1)!; + const first = variable.getRangeInfo(-1)!; first.defaultRange[0] = -999; first.customRange[1] = 999; - const second = spectrum.getRangeInfo(-1)!; + const second = variable.getRangeInfo(-1)!; expect(second.defaultRange).toEqual([0, 10]); expect(second.customRange).toEqual([0, 10]); @@ -370,7 +370,7 @@ describe('getSpectrumManager', () => { }); test('setCustomRange changes only the custom range', () => { - const spectrum = addSingleSpectrum( + const variable = addSingleVariable( createDataArray({ ranges: [ [-1, 1], @@ -380,45 +380,45 @@ describe('getSpectrumManager', () => { }) ); - spectrum.setCustomRange(1, -20, 20); + variable.setCustomRange(1, -20, 20); - expect(spectrum.getRangeInfo(1)).toEqual({ + expect(variable.getRangeInfo(1)).toEqual({ defaultRange: [-2, 2], customRange: [-20, 20], }); }); test('setCustomRange can change the magnitude range', () => { - const spectrum = addSingleSpectrum( + const variable = addSingleVariable( createDataArray({ magnitudeRange: [0, 10], }) ); - spectrum.setCustomRange(-1, 2, 8); + variable.setCustomRange(-1, 2, 8); - expect(spectrum.getRangeInfo(-1)).toEqual({ + expect(variable.getRangeInfo(-1)).toEqual({ defaultRange: [0, 10], customRange: [2, 8], }); }); test('setCustomRange ignores invalid component IDs', () => { - const spectrum = addSingleSpectrum(createDataArray()); + const variable = addSingleVariable(createDataArray()); - spectrum.setCustomRange(-2, -100, 100); - spectrum.setCustomRange(100, -100, 100); + variable.setCustomRange(-2, -100, 100); + variable.setCustomRange(100, -100, 100); - expect(spectrum.getRangeInfo(-1)).toEqual({ + expect(variable.getRangeInfo(-1)).toEqual({ defaultRange: [0, 10], customRange: [0, 10], }); }); }); - describe('duplicate spectrum aggregation', () => { - test('reuses the same spectrum object for the same ID', () => { - const manager = getSpectrumManager(); + describe('duplicate variable aggregation', () => { + test('reuses the same variable object for the same ID', () => { + const manager = getVariableManager(); const firstCollection = manager.addDataArrayMetadata([createDataArray()]); @@ -428,9 +428,9 @@ describe('getSpectrumManager', () => { }); test('expands default ranges using duplicate metadata', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); - const firstSpectrum = manager.addDataArrayMetadata([ + const firstVariable = manager.addDataArrayMetadata([ createDataArray({ magnitudeRange: [0, 10], ranges: [ @@ -452,39 +452,39 @@ describe('getSpectrumManager', () => { }), ]); - expect(firstSpectrum.getRangeInfo(-1)).toEqual({ + expect(firstVariable.getRangeInfo(-1)).toEqual({ defaultRange: [-5, 20], customRange: [-5, 20], }); - expect(firstSpectrum.getRangeInfo(0)).toEqual({ + expect(firstVariable.getRangeInfo(0)).toEqual({ defaultRange: [-10, 1], customRange: [-10, 1], }); - expect(firstSpectrum.getRangeInfo(1)).toEqual({ + expect(firstVariable.getRangeInfo(1)).toEqual({ defaultRange: [-2, 15], customRange: [-2, 15], }); - expect(firstSpectrum.getRangeInfo(2)).toEqual({ + expect(firstVariable.getRangeInfo(2)).toEqual({ defaultRange: [-30, 30], customRange: [-30, 30], }); }); test('resets custom ranges to the expanded defaults when duplicate metadata is added', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); - const spectrum = manager.addDataArrayMetadata([ + const variable = manager.addDataArrayMetadata([ createDataArray({ magnitudeRange: [0, 10], }), ]).array[0]; - spectrum.setCustomRange(-1, 2, 8); + variable.setCustomRange(-1, 2, 8); - expect(spectrum.getRangeInfo(-1)?.customRange).toEqual([2, 8]); + expect(variable.getRangeInfo(-1)?.customRange).toEqual([2, 8]); manager.addDataArrayMetadata([ createDataArray({ @@ -492,16 +492,16 @@ describe('getSpectrumManager', () => { }), ]); - expect(spectrum.getRangeInfo(-1)).toEqual({ + expect(variable.getRangeInfo(-1)).toEqual({ defaultRange: [-5, 20], customRange: [-5, 20], }); }); test('keeps existing bounds when duplicate ranges are narrower', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); - const spectrum = manager.addDataArrayMetadata([ + const variable = manager.addDataArrayMetadata([ createDataArray({ magnitudeRange: [-10, 20], }), @@ -513,16 +513,16 @@ describe('getSpectrumManager', () => { }), ]); - expect(spectrum.getRangeInfo(-1)).toEqual({ + expect(variable.getRangeInfo(-1)).toEqual({ defaultRange: [-10, 20], customRange: [-10, 20], }); }); }); - describe('global spectrum collection', () => { + describe('global variable collection', () => { test('contains spectra added before finishing', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); manager.addDataArrayMetadata([ createDataArray({ @@ -535,14 +535,14 @@ describe('getSpectrumManager', () => { manager.finishAddingDataArrayMetadata(); - const global = manager.globalSpectrumCollection; + const global = manager.globalVariableCollection; expect(global.array).toHaveLength(2); expect(global.array.map((item) => item.name)).toEqual(['temperature', 'pressure']); }); - test('contains only one entry for duplicate spectrum IDs', () => { - const manager = getSpectrumManager(); + test('contains only one entry for duplicate variable IDs', () => { + const manager = getVariableManager(); manager.addDataArrayMetadata([ createDataArray({ @@ -558,39 +558,39 @@ describe('getSpectrumManager', () => { manager.finishAddingDataArrayMetadata(); - expect(manager.globalSpectrumCollection.array).toHaveLength(1); + expect(manager.globalVariableCollection.array).toHaveLength(1); }); - test('returns the same spectrum object as a local collection', () => { - const manager = getSpectrumManager(); + test('returns the same variable object as a local collection', () => { + const manager = getVariableManager(); const local = manager.addDataArrayMetadata([createDataArray()]); manager.finishAddingDataArrayMetadata(); const id = local.array[0].id; - const global = manager.globalSpectrumCollection; + const global = manager.globalVariableCollection; - expect(global.getSpectrum(id)).toBe(local.array[0]); + expect(global.getVariable(id)).toBe(local.array[0]); }); test('returns null for null and unknown global IDs', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); manager.addDataArrayMetadata([createDataArray()]); manager.finishAddingDataArrayMetadata(); - expect(manager.globalSpectrumCollection.getSpectrum(null)).toBeNull(); + expect(manager.globalVariableCollection.getVariable(null)).toBeNull(); - expect(manager.globalSpectrumCollection.getSpectrum('unknown')).toBeNull(); + expect(manager.globalVariableCollection.getVariable('unknown')).toBeNull(); }); test('returns a frozen global collection', () => { - const manager = getSpectrumManager(); + const manager = getVariableManager(); manager.finishAddingDataArrayMetadata(); - expect(Object.isFrozen(manager.globalSpectrumCollection)).toBe(true); + expect(Object.isFrozen(manager.globalVariableCollection)).toBe(true); }); }); }); @@ -626,8 +626,8 @@ function createDataArray({ }); } -function addSingleSpectrum(dataArray: VisorVtkDataArray): VisorSpectrumInfo { - const manager = getSpectrumManager(); +function addSingleVariable(dataArray: VisorVtkDataArray): VisorVariableInfo { + const manager = getVariableManager(); return manager.addDataArrayMetadata([dataArray]).array[0]; } 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 9c0adbc9..e33b6856 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 @@ -219,9 +219,9 @@ describe('WasmRenderer per-part sends: trigger name and payload', () => { const { renderer } = await makeRenderer(sender); await renderer.sendPartColorVariableAsync(7, { - spectrumId: 'POINT::pressure::1', - spectrumType: 'POINT', - spectrumName: 'pressure', + variableId: 'POINT::pressure::1', + variableType: 'POINT', + variableName: 'pressure', component: 0, min: 2, max: 8, @@ -243,9 +243,9 @@ describe('WasmRenderer per-part sends: trigger name and payload', () => { const { renderer } = await makeRenderer(sender); await renderer.sendPartColorVariableAsync(7, { - spectrumId: 'CELL::temperature::3', - spectrumType: 'CELL', - spectrumName: 'temperature', + variableId: 'CELL::temperature::3', + variableType: 'CELL', + variableName: 'temperature', component: -1, min: -1.5, max: 4.5, @@ -270,9 +270,9 @@ describe('WasmRenderer per-part sends: trigger name and payload', () => { const { renderer } = await makeRenderer(sender); await renderer.sendPartColorVariableAsync(7, { - spectrumId: 'not::a::parseable::id::at::all', - spectrumType: 'POINT', - spectrumName: 'pressure', + variableId: 'not::a::parseable::id::at::all', + variableType: 'POINT', + variableName: 'pressure', component: 0, min: 0, max: 1, @@ -336,9 +336,9 @@ describe('WasmRenderer per-part applies still mutate their wasm objects', () => const { renderer, mapper, lut } = await makeRenderer(makeSender()); await renderer.setColorVariableAsync(NODE_ID, { - spectrumId: 'POINT::pressure::1', - spectrumType: 'POINT', - spectrumName: 'pressure', + variableId: 'POINT::pressure::1', + variableType: 'POINT', + variableName: 'pressure', component: 0, min: 2, max: 8, @@ -373,9 +373,9 @@ describe('WasmRenderer sends when no sender is injected', () => { await expect(renderer.sendPartSelectedAsync(7, true)).resolves.toBeUndefined(); await expect( renderer.sendPartColorVariableAsync(7, { - spectrumId: 'POINT::pressure::1', - spectrumType: 'POINT', - spectrumName: 'pressure', + variableId: 'POINT::pressure::1', + variableType: 'POINT', + variableName: 'pressure', component: 0, min: 2, max: 8, diff --git a/src/ansys/visor/visor-client/src/renderer/IRenderer.ts b/src/ansys/visor/visor-client/src/renderer/IRenderer.ts index 705140d1..c992f736 100644 --- a/src/ansys/visor/visor-client/src/renderer/IRenderer.ts +++ b/src/ansys/visor/visor-client/src/renderer/IRenderer.ts @@ -45,9 +45,9 @@ export type CameraOrigin = 'gesture' | 'programmatic'; /** Descriptor consumed by setColorVariableAsync. */ export type ColorVariableDescriptor = Readonly<{ - spectrumId: string; - spectrumType: 'POINT' | 'CELL'; - spectrumName: string; + variableId: string; + variableType: 'POINT' | 'CELL'; + variableName: string; component: number; // -1 = magnitude min: number; max: number; diff --git a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts index 8998e36f..e2522615 100644 --- a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts +++ b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts @@ -342,14 +342,14 @@ export class WasmRenderer implements IRenderer { return; } const wasmMapper = this.#vtkScene.getVtkObject(handles.mapperId); - if (descriptor.spectrumType === 'POINT') { + if (descriptor.variableType === 'POINT') { await wasmMapper.SetScalarModeToUsePointFieldData(); } else { await wasmMapper.SetScalarModeToUseCellFieldData(); } await wasmMapper.SetScalarRange(descriptor.min, descriptor.max); await wasmMapper.SetColorModeToMapScalars(); - await wasmMapper.ColorByArrayComponent(descriptor.spectrumName, descriptor.component); + await wasmMapper.ColorByArrayComponent(descriptor.variableName, descriptor.component); await wasmMapper.SetScalarVisibility(1); // Force creation of LUT if not already done. (Alternatively, after 9.5.20250802.dev0, you can call mapper.SetLookupTable(null)) await wasmMapper.CreateDefaultLookupTable(); @@ -480,9 +480,9 @@ export class WasmRenderer implements IRenderer { // 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, + variableId: descriptor.variableId, + association: descriptor.variableType, + arrayName: descriptor.variableName, component: descriptor.component, min: descriptor.min, max: descriptor.max, diff --git a/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx b/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx index 769e9917..32cec1ce 100644 --- a/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx +++ b/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx @@ -1,9 +1,9 @@ import VisorColor from '../utils/VisorColor.tsx'; import { - getSpectrumManager, - VisorSpectrumCollection, - VisorSpectrumManager, -} from './VisorSpectrumManager.tsx'; + getVariableManager, + VisorVariableCollection, + VisorVariableManager, +} from './VisorVariableManager.tsx'; import VisorVtkDataArray from './appstate/vtkInfo/VisorVtkDataArray.tsx'; import { IRenderer } from '../renderer/IRenderer'; @@ -47,11 +47,11 @@ export type VisorSceneNodeExtended = Readonly<{ clearColorVariableAsync: () => Promise; setColorVariableAsync: (id: string, component?: number | null) => Promise; setScalarRangeAsync: (min: number, max: number) => Promise; - spectrumCollection: VisorSpectrumCollection; - spectrumId: string | null; - spectrumComponent: number; - spectrumMin: number; - spectrumMax: number; + variableCollection: VisorVariableCollection; + variableId: string | null; + variableComponent: number; + variableMin: number; + variableMax: number; defaultDiffuseColorRgb: Readonly; defaultDiffuseColorHex: string; customDiffuseColorRgb: Readonly; @@ -66,7 +66,7 @@ export type VisorSceneNodeExtended = Readonly<{ export const CreateVisorSceneGraph = (() => { return ( string_or_object: string | VisorSceneNodeSimple, - spectrumManager?: VisorSpectrumManager, + variableManager?: VisorVariableManager, renderer?: IRenderer ) => { let simpleState: VisorSceneNodeSimple; @@ -75,33 +75,33 @@ export const CreateVisorSceneGraph = (() => { } else { simpleState = string_or_object; } - const localSpectrumManager = spectrumManager == null; - spectrumManager ??= getSpectrumManager(); - const rootNode = extendSimpleNode(simpleState, spectrumManager, null, null, renderer); - localSpectrumManager && spectrumManager.finishAddingDataArrayMetadata(); + const localVariableManager = variableManager == null; + variableManager ??= getVariableManager(); + const rootNode = extendSimpleNode(simpleState, variableManager, null, null, renderer); + localVariableManager && variableManager.finishAddingDataArrayMetadata(); return rootNode; }; function extendSimpleNode( simpleNode: VisorSceneNodeSimple, - spectrumManager: VisorSpectrumManager, + variableManager: VisorVariableManager, rootNodeSimple?: VisorSceneNodeSimple | null, rootNodeExtended?: VisorSceneNodeExtended | null, renderer?: IRenderer ): VisorSceneNodeExtended { rootNodeSimple ??= simpleNode; - if (spectrumManager == null) { - throw new Error(`spectrumManager cannot be null`); + if (variableManager == null) { + throw new Error(`variableManager cannot be null`); } else if (rootNodeSimple.nodeType !== 'root') { throw new Error(`rootNodeSimple.nodeType must be 'root'`); } let _visible: boolean = true; let _selected: boolean = false; let _opacity: number = 1; - let _spectrumId: string | null = null; - let _spectrumComponent: number = -1; - let _spectrumMin: number = -1; - let _spectrumMax: number = -1; + let _variableId: string | null = null; + let _variableComponent: number = -1; + let _variableMin: number = -1; + let _variableMax: number = -1; const defaultDiffuseColor = new VisorColor(); const customDiffuseColor = new VisorColor(); if (simpleNode.isActorNode) { @@ -115,7 +115,7 @@ export const CreateVisorSceneGraph = (() => { } const nodeId = simpleNode.id; - const spectrumCollection = spectrumManager.addDataArrayMetadata(simpleNode.dataArrays); + const variableCollection = variableManager.addDataArrayMetadata(simpleNode.dataArrays); const node: VisorSceneNodeExtended = { id: simpleNode.id, @@ -146,7 +146,7 @@ export const CreateVisorSceneGraph = (() => { get opacity() { return _opacity; }, - spectrumCollection, + variableCollection, async resetDiffuseColorAsync() { customDiffuseColor.setHex(defaultDiffuseColor.hex); await renderer!.resetDiffuseColorAsync( @@ -184,37 +184,37 @@ export const CreateVisorSceneGraph = (() => { await renderer!.sendPartDiffuseColorAsync(nodeId, rgbNormalized); }, async clearColorVariableAsync() { - _spectrumId = null; - _spectrumComponent = -1; - _spectrumMin = -1; - _spectrumMax = -1; + _variableId = null; + _variableComponent = -1; + _variableMin = -1; + _variableMax = -1; await renderer!.clearColorVariableAsync(nodeId); await renderer!.sendClearPartColorVariableAsync(nodeId); }, async setColorVariableAsync(id, component) { - if (_spectrumId === id && _spectrumComponent === component) { + if (_variableId === id && _variableComponent === component) { return; } else if (component == null) { return; } - const spectrum = spectrumCollection.getSpectrum(id); - if (spectrum == null) { + const variable = variableCollection.getVariable(id); + if (variable == null) { return; } - const rangeInfo = spectrum.getRangeInfo(component); + const rangeInfo = variable.getRangeInfo(component); if (rangeInfo == null) { return; } const min = rangeInfo.customRange[0]; const max = rangeInfo.customRange[1]; - _spectrumId = id; - _spectrumComponent = component; - _spectrumMin = min; - _spectrumMax = max; + _variableId = id; + _variableComponent = component; + _variableMin = min; + _variableMax = max; const descriptor = { - spectrumId: id, - spectrumType: spectrum.type, - spectrumName: spectrum.name, + variableId: id, + variableType: variable.type, + variableName: variable.name, component, min, max, @@ -281,17 +281,17 @@ export const CreateVisorSceneGraph = (() => { await renderer!.setOpacityAsync(nodeId, opacity); await renderer!.sendPartOpacityAsync(nodeId, opacity); }, - get spectrumId() { - return _spectrumId; + get variableId() { + return _variableId; }, - get spectrumComponent(): number { - return _spectrumComponent; + get variableComponent(): number { + return _variableComponent; }, - get spectrumMin(): number { - return _spectrumMin; + get variableMin(): number { + return _variableMin; }, - get spectrumMax(): number { - return _spectrumMax; + get variableMax(): number { + return _variableMax; }, get customDiffuseColorRgb() { return customDiffuseColor.rgb; @@ -321,7 +321,7 @@ export const CreateVisorSceneGraph = (() => { simpleNode.children.forEach((simpleChild) => { const extendedChild = extendSimpleNode( simpleChild, - spectrumManager, + variableManager, rootNodeSimple, rootNodeExtended, renderer diff --git a/src/ansys/visor/visor-client/src/state/VisorSpectrumManager.tsx b/src/ansys/visor/visor-client/src/state/VisorVariableManager.tsx similarity index 72% rename from src/ansys/visor/visor-client/src/state/VisorSpectrumManager.tsx rename to src/ansys/visor/visor-client/src/state/VisorVariableManager.tsx index e5b96c8b..db677c3c 100644 --- a/src/ansys/visor/visor-client/src/state/VisorSpectrumManager.tsx +++ b/src/ansys/visor/visor-client/src/state/VisorVariableManager.tsx @@ -1,13 +1,13 @@ import VisorVtkDataArray, { FieldAssociation } from './appstate/vtkInfo/VisorVtkDataArray.tsx'; /** - * Describes a selectable component of a spectrum. + * Describes a selectable component of a variable. * * @remarks * The magnitude component uses an ID of `-1`. Individual array components * begin at `0`. */ -export type VisorSpectrumComponentMetadata = Readonly<{ +export type VisorVariableComponentMetadata = Readonly<{ /** Numeric identifier used to select the component. */ id: number; @@ -16,15 +16,15 @@ export type VisorSpectrumComponentMetadata = Readonly<{ }>; /** - * Describes a spectrum derived from one or more compatible VTK data arrays. + * Describes a variable derived from one or more compatible VTK data arrays. * * @remarks - * Spectrum objects are immutable, although their custom ranges can be changed - * through {@link VisorSpectrumInfo.setCustomRange}. + * Variable objects are immutable, although their custom ranges can be changed + * through {@link VisorVariableInfo.setCustomRange}. */ -export type VisorSpectrumInfo = Readonly<{ +export type VisorVariableInfo = Readonly<{ /** - * Unique spectrum identifier. + * Unique variable identifier. * * @remarks * The identifier has the format @@ -47,8 +47,8 @@ export type VisorSpectrumInfo = Readonly<{ /** Number of components in the underlying data array. */ numComponents: number; - /** Components that can be selected when displaying the spectrum. */ - componentOptions: VisorSpectrumComponentMetadata[]; + /** Components that can be selected when displaying the variable. */ + componentOptions: VisorVariableComponentMetadata[]; /** * Gets the default and custom ranges for a component. @@ -82,43 +82,43 @@ export type VisorSpectrumInfo = Readonly<{ /** * A collection of spectra associated with a group of data arrays. */ -export type VisorSpectrumCollection = Readonly<{ +export type VisorVariableCollection = Readonly<{ /** Spectra in collection order. */ - array: VisorSpectrumInfo[]; + array: VisorVariableInfo[]; /** - * Finds a spectrum by its unique identifier. + * Finds a variable by its unique identifier. * - * @param id - Spectrum identifier, or `null` when no spectrum is selected. - * @returns The matching spectrum, or `null` when no match exists. + * @param id - Variable identifier, or `null` when no variable is selected. + * @returns The matching variable, or `null` when no match exists. */ - getSpectrum: (id: string | null) => VisorSpectrumInfo | null; + getVariable: (id: string | null) => VisorVariableInfo | null; }>; /** - * Coordinates spectrum metadata across multiple groups of VTK data arrays. + * Coordinates variable metadata across multiple groups of VTK data arrays. * * @remarks - * Call {@link VisorSpectrumManager.addDataArrayMetadata} for every relevant + * Call {@link VisorVariableManager.addDataArrayMetadata} for every relevant * group of arrays, then call - * {@link VisorSpectrumManager.finishAddingDataArrayMetadata} once. The global + * {@link VisorVariableManager.finishAddingDataArrayMetadata} once. The global * collection is unavailable until finalization is complete. */ -export type VisorSpectrumManager = Readonly<{ +export type VisorVariableManager = Readonly<{ /** * Adds metadata for a group of VTK data arrays. * - * @param dataArrays - Data arrays from which spectrum metadata is derived. - * @returns A collection containing one spectrum for each supplied data array. + * @param dataArrays - Data arrays from which variable metadata is derived. + * @returns A collection containing one variable for each supplied data array. * * @remarks * Arrays with the same type, name, and component count share a global - * spectrum. Their default ranges are expanded to include all observed values. + * variable. Their default ranges are expanded to include all observed values. */ - addDataArrayMetadata: (dataArrays: VisorVtkDataArray[]) => VisorSpectrumCollection; + addDataArrayMetadata: (dataArrays: VisorVtkDataArray[]) => VisorVariableCollection; /** - * Finalizes the global spectrum collection. + * Finalizes the global variable collection. * * @throws Error if this method has already been called. */ @@ -128,28 +128,28 @@ export type VisorSpectrumManager = Readonly<{ * Finalized collection of all globally registered spectra. * * @throws Error if - * {@link VisorSpectrumManager.finishAddingDataArrayMetadata} has not yet + * {@link VisorVariableManager.finishAddingDataArrayMetadata} has not yet * been called. */ - globalSpectrumCollection: VisorSpectrumCollection; + globalVariableCollection: VisorVariableCollection; }>; /** - * Creates a spectrum manager for aggregating metadata from VTK data arrays. + * Creates a variable manager for aggregating metadata from VTK data arrays. * - * @returns A new spectrum manager with no registered spectra. + * @returns A new variable manager with no registered spectra. */ -export function getSpectrumManager(): VisorSpectrumManager { - /** Tracks spectrum IDs and their assigned lookup positions. */ - const spectrumIdLookup: Map = new Map(); +export function getVariableManager(): VisorVariableManager { + /** Tracks variable IDs and their assigned lookup positions. */ + const variableIdLookup: Map = new Map(); - /** Stores each globally unique spectrum by its ID. */ - const globalSpectrumMap: Map = new Map(); + /** Stores each globally unique variable by its ID. */ + const globalVariableMap: Map = new Map(); - /** Stores aggregate default ranges for each spectrum. */ + /** Stores aggregate default ranges for each variable. */ const globalDefaultRanges: Map = new Map(); - /** Stores user-configurable ranges for each spectrum. */ + /** Stores user-configurable ranges for each variable. */ const globalCustomRanges: Map = new Map(); /** Maps supported component counts to shape and component-label metadata. */ @@ -196,7 +196,7 @@ export function getSpectrumManager(): VisorSpectrumManager { }); /** Finalized global collection, or `null` until registration is complete. */ - let globalSpectrumCollection: VisorSpectrumCollection | null = null; + let globalVariableCollection: VisorVariableCollection | null = null; return Object.freeze({ addDataArrayMetadata, @@ -207,65 +207,65 @@ export function getSpectrumManager(): VisorSpectrumManager { * @throws Error if the global collection has already been finalized. */ finishAddingDataArrayMetadata() { - if (globalSpectrumCollection != null) { + if (globalVariableCollection != null) { throw new Error(`finishAddingDataArrayMetadata() has already been called`); } const array = []; - for (const item of globalSpectrumMap.values()) { + for (const item of globalVariableMap.values()) { array.push(item); } - globalSpectrumCollection = Object.freeze({ + globalVariableCollection = Object.freeze({ array, /** - * Finds a globally registered spectrum. + * Finds a globally registered variable. * - * @param id - Spectrum identifier, or `null`. - * @returns The matching spectrum, or `null` when none exists. + * @param id - Variable identifier, or `null`. + * @returns The matching variable, or `null` when none exists. */ - getSpectrum(id: string | null) { - return id != null ? (globalSpectrumMap.get(id) ?? null) : null; + getVariable(id: string | null) { + return id != null ? (globalVariableMap.get(id) ?? null) : null; }, }); }, /** - * Gets the finalized global spectrum collection. + * Gets the finalized global variable collection. * * @throws Error if metadata registration has not yet been finalized. */ - get globalSpectrumCollection() { - if (globalSpectrumCollection == null) { + get globalVariableCollection() { + if (globalVariableCollection == null) { throw new Error(`finishAddingDataArrayMetadata() has not been called yet`); } - return globalSpectrumCollection; + return globalVariableCollection; }, }); /** - * Creates or updates spectrum information for a data array. + * Creates or updates variable information for a data array. * * @param dataArray - Source data array metadata. - * @returns The newly created spectrum, or the existing compatible spectrum. + * @returns The newly created variable, or the existing compatible variable. * * @remarks - * When a compatible spectrum already exists, its default ranges are expanded + * When a compatible variable already exists, its default ranges are expanded * to include the new array's ranges. Its custom ranges are then reset to the * updated defaults. * * @throws Error if the data array has an unsupported component count. */ - function tryAddSpectrumInfo(dataArray: VisorVtkDataArray): VisorSpectrumInfo { + function tryAddVariableInfo(dataArray: VisorVtkDataArray): VisorVariableInfo { const { type, name, numComponents, magnitudeRange, ranges } = dataArray; // Use a human-readable ID like 'point::displacement::3' const id = `${type}::${name}::${numComponents}`; - if (globalSpectrumMap.has(id)) { - // Update the existing ranges for this spectrum + if (globalVariableMap.has(id)) { + // Update the existing ranges for this variable // with each subsequent new set of ranges. const defaultRanges = globalDefaultRanges.get(id)!; const customRanges = globalCustomRanges.get(id)!; @@ -283,7 +283,7 @@ export function getSpectrumManager(): VisorSpectrumManager { range[1] = defaultRanges[i][1]; }); - return globalSpectrumMap.get(id)!; + return globalVariableMap.get(id)!; } const labelInfo = labelInfoMap.get(numComponents); @@ -292,7 +292,7 @@ export function getSpectrumManager(): VisorSpectrumManager { throw new Error(`${msg} for data arrays with ${numComponents} component(s)?`); } - const componentOptions: VisorSpectrumComponentMetadata[] = []; + const componentOptions: VisorVariableComponentMetadata[] = []; for (let i = 0; i < labelInfo.componentLabels.length; i++) { componentOptions.push({ id: i - 1, @@ -318,7 +318,7 @@ export function getSpectrumManager(): VisorSpectrumManager { globalDefaultRanges.set(id, defaultRanges); globalCustomRanges.set(id, customRanges); - const info: VisorSpectrumInfo = Object.freeze({ + const info: VisorVariableInfo = Object.freeze({ id, type, name, @@ -366,22 +366,22 @@ export function getSpectrumManager(): VisorSpectrumManager { }, }); - globalSpectrumMap.set(id, info); + globalVariableMap.set(id, info); return info; } /** - * Registers a group of data arrays and creates its local spectrum collection. + * Registers a group of data arrays and creates its local variable collection. * * @param dataArrays - Data arrays to register. * @returns An immutable collection containing spectra for the supplied arrays. */ - function addDataArrayMetadata(dataArrays: VisorVtkDataArray[]): VisorSpectrumCollection { - const array: VisorSpectrumInfo[] = []; - const map: Map = new Map(); + function addDataArrayMetadata(dataArrays: VisorVtkDataArray[]): VisorVariableCollection { + const array: VisorVariableInfo[] = []; + const map: Map = new Map(); for (let i = 0; i < dataArrays.length; i++) { - const info = tryAddSpectrumInfo(dataArrays[i]); + const info = tryAddVariableInfo(dataArrays[i]); array.push(info); map.set(info.id, info); } @@ -392,12 +392,12 @@ export function getSpectrumManager(): VisorSpectrumManager { array, /** - * Finds a spectrum within this local collection. + * Finds a variable within this local collection. * - * @param id - Spectrum identifier, or `null`. - * @returns The matching spectrum, or `null` when none exists. + * @param id - Variable identifier, or `null`. + * @returns The matching variable, or `null` when none exists. */ - getSpectrum(id: string | null) { + getVariable(id: string | null) { return id != null ? (map.get(id) ?? null) : null; }, }); diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorPartState.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorPartState.tsx index bb2da2e2..406342de 100644 --- a/src/ansys/visor/visor-client/src/state/appstate/VisorPartState.tsx +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorPartState.tsx @@ -17,8 +17,8 @@ export default class VisorPartState { private _visible: boolean | undefined = undefined; private _diffuseRgb: Readonly | number[] | undefined = undefined; private _selected: boolean | undefined = undefined; - private _spectrumId: string | null | undefined = undefined; - private _spectrumComponent: number | undefined = undefined; + private _variableId: string | null | undefined = undefined; + private _variableComponent: number | undefined = undefined; constructor( state: StateInput = null, @@ -123,33 +123,33 @@ export default class VisorPartState { } } - get spectrumId(): string | null | undefined { - return this._spectrumId; + get variableId(): string | null | undefined { + return this._variableId; } - setSpectrumId(val: string | null | undefined, replace = false): void { + setVariableId(val: string | null | undefined, replace = false): void { if (val === undefined) { if (replace) { - this._spectrumId = val; + this._variableId = val; } } else { ensureStringOrNull(val, 'val'); - this._spectrumId = val; + this._variableId = val; } } - get spectrumComponent(): number | undefined { - return this._spectrumComponent; + get variableComponent(): number | undefined { + return this._variableComponent; } - setSpectrumComponent(val: number | undefined, replace = false): void { + setVariableComponent(val: number | undefined, replace = false): void { if (val === undefined) { if (replace) { - this._spectrumComponent = val; + this._variableComponent = val; } } else { ensureNumber(val, 'val'); - this._spectrumComponent = val; + this._variableComponent = val; } } @@ -197,14 +197,14 @@ export default class VisorPartState { replace ); this.setSelected(data.selected === undefined ? this._selected : data.selected, replace); - this.setSpectrumId( - data.spectrumId === undefined ? this._spectrumId : data.spectrumId, + this.setVariableId( + data.variableId === undefined ? this._variableId : data.variableId, replace ); - this.setSpectrumComponent( - data.spectrumComponent === undefined - ? this._spectrumComponent - : data.spectrumComponent, + this.setVariableComponent( + data.variableComponent === undefined + ? this._variableComponent + : data.variableComponent, replace ); } @@ -224,8 +224,8 @@ export default class VisorPartState { visible: this.visible, diffuseRgb: this.diffuseRgb, selected: this.selected, - spectrumId: this.spectrumId, - spectrumComponent: this.spectrumComponent, + variableId: this.variableId, + variableComponent: this.variableComponent, }; } } diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorSceneState.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorSceneState.tsx index 9c5f019d..86bb5151 100644 --- a/src/ansys/visor/visor-client/src/state/appstate/VisorSceneState.tsx +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorSceneState.tsx @@ -6,7 +6,7 @@ import { parseState, StateInput, } from './VisorStateCommon.tsx'; -import VisorSpectrumState from './VisorSpectrumState.tsx'; +import VisorVariableState from './VisorVariableState.tsx'; import VisorCameraState from './VisorCameraState.tsx'; import VisorCrossSectionState from './VisorCrossSectionState.tsx'; @@ -15,7 +15,7 @@ export default class VisorSceneState { private _camera: VisorCameraState = new VisorCameraState(); private _crossSection: VisorCrossSectionState = new VisorCrossSectionState(); private _datasetStates: Record = {}; - private _spectrumStates: Record = {}; + private _variableStates: Record = {}; private _orthographicEnabled: boolean | undefined = undefined; private _crossSectionEnabled: boolean | undefined = undefined; private _edgesEnabled: boolean | undefined = undefined; @@ -88,36 +88,36 @@ export default class VisorSceneState { return delete this._datasetStates[idStr]; } - getSpectrumState(idStr: string): VisorSpectrumState | null { + getVariableState(idStr: string): VisorVariableState | null { ensureString(idStr, 'idStr'); - return this._spectrumStates[idStr] ?? null; + return this._variableStates[idStr] ?? null; } - getSpectrumStates(): VisorSpectrumState[] { - return Object.values(this._spectrumStates); + getVariableStates(): VisorVariableState[] { + return Object.values(this._variableStates); } - removeSpectrum(idStr: string): boolean { - return delete this._spectrumStates[idStr]; + removeVariable(idStr: string): boolean { + return delete this._variableStates[idStr]; } - copySpectrum( - spectrumState: StateInput, + copyVariable( + variableState: StateInput, replace = false, key: string | number | null | undefined = null - ): VisorSpectrumState | null { - if (spectrumState == null) { + ): VisorVariableState | null { + if (variableState == null) { return null; } - const newState = new VisorSpectrumState(spectrumState, key); - const existing = this._spectrumStates[newState.id]; + const newState = new VisorVariableState(variableState, key); + const existing = this._variableStates[newState.id]; if (existing) { return existing.copy(newState, replace); } - this._spectrumStates[newState.id] = newState; + this._variableStates[newState.id] = newState; return newState; } @@ -234,15 +234,15 @@ export default class VisorSceneState { } } - const spectrumStates = data.spectrumStates; - if (spectrumStates) { - for (const [k, v] of Object.entries(spectrumStates)) { - this.copySpectrum(v as JsonDict, replace, k); + const variableStates = data.variableStates; + if (variableStates) { + for (const [k, v] of Object.entries(variableStates)) { + this.copyVariable(v as JsonDict, replace, k); } if (replace) { - for (const k of Object.keys(this._spectrumStates)) { - if (!spectrumStates[k]) { - delete this._spectrumStates[k]; + for (const k of Object.keys(this._variableStates)) { + if (!variableStates[k]) { + delete this._variableStates[k]; } } } @@ -256,10 +256,10 @@ export default class VisorSceneState { } toDict(): JsonDict { - const spectrumStates: JsonDict = {}; - for (const [k, v] of Object.entries(this._spectrumStates)) { + const variableStates: JsonDict = {}; + for (const [k, v] of Object.entries(this._variableStates)) { if (k === v.id) { - spectrumStates[k] = v.toDict(); + variableStates[k] = v.toDict(); } } const datasetStates: JsonDict = {}; @@ -276,7 +276,7 @@ export default class VisorSceneState { crossSectionEnabled: this.crossSectionEnabled, edgesEnabled: this.edgesEnabled, boundingBoxEnabled: this.boundingBoxEnabled, - spectrumStates, + variableStates, datasetStates, }; } diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorSpectrumState.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorVariableState.tsx similarity index 93% rename from src/ansys/visor/visor-client/src/state/appstate/VisorSpectrumState.tsx rename to src/ansys/visor/visor-client/src/state/appstate/VisorVariableState.tsx index c2151b7f..a2d01884 100644 --- a/src/ansys/visor/visor-client/src/state/appstate/VisorSpectrumState.tsx +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorVariableState.tsx @@ -9,7 +9,7 @@ import { } from './VisorStateCommon.tsx'; import type { FieldAssociation } from './vtkInfo/VisorVtkDataArray.tsx'; -export default class VisorSpectrumState { +export default class VisorVariableState { private _id: string = ''; private _arrayName: string = ''; private _type: FieldAssociation | undefined = undefined; @@ -18,7 +18,7 @@ export default class VisorSpectrumState { private _ranges: (number[] | undefined)[] = []; constructor( - state: StateInput = null, + state: StateInput = null, key: string | number | null | undefined = null ) { this.copy(state, false, key); @@ -99,14 +99,14 @@ export default class VisorSpectrumState { } copy( - state: StateInput = null, + state: StateInput = null, replace = false, key: string | number | null | undefined = null ): this { if (state != null) { let data: JsonDict; - if (state instanceof VisorSpectrumState) { + if (state instanceof VisorVariableState) { data = state.toDict(); } else { data = parseState(state)!; @@ -115,7 +115,7 @@ export default class VisorSpectrumState { const thisId = (data.id ?? this._id)?.toString(); if ((key = key?.toString()) != null && key !== thisId) { - throw new Error(`Spectrum state with id '${thisId}' does not equal key '${key}'`); + throw new Error(`Variable state with id '${thisId}' does not equal key '${key}'`); } this.setId(thisId); diff --git a/tests/e2e/regressions/test_variable_components.py b/tests/e2e/regressions/test_variable_components.py index 9cf947da..bf13a85c 100644 --- a/tests/e2e/regressions/test_variable_components.py +++ b/tests/e2e/regressions/test_variable_components.py @@ -130,7 +130,7 @@ def test_component_selection_changes_rendering(self, page: Page, visor_server): @pytest.mark.regression class TestTensorVariableDropdown: """ - Verify that the variable/spectrum dropdown in the UI contains + Verify that the variable dropdown in the UI contains all 9 tensor components plus Magnitude (10 total) when a dataset with a 9-component variable is loaded. """ diff --git a/tests/integration/test_save_load_state.py b/tests/integration/test_save_load_state.py index 1d966a11..a42d5cf6 100644 --- a/tests/integration/test_save_load_state.py +++ b/tests/integration/test_save_load_state.py @@ -526,8 +526,8 @@ def test_reloading_the_saved_state_restores_the_registry(self, file_io, iface, t 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 + assert record.variable_id == "POINT::pressure::1" + assert record.variable_component == 0 def test_saved_visor_json_carries_the_camera_record_not_the_browsers(self, iface, tmp_path): """save_state writes the server's camera record, not the browser's reply. diff --git a/tests/references/dash_snapshot_reference.json b/tests/references/dash_snapshot_reference.json index 569da5ce..d6f5fafc 100644 --- a/tests/references/dash_snapshot_reference.json +++ b/tests/references/dash_snapshot_reference.json @@ -4,7 +4,7 @@ }, "scene": { "unit": "m", - "spectrumStates": { + "variableStates": { "POINT::Normals::3": { "id": "POINT::Normals::3", "magnitudeRange": [ @@ -79,8 +79,8 @@ "opacity": 0.1111111111111111, "visible": true, "selected": false, - "spectrumId": "", - "spectrumComponent": -1 + "variableId": "", + "variableComponent": -1 }, "level_1_block_0_part_1": { "id": "7704132612406141", @@ -89,8 +89,8 @@ "opacity": 0.1111111111111111, "visible": true, "selected": false, - "spectrumId": "", - "spectrumComponent": -1 + "variableId": "", + "variableComponent": -1 }, "level_1_block_0_part_2": { "id": "6038438258649815", @@ -99,8 +99,8 @@ "opacity": 0.1111111111111111, "visible": true, "selected": false, - "spectrumId": "", - "spectrumComponent": -1 + "variableId": "", + "variableComponent": -1 }, "level_1_block_1_part_0": { "id": "5212416988329659", @@ -109,8 +109,8 @@ "opacity": 0.2222222222222222, "visible": true, "selected": false, - "spectrumId": "", - "spectrumComponent": -1 + "variableId": "", + "variableComponent": -1 }, "level_1_block_1_part_1": { "id": "3795994468691737", @@ -119,8 +119,8 @@ "opacity": 0.2222222222222222, "visible": true, "selected": false, - "spectrumId": "", - "spectrumComponent": -1 + "variableId": "", + "variableComponent": -1 }, "level_1_block_1_part_2": { "id": "2633610373980222", @@ -129,8 +129,8 @@ "opacity": 0.2222222222222222, "visible": true, "selected": false, - "spectrumId": "", - "spectrumComponent": -1 + "variableId": "", + "variableComponent": -1 }, "level_1_block_2_part_0": { "id": "1366333556315428", @@ -139,8 +139,8 @@ "opacity": 0.3333333333333333, "visible": true, "selected": false, - "spectrumId": "", - "spectrumComponent": -1 + "variableId": "", + "variableComponent": -1 }, "level_1_block_2_part_1": { "id": "1809875816089870", @@ -149,8 +149,8 @@ "opacity": 0.3333333333333333, "visible": true, "selected": false, - "spectrumId": "", - "spectrumComponent": -1 + "variableId": "", + "variableComponent": -1 }, "level_1_block_2_part_2": { "id": "4221596239203654", @@ -159,8 +159,8 @@ "opacity": 0.3333333333333333, "visible": true, "selected": false, - "spectrumId": "", - "spectrumComponent": -1 + "variableId": "", + "variableComponent": -1 } } } diff --git a/tests/unit/models/test_dataset_state.py b/tests/unit/models/test_dataset_state.py index 761a25a3..9820ac8f 100644 --- a/tests/unit/models/test_dataset_state.py +++ b/tests/unit/models/test_dataset_state.py @@ -19,59 +19,59 @@ def test_parts_key_types_preserved(): # --- Serialization: undefined-vs-null contract --- -def test_spectrum_id_null_always_serialized(): - """spectrum_id=None must appear in the output as null (not be omitted). +def test_variable_id_null_always_serialized(): + """variable_id=None must appear in the output as null (not be omitted). - Frontend distinguishes null ("remove spectrum") from absent ("pass-through"). - The wire format uses the camelCase alias ``spectrumId``. + Frontend distinguishes null ("remove variable") from absent ("pass-through"). + The wire format uses the camelCase alias ``variableId``. """ - p = RuntimePartProperties(id=1, spectrum_id=None) + p = RuntimePartProperties(id=1, variable_id=None) data = p.model_dump(by_alias=True) - assert "spectrumId" in data - assert data["spectrumId"] is None + assert "variableId" in data + assert data["variableId"] is None -def test_spectrum_id_value_serialized(): - """spectrum_id with a real value must be present.""" - p = RuntimePartProperties(id=1, spectrum_id="pressure") +def test_variable_id_value_serialized(): + """variable_id with a real value must be present.""" + p = RuntimePartProperties(id=1, variable_id="pressure") data = p.model_dump(by_alias=True) - assert data["spectrumId"] == "pressure" + assert data["variableId"] == "pressure" def test_optional_fields_omitted_when_none(): - """opacity, visible, selected, spectrumComponent, diffuseRgb must be omitted when None. + """opacity, visible, selected, variableComponent, diffuseRgb must be omitted when None. The frontend interprets an absent key as undefined / pass-through. """ p = RuntimePartProperties(id=1) # all optional fields default to None data = p.model_dump(by_alias=True) - for field in ("opacity", "visible", "selected", "spectrumComponent", "diffuseRgb"): + for field in ("opacity", "visible", "selected", "variableComponent", "diffuseRgb"): assert field not in data, f"Expected '{field}' to be omitted when None, but it was present" def test_optional_fields_present_when_set(): """Optional fields must appear when they carry an actual value.""" - p = RuntimePartProperties(id=1, opacity=0.5, visible=True, selected=False, spectrum_component=2, + p = RuntimePartProperties(id=1, opacity=0.5, visible=True, selected=False, variable_component=2, diffuse_rgb=[0.1, 0.2, 0.3]) data = p.model_dump(by_alias=True) assert data["opacity"] == 0.5 assert data["visible"] is True assert data["selected"] is False - assert data["spectrumComponent"] == 2 + assert data["variableComponent"] == 2 assert data["diffuseRgb"] == [0.1, 0.2, 0.3] -def test_clear_spectrum_round_trip(): - """A part with spectrum_id=None serializes correctly and round-trips through PartProperties.""" +def test_clear_variable_round_trip(): + """A part with variable_id=None serializes correctly and round-trips through PartProperties.""" from ansys.visor.viewer.models.common.part_properties import PartProperties props = PartProperties(color_by=None) p = RuntimePartProperties.from_part_properties(id=42, props=props) data = p.model_dump(by_alias=True) - # spectrumId must be present and null so the frontend removes the spectrum - assert "spectrumId" in data - assert data["spectrumId"] is None + # variableId must be present and null so the frontend removes the variable + assert "variableId" in data + assert data["variableId"] is None # Round-trip back to PartProperties back = p.to_part_properties() diff --git a/tests/unit/models/test_runtime_scene_state.py b/tests/unit/models/test_runtime_scene_state.py index 2111120b..6b4210b5 100644 --- a/tests/unit/models/test_runtime_scene_state.py +++ b/tests/unit/models/test_runtime_scene_state.py @@ -66,7 +66,7 @@ def test_default_fields(): state = RuntimeSceneState() assert state.dataset_states == {} - assert state.spectrum_states == {} + assert state.variable_states == {} assert state.unit is None @@ -98,17 +98,17 @@ def test_dataset_states_accept_valid_mapping(): # ------------------------------------------------------------------ -# spectrum_states +# variable_states # ------------------------------------------------------------------ -def test_spectrum_states_default_and_assignment(): - """spectrum_states should accept valid mapping.""" +def test_variable_states_default_and_assignment(): + """variable_states should accept valid mapping.""" data = { "a": MagicMock(spec=VisorVariableState), } - state = RuntimeSceneState(spectrum_states=data) + state = RuntimeSceneState(variable_states=data) - assert "a" in state.spectrum_states - assert isinstance(state.spectrum_states["a"], VisorVariableState) + assert "a" in state.variable_states + assert isinstance(state.variable_states["a"], VisorVariableState) diff --git a/tests/unit/models/test_visor_save_state_response.py b/tests/unit/models/test_visor_save_state_response.py index f0de700c..b5d97b41 100644 --- a/tests/unit/models/test_visor_save_state_response.py +++ b/tests/unit/models/test_visor_save_state_response.py @@ -118,7 +118,7 @@ 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 + and ``RuntimeSceneState.variable_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, @@ -128,7 +128,7 @@ def test_save_path_rejects_a_variable_state_missing_the_identity_fields(): "requestId": 1, "appState": { "scene": { - "spectrumStates": { + "variableStates": { "POINT::pressure::1": { "id": "POINT::pressure::1", "magnitudeRange": [0.0, 1.0], @@ -153,7 +153,7 @@ def test_save_path_accepts_a_variable_state_carrying_the_identity_fields(): "requestId": 1, "appState": { "scene": { - "spectrumStates": { + "variableStates": { "POINT::pressure::1": { "id": "POINT::pressure::1", "arrayName": "pressure", @@ -169,7 +169,7 @@ def test_save_path_accepts_a_variable_state_carrying_the_identity_fields(): resp = VisorSaveStateResponse.model_validate(payload) - stored = resp.app_state.scene.spectrum_states["POINT::pressure::1"] + stored = resp.app_state.scene.variable_states["POINT::pressure::1"] assert stored.array_name == "pressure" assert stored.num_components == 1 diff --git a/tests/unit/renderer/test_local_renderer.py b/tests/unit/renderer/test_local_renderer.py index ca9119a1..386f5320 100644 --- a/tests/unit/renderer/test_local_renderer.py +++ b/tests/unit/renderer/test_local_renderer.py @@ -615,7 +615,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.""" + """The association is forwarded unchanged; variable_id is not forwarded.""" pipe = MagicMock(name="pipeline") renderer._pipelines[4] = pipe diff --git a/tests/unit/vtk/datasets/test_visor_dataset_registry.py b/tests/unit/vtk/datasets/test_visor_dataset_registry.py index 0020b47c..b38197a7 100644 --- a/tests/unit/vtk/datasets/test_visor_dataset_registry.py +++ b/tests/unit/vtk/datasets/test_visor_dataset_registry.py @@ -300,33 +300,33 @@ def test_set_part_selected_mutates_record_and_rejects_unknown_part(registry): 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.""" + """Verify set_part_color_variable sets variable_id and variable_component in one call.""" seed = RuntimePartProperties(id=10) - assert seed.spectrum_id is None - assert seed.spectrum_component is None + assert seed.variable_id is None + assert seed.variable_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 result.variable_id == "POINT::pressure::1" + assert result.variable_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) + """Verify clear_part_color_variable clears variable_id and variable_component in one call.""" + seed = RuntimePartProperties(id=10, variable_id="POINT::pressure::1", variable_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 result.variable_id is None + assert result.variable_component is None assert registry.clear_part_color_variable(999999) is False diff --git a/tests/unit/vtk/scene/test_base.py b/tests/unit/vtk/scene/test_base.py index e51f98e8..5c7daf60 100644 --- a/tests/unit/vtk/scene/test_base.py +++ b/tests/unit/vtk/scene/test_base.py @@ -445,8 +445,8 @@ def test_set_part_color_variable_writes_the_registry_record(scene, registry): ) state = registry.get_part_state(NODE_ID) - assert state.spectrum_id == "POINT::pressure::1" - assert state.spectrum_component == 0 + assert state.variable_id == "POINT::pressure::1" + assert state.variable_component == 0 def test_set_part_color_variable_applies_to_the_vtk_mapper(scene, pipeline): @@ -499,8 +499,8 @@ def test_clear_part_color_variable_clears_the_registry_record(scene, registry): 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 + assert state.variable_id is None + assert state.variable_component is None def test_clear_part_color_variable_disables_scalar_visibility_on_the_mapper(scene, pipeline): @@ -1174,7 +1174,7 @@ def test_apply_state_pushes_a_json_encodable_runtime_state(scene, registry): _apply( scene, _runtime_state( - {NODE_ID: RuntimePartProperties(id=NODE_ID, opacity=0.25, spectrum_id=VARIABLE_ID)}, + {NODE_ID: RuntimePartProperties(id=NODE_ID, opacity=0.25, variable_id=VARIABLE_ID)}, variable_states={VARIABLE_ID: _variable_state()}, ), ) @@ -1737,7 +1737,7 @@ def _color_variable_state(component, **variable_kwargs): return _runtime_state( { NODE_ID: RuntimePartProperties( - id=NODE_ID, spectrum_id=VARIABLE_ID, spectrum_component=component + id=NODE_ID, variable_id=VARIABLE_ID, variable_component=component ) }, variable_states={VARIABLE_ID: _variable_state(**variable_kwargs)}, @@ -1824,7 +1824,7 @@ def test_apply_state_unknown_variable_identifier_is_a_logged_no_op( runtime = _runtime_state( { NODE_ID: RuntimePartProperties( - id=NODE_ID, spectrum_id=VARIABLE_ID, spectrum_component=0 + id=NODE_ID, variable_id=VARIABLE_ID, variable_component=0 ) }, variable_states={}, @@ -1884,7 +1884,7 @@ def test_apply_state_variable_id_without_component_is_a_logged_no_op( _seed_part_variables(registry, [_pressure_variable()]) _seed_unconfigured_mapper(pipeline) runtime = _runtime_state( - {NODE_ID: RuntimePartProperties(id=NODE_ID, spectrum_id=VARIABLE_ID)}, + {NODE_ID: RuntimePartProperties(id=NODE_ID, variable_id=VARIABLE_ID)}, variable_states={VARIABLE_ID: _variable_state()}, ) @@ -1901,7 +1901,7 @@ def test_apply_state_component_without_variable_id_does_not_clear( """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)}, + {NODE_ID: RuntimePartProperties(id=NODE_ID, variable_component=0)}, variable_states={VARIABLE_ID: _variable_state()}, ) diff --git a/tests/unit/vtk/scene/test_visor_state_mapper.py b/tests/unit/vtk/scene/test_visor_state_mapper.py index 6fc54133..80f43608 100644 --- a/tests/unit/vtk/scene/test_visor_state_mapper.py +++ b/tests/unit/vtk/scene/test_visor_state_mapper.py @@ -66,7 +66,7 @@ def fake_from_components(**kwargs): "scene": type("Scene", (), { "unit": "m", "camera": "cam", - "spectrum_states": {"var": 1}, + "variable_states": {"var": 1}, "dataset_states": {1: {"state": 123}}, "cross_section": "cs", "orthographic_enabled": True, @@ -107,7 +107,7 @@ def fake_from_components(**kwargs): "scene": type("Scene", (), { "unit": "m", "camera": "cam", - "spectrum_states": {}, + "variable_states": {}, "dataset_states": {123: {"data": 1}}, "cross_section": None, "orthographic_enabled": False, From 14bab37d10e46f9c2dfd30bc3084bec60621a53a Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Mon, 28 Sep 2026 09:37:22 -0700 Subject: [PATCH 2/5] bump VisorSceneDetails version after rename --- src/ansys/visor/viewer/models/runtime/visor_scene_details.py | 2 +- .../visor/visor-client/src/state/appstate/VisorSceneDetails.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ansys/visor/viewer/models/runtime/visor_scene_details.py b/src/ansys/visor/viewer/models/runtime/visor_scene_details.py index 80a45ca5..dc8c5c89 100644 --- a/src/ansys/visor/viewer/models/runtime/visor_scene_details.py +++ b/src/ansys/visor/viewer/models/runtime/visor_scene_details.py @@ -11,7 +11,7 @@ from ansys.visor.viewer.models.runtime.vtk.scene_graph_node_info import SceneGraphNodeInfo # Bumped whenever the scene-details wire shape changes incompatibly. -SCENE_DETAILS_SCHEMA_VERSION: int = 2 +SCENE_DETAILS_SCHEMA_VERSION: int = 3 class VisorSceneDetails(BaseModel): diff --git a/src/ansys/visor/visor-client/src/state/appstate/VisorSceneDetails.tsx b/src/ansys/visor/visor-client/src/state/appstate/VisorSceneDetails.tsx index 4a356cb0..dc4ba29e 100644 --- a/src/ansys/visor/visor-client/src/state/appstate/VisorSceneDetails.tsx +++ b/src/ansys/visor/visor-client/src/state/appstate/VisorSceneDetails.tsx @@ -7,7 +7,7 @@ import VisorAppState from './VisorAppState.tsx'; * (`ansys.visor.viewer.models.runtime.visor_scene_details`). Bumped whenever * the scene-details wire shape changes incompatibly. See proposal §2. */ -export const SCENE_DETAILS_SCHEMA_VERSION = 2; +export const SCENE_DETAILS_SCHEMA_VERSION = 3; /** * Thrown when the payload's `schemaVersion` does not match the client's From ba64f47c5cc1d513f9403f82c0a9598e7aef024b Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:44:10 +0000 Subject: [PATCH 3/5] chore: adding changelog file 150.maintenance.md [dependabot-skip] --- doc/changelog.d/150.maintenance.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/150.maintenance.md diff --git a/doc/changelog.d/150.maintenance.md b/doc/changelog.d/150.maintenance.md new file mode 100644 index 00000000..31ef9c86 --- /dev/null +++ b/doc/changelog.d/150.maintenance.md @@ -0,0 +1 @@ +Align variable naming across stacks From 9666c5538454b4867daa3aaf650aa73c28c7781a Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Mon, 28 Sep 2026 14:07:29 -0700 Subject: [PATCH 4/5] rename spectra to variables --- .../src/aggregate/AggregateSelectionInfo.tsx | 2 +- .../AggregateSelectionInfo.test.tsx | 44 +++++++-------- .../AggregateVariableComponentInfo.test.tsx | 36 ++++++------- .../jest-tests/AggregateVariableInfo.test.tsx | 54 +++++++++---------- .../jest-tests/VisorVariableManager.test.tsx | 10 ++-- .../src/state/VisorVariableManager.tsx | 10 ++-- 6 files changed, 78 insertions(+), 78 deletions(-) diff --git a/src/ansys/visor/visor-client/src/aggregate/AggregateSelectionInfo.tsx b/src/ansys/visor/visor-client/src/aggregate/AggregateSelectionInfo.tsx index 83c7cc21..c1b767d4 100644 --- a/src/ansys/visor/visor-client/src/aggregate/AggregateSelectionInfo.tsx +++ b/src/ansys/visor/visor-client/src/aggregate/AggregateSelectionInfo.tsx @@ -63,7 +63,7 @@ export class AggregateSelectionInfo { #displayDiffuseColor: string | null | undefined = null; /** - * Available spectra collected from all selected nodes, keyed by variable ID. + * Available variables collected from all selected nodes, keyed by variable ID. */ #variableOptions: Map = new Map(); diff --git a/src/ansys/visor/visor-client/src/jest-tests/AggregateSelectionInfo.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/AggregateSelectionInfo.test.tsx index b82838ad..adbd207f 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/AggregateSelectionInfo.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/AggregateSelectionInfo.test.tsx @@ -31,7 +31,7 @@ describe('AggregateSelectionInfo', () => { variableId: '10', variableComponent: 1, customDiffuseColorHex: '#123456', - spectra: [variable], + variables: [variable], }), createActorNode({ name: 'mesh', @@ -39,7 +39,7 @@ describe('AggregateSelectionInfo', () => { variableId: '10', variableComponent: 1, customDiffuseColorHex: '#123456', - spectra: [variable], + variables: [variable], }), ]; @@ -61,17 +61,17 @@ describe('AggregateSelectionInfo', () => { const actorNodes = [ createActorNode({ - spectra: [variable], + variables: [variable], variableId: '10', variableComponent: 1, }), createActorNode({ - spectra: [variable], + variables: [variable], variableId: '10', variableComponent: 1, }), createActorNode({ - spectra: [variable], + variables: [variable], variableId: '10', variableComponent: 1, }), @@ -89,12 +89,12 @@ describe('AggregateSelectionInfo', () => { const actorNodes = [ createActorNode({ - spectra: [variable10], + variables: [variable10], variableId: '10', variableComponent: 1, }), createActorNode({ - spectra: [variable20], + variables: [variable20], variableId: '10', variableComponent: 1, }), @@ -116,7 +116,7 @@ describe('AggregateSelectionInfo', () => { variableId: '10', variableComponent: 1, customDiffuseColorHex: '#111111', - spectra: [variable10, variable20], + variables: [variable10, variable20], }), createActorNode({ name: 'mesh B', @@ -124,7 +124,7 @@ describe('AggregateSelectionInfo', () => { variableId: '20', variableComponent: 1, customDiffuseColorHex: '#222222', - spectra: [variable10, variable20], + variables: [variable10, variable20], }), ]; @@ -147,7 +147,7 @@ describe('AggregateSelectionInfo', () => { variableId: '10', variableComponent: 1, customDiffuseColorHex: '#123456', - spectra: [variable], + variables: [variable], }), createActorNode({ name: 'same name', @@ -155,7 +155,7 @@ describe('AggregateSelectionInfo', () => { variableId: '10', variableComponent: 1, customDiffuseColorHex: '#123456', - spectra: [variable], + variables: [variable], }), ]; @@ -173,7 +173,7 @@ describe('AggregateSelectionInfo', () => { const actorNode = createActorNode({ variableId: '999', variableComponent: 1, - spectra: [createVariable(10, [1])], + variables: [createVariable(10, [1])], }); const result = await AggregateSelectionInfo.getInstanceAsync([actorNode]); @@ -234,7 +234,7 @@ describe('AggregateSelectionInfo', () => { createActorNode({ variableId: '10', variableComponent: 1, - spectra: [variable10, variable20], + variables: [variable10, variable20], }), ]); @@ -252,7 +252,7 @@ describe('AggregateSelectionInfo', () => { createActorNode({ variableId: null, variableComponent: 1, - spectra: [variable], + variables: [variable], }), ]); @@ -269,7 +269,7 @@ describe('AggregateSelectionInfo', () => { createActorNode({ variableId: '10', variableComponent: 1, - spectra: [variable], + variables: [variable], }), ]); @@ -287,7 +287,7 @@ describe('AggregateSelectionInfo', () => { createActorNode({ variableId: '10', variableComponent: 1, - spectra: [variable], + variables: [variable], }), ]); @@ -308,7 +308,7 @@ describe('AggregateSelectionInfo', () => { createActorNode({ variableId: '10', variableComponent: 1, - spectra: [variable10, variable20], + variables: [variable10, variable20], }), ]); @@ -328,7 +328,7 @@ describe('AggregateSelectionInfo', () => { createActorNode({ variableId: '10', variableComponent: 1, - spectra: [variable], + variables: [variable], }), ]); @@ -362,7 +362,7 @@ interface ActorNodeOptions { variableId?: string | null; variableComponent?: number; customDiffuseColorHex?: string | null; - spectra?: VariableFixture[]; + variables?: VariableFixture[]; } function createVariable(id: number, componentIds: number[]): VariableFixture { @@ -419,7 +419,7 @@ function createActorNode({ variableId = null, variableComponent = 0, customDiffuseColorHex = '#ffffff', - spectra = [], + variables = [], }: ActorNodeOptions = {}): VisorSceneNodeExtended { return { name, @@ -429,10 +429,10 @@ function createActorNode({ customDiffuseColorHex, variableCollection: { - array: spectra.map((variable) => variable.metadata), + array: variables.map((variable) => variable.metadata), getVariable: jest.fn((id: number | string) => { - const match = spectra.find( + const match = variables.find( (variable) => variable.metadata.id.toString() === id.toString() ); diff --git a/src/ansys/visor/visor-client/src/jest-tests/AggregateVariableComponentInfo.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/AggregateVariableComponentInfo.test.tsx index 7c19c0e1..eed64a44 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/AggregateVariableComponentInfo.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/AggregateVariableComponentInfo.test.tsx @@ -23,7 +23,7 @@ describe('AggregateVariableComponentInfo', () => { createActorNode({ variableId: parentVariable.id, variableComponent: 0, - spectra: [ + variables: [ createVariable({ id: parentVariable.id, componentIds: [-1, 0, 1, 2], @@ -78,12 +78,12 @@ describe('AggregateVariableComponentInfo', () => { createActorNode({ variableId: parentVariable.id, variableComponent: 0, - spectra: [firstVariable], + variables: [firstVariable], }), createActorNode({ variableId: parentVariable.id, variableComponent: 0, - spectra: [secondVariable], + variables: [secondVariable], }), ], parentVariable, @@ -117,12 +117,12 @@ describe('AggregateVariableComponentInfo', () => { createActorNode({ variableId: parentVariable.id, variableComponent: 0, - spectra: [firstVariable], + variables: [firstVariable], }), createActorNode({ variableId: parentVariable.id, variableComponent: 0, - spectra: [secondVariable], + variables: [secondVariable], }), ], parentVariable, @@ -157,12 +157,12 @@ describe('AggregateVariableComponentInfo', () => { createActorNode({ variableId: parentVariable.id, variableComponent: 0, - spectra: [matchingVariable], + variables: [matchingVariable], }), createActorNode({ variableId: unrelatedVariable.id, variableComponent: -1, - spectra: [unrelatedVariable], + variables: [unrelatedVariable], }), ], parentVariable, @@ -196,12 +196,12 @@ describe('AggregateVariableComponentInfo', () => { createActorNode({ variableId: parentVariable.id, variableComponent: 0, - spectra: [variableWithRange], + variables: [variableWithRange], }), createActorNode({ variableId: parentVariable.id, variableComponent: 0, - spectra: [variableWithoutRange], + variables: [variableWithoutRange], }), ], parentVariable, @@ -237,12 +237,12 @@ describe('AggregateVariableComponentInfo', () => { createActorNode({ variableId: parentVariable.id, variableComponent: 0, - spectra: [missingRangeVariable], + variables: [missingRangeVariable], }), createActorNode({ variableId: parentVariable.id, variableComponent: 0, - spectra: [laterVariable], + variables: [laterVariable], }), ], parentVariable, @@ -460,7 +460,7 @@ interface CreateVariableOptions { interface CreateActorNodeOptions { variableId: string | null; variableComponent: number; - spectra: VisorVariableInfo[]; + variables: VisorVariableInfo[]; } async function createParentVariable(): Promise { @@ -474,7 +474,7 @@ async function createParentVariable(): Promise { const actorNode = createActorNode({ variableId: variable.id, variableComponent: 0, - spectra: [variable], + variables: [variable], }); const selection = await AggregateSelectionInfo.getInstanceAsync([actorNode]); @@ -503,7 +503,7 @@ async function createComponentInfo(): Promise { createActorNode({ variableId: parentVariable.id, variableComponent: 0, - spectra: [actorVariable], + variables: [actorVariable], }), ], parentVariable, @@ -593,8 +593,8 @@ function createVariable({ }; } -function createVariableCollection(spectra: VisorVariableInfo[]): VisorVariableCollection { - const array = [...spectra]; +function createVariableCollection(variables: VisorVariableInfo[]): VisorVariableCollection { + const array = [...variables]; const map = new Map( array.map((variable) => [variable.id, variable]) @@ -616,7 +616,7 @@ function createVariableCollection(spectra: VisorVariableInfo[]): VisorVariableCo function createActorNode({ variableId, variableComponent, - spectra, + variables, }: CreateActorNodeOptions): VisorSceneNodeExtended { return { name: 'mesh', @@ -624,7 +624,7 @@ function createActorNode({ variableId, variableComponent, customDiffuseColorHex: '#ffffff', - variableCollection: createVariableCollection(spectra), + variableCollection: createVariableCollection(variables), } as unknown as VisorSceneNodeExtended; } diff --git a/src/ansys/visor/visor-client/src/jest-tests/AggregateVariableInfo.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/AggregateVariableInfo.test.tsx index de557d8a..4d2ac383 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/AggregateVariableInfo.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/AggregateVariableInfo.test.tsx @@ -24,7 +24,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: variable.id, variableComponent: 0, - spectra: [variable], + variables: [variable], }), ]); @@ -46,7 +46,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: variable.id, variableComponent: 0, - spectra: [variable], + variables: [variable], }), ]); @@ -71,7 +71,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: variable.id, variableComponent: 1, - spectra: [variable], + variables: [variable], }), ]); @@ -99,12 +99,12 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: firstVariable.id, variableComponent: 1, - spectra: [firstVariable], + variables: [firstVariable], }), createActorNode({ variableId: secondVariable.id, variableComponent: 1, - spectra: [secondVariable], + variables: [secondVariable], }), ]); @@ -131,12 +131,12 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: firstVariable.id, variableComponent: 0, - spectra: [firstVariable], + variables: [firstVariable], }), createActorNode({ variableId: secondVariable.id, variableComponent: 1, - spectra: [secondVariable], + variables: [secondVariable], }), ]); @@ -157,7 +157,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: variable.id, variableComponent: 999, - spectra: [variable], + variables: [variable], }), ]); @@ -196,12 +196,12 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: firstVariable.id, variableComponent: 1, - spectra: [firstVariable], + variables: [firstVariable], }), createActorNode({ variableId: secondVariable.id, variableComponent: 1, - spectra: [secondVariable], + variables: [secondVariable], }), ]); @@ -238,12 +238,12 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: firstVariable.id, variableComponent: 0, - spectra: [firstVariable], + variables: [firstVariable], }), createActorNode({ variableId: secondVariable.id, variableComponent: 0, - spectra: [secondVariable], + variables: [secondVariable], }), ]); @@ -274,7 +274,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: variable.id, variableComponent: 999, - spectra: [variable], + variables: [variable], }), ]); @@ -296,7 +296,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: variable.id, variableComponent: 0, - spectra: [variable], + variables: [variable], }), ]); @@ -319,7 +319,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: variable.id, variableComponent: 0, - spectra: [variable], + variables: [variable], }), ]); @@ -341,7 +341,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: variable.id, variableComponent: 0, - spectra: [variable], + variables: [variable], }), ]); @@ -364,7 +364,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: variable.id, variableComponent: 0, - spectra: [variable], + variables: [variable], }), ]); @@ -386,7 +386,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: variable.id, variableComponent: 0, - spectra: [variable], + variables: [variable], }), ]); @@ -408,7 +408,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: variable.id, variableComponent: 0, - spectra: [variable], + variables: [variable], }), ]); @@ -434,7 +434,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: variable.id, variableComponent: 0, - spectra: [variable], + variables: [variable], }), ]); @@ -464,7 +464,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: velocity.id, variableComponent: 0, - spectra: [velocity, acceleration], + variables: [velocity, acceleration], }), ]); @@ -492,7 +492,7 @@ describe('AggregateVariableInfo', () => { createActorNode({ variableId: velocity.id, variableComponent: 0, - spectra: [velocity, acceleration], + variables: [velocity, acceleration], }), ]); @@ -526,7 +526,7 @@ interface CreateVariableOptions { interface CreateActorNodeOptions { variableId: string | null; variableComponent: number; - spectra: VisorVariableInfo[]; + variables: VisorVariableInfo[]; } async function createSelection( @@ -602,8 +602,8 @@ function createVariable({ }; } -function createVariableCollection(spectra: VisorVariableInfo[]): VisorVariableCollection { - const array = [...spectra]; +function createVariableCollection(variables: VisorVariableInfo[]): VisorVariableCollection { + const array = [...variables]; const map = new Map(array.map((variable) => [variable.id, variable])); return { @@ -622,7 +622,7 @@ function createVariableCollection(spectra: VisorVariableInfo[]): VisorVariableCo function createActorNode({ variableId, variableComponent, - spectra, + variables, }: CreateActorNodeOptions): VisorSceneNodeExtended { return { name: 'mesh', @@ -630,7 +630,7 @@ function createActorNode({ variableId, variableComponent, customDiffuseColorHex: '#ffffff', - variableCollection: createVariableCollection(spectra), + variableCollection: createVariableCollection(variables), } as unknown as VisorSceneNodeExtended; } diff --git a/src/ansys/visor/visor-client/src/jest-tests/VisorVariableManager.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/VisorVariableManager.test.tsx index 5c516c79..7630d4ad 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/VisorVariableManager.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/VisorVariableManager.test.tsx @@ -196,7 +196,7 @@ describe('getVariableManager', () => { }).toThrow('Do we support label info for data arrays with 5 component(s)?'); }); - test('returns spectra in input order', () => { + test('returns variables in input order', () => { const manager = getVariableManager(); const collection = manager.addDataArrayMetadata([ @@ -211,7 +211,7 @@ describe('getVariableManager', () => { expect(collection.array.map((item) => item.name)).toEqual(['first', 'second']); }); - test('creates separate spectra for different names', () => { + test('creates separate variables for different names', () => { const manager = getVariableManager(); const collection = manager.addDataArrayMetadata([ @@ -227,7 +227,7 @@ describe('getVariableManager', () => { expect(collection.array[0]).not.toBe(collection.array[1]); }); - test('creates separate spectra for different types', () => { + test('creates separate variables for different types', () => { const manager = getVariableManager(); const collection = manager.addDataArrayMetadata([ @@ -244,7 +244,7 @@ describe('getVariableManager', () => { expect(collection.array[1].id).toBe('CELL::displacement::3'); }); - test('creates separate spectra for different component counts', () => { + test('creates separate variables for different component counts', () => { const manager = getVariableManager(); const collection = manager.addDataArrayMetadata([ @@ -521,7 +521,7 @@ describe('getVariableManager', () => { }); describe('global variable collection', () => { - test('contains spectra added before finishing', () => { + test('contains variables added before finishing', () => { const manager = getVariableManager(); manager.addDataArrayMetadata([ diff --git a/src/ansys/visor/visor-client/src/state/VisorVariableManager.tsx b/src/ansys/visor/visor-client/src/state/VisorVariableManager.tsx index db677c3c..0c973f20 100644 --- a/src/ansys/visor/visor-client/src/state/VisorVariableManager.tsx +++ b/src/ansys/visor/visor-client/src/state/VisorVariableManager.tsx @@ -80,10 +80,10 @@ export type VisorVariableInfo = Readonly<{ }>; /** - * A collection of spectra associated with a group of data arrays. + * A collection of variables associated with a group of data arrays. */ export type VisorVariableCollection = Readonly<{ - /** Spectra in collection order. */ + /** Variables in collection order. */ array: VisorVariableInfo[]; /** @@ -125,7 +125,7 @@ export type VisorVariableManager = Readonly<{ finishAddingDataArrayMetadata: () => void; /** - * Finalized collection of all globally registered spectra. + * Finalized collection of all globally registered variables. * * @throws Error if * {@link VisorVariableManager.finishAddingDataArrayMetadata} has not yet @@ -137,7 +137,7 @@ export type VisorVariableManager = Readonly<{ /** * Creates a variable manager for aggregating metadata from VTK data arrays. * - * @returns A new variable manager with no registered spectra. + * @returns A new variable manager with no registered variables. */ export function getVariableManager(): VisorVariableManager { /** Tracks variable IDs and their assigned lookup positions. */ @@ -374,7 +374,7 @@ export function getVariableManager(): VisorVariableManager { * Registers a group of data arrays and creates its local variable collection. * * @param dataArrays - Data arrays to register. - * @returns An immutable collection containing spectra for the supplied arrays. + * @returns An immutable collection containing variables for the supplied arrays. */ function addDataArrayMetadata(dataArrays: VisorVtkDataArray[]): VisorVariableCollection { const array: VisorVariableInfo[] = []; From 9fa3ad41b7cbf770fd8a805b5e52cc2c3fa8f26d Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Thu, 1 Oct 2026 10:14:39 -0700 Subject: [PATCH 5/5] cross section plane authority moves to server --- doc/changelog.d/139.added.md | 1 + src/ansys/visor/viewer/app/trame/local_app.py | 26 ++ src/ansys/visor/viewer/app/visor_vtk.py | 3 - .../runtime/requests/widget_state_payloads.py | 20 +- src/ansys/visor/viewer/renderer/base.py | 21 ++ .../visor/viewer/renderer/local_renderer.py | 78 ++++- .../visor/viewer/renderer/null_renderer.py | 37 +- src/ansys/visor/viewer/vtk/scene/base.py | 56 +++ .../visor/viewer/vtk/scene/local_scene.py | 4 +- .../viewer/vtk/widgets/visor_cross_section.py | 6 + .../src/jest-tests/CrossSectionWidget.test.ts | 100 ++++++ .../WasmRendererWidgetTriggers.test.tsx | 53 ++- .../visor-client/src/renderer/WasmRenderer.ts | 34 ++ .../src/widgets/crossSectionWidget.ts | 22 ++ ...test_local_app_sync_cross_section_plane.py | 137 ++++++++ tests/unit/app/test_visor_vtk_local.py | 35 +- tests/unit/renderer/test_local_renderer.py | 260 ++++++++++++++ tests/unit/vtk/scene/test_base.py | 328 +++++++++++++++++- .../test_visor_cross_section_widget.py | 12 + 19 files changed, 1205 insertions(+), 28 deletions(-) create mode 100644 doc/changelog.d/139.added.md create mode 100644 src/ansys/visor/visor-client/src/jest-tests/CrossSectionWidget.test.ts create mode 100644 tests/unit/app/test_local_app_sync_cross_section_plane.py diff --git a/doc/changelog.d/139.added.md b/doc/changelog.d/139.added.md new file mode 100644 index 00000000..72c6ffc1 --- /dev/null +++ b/doc/changelog.d/139.added.md @@ -0,0 +1 @@ +[Remote rendering 3.3c] server-authoritative cross-section plane diff --git a/src/ansys/visor/viewer/app/trame/local_app.py b/src/ansys/visor/viewer/app/trame/local_app.py index 4f2c25be..f702b1ab 100644 --- a/src/ansys/visor/viewer/app/trame/local_app.py +++ b/src/ansys/visor/viewer/app/trame/local_app.py @@ -18,6 +18,7 @@ SetCrossSectionVisibilityPayload, SetEdgesVisiblePayload, SetProjectionPayload, + SyncCrossSectionPlanePayload, ) logger = VisorDefaultLogger(__name__) @@ -71,6 +72,10 @@ def set_bounding_box_visibility(self, visible: bool) -> None: ... def set_projection(self, parallel: bool) -> None: ... + def sync_cross_section_plane( + self, origin: List[float], normal: List[float] + ) -> None: ... + # ---------------------------------------------------------------------- # Trigger payload models @@ -203,6 +208,7 @@ class LocalApp: set_edges_visible: shows or hides edges on every part set_bounding_box_visibility: shows or hides the bounding-box outline set_projection: sets parallel or perspective projection on the camera record + sync_cross_section_plane: records a settled cross-section plane reported by the frontend set_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. @@ -541,6 +547,26 @@ def set_projection(self, payload) -> None: return api.set_projection(payload.parallel) + @trigger("sync_cross_section_plane") + @parse_payload(SyncCrossSectionPlanePayload) + def sync_cross_section_plane(self, payload) -> None: + """Frontend -> Backend: a settled drag reports the cross-section plane. + + Not a toggle: it carries the origin and the normal the widget settled + on, both required and both exactly three components. A half-plane -- + a missing vector, or one of the wrong length -- fails validation and + is a logged no-op at this boundary rather than a half-applied plane, + which is ``sync_camera``'s stated posture for the same reason. + + The coordinator writes the record and both server VTK objects and + re-serialises them in one critical section; nothing is pushed from + here. + """ + api = self._mutation_api("sync_cross_section_plane", payload) + if api is None: + return + api.sync_cross_section_plane(payload.origin, payload.normal) + 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.py b/src/ansys/visor/viewer/app/visor_vtk.py index 8e155127..2d8442db 100644 --- a/src/ansys/visor/viewer/app/visor_vtk.py +++ b/src/ansys/visor/viewer/app/visor_vtk.py @@ -459,9 +459,6 @@ def _load_datasets_from_state(self, state: "PersistedViewerStateV1") -> None: if dataset is not None: dataset.mark_clean() - if self._scene.dataset_count > 0: - self._scene.finalize_scene(skip_reset_camera=True) - """ Protected Methods (async start and stop for FastAPI) """ @require_server_off @validate_input_metadata_types diff --git a/src/ansys/visor/viewer/models/runtime/requests/widget_state_payloads.py b/src/ansys/visor/viewer/models/runtime/requests/widget_state_payloads.py index 3101e16b..610f1b98 100644 --- a/src/ansys/visor/viewer/models/runtime/requests/widget_state_payloads.py +++ b/src/ansys/visor/viewer/models/runtime/requests/widget_state_payloads.py @@ -12,9 +12,17 @@ per-part triggers carrying camelCase aliases. These are neither. The precedent is ``sync_camera_payload.py``, the one existing non-per-part trigger, whose model lives in this package. + +``SyncCrossSectionPlanePayload`` is not a toggle either. It is the plane +the client's drag settled on, reported at end of interaction, and it +carries two three-component vectors rather than a boolean. It lives here +rather than beside the per-part models for the same reason the four above +do: it is scene-wide and carries no ``nodeId``. """ -from pydantic import BaseModel, ConfigDict +from typing import List + +from pydantic import BaseModel, ConfigDict, Field class SetCrossSectionVisibilityPayload(BaseModel): @@ -47,3 +55,13 @@ class SetProjectionPayload(BaseModel): model_config = ConfigDict(populate_by_name=True) parallel: bool + + +class SyncCrossSectionPlanePayload(BaseModel): + """Payload of the ``sync_cross_section_plane`` trigger.""" + + model_config = ConfigDict(populate_by_name=True) + + origin: List[float] = Field(min_length=3, max_length=3) + normal: List[float] = Field(min_length=3, max_length=3) + diff --git a/src/ansys/visor/viewer/renderer/base.py b/src/ansys/visor/viewer/renderer/base.py index 86346cad..27d7096f 100644 --- a/src/ansys/visor/viewer/renderer/base.py +++ b/src/ansys/visor/viewer/renderer/base.py @@ -23,6 +23,9 @@ from vtkmodules.vtkCommonDataModel import vtkDataObject 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.runtime.vtk.renderer_annotation import RendererAnnotation from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraphPartNode @@ -254,6 +257,24 @@ def sync_cross_section_plane( to server-side VTK objects. """ + @abstractmethod + def get_cross_section_plane(self) -> "VisorCrossSectionState | None": + """ + Return the cross-section plane record, or ``None`` if nothing has + written one yet. + """ + + @abstractmethod + def serialize_cross_section_state(self) -> None: + """Make the state served to the client current for the plane. + + Names its ids explicitly and never relies on a render following. + + **Serialize only; do not notify.** + + No-op on a renderer that serves the client no VTK object state. + """ + @abstractmethod def set_bounding_box_visibility(self, visible: bool) -> None: """Show or hide the bounding-box outline.""" diff --git a/src/ansys/visor/viewer/renderer/local_renderer.py b/src/ansys/visor/viewer/renderer/local_renderer.py index 522703a8..07986440 100644 --- a/src/ansys/visor/viewer/renderer/local_renderer.py +++ b/src/ansys/visor/viewer/renderer/local_renderer.py @@ -29,6 +29,7 @@ from ansys.visor.viewer.core.visor_colors import VisorColors from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger 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.runtime.vtk.renderer_annotation import ( WasmNodeHandles, WasmRendererAnnotation, @@ -62,12 +63,14 @@ class VisorLocalRenderer(IRenderer): # Increment I5; mutated exclusively via register_node / deregister_node. _pipelines: dict[int, VtkNodePipeline] _last_camera_state: Optional["VisorCameraState"] + _last_cross_section_state: Optional["VisorCrossSectionState"] def __init__(self, server: Server): """Build the full local-mode VTK infrastructure and seed wasm state.""" self._server = server self._pipelines = {} self._last_camera_state = None + self._last_cross_section_state = None self._vtk_renderer = self._initialize_vtk_renderer() self._render_window = self._initialize_render_window() @@ -401,7 +404,47 @@ def set_cross_section_visibility(self, visible: bool) -> None: def sync_cross_section_plane( self, origin: list[float], normal: list[float] ) -> None: - """No-op in Story 1.2. Phase 3 populates.""" + """See :meth:`IRenderer.sync_cross_section_plane`. + + Writes the record, then both VTK objects. ``set_origin`` and + ``set_normal`` each write the plane *and* the representation, so the + clip function and the draggable handle cannot diverge on this path -- + which is the defect the client's own set/get asymmetry has. + """ + self._last_cross_section_state = VisorCrossSectionState( + origin=list(origin), normal=list(normal) + ) + self._cross_section_widget.set_origin(origin) + self._cross_section_widget.set_normal(normal) + + def get_cross_section_plane(self) -> Optional["VisorCrossSectionState"]: + """See :meth:`IRenderer.get_cross_section_plane`.""" + return self._last_cross_section_state + + def serialize_cross_section_state(self) -> None: + """See :meth:`IRenderer.serialize_cross_section_state`. + + Two ids, named one at a time, each derived the way + :meth:`serialize_camera_state` derives the camera's: ``GetId`` on the + object itself, not the id ``register_vtk_object`` returned. Deriving + it this way means the equivalence of those two id spaces never has to + be established. + + The plane is the clip function every pipeline holds; the + representation is the visible handle and is also what the client + reads on save. A plane-only re-serialise leaves the client saving a + stale handle. + + The widget id is deliberately not named: ``set_origin`` and + ``set_normal`` do not write the widget, whose changing state is + enablement. See MC-6. + """ + self._object_manager.UpdateStateFromObject( + self._object_manager.GetId(self._cross_section_widget.plane) + ) + self._object_manager.UpdateStateFromObject( + self._object_manager.GetId(self._cross_section_widget.plane_representation) + ) def set_bounding_box_visibility(self, visible: bool) -> None: """No-op in Story 1.2. Phase 3 populates.""" @@ -411,9 +454,40 @@ def set_bounding_box_visibility(self, visible: bool) -> None: # ------------------------------------------------------------------ def update_bounds(self, bounds: list[float]) -> None: - """See :meth:`IRenderer.update_bounds`.""" + """See :meth:`IRenderer.update_bounds`. + + Seeds the cross-section record from the values the widget's own + ``update_bounds`` just wrote through ``set_origin``/``set_normal``, so + the record is never ``None`` once a scene has been populated and + ``get_state`` can assign it unconditionally, as it does the camera's. + + **The seed runs only when there is no record.** This method is not + reached once per scene: ``set_part_visibility`` fans out to + ``_update_widget_bounds``, so a part toggle or a dataset add reaches + here too. Seeding unconditionally would therefore discard a plane the + user had dragged, on the next toggle, and the widget's own + ``update_bounds`` would already have overwritten both VTK objects with + the default plane. With a record present this writes the record + *back* to the widget instead -- after the widget's own call, never + before -- and leaves the record itself untouched. + + Ordering is load-bearing in both branches. Seeded before the widget + call, the record holds the previous plane and is one populate behind; + written back before it, the widget's defaults win and the drag is + lost. Either compiles, and either passes any test that does not + assert the order. + """ self._cross_section_widget.update_bounds(bounds) self._bounding_box_widget.update_bounds(bounds) + if self._last_cross_section_state is None: + plane = self._cross_section_widget.plane + self._last_cross_section_state = VisorCrossSectionState( + origin=list(plane.GetOrigin()), + normal=list(plane.GetNormal()), + ) + else: + self._cross_section_widget.set_origin(self._last_cross_section_state.origin) + self._cross_section_widget.set_normal(self._last_cross_section_state.normal) def update_actor_count(self, count: int) -> None: """See :meth:`IRenderer.update_actor_count`.""" diff --git a/src/ansys/visor/viewer/renderer/null_renderer.py b/src/ansys/visor/viewer/renderer/null_renderer.py index 5a3f3463..20eaeba7 100644 --- a/src/ansys/visor/viewer/renderer/null_renderer.py +++ b/src/ansys/visor/viewer/renderer/null_renderer.py @@ -8,10 +8,11 @@ annotated return the simplest valid empty value for that type; all others are ``pass``. No VTK imports, no local view, no side effects. -One exception to "no state": the camera record, ``_last_camera_state``. The -record half of the :class:`IRenderer` camera contract is not optional on any -implementation -- only the projection half is, and here it is a no-op because -there is no pipeline camera to project onto. +One exception to "no state": the camera record, ``_last_camera_state``, and +the cross-section plane record, ``_last_cross_section_state``. The record +half of the :class:`IRenderer` camera and plane contracts is not optional on +any implementation -- only the projection half is, and here it is a no-op +because there is no pipeline camera and no widget to project onto. """ from __future__ import annotations @@ -19,6 +20,7 @@ from typing import TYPE_CHECKING, Optional from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger +from ansys.visor.viewer.models.common.visor_cross_section_state import VisorCrossSectionState from ansys.visor.viewer.renderer.base import IRenderer if TYPE_CHECKING: @@ -34,10 +36,12 @@ class NullRenderer(IRenderer): """Null-object implementation of :class:`IRenderer` for use in tests.""" _last_camera_state: Optional["VisorCameraState"] + _last_cross_section_state: Optional["VisorCrossSectionState"] def __init__(self) -> None: - """Initialize the camera record.""" + """Initialize the camera and cross-section records.""" self._last_camera_state = None + self._last_cross_section_state = None # ------------------------------------------------------------------ # Wire contract @@ -159,7 +163,28 @@ def set_cross_section_visibility(self, visible: bool) -> None: def sync_cross_section_plane( self, origin: list[float], normal: list[float] ) -> None: - pass + """See :meth:`IRenderer.sync_cross_section_plane`. + + Record only: there are no VTK widget objects to project onto. The + record takes a copy of each list, as :class:`VisorLocalRenderer` does. + """ + self._last_cross_section_state = VisorCrossSectionState( + origin=list(origin), normal=list(normal) + ) + + def get_cross_section_plane(self) -> "VisorCrossSectionState | None": + """See :meth:`IRenderer.get_cross_section_plane`. + + ``None`` unless :meth:`sync_cross_section_plane` wrote one: there is + no widget here for :meth:`update_bounds` to seed a record from. + """ + return self._last_cross_section_state + + def serialize_cross_section_state(self) -> None: + """See :meth:`IRenderer.serialize_cross_section_state`. + + No-op: this renderer serves the client no VTK object state. + """ def set_bounding_box_visibility(self, visible: bool) -> None: pass diff --git a/src/ansys/visor/viewer/vtk/scene/base.py b/src/ansys/visor/viewer/vtk/scene/base.py index 9c53aa13..ca222060 100644 --- a/src/ansys/visor/viewer/vtk/scene/base.py +++ b/src/ansys/visor/viewer/vtk/scene/base.py @@ -190,6 +190,13 @@ async def get_state(self, timeout: float) -> PersistedViewerStateV1: ``orthographic_enabled`` is derived from that same record, not stored separately, so it can't disagree with the camera. ``None`` means "nothing was ever written." + + The cross-section plane is the camera's twin, taken from the + renderer's record on exactly the same terms -- see the camera + paragraph above for why the assignment is unconditional and what + ``None`` means. In normal operation there is no ``None`` case to + guard because the renderer seeds the record from its own widget the + first time bounds are pushed. """ runtime_state = await self._get_runtime_state_async(timeout) @@ -203,6 +210,7 @@ async def get_state(self, timeout: float) -> PersistedViewerStateV1: runtime_state.scene.orthographic_enabled = ( camera_record.parallel_projection if camera_record is not None else None ) + runtime_state.scene.cross_section = self._renderer.get_cross_section_plane() runtime_state.scene.cross_section_enabled = self._cross_section_enabled runtime_state.scene.edges_enabled = self._edges_enabled runtime_state.scene.bounding_box_enabled = self._bounding_box_enabled @@ -237,6 +245,25 @@ def apply_state(self, state: PersistedViewerStateV1): self._restore_camera_state(runtime_app_state) # TODO: restore UI state and variable states when they are synced back to the server. + # Finalize here, not on the load path. On a cold load -- viewer started with no dataset, + # then a state loaded -- load_state adds the datasets and only then calls apply_state, so a + # finalize on the load path syncs wasm from a scene that predates every restore above: the + # client was served a scene with no cross-section plane, and neither the plane nor its + # handle ever rendered. Finalizing here syncs after the restores. + # + # Before _push_runtime_state, never after. finalize_scene -> render() ends in + # LocalView.update(), the same wasm flush flush_wasm_state performs; placed after the push + # it *is* the flush the note below refuses, racing the client's rebuild against the + # fire-and-forget set_state. See VisorLocalScene._push_runtime_state, which is written on + # the assumption that the full render and wasm sync have already happened by the time it runs. + # + # Correct only after _restore_widget_state: populate_scene reaches IRenderer.update_bounds, + # which writes an existing cross-section record back to the widget rather than seeding from + # the widget's defaults. With no record -- which is what the load path had on a cold load -- + # that branch seeds the defaults instead. + if self.dataset_count > 0: + self.finalize_scene(skip_reset_camera=True) + # The server's copy is now current; deliver it to the rendering backend. # wasm: set_state() to the browser; RCA: a rendered frame; headless: no-op. self._push_runtime_state(runtime_app_state) @@ -493,6 +520,22 @@ def sync_camera(self, camera_state: VisorCameraState) -> None: self._renderer.sync_camera(camera_state) self._renderer.serialize_camera_state() + def sync_cross_section_plane( + self, origin: list[float], normal: list[float] + ) -> None: + """Record a cross-section plane the frontend reported, and project it. + + Both halves in one critical section, re-serialisation part of the + write, no notify -- for the reasons :meth:`sync_camera` gives. The + plane has exactly one delivery channel to a rebuilt or reconnecting + client, the wasm state fetch that follows this re-serialisation, so an + id that is not re-serialised here is simply not delivered and the + user's drag reappears where it started after a page reload. + """ + with self._vtk_lock: + self._renderer.sync_cross_section_plane(origin, normal) + self._renderer.serialize_cross_section_state() + def pick_geometry(self, actor_wasm_id, cell_id, mode, world_x, world_y, world_z) -> dict: """ Frontend-trigger entry point for cell picking. Packs the world-space @@ -768,6 +811,14 @@ def _restore_widget_state(self, runtime_app_state: "RuntimeAppState") -> None: compatibility and ignored here, because two readers of one property is the divergence this story removed. + The cross-section plane is restored here too, and both-or-neither: + ``sync_cross_section_plane`` takes an origin and a normal together + and the model allows either to be absent, so a half-plane says + nothing rather than half-applying. The re-serialisation follows the + write for the reason :meth:`sync_cross_section_plane` gives -- a + loaded plane that is written but not re-serialised leaves the client + fetching the pre-load one. + Callers must hold ``_vtk_lock``. """ scene = runtime_app_state.scene @@ -780,6 +831,11 @@ def _restore_widget_state(self, runtime_app_state: "RuntimeAppState") -> None: if scene.bounding_box_enabled is not None: self._bounding_box_enabled = scene.bounding_box_enabled self._renderer.set_bounding_box_visibility(scene.bounding_box_enabled) + if scene.cross_section is not None: + cs = scene.cross_section + if cs.origin is not None and cs.normal is not None: + self._renderer.sync_cross_section_plane(cs.origin, cs.normal) + self._renderer.serialize_cross_section_state() def _restore_one_part_state( self, diff --git a/src/ansys/visor/viewer/vtk/scene/local_scene.py b/src/ansys/visor/viewer/vtk/scene/local_scene.py index 42faf6da..1970e332 100644 --- a/src/ansys/visor/viewer/vtk/scene/local_scene.py +++ b/src/ansys/visor/viewer/vtk/scene/local_scene.py @@ -59,8 +59,8 @@ def _push_runtime_state(self, runtime_app_state: "RuntimeAppState") -> None: the React frontend, which clears the setState event listener while it rebuilds the scene. If the set_state() JS call below arrives during that window the persisted state is silently lost. - finalize_scene() (called just before apply_state when loading from an - empty scene) has already done a full render + wasm sync; all we need + finalize_scene() (called from apply_state, in the statement immediately + before this one) has already done a full render + wasm sync; all we need here is a lightweight VTK flush before the JS payload is sent. """ self._renderer.render_window_only() diff --git a/src/ansys/visor/viewer/vtk/widgets/visor_cross_section.py b/src/ansys/visor/viewer/vtk/widgets/visor_cross_section.py index 9a28f450..209ad39f 100644 --- a/src/ansys/visor/viewer/vtk/widgets/visor_cross_section.py +++ b/src/ansys/visor/viewer/vtk/widgets/visor_cross_section.py @@ -40,6 +40,7 @@ class VisorCrossSectionWidget: Properties: plane: Returns the VTK plane object. + plane_representation: Returns the VTK plane representation (the draggable handle). algorithm_filter: Returns the VTK algorithm filter. plane_wasm_id: Returns the WebAssembly ID for the plane object. plane_widget_wasm_id: Returns the WebAssembly ID for the plane widget. @@ -121,6 +122,11 @@ def plane(self) -> vtkPlane: """Returns the widget VTK plane object.""" return self._plane + @property + def plane_representation(self) -> vtkImplicitPlaneRepresentation: + """The draggable handle's representation.""" + return self._plane_representation + @property def algorithm_filter(self): """Returns the VTK algorithm filter.""" diff --git a/src/ansys/visor/visor-client/src/jest-tests/CrossSectionWidget.test.ts b/src/ansys/visor/visor-client/src/jest-tests/CrossSectionWidget.test.ts new file mode 100644 index 00000000..5e74e078 --- /dev/null +++ b/src/ansys/visor/visor-client/src/jest-tests/CrossSectionWidget.test.ts @@ -0,0 +1,100 @@ +import { CrossSectionWidget } from '../widgets/crossSectionWidget'; +import type VtkScene from '../wasm/VtkScene'; + +/** + * The client cross-section widget's **set** path. + * + * The widget holds two wasm objects: the plane, which is the clip function + * every pipeline holds, and the representation, which is the draggable + * handle. The read path deliberately reads the representation, because that + * is what the handle moved. The write path used to write only the plane, and + * that asymmetry is the defect pinned here: a load set the clip and left the + * handle where it was, and the next end-of-drag report then carried the stale + * handle to the server, so the server-authoritative plane would have been + * *given* the wrong value rather than merely displaying one. + * + * Two tests, one per setter, because a fix applied to one of them only is a + * shippable defect in its own right -- the clip would follow a dragged origin + * and not a dragged normal -- and the two fail independently. + * + * Every value below is a hand-written literal, restated in the expectation + * rather than read back off the double. Nothing here is derived from VTK, and + * the doubles for the plane and the representation are separate objects, which + * is the whole point: a single shared double could not tell the two writes + * apart. + */ + +const PLANE_ID = 202; +const PLANE_WIDGET_ID = 203; +const PLANE_REPRESENTATION_ID = 204; + +function makeWidget() { + const plane = { + SetOrigin: jest.fn(async () => undefined), + SetNormal: jest.fn(async () => undefined), + GetOrigin: jest.fn(async () => [0, 0, 0]), + GetNormal: jest.fn(async () => [0, 0, 1]), + }; + const rep = { + SetOrigin: jest.fn(async () => undefined), + SetNormal: jest.fn(async () => undefined), + GetOrigin: jest.fn(async () => [0, 0, 0]), + GetNormal: jest.fn(async () => [0, 0, 1]), + }; + const planeWidget = { + On: jest.fn(async () => undefined), + Off: jest.fn(async () => undefined), + observe: jest.fn(), + }; + const scene = { + render: jest.fn(), + getVtkObject: (wasmId: number) => { + switch (wasmId) { + case PLANE_ID: + return plane; + case PLANE_REPRESENTATION_ID: + return rep; + default: + return planeWidget; + } + }, + }; + const widget = new CrossSectionWidget(scene as unknown as VtkScene, { + planeWasmId: PLANE_ID, + representationWasmId: PLANE_REPRESENTATION_ID, + widgetWasmId: PLANE_WIDGET_ID, + }); + return { widget, plane, rep }; +} + +describe('CrossSectionWidget writes both plane objects on the set path', () => { + test('setOriginAsync writes the representation and then the plane', async () => { + const { widget, plane, rep } = makeWidget(); + + await widget.setOriginAsync([1, 2, 3]); + + expect(rep.SetOrigin).toHaveBeenCalledTimes(1); + expect(rep.SetOrigin).toHaveBeenCalledWith([1, 2, 3]); + expect(plane.SetOrigin).toHaveBeenCalledTimes(1); + expect(plane.SetOrigin).toHaveBeenCalledWith([1, 2, 3]); + // Representation first, plane second, matching the order the server's + // own set_origin writes them in. + expect(rep.SetOrigin.mock.invocationCallOrder[0]).toBeLessThan( + plane.SetOrigin.mock.invocationCallOrder[0] + ); + }); + + test('setNormalAsync writes the representation and then the plane', async () => { + const { widget, plane, rep } = makeWidget(); + + await widget.setNormalAsync([0, 1, 0]); + + expect(rep.SetNormal).toHaveBeenCalledTimes(1); + expect(rep.SetNormal).toHaveBeenCalledWith([0, 1, 0]); + expect(plane.SetNormal).toHaveBeenCalledTimes(1); + expect(plane.SetNormal).toHaveBeenCalledWith([0, 1, 0]); + expect(rep.SetNormal.mock.invocationCallOrder[0]).toBeLessThan( + plane.SetNormal.mock.invocationCallOrder[0] + ); + }); +}); diff --git a/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx index 4c87d189..0ce6a698 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx @@ -94,8 +94,12 @@ function makeFakeWasmObjects() { GetVisibility: jest.fn(async () => 0), SetVisibility: jest.fn(async () => undefined), SetBounds: jest.fn(async () => undefined), - GetOrigin: jest.fn(async () => [0, 0, 0]), - GetNormal: jest.fn(async () => [0, 0, 1]), + // Distinctive hand-written literals. They stand in for the + // *representation's* origin and normal, and they are deliberately + // nothing any default would produce, so a payload carrying them can + // only have come from reading the representation back. + GetOrigin: jest.fn(async () => [1.5, -2.5, 3.5]), + GetNormal: jest.fn(async () => [0, 1, 0]), SetOrigin: jest.fn(async () => undefined), SetNormal: jest.fn(async () => undefined), }; @@ -287,3 +291,48 @@ describe('WasmRenderer.createAsync seeds the orthographic flag from the wasm cam expect(renderer.isOrthographicEnabled()).toBe(false); }); }); + +describe('WasmRenderer reports the cross-section plane on the end-of-drag event', () => { + // The event itself does not exist under jsdom, so what is pinned here is + // the *registration*: which event the report is bound to, and what the + // callback bound to it sends. Whether that event ever fires is MC-5's + // subject and no gate reaches it. + test('the plane report is registered on EndInteractionEvent and sends the representation plane once', async () => { + const sender = makeSender(); + const { renderer, widget } = await makeRenderer(sender); + + const endCalls = widget.observe.mock.calls.filter( + (call) => call[0] === 'EndInteractionEvent' + ); + expect(endCalls).toHaveLength(1); + + await endCalls[0][1](); + + expect(sender).toHaveBeenCalledTimes(1); + expect(sender).toHaveBeenCalledWith('sync_cross_section_plane', { + origin: [1.5, -2.5, 3.5], + normal: [0, 1, 0], + }); + // `renderer` is held so the construction that registered the observer + // is not mistaken for dead code by a future reader. + expect(renderer.isCrossSectionVisible()).toBe(false); + }); + + test('no callback registered on InteractionEvent sends anything', async () => { + // InteractionEvent fires many times across one drag. A report bound + // to it would satisfy every other assertion in this module and flood + // the trigger channel, which is a fault no other gate can see. + const sender = makeSender(); + const { widget } = await makeRenderer(sender); + + const interactionCalls = widget.observe.mock.calls.filter( + (call) => call[0] === 'InteractionEvent' + ); + expect(interactionCalls.length).toBeGreaterThan(0); + for (const call of interactionCalls) { + await call[1](); + } + + expect(sender).not.toHaveBeenCalled(); + }); +}); diff --git a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts index e2522615..db877e19 100644 --- a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts +++ b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts @@ -62,6 +62,40 @@ export class WasmRenderer implements IRenderer { const planeWidget = vtkScene.getVtkObject(wasmPlaneWidgetId); planeWidget.observe('InteractionEvent', this.#crossSectionWidget.interactionHandler); + /** + * Report the settled plane to the server, once per handle release. + * + * `EndInteractionEvent`, not `InteractionEvent`: the per-motion event + * fires many times across one drag, and a report bound to it passes + * every other gate and floods the trigger channel. The end event + * fires once when the handle is released, and not on a camera orbit. + * + * A bare click on the handle with no drag also releases it, so it + * also reports -- carrying the plane the server already holds. That + * arrival is accepted as idempotent and is deliberately **not** + * suppressed: there is no last-sent cache and no debounce here, so a + * drag that produced no arrival means the observer is not wired, + * rather than meaning a filter swallowed it. + * + * The values come from `getOriginAsync` / `getNormalAsync`, which read + * the **representation** -- the object the handle actually moved, and + * the one whose values the client would otherwise save. The payload + * keys are `origin` and `normal`, snake_case and unaliased, three + * floats each, which is what the server's payload model requires. + * + * Deliberately not defensive: `#sendWidgetTriggerAsync` already + * swallows and logs a send rejection, and a `catch` around the two + * reads would turn a broken representation read into a silent + * no-report, which is the one outcome that reads as "the observer was + * never wired". + */ + planeWidget.observe('EndInteractionEvent', async () => { + await this.#sendWidgetTriggerAsync('sync_cross_section_plane', { + origin: await this.#crossSectionWidget.getOriginAsync(), + normal: await this.#crossSectionWidget.getNormalAsync(), + }); + }); + // Bounding-box ids are stashed for attachSceneGraph, which is the // point at which the live sceneGraph (needed by BoundingBoxWidget) // becomes available. diff --git a/src/ansys/visor/visor-client/src/widgets/crossSectionWidget.ts b/src/ansys/visor/visor-client/src/widgets/crossSectionWidget.ts index 5774aacd..041b1938 100644 --- a/src/ansys/visor/visor-client/src/widgets/crossSectionWidget.ts +++ b/src/ansys/visor/visor-client/src/widgets/crossSectionWidget.ts @@ -76,10 +76,32 @@ export class CrossSectionWidget { getNormalAsync: () => Promise = async () => { return await this.#rep.GetNormal(); }; + /** + * Write a plane to **both** objects: the representation first, then the + * plane. + * + * The two setters below and the two getters above are deliberately no + * longer symmetric in what they touch. The getters read the + * representation, because that is the object the draggable handle moves. + * The setters write the representation *and* the plane, because the + * representation is the handle and the plane is the clip function every + * pipeline holds, and a write that reached only one of them left the two + * disagreeing: a load set the clip while the handle stayed where it was, + * and the next end-of-drag report then carried that stale handle to the + * server. + * + * Representation first, plane second, matching the order the server's own + * `VisorCrossSectionWidget.set_origin` / `set_normal` write them in. The + * order is consistency with the server rather than a correctness + * requirement of its own -- these are two independent objects and neither + * write feeds the other. + */ setOriginAsync: (origin: number[]) => Promise = async (origin) => { + await this.#rep.SetOrigin(origin); await this.#plane.SetOrigin(origin); }; setNormalAsync: (normal: number[]) => Promise = async (normal) => { + await this.#rep.SetNormal(normal); await this.#plane.SetNormal(normal); }; setVisibilityAsync: (visible?: boolean) => Promise; diff --git a/tests/unit/app/test_local_app_sync_cross_section_plane.py b/tests/unit/app/test_local_app_sync_cross_section_plane.py new file mode 100644 index 00000000..9f9564ab --- /dev/null +++ b/tests/unit/app/test_local_app_sync_cross_section_plane.py @@ -0,0 +1,137 @@ +"""Unit tests for ``LocalApp.sync_cross_section_plane`` -- the plane trigger. + +A module of its own rather than an addition to ``test_local_app.py``, on the +precedent ``test_local_app_sync_camera.py`` set: that module's +``TRIGGER_NAMES`` list drives parametrised tests whose meaning is "one of the +six per-part triggers", and every per-part payload model carries +``node_id: int = Field(alias="nodeId")``. This trigger carries no node id -- +it carries two three-component vectors and is scene-wide -- so adding a name +there would multiply the per-part tests by one more and rewrite them. + +Two tests, not six. The no-coordinator path and the not-a-mapping path run +through ``_mutation_api`` and ``parse_payload``, which three sibling trigger +modules already pin against the same two functions; repeating them here would +be a third and fourth copy of one failure mode rather than a new one. What is +unique to this trigger is that it forwards *two* vectors in a fixed order, and +that its name has to survive the payload decorator. + +Every payload here is a hand-written literal. The wire keys are ``origin`` +and ``normal`` and carry no alias: snake_case and camelCase coincide, which is +itself asserted by the delegation test passing a literal dict. +""" + +from unittest.mock import MagicMock + +import pytest + +from ansys.visor.viewer.app.trame.local_app import LocalApp + +# The trigger name, written out rather than imported, so that a rename on the +# production side fails here by name instead of following along. +PLANE_TRIGGER = "sync_cross_section_plane" + +# Hand-written literals. No component is shared between the origin and the +# normal and neither is a permutation of the other, so a handler that swapped +# the two arguments fails on value rather than coinciding. +REPORTED_ORIGIN = [1.5, 2.5, 3.5] +REPORTED_NORMAL = [0.0, 1.0, 0.0] + + +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 scene coordinator.""" + return MagicMock(name="scene_mutation_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_mutation_api=api, + ) + + +# =========================================================================== +# Delegation +# =========================================================================== + +def test_sync_cross_section_plane_delegates_the_origin_and_normal(app, api): + """The trigger hands the coordinator both vectors, in that order. + + Positional, and asserted positionally, because the coordinator's signature + is ``(origin, normal)`` and a handler that passed them the other way round + would type-check, validate and run -- and would leave the plane at right + angles to where the user dragged it. The two literals share no component, + so the swap fails on value. + """ + result = app.sync_cross_section_plane( + {"origin": REPORTED_ORIGIN, "normal": REPORTED_NORMAL} + ) + + assert result is None + api.sync_cross_section_plane.assert_called_once_with( + REPORTED_ORIGIN, REPORTED_NORMAL + ) + + +# =========================================================================== +# Registration +# =========================================================================== + +def test_sync_cross_section_plane_trigger_name_is_registered_after_decoration( + app, mock_server +): + """The name survives the payload decorator, once, and takes a raw dict. + + ``@trigger`` is outermost above ``@parse_payload``; this asserts the pair + registers under the name rather than under the wrapper, and that the + registered callable accepts the raw dict the client sends. + + Registered exactly once: a second registration under the same name would + leave which handler the client reaches dependent on registration order, + and nothing else in the suite would see it. + """ + 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 + ] + registered = dict(zip(names, functions)) + + assert names.count(PLANE_TRIGGER) == 1 + assert PLANE_TRIGGER in registered + assert registered[PLANE_TRIGGER]( + {"origin": REPORTED_ORIGIN, "normal": REPORTED_NORMAL} + ) is None + diff --git a/tests/unit/app/test_visor_vtk_local.py b/tests/unit/app/test_visor_vtk_local.py index 3a71d91a..4ac73634 100644 --- a/tests/unit/app/test_visor_vtk_local.py +++ b/tests/unit/app/test_visor_vtk_local.py @@ -412,7 +412,19 @@ def _make_state_with_datasets(snapshot_path: str | None, name="model", unit="m") def test_load_state_restores_datasets_when_registry_empty(tmp_path, iface): - """Verify that datasets are restored when no datasets are currently loaded.""" + """Verify that datasets are restored when no datasets are currently loaded. + + ``finalize_scene`` is asserted *not* called: it belongs to ``apply_state``, + which is mocked here, so a call arriving at this level could only have come + from the load path. That is the pin against returning it to + ``_load_datasets_from_state``; its placement *within* ``apply_state`` is + pinned by + tests/unit/vtk/scene/test_base.py::test_apply_state_flushes_before_the_bridge_call. + + ``side_effect`` is a one-element list rather than a return value so that + the number of ``dataset_count`` reads stays pinned: ``load_state`` reads it + once, and a second read raises StopIteration. + """ snapshot = tmp_path / "model_snapshot.vtkhdf" snapshot.touch() state = _make_state_with_datasets(str(snapshot), name="model") @@ -421,7 +433,7 @@ def test_load_state_restores_datasets_when_registry_empty(tmp_path, iface): iface._file_io.read_state = MagicMock(return_value=state) iface._file_io.read_snapshot = MagicMock(return_value=mock_data) iface._file_io.build_metadata_for_load_state = MagicMock(return_value=MagicMock(spec=ExtendedMetadata)) - type(iface._scene).dataset_count = PropertyMock(side_effect=[0, 1]) + type(iface._scene).dataset_count = PropertyMock(side_effect=[0]) loaded_ds = MagicMock() iface._scene.add_dataset = MagicMock(return_value=42) iface._scene.datasets = {42: loaded_ds} @@ -433,7 +445,7 @@ def test_load_state_restores_datasets_when_registry_empty(tmp_path, iface): iface._file_io.read_snapshot.assert_called_once_with(str(snapshot)) iface._scene.add_dataset.assert_called_once_with(mock_data, iface._file_io.build_metadata_for_load_state.return_value) loaded_ds.mark_clean.assert_called_once() - iface._scene.finalize_scene.assert_called_once() + iface._scene.finalize_scene.assert_not_called() iface._scene.apply_state.assert_called_once_with(state) @@ -456,19 +468,28 @@ def test_load_state_skips_dataset_restore_when_registry_not_empty(tmp_path, ifac def test_load_state_skips_missing_snapshot(tmp_path, iface): - """Verify that missing dataset snapshots are ignored during state loading.""" + """Verify that missing dataset snapshots are ignored during state loading. + + This test pins the missing-snapshot skip and nothing else. It previously + also asserted ``finalize_scene`` was not called; that assertion no longer + pins anything, because ``finalize_scene`` is not reachable from + ``load_state`` at all -- it lives in ``apply_state``, which is mocked here + -- so it passed for a reason unrelated to the missing snapshot. It has + been removed rather than kept green. The load path is pinned by + ``test_load_state_restores_datasets_when_registry_empty`` and the placement + by + tests/unit/vtk/scene/test_base.py::test_apply_state_flushes_before_the_bridge_call. + """ state = _make_state_with_datasets("/nonexistent/snap.vtkhdf", name="model") iface._file_io.read_state = MagicMock(return_value=state) iface._file_io.read_snapshot = MagicMock() type(iface._scene).dataset_count = PropertyMock(return_value=0) iface._scene.apply_state = MagicMock() - iface._scene.finalize_scene = MagicMock() iface.load_state(str(tmp_path)) iface._file_io.read_snapshot.assert_not_called() - iface._scene.finalize_scene.assert_not_called() iface._scene.apply_state.assert_called_once_with(state) @@ -507,7 +528,7 @@ def test_load_state_passes_correct_metadata_to_add_dataset(tmp_path, iface): iface._file_io.read_snapshot = MagicMock(return_value=mock_data) iface._file_io.build_metadata_for_load_state = MagicMock(return_value=built_meta) iface._scene.add_dataset = MagicMock(return_value=1) - type(iface._scene).dataset_count = PropertyMock(side_effect=[0, 1]) + type(iface._scene).dataset_count = PropertyMock(side_effect=[0]) iface._scene.datasets = {1: MagicMock()} iface._scene.apply_state = MagicMock() iface._scene.finalize_scene = MagicMock() diff --git a/tests/unit/renderer/test_local_renderer.py b/tests/unit/renderer/test_local_renderer.py index 386f5320..3ec38521 100644 --- a/tests/unit/renderer/test_local_renderer.py +++ b/tests/unit/renderer/test_local_renderer.py @@ -1022,3 +1022,263 @@ def test_pick_unknown_mode_returns_not_found(self, renderer): res = renderer.pick_geometry(1, 0, "INVALID_MODE", (0.0, 0.0, 0.0)) assert res == {"found": False} + +# =========================================================================== +# 7. The cross-section plane record +# +# Hand-written widget double rather than a MagicMock, on the precedent +# ``_CameraDouble`` sets in this module: the seed reads ``GetOrigin`` and +# ``GetNormal`` back into a pydantic model, which rejects Mock attributes, and +# the ordering tests need one shared call list across three widget methods +# rather than three independent recorders. +# +# Every literal below is hand-written. None is computed the way +# ``_get_default_plane_info`` computes it and none originates from VTK, so no +# assertion here can be satisfied by a VTK-constructed default. +# =========================================================================== + +# What the widget's own update_bounds left on the plane, read back by the seed. +SEEDED_PLANE_ORIGIN = [1.0, 2.0, 3.0] +SEEDED_PLANE_NORMAL = [0.0, 0.0, 1.0] + +# What a settled drag reports through sync_cross_section_plane. +REPORTED_PLANE_ORIGIN = [4.0, 5.0, 6.0] +REPORTED_PLANE_NORMAL = [0.0, 1.0, 0.0] + +# What an already-present record holds when update_bounds runs again. Every +# component differs from both sets above, so "wrote the record back" and +# "re-seeded from the widget" cannot both pass. +RECORDED_PLANE_ORIGIN = [7.0, 8.0, 9.0] +RECORDED_PLANE_NORMAL = [1.0, 0.0, 0.0] + +# One literal per object, so asserting these two fails -- rather than +# coincides -- if production names the widget, the render window or anything +# else. Distinct from the camera's literals above for the same reason. +CROSS_SECTION_PLANE_WASM_ID = 11 +CROSS_SECTION_REPRESENTATION_WASM_ID = 12 + + +class _PlaneDouble: + """Stand-in for the widget's ``vtkPlane``. + + Answers the two getters the seed reads with hand-written literals, so the + seeded record is asserted against values that never passed through VTK. + """ + + def GetOrigin(self): # noqa: N802 + return tuple(SEEDED_PLANE_ORIGIN) + + def GetNormal(self): # noqa: N802 + return tuple(SEEDED_PLANE_NORMAL) + + +class _CrossSectionWidgetDouble: + """Stand-in for ``VisorCrossSectionWidget``. + + ``calls`` is one shared, ordered list across ``update_bounds``, + ``set_origin`` and ``set_normal``, because what has to be pinned is their + order relative to each other and not merely that each happened. + + ``plane`` and ``plane_representation`` are two distinct objects, so an id + lookup keyed on identity can tell them apart and a re-serialise that named + the same object twice fails. + """ + + def __init__(self): + self.calls = [] + self._plane = _PlaneDouble() + self._plane_representation = object() + + @property + def plane(self): + return self._plane + + @property + def plane_representation(self): + return self._plane_representation + + def update_bounds(self, bounds): + self.calls.append(("update_bounds", list(bounds))) + + def set_origin(self, origin): + self.calls.append(("set_origin", list(origin))) + + def set_normal(self, normal): + self.calls.append(("set_normal", list(normal))) + + +@pytest.fixture +def widget(renderer): + """Install the hand-written cross-section widget double on *renderer*.""" + double = _CrossSectionWidgetDouble() + renderer._cross_section_widget = double + return double + + +BOUNDS = [-1.0, 1.0, -2.0, 2.0, -3.0, 3.0] + + +class TestCrossSectionPlane: + """The record, the two VTK writes, the two-id re-serialise, and the seed.""" + + # -- sync_cross_section_plane ------------------------------------------- + # + # The record half and the VTK half are separate tests: either can silently + # do nothing while the other succeeds, and a record that is never + # projected leaves the server's own clip plane where it was while + # ``get_state`` reports the new one. + + def test_sync_cross_section_plane_writes_the_record(self, renderer, widget): + """Store half: a reported plane becomes the server's record.""" + renderer.sync_cross_section_plane(REPORTED_PLANE_ORIGIN, REPORTED_PLANE_NORMAL) + + record = renderer.get_cross_section_plane() + assert record.origin == REPORTED_PLANE_ORIGIN + assert record.normal == REPORTED_PLANE_NORMAL + + def test_sync_cross_section_plane_writes_both_vtk_objects(self, renderer, widget): + """Apply half: the origin and the normal both reach the widget. + + Asserted as the whole call list, not with two ``assert_called_with``: + a body that wrote the origin twice, or that wrote the normal and not + the origin, is caught by the list and not by the pair. ``set_origin`` + and ``set_normal`` each write the plane *and* the representation, so + naming both here is what stops the clip and the handle diverging. + """ + renderer.sync_cross_section_plane(REPORTED_PLANE_ORIGIN, REPORTED_PLANE_NORMAL) + + assert widget.calls == [ + ("set_origin", REPORTED_PLANE_ORIGIN), + ("set_normal", REPORTED_PLANE_NORMAL), + ] + + # -- serialize_cross_section_state -------------------------------------- + + def test_serialize_cross_section_state_names_the_plane_and_the_representation_ids( + self, renderer, widget + ): + """The re-serialise names exactly two objects: the plane and the handle. + + This is the assertion that pins the increment. The id source is keyed + on object identity, so every object other than those two resolves to a + third, equally distinctive literal; asserting these two therefore + fails -- rather than coincides -- if production names the widget, the + render window or the renderer. + + Revert to a plane-only re-serialise and the call count is 1. That + revert leaves the server correct and the client saving a stale handle, + which no other assertion in this suite sees. + + No render is in this test at all, which is what pins the "never relies + on a render following" clause: the two calls are the whole mechanism. + """ + renderer._object_manager.GetId.side_effect = ( + lambda obj: CROSS_SECTION_PLANE_WASM_ID + if obj is widget.plane + else CROSS_SECTION_REPRESENTATION_WASM_ID + if obj is widget.plane_representation + else WRONG_OBJECT_WASM_ID + ) + + renderer.serialize_cross_section_state() + + calls = renderer._object_manager.UpdateStateFromObject.call_args_list + assert len(calls) == 2 + assert [call.args[0] for call in calls] == [ + CROSS_SECTION_PLANE_WASM_ID, + CROSS_SECTION_REPRESENTATION_WASM_ID, + ] + + # -- update_bounds: the seed -------------------------------------------- + + def test_update_bounds_seeds_the_record_when_there_is_none(self, renderer, widget): + """With no record, the record is seeded from the widget's own plane. + + This is what lets ``get_state`` assign the plane unconditionally, as + it does the camera. Reverted, the record stays ``None`` past the + first populate and a save of a scene nobody has dragged writes no + plane at all. + """ + assert renderer.get_cross_section_plane() is None + + renderer.update_bounds(BOUNDS) + + record = renderer.get_cross_section_plane() + assert record.origin == SEEDED_PLANE_ORIGIN + assert record.normal == SEEDED_PLANE_NORMAL + + def test_update_bounds_seeds_after_the_widgets_own_update_bounds( + self, renderer, widget + ): + """The seed reads the plane *after* the widget has rewritten it. + + Its own test, and not an extra assertion above, because the failure is + different in kind: a seed written before the widget call records the + *previous* plane, so the record is one populate behind and lags the + scene by one dataset load. The values test above still passes in that + arrangement whenever the two planes happen to agree, which is most of + the time. + + Asserted on the widget's own ordered call list, with the read-back + taken after the call returns: the widget call must have been made, and + the record must hold what the double reports *now*. + """ + renderer.update_bounds(BOUNDS) + + assert widget.calls == [("update_bounds", BOUNDS)] + assert renderer.get_cross_section_plane().origin == SEEDED_PLANE_ORIGIN + + # -- update_bounds: an existing record wins ----------------------------- + # + # ``set_part_visibility`` fans out to ``_update_widget_bounds``, so this + # method runs on every part toggle and every dataset add, not once per + # scene. The two tests below are what stop that fan-out discarding a + # plane the user dragged. They are separate because a body that wrote the + # record back to the widget *and* re-seeded it afterwards passes the first + # and fails the second. + + def test_update_bounds_with_a_record_writes_the_record_back_to_the_widget( + self, renderer, widget + ): + """An existing record is pushed back over the widget's defaults. + + The widget's own ``update_bounds`` unconditionally rewrites both VTK + objects to the default plane for the new bounds, so without this + write-back a part toggle moves the clip and the handle even though the + record still holds the dragged plane. + + Ordered after the widget call, and asserted as one list for that + reason: written before it, the widget's defaults win and the drag is + lost with every assertion on the record still passing. + """ + renderer.sync_cross_section_plane(RECORDED_PLANE_ORIGIN, RECORDED_PLANE_NORMAL) + widget.calls.clear() + + renderer.update_bounds(BOUNDS) + + assert widget.calls == [ + ("update_bounds", BOUNDS), + ("set_origin", RECORDED_PLANE_ORIGIN), + ("set_normal", RECORDED_PLANE_NORMAL), + ] + + def test_update_bounds_with_a_record_leaves_the_record_unchanged( + self, renderer, widget + ): + """The record is not re-seeded from the widget when it already exists. + + The negative twin of the test above. The widget double reports the + seed literals, which differ in every component from the record's, so a + body that wrote back *and* then re-seeded passes the write-back test + and fails this one -- and in the running application would still throw + the dragged plane away on the next save. + """ + renderer.sync_cross_section_plane(RECORDED_PLANE_ORIGIN, RECORDED_PLANE_NORMAL) + + renderer.update_bounds(BOUNDS) + + record = renderer.get_cross_section_plane() + assert record.origin == RECORDED_PLANE_ORIGIN + assert record.normal == RECORDED_PLANE_NORMAL + + diff --git a/tests/unit/vtk/scene/test_base.py b/tests/unit/vtk/scene/test_base.py index 5c7daf60..5810d601 100644 --- a/tests/unit/vtk/scene/test_base.py +++ b/tests/unit/vtk/scene/test_base.py @@ -28,6 +28,7 @@ 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_camera_state import VisorCameraState +from ansys.visor.viewer.models.common.visor_cross_section_state import VisorCrossSectionState from ansys.visor.viewer.models.common.visor_ui_state import VisorUIState from ansys.visor.viewer.models.common.visor_variable_state import VisorVariableState from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1 @@ -1030,15 +1031,23 @@ class _CameraRecordRenderer: Records the scene's lock depth at the moment the record is read, so the read can be asserted to happen with the lock *held* rather than merely taken at some point. + + ``get_cross_section_plane`` is answered because ``get_state`` now reads + the plane record on every call, beside the camera's. It defaults to + ``None`` -- the camera tests above say nothing about the plane and must + keep saying nothing -- and the plane tests below pass one in. """ - def __init__(self, record, pipeline_camera, scene=None): + def __init__(self, record, pipeline_camera, scene=None, cross_section=None): self._record = record self._pipeline_camera = pipeline_camera self._scene = scene + self._cross_section = cross_section self.record_reads = 0 self.pipeline_reads = 0 + self.plane_reads = 0 self.depth_at_read = None + self.plane_depth_at_read = None def get_camera_state(self): self.record_reads += 1 @@ -1046,6 +1055,12 @@ def get_camera_state(self): self.depth_at_read = getattr(self._scene._vtk_lock, "depth", None) return self._record + def get_cross_section_plane(self): + self.plane_reads += 1 + if self._scene is not None: + self.plane_depth_at_read = getattr(self._scene._vtk_lock, "depth", None) + return self._cross_section + def _read_pipeline_camera(self): self.pipeline_reads += 1 return self._pipeline_camera @@ -1223,15 +1238,37 @@ def test_apply_state_applies_every_part_to_the_pipeline( 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.""" +def test_apply_state_flushes_before_the_bridge_call(scene, registry): + """Exactly one flush, and it is ordered before the bridge call. + + This is the assertion that pins the placement of the ``finalize_scene`` + call in ``apply_state``. The flush is what carries the restored object + graph -- the cross-section plane above all -- to the client, so it has to + run *after* every restore and *before* the fire-and-forget ``set_state`` + the bridge call issues. A flush after the push races the client's rebuild + against that call, which is the loss ``VisorLocalScene._push_runtime_state`` + documents. + + The spy is on ``_local_view.update`` and not on ``flush_wasm_state``, + deliberately. ``flush_wasm_state`` *is* ``_local_view.update()``, and + ``render()`` ends in the same call, so only a spy at that boundary sees the + flush whichever route it arrives by. The previous version of this test + spied ``flush_wasm_state`` and was therefore blind to the flush + ``finalize_scene`` performs: it stayed green with the call before the push, + after the push, and absent entirely. + + Three wrong placements, one failing assertion: after the push gives + ``["bridge", "flush"]``, removed gives ``["bridge"]``, and returned to the + load path also gives ``["bridge"]``, since nothing in this module drives + ``load_state``. + """ order = [] scene._push_runtime_state = lambda state: order.append("bridge") - scene._renderer.flush_wasm_state = lambda: order.append("flush") + scene._renderer._local_view.update = lambda: order.append("flush") _apply(scene, _runtime_state({NODE_ID: RuntimePartProperties(id=NODE_ID, opacity=0.25)})) - assert order == ["bridge"] + assert order == ["flush", "bridge"] # --------------------------------------------------------------------------- @@ -1945,11 +1982,24 @@ class _ToggleSpyRenderer: is an AttributeError here rather than a silently absorbed no-op, and so that the lock depth can be read at the moment of the call rather than after the fact. + + ``update_bounds``, ``update_actor_count`` and ``render`` are the scene + surface ``apply_state`` reaches through its ``finalize_scene`` call, and + are the only three names added for it: with a toggle-only state the part + loop makes no renderer call, the cross-section and camera restores are + both guarded off, and ``skip_reset_camera=True`` keeps ``reset_camera`` + out. Anything else is still an AttributeError, which is the point of the + double. They record into ``scene_calls`` rather than ``calls`` so that + the toggle assertions keep their exact meaning; nothing asserts + ``scene_calls``, which exists so a failure dump shows what was reached. + The placement of the finalize call is pinned in + ``test_apply_state_flushes_before_the_bridge_call``. """ def __init__(self, scene): self._scene = scene self.calls = [] + self.scene_calls = [] self.depths = {} def _record(self, name, visible): @@ -1965,6 +2015,15 @@ def set_edges_visible(self, visible): def set_bounding_box_visibility(self, visible): self._record("set_bounding_box_visibility", visible) + def update_bounds(self, bounds): + self.scene_calls.append("update_bounds") + + def update_actor_count(self, count): + self.scene_calls.append("update_actor_count") + + def render(self): + self.scene_calls.append("render") + class _SceneDetailsRenderer: """Renderer double for the scene-details delivery path. @@ -2568,3 +2627,262 @@ def test_get_state_orthographic_enabled_is_none_when_the_record_is_empty(scene): assert persisted.scene.orthographic_enabled is None assert persisted.scene.camera is None + + +# =========================================================================== +# The cross-section plane -- trigger path, save path, load path +# +# ``VisorSceneBase.sync_cross_section_plane`` is the coordinator method the +# ``sync_cross_section_plane`` trigger routes through. As with the camera, +# the handler arrives on trame's daemon thread and must not touch the renderer +# directly, so what is asserted is the whole critical section: the write, the +# re-serialisation that follows it, and the lock held across both. +# +# The plane has exactly one delivery channel to a rebuilt or reconnecting +# client -- the wasm state fetch after the re-serialisation. It is not on the +# scene-details payload. That is why the re-serialisation is asserted here at +# all: dropping it leaves the server correct, every other gate green, and the +# user's drag snapping back on the next page reload. +# +# Own literals, distinct from the camera's above, so that a failure names the +# path that broke. +# =========================================================================== + +# What the renderer's record holds at save time. +RECORD_CROSS_SECTION_ORIGIN = [51.0, 52.0, 53.0] +RECORD_CROSS_SECTION_NORMAL = [0.0, 1.0, 0.0] + +# What the browser answers getState with. Never the right answer, and +# different in every component, so "read the record" and "read the reply" +# cannot both pass. +REPLY_CROSS_SECTION_ORIGIN = [61.0, 62.0, 63.0] +REPLY_CROSS_SECTION_NORMAL = [1.0, 0.0, 0.0] + +# What a settled drag reports, and what a save file carries on load. +GESTURE_CROSS_SECTION_ORIGIN = [71.0, 72.0, 73.0] +GESTURE_CROSS_SECTION_NORMAL = [0.0, 0.0, 1.0] + +LOADED_CROSS_SECTION_ORIGIN = [81.0, 82.0, 83.0] +LOADED_CROSS_SECTION_NORMAL = [0.0, 1.0, 0.0] + + +def _record_plane() -> VisorCrossSectionState: + """The renderer's plane record, from hand-written literals.""" + return VisorCrossSectionState( + origin=RECORD_CROSS_SECTION_ORIGIN, + normal=RECORD_CROSS_SECTION_NORMAL, + ) + + +def _reply_plane() -> VisorCrossSectionState: + """What the browser answers getState with. Never the right answer.""" + return VisorCrossSectionState( + origin=REPLY_CROSS_SECTION_ORIGIN, + normal=REPLY_CROSS_SECTION_NORMAL, + ) + + +def _plane_save_scene(scene, record_plane, reply_plane): + """Wire *scene* for a save: renderer record *record_plane*, reply *reply_plane*. + + The same shape as ``_save_scene`` above, but the reply carries a plane + rather than a camera, because that is the field whose source is under + test. The registry is emptied so the mapper's per-dataset loop + contributes nothing. + """ + double = _CameraRecordRenderer( + None, _pipeline_camera(), scene=scene, cross_section=record_plane + ) + scene._renderer = double + scene._dataset_registry = VisorDatasetRegistry() + + async def _get_runtime_state_async(timeout): + return RuntimeAppState.from_components( + dark_mode=False, + unit="m", + dataset_states={}, + cross_section=reply_plane, + ) + + scene._get_runtime_state_async = _get_runtime_state_async + return double + + +# --------------------------------------------------------------------------- +# The coordinator +# --------------------------------------------------------------------------- + +def test_sync_cross_section_plane_applies_to_the_renderer_and_then_serializes(scene): + """The re-serialisation follows the write, and there are two of them. + + Reverted -- the write kept and the re-serialisation dropped -- the record + is right, the server's own plane and representation are right, every other + test in this module still passes, and the client is served the pre-drag + plane on its next fetch. The user sees a page reload snap the plane back + to where it was before they dragged it. + + Two serialise calls, not one: ``serialize_cross_section_state`` names the + plane and the representation separately. Which object each names is + pinned in tests/unit/renderer/test_local_renderer.py, against a widget + double whose two objects have distinct ids; what is pinned here is that + the coordinator reaches that method at all, and reaches it after the + write. + """ + order = [] + real_sync = scene._renderer.sync_cross_section_plane + + def _sync(origin, normal): + order.append("plane") + return real_sync(origin, normal) + + scene._renderer.sync_cross_section_plane = _sync + scene._renderer._object_manager.UpdateStateFromObject = ( + lambda object_id: order.append("serialize") + ) + + scene.sync_cross_section_plane( + GESTURE_CROSS_SECTION_ORIGIN, GESTURE_CROSS_SECTION_NORMAL + ) + + assert order == ["plane", "serialize", "serialize"] + + +def test_sync_cross_section_plane_holds_the_lock_across_both_halves(scene): + """Both halves run inside one and the same critical section. + + Its own test rather than another assertion on the ordering test, on the + precedent of ``test_sync_camera_holds_the_lock_across_both_halves``: lock + depth and call order fail for different reasons and want to be readable + apart. The pair discriminates precisely -- a re-serialisation that was + dropped fails both, one that was merely moved below the ``with`` block + fails only this one. + + The two depths are asserted **equal** as well as non-zero. Non-zero alone + would pass a body that released and re-took the lock between the write and + the re-serialise, which is not one critical section: another thread can + mutate the VTK object graph in the gap, and the client is then served a + half-written scene. That failure is intermittent and never reproduces + under a gate. + """ + scene._vtk_lock = _LockSpy() + observed = {} + real_sync = scene._renderer.sync_cross_section_plane + + def _sync(origin, normal): + observed["write_depth"] = scene._vtk_lock.depth + return real_sync(origin, normal) + + scene._renderer.sync_cross_section_plane = _sync + scene._renderer._object_manager.UpdateStateFromObject = ( + lambda object_id: observed.update(serialize_depth=scene._vtk_lock.depth) + ) + + scene.sync_cross_section_plane( + GESTURE_CROSS_SECTION_ORIGIN, GESTURE_CROSS_SECTION_NORMAL + ) + + assert observed["write_depth"] >= 1 + assert observed["serialize_depth"] >= 1 + assert observed["serialize_depth"] == observed["write_depth"] + assert scene._vtk_lock.depth == 0 + assert scene._vtk_lock.enter_count == scene._vtk_lock.exit_count + + +# --------------------------------------------------------------------------- +# get_state -- the plane comes from the record, not from the reply +# --------------------------------------------------------------------------- + +def test_get_state_takes_the_cross_section_from_the_record(scene): + """The saved plane is the record's, with the browser saying otherwise. + + The record and the reply differ in every component, so "server + authoritative" and "round-trips the client's answer" cannot both pass. + Revert the assignment and both assertions below report the reply's + numbers. + + Asserted on what get_state RETURNS -- the object that reaches the writer + -- not on the runtime state it was built from. An assignment placed after + ``runtime_to_persisted`` passes every assertion made against the runtime + object and still writes the wrong file. + """ + _plane_save_scene(scene, _record_plane(), _reply_plane()) + + persisted = asyncio.run(scene.get_state(timeout=1.0)) + + assert persisted.scene.cross_section.origin == RECORD_CROSS_SECTION_ORIGIN + assert persisted.scene.cross_section.normal == RECORD_CROSS_SECTION_NORMAL + + +def test_get_state_writes_no_cross_section_when_the_record_is_empty(scene): + """An empty record writes ``None`` through, reply notwithstanding. + + The assignment is unconditional, exactly as the camera's, and this is the + only test that a guarded one -- one that skipped the write when the record + was ``None`` -- would fail. The reply carries a perfectly valid plane, so + the guarded version would save the browser's answer and look correct + everywhere else. + + ``None`` says "no plane was ever written". The guard for "absent says + nothing" belongs to the load path, not here. + """ + _plane_save_scene(scene, None, _reply_plane()) + + persisted = asyncio.run(scene.get_state(timeout=1.0)) + + assert persisted.scene.cross_section is None + + +# --------------------------------------------------------------------------- +# apply_state -- the load path writes the renderer and re-serialises +# --------------------------------------------------------------------------- + +def test_apply_state_serializes_the_loaded_plane_after_syncing_it(scene): + """A plane in the file reaches the renderer, and is then re-serialised. + + Driven with a real ``PersistedViewerStateV1`` through the real state + mapper, so this also exercises the mapper's plane pass-through; it is the + first test here that would notice if the mapper stopped handing + ``cross_section`` on verbatim. + + ``camera=None`` keeps the camera step out of the recording, so every + ``"serialize"`` below is the plane's. Reverted -- the sync kept and the + re-serialise dropped -- the server's plane is right and the client is + served the pre-load one, which is the defect the load path already had for + the camera and fixed for the same reason. + """ + order = [] + real_sync = scene._renderer.sync_cross_section_plane + + def _sync(origin, normal): + order.append(("plane", list(origin), list(normal))) + return real_sync(origin, normal) + + scene._renderer.sync_cross_section_plane = _sync + scene._renderer._object_manager.UpdateStateFromObject = ( + lambda object_id: order.append("serialize") + ) + + scene.apply_state( + PersistedViewerStateV1.from_components( + ui_state=VisorUIState(dark_theme=False), + unit="m", + orthographic_enabled=None, + cross_section_enabled=None, + edges_enabled=None, + bounding_box_enabled=None, + datasets={}, + camera=None, + cross_section=VisorCrossSectionState( + origin=LOADED_CROSS_SECTION_ORIGIN, + normal=LOADED_CROSS_SECTION_NORMAL, + ), + ) + ) + + assert order == [ + ("plane", LOADED_CROSS_SECTION_ORIGIN, LOADED_CROSS_SECTION_NORMAL), + "serialize", + "serialize", + ] + + diff --git a/tests/unit/vtk/widgets/test_visor_cross_section_widget.py b/tests/unit/vtk/widgets/test_visor_cross_section_widget.py index af754edf..8bf23e47 100644 --- a/tests/unit/vtk/widgets/test_visor_cross_section_widget.py +++ b/tests/unit/vtk/widgets/test_visor_cross_section_widget.py @@ -74,6 +74,18 @@ def test_plane_property(mocks): widget = VisorCrossSectionWidget(mocks['interactor']) assert widget.plane is widget._plane +def test_plane_representation_property(mocks): + """Verify that plane_representation returns the internal representation. + + Asserted by identity against ``_plane_representation`` and, separately, + as *not* the plane: the re-serialise names both objects one at a time and + a property that answered the plane would make it name the same object + twice, which is a plane-only re-serialise wearing two ids. + """ + widget = VisorCrossSectionWidget(mocks['interactor']) + assert widget.plane_representation is widget._plane_representation + assert widget.plane_representation is not widget.plane + def test_algorithm_filter_property(mocks): """Verify that the algorithm_filter property returns the internal algorithm filter object.""" widget = VisorCrossSectionWidget(mocks['interactor'])
Array Type:${escapeHtml(spectrumMetadata.type)}${escapeHtml(variableMetadata.type)}
Array Name:${escapeHtml(spectrumMetadata.name)}${escapeHtml(variableMetadata.name)}
Array Shape:${escapeHtml(spectrumMetadata.shape)}${escapeHtml(variableMetadata.shape)}
Component: