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 =====================