Calibration tab: split layout — 3D left, debug images right

- Split view: 3D scene on left, camera debug images + controls on right
- Debug images show detected court quad + computed camera XYZ
- Camera positions displayed as colored cones with look-direction lines on 3D scene
- Cameras cleared and redrawn on re-calibration (no duplicates)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruslan Bakiev
2026-03-22 14:44:11 +07:00
parent ba70200353
commit 2a7e285e70

View File

@@ -293,6 +293,69 @@
.info-item .label { color: #666; }
.info-item .value { color: #4ecca3; font-weight: 600; margin-left: 4px; }
/* Calibration split layout */
.cal-split {
display: flex;
height: calc(100vh - 48px);
}
.cal-left {
flex: 1;
min-width: 0;
}
.cal-left .viewport-3d {
width: 100%;
height: 100%;
border-top: none;
}
.cal-right {
width: 360px;
flex-shrink: 0;
display: flex;
flex-direction: column;
border-left: 1px solid #2a2a4a;
background: #0d0d20;
overflow-y: auto;
}
.cal-controls {
padding: 16px;
border-bottom: 1px solid #2a2a4a;
}
.cal-debug-images {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
}
.cal-debug-card {
position: relative;
}
.cal-debug-card img {
width: 100%;
border-radius: 4px;
border: 1px solid #333;
display: block;
}
.cal-debug-card .cal-live {
opacity: 0.5;
}
.cal-debug-card img[src=""],
.cal-debug-card img:not([src]) {
display: none;
}
.cal-debug-label {
position: absolute;
top: 4px;
left: 4px;
background: rgba(0,0,0,0.7);
color: #4ecca3;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
z-index: 1;
}
</style>
</head>
<body>
@@ -324,28 +387,36 @@
</div>
</div>
<!-- Tab 2: Calibration — 3D main, cameras small bottom center -->
<!-- Tab 2: Calibration — split: 3D left, debug right -->
<div class="tab-content" id="tab-calibration">
<div class="tab-full">
<div class="viewport-3d" id="calibration-3d"></div>
<div class="bottom-bar">
<div class="bottom-card"><img id="cal-cam1" alt="Camera 1"></div>
<div class="bottom-card"><img id="cal-cam0" alt="Camera 0"></div>
<div class="cam-card" style="width:auto;min-width:160px">
<div class="cc-title">Calibration</div>
<button class="btn-calibrate" id="btnCalibrate" onclick="doCalibrate()">Calibrate</button>
<div class="cal-split">
<div class="cal-left">
<div class="viewport-3d" id="calibration-3d"></div>
</div>
<div class="cal-right">
<div class="cal-controls">
<button class="btn-calibrate" id="btnCalibrate" onclick="doCalibrate()" style="padding:8px 24px;font-size:14px">Calibrate</button>
<div class="calibrate-status" id="calStatus">
<span id="calStatusText">Not calibrated</span>
</div>
<div id="calError" style="color:#ff4444;font-size:8px;word-break:break-all;display:none"></div>
<div id="calPositions" style="display:none">
<div class="cc-divider"></div>
<div class="cc-item" id="calPos0" style="color:#4ecca3;font-size:8px"></div>
<div class="cc-item" id="calPos1" style="color:#ff88cc;font-size:8px"></div>
<div id="calError" style="color:#ff4444;font-size:11px;word-break:break-all;display:none;margin-top:4px"></div>
<div id="calPositions" style="display:none;margin-top:6px;font-size:11px">
<div id="calPos0" style="color:#4ecca3"></div>
<div id="calPos1" style="color:#ff88cc"></div>
</div>
</div>
<div class="cal-debug-images">
<div class="cal-debug-card">
<div class="cal-debug-label">CAM 0</div>
<img id="calDebugImg0" alt="CAM 0">
<img id="cal-cam0" class="cal-live" alt="CAM 0 live">
</div>
<div class="cal-debug-card">
<div class="cal-debug-label">CAM 1</div>
<img id="calDebugImg1" alt="CAM 1">
<img id="cal-cam1" class="cal-live" alt="CAM 1 live">
</div>
</div>
<div class="bottom-card" id="calDebug0" style="display:none"><img id="calDebugImg0" alt="CAM 0 debug"></div>
<div class="bottom-card" id="calDebug1" style="display:none"><img id="calDebugImg1" alt="CAM 1 debug"></div>
</div>
</div>
</div>
@@ -427,12 +498,8 @@ function doCalibrate() {
for (var sid in data.result) {
var r = data.result[sid];
if (r.debug_image) {
var dbgEl = document.getElementById('calDebug' + sid);
var imgEl = document.getElementById('calDebugImg' + sid);
if (dbgEl && imgEl) {
imgEl.src = 'data:image/jpeg;base64,' + r.debug_image;
dbgEl.style.display = 'block';
}
if (imgEl) imgEl.src = 'data:image/jpeg;base64,' + r.debug_image;
}
}
}
@@ -510,30 +577,40 @@ function updateCalibrationStatus() {
updateCalibrationStatus();
setInterval(updateCalibrationStatus, 3000);
var cameraMeshes = [];
function addCamerasToScene(camData) {
if (!courtSceneInitialized) return;
// Remove old camera meshes
cameraMeshes.forEach(function(m) { courtScene.remove(m); });
cameraMeshes = [];
for (var sid in camData) {
var cam = camData[sid];
var pos = cam.position;
var color = sid === '0' ? 0x44aaff : 0xff44aa;
// Camera pyramid
// Camera body
var geo = new THREE.ConeGeometry(0.3, 0.5, 4);
var mat = new THREE.MeshBasicMaterial({
color: sid === '0' ? 0x44aaff : 0xff44aa,
wireframe: true
});
var mat = new THREE.MeshBasicMaterial({ color: color, wireframe: true });
var mesh = new THREE.Mesh(geo, mat);
mesh.position.set(pos[0], pos[1], pos[2]);
// Point cone toward look direction
var dir = cam.look_direction;
mesh.lookAt(pos[0] + dir[0], pos[1] + dir[1], pos[2] + dir[2]);
courtScene.add(mesh);
cameraMeshes.push(mesh);
// Label
// (Three.js r128 doesn't have CSS2DRenderer built-in, so skip label for now)
// Look direction line
var lineGeo = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(pos[0], pos[1], pos[2]),
new THREE.Vector3(pos[0] + dir[0] * 3, pos[1] + dir[1] * 3, pos[2] + dir[2] * 3)
]);
var line = new THREE.Line(lineGeo, new THREE.LineBasicMaterial({ color: color }));
courtScene.add(line);
cameraMeshes.push(line);
}
}