From b6a9705e2017411d4da8b60d81a7ebfd4bcf16d1 Mon Sep 17 00:00:00 2001 From: rakadu1 Date: Tue, 14 Oct 2025 17:20:01 +0530 Subject: [PATCH] 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. --- steps/index.html | 2 +- steps/src/drawUtils.js | 36 ++++++---- steps/src/p5/radarSketch.js | 80 ++++++++++++++------- steps/src/p5/zoomSketch.js | 135 +----------------------------------- steps/src/state.js | 1 + 5 files changed, 82 insertions(+), 172 deletions(-) diff --git a/steps/index.html b/steps/index.html index 26c3930..39e9e61 100644 --- a/steps/index.html +++ b/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) + class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> GOD MODE diff --git a/steps/src/drawUtils.js b/steps/src/drawUtils.js index 0551faf..e6fa1eb 100644 --- a/steps/src/drawUtils.js +++ b/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,8 +1236,7 @@ export function drawClusterCentroids(p, clustersInput, plotScales) { // // 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. // * This is a new helper function. @@ -1348,4 +1358,4 @@ export function drawClusterCentroids(p, clustersInput, plotScales) { // ); // b.pop(); -// } \ No newline at end of file +// } diff --git a/steps/src/p5/radarSketch.js b/steps/src/p5/radarSketch.js index caf925e..00b1bd6 100644 --- a/steps/src/p5/radarSketch.js +++ b/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) { diff --git a/steps/src/p5/zoomSketch.js b/steps/src/p5/zoomSketch.js index 19bd317..d2ff8ed 100644 --- a/steps/src/p5/zoomSketch.js +++ b/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(); diff --git a/steps/src/state.js b/steps/src/state.js index 92016db..13c9801 100644 --- a/steps/src/state.js +++ b/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.)