feat: editable camera params panel on Court tab with live 3D update

Position Y, Height, Stereo gap, Rotation angle, HFOV — all adjustable
in real time, 3D scene updates on change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruslan Bakiev
2026-03-07 12:10:16 +07:00
parent 4605ed1e2c
commit 99ccca3968

View File

@@ -213,6 +213,34 @@
height: 60px; height: 60px;
} }
/* Camera params panel */
.cam-params {
display: flex;
gap: 8px;
align-items: center;
padding: 4px 8px;
}
.param-row {
display: flex;
flex-direction: column;
gap: 2px;
}
.param-row label {
font-size: 10px;
color: #888;
white-space: nowrap;
}
.param-row input {
width: 60px;
padding: 3px 5px;
background: #1a1a2e;
border: 1px solid #333;
border-radius: 4px;
color: #4ecca3;
font-size: 12px;
text-align: center;
}
/* Event banner */ /* Event banner */
.event-banner { .event-banner {
display: none; display: none;
@@ -286,8 +314,30 @@
<div class="bottom-bar"> <div class="bottom-bar">
<div class="cam-thumb"><img id="court-cam1" alt="Camera 1"></div> <div class="cam-thumb"><img id="court-cam1" alt="Camera 1"></div>
<div class="cam-thumb"><img id="court-cam0" alt="Camera 0"></div> <div class="cam-thumb"><img id="court-cam0" alt="Camera 0"></div>
<div class="cam-params">
<div class="param-row">
<label>Pos Y (м от корта)</label>
<input type="number" id="paramPosY" value="1" step="0.1" min="0.1" max="5" onchange="updateCamParams()">
</div>
<div class="param-row">
<label>Pos Z (высота м)</label>
<input type="number" id="paramPosZ" value="1" step="0.1" min="0.1" max="5" onchange="updateCamParams()">
</div>
<div class="param-row">
<label>Stereo (см)</label>
<input type="number" id="paramStereo" value="6" step="1" min="1" max="30" onchange="updateCamParams()">
</div>
<div class="param-row">
<label>Разворот (°)</label>
<input type="number" id="paramAngle" value="15" step="1" min="0" max="45" onchange="updateCamParams()">
</div>
<div class="param-row">
<label>HFOV (°)</label>
<input type="number" id="paramHfov" value="128" step="1" min="30" max="180" onchange="updateCamParams()">
</div>
</div>
<div class="sidebar-panel"> <div class="sidebar-panel">
<button class="btn-calibrate" id="btnCalibrate" onclick="doCalibrate()">Calibrate Court</button> <button class="btn-calibrate" id="btnCalibrate" onclick="doCalibrate()">Calibrate</button>
<div class="calibrate-status" id="calStatus"> <div class="calibrate-status" id="calStatus">
<span id="calStatusText">Not calibrated</span> <span id="calStatusText">Not calibrated</span>
</div> </div>
@@ -507,7 +557,7 @@ function initCourtScene() {
courtScene.add(new THREE.AmbientLight(0xffffff, 0.8)); courtScene.add(new THREE.AmbientLight(0xffffff, 0.8));
// Physical camera marker: 1m from net, center, 1m height // Physical camera marker: 1m from net, center, 1m height
addStereocameras(courtScene); addStereocameras(courtScene, getCamParams());
// Load existing calibration cameras // Load existing calibration cameras
fetch('/api/calibration/data') fetch('/api/calibration/data')
@@ -608,7 +658,7 @@ function initTrajectoryScene() {
trajScene.add(new THREE.AmbientLight(0xffffff, 0.8)); trajScene.add(new THREE.AmbientLight(0xffffff, 0.8));
// Physical camera marker: 1m from net, center, 1m height // Physical camera marker: 1m from net, center, 1m height
addStereocameras(trajScene); addStereocameras(trajScene, getCamParams());
function animateTraj() { function animateTraj() {
requestAnimationFrame(animateTraj); requestAnimationFrame(animateTraj);
@@ -627,12 +677,26 @@ function initTrajectoryScene() {
} }
// ===================== Stereo camera rig with court coverage ===================== // ===================== Stereo camera rig with court coverage =====================
function addStereocameras(scene) { // Track camera overlay objects for live update
// Position: net line (X=6.7), 1m outside court edge (Y=-1), 1m height (Z=1) var camOverlayObjects = [];
var baseX = 6.7, baseY = -1, baseZ = 1;
var stereoGap = 0.06; // 6cm between cameras function addStereocameras(scene, params) {
var camAngle = 15; // degrees each camera is rotated outward from straight +Y params = params || {};
var hfov = 128; // horizontal FOV degrees (IMX219 160° diagonal, 4:3 sensor) var baseX = 6.7;
var baseY = -(params.posY || 1);
var baseZ = params.posZ || 1;
var stereoGap = (params.stereo || 6) / 100;
var camAngle = params.angle || 15;
var hfov = params.hfov || 128;
// Remove old overlay objects
camOverlayObjects.forEach(function(obj) { scene.remove(obj); });
camOverlayObjects = [];
function addObj(obj) {
scene.add(obj);
camOverlayObjects.push(obj);
}
var cam0x = baseX - stereoGap / 2; var cam0x = baseX - stereoGap / 2;
var cam1x = baseX + stereoGap / 2; var cam1x = baseX + stereoGap / 2;
@@ -644,7 +708,7 @@ function addStereocameras(scene) {
new THREE.MeshBasicMaterial({ color: color }) new THREE.MeshBasicMaterial({ color: color })
); );
body.position.set(cx, baseY, baseZ); body.position.set(cx, baseY, baseZ);
scene.add(body); addObj(body);
} }
drawCamBody(cam0x, 0x44aaff); drawCamBody(cam0x, 0x44aaff);
drawCamBody(cam1x, 0xff44aa); drawCamBody(cam1x, 0xff44aa);
@@ -654,7 +718,7 @@ function addStereocameras(scene) {
new THREE.Vector3(baseX, baseY, 0), new THREE.Vector3(baseX, baseY, 0),
new THREE.Vector3(baseX, baseY, baseZ) new THREE.Vector3(baseX, baseY, baseZ)
]); ]);
scene.add(new THREE.Line(poleGeo, new THREE.LineBasicMaterial({ color: 0x666666 }))); addObj(new THREE.Line(poleGeo, new THREE.LineBasicMaterial({ color: 0x666666 })));
// Court boundaries for clipping // Court boundaries for clipping
var courtMinX = 0, courtMaxX = 13.4, courtMinY = 0, courtMaxY = 6.1; var courtMinX = 0, courtMaxX = 13.4, courtMinY = 0, courtMaxY = 6.1;
@@ -713,7 +777,7 @@ function addStereocameras(scene) {
var triGeo = new THREE.BufferGeometry().setFromPoints([ var triGeo = new THREE.BufferGeometry().setFromPoints([
camOnCourt, farPoints[i], farPoints[i + 1] camOnCourt, farPoints[i], farPoints[i + 1]
]); ]);
scene.add(new THREE.Mesh(triGeo, mat)); addObj(new THREE.Mesh(triGeo, mat));
} }
} }
@@ -886,6 +950,23 @@ setInterval(function() {
} }
}).catch(function() {}); }).catch(function() {});
}, 1000); }, 1000);
// ===================== Camera params live update =====================
function getCamParams() {
return {
posY: parseFloat(document.getElementById('paramPosY').value) || 1,
posZ: parseFloat(document.getElementById('paramPosZ').value) || 1,
stereo: parseFloat(document.getElementById('paramStereo').value) || 6,
angle: parseFloat(document.getElementById('paramAngle').value) || 15,
hfov: parseFloat(document.getElementById('paramHfov').value) || 128
};
}
function updateCamParams() {
var p = getCamParams();
if (courtSceneInitialized) addStereocameras(courtScene, p);
if (trajSceneInitialized) addStereocameras(trajScene, p);
}
</script> </script>
</body> </body>
</html> </html>