|
44 | 44 | from ansys.visor.viewer.app.visor_vtk import VisorVTK |
45 | 45 | from ansys.visor.viewer.core.metadata import ExtendedMetadata |
46 | 46 | from ansys.visor.viewer.models.common.part_properties import PartProperties |
| 47 | +from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState |
47 | 48 | from ansys.visor.viewer.models.common.visor_ui_state import VisorUIState |
48 | 49 | from ansys.visor.viewer.models.persist.dataset.persisted_dataset_state import PersistedDatasetState |
49 | 50 | from ansys.visor.viewer.models.persist.persisted_viewer_state import PersistedViewerStateV1 |
@@ -100,6 +101,46 @@ def iface(): |
100 | 101 | pass |
101 | 102 |
|
102 | 103 |
|
| 104 | +# ------------------------------------------------------------------ # |
| 105 | +# Camera literals |
| 106 | +# |
| 107 | +# Hand-written, and different in every field between the two, so the saved |
| 108 | +# file names its source by value rather than by a recorded call. No value |
| 109 | +# here originates from VTK. |
| 110 | +# ------------------------------------------------------------------ # |
| 111 | + |
| 112 | +RECORD_CAMERA_POSITION = [11.0, 12.0, 13.0] |
| 113 | +RECORD_CAMERA_CLIPPING_RANGE = [17.0, 18.0] |
| 114 | +REPLY_CAMERA_POSITION = [21.0, 22.0, 23.0] |
| 115 | +REPLY_CAMERA_CLIPPING_RANGE = [27.0, 28.0] |
| 116 | + |
| 117 | + |
| 118 | +def _record_camera() -> VisorCameraState: |
| 119 | + """The camera the server's record holds at save time.""" |
| 120 | + return VisorCameraState( |
| 121 | + position=RECORD_CAMERA_POSITION, |
| 122 | + focal_point=[14.0, 15.0, 16.0], |
| 123 | + view_up=[0.0, 1.0, 0.0], |
| 124 | + clipping_range=RECORD_CAMERA_CLIPPING_RANGE, |
| 125 | + parallel_projection=True, |
| 126 | + view_angle=31.0, |
| 127 | + parallel_scale=19.0, |
| 128 | + ) |
| 129 | + |
| 130 | + |
| 131 | +def _reply_camera() -> VisorCameraState: |
| 132 | + """The camera the browser answers getState with.""" |
| 133 | + return VisorCameraState( |
| 134 | + position=REPLY_CAMERA_POSITION, |
| 135 | + focal_point=[24.0, 25.0, 26.0], |
| 136 | + view_up=[1.0, 0.0, 0.0], |
| 137 | + clipping_range=REPLY_CAMERA_CLIPPING_RANGE, |
| 138 | + parallel_projection=False, |
| 139 | + view_angle=32.0, |
| 140 | + parallel_scale=29.0, |
| 141 | + ) |
| 142 | + |
| 143 | + |
103 | 144 | # ================================================================== # |
104 | 145 | # write_dataset / read_dataset round-trips |
105 | 146 | # ================================================================== # |
@@ -488,4 +529,47 @@ def test_reloading_the_saved_state_restores_the_registry(self, file_io, iface, t |
488 | 529 | assert record.spectrum_id == "POINT::pressure::1" |
489 | 530 | assert record.spectrum_component == 0 |
490 | 531 |
|
| 532 | + def test_saved_visor_json_carries_the_camera_record_not_the_browsers(self, iface, tmp_path): |
| 533 | + """save_state writes the server's camera record, not the browser's reply. |
| 534 | +
|
| 535 | + The record is seeded through ``sync_camera``, which also projects onto |
| 536 | + the pipeline camera, so record and pipeline hold the same values here. |
| 537 | + This case therefore discriminates the **record from the browser's |
| 538 | + reply** and nothing more; separating the record from its own pipeline |
| 539 | + projection is done in tests/unit/vtk/scene/test_base.py, against a |
| 540 | + renderer double whose pipeline read answers with different numbers. |
| 541 | +
|
| 542 | + No dataset is added, so ``finalize_scene``'s reset never runs and |
| 543 | + cannot overwrite the seeded record with a VTK-derived one. |
| 544 | + """ |
| 545 | + iface._scene._renderer.sync_camera(_record_camera()) |
| 546 | + |
| 547 | + # The browser answers getState with a different camera in every field. |
| 548 | + # A pass therefore proves the file came from the record. |
| 549 | + frontend_state = RuntimeAppState.from_components( |
| 550 | + dark_mode=False, |
| 551 | + unit="m", |
| 552 | + dataset_states={}, |
| 553 | + camera=_reply_camera(), |
| 554 | + ) |
| 555 | + |
| 556 | + async def _frontend_round_trip(timeout: float = 5.0): |
| 557 | + return frontend_state |
| 558 | + |
| 559 | + iface._scene._get_runtime_state_async = _frontend_round_trip |
| 560 | + iface._server_manager = MagicMock() |
| 561 | + iface._server_manager.running = True |
| 562 | + |
| 563 | + asyncio.run(iface.save_state(str(tmp_path))) |
| 564 | + |
| 565 | + with open(os.path.join(str(tmp_path), "visor.json"), "r") as fh: |
| 566 | + written = json.load(fh) |
| 567 | + |
| 568 | + # write_state dumps by_alias, so the camera's own fields are aliased. |
| 569 | + camera = written["scene"]["camera"] |
| 570 | + assert camera["position"] == RECORD_CAMERA_POSITION |
| 571 | + assert camera["clippingRange"] == RECORD_CAMERA_CLIPPING_RANGE |
| 572 | + assert camera["position"] != REPLY_CAMERA_POSITION |
| 573 | + assert camera["clippingRange"] != REPLY_CAMERA_CLIPPING_RANGE |
| 574 | + |
491 | 575 |
|
0 commit comments