Visualizer work
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

540 lines
19 KiB

import { appState } from "../state.js";
import { debugFlags } from "../debug.js";
import {
RADAR_X_MAX,
// Define radar plot boundaries
RADAR_X_MIN,
RADAR_Y_MAX,
RADAR_Y_MIN,
} from "../constants.js";
import {
canvasContainer,
toggleSnrColor,
toggleTracks,
togglePredictedPos,
toggleCovariance,
toggleVelocity,
toggleClusterColor,
} from "../dom.js";
import {
drawStaticRegionsToBuffer,
drawAxes,
drawPointCloud,
drawTrajectories,
drawEgoVehicle,
drawTrackMarkers,
snrColors,
handleCloseUpDisplay,
drawCovarianceEllipse,
ttcColors,
drawRegionsOfInterest,
drawClusterCentroids,
} from "../drawUtils.js";
export const radarSketch = function (p) {
// Object to store calculated plot scales
let plotScales = {
plotScaleX: 1,
plotScaleY: 1,
};
// p5.Graphics buffers for static elements to optimize drawing
let staticBackgroundBuffer, snrLegendBuffer, trackLegendBuffer;
// --- START: Mouse Smoothing Variables ---
let smoothedMouseX = 0;
let smoothedMouseY = 0;
let isFirstFrame = true; // Flag to initialize smoothed position
// --- END: Mouse Smoothing Variables ---
// --- START: FPS Calculation Variables ---
let lastFrameTime = 0;
// --- END: FPS Calculation Variables ---
// Helper function to allow other sketches to access the static background
p.getStaticBackground = function () {
return staticBackgroundBuffer;
};
// Function to calculate scaling factors for radar coordinates to canvas pixels
function calculatePlotScales() {
// Padding and offset values for the plot area
const hPad = 0.05,
vPad = 0.05,
bOff = 0.05;
// Calculate available width and height for the plot
const aW = p.width * (1 - 2 * hPad);
const aH = p.height * (1 - bOff - vPad);
// Determine plot scales based on radar boundaries and available canvas space
plotScales.plotScaleX = aW / (RADAR_X_MAX - RADAR_X_MIN);
plotScales.plotScaleY = aH / (RADAR_Y_MAX - RADAR_Y_MIN);
}
p.setup = function () {
// Create the p5.js canvas and attach it to the specified DOM element
let canvas = p.createCanvas(
canvasContainer.offsetWidth,
canvasContainer.offsetHeight
);
canvas.parent("canvas-container");
// --- START: ADD MOUSE WHEEL LISTENER HERE ---
canvas.mouseWheel((event) => {
// Only run this logic if the close-up mode is active
if (appState.isCloseUpMode) {
event.preventDefault(); // Prevent the page from scrolling
const zoomSpeed = 0.5;
const direction = Math.sign(event.deltaY);
let newZoomFactor = appState.zoomFactor - direction * zoomSpeed;
// Clamp the zoom factor to a reasonable range
newZoomFactor = p.constrain(newZoomFactor, 1.5, 30);
appState.zoomFactor = newZoomFactor;
// IMPORTANT: We must manually trigger a redraw of the zoom sketch
// so it immediately updates with the new zoom factor.
if (
appState.zoomSketchInstance &&
appState.zoomSketchInstance.updateAndDraw
) {
// We just need to trigger an update; the zoom sketch will read the new
// appState.zoomFactor when it redraws.
// We find the current hovered items again to pass them.
const hoveredItems = handleCloseUpDisplay(p, plotScales);
appState.zoomSketchInstance.updateAndDraw(
p.mouseX,
p.mouseY,
hoveredItems,
plotScales
);
}
}
});
// --- END: ADD MOUSE WHEEL LISTENER HERE ---
// Initialize graphics buffers
staticBackgroundBuffer = p.createGraphics(p.width, p.height);
snrLegendBuffer = p.createGraphics(100, 450);
trackLegendBuffer = p.createGraphics(120, 120); // create track legend
calculatePlotScales();
p.drawSnrLegendToBuffer(appState.globalMinSnr, appState.globalMaxSnr);
p.drawTrackLegendToBuffer(); // Call the new function to draw the legend
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
p.noLoop();
// Disable continuous looping, redraw will be called manually
};
p.draw = function () {
if (debugFlags.drawing) console.log("draw_DEBUG: radarSketch.draw() called.");
// --- START: FPS Calculation & Display ---
const currentTime = p.millis();
if (lastFrameTime > 0) {
const delta = currentTime - lastFrameTime;
if (delta > 0) {
const currentFps = 1000 / delta;
// Use exponential moving average for smoothing
const smoothingFactor = 0.95;
appState.fps =
appState.fps * smoothingFactor + currentFps * (1 - smoothingFactor);
}
}
lastFrameTime = currentTime;
// --- END: FPS Calculation & Display ---
// Set background color based on current theme (dark/light)
p.background(
document.documentElement.classList.contains("dark")
? p.color(55, 65, 81)
: 255
);
// If no visualization data is loaded, stop drawing
if (!appState.vizData) return;
// Draw the pre-rendered static background elements
p.image(staticBackgroundBuffer, 0, 0);
// Apply transformations for radar coordinate system (origin at bottom-center, Y-axis inverted)
p.push();
p.translate(p.width / 2, p.height * 0.95);
p.scale(1, -1);
// Recalculate plot scales (important for window resizing)
calculatePlotScales();
// Draw coordinate axes
drawAxes(p, plotScales);
drawEgoVehicle(p, plotScales);
// Get current frame data
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (frameData) {
drawPointCloud(p, frameData.pointCloud, plotScales);
if (!appState.isRawOnlyMode) {
drawRegionsOfInterest(p, frameData, plotScales);
drawTrackMarkers(p, plotScales);
// Draw object trajectories and markers if enabled
// if (toggleVelocity.checked) {
// drawTrackMarkers(p, plotScales);
// }
if (togglePredictedPos.checked) {
for (const track of appState.vizData.tracks) {
const log = track.historyLog.find(
(log) => log.frameIdx === appState.currentFrame
);
if (
log &&
log.predictedPosition &&
log.predictedPosition[0] !== null
) {
const pos = log.predictedPosition;
const x = pos[0] * plotScales.plotScaleX;
const y = pos[1] * plotScales.plotScaleY;
p.push();
p.stroke(255, 0, 0); // Red for predicted
p.strokeWeight(2);
p.line(x - 4, y - 4, x + 4, y + 4);
p.line(x + 4, y - 4, x - 4, y + 4);
p.pop();
}
}
}
if (toggleTracks.checked) {
drawTrajectories(p, plotScales);
if (toggleCovariance.checked) {
for (const track of appState.vizData.tracks) {
const log = track.historyLog.find(
(log) => log.frameIdx === appState.currentFrame + 1
);
if (
log &&
log.ellipseRadii &&
typeof log.ellipseAngle !== "undefined"
) {
const pos = log.predictedPosition;
if (pos && pos[0] !== null) {
drawCovarianceEllipse(
p,
pos,
log.ellipseRadii,
log.ellipseAngle,
plotScales,
log.isStationary
);
}
}
}
}
}
// Draw cluster centroids if enabled
if (toggleClusterColor.checked) {
drawClusterCentroids(p, frameData.clusters, plotScales);
}
}
}
p.pop();
// 4. Draw the new legend buffer onto the main canvas
// This is placed at the bottom-right corner.
if (toggleTracks.checked && !appState.isRawOnlyMode) {
p.image(
trackLegendBuffer,
p.width - trackLegendBuffer.width - 10,
p.height - trackLegendBuffer.height - 20
);
}
// End main radar transformations
// BUG FIX 1: Call the close-up handler if the mode is active
// --- Zoom and Tooltip Logic ---
const COOLING_PERIOD_MS = 2000; // Set to 3 seconds for the countdown
const zoomPanel = document.getElementById("zoom-panel");
if (appState.isCloseUpMode) {
// --- START: Mouse Smoothing Logic ---
// On the first frame of zoom, snap the smoothed position to the real mouse position.
if (isFirstFrame) {
smoothedMouseX = p.mouseX;
smoothedMouseY = p.mouseY;
isFirstFrame = false;
}
// The smoothing factor. A smaller value (e.g., 0.1) means more smoothing.
// This can be adjusted to feel more or less responsive.
const smoothingFactor = 0.5;
// Linearly interpolate the smoothed position towards the actual mouse position.
smoothedMouseX = p.lerp(smoothedMouseX, p.mouseX, smoothingFactor);
smoothedMouseY = p.lerp(smoothedMouseY, p.mouseY, smoothingFactor);
// Use the smoothed coordinates for all subsequent zoom-related calculations.
const hoveredItems = handleCloseUpDisplay(p, plotScales, smoothedMouseX, smoothedMouseY);
// --- END: Mouse Smoothing Logic ---
// --- START: Draw Zoom Area Rectangle & Debug Circle ---
const zoomWindow = document.getElementById("zoom-canvas-container");
if (zoomWindow) {
const zoomWindowWidth = zoomWindow.offsetWidth;
const zoomWindowHeight = zoomWindow.offsetHeight;
// Calculate the dimensions of the source rectangle on the main canvas.
const sourceWidth = zoomWindowWidth / appState.zoomFactor;
const sourceHeight = zoomWindowHeight / appState.zoomFactor;
// --- START: THEME-AWARE COLOR LOGIC ---
const isDark = document.documentElement.classList.contains("dark");
const rectColor = isDark ? p.color(255, 80, 80, 200) : p.color(220, 20, 60, 180);
// --- END: THEME-AWARE COLOR LOGIC ---
p.push();
p.noFill();
p.stroke(rectColor);
p.strokeWeight(1); // Reduced thickness.
p.drawingContext.setLineDash([5, 3]); // Dashed line.
p.rectMode(p.CENTER);
p.rect(smoothedMouseX, smoothedMouseY, sourceWidth, sourceHeight); // Use smoothed values
p.drawingContext.setLineDash([]); // Reset line dash
p.pop();
}
// Draw a temporary debug circle representing the hover radius.
// This formula must match the one in drawUtils.js for accurate visualization.
const hoverRadius = p.constrain(80 / appState.zoomFactor, 5, 25);
const isDark = document.documentElement.classList.contains("dark");
const circleColor = isDark ? p.color(50, 205, 50, 200) : p.color(0, 128, 0, 180);
p.push();
p.noFill();
p.stroke(circleColor);
p.strokeWeight(1);
p.ellipse(smoothedMouseX, smoothedMouseY, hoverRadius * 2, hoverRadius * 2); // Use smoothed values
p.pop();
// --- END: Draw Zoom Area Rectangle & Debug Circle ---
if (hoveredItems.length > 0) {
// If we are hovering, cancel any existing countdown.
clearTimeout(appState.zoomHideDelayTimeout);
appState.zoomHideDelayTimeout = null;
clearInterval(appState.zoomCountdownInterval);
appState.zoomCountdownInterval = null;
appState.zoomCountdown = null;
if (zoomPanel.style.display !== "block") {
zoomPanel.style.display = "block";
}
if (
appState.zoomSketchInstance &&
appState.zoomSketchInstance.updateAndDraw
) {
appState.zoomSketchInstance.updateAndDraw(
smoothedMouseX, // Use smoothed values
smoothedMouseY, // Use smoothed values
hoveredItems,
plotScales
);
}
} else if (zoomPanel.style.display === "block") {
// --- START: FIX for Grace Period Freeze ---
// If NOT hovering, but the panel is still visible, we must continue
// to update the zoom sketch so it follows the mouse.
// We pass an empty array for hoveredItems, so no tooltip is drawn.
if (
appState.zoomSketchInstance &&
appState.zoomSketchInstance.updateAndDraw
) {
appState.zoomSketchInstance.updateAndDraw(
smoothedMouseX, // Use smoothed values
smoothedMouseY, // Use smoothed values
[], // Pass empty array to hide tooltips
plotScales
);
}
// --- END: FIX for Grace Period Freeze ---
// 2. If a "hide" timer isn't already running, start one.
if (!appState.zoomHideDelayTimeout && !appState.zoomCountdownInterval) {
// Start a 2-second delay before the countdown begins.
appState.zoomHideDelayTimeout = setTimeout(() => {
appState.zoomHideDelayTimeout = null; // Clear the delay timer ID
// Now, start the actual 3-second countdown interval.
appState.zoomCountdown = Math.floor(COOLING_PERIOD_MS / 1000);
appState.zoomCountdownInterval = setInterval(() => {
appState.zoomCountdown--;
if (appState.zoomCountdown <= 0) {
// When countdown finishes, hide panel and clear interval.
clearInterval(appState.zoomCountdownInterval);
appState.zoomCountdownInterval = null;
appState.zoomCountdown = null;
zoomPanel.style.display = "none";
} else {
// Force a redraw of the zoom sketch to show the new countdown value.
// This call is still needed inside the interval to update the countdown text.
if (appState.zoomSketchInstance && appState.zoomSketchInstance.updateAndDraw) {
// Pass empty hoveredItems to show the countdown text.
appState.zoomSketchInstance.updateAndDraw(
smoothedMouseX,
smoothedMouseY,
[],
plotScales);
}
}
}, 1000);
}, 1000); // 1000ms = 1 second delay
}
}
} else {
// --- START: Cleanup Logic ---
// When zoom mode is turned off, ensure all timers are cleared.
if (appState.zoomHideDelayTimeout) {
clearTimeout(appState.zoomHideDelayTimeout);
appState.zoomHideDelayTimeout = null;
}
if (appState.zoomCountdownInterval) {
clearInterval(appState.zoomCountdownInterval);
appState.zoomCountdownInterval = null;
}
// --- END: Cleanup Logic ---
zoomPanel.style.display = "none";
isFirstFrame = true; // Reset for the next time zoom mode is enabled
}
// --- Legend Drawing ---
// Draw the SNR legend if enabled
if (toggleSnrColor.checked) {
p.image(snrLegendBuffer, 10, p.height - snrLegendBuffer.height - 10);
}
};
// 5. Create the new function to draw the track legend's content
p.drawTrackLegendToBuffer = function () {
const b = trackLegendBuffer;
const localTtcColors = ttcColors(p);
const isDark = document.documentElement.classList.contains("dark");
b.clear();
b.push();
// Set styles based on theme
const textColor = isDark ? 255 : 0;
const bgColor = isDark
? p.color(55, 65, 81, 100)
: p.color(255, 255, 255, 100);
// Draw semi-transparent background for the legend
b.fill(bgColor);
b.stroke(1);
b.strokeWeight(0.25);
b.rect(0, 0, b.width, b.height, 8); // Rounded corners
b.fill(textColor);
b.textSize(12);
b.textStyle(b.BOLD);
b.text("Track Legend", 10, 20);
b.textSize(10);
b.textStyle(b.NORMAL);
b.strokeWeight(3);
let yPos = 40;
// Legend items
const legendItems = [
{ label: "Critical Risk", color: localTtcColors.critical },
{ label: "High Risk", color: localTtcColors.high },
{ label: "Medium Risk", color: localTtcColors.medium },
{ label: "Low Risk", color: localTtcColors.low },
{ label: "Moving Away", color: localTtcColors.away },
{ label: "Stationary", color: p.color(34, 139, 34), dashed: true },
];
for (const item of legendItems) {
b.stroke(item.color);
if (item.dashed) {
b.drawingContext.setLineDash([3, 3]);
}
b.line(15, yPos, 45, yPos);
b.drawingContext.setLineDash([]); // Reset dash for next items
b.noStroke();
b.text(item.label, 55, yPos + 4);
yPos += 18;
}
b.pop();
};
p.windowResized = function () {
console.log("radarSketch: windowResized triggered!");
// Immediately resize the elements that we know are stable.
p.resizeCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight);
staticBackgroundBuffer = p.createGraphics(p.width, p.height);
trackLegendBuffer = p.createGraphics(120, 120);
p.drawTrackLegendToBuffer();
calculatePlotScales();
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
// Defer the call to destroy the zoom canvas.
if (appState.zoomSketchInstance && appState.isCloseUpMode) {
setTimeout(() => {
console.log(
"radarSketch: Executing deferred call to zoomSketch.handleResize()."
);
appState.zoomSketchInstance.handleResize();
}, 10); // A 10ms delay is slightly more robust than 0.
}
if (appState.vizData) {
p.redraw();
}
};
// Function to draw the SNR legend to its buffer
p.drawSnrLegendToBuffer = function (minV, maxV) {
// Reference to the SNR legend buffer
const b = snrLegendBuffer;
const localSnrColors = snrColors(p);
b.clear();
b.push();
const lx = 10,
ly = 20,
lw = 15,
// Dimensions for the color bar
lh = 400;
for (let i = 0; i < lh; i++) {
const amt = b.map(i, 0, lh, 1, 0);
let c;
if (amt < 0.25)
c = b.lerpColor(localSnrColors.c1, localSnrColors.c2, amt / 0.25);
else if (amt < 0.5)
c = b.lerpColor(
localSnrColors.c2,
localSnrColors.c3,
(amt - 0.25) / 0.25
);
else if (amt < 0.75)
c = b.lerpColor(
localSnrColors.c3,
localSnrColors.c4,
(amt - 0.5) / 0.25
);
else
c = b.lerpColor(
localSnrColors.c4,
localSnrColors.c5,
// Interpolate colors based on position
(amt - 0.75) / 0.25
);
b.stroke(c);
b.line(lx, ly + i, lx + lw, ly + i);
}
// Set text color based on theme
b.fill(document.documentElement.classList.contains("dark") ? 255 : 0);
b.noStroke();
b.textSize(10);
b.textAlign(b.LEFT, b.CENTER);
// Draw min/max SNR values and label
b.text(maxV.toFixed(1), lx + lw + 5, ly);
b.text(minV.toFixed(1), lx + lw + 5, ly + lh);
b.text("SNR", lx, ly - 10);
b.pop();
};
};