diff --git a/v2/crates/wifi-densepose-pointcloud/src/viewer.html b/v2/crates/wifi-densepose-pointcloud/src/viewer.html
index be142a49..2aea08c1 100644
--- a/v2/crates/wifi-densepose-pointcloud/src/viewer.html
+++ b/v2/crates/wifi-densepose-pointcloud/src/viewer.html
@@ -328,13 +328,15 @@
// We interpolate 6 splats per edge → ~8000 splats per face vs 478 vertices.
var FACE_EDGES = (typeof FACEMESH_TESSELATION !== "undefined") ? FACEMESH_TESSELATION : null;
- function faceMeshFrame() {
- if (faceMeshState !== "running" || !latestFaceLandmarks) return null;
+ // Push the user's face mesh point cloud into `splats` (no Foundation
+ // context — that is the demo path's responsibility). Used both as the
+ // demo subject AND as an overlay on top of live/remote backend data
+ // when the camera is enabled. Returns true if any splats were pushed.
+ function pushFaceSplats(splats) {
+ if (faceMeshState !== "running" || !latestFaceLandmarks) return false;
var lms = latestFaceLandmarks;
- var splats = [];
- var i, lm;
-
- // 1. Original 478 vertices — bright, slightly larger to anchor features
+ var i;
+ // 1. Original 478 vertices — bright anchor points for features.
for (i = 0; i < lms.length; i++) {
splats.push({
center: lmToCenter(lms[i]),
@@ -343,30 +345,36 @@
scale: [0.010, 0.010, 0.010]
});
}
-
- // 2. Edge interpolation — 6 splats per FACEMESH_TESSELATION edge
+ // 2. Edge interpolation — 6 splats per FACEMESH_TESSELATION edge.
if (FACE_EDGES) {
var edgeCount = FACE_EDGES.length;
var SAMPLES = 6;
- var e, a, b, t, f, ax, ay, az, bx, by, bz, cx, cy, cz;
+ var e, a, b, t, f;
for (e = 0; e < edgeCount; e += 2) {
a = lms[FACE_EDGES[e]];
b = lms[FACE_EDGES[e + 1]];
if (!a || !b) continue;
var aPos = lmToCenter(a);
var bPos = lmToCenter(b);
- ax = aPos[0]; ay = aPos[1]; az = aPos[2];
- bx = bPos[0]; by = bPos[1]; bz = bPos[2];
+ var ax = aPos[0], ay = aPos[1], az = aPos[2];
+ var bx = bPos[0], by = bPos[1], bz = bPos[2];
for (t = 1; t <= SAMPLES; t++) {
f = t / (SAMPLES + 1);
- cx = ax * (1 - f) + bx * f;
- cy = ay * (1 - f) + by * f;
- cz = az * (1 - f) + bz * f;
- pushFaceSplat(splats, [cx, cy, cz], 0.85);
+ pushFaceSplat(splats, [
+ ax * (1 - f) + bx * f,
+ ay * (1 - f) + by * f,
+ az * (1 - f) + bz * f
+ ], 0.85);
}
}
}
+ return true;
+ }
+ function faceMeshFrame() {
+ if (faceMeshState !== "running" || !latestFaceLandmarks) return null;
+ var splats = [];
+ pushFaceSplats(splats);
pushFoundationContext(splats);
demoFrameNum += 1;
return {
@@ -511,7 +519,23 @@
}
prevTimestamp = now;
lastFrame = data.frame;
- updateSplats(data.splats);
+
+ // Overlay browser face mesh on top of backend splats when both
+ // are active — lets visitors see their own face *plus* the
+ // ESP32-driven point cloud in the same scene. Demo mode (where
+ // data.source === "face-mesh") already includes the face, so
+ // we skip this branch there to avoid double-counting.
+ var rendered = data.splats;
+ var faceOverlay = false;
+ if (data.source !== "face-mesh"
+ && faceMeshState === "running"
+ && latestFaceLandmarks) {
+ rendered = data.splats.slice();
+ pushFaceSplats(rendered);
+ faceOverlay = true;
+ }
+ data._faceOverlay = faceOverlay;
+ updateSplats(rendered);
// Draw skeleton if available
var pipe = data.pipeline;
@@ -532,8 +556,12 @@
} else {
mode = '● DEMO Synthetic';
}
+ if (data._faceOverlay) {
+ mode += ' + face overlay';
+ }
+ var splatCount = rendered ? rendered.length : data.count;
var html = mode + "
"
- + "Splats: " + data.count + "
"
+ + "Splats: " + splatCount + "
"
+ "Frame: " + data.frame;
// CSI frame rate
@@ -588,18 +616,26 @@
}
} catch(e) {}
}
- // Wire the camera CTA: shown only when we'll be rendering the demo path
- // (auto-with-no-backend or explicit ?backend=demo). Hidden in live/remote.
+ // Wire the camera CTA. The camera is now overlay-able on every
+ // transport mode: in demo it IS the subject; in live/remote it
+ // overlays the backend splats so the visitor sees their face
+ // alongside the ESP32-driven point cloud.
(function wireCamCta() {
var btn = document.getElementById("cam-cta");
if (!btn) return;
- // Hide CTA when user explicitly required live data.
- if (requireLive || backendArg.startsWith("http")) {
+ if (requireLive) {
+ // Strict-live mode shows the offline panel — no camera UI.
btn.classList.add("hidden");
return;
}
+ // In remote mode, label the button as an overlay action.
+ if (backendArg.startsWith("http")) {
+ btn.textContent = "▶ Add face overlay";
+ }
btn.addEventListener("click", function() {
- btn.textContent = "Initializing the Vault…";
+ btn.textContent = backendArg.startsWith("http")
+ ? "Starting overlay…"
+ : "Initializing the Vault…";
startFaceMesh();
});
})();