diff --git a/apps/viewer-web/e2e/world-cell-real-camera.spec.ts b/apps/viewer-web/e2e/world-cell-real-camera.spec.ts index 4275b58..8e38c65 100644 --- a/apps/viewer-web/e2e/world-cell-real-camera.spec.ts +++ b/apps/viewer-web/e2e/world-cell-real-camera.spec.ts @@ -143,6 +143,12 @@ test("official photographic views sustain authoritative World Cell geometry", as if (observation.done || observation.fixtureError) break; } await expect(page.locator("html")).toHaveAttribute("data-world-cell-sealed", "true", { timeout: 15_000 }); + await page.locator("#stop-button").click(); + await expect(page.locator("html")).toHaveAttribute("data-capture-active", "false"); + await expect(page.locator("html")).toHaveAttribute("data-source-frame-retained", "true"); + await expect(page.locator("html")).toHaveAttribute("data-reconstruction-visible", "true"); + await expect(page.locator("#capture-state")).toHaveText("RECONSTRUCTION READY"); + await expect(page.locator("#stage-message b")).toHaveText("RELATIVE RECONSTRUCTION READY"); const terminal = await page.locator("html").evaluate((node) => ({ surfels: Number(node.dataset.authoritativeSurfels ?? 0), @@ -188,14 +194,15 @@ test("official photographic views sustain authoritative World Cell geometry", as expect(maximumRevision).toBeGreaterThanOrEqual(3); expect(terminal.surfels).toBeGreaterThanOrEqual(2_000); expect(terminal.revision).toBeGreaterThanOrEqual(3); - expect(terminal.surfaceMode).toBe("relative-live-preview"); + expect(terminal.surfaceMode).toBe("relative-native-triangles"); expect(terminal.surfaceVertices).toBeGreaterThanOrEqual(300); expect(terminal.surfaceTriangles).toBeGreaterThanOrEqual(100); expect(terminal.surfaceMaximumRadius).toBe(0); expect(terminal.surfaceMaximumAngularRadius).toBe(0); expect(terminal.surfaceBuildMilliseconds).toBeLessThan(60); - expect(await page.locator("#camera").evaluate((video) => Number.parseFloat(getComputedStyle(video).opacity))).toBeGreaterThanOrEqual(0.99); - expect(await page.locator(".stage-panel").evaluate((stage) => stage.classList.contains("has-authoritative-surface"))).toBe(false); + expect(await page.locator("#camera").evaluate((video) => Number.parseFloat(getComputedStyle(video).opacity))).toBe(0); + expect(await page.locator("#retained-source-frame").evaluate((canvas) => Number.parseFloat(getComputedStyle(canvas).opacity))).toBeGreaterThanOrEqual(0.5); + expect(await page.locator(".stage-panel").evaluate((stage) => stage.classList.contains("has-authoritative-surface"))).toBe(true); expect(terminal.rootprint).toMatch(/^[0-9A-F]{16}$/u); expect(pageErrors).toEqual([]); }); diff --git a/apps/viewer-web/scripts/verify-live-release.mjs b/apps/viewer-web/scripts/verify-live-release.mjs index f472b19..f5aa883 100644 --- a/apps/viewer-web/scripts/verify-live-release.mjs +++ b/apps/viewer-web/scripts/verify-live-release.mjs @@ -97,6 +97,7 @@ if (keyxymManifest.schema !== "keyxym.browser-runtime-provenance/v11" || const legacyTheater = (await fetchBytes("world-cell-theater.html")).bytes.toString("utf8"); if (!/type="module"[^>]+src="\.\/assets\//u.test(legacyTheater) || + !legacyTheater.includes('id="retained-source-frame"') || legacyTheater.includes("/src/world-cell-authority-entry.ts")) { throw new Error("live legacy World Cell Theater route is not the built application entry"); } @@ -111,6 +112,7 @@ if (!canonicalInventory || canonicalRoute.bytes.byteLength !== canonicalInventor } if (!//u.test(canonicalTheater) || !/type="module"[^>]+src="\.\.\/assets\//u.test(canonicalTheater) || + !canonicalTheater.includes('id="retained-source-frame"') || canonicalTheater.includes("/src/world-cell-authority-entry.ts")) { throw new Error("live extensionless World Cell Theater route lacks its root asset contract"); } @@ -133,6 +135,8 @@ for (const marker of [ "keyxym-v26-reality-authority-spatial-surface-3", "native-triangles", "relative-live-preview", + "relative-native-triangles", + "RELATIVE RECONSTRUCTION READY", "tessaryn/spatial-calibration/v1", "Metric capture requires an exact browser media-frame identity", "Host-verified synchronized RGB-D", diff --git a/apps/viewer-web/src/world-cell-guidance.ts b/apps/viewer-web/src/world-cell-guidance.ts index 9c35c31..4068b5d 100644 --- a/apps/viewer-web/src/world-cell-guidance.ts +++ b/apps/viewer-web/src/world-cell-guidance.ts @@ -27,20 +27,21 @@ export function installWorldCellGuidance(): () => void { const update = () => { if (root.dataset.keyxymAuthority !== "verified") return; + if (root.dataset.captureActive !== "true") return; const authorityStage = root.dataset.authorityStage ?? "forming"; if (authorityStage !== "forming") { establishedTracking = true; return; } - if ((capture.textContent ?? "").trim() === "READY") return; - const mask = numericAttribute(root, "authorityRejectionMask"); if (mask & REJECT_REPROJECTION) { heading.textContent = "REDUCE MOTION BLUR"; detail.textContent = "Move more slowly, keep textured edges sharp, and maintain the same objects in view until reprojection returns below the authority limit."; } else if (mask & MOTION_REJECTIONS) { - heading.textContent = establishedTracking ? "REACQUIRE TRACKING" : "CREATE 3D PARALLAX"; - detail.textContent = "Move slowly sideways around textured objects at different depths. Keep them in view; avoid flat walls, digital screens, blur, and repeating patterns."; + heading.textContent = establishedTracking ? "RELOCALIZING / MAP RETAINED" : "CREATE 3D PARALLAX"; + detail.textContent = establishedTracking + ? "The accumulated reconstruction is intact. Move slowly back toward a textured view already observed, then continue sideways." + : "Move slowly sideways around textured objects at different depths. Keep them in view; avoid flat walls, digital screens, blur, and repeating patterns."; } else if (mask & (REJECT_GEOMETRY | REJECT_CONTINUITY)) { heading.textContent = "BUILD VERIFIED GEOMETRY"; detail.textContent = "Continue a slow arc, then revisit the same surfaces so Keyxym can confirm them across frames before enabling a Moment."; diff --git a/apps/viewer-web/src/world-cell-theater-v26.ts b/apps/viewer-web/src/world-cell-theater-v26.ts index fe94b9d..ddf458e 100644 --- a/apps/viewer-web/src/world-cell-theater-v26.ts +++ b/apps/viewer-web/src/world-cell-theater-v26.ts @@ -243,6 +243,7 @@ function validateMoment(moment: MomentRecord, expectedParent: string): void { class TheaterController { private readonly video = element("camera"); + private readonly retainedSourceFrame = element("retained-source-frame"); private readonly canvas = element("stage"); private readonly renderer = new THREE.WebGLRenderer({ canvas: this.canvas, @@ -283,6 +284,8 @@ class TheaterController { private frameNumber = 0; private geometrySnapshot: KeyxymGeometrySnapshot = { revision: 0n, surfels: [] }; private surfaceSnapshot: KeyxymSurfaceSnapshot = { revision: 0n, vertices: [] }; + private renderedSurfelCount = 0; + private renderedSurfaceTriangles = 0; private pose: KeyxymPoseEstimate | null = null; private quality: KeyxymQuality | null = null; private authority: KeyxymAuthorityDecision | null = null; @@ -348,6 +351,8 @@ class TheaterController { this.runtime = await KeyxymV26TheaterRuntime.load(this.manifest); this.bindControls(); document.documentElement.dataset.worldCellController = "keyxym-v026-worker-v1"; + document.documentElement.dataset.captureActive = "false"; + document.documentElement.dataset.reconstructionVisible = "false"; setText("backend-name", "KEYXYM V0.26 / REALITY"); setText("adapter-name", this.manifest.source_commit.slice(0, 12).toUpperCase()); setText("gpu-badge", "WORKER WASM READY"); @@ -442,10 +447,14 @@ class TheaterController { this.video.srcObject = this.mediaStream; await this.video.play(); this.running = true; + document.documentElement.dataset.captureActive = "true"; + document.documentElement.dataset.sourceFrameRetained = "false"; + this.canvas.closest(".stage-panel")?.classList.remove("has-retained-source"); this.lastProcessedAt = 0; element("start-button").disabled = true; element("stop-button").disabled = false; setText("capture-state", "FORMING"); + this.updatePresentationMode(); this.hideStageMessage(); await this.refreshSpatialCalibration(); this.scheduleFrame(); @@ -453,19 +462,43 @@ class TheaterController { private stopCamera(): void { this.running = false; + document.documentElement.dataset.captureActive = "false"; if (this.frameCallback && this.video.cancelVideoFrameCallback) this.video.cancelVideoFrameCallback(this.frameCallback); if (this.fallbackTimer) window.clearTimeout(this.fallbackTimer); this.frameCallback = 0; this.fallbackTimer = 0; + this.retainCurrentSourceFrame(); this.mediaStream?.getTracks().forEach((track) => track.stop()); this.mediaStream = null; - this.video.srcObject = null; + // Keep the ended stream attached so the last sensory frame remains a + // visible reference when no native geometry was defensible. Reset or a + // new capture replaces it explicitly; hardware tracks are already closed. + this.video.pause(); element("start-button").disabled = false; element("stop-button").disabled = true; - setText("capture-state", "READY"); + this.showStoppedResult(); this.updateControls(); } + private retainCurrentSourceFrame(): void { + const width = this.video.videoWidth; + const height = this.video.videoHeight; + if (width < 1 || height < 1) { + document.documentElement.dataset.sourceFrameRetained = "false"; + return; + } + this.retainedSourceFrame.width = width; + this.retainedSourceFrame.height = height; + const context = this.retainedSourceFrame.getContext("2d", { alpha: false }); + if (!context) { + document.documentElement.dataset.sourceFrameRetained = "false"; + return; + } + context.drawImage(this.video, 0, 0, width, height); + document.documentElement.dataset.sourceFrameRetained = "true"; + this.canvas.closest(".stage-panel")?.classList.add("has-retained-source"); + } + private scheduleFrame(): void { if (!this.running) return; if (this.video.requestVideoFrameCallback) { @@ -485,7 +518,8 @@ class TheaterController { try { await this.processFrame(now, frameIdentity); } catch (error) { - this.freezeOnTrackingFailure(error); + if (this.running) this.handleFrameFailure(error); + else this.showStoppedResult(); } finally { this.processing = false; } @@ -582,10 +616,12 @@ class TheaterController { } } } - if (previouslyRecovered && !result.pose.recovered) { - this.setStageMessage("TRACKING FROZEN", "Authoritative geometry is frozen. Return to a textured, previously observed view for relocalization.", true); - } else if (result.pose.recovered) { - this.hideStageMessage(); + if (this.running) { + if (previouslyRecovered && !result.pose.recovered) { + this.setStageMessage("RELOCALIZING", "The accumulated reconstruction is retained. Return to a textured, previously observed view to continue it.", true); + } else if (result.pose.recovered) { + this.hideStageMessage(); + } } document.documentElement.dataset.formingSamples = String(result.forming.length); document.documentElement.dataset.authoritativeSurfels = String(this.geometrySnapshot.surfels.length); @@ -596,6 +632,7 @@ class TheaterController { document.documentElement.dataset.sealAllowed = String(result.authority.sealAllowed); if (result.authority.momentAllowed) document.documentElement.dataset.everMomentReady = "true"; if (result.authority.sealAllowed) document.documentElement.dataset.everSealReady = "true"; + if (!this.running) this.showStoppedResult(); } private updateFormingCloud(samples: KeyxymFormingSample[], coverage: number): void { @@ -685,18 +722,9 @@ class TheaterController { this.authoritySurface.position.set(0, 0, 0); this.authoritySurface.scale.setScalar(1); const nativeTriangles = nativeSurface ? nativeVertices.length / 3 : 0; - const hasSurface = nativeTriangles >= 16; - const hasContinuum = nativeTriangles >= 64; - const metricContinuum = this.quality?.metricScale === true && hasContinuum; - this.authoritySurface.visible = metricContinuum; - this.authorityCloud.visible = this.quality?.metricScale === true && !hasSurface; - this.formingCloud.visible = this.quality?.metricScale === true && surfels.length < 1_024; - const stage = this.canvas.closest(".stage-panel"); - stage?.classList.toggle("has-authority", hasSurface); - stage?.classList.toggle("has-authoritative-surface", metricContinuum); - document.documentElement.dataset.surfaceMode = this.quality?.metricScale === true - ? (nativeSurface ? "native-triangles" : "metric-surfels-pending") - : "relative-live-preview"; + this.renderedSurfelCount = surfels.length; + this.renderedSurfaceTriangles = nativeTriangles; + this.updatePresentationMode(); document.documentElement.dataset.surfacePatches = "0"; document.documentElement.dataset.surfaceVertices = String(nativeSurface ? nativeVertices.length : 0); document.documentElement.dataset.surfaceTriangles = String(nativeTriangles); @@ -708,6 +736,44 @@ class TheaterController { setText("surfel-count", surfels.length.toLocaleString()); } + private updatePresentationMode(): void { + const metric = this.quality?.metricScale === true; + const hasSurface = this.renderedSurfaceTriangles >= 16; + const hasContinuum = this.renderedSurfaceTriangles >= 64; + const metricContinuum = metric && hasContinuum; + const relativeSurface = !metric && !this.running && hasSurface; + const relativePoints = !metric && !this.running && !hasSurface && this.renderedSurfelCount > 0; + const surfaceVisible = metricContinuum || relativeSurface; + const pointsVisible = (metric && !hasSurface && this.renderedSurfelCount > 0) || relativePoints; + const reconstructionVisible = surfaceVisible || pointsVisible; + + const surfaceMaterial = this.authoritySurface.material as THREE.MeshStandardMaterial; + if (surfaceMaterial.transparent !== relativeSurface) { + surfaceMaterial.transparent = relativeSurface; + surfaceMaterial.needsUpdate = true; + } + surfaceMaterial.opacity = relativeSurface ? 0.72 : 1; + surfaceMaterial.depthWrite = !relativeSurface; + this.authoritySurface.visible = surfaceVisible; + this.authorityCloud.visible = pointsVisible; + this.formingCloud.visible = this.running && metric && this.renderedSurfelCount < 1_024; + const stage = this.canvas.closest(".stage-panel"); + stage?.classList.toggle("has-authority", hasSurface || this.renderedSurfelCount > 0); + stage?.classList.toggle("has-authoritative-surface", surfaceVisible); + stage?.classList.toggle("has-metric-continuum", metricContinuum); + stage?.classList.toggle("has-relative-reconstruction", relativeSurface || relativePoints); + document.documentElement.dataset.reconstructionVisible = String(reconstructionVisible); + document.documentElement.dataset.surfaceMode = metric + ? (hasSurface ? "native-triangles" : "metric-surfels-pending") + : this.running + ? "relative-live-preview" + : hasSurface + ? "relative-native-triangles" + : relativePoints + ? "relative-native-surfels" + : "relative-source-frame"; + } + private updateQualityUi(): void { if (!this.pose || !this.quality || !this.authority) return; const labels: Record = { @@ -1091,15 +1157,42 @@ class TheaterController { element("send-button").disabled = !this.sealedCell || !this.channel || this.channel.readyState !== "open"; } - private freezeOnTrackingFailure(error: unknown): void { + private handleFrameFailure(error: unknown): void { const reason = error instanceof Error ? error.message : String(error); - setText("pose-state", "TRACKING FROZEN"); - setText("capture-state", "FROZEN"); - setText("cell-state", "WORLD CELL / AUTHORITY FROZEN"); - this.setStageMessage("TRACKING FROZEN", `${reason}. Authoritative geometry was not advanced.`, true); + console.error("World Cell frame rejected", error); + setText("pose-state", "FRAME REJECTED"); + setText("capture-state", "CAPTURE ACTIVE"); + this.setStageMessage("FRAME REJECTED", `${reason}. The accumulated reconstruction is intact; capture can continue.`, true); this.updateControls(); } + private showStoppedResult(): void { + this.updatePresentationMode(); + const metric = this.quality?.metricScale === true; + const reconstructionVisible = document.documentElement.dataset.reconstructionVisible === "true"; + if (reconstructionVisible) { + const title = metric ? "METRIC CONTINUUM READY" : "RELATIVE RECONSTRUCTION READY"; + setText("pose-state", "RECONSTRUCTION READY"); + setText("capture-state", "RECONSTRUCTION READY"); + this.setStageMessage( + title, + "Capture stopped. Native geometry remains visible over the retained sensory frame; drag to inspect it or restart the camera to extend it.", + true, + ); + } else { + setText("pose-state", "CAPTURE PAUSED"); + setText("capture-state", "PAUSED"); + this.setStageMessage( + "CAPTURE PAUSED", + "No native reconstruction was established. The final source frame is retained; restart and move slowly sideways around textured objects at different depths.", + true, + ); + } + if (this.sealedCell) { + setText("cell-state", `WORLD CELL / SEALED / ${this.sealedCell.scaleState.toUpperCase()} / ${this.sealedCell.moments.length} MOMENTS`); + } + } + private setStageMessage(title: string, detail: string, visible = true): void { const message = element("stage-message"); const heading = message.querySelector("b"); @@ -1167,6 +1260,9 @@ class TheaterController { private async reset(): Promise { this.stopCamera(); + this.video.srcObject = null; + this.retainedSourceFrame.width = 1; + this.retainedSourceFrame.height = 1; if (this.playTimer) window.clearInterval(this.playTimer); this.playTimer = 0; this.runtime?.destroy(); @@ -1176,6 +1272,8 @@ class TheaterController { this.analysisIntervalMs = 50; this.geometrySnapshot = { revision: 0n, surfels: [] }; this.surfaceSnapshot = { revision: 0n, vertices: [] }; + this.renderedSurfelCount = 0; + this.renderedSurfaceTriangles = 0; this.pose = null; this.quality = null; this.authority = null; this.receipts = null; this.sourceCommitment = ZERO_DIGEST; this.formingSamples = []; @@ -1191,7 +1289,13 @@ class TheaterController { setPointGeometry(this.formingGeometry, new Float32Array(), new Float32Array(), new Float32Array(), new Float32Array()); setPointGeometry(this.authorityGeometry, new Float32Array(), new Float32Array(), new Float32Array(), new Float32Array()); setSurfaceGeometry(this.authoritySurfaceGeometry, new Float32Array(), new Float32Array(), new Float32Array()); - this.canvas.closest(".stage-panel")?.classList.remove("has-authority", "has-authoritative-surface"); + this.canvas.closest(".stage-panel")?.classList.remove( + "has-authority", "has-authoritative-surface", "has-metric-continuum", "has-relative-reconstruction", + "has-retained-source", + ); + document.documentElement.dataset.captureActive = "false"; + document.documentElement.dataset.reconstructionVisible = "false"; + document.documentElement.dataset.sourceFrameRetained = "false"; delete document.documentElement.dataset.surfacePatches; delete document.documentElement.dataset.surfaceMode; delete document.documentElement.dataset.surfaceVertices; diff --git a/apps/viewer-web/src/world-cell-theater.css b/apps/viewer-web/src/world-cell-theater.css index 3d0a875..5bc2c4b 100644 --- a/apps/viewer-web/src/world-cell-theater.css +++ b/apps/viewer-web/src/world-cell-theater.css @@ -2,7 +2,12 @@ :root{color-scheme:dark;font-family:Inter,ui-sans-serif,system-ui;background:#05080d;color:#e6eef8}.theater-shell{min-height:100vh;background:radial-gradient(circle at 50% 20%,#102238 0,#07101b 34%,#05080d 70%);padding-bottom:58px}.theater-topbar{position:sticky;top:0;z-index:20}.theater-main{display:grid;grid-template-columns:minmax(0,1fr) 410px;gap:14px;padding:14px}.stage-panel{position:relative;min-height:calc(100vh - 114px);overflow:hidden;border:1px solid rgba(124,207,255,.22);border-radius:6px;background:linear-gradient(180deg,rgba(6,15,26,.96),rgba(3,7,12,.98));box-shadow:0 24px 80px rgba(0,0,0,.45)}#camera{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.16;filter:saturate(.6) contrast(1.2)}#stage{position:absolute;inset:0;width:100%;height:100%}.scan-overlay{position:absolute;inset:18px;pointer-events:none}.scan-overlay i{position:absolute;width:36px;height:36px;border-color:#61d8ff}.scan-overlay i:nth-child(1){left:0;top:0;border-left:1px solid;border-top:1px solid}.scan-overlay i:nth-child(2){right:0;top:0;border-right:1px solid;border-top:1px solid}.scan-overlay i:nth-child(3){left:0;bottom:0;border-left:1px solid;border-bottom:1px solid}.scan-overlay i:nth-child(4){right:0;bottom:0;border-right:1px solid;border-bottom:1px solid}.stage-hud{position:absolute;left:14px;right:14px;top:14px;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.stage-hud span{padding:9px 10px;background:rgba(5,10,17,.78);backdrop-filter:blur(18px);border:1px solid rgba(118,184,229,.16);border-radius:4px}.stage-hud small,.stage-hud b{display:block}.stage-hud small{font:600 9px/1.2 ui-monospace,monospace;color:#6f8398;letter-spacing:.12em}.stage-hud b{margin-top:4px;font:600 11px/1.2 ui-monospace,monospace;color:#dff6ff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stage-message{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);text-align:center;padding:20px 26px;background:rgba(4,9,15,.78);backdrop-filter:blur(20px);border:1px solid rgba(97,216,255,.18);border-radius:6px}.stage-message b,.stage-message span{display:block}.stage-message b{font:700 18px/1 ui-monospace,monospace;letter-spacing:.14em}.stage-message span{margin-top:9px;color:#91a4b6;font-size:13px}.instrument-stack{display:flex;flex-direction:column;gap:10px;max-height:calc(100vh - 114px);overflow:auto}.instrument-panel{padding:13px}.instrument-panel header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:11px}.instrument-panel header small,.instrument-panel header b{display:block}.instrument-panel header small{font:600 9px/1 ui-monospace,monospace;color:#718497;letter-spacing:.13em}.instrument-panel header b{margin-top:4px;font:650 12px/1.2 ui-monospace,monospace}.command-grid,.pair-actions{display:grid;grid-template-columns:1fr 1fr;gap:7px}.instrument-panel p{font-size:12px;line-height:1.5;color:#8fa1b3}.compute-meter,.transfer-track,.quality-meter{height:4px;background:#111d29;border-radius:999px;overflow:hidden}.compute-meter i,.transfer-track i,.quality-meter i{display:block;width:0;height:100%;background:linear-gradient(90deg,#4ac8ff,#d2b66e);transition:width .2s}.quality-meter i{background:linear-gradient(90deg,#c56b51,#d2b66e,#61d8ff)}.instrument-panel dl{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:11px 0 0}.instrument-panel dl div{padding:8px;background:rgba(5,9,14,.55);border:1px solid rgba(255,255,255,.05)}.instrument-panel dt{font:600 8px/1 ui-monospace,monospace;color:#64778b;letter-spacing:.12em}.instrument-panel dd{margin:5px 0 0;font:600 10px/1.3 ui-monospace,monospace;color:#d4e4ef;word-break:break-all}.metric-quality-panel{position:relative;overflow:hidden}.metric-quality-panel::after{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(135deg,transparent 60%,rgba(97,216,255,.035))}.quality-grid{grid-template-columns:repeat(4,1fr)!important}.quality-grid div:last-child{grid-column:span 2}.quality-grid dd{font-size:11px!important;color:#f1fbff!important}.timeline{display:flex;gap:6px;overflow:auto;padding:4px 0 8px}.timeline button{min-width:116px;padding:9px;text-align:left;background:#09121d;color:#b8c8d6;border:1px solid rgba(130,199,235,.12);border-radius:4px}.timeline button.active{border-color:#5ed7ff;color:#fff;box-shadow:inset 0 0 0 1px rgba(94,215,255,.22)}.timeline button small,.timeline button b{display:block}.timeline .empty{color:#6f8193;font-size:12px}.replay-controls{display:grid;grid-template-columns:36px 1fr 58px 36px;gap:6px;align-items:center}.replay-controls button{height:34px}.replay-controls input{width:100%}textarea{width:100%;min-height:118px;resize:vertical;margin-top:9px;padding:9px;background:#03070c;color:#9adfff;border:1px solid rgba(95,202,255,.18);border-radius:4px;font:10px/1.45 ui-monospace,monospace}.theater-controls{z-index:30}.theater-dialog{width:min(620px,calc(100vw - 24px));background:#07101a;color:#e7f2fa;border:1px solid rgba(102,205,255,.24);border-radius:6px;padding:0}.theater-dialog::backdrop{background:rgba(0,0,0,.76);backdrop-filter:blur(8px)}.theater-dialog header{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;border-bottom:1px solid rgba(255,255,255,.07)}.theater-dialog header small,.theater-dialog header b{display:block}.theater-dialog header small{font:600 9px ui-monospace,monospace;color:#71879a;letter-spacing:.13em}.theater-dialog header b{margin-top:4px;font:650 13px ui-monospace,monospace}.dialog-body{padding:14px}.dialog-body pre{white-space:pre-wrap;word-break:break-word;max-height:50vh;overflow:auto;padding:12px;background:#03070c;border:1px solid rgba(255,255,255,.06);font:11px/1.5 ui-monospace,monospace;color:#9fdcff}.metric-input-label{display:grid;gap:8px;margin:14px 0;color:#8da1b5;font:600 10px/1 ui-monospace,monospace;letter-spacing:.12em}.metric-input-label input{height:44px;padding:0 12px;background:#03070c;color:#e9f8ff;border:1px solid rgba(97,216,255,.24);border-radius:4px;font:600 16px ui-monospace,monospace}@media(max-width:980px){.theater-main{grid-template-columns:1fr}.stage-panel{min-height:62vh}.instrument-stack{max-height:none;overflow:visible}}@media(max-width:640px){.theater-main{padding:8px;gap:8px}.stage-panel{min-height:56vh}.stage-hud{grid-template-columns:1fr 1fr}.instrument-stack{gap:8px}.command-grid,.pair-actions{grid-template-columns:1fr}.quality-grid{grid-template-columns:1fr 1fr!important}.quality-grid div:last-child{grid-column:auto}.theater-controls{font-size:9px}.top-states span:nth-child(2){display:none}} .navigation-hint{position:absolute;left:50%;bottom:14px;z-index:4;transform:translateX(-50%);max-width:calc(100% - 28px);padding:7px 10px;white-space:nowrap;pointer-events:none;color:#87a4b8;background:rgba(3,8,14,.68);border:1px solid rgba(97,216,255,.14);border-radius:4px;backdrop-filter:blur(12px);font:600 9px/1.3 ui-monospace,monospace;letter-spacing:.08em} .stage-panel #camera{opacity:1;filter:saturate(.9) contrast(1.05);transition:opacity .35s ease} -.stage-panel.has-authoritative-surface #camera{opacity:0} +#retained-source-frame{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;filter:saturate(.75) contrast(1.08);transition:opacity .35s ease} +.stage-panel.has-retained-source #camera{opacity:0} +.stage-panel.has-retained-source #retained-source-frame{opacity:1} +.stage-panel.has-metric-continuum #camera{opacity:0} +.stage-panel.has-metric-continuum #retained-source-frame{opacity:0} +.stage-panel.has-relative-reconstruction #retained-source-frame{opacity:.56;filter:saturate(.5) contrast(1.12)} .stage-panel.has-authority .stage-message{top:auto;bottom:48px;max-width:min(520px,calc(100% - 72px));padding:10px 14px;transform:translateX(-50%);background:rgba(4,9,15,.9)} .stage-panel.has-authority .stage-message b{font-size:11px} .stage-panel.has-authority .stage-message span{margin-top:5px;font-size:10px} diff --git a/apps/viewer-web/tests/live-scan-release-gate.test.mjs b/apps/viewer-web/tests/live-scan-release-gate.test.mjs index 5adb20b..b972ec5 100644 --- a/apps/viewer-web/tests/live-scan-release-gate.test.mjs +++ b/apps/viewer-web/tests/live-scan-release-gate.test.mjs @@ -12,6 +12,9 @@ test("live release verification requires the calibrated spatial continuum and re assert.match(source, /48a9de27f8a212fabc2f4f72108109dad0fe166f1e81eef806da282f42aa6a85/); assert.match(source, /native-triangles/); assert.match(source, /relative-live-preview/); + assert.match(source, /relative-native-triangles/); + assert.match(source, /RELATIVE RECONSTRUCTION READY/); + assert.match(source, /retained-source-frame/); assert.match(source, /tessaryn\/spatial-calibration\/v1/); assert.match(source, /Metric capture requires an exact browser media-frame identity/); assert.match(source, /Host-verified synchronized RGB-D/); diff --git a/apps/viewer-web/tests/world-cell-consensus.test.mjs b/apps/viewer-web/tests/world-cell-consensus.test.mjs index de9d868..6971f17 100644 --- a/apps/viewer-web/tests/world-cell-consensus.test.mjs +++ b/apps/viewer-web/tests/world-cell-consensus.test.mjs @@ -57,6 +57,28 @@ test("Theater consumes native authority decisions and contains no pose solver", assert.doesNotMatch(source, /tracking\s*>=\s*0\.2|parallaxDegrees\s*>=\s*0\.3|confirmed\.length\s*>=\s*4/); }); +test("stopping capture retains the source frame and exposes native relative reconstruction", async () => { + const [html, source, guidance, styles] = await Promise.all([ + read("world-cell-theater.html"), + read("src/world-cell-theater-v26.ts"), + read("src/world-cell-guidance.ts"), + read("src/world-cell-theater.css"), + ]); + assert.match(html, /id="retained-source-frame"/); + assert.match(source, /retainCurrentSourceFrame/); + assert.match(source, /dataset\.captureActive = "false"/); + assert.match(source, /dataset\.reconstructionVisible/); + assert.match(source, /relative-native-triangles/); + assert.match(source, /RELATIVE RECONSTRUCTION READY/); + assert.match(source, /The accumulated reconstruction is intact/); + assert.doesNotMatch(source, /setText\("capture-state", "FROZEN"\)/); + assert.match(guidance, /dataset\.captureActive !== "true"/); + assert.match(guidance, /RELOCALIZING \/ MAP RETAINED/); + assert.doesNotMatch(guidance, /REACQUIRE TRACKING/); + assert.match(styles, /has-relative-reconstruction/); + assert.match(styles, /retained-source-frame/); +}); + test("forming observations cannot enter Moments or seals", async () => { const source = await read("src/world-cell-theater-v26.ts"); assert.match(source, /const geometry = confirmedGeometry\(this\.geometrySnapshot\.surfels\)/); diff --git a/apps/viewer-web/world-cell-theater.html b/apps/viewer-web/world-cell-theater.html index a7aed2e..7ba2354 100644 --- a/apps/viewer-web/world-cell-theater.html +++ b/apps/viewer-web/world-cell-theater.html @@ -22,6 +22,7 @@
+
diff --git a/docs/WORLD_CELL_CONTINUUM_RUNTIME.md b/docs/WORLD_CELL_CONTINUUM_RUNTIME.md index bc898ba..a78a0ce 100644 --- a/docs/WORLD_CELL_CONTINUUM_RUNTIME.md +++ b/docs/WORLD_CELL_CONTINUUM_RUNTIME.md @@ -33,6 +33,10 @@ camera RGBA + source digest Without the spatial adapter, Keyxym executes bounded monocular tracking and relative pose recovery. The Theater deliberately keeps the live camera visible instead of presenting a sparse relative point field as an inhabitable place. +When capture stops, it freezes the final sensory frame and renders the +accumulated native relative triangles over that frame. The recovered result is +therefore inspectable without representing relative geometry as metric depth or +discarding the participant's last recognizable observation. Pose tracks and surface samples are intentionally separate. Dense image samples cannot vote a weak camera model into existence. Once a pose is accepted, diff --git a/docs/WORLD_CELL_THEATER_RUNTIME_BOUNDARY.md b/docs/WORLD_CELL_THEATER_RUNTIME_BOUNDARY.md index e3be7c2..bc41165 100644 --- a/docs/WORLD_CELL_THEATER_RUNTIME_BOUNDARY.md +++ b/docs/WORLD_CELL_THEATER_RUNTIME_BOUNDARY.md @@ -26,3 +26,6 @@ Ordinary monocular capture remains useful as a live relative preview, but it does not render sparse points as a finished place and never claims metric geometry. WebUSB, WebSerial, WebXR availability, an IMU, or a typed reference length does not independently activate metric authority. +When capture stops, the final sensory frame remains visible beneath the native +relative triangle or surfel reconstruction. Relocalization and rejected-frame +messages cannot erase that accumulated result.