HoloDeck_Robot_System/static/view.html

267 lines
8.3 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Robot Tracker v009</title>
<style>
html, body { margin: 0; padding: 0; background: #0e0e11; color: #e0e0e0; font-family: system-ui, sans-serif; }
canvas { display: block; margin: auto; background: #14141a; cursor: crosshair; }
#hud {
position: absolute; top: 10px; left: 50%;
transform: translateX(-50%);
display: flex; gap: 10px; align-items: center;
pointer-events: none;
}
#status { font-size: 12px; padding: 4px 12px; border-radius: 99px; background: #1f1f28; color: #888; }
#status.ok { color: #4fd1c5; }
#status.err { color: #f87171; }
#cal-status { font-size: 12px; padding: 4px 12px; border-radius: 99px; background: #1f1f28; color: #f59e0b; }
#cal-status.ok { color: #4fd1c5; }
#cal-btn { font-size: 12px; padding: 4px 14px; border-radius: 99px; background: #4fd1c5; color: #0e0e11; border: none; cursor: pointer; pointer-events: all; font-weight: 600; }
#cal-btn:hover { background: #38b2ac; }
#info { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); font-size: 12px; color: #555; pointer-events: none; }
</style>
</head>
<body>
<div id="hud">
<div id="status">Verbinde…</div>
<div id="cal-status">Nicht kalibriert</div>
<button id="cal-btn" onclick="calibrate()">Kalibrieren</button>
</div>
<canvas id="view" width="1280" height="720"></canvas>
<div id="info">Klick → Ziel setzen | Hindernisse = rot markiert | Geschwindigkeit = grüner Balken</div>
<script>
const canvas = document.getElementById("view");
const ctx = canvas.getContext("2d");
const statusEl = document.getElementById("status");
const calEl = document.getElementById("cal-status");
const FADE_SECONDS = 0.8;
const ARROW_LENGTH = 34;
const VEL_SCALE = 0.3;
const VEL_MIN_SHOW = 8;
const SAFE_DIST_PX = 60;
const SPEED_MAX_MMS = 350; // für Balkenanzeige
let goalX = null, goalY = null;
let isCalibrated = false;
let ws;
function drawGrid() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = "#1f1f28";
ctx.lineWidth = 1;
const step = 80;
for (let x = 0; x <= canvas.width; x += step) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
}
for (let y = 0; y <= canvas.height; y += step) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
}
ctx.strokeStyle = "#3a3a44";
ctx.strokeRect(0, 0, canvas.width, canvas.height);
}
function drawFieldCorners(corners) {
if (!corners || corners.length < 4) return;
ctx.save();
ctx.strokeStyle = "#1D9E75";
ctx.lineWidth = 1.5;
ctx.setLineDash([8, 4]);
ctx.beginPath();
ctx.moveTo(corners[0][0], corners[0][1]);
ctx.lineTo(corners[1][0], corners[1][1]);
ctx.lineTo(corners[2][0], corners[2][1]);
ctx.lineTo(corners[3][0], corners[3][1]);
ctx.closePath();
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = "#1D9E75";
ctx.font = "11px system-ui";
ctx.fillText("Kalibrierfeld", corners[0][0] + 5, corners[0][1] + 15);
ctx.restore();
}
function drawSpeedBar(x, y, speed_mm_s, speed_l, speed_r) {
if (speed_mm_s === undefined) return;
const barW = 60;
const barH = 6;
const bx = x - barW / 2;
const by = y + 28;
const pct = Math.min(1, Math.abs(speed_mm_s) / SPEED_MAX_MMS);
// Hintergrund
ctx.save();
ctx.fillStyle = "#2a2a35";
ctx.fillRect(bx, by, barW, barH);
// Füllstand
const color = speed_mm_s > 200 ? "#f59e0b" : speed_mm_s > 100 ? "#4fd1c5" : "#1D9E75";
ctx.fillStyle = color;
ctx.fillRect(bx, by, barW * pct, barH);
// Geschwindigkeit als Text
ctx.fillStyle = "#e0e0e0";
ctx.font = "10px system-ui";
ctx.textAlign = "center";
ctx.fillText(Math.round(speed_mm_s) + " mm/s", x, by + 18);
ctx.restore();
}
function drawObstacle(obs) {
const { x, y, id, x_mm, y_mm } = obs;
ctx.save();
ctx.strokeStyle = "rgba(220, 50, 50, 0.25)";
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.beginPath(); ctx.arc(x, y, SAFE_DIST_PX, 0, Math.PI * 2); ctx.stroke();
ctx.setLineDash([]);
ctx.strokeStyle = "#E24B4A";
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(x, y, 12, 0, Math.PI * 2); ctx.stroke();
ctx.fillStyle = "#E24B4A";
ctx.font = "11px system-ui";
ctx.textAlign = "center";
ctx.fillText("ID " + id, x, y - 18);
if (x_mm !== undefined) {
ctx.fillStyle = "#888";
ctx.fillText(x_mm.toFixed(0) + "mm, " + y_mm.toFixed(0) + "mm", x, y - 6);
}
ctx.restore();
}
function drawGoal(x, y) {
ctx.save();
ctx.strokeStyle = "#f59e0b";
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.beginPath(); ctx.arc(x, y, 30, 0, Math.PI * 2); ctx.stroke();
ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(x - 10, y); ctx.lineTo(x + 10, y);
ctx.moveTo(x, y - 10); ctx.lineTo(x, y + 10);
ctx.stroke();
ctx.fillStyle = "#f59e0b";
ctx.font = "11px system-ui";
ctx.fillText("Ziel", x + 14, y - 14);
ctx.restore();
}
function drawTag(tag) {
const { id, x, y, theta, age, vx = 0, vy = 0, omega = 0, x_mm, y_mm, speed_mm_s, speed_actual_l, speed_actual_r } = tag;
const alpha = Math.max(0, 1.0 - (age ?? 0) / FADE_SECONDS);
if (alpha <= 0) return;
ctx.save();
ctx.globalAlpha = alpha;
ctx.save();
ctx.translate(x, y);
ctx.rotate(theta * Math.PI / 180);
ctx.strokeStyle = "#4fd1c5";
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, -ARROW_LENGTH); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-5, -ARROW_LENGTH + 8); ctx.lineTo(0, -ARROW_LENGTH); ctx.lineTo(5, -ARROW_LENGTH + 8); ctx.stroke();
ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI * 2); ctx.stroke();
ctx.restore();
const speed = Math.sqrt(vx * vx + vy * vy);
if (speed >= VEL_MIN_SHOW) {
ctx.save();
ctx.translate(x, y);
ctx.strokeStyle = "#a78bfa";
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(vx * VEL_SCALE, vy * VEL_SCALE); ctx.stroke();
ctx.restore();
}
ctx.fillStyle = "#e0e0e0";
ctx.font = "11px system-ui";
ctx.textAlign = "left";
ctx.textBaseline = "top";
const lines = [
"ID " + id,
x_mm !== undefined ? x_mm.toFixed(1) + "mm, " + y_mm.toFixed(1) + "mm" : "x=" + x.toFixed(0) + " y=" + y.toFixed(0),
"θ=" + theta.toFixed(1) + "°",
];
// Geschwindigkeit nur für Roboter (wenn speed_mm_s vorhanden)
if (speed_mm_s !== undefined) {
lines.push("L:" + (speed_actual_l||0).toFixed(0) + " R:" + (speed_actual_r||0).toFixed(0) + " mm/s");
}
lines.forEach((line, i) => ctx.fillText(line, x + 10, y + 10 + i * 13));
// Geschwindigkeitsbalken für Roboter
if (speed_mm_s !== undefined) {
drawSpeedBar(x, y, speed_mm_s, speed_actual_l, speed_actual_r);
}
ctx.restore();
}
function calibrate() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "calibrate" }));
calEl.textContent = "Kalibriere…";
}
}
canvas.addEventListener("click", (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
goalX = (e.clientX - rect.left) * scaleX;
goalY = (e.clientY - rect.top) * scaleY;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "goal", x: goalX, y: goalY }));
}
});
drawGrid();
let reconnectTimer;
function connect() {
ws = new WebSocket("ws://" + location.host + "/ws");
ws.onopen = () => { statusEl.textContent = "Verbunden"; statusEl.className = "ok"; };
ws.onmessage = (ev) => {
const data = JSON.parse(ev.data);
if (data.type === "calibration_result") {
calEl.textContent = data.success ? "Kalibriert ✓" : "Fehlgeschlagen";
calEl.className = data.success ? "ok" : "";
return;
}
if (data.calibrated) {
isCalibrated = true;
calEl.textContent = "Kalibriert ✓";
calEl.className = "ok";
}
drawGrid();
if (data.field_corners_px) drawFieldCorners(data.field_corners_px);
(data.obstacles || []).forEach(drawObstacle);
if (goalX !== null) drawGoal(goalX, goalY);
(data.tags || []).forEach(tag => {
const isObstacle = (data.obstacles || []).some(o => o.id === tag.id);
if (!isObstacle) drawTag(tag);
});
};
ws.onerror = () => { statusEl.textContent = "Verbindungsfehler"; statusEl.className = "err"; };
ws.onclose = () => {
statusEl.textContent = "Getrennt — reconnect…";
statusEl.className = "err";
clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(connect, 2000);
};
}
connect();
</script>
</body>
</html>