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. 2
      steps/index.html
  2. 32
      steps/src/drawUtils.js
  3. 80
      steps/src/p5/radarSketch.js
  4. 135
      steps/src/p5/zoomSketch.js
  5. 1
      steps/src/state.js

2
steps/index.html

@ -147,7 +147,7 @@
id="toggle-debug2-overlay" class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> Show
Advanced Debug (A)</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-close-up"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> GOD MODE </label>
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> GOD MODE </label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-predicted-pos"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" checked /> Show Predicted Position
(P)</label>

32
steps/src/drawUtils.js

@ -532,13 +532,26 @@ export function handleCloseUpDisplay(p, plotScales) {
// ... (Step 1a: Find hovered points - no changes here) ...
if (frameData.pointCloud) {
for (const pt of frameData.pointCloud) {
if (pt.x === null || pt.y === null) continue;
const screenX = pt.x * plotScales.plotScaleX + p.width / 2;
const screenY = p.height * 0.95 - pt.y * plotScales.plotScaleY;
const d = p.dist(p.mouseX, p.mouseY, screenX, screenY);
if (d < radius) {
hoveredItems.push({ type: "point", data: pt, screenX, screenY });
// 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;
const screenX = pt.x * plotScales.plotScaleX + p.width / 2;
const screenY = p.height * 0.95 - pt.y * plotScales.plotScaleY;
const d = p.dist(p.mouseX, p.mouseY, screenX, screenY);
if (d < radius) {
// 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":
const vel = data.velocity !== null ? data.velocity.toFixed(2) : "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
)} | V:${vel}, SNR:${snr}, Cluster: ${data.clusterNumber}`;
break;
@ -843,8 +856,6 @@ export function drawEgoVehicle(p, plotScales) {
p.pop();
}
//OLD_Solid Fill Logic
/**
@ -1225,7 +1236,6 @@ export function drawClusterCentroids(p, clustersInput, plotScales) {
// // p.pop(); // Restore the original global drawing state.
// // }
// OLD HATCH FILL logic
// /**
// * Draws a hatched pattern inside a rectangle defined by corner points.

80
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
// --- Zoom and Tooltip Logic ---
const COOLING_PERIOD_MS = 2000;
const zoomPanel = document.getElementById("zoom-panel");
if (appState.isCloseUpMode) {
const hoveredItems = handleCloseUpDisplay(p, plotScales);
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 (
appState.zoomSketchInstance &&
appState.zoomSketchInstance.updateAndDraw
@ -237,9 +242,30 @@ export const radarSketch = function (p) {
plotScales
);
}
} else {
zoomPanel.style.display = "none";
}
} 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";
appState.zoomHoverTimeout = null;
}, COOLING_PERIOD_MS);
}
}
} else {
zoomPanel.style.display = "none";
}
@ -324,29 +350,31 @@ export const radarSketch = function (p) {
// In src/p5/radarSketch.js
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.
}
p.windowResized = function () {
console.log("radarSketch: windowResized triggered!");
if (appState.vizData) {
p.redraw();
}
};
// 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) {

135
steps/src/p5/zoomSketch.js

@ -17,133 +17,7 @@ import {
toggleCovariance,
} 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) {
if (!hoveredItems || hoveredItems.length === 0) return;
@ -154,7 +28,7 @@ function drawZoomTooltip(p, hoveredItems, mainMouseX) {
const data = item.data;
switch (item.type) {
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
)} | V:${data.velocity?.toFixed(2)}, SNR:${data.snr?.toFixed(1)}`;
break;
@ -295,13 +169,12 @@ export const zoomSketch = function (p) {
if (container && container.offsetWidth > 0) {
canvas = p.createCanvas(container.offsetWidth, container.offsetHeight);
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 {
console.warn("zoomSketch: updateAndDraw called, but container is not ready. Aborting draw."); //debug
return;
}
}
console.log(`zoomSketch: updateAndDraw is running. Canvas dimensions are ${p.width}x${p.height}. Hovered items: ${hoveredItems.length}`); //debug
p.redraw();
};
@ -316,8 +189,6 @@ export const zoomSketch = function (p) {
}
p.draw = function () {
if (!appState.vizData || !canvas) return;
console.log("zoomSketch: Draw function is executing."); //debug
p.background(
document.documentElement.classList.contains("dark")
? p.color(55, 65, 81)
@ -406,7 +277,7 @@ export const zoomSketch = function (p) {
p.fill(textColor);
p.noStroke();
p.textSize(16);
p.textAlign(p.LEFT, p.TOP);
p.textAlign(p.LEFT -2, p.TOP);
p.textStyle(p.BOLD);
p.text(titleText, 10, 10);
p.pop();

1
steps/src/state.js

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

Loading…
Cancel
Save