Completes M18 on top of the WIP checkpoint (6a703c3). Three lanes (H/J/K) depend only on
what this freezes.
- synthetic.py: base testsrc2 saturation/brightness-muted so its magenta/cyan colour bars
don't swamp lane H's colour detector; the two moving markers stay the only detectable blobs.
- Frontend (step 6): friendTracks.js stub (never-throws, fx.js pattern) wired into main.js;
hidden #btn-friends + #friends-panel; state.tracks loaded when manifest.has_tracks.
- Tests (step 8): test_tracks_foundation.py (20) — DB helpers, /api/tracks routes, solve
degradation, manifest.has_tracks, frozen OOK protocol + encoders, track_truth shape, the
projection round-trip (rendered pixels back-project to 3D truth, worst err <1e-3 → lane H's
median<0.3 reachable by construction), and a real-ffmpeg render test proving marker A is the
dominant magenta blob after base-muting.
- CR-6: the two sanctioned M18 test-lock edits (test_api manifest key-set +has_tracks;
test_capsule bundle paths +api/tracks). Additive; flagged for integration3 ratification.
Suite: 14 pre-existing files sum to 189 (floor held) + 20 new = 209 passed, exit 0.
Frontend `npm run build` clean. Do NOT merge — coordinator reviews + merges (DIRECTIVES R6 #3).
Recovered by the coordinator session after the foundation3 agent's isolation worktree was
removed mid-run, leaving the work uncommitted; every file independently reviewed, full suite +
build re-run green, then committed. No work redone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
3.3 KiB
JavaScript
67 lines
3.3 KiB
JavaScript
// Global store: manifest, playhead (t_global), playing, rate, selected camera, audio source,
|
|
// plus the loaded poses / anchors / events and per-video live playback status. Single source
|
|
// of truth: the transport writes `tGlobal` every animation frame; every module reads it.
|
|
// Discrete changes (play/pause/seek/selection) go through the tiny pub/sub below.
|
|
|
|
// API origin/prefix. Local dev talks to the uvicorn server cross-origin on :8000 (default).
|
|
// A hosted build sets VITE_API_BASE to a same-origin path prefix (e.g. "/festifun") so every
|
|
// request — fetch, <video> src, PLY loader — goes through the reverse proxy under that path.
|
|
// Empty string = same-origin at root. See deploy/DEPLOY.md.
|
|
// Phase-5 contract #5: a RUNTIME override wins over the build-time value — the memory capsule
|
|
// (M17) injects `window.__F4D_API_BASE__ = ""` into its baked index.html so the same dist
|
|
// bundle turns zero-backend. `??` (not `||`): empty string is a valid value (same-origin).
|
|
export const API_BASE =
|
|
window.__F4D_API_BASE__ ?? import.meta.env.VITE_API_BASE ?? "http://localhost:8000";
|
|
|
|
export const state = {
|
|
apiBase: API_BASE,
|
|
|
|
// loaded from the API (see main.js boot)
|
|
manifest: null,
|
|
videos: [], // [{id, filename, url, duration_s, fps, width, height, offset_ms, drift_ppm}]
|
|
tGlobalMax: 0,
|
|
hasPoses: false,
|
|
hasSplat: false, // optional 3DGS splat at /api/splat (preferred over the point cloud)
|
|
capsule: false, // manifest.capsule (M17): static baked bundle => hide ALL write UI
|
|
poses: {}, // id -> [{frame_idx, t_video_s, q:[w,x,y,z], t:[x,y,z], intrinsics, registered}]
|
|
anchors: [], // [{id, label, x, y, z, color}]
|
|
events: [], // [{id, t_global_s, duration_s, event_type, confidence, description, source}]
|
|
hasTracks: false, // manifest.has_tracks (M18): any solved friend track => load + render tracks
|
|
tracks: [], // [{id, marker_key, label, color, points:[{t_global_s,x,y,z,quality,views}]}] (M18)
|
|
|
|
// transport / playback
|
|
tGlobal: 0,
|
|
playing: false,
|
|
rate: 1,
|
|
audioSourceId: null, // video id whose audio you hear (WebAudio soundtrack; fallback: unmuted <video>)
|
|
spatialAudio: false, // 🎧 every camera is a positional emitter at its 3D pose; listener = viewer cam
|
|
enabled: {}, // id -> bool (per-video enable; disabled videos pause + dim + drop from sync)
|
|
inRange: {}, // id -> bool (t_video within [0, duration] and enabled)
|
|
syncErrorMs: {}, // id -> number | null (currentTime - target, ms; null when out of range)
|
|
|
|
// 3D viewer
|
|
followCameraId: null, // video id the viewer camera is snapped to, or null for free roam
|
|
};
|
|
|
|
// --- tiny pub/sub for discrete events -------------------------------------------------------
|
|
const listeners = new Map();
|
|
|
|
/** Subscribe to an event; returns an unsubscribe fn. */
|
|
export function on(evt, fn) {
|
|
if (!listeners.has(evt)) listeners.set(evt, new Set());
|
|
listeners.get(evt).add(fn);
|
|
return () => listeners.get(evt)?.delete(fn);
|
|
}
|
|
|
|
/** Emit an event to all subscribers. */
|
|
export function emit(evt, payload) {
|
|
const set = listeners.get(evt);
|
|
if (set) for (const fn of [...set]) fn(payload);
|
|
}
|
|
|
|
/** The reference video (offset 0) if present, else the first video. Used as default audio source. */
|
|
export function referenceVideoId() {
|
|
const ref = state.videos.find((v) => (v.offset_ms ?? 0) === 0);
|
|
return ref ? ref.id : state.videos[0]?.id ?? null;
|
|
}
|