From fedf46670a0af35e86327363f88090c1dba4c9cf Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:31:39 +0700 Subject: [PATCH] fix: project camera FOV coverage directly onto court surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FOV clipped to court boundaries (0,0)-(13.4,6.1) - Colored overlay shows which court areas each camera covers - Dashed center line shows each camera's look direction - cam0 (blue) -28° from +Y, cam1 (pink) +28° from +Y Co-Authored-By: Claude Opus 4.6 --- src/web/templates/index.html | 143 +++++++++++++++++++++-------------- 1 file changed, 85 insertions(+), 58 deletions(-) diff --git a/src/web/templates/index.html b/src/web/templates/index.html index 24cae10..e0f4d8a 100644 --- a/src/web/templates/index.html +++ b/src/web/templates/index.html @@ -626,106 +626,133 @@ function initTrajectoryScene() { }); } -// ===================== Stereo camera rig with FOV projection ===================== +// ===================== Stereo camera rig with court coverage ===================== function addStereocameras(scene) { // Position: net line (X=6.7), 1m outside court edge (Y=-1), 1m height (Z=1) var baseX = 6.7, baseY = -1, baseZ = 1; var stereoGap = 0.06; // 6cm between cameras - var camAngle = 28; // degrees each camera is rotated outward + var camAngle = 28; // degrees each camera is rotated outward from straight +Y var hfov = 160; // horizontal FOV degrees - var cam0x = baseX - stereoGap / 2; // left cam - var cam1x = baseX + stereoGap / 2; // right cam + var cam0x = baseX - stereoGap / 2; + var cam1x = baseX + stereoGap / 2; - // Draw each camera - function drawCamBody(cx, cy, cz, color) { + // Small camera bodies + function drawCamBody(cx, color) { var body = new THREE.Mesh( new THREE.BoxGeometry(0.12, 0.08, 0.08), new THREE.MeshBasicMaterial({ color: color }) ); - body.position.set(cx, cy, cz); + body.position.set(cx, baseY, baseZ); scene.add(body); } + drawCamBody(cam0x, 0x44aaff); + drawCamBody(cam1x, 0xff44aa); - drawCamBody(cam0x, baseY, baseZ, 0x44aaff); // cam0 blue - drawCamBody(cam1x, baseY, baseZ, 0xff44aa); // cam1 pink - - // Pole/mount + // Pole var poleGeo = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(baseX, baseY, 0), new THREE.Vector3(baseX, baseY, baseZ) ]); scene.add(new THREE.Line(poleGeo, new THREE.LineBasicMaterial({ color: 0x666666 }))); - // FOV projection on ground plane (z=0.02) - // Base look direction is +Y (into the court) - // cam0 rotated -28° (toward -X), cam1 rotated +28° (toward +X) + // Court boundaries for clipping + var courtMinX = 0, courtMaxX = 13.4, courtMinY = 0, courtMaxY = 6.1; var deg2rad = Math.PI / 180; var halfFov = hfov / 2; - var projDist = 16; // ray length for projection - function drawFov(cx, cy, cz, angleDeg, color) { - var centerAngle = 90 - angleDeg; // 90° = +Y direction, offset by camera rotation + function drawCourtCoverage(cx, angleDeg, color) { + // Center direction: +Y (90°) rotated by angleDeg + var centerAngle = 90 + angleDeg; // +angle = toward +X, -angle = toward -X var leftAngle = centerAngle + halfFov; var rightAngle = centerAngle - halfFov; - // Project rays to ground: from (cx, cy, cz) in direction, find where z=0 - function rayToGround(angleDeg2) { + // Cast ray from camera ground pos, clip to court rect + var ox = cx, oy = baseY; + + function rayCourtIntersect(angleDeg2) { var rad = angleDeg2 * deg2rad; var dx = Math.cos(rad); var dy = Math.sin(rad); - // Extend ray to projDist in XY, at ground level - return new THREE.Vector3(cx + dx * projDist, cy + dy * projDist, 0.02); + if (Math.abs(dx) < 1e-9 && Math.abs(dy) < 1e-9) return null; + + var tMin = 0.01, tMax = 50; + // Clip to court Y bounds + if (dy > 1e-9) { + tMax = Math.min(tMax, (courtMaxY - oy) / dy); + } else if (dy < -1e-9) { + tMax = Math.min(tMax, (courtMinY - oy) / dy); + } + // Clip to court X bounds + if (dx > 1e-9) { + var tx = (courtMaxX - ox) / dx; + tMax = Math.min(tMax, tx); + } else if (dx < -1e-9) { + var tx = (courtMinX - ox) / dx; + tMax = Math.min(tMax, tx); + } + // Must enter court (Y >= 0) + if (dy > 1e-9) { + tMin = Math.max(tMin, (courtMinY - oy) / dy); + } else if (dy <= 0) { + return null; // ray goes away from court + } + + if (tMax <= tMin) return null; + return new THREE.Vector3(ox + dx * tMax, oy + dy * tMax, 0.02); } - var camPos = new THREE.Vector3(cx, cy, cz); - var groundCenter = new THREE.Vector3(cx, cy, 0.02); - - // Draw FOV edges - var edgeMat = new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0.6 }); - - var leftPt = rayToGround(leftAngle); - var rightPt = rayToGround(rightAngle); - var centerPt = rayToGround(centerAngle); - - // Edge lines from camera to ground projection - [leftPt, rightPt, centerPt].forEach(function(pt) { - var geo = new THREE.BufferGeometry().setFromPoints([camPos, pt]); - scene.add(new THREE.Line(geo, edgeMat)); - }); - - // Fill FOV area on ground with semi-transparent triangle fan - var fanPoints = [groundCenter.clone()]; - var steps = 24; + // Build coverage polygon on court surface + var points = []; + var origin = new THREE.Vector3(ox, Math.max(oy, courtMinY), 0.02); + var steps = 48; for (var i = 0; i <= steps; i++) { var a = rightAngle + (leftAngle - rightAngle) * (i / steps); - fanPoints.push(rayToGround(a)); + var pt = rayCourtIntersect(a); + if (pt) { + // Clamp to court bounds + pt.x = Math.max(courtMinX, Math.min(courtMaxX, pt.x)); + pt.y = Math.max(courtMinY, Math.min(courtMaxY, pt.y)); + points.push(pt); + } } - for (var i = 1; i < fanPoints.length - 1; i++) { + if (points.length < 2) return; + + // Draw coverage edge line on court + var edgeGeo = new THREE.BufferGeometry().setFromPoints(points); + scene.add(new THREE.Line(edgeGeo, new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0.7 }))); + + // Fill coverage area as triangle fan from camera ground position + var fanOrigin = new THREE.Vector3(ox, courtMinY, 0.02); + for (var i = 0; i < points.length - 1; i++) { var triGeo = new THREE.BufferGeometry().setFromPoints([ - fanPoints[0], fanPoints[i], fanPoints[i + 1] + fanOrigin, points[i], points[i + 1] ]); - var triMesh = new THREE.Mesh(triGeo, new THREE.MeshBasicMaterial({ - color: color, transparent: true, opacity: 0.08, side: THREE.DoubleSide - })); - scene.add(triMesh); + scene.add(new THREE.Mesh(triGeo, new THREE.MeshBasicMaterial({ + color: color, transparent: true, opacity: 0.12, side: THREE.DoubleSide + }))); } - // FOV arc line on ground - var arcPoints = []; - for (var i = 0; i <= steps; i++) { - var a = rightAngle + (leftAngle - rightAngle) * (i / steps); - arcPoints.push(rayToGround(a)); + // Center line on court + var centerPt = rayCourtIntersect(centerAngle); + if (centerPt) { + centerPt.x = Math.max(courtMinX, Math.min(courtMaxX, centerPt.x)); + centerPt.y = Math.max(courtMinY, Math.min(courtMaxY, centerPt.y)); + var clGeo = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(ox, courtMinY, 0.02), centerPt + ]); + scene.add(new THREE.Line(clGeo, new THREE.LineDashedMaterial({ + color: color, dashSize: 0.2, gapSize: 0.1 + }))); + scene.children[scene.children.length - 1].computeLineDistances(); } - var arcGeo = new THREE.BufferGeometry().setFromPoints(arcPoints); - scene.add(new THREE.Line(arcGeo, new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0.3 }))); } - // cam0: rotated -28° (looking slightly left / toward -X) - drawFov(cam0x, baseY, baseZ, -camAngle, 0x44aaff); - // cam1: rotated +28° (looking slightly right / toward +X) - drawFov(cam1x, baseY, baseZ, camAngle, 0xff44aa); + // cam0: rotated -28° (looks slightly toward -X) + drawCourtCoverage(cam0x, -camAngle, 0x44aaff); + // cam1: rotated +28° (looks slightly toward +X) + drawCourtCoverage(cam1x, camAngle, 0xff44aa); } // ===================== Draw court lines =====================