Skip to content

Commit 094f434

Browse files
committed
Push every state field with a value and bump the scene schema
1 parent 2f42ff7 commit 094f434

18 files changed

Lines changed: 254 additions & 161 deletions

‎src/ansys/visor/viewer/models/runtime/dataset/runtime_dataset_state.py‎

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,7 @@
22

33
from typing import Any, Dict, List
44

5-
from pydantic import (
6-
BaseModel,
7-
ConfigDict,
8-
Field,
9-
SerializationInfo,
10-
SerializerFunctionWrapHandler,
11-
field_validator,
12-
model_serializer,
13-
)
5+
from pydantic import BaseModel, ConfigDict, Field, field_validator
146

157
from ansys.visor.viewer.core.visor_colors import VisorPartDefaults
168
from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger
@@ -34,10 +26,9 @@ class RuntimePartProperties(BaseModel):
3426
No field holds ``None`` except ``color_variable``, whose ``None`` is the
3527
resolved value "not coloured by a variable".
3628
37-
The wire carries the reference flat, as ``variableId`` and
38-
``variableComponent``. With no reference, ``variableId`` is ``null`` and
39-
``variableComponent`` is absent; under ``exclude_none`` ``variableId`` is
40-
absent too. Callers pass ``by_alias=True`` so the keys stay camelCase.
29+
The wire carries the reference nested, as ``colorVariable`` holding
30+
``variableId`` and ``variableComponent``, or ``null`` with no reference.
31+
Callers pass ``by_alias=True`` so the keys stay camelCase.
4132
4233
id: The part's scene-graph node ID.
4334
opacity: Between 0.0 (fully transparent) and 1.0 (fully opaque).
@@ -53,23 +44,7 @@ class RuntimePartProperties(BaseModel):
5344
visible: bool
5445
selected: bool
5546
diffuse_rgb: List[float] = Field(alias="diffuseRgb")
56-
color_variable: VariableReference | None
57-
58-
@model_serializer(mode="wrap")
59-
def _serialize(self, handler: SerializerFunctionWrapHandler, info: SerializationInfo) -> dict:
60-
"""Emit the reference flat as variableId/variableComponent; with none, variableId is null unless
61-
exclude_none."""
62-
data: dict = handler(self)
63-
data.pop("color_variable", None)
64-
id_key = "variableId" if info.by_alias else "variable_id"
65-
component_key = "variableComponent" if info.by_alias else "variable_component"
66-
reference = self.color_variable
67-
if reference is not None:
68-
data[id_key] = reference.variable_id
69-
data[component_key] = reference.variable_component
70-
elif not info.exclude_none:
71-
data[id_key] = None
72-
return data
47+
color_variable: VariableReference | None = Field(alias="colorVariable")
7348

7449
@classmethod
7550
def default_for(cls, part_id: int) -> "RuntimePartProperties":
Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
11
"""Model for loading a Visor viewer state."""
22

3-
from pydantic import (
4-
BaseModel,
5-
ConfigDict,
6-
Field,
7-
SerializationInfo,
8-
SerializerFunctionWrapHandler,
9-
model_serializer,
10-
)
3+
from pydantic import BaseModel, ConfigDict, Field
114

125
from ansys.visor.viewer.models.runtime.scene.runtime_app_state import RuntimeAppState
13-
from ansys.visor.viewer.models.runtime.scene.runtime_view_state import scene_dump_with_view
146

157

168
class VisorLoadStateRequest(BaseModel):
@@ -22,15 +14,3 @@ class VisorLoadStateRequest(BaseModel):
2214
# A unique identifier for the load state request.
2315
request_id: int
2416
app_state: RuntimeAppState = Field(alias="appState")
25-
26-
@model_serializer(mode="wrap")
27-
def _dump_in_the_flat_scene_shape(
28-
self, handler: SerializerFunctionWrapHandler, info: SerializationInfo
29-
) -> dict:
30-
"""Move the view's camera and crossSection into appState.scene after unit; emit no view key."""
31-
data: dict = handler(self)
32-
app_key = "appState" if info.by_alias else "app_state"
33-
app_state: dict = data[app_key]
34-
view: dict = app_state.pop("view")
35-
app_state["scene"] = scene_dump_with_view(app_state["scene"], view)
36-
return data
Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Model for the view records the renderer tracks."""
22

3-
from typing import Any, Dict
4-
53
from pydantic import BaseModel, ConfigDict, Field
64

75
from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState
@@ -18,17 +16,3 @@ class RuntimeViewState(BaseModel):
1816

1917
camera: VisorCameraState | None # None until the renderer first sets it
2018
cross_section: VisorCrossSectionState | None = Field(alias="crossSection") # None until the renderer first sets it
21-
22-
23-
def scene_dump_with_view(scene: Dict[str, Any], view: Dict[str, Any]) -> Dict[str, Any]:
24-
"""A dumped scene record with the dumped view's keys placed directly after ``unit``.
25-
26-
The wire carries the camera and the plane inside the scene object, not
27-
under a key of their own; this rebuilds that shape from the two dumps.
28-
"""
29-
out: Dict[str, Any] = {}
30-
for key, value in scene.items():
31-
out[key] = value
32-
if key == "unit":
33-
out.update(view)
34-
return out
Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
11
"""Model for visor scene details, the data transfer object sent to the client."""
22

3-
from pydantic import (
4-
BaseModel,
5-
ConfigDict,
6-
Field,
7-
SerializationInfo,
8-
SerializerFunctionWrapHandler,
9-
model_serializer,
10-
)
3+
from pydantic import BaseModel, ConfigDict, Field
114

125
from ansys.visor.viewer.models.runtime.scene.runtime_scene_state import RuntimeSceneState
136
from ansys.visor.viewer.models.runtime.scene.runtime_ui_state import RuntimeUIState
14-
from ansys.visor.viewer.models.runtime.scene.runtime_view_state import scene_dump_with_view
157
from ansys.visor.viewer.models.runtime.vtk.renderer_annotation import RendererAnnotation
168
from ansys.visor.viewer.models.runtime.vtk.runtime_vtk_info import RuntimeVTKInfo
179
from ansys.visor.viewer.models.runtime.vtk.scene_graph_node_info import SceneGraphNodeInfo
1810

1911
# Bumped whenever the scene-details wire shape changes incompatibly.
20-
SCENE_DETAILS_SCHEMA_VERSION: int = 3
12+
SCENE_DETAILS_SCHEMA_VERSION: int = 4
2113

2214

2315
class VisorSceneDetails(BaseModel):
@@ -50,31 +42,5 @@ def from_components(
5042
)
5143
return cls(ui=ui, scene=scene, vtk_info=vtk_info)
5244

53-
@model_serializer(mode="wrap")
54-
def _dump_in_the_app_state_shape(
55-
self, handler: SerializerFunctionWrapHandler, info: SerializationInfo
56-
) -> dict:
57-
"""Nest ui and scene under appState, beside schemaVersion and vtkInfo.
58-
59-
The scene object carries null ``camera`` and ``crossSection`` keys
60-
after ``unit`` unless ``exclude_none`` is set, which is the shape the
61-
client reads.
62-
"""
63-
data: dict = handler(self)
64-
schema_key = "schemaVersion" if info.by_alias else "schema_version"
65-
app_key = "appState" if info.by_alias else "app_state"
66-
vtk_key = "vtkInfo" if info.by_alias else "vtk_info"
67-
view = {} if info.exclude_none else {
68-
"camera": None,
69-
"crossSection" if info.by_alias else "cross_section": None,
70-
}
71-
out: dict = {}
72-
if schema_key in data:
73-
out[schema_key] = data[schema_key]
74-
out[app_key] = {"ui": data["ui"], "scene": scene_dump_with_view(data["scene"], view)}
75-
if vtk_key in data:
76-
out[vtk_key] = data[vtk_key]
77-
return out
78-
7945

8046
VisorSceneDetails.model_rebuild()

‎src/ansys/visor/viewer/vtk/scene/base.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ def get_scene_details(self) -> VisorSceneDetails:
405405

406406
def get_scene_details_json(self) -> str:
407407
"""Return the VisorVtkPipelineState as JSON string."""
408-
return json.dumps(self.get_scene_details().model_dump(exclude_none=True, by_alias=True))
408+
return json.dumps(self.get_scene_details().model_dump(by_alias=True))
409409

410410
def clear(self):
411411
"""Remove all actors from the renderer and reset the scene.

‎src/ansys/visor/visor-client/src/jest-tests/AppStateProjectionLoad.test.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type VisorVtkSceneNode from '../state/appstate/vtkInfo/VisorVtkSceneNode.
88
* widget flag.
99
*
1010
* Two persisted fields used to write the same wasm camera property --
11-
* `scene.orthographicEnabled` and `scene.camera.parallelProjection` -- both
11+
* `scene.orthographicEnabled` and `view.camera.parallelProjection` -- both
1212
* pushed into one unordered promise array, with only the first also setting
1313
* the cached flag `getAppStateAsync` reads back. Which one won was
1414
* unspecified. This module pins that there is now exactly one writer, that it
@@ -110,6 +110,8 @@ describe('setAppStateAsync applies projection from the camera alone', () => {
110110
{
111111
scene: {
112112
orthographicEnabled: PERSISTED_ORTHOGRAPHIC_ENABLED,
113+
},
114+
view: {
113115
camera: { parallelProjection: CAMERA_PARALLEL_PROJECTION },
114116
},
115117
},
@@ -131,6 +133,8 @@ describe('setAppStateAsync applies projection from the camera alone', () => {
131133
{
132134
scene: {
133135
orthographicEnabled: true,
136+
},
137+
view: {
134138
camera: {},
135139
},
136140
},
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import VisorPartState from '../state/appstate/VisorPartState.tsx';
2+
3+
describe('a part state parses the delivered colorVariable', () => {
4+
test('a nested colorVariable parses onto the node reference', () => {
5+
const part = new VisorPartState({
6+
id: '1',
7+
colorVariable: { variableId: 'POINT::pressure::1', variableComponent: 2 },
8+
});
9+
10+
expect(part.variableId).toBe('POINT::pressure::1');
11+
expect(part.variableComponent).toBe(2);
12+
});
13+
14+
test('a null colorVariable parses as no reference', () => {
15+
const part = new VisorPartState({ id: '1', colorVariable: null });
16+
17+
expect(part.variableId).toBeNull();
18+
expect(part.variableComponent).toBeUndefined();
19+
});
20+
});
21+

‎src/ansys/visor/visor-client/src/jest-tests/SchemaVersion.test.tsx‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,43 @@ describe('SchemaVersion', () => {
2626
expect(error.expectedVersion).toBe(SCENE_DETAILS_SCHEMA_VERSION);
2727
});
2828

29+
test('scene details at schema version 3 are rejected', () => {
30+
let thrown: unknown = null;
31+
try {
32+
new VisorSceneDetails({ schemaVersion: 3, appState: undefined, vtkInfo: undefined });
33+
} catch (err) {
34+
thrown = err;
35+
}
36+
expect(thrown).toBeInstanceOf(SchemaVersionMismatchError);
37+
const error = thrown as SchemaVersionMismatchError;
38+
expect(error.payloadVersion).toBe(3);
39+
expect(error.expectedVersion).toBe(4);
40+
});
41+
42+
test('scene details with a null orthographicEnabled parse and leave it undefined', () => {
43+
const details = new VisorSceneDetails({
44+
schemaVersion: 4,
45+
ui: {},
46+
scene: { unit: '', orthographicEnabled: null },
47+
vtkInfo: {
48+
sceneGraph: {
49+
id: 0,
50+
dataArrays: [],
51+
name: '',
52+
isGroupNode: true,
53+
isActorNode: false,
54+
nodeType: 'root',
55+
diffuseColor: [1, 1, 1],
56+
bounds: [],
57+
children: [],
58+
},
59+
rendererAnnotation: null,
60+
},
61+
});
62+
63+
expect(details.appState.scene.orthographicEnabled).toBeUndefined();
64+
});
65+
2966
test('parseRendererAnnotation returns null for a null or undefined annotation', () => {
3067
expect(parseRendererAnnotation(null)).toBeNull();
3168
expect(parseRendererAnnotation(undefined)).toBeNull();

‎src/ansys/visor/visor-client/src/jest-tests/VariableRecordProjection.test.tsx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,10 @@ describe('setAppStateAsync holds a delivered variable block', () => {
380380
'0': {
381381
id: '0',
382382
partStates: {
383-
'1': { id: '1', variableId: PRESSURE_ID, variableComponent: -1 },
383+
'1': {
384+
id: '1',
385+
colorVariable: { variableId: PRESSURE_ID, variableComponent: -1 },
386+
},
384387
},
385388
},
386389
},
@@ -529,7 +532,10 @@ function deliveryColouringPartA(record: Record<string, unknown>) {
529532
'0': {
530533
id: '0',
531534
partStates: {
532-
'1': { id: '1', variableId: PRESSURE_ID, variableComponent: -1 },
535+
'1': {
536+
id: '1',
537+
colorVariable: { variableId: PRESSURE_ID, variableComponent: -1 },
538+
},
533539
},
534540
},
535541
},

‎src/ansys/visor/visor-client/src/state/appstate/VisorAppState.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export default class VisorAppState {
3030

3131
this._ui.copy(data.ui, replace);
3232
this._scene.copy(data.scene, replace);
33+
// The camera and the plane arrive under view and are held on the
34+
// scene's camera and crossSection records.
35+
this._scene.camera.copy(data.view?.camera, replace);
36+
this._scene.crossSection.copy(data.view?.crossSection, replace);
3337
}
3438
return this;
3539
}
@@ -42,6 +46,10 @@ export default class VisorAppState {
4246
return {
4347
ui: this.ui.toDict(),
4448
scene: this.scene.toDict(),
49+
view: {
50+
camera: this.scene.camera.toDict(),
51+
crossSection: this.scene.crossSection.toDict(),
52+
},
4553
};
4654
}
4755
}

0 commit comments

Comments
 (0)