diff --git a/src/web/templates/index.html b/src/web/templates/index.html
index 5b516dd..102970e 100644
--- a/src/web/templates/index.html
+++ b/src/web/templates/index.html
@@ -661,98 +661,54 @@ function addStereocameras(scene) {
var deg2rad = Math.PI / 180;
var halfFov = hfov / 2;
- 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;
+ // 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);
- // Cast ray from camera ground pos, clip to court rect
- 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 rayCourtIntersect(angleDeg2) {
- var rad = angleDeg2 * deg2rad;
- var dx = Math.cos(rad);
- var dy = Math.sin(rad);
- 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);
+ 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;
+ }
}
- // 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);
- }
-
- // 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);
- 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);
- }
- }
-
- 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([
- fanOrigin, points[i], points[i + 1]
- ]);
- scene.add(new THREE.Mesh(triGeo, new THREE.MeshBasicMaterial({
- color: color, transparent: true, opacity: 0.4, side: THREE.DoubleSide
- })));
- }
-
- // 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();
}
}
- // cam0: rotated -28° (looks slightly toward -X)
- drawCourtCoverage(cam0x, -camAngle, 0x44aaff);
- // cam1: rotated +28° (looks slightly toward +X)
- drawCourtCoverage(cam1x, camAngle, 0xff44aa);
+ 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);
+ }
+ }
}
// ===================== Draw court lines =====================