@@ -192,8 +192,10 @@ async fn index() -> Html<String> {
192192 <style>
193193 body { margin: 0; background: #0a0a0a; color: #e8a634; font-family: monospace; }
194194 canvas { display: block; }
195- #info { position: absolute; top: 10px; left: 10px; padding: 12px; background: rgba(0,0,0,0.85); border: 1px solid #e8a634; border-radius: 6px; min-width: 200px ; }
195+ #info { position: absolute; top: 10px; left: 10px; padding: 12px; background: rgba(0,0,0,0.85); border: 1px solid #e8a634; border-radius: 6px; min-width: 240px; font-size: 13px; line-height: 1.5 ; }
196196 .live { color: #4f4; } .demo { color: #f44; }
197+ .section { margin-top: 6px; padding-top: 6px; border-top: 1px solid #333; }
198+ .label { color: #888; }
197199 </style>
198200 <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
199201 <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
@@ -204,38 +206,171 @@ async fn index() -> Html<String> {
204206 <div id="stats">Loading...</div>
205207 </div>
206208 <script>
207- const scene = new THREE.Scene();
209+ var scene = new THREE.Scene();
208210 scene.background = new THREE.Color(0x0a0a0a);
209- const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 100);
210- camera.position.set(0, 0 , -2 );
211- camera.lookAt(0, 0, 3 );
211+ var camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 100);
212+ camera.position.set(0, 2 , -4 );
213+ camera.lookAt(0, 0, 2 );
212214
213- const renderer = new THREE.WebGLRenderer({ antialias: true });
215+ var renderer = new THREE.WebGLRenderer({ antialias: true });
214216 renderer.setSize(window.innerWidth, window.innerHeight);
215217 document.body.appendChild(renderer.domElement);
216218
217- const controls = new THREE.OrbitControls(camera, renderer.domElement);
219+ var controls = new THREE.OrbitControls(camera, renderer.domElement);
218220 controls.enableDamping = true;
219- controls.target.set(0, 0, 3 );
221+ controls.target.set(0, 0, 2 );
220222
221- let pointsMesh = null;
222- let lastFrame = -1;
223+ var pointsMesh = null;
224+ var lastFrame = -1;
225+ var skeletonGroup = null;
226+ var prevTimestamp = 0;
227+ var frameRateVal = 0;
228+
229+ // COCO skeleton connections: pairs of keypoint indices
230+ // 0=nose 1=leftEye 2=rightEye 3=leftEar 4=rightEar
231+ // 5=leftShoulder 6=rightShoulder 7=leftElbow 8=rightElbow
232+ // 9=leftWrist 10=rightWrist 11=leftHip 12=rightHip
233+ // 13=leftKnee 14=rightKnee 15=leftAnkle 16=rightAnkle
234+ var COCO_BONES = [
235+ [0,1],[0,2],[1,3],[2,4],
236+ [5,6],[5,7],[7,9],[6,8],[8,10],
237+ [5,11],[6,12],[11,12],
238+ [11,13],[13,15],[12,14],[14,16]
239+ ];
240+
241+ function clearSkeleton() {
242+ if (skeletonGroup) {
243+ scene.remove(skeletonGroup);
244+ skeletonGroup.traverse(function(obj) {
245+ if (obj.geometry) obj.geometry.dispose();
246+ if (obj.material) obj.material.dispose();
247+ });
248+ skeletonGroup = null;
249+ }
250+ }
251+
252+ function drawSkeleton(keypoints) {
253+ clearSkeleton();
254+ if (!keypoints || keypoints.length < 17) return;
255+ skeletonGroup = new THREE.Group();
256+
257+ // Map keypoints from [0,1] to scene coords
258+ // x: [-2, 2], y: [2, -2] (flip y), z: fixed at 2
259+ var sphereGeo = new THREE.SphereGeometry(0.04, 8, 8);
260+ var sphereMat = new THREE.MeshBasicMaterial({ color: 0xffff00 });
261+ var positions3D = [];
262+ var i, kp, sx, sy;
263+ for (i = 0; i < 17; i++) {
264+ kp = keypoints[i];
265+ if (!kp) { positions3D.push(null); continue; }
266+ sx = (kp[0] - 0.5) * 4;
267+ sy = (0.5 - kp[1]) * 4;
268+ positions3D.push([sx, sy, 2]);
269+ var sphere = new THREE.Mesh(sphereGeo, sphereMat);
270+ sphere.position.set(sx, sy, 2);
271+ skeletonGroup.add(sphere);
272+ }
273+
274+ // Draw bones as white lines
275+ var lineMat = new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 2 });
276+ var b, a, bIdx;
277+ for (b = 0; b < COCO_BONES.length; b++) {
278+ a = COCO_BONES[b][0];
279+ bIdx = COCO_BONES[b][1];
280+ if (!positions3D[a] || !positions3D[bIdx]) continue;
281+ var lineGeo = new THREE.BufferGeometry();
282+ var verts = new Float32Array([
283+ positions3D[a][0], positions3D[a][1], positions3D[a][2],
284+ positions3D[bIdx][0], positions3D[bIdx][1], positions3D[bIdx][2]
285+ ]);
286+ lineGeo.setAttribute("position", new THREE.BufferAttribute(verts, 3));
287+ var line = new THREE.Line(lineGeo, lineMat);
288+ skeletonGroup.add(line);
289+ }
290+
291+ scene.add(skeletonGroup);
292+ }
223293
224294 async function fetchCloud() {
225295 try {
226- const resp = await fetch(' /api/splats' );
227- const data = await resp.json();
296+ var resp = await fetch(" /api/splats" );
297+ var data = await resp.json();
228298 if (data.splats && data.frame !== lastFrame) {
299+ // Compute CSI frame rate
300+ var now = Date.now();
301+ if (prevTimestamp > 0) {
302+ var dt = (now - prevTimestamp) / 1000.0;
303+ if (dt > 0) frameRateVal = (1.0 / dt).toFixed(1);
304+ }
305+ prevTimestamp = now;
229306 lastFrame = data.frame;
230307 updateSplats(data.splats);
231- const mode = data.live ? '<span class="live">● LIVE</span>' : '<span class="demo">● DEMO</span>';
232- let csiInfo = '';
233- if (data.csi) {
234- const m = (data.csi.motion * 100).toFixed(0);
235- csiInfo = `<br>CSI: ${data.csi.frames} frames, motion ${m}%<br>Distance: ${data.csi.distance_m.toFixed(1)}m`;
308+
309+ // Draw skeleton if available
310+ var pipe = data.pipeline;
311+ if (pipe && pipe.skeleton && pipe.skeleton.keypoints) {
312+ drawSkeleton(pipe.skeleton.keypoints);
313+ } else {
314+ clearSkeleton();
236315 }
237- document.getElementById('stats').innerHTML =
238- `${mode} Camera + CSI<br>Splats: ${data.count}<br>Frame: ${data.frame}${csiInfo}`;
316+
317+ // Build info panel
318+ var mode = data.live
319+ ? '<span class="live">● LIVE</span>'
320+ : '<span class="demo">● DEMO</span>';
321+ var html = mode + " Camera + CSI<br>"
322+ + "Splats: " + data.count + "<br>"
323+ + "Frame: " + data.frame;
324+
325+ // CSI frame rate
326+ html += '<div class="section">'
327+ + '<span class="label">CSI Rate:</span> '
328+ + frameRateVal + " fps</div>";
329+
330+ // Skeleton confidence
331+ if (pipe && pipe.skeleton && pipe.skeleton.confidence !== undefined) {
332+ var conf = (pipe.skeleton.confidence * 100).toFixed(0);
333+ html += '<div class="section">'
334+ + '<span class="label">Skeleton:</span> '
335+ + conf + "% confidence</div>";
336+ }
337+
338+ // Weather data
339+ if (pipe && pipe.weather) {
340+ var w = pipe.weather;
341+ html += '<div class="section">'
342+ + '<span class="label">Weather:</span> ';
343+ if (w.temperature !== undefined) {
344+ html += w.temperature + "°C";
345+ }
346+ if (w.conditions) {
347+ html += " " + w.conditions;
348+ }
349+ html += "</div>";
350+ }
351+
352+ // Building count from geo
353+ if (pipe && pipe.geo && pipe.geo.building_count !== undefined) {
354+ html += '<div class="section">'
355+ + '<span class="label">Buildings:</span> '
356+ + pipe.geo.building_count + "</div>";
357+ }
358+
359+ // Vitals
360+ if (pipe && pipe.vitals) {
361+ var v = pipe.vitals;
362+ html += '<div class="section">'
363+ + '<span class="label">Vitals:</span> ';
364+ if (v.breathing_rate !== undefined) {
365+ html += "BR " + v.breathing_rate + "/min";
366+ }
367+ if (v.motion_score !== undefined) {
368+ html += " Motion " + (v.motion_score * 100).toFixed(0) + "%";
369+ }
370+ html += "</div>";
371+ }
372+
373+ document.getElementById("stats").innerHTML = html;
239374 }
240375 } catch(e) {}
241376 }
@@ -244,21 +379,23 @@ async fn index() -> Html<String> {
244379
245380 function updateSplats(splats) {
246381 if (pointsMesh) scene.remove(pointsMesh);
247- const geometry = new THREE.BufferGeometry();
248- const positions = new Float32Array(splats.length * 3);
249- const colors = new Float32Array(splats.length * 3);
250- splats.forEach((s, i) => {
382+ var geometry = new THREE.BufferGeometry();
383+ var positions = new Float32Array(splats.length * 3);
384+ var colors = new Float32Array(splats.length * 3);
385+ var i, s;
386+ for (i = 0; i < splats.length; i++) {
387+ s = splats[i];
251388 positions[i*3] = s.center[0];
252389 positions[i*3+1] = -s.center[1];
253390 positions[i*3+2] = s.center[2];
254391 colors[i*3] = s.color[0];
255392 colors[i*3+1] = s.color[1];
256393 colors[i*3+2] = s.color[2];
257- });
258- geometry.setAttribute(' position' , new THREE.BufferAttribute(positions, 3));
259- geometry.setAttribute(' color' , new THREE.BufferAttribute(colors, 3));
394+ }
395+ geometry.setAttribute(" position" , new THREE.BufferAttribute(positions, 3));
396+ geometry.setAttribute(" color" , new THREE.BufferAttribute(colors, 3));
260397 pointsMesh = new THREE.Points(geometry, new THREE.PointsMaterial({
261- size: 0.025 , vertexColors: true, sizeAttenuation: true,
398+ size: 0.02 , vertexColors: true, sizeAttenuation: true
262399 }));
263400 scene.add(pointsMesh);
264401 }
@@ -269,7 +406,7 @@ async fn index() -> Html<String> {
269406 renderer.render(scene, camera);
270407 }
271408 animate();
272- window.addEventListener(' resize', () => {
409+ window.addEventListener(" resize", function() {
273410 camera.aspect = window.innerWidth / window.innerHeight;
274411 camera.updateProjectionMatrix();
275412 renderer.setSize(window.innerWidth, window.innerHeight);
0 commit comments