Skip to content

Add replay chart v2 with ceiling/GF overlays and fullscreen (PRO-51) - #163

Merged
shaheinm merged 6 commits into
mainfrom
feature/pro-51-replay-chart-v2
Jun 11, 2026
Merged

shaheinm merged 6 commits into
mainfrom
feature/pro-51-replay-chart-v2

Conversation

@shaheinm

@shaheinm shaheinm commented Apr 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Phase 4 of the dive replay engine (PRO-23). Ports the animation work from closed PR #160 forward onto latest main now that PRO-60 (deco accuracy) and PRO-61 (actual-dive samples) have reshaped ReplayProfileSheet.

  • ReplayChartData precomputes depth, ceiling, GF99, SurfGF, gas-switch, and deco-stop data with O(1) scrub lookups.
  • ReplayChartFullscreenView presents the chart landscape with overlay toggle chips (Ceiling, GF99, SurfGF).
  • ReplayProfileSheet wires the chart into the existing parameter sheet.
  • planned_stops added to ProfileGenResult (Rust + UDL + Swift bindings) to surface the pass-1 stop schedule for chart band visualization.

Closes PRO-51. Supersedes #160 (closed due to ReplayProfileSheet conflicts from PRO-60/61). Completes the "Replay Complete" milestone and unblocks PRO-53 (Phase 5 comparison view).

Tests

  • Rust (2 new): test_planned_stops_surface_deco_dive, test_shallow_no_deco (extended) — ordering, duration, and sample-hold correspondence.
  • Swift (2 new): FFI round-trip for plannedStops on both shallow NDL (empty) and deep conservative-GF (non-empty) profiles.
  • Full suites green locally: Rust 303 pass, Swift 400 pass, SwiftLint 0 violations.

Test plan

  • Open a dive, Replay, configure params, Generate, verify stat cards + chart appear
  • Play, verify progressive drawing at selected speed (1x–60x)
  • Pause, drag slider, verify scrub
  • Change speed mid-animation
  • Reset clears to time 0
  • Fullscreen + landscape, overlay chips (Ceiling, GF99, SurfGF) toggle
  • Close fullscreen via X button and via swipe gesture, verify timer pauses
  • No-deco dive: no ceiling/stops, clean depth line
  • Deep deco dive with gas switches: stop bands + switch markers render correctly
  • Shallow dive: empty stop bands

Follow-ups

  • PRO-53 (Phase 5): side-by-side comparison of real vs simulated stats

Made with Cursor

Adds core/mutants.out/ and core/mutants.out.old/ to .gitignore so local
mutation-testing runs don't clutter git status.

Made-with: Cursor
Phase 4 of the dive replay engine (PRO-23). Ports the PR #160 animation
work forward onto latest main now that PRO-60 (deco accuracy) and PRO-61
(actual-dive samples) have reshaped ReplayProfileSheet.

- ReplayChartData precomputes depth, ceiling, GF99, SurfGF, gas-switch
  and deco-stop data with O(1) scrub lookups.
- ReplayChartFullscreenView presents the chart landscape with overlay
  toggle chips (Ceiling, GF99, SurfGF).
- ReplayProfileSheet wires the chart into the existing parameter sheet.
- planned_stops added to ProfileGenResult (Rust + UDL + Swift bindings)
  to surface the pass-1 stop schedule for chart band visualization.

Tests:
- Rust: planned_stops ordering, duration, and sample-hold correspondence
  (test_planned_stops_surface_deco_dive, test_shallow_no_deco extended).
- Swift: FFI round-trip for plannedStops on both shallow NDL (empty) and
  deep conservative-GF (non-empty) dive profiles.

Closes PRO-51. Supersedes #160 (closed due to ReplayProfileSheet
conflicts from PRO-60/61). Completes the Replay Complete milestone and
unblocks PRO-53 (Phase 5 comparison view).

Made-with: Cursor
@codecov

codecov Bot commented Apr 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.44444% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
core/src/deco/profile_generator.rs 94.44% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

…bundle id

Addresses second-opinion review on PR #163 (gpt-5.3-codex subagent):

1. Actual-dive mode now renders planned deco stop bands. The second
   engine call (planAscent: true) was dead code — ReplayChartData's
   actual-dive initializer hardcoded decoStopBands = []. Now accepts
   plannedStops: [DecoStop] and matches stops against sample depth with
   a 1.0 m tolerance (wider than synthetic's 0.5 m to accommodate real
   diver behavior). Uses the max-depth sample as the bottom-end proxy
   so descent pass-through at stop depths can't open spurious bands.

2. Parallelize dual deco engine call with `async let`. The two calls
   (overlay + planned) are independent; previously serialized, now run
   concurrently, roughly halving perceived latency on longer dives.

3. Defensive sort on samples in actual-dive ReplayChartData init. The
   O(1) scrub lookup and band matching rely on monotonic tSec; sort at
   entry protects against any out-of-order imports.

4. Revert PRODUCT_BUNDLE_IDENTIFIER to azlucis.Profundum. The change
   to com.azlucis.Profundum was accidental Xcode drift when saving
   project.pbxproj for the new Swift files — out of scope for PRO-51
   and would affect app signing/provisioning.

Made-with: Cursor
Cursor's Task tool supports subagents with explicit model overrides
(e.g. gpt-5.3-codex, claude-opus-4-7-thinking-medium). This provides
the independent-model perspective that step 8 asks for without the
MCP setup cost. External MCP not required.

Made-with: Cursor
@shaheinm

Copy link
Copy Markdown
Contributor Author

Second-opinion review (gpt-5.3-codex) — REQUEST-CHANGES → addressed in d6e6d47

Ran an independent review with a gpt-5.3-codex Cursor subagent instead of the Codex MCP (follow-up commit bac8b6a updates CLAUDE.md step 8 to reflect that multi-model subagents supersede the external MCP).

Findings → fixes

Blocker

  • Actual-dive mode ran the second deco engine call (planAscent: true) but the actual-dive ReplayChartData initializer hardcoded decoStopBands = [] — the planned stops were silently dropped before rendering.
    → Fixed: ReplayChartData(samples:decoResult:plannedStops:gasMixes:depthUnit:) now accepts the planned stops and matches them against sample depths (1.0 m tolerance, max-depth sample as bottom-end proxy).

Should fix

  • Dual engine calls were serialized inside a single detached task.
    → Fixed: async let + two detached calls; roughly halves latency on longer dives.
  • Actual-dive initializer assumed monotonic tSec but didn't enforce it (scrub + band matching use binary search).
    → Fixed: defensive sorted { $0.tSec < $1.tSec } at entry.
  • PRODUCT_BUNDLE_IDENTIFIER changed from azlucis.Profundum → com.azlucis.Profundum in project.pbxproj. This was accidental Xcode drift from saving the pbxproj for the new files, out of scope for PRO-51 (would affect signing/provisioning).
    → Fixed: reverted both occurrences.

Nits (not addressed in this PR)

  • ReplayChart.swift at ~1140 lines could be split into data vs. rendering. Worth a dedicated refactor ticket; not blocking.
  • Explicit VoiceOver hint on the accessible scrub slider — small, can follow up.

Mutation-testing note

  • core/mutants.out shows no missed profile_generator mutants but one timeout in generate_ascent (line 492). Worth tracking but not a regression introduced by this PR.

All local checks re-run: Rust 303/303 pass · Swift 400/400 pass · macOS build green · SwiftLint clean.

… unstick guard

- Sort actual-dive samples before passing to ReplayChartSection so
  nearestTSec binary search matches ReplayChartData's defensive sort
- extractResultData uses max tSec instead of unsorted .last for
  recorded total time
- Empty-params guard in generateFromActualSamples now resets
  isGenerating and surfaces an error instead of silently returning

Co-authored-by: Cursor <cursoragent@cursor.com>
@shaheinm

Copy link
Copy Markdown
Contributor Author

Second-opinion review round 2 (gpt-5.3-codex) — no blockers → addressed in acc7b29

Fresh independent review of the current head after the round-1 fixes. Confirmed all round-1 fixes are correct (planned stops wired in actual mode, parallel engine calls, defensive sort, bundle id intact).

Findings → fixes

Should fix

  • Actual-dive scrub readout: resultSummary passed unsorted samples.toSampleInputs() to ReplayChartSection, but nearestTSec binary-searches assuming monotonic tSec — out-of-order imports would get corrupted scrub values.
    → Fixed: sorted before mapping, matching ReplayChartData's defensive sort.
  • extractResultData used unsorted samples.last?.tSec for Recorded Time.
    → Fixed: samples.map(\.tSec).max().

Found in self-review

  • The empty-params guard in generateFromActualSamples returned silently after generate() set isGenerating = true, leaving the spinner stuck if ever hit.
    → Fixed: resets isGenerating and surfaces an error message.

Nit (not addressed)

  • Task.detached inside Task + async let is redundant; plain async let would be cleaner. Cosmetic, leaving as-is.

Local checks: SwiftLint clean, macOS build green (only pre-existing warnings in unrelated files).

@shaheinm
shaheinm marked this pull request as ready for review June 11, 2026 15:42
- fullScreenCover has no built-in interactive dismissal; add a downward
  drag gesture that pauses the animation and dismisses
- Reset path now requests portrait geometry and calls
  setNeedsUpdateOfSupportedInterfaceOrientations() — UIKit caches
  supported orientations, so the orientationLock reset alone was never
  picked up and rotation stayed locked after closing

Co-authored-by: Cursor <cursoragent@cursor.com>
@shaheinm
shaheinm merged commit 5f5cc10 into main Jun 11, 2026
8 checks passed
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.

1 participant