Skip to content

feat(view_state): JSON save/restore for camera + render-look state#6

Merged
xarthurx merged 15 commits into
mainfrom
feat/view-save-restore
May 19, 2026
Merged

feat(view_state): JSON save/restore for camera + render-look state#6
xarthurx merged 15 commits into
mainfrom
feat/view-save-restore

Conversation

@xarthurx

Copy link
Copy Markdown
Owner

Summary

Adds JSON save/restore of the camera pose + render-look state (background, ground plane, transparency, SSAO, SSAA). Ports upstream C++ Polyscope's `getViewAsJson`/`setViewFromJson` (commit `7570a40` + FOV fix in PR #389) — but with a Rust-native idiomatic API.

  • New public API: `save_view_to_json`, `load_view_from_json`, `save_view_to_file`, `load_view_from_file`, `current_view_state`, `apply_view_state`, plus the `ViewTransition` enum (`Instant` / `FlyTo`).
  • Two UI buttons in the View Controls panel open native file dialogs (via `rfd`); the dialog runs after the egui frame to avoid multi-pass reruns.
  • Headless rendering honors a pre-loaded view (scripts can do `load_view_from_file(...)?; render_to_file(...)?` for reproducible figures).
  • JSON format is Rust-native (not byte-compatible with C++ Polyscope) and versioned (`v1`).

Design

Full design spec lives at `docs/plans/2026-05-19-view-save-restore-design.md` (V2 after codex review caught state-ownership mistakes in V1).

Core mechanism: two new private buffers on `Context` — `view_state_snapshot` (App-published, frame-by-frame) and `pending_view_apply` (queued by callers, App-consumed). This lets free functions reach Camera (which lives in `RenderEngine`, not `Context`) without breaking the layering.

JSON shape:

```json
{
"version": 1,
"camera": { "position": [..], "target": [..], "up": [..], "fov": .., "near": .., "far": ..,
"projection_mode": "perspective", "ortho_scale": ..,
"navigation_style": "turntable", "up_direction": "pos_y", "front_direction": "neg_z" },
"render": { "background_color": [..], "ground_plane": { "mode": "tile_reflection", .. },
"transparency": { "mode": "simple", .. }, "ssao": {..}, "ssaa_factor": 1 }
}
```

Commits (14, TDD throughout)

Commit What
`7b47be7` Foundation: 2 error variants (`InvalidViewState`, `NoActiveView`) + Context buffer fields
`097e175` `CameraState` DTO + `Camera::apply` for Instant transition
`725fb49` FlyTo + unknown-enum fallback tests
`6eeef22` `RenderState` DTO + enum string helpers
`ea3259a` `ViewState` + `PartialViewState` + validation
`2cd2f80` 6 public free functions (file + string API)
`c1b422a` `App::current_view_state` + `apply_view_state` (distributed state gather/scatter)
`10c130c` Frame-loop drain pending + publish snapshot
`b7ad676` Headless reproducibility (drain + skip auto-fit)
`fe4a80e` `ViewAction` variants + UI buttons
`f0cbd6f` rfd dialog dispatch (after egui frame)
`89b6eeb` `view_state_demo` example
`bbaa6e9` CHANGELOG + feature-status updates
`70fcfb4` Simplify pass: typed enums in RenderState, single-lock publish, private Context fields (−71 LOC)

Test Plan

  • `cargo test --workspace` — 248 passed, 2 ignored
  • `cargo clippy --workspace -- -D warnings` — zero warnings
  • `cargo fmt --check` — clean
  • Headless reproducibility integration test pins the auto-fit-skip behavior
  • Unit tests cover: roundtrip, snake_case enum serialization, version mismatch, missing-version, partial deserialization, validation rules (nonfinite fov, fov range, near≥far, zero ssaa, unknown enum strings), JSON shape via `serde_json::Value`
  • Manual visual check (please run before merging): `cargo run --release --example view_state_demo` — orbit camera → Save View → /tmp/v.json → move camera → Load View → verify smooth fly-to back to saved pose

Notes

  • /codex-review caught major state-ownership mistakes in V1 of the design (assumed Camera lived in Context — it doesn't; lives in RenderEngine). V2 introduces the snapshot/pending buffer pattern documented in the spec. All 10 codex "must fix" findings addressed.
  • /simplify pass removed 71 LOC after the initial implementation — typed enums replaced 5 helper-pair stringly-typed paths; single-lock publish replaced double lock acquisition per frame.
  • Public API of `GroundPlaneMode` and `TransparencyMode` enums gains `#[serde(rename_all = "snake_case")]`. No shipping consumer of those derives (pre-1.0).

🤖 Generated with Claude Code

xarthurx added 15 commits May 19, 2026 12:05
…h, private buffers

Applies findings from /simplify code review:

- Replace `mode: String` with typed `GroundPlaneMode` / `TransparencyMode`
  enums in RenderState DTOs. Adds `#[serde(rename_all = "snake_case")]` to
  both enum derives. Removes parse_*/_name helper pairs and KNOWN_GP/KNOWN_TR
  validation lists (~50 lines).
- `#[derive(Default)]` on the four Partial* DTOs collapses the merge()
  function's None-literal placeholders (~25 lines).
- Single-lock per-frame publish: extract `App::view_state_from_options`
  taking borrowed Options, called inside one with_context_mut closure.
  Removes the double lock acquisition from the per-frame path.
- `apply_view_state` now only sets `camera_fitted = true` when an engine
  exists — prevents silent disabling of future auto-fit before engine init.
- Context buffer fields are now fully private; access via `set_*`/`take_*`/
  reader accessors + test-only `clear_view_buffers`/`pending_view_apply`.
- Removed stale `#[allow(dead_code)]` annotations from App methods.
- Removed redundant `App::current_view_state` (public callers use the free
  function in core; internal callers use `view_state_from_options` directly).
Copilot AI review requested due to automatic review settings May 19, 2026 12:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a versioned JSON save/restore mechanism for the current “view” (camera pose + render-look settings) with both programmatic APIs and UI buttons, including support for deterministic headless rendering.

Changes:

  • Introduces ViewState / RenderState / ViewTransition DTOs plus save_view_* / load_view_* APIs backed by Context snapshot + pending-apply buffers.
  • Wires App frame loop + headless rendering to drain queued view applies and publish per-frame view snapshots.
  • Adds UI “Save View…” / “Load View…” actions (native dialogs via rfd), integration tests, and an example demo.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
examples/view_state_demo.rs New example demonstrating save/load workflow via UI buttons.
docs/plans/2026-05-19-view-save-restore-implementation.md Detailed implementation plan for the feature.
docs/plans/2026-05-19-view-save-restore-design.md Design spec documenting ownership, JSON shape, and APIs.
docs/feature-status.md Marks view save/restore as completed and removes from planned work.
crates/polyscope/tests/view_state_headless.rs Headless integration test ensuring queued view state is honored (auto-fit skipped).
crates/polyscope/src/headless.rs Drains pending view apply in headless renders and publishes snapshot post-render.
crates/polyscope/src/app/view_state.rs App-side gather/apply of distributed view state (camera + render/look + options).
crates/polyscope/src/app/render.rs Drains pending view apply at frame start; publishes snapshot at frame end.
crates/polyscope/src/app/render_ui.rs Handles new ViewAction intents and opens rfd dialogs after egui passes.
crates/polyscope/src/app/mod.rs Registers the new view_state submodule.
crates/polyscope/Cargo.toml Adds rfd/serde_json deps and registers the new example.
crates/polyscope-ui/src/panels.rs Adds new ViewAction variants and Save/Load buttons in controls section.
crates/polyscope-render/Cargo.toml Adds serde (and serde_json) to support camera state serialization tests/DTO.
crates/polyscope-render/src/camera.rs Adds CameraState DTO + apply logic (Instant/FlyTo) + conversions to core DTO.
crates/polyscope-render/src/lib.rs Re-exports CameraState.
crates/polyscope-core/src/view_state.rs Core view-state DTOs, JSON/file APIs, partial merge, and validation.
crates/polyscope-core/src/state.rs Adds Context buffers + accessors for snapshot/pending apply.
crates/polyscope-core/src/ssao.rs Adds PartialEq to SsaoConfig for DTO/test comparisons.
crates/polyscope-core/src/options.rs Adds snake_case serde rename for TransparencyMode.
crates/polyscope-core/src/ground_plane.rs Adds snake_case serde rename for GroundPlaneMode.
crates/polyscope-core/src/error.rs Adds InvalidViewState and NoActiveView error variants + tests.
crates/polyscope-core/src/lib.rs Exposes view_state module and re-exports its public API.
CHANGELOG.md Documents the new view save/restore feature and APIs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

state.render.background_color[2],
);

self.ground_plane.mode = state.render.ground_plane.mode;
Comment on lines +297 to +306
let finite_arr = |a: &[f32; 3]| a.iter().all(|x| x.is_finite());
if !finite_arr(&s.camera.position) {
return invalid("camera.position has non-finite values");
}
if !finite_arr(&s.camera.target) {
return invalid("camera.target has non-finite values");
}
if !finite_arr(&s.camera.up) {
return invalid("camera.up has non-finite values");
}
Comment on lines 21 to 25
thiserror.workspace = true
bytemuck.workspace = true
serde = { workspace = true }
serde_json = { workspace = true }
rand = "0.8"
@xarthurx xarthurx merged commit bf47ed6 into main May 19, 2026
6 checks passed
@xarthurx xarthurx deleted the feat/view-save-restore branch May 19, 2026 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants