diff --git a/src/web/templates/index.html b/src/web/templates/index.html
index e489635..8e8f9aa 100644
--- a/src/web/templates/index.html
+++ b/src/web/templates/index.html
@@ -661,62 +661,66 @@ function addStereocameras(scene) {
var deg2rad = Math.PI / 180;
var halfFov = hfov / 2;
- function buildCoveragePolygon(cx, angleDeg) {
+ function drawCoverage(cx, angleDeg, color) {
var centerAngle = 90 + angleDeg;
var leftAngle = centerAngle + halfFov;
var rightAngle = centerAngle - halfFov;
var ox = cx, oy = baseY;
- function rayToCourtEdge(aDeg) {
+ // Cast ray, return farthest point on court boundary
+ function castRay(aDeg) {
var rad = aDeg * deg2rad;
var dx = Math.cos(rad);
var dy = Math.sin(rad);
if (dy <= 0) return null;
- var tMax = 50;
- if (dy > 1e-9) tMax = Math.min(tMax, (courtMaxY - oy) / dy);
- if (dx > 1e-9) tMax = Math.min(tMax, (courtMaxX - ox) / dx);
- else if (dx < -1e-9) tMax = Math.min(tMax, (courtMinX - ox) / dx);
-
+ // t where ray enters court (Y=0)
var tEnter = (courtMinY - oy) / dy;
- if (tMax <= tEnter) return null;
- var t = Math.max(tEnter, 0.01);
- t = tMax;
- var px = ox + dx * t;
- var py = oy + dy * t;
+ // t where ray exits court
+ var tExit = (courtMaxY - oy) / dy;
+ // clip X bounds
+ if (dx > 1e-9) tExit = Math.min(tExit, (courtMaxX - ox) / dx);
+ else if (dx < -1e-9) tExit = Math.min(tExit, (courtMinX - ox) / dx);
+
+ if (tExit <= tEnter) return null;
+
+ var px = ox + dx * tExit;
+ var py = oy + dy * tExit;
px = Math.max(courtMinX, Math.min(courtMaxX, px));
py = Math.max(courtMinY, Math.min(courtMaxY, py));
return new THREE.Vector3(px, py, 0.015);
}
- var points = [];
+ // Camera position projected onto court near edge (Y=0)
+ var camOnCourt = new THREE.Vector3(
+ Math.max(courtMinX, Math.min(courtMaxX, cx)), courtMinY, 0.015
+ );
+
+ // Far edge points
+ var farPoints = [];
var steps = 64;
for (var i = 0; i <= steps; i++) {
var a = rightAngle + (leftAngle - rightAngle) * (i / steps);
- var pt = rayToCourtEdge(a);
- if (pt) points.push(pt);
+ var pt = castRay(a);
+ if (pt) farPoints.push(pt);
}
- return points;
- }
+ if (farPoints.length < 2) return;
- function drawCoverageFan(points, color) {
- if (points.length < 2) return;
- var origin = new THREE.Vector3(points[0].x, courtMinY, 0.015);
- for (var i = 0; i < points.length - 1; i++) {
+ // Draw triangle fan from camera court position to far edge
+ var mat = new THREE.MeshBasicMaterial({
+ color: color, transparent: true, opacity: 0.35,
+ side: THREE.DoubleSide, depthWrite: false
+ });
+ for (var i = 0; i < farPoints.length - 1; i++) {
var triGeo = new THREE.BufferGeometry().setFromPoints([
- origin, points[i], points[i + 1]
+ camOnCourt, farPoints[i], farPoints[i + 1]
]);
- scene.add(new THREE.Mesh(triGeo, new THREE.MeshBasicMaterial({
- color: color, transparent: true, opacity: 0.35, side: THREE.DoubleSide,
- depthWrite: false
- })));
+ scene.add(new THREE.Mesh(triGeo, mat));
}
}
- var poly0 = buildCoveragePolygon(cam0x, -camAngle);
- var poly1 = buildCoveragePolygon(cam1x, camAngle);
- drawCoverageFan(poly0, 0x4488ff); // blue
- drawCoverageFan(poly1, 0xff44aa); // pink
+ drawCoverage(cam0x, -camAngle, 0x4488ff); // blue
+ drawCoverage(cam1x, camAngle, 0xff44aa); // pink
// overlap blends to purple naturally
}