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 }