diff --git a/src/web/templates/index.html b/src/web/templates/index.html
index 102970e..e489635 100644
--- a/src/web/templates/index.html
+++ b/src/web/templates/index.html
@@ -631,7 +631,7 @@ 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 from straight +Y
+ var camAngle = 15; // degrees each camera is rotated outward from straight +Y
var hfov = 160; // horizontal FOV degrees
var cam0x = baseX - stereoGap / 2;
@@ -661,54 +661,63 @@ function addStereocameras(scene) {
var deg2rad = Math.PI / 180;
var halfFov = hfov / 2;
- // Rasterize coverage onto a grid, then draw colored quads
- var gridRes = 0.2; // 20cm per cell
- var cols = Math.ceil((courtMaxX - courtMinX) / gridRes);
- var rows = Math.ceil((courtMaxY - courtMinY) / gridRes);
- // 0 = no coverage, 1 = cam0 only, 2 = cam1 only, 3 = both
- var grid = new Array(cols * rows).fill(0);
+ function buildCoveragePolygon(cx, angleDeg) {
+ var centerAngle = 90 + angleDeg;
+ var leftAngle = centerAngle + halfFov;
+ var rightAngle = centerAngle - halfFov;
+ var ox = cx, oy = baseY;
- function markCoverage(cx, angleDeg, bit) {
- var centerAngle = (90 + angleDeg) * deg2rad;
- var halfFovRad = halfFov * deg2rad;
- var leftAngle = centerAngle + halfFovRad;
- var rightAngle = centerAngle - halfFovRad;
+ function rayToCourtEdge(aDeg) {
+ var rad = aDeg * deg2rad;
+ var dx = Math.cos(rad);
+ var dy = Math.sin(rad);
+ if (dy <= 0) return null;
- for (var r = 0; r < rows; r++) {
- for (var c = 0; c < cols; c++) {
- var cellX = courtMinX + (c + 0.5) * gridRes;
- var cellY = courtMinY + (r + 0.5) * gridRes;
- var dx = cellX - cx;
- var dy = cellY - baseY;
- var angle = Math.atan2(dy, dx);
- if (angle >= rightAngle && angle <= leftAngle) {
- grid[r * cols + c] |= bit;
- }
- }
+ 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);
+
+ 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;
+ 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 = [];
+ 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);
+ }
+ return points;
+ }
+
+ 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++) {
+ var triGeo = new THREE.BufferGeometry().setFromPoints([
+ origin, points[i], points[i + 1]
+ ]);
+ scene.add(new THREE.Mesh(triGeo, new THREE.MeshBasicMaterial({
+ color: color, transparent: true, opacity: 0.35, side: THREE.DoubleSide,
+ depthWrite: false
+ })));
}
}
- markCoverage(cam0x, -camAngle, 1);
- markCoverage(cam1x, camAngle, 2);
-
- // Colors: cam0 only = blue, cam1 only = pink, overlap = purple
- var colors = { 1: 0x4488ff, 2: 0xff44aa, 3: 0xaa44ff };
- var opacities = { 1: 0.35, 2: 0.35, 3: 0.5 };
-
- for (var r = 0; r < rows; r++) {
- for (var c = 0; c < cols; c++) {
- var val = grid[r * cols + c];
- if (val === 0) continue;
- var x1 = courtMinX + c * gridRes;
- var y1 = courtMinY + r * gridRes;
- var quad = new THREE.PlaneGeometry(gridRes, gridRes);
- var mesh = new THREE.Mesh(quad, new THREE.MeshBasicMaterial({
- color: colors[val], transparent: true, opacity: opacities[val], side: THREE.DoubleSide
- }));
- mesh.position.set(x1 + gridRes / 2, y1 + gridRes / 2, 0.015);
- scene.add(mesh);
- }
- }
+ var poly0 = buildCoveragePolygon(cam0x, -camAngle);
+ var poly1 = buildCoveragePolygon(cam1x, camAngle);
+ drawCoverageFan(poly0, 0x4488ff); // blue
+ drawCoverageFan(poly1, 0xff44aa); // pink
+ // overlap blends to purple naturally
}
// ===================== Draw court lines =====================