Browse Source

feat: Enhance zoom sketch usability and fix critical bugs

This commit introduces several major improvements to the "God Mode" zoom sketch, making it more intuitive and robust. It also resolves two critical race-condition bugs related to canvas resizing and initial data loading.

**Features:**

-   **Automatic Zoom on Hover:** The zoom sketch now appears automatically when the user hovers over any data point (track, point cloud, etc.) and disappears after a configurable cooling-off period. This provides a more dynamic and professional user experience, allowing for quick inspection without manual toggling. The zoom window now smoothly follows the cursor during this cooling-off period.

-   **Point Index in Tooltip:** The zoom sketch tooltip has been enhanced to display the index of the hovered point within its `pointCloud` array (e.g., "Point 123"). This adds valuable context for debugging and detailed data analysis.

**Bug Fixes:**

-   **fix(p5):** Resolves a critical bug where the zoom sketch would go blank after the browser window was resized. This was caused by a race condition where the sketch's canvas was being resized to 0x0 before its container had updated. The fix defers the resize call and correctly destroys the old canvas, allowing it to be recreated with the proper dimensions.

-   **fix(loading):** Corrects a race condition in the drag-and-drop file loading pipeline (`processFilePipeline`). The speed graph sketch will now reliably initialize on the first load, as the code now uses `Promise.all` to ensure both the JSON data is parsed and the video's metadata (duration) is available before attempting to render the visualization.
refactor/modularize
RUSHIL AMBARISH KADU 7 months ago
parent
commit
b6a9705e20
  1. 22
      steps/src/drawUtils.js
  2. 38
      steps/src/p5/radarSketch.js
  3. 135
      steps/src/p5/zoomSketch.js
  4. 1
      steps/src/state.js

22
steps/src/drawUtils.js

@ -532,13 +532,26 @@ export function handleCloseUpDisplay(p, plotScales) {
// ... (Step 1a: Find hovered points - no changes here) ... // ... (Step 1a: Find hovered points - no changes here) ...
if (frameData.pointCloud) { if (frameData.pointCloud) {
for (const pt of frameData.pointCloud) {
// In steps/src/drawUtils.js
// Find hovered points
if (frameData.pointCloud) {
for (let i = 0; i < frameData.pointCloud.length; i++) {
const pt = frameData.pointCloud[i];
if (pt.x === null || pt.y === null) continue; if (pt.x === null || pt.y === null) continue;
const screenX = pt.x * plotScales.plotScaleX + p.width / 2; const screenX = pt.x * plotScales.plotScaleX + p.width / 2;
const screenY = p.height * 0.95 - pt.y * plotScales.plotScaleY; const screenY = p.height * 0.95 - pt.y * plotScales.plotScaleY;
const d = p.dist(p.mouseX, p.mouseY, screenX, screenY); const d = p.dist(p.mouseX, p.mouseY, screenX, screenY);
if (d < radius) { if (d < radius) {
hoveredItems.push({ type: "point", data: pt, screenX, screenY });
// Add the index 'i' to the object we push
hoveredItems.push({
type: "point",
data: pt,
screenX,
screenY,
index: i,
});
}
} }
} }
} }
@ -631,7 +644,7 @@ export function handleCloseUpDisplay(p, plotScales) {
case "point": case "point":
const vel = data.velocity !== null ? data.velocity.toFixed(2) : "N/A"; const vel = data.velocity !== null ? data.velocity.toFixed(2) : "N/A";
const snr = data.snr !== null ? data.snr.toFixed(1) : "N/A"; const snr = data.snr !== null ? data.snr.toFixed(1) : "N/A";
infoText = `Point | X:${data.x.toFixed(2)}, Y:${data.y.toFixed(
infoText = `Point ${item.index} | X:${data.x.toFixed(2)}, Y:${data.y.toFixed(
2 2
)} | V:${vel}, SNR:${snr}, Cluster: ${data.clusterNumber}`; )} | V:${vel}, SNR:${snr}, Cluster: ${data.clusterNumber}`;
break; break;
@ -843,8 +856,6 @@ export function drawEgoVehicle(p, plotScales) {
p.pop(); p.pop();
} }
//OLD_Solid Fill Logic //OLD_Solid Fill Logic
/** /**
@ -1225,7 +1236,6 @@ export function drawClusterCentroids(p, clustersInput, plotScales) {
// // p.pop(); // Restore the original global drawing state. // // p.pop(); // Restore the original global drawing state.
// // } // // }
// OLD HATCH FILL logic // OLD HATCH FILL logic
// /** // /**
// * Draws a hatched pattern inside a rectangle defined by corner points. // * Draws a hatched pattern inside a rectangle defined by corner points.

38
steps/src/p5/radarSketch.js

@ -221,11 +221,16 @@ export const radarSketch = function (p) {
// BUG FIX 1: Call the close-up handler if the mode is active // BUG FIX 1: Call the close-up handler if the mode is active
// --- Zoom and Tooltip Logic --- // --- Zoom and Tooltip Logic ---
const COOLING_PERIOD_MS = 2000;
const zoomPanel = document.getElementById("zoom-panel"); const zoomPanel = document.getElementById("zoom-panel");
if (appState.isCloseUpMode) { if (appState.isCloseUpMode) {
const hoveredItems = handleCloseUpDisplay(p, plotScales); const hoveredItems = handleCloseUpDisplay(p, plotScales);
if (hoveredItems.length > 0) { if (hoveredItems.length > 0) {
zoomPanel.style.display = "block"; // show the panel
clearTimeout(appState.zoomHoverTimeout); // Cancel the timer
appState.zoomHoverTimeout = null;
if (zoomPanel.style.display !== "block") {
zoomPanel.style.display = "block";
}
if ( if (
appState.zoomSketchInstance && appState.zoomSketchInstance &&
appState.zoomSketchInstance.updateAndDraw appState.zoomSketchInstance.updateAndDraw
@ -237,9 +242,30 @@ export const radarSketch = function (p) {
plotScales plotScales
); );
} }
} else {
} else if (zoomPanel.style.display === "block") {
// --- THIS BLOCK IS THE FIX ---
// If NOT hovering, but the panel is still visible:
// 1. Continue to update the zoom sketch's position to follow the mouse.
// We pass an empty array for hoveredItems, so no tooltip is drawn.
if (appState.zoomSketchInstance && appState.zoomSketchInstance.updateAndDraw) {
appState.zoomSketchInstance.updateAndDraw(
p.mouseX,
p.mouseY,
[], // Pass empty array
plotScales
);
}
// 2. If a "hide" timer isn't already running, start one.
if (!appState.zoomHoverTimeout) {
appState.zoomHoverTimeout = setTimeout(() => {
console.log("Cooling period ended. Hiding zoom panel.");
zoomPanel.style.display = "none"; zoomPanel.style.display = "none";
appState.zoomHoverTimeout = null;
}, COOLING_PERIOD_MS);
} }
}
} else { } else {
zoomPanel.style.display = "none"; zoomPanel.style.display = "none";
} }
@ -324,7 +350,7 @@ export const radarSketch = function (p) {
// In src/p5/radarSketch.js // In src/p5/radarSketch.js
p.windowResized = function () {
p.windowResized = function () {
console.log("radarSketch: windowResized triggered!"); console.log("radarSketch: windowResized triggered!");
// Immediately resize the elements that we know are stable. // Immediately resize the elements that we know are stable.
@ -338,7 +364,9 @@ p.windowResized = function () {
// Defer the call to destroy the zoom canvas. // Defer the call to destroy the zoom canvas.
if (appState.zoomSketchInstance && appState.isCloseUpMode) { if (appState.zoomSketchInstance && appState.isCloseUpMode) {
setTimeout(() => { setTimeout(() => {
console.log("radarSketch: Executing deferred call to zoomSketch.handleResize().");
console.log(
"radarSketch: Executing deferred call to zoomSketch.handleResize()."
);
appState.zoomSketchInstance.handleResize(); appState.zoomSketchInstance.handleResize();
}, 10); // A 10ms delay is slightly more robust than 0. }, 10); // A 10ms delay is slightly more robust than 0.
} }
@ -346,7 +374,7 @@ p.windowResized = function () {
if (appState.vizData) { if (appState.vizData) {
p.redraw(); p.redraw();
} }
};
};
// Function to draw the SNR legend to its buffer // Function to draw the SNR legend to its buffer
p.drawSnrLegendToBuffer = function (minV, maxV) { p.drawSnrLegendToBuffer = function (minV, maxV) {

135
steps/src/p5/zoomSketch.js

@ -17,133 +17,7 @@ import {
toggleCovariance, toggleCovariance,
} from "../dom.js"; } from "../dom.js";
/**
* A dedicated tooltip function for the zoom sketch.
* It draws the tooltip relative to the hovered items and compensates for the zoom factor.
*/
/**
* A dedicated tooltip function for the zoom sketch with full features.
* It draws the tooltip in the least cluttered quadrant, has dynamic connectors,
* highlights items, and compensates for the zoom factor.
*/
/**
* A dedicated tooltip function for the zoom sketch with smart quadrant positioning.
*/
/**
* A dedicated tooltip function for the zoom sketch that "pushes" the tooltip
* 100 pixels away from the hovered items towards the least cluttered corner.
*/
// function drawZoomTooltip(p, hoveredItems) {
// if (!hoveredItems || hoveredItems.length === 0) return;
// // 1. Generate text content (this is unchanged)
// const infoStrings = [];
// for (const item of hoveredItems) {
// let infoText = '';
// const data = item.data;
// switch (item.type) {
// case 'point': infoText = `Point | X:${data.x.toFixed(2)}, Y:${data.y.toFixed(2)} | V:${data.velocity?.toFixed(2)}, SNR:${data.snr?.toFixed(1)}`; break;
// case 'cluster': infoText = `Cluster ${data.id} | X:${data.x.toFixed(2)}, Y:${data.y.toFixed(2)} | rSpeed:${data.radialSpeed?.toFixed(2)}`; break;
// case 'track': infoText = `Track ${item.trackId} | X:${data.correctedPosition[0].toFixed(2)}, Y:${data.correctedPosition[1].toFixed(2)}`; break;
// case 'prediction': infoText = `Pred. for ${item.trackId} | X:${data.predictedPosition[0].toFixed(2)}, Y:${data.predictedPosition[1].toFixed(2)}`; break;
// }
// if (infoText) infoStrings.push({ text: infoText, color: item.color || null });
// }
// // 2. Find the average screen position of hovered items. This is our anchor point.
// const avgX = hoveredItems.reduce((acc, item) => acc + item.screenX, 0) / hoveredItems.length;
// const avgY = hoveredItems.reduce((acc, item) => acc + item.screenY, 0) / hoveredItems.length;
// p.push();
// // 3. Compensate for zoom factor for all drawing operations (unchanged)
// const zoomFactor = appState.zoomFactor || 6.0;
// p.textSize(12 / zoomFactor);
// p.strokeWeight(1 / zoomFactor);
// const lineHeight = 15 / zoomFactor;
// const boxPadding = 8 / zoomFactor;
// let boxWidth = 0;
// infoStrings.forEach(info => {
// boxWidth = Math.max(boxWidth, p.textWidth(info.text));
// });
// const boxHeight = (infoStrings.length * lineHeight) + (boxPadding * 2);
// boxWidth += (boxPadding * 2);
// // --- START: New "Push" Positioning Logic ---
// // Line 1: Define the push distance. We scale it by the zoomFactor so it's a consistent
// // visual distance on the screen, regardless of zoom level.
// const pushDistance = 100 / zoomFactor;
// // Line 2: Determine the horizontal direction. If the items are on the right, our direction is left (-1).
// // If they are on the left, our direction is right (1).
// const dirX = (avgX > appState.p5_instance.width / 2) ? -1 : 1;
// // Line 3: Determine the vertical direction. If the items are on the bottom, our direction is up (-1).
// // If they are on the top, our direction is down (1).
// const dirY = (avgY > appState.p5_instance.height / 2) ? -1 : 1;
// // Line 4: Create a p5.Vector object. This is like an arrow representing our direction (e.g., up and to the right).
// const pushVector = p.createVector(dirX, dirY);
// // Line 5: Normalize the vector. This makes its length exactly 1, so it only represents a pure direction.
// pushVector.normalize();
// // Line 6: Scale the vector. Now it's an arrow that is exactly `pushDistance` pixels long.
// pushVector.mult(pushDistance);
// // Line 7: Calculate the tooltip's corner position by adding our push vector to the anchor point.
// let boxX = avgX + pushVector.x;
// let boxY = avgY + pushVector.y;
// // Line 8: Define where the connector line should attach to the box.
// // If we pushed right, the connector attaches to the left side of the box.
// let connectorAnchorX = (dirX > 0) ? boxX : boxX + boxWidth;
// // Line 9: If we pushed down, the connector attaches to the top side of the box.
// let connectorAnchorY = (dirY > 0) ? boxY : boxY + boxHeight;
// // Line 10: Adjust the box's final position to account for its own size, so the *corner*
// // of the box is at our calculated position, not its top-left.
// if (dirX < 0) boxX -= boxWidth; // If we pushed left, shift the box left by its own width.
// if (dirY < 0) boxY -= boxHeight; // If we pushed up, shift the box up by its own height.
// // --- END: New "Push" Positioning Logic ---
// // 4. Draw highlights, box, text, and connectors (this logic is now restored and complete)
// const highlightColor = p.color(46, 204, 113);
// hoveredItems.forEach(item => {
// p.noFill(); p.stroke(highlightColor); p.strokeWeight(2 / zoomFactor);
// p.ellipse(item.screenX, item.screenY, 15 / zoomFactor, 15 / zoomFactor);
// });
// const bgColor = document.documentElement.classList.contains('dark') ? p.color(20, 20, 30, 220) : p.color(245, 245, 245, 220);
// p.fill(bgColor); p.stroke(highlightColor); p.strokeWeight(1 / zoomFactor);
// p.rect(boxX, boxY, boxWidth, boxHeight, 4 / zoomFactor);
// const defaultTextColor = document.documentElement.classList.contains('dark') ? p.color(230) : p.color(20);
// p.noStroke(); p.textAlign(p.LEFT, p.TOP);
// infoStrings.forEach((info, i) => {
// p.fill(info.color || defaultTextColor);
// p.text(info.text, boxX + boxPadding, boxY + boxPadding + (i * lineHeight));
// });
// hoveredItems.forEach((item, i) => {
// p.stroke(highlightColor); p.strokeWeight(1 / zoomFactor);
// p.line(connectorAnchorX, connectorAnchorY, item.screenX, item.screenY);
// });
// p.pop();
// }
/**
* A dedicated tooltip function for the zoom sketch with smart quadrant positioning,
* individual dynamic connectors, and item highlighting.
*/
/**
* A dedicated tooltip function for the zoom sketch with full features and customizations.
*/
function drawZoomTooltip(p, hoveredItems, mainMouseX) { function drawZoomTooltip(p, hoveredItems, mainMouseX) {
if (!hoveredItems || hoveredItems.length === 0) return; if (!hoveredItems || hoveredItems.length === 0) return;
@ -154,7 +28,7 @@ function drawZoomTooltip(p, hoveredItems, mainMouseX) {
const data = item.data; const data = item.data;
switch (item.type) { switch (item.type) {
case "point": case "point":
infoText = `Point | X:${data.x.toFixed(2)}, Y:${data.y.toFixed(
infoText = `Point${item.index} | X:${data.x.toFixed(2)}, Y:${data.y.toFixed(
2 2
)} | V:${data.velocity?.toFixed(2)}, SNR:${data.snr?.toFixed(1)}`; )} | V:${data.velocity?.toFixed(2)}, SNR:${data.snr?.toFixed(1)}`;
break; break;
@ -295,13 +169,12 @@ export const zoomSketch = function (p) {
if (container && container.offsetWidth > 0) { if (container && container.offsetWidth > 0) {
canvas = p.createCanvas(container.offsetWidth, container.offsetHeight); canvas = p.createCanvas(container.offsetWidth, container.offsetHeight);
canvas.parent(containerId); canvas.parent(containerId);
console.log(`zoomSketch: Canvas CREATED with dimensions ${p.width}x${p.height}`); // debug
//console.log(`zoomSketch: Canvas CREATED with dimensions ${p.width}x${p.height}`); // debug
} else { } else {
console.warn("zoomSketch: updateAndDraw called, but container is not ready. Aborting draw."); //debug console.warn("zoomSketch: updateAndDraw called, but container is not ready. Aborting draw."); //debug
return; return;
} }
} }
console.log(`zoomSketch: updateAndDraw is running. Canvas dimensions are ${p.width}x${p.height}. Hovered items: ${hoveredItems.length}`); //debug
p.redraw(); p.redraw();
}; };
@ -316,8 +189,6 @@ export const zoomSketch = function (p) {
} }
p.draw = function () { p.draw = function () {
if (!appState.vizData || !canvas) return; if (!appState.vizData || !canvas) return;
console.log("zoomSketch: Draw function is executing."); //debug
p.background( p.background(
document.documentElement.classList.contains("dark") document.documentElement.classList.contains("dark")
? p.color(55, 65, 81) ? p.color(55, 65, 81)
@ -406,7 +277,7 @@ export const zoomSketch = function (p) {
p.fill(textColor); p.fill(textColor);
p.noStroke(); p.noStroke();
p.textSize(16); p.textSize(16);
p.textAlign(p.LEFT, p.TOP);
p.textAlign(p.LEFT -2, p.TOP);
p.textStyle(p.BOLD); p.textStyle(p.BOLD);
p.text(titleText, 10, 10); p.text(titleText, 10, 10);
p.pop(); p.pop();

1
steps/src/state.js

@ -1,4 +1,5 @@
export const appState = { export const appState = {
zoomHoverTimeout: null, // timeout for hovering over the GOD MODE
isRawOnlyMode: false, // <-- ADD THIS LINE isRawOnlyMode: false, // <-- ADD THIS LINE
// Stores the parsed visualization data (radar frames, tracks, etc.) // Stores the parsed visualization data (radar frames, tracks, etc.)

Loading…
Cancel
Save