Browse Source

feat(zoom): Implement interactive high-fidelity zoom window

Adds a new zoom panel that provides a magnified, real-time view of the area under the cursor when "Close-Up Mode" is active. This feature enhances the tool's precision for detailed data analysis.

Key features and fixes include:
- Renders a high-fidelity, vector-based redraw of the scene, not a pixelated image.
- Implemented dynamic zoom control via the mouse wheel when hovering over the main radar canvas.
- The zoom sketch is fully decoupled from the main radar sketch to ensure stability and prevent UI freezes.
- Includes a self-contained tooltip within the zoom window that correctly scales its size and text to match the zoom level.
- The tooltip's position is now smart, dynamically moving to the least cluttered quadrant to avoid obstructing data points.
- Connector lines and item highlighting are now fully functional and styled to match the main view's tooltip.

Motion state added in the persistent overlays
refactor/modularize
RUSHIL AMBARISH KADU 8 months ago
parent
commit
9790e98257
  1. 13
      steps/index.html
  2. 4
      steps/src/dom.js
  3. 724
      steps/src/drawUtils.js
  4. 27
      steps/src/main.js
  5. 83
      steps/src/p5/radarSketch.js
  6. 416
      steps/src/p5/zoomSketch.js
  7. 1
      steps/src/state.js

13
steps/index.html

@ -147,14 +147,15 @@
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" /> CLOSE-UP (C)</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
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" checked /> Show Predicted Position
(P)</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-covariance"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> Show Covariance</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-confirmed-only"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" checked />
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-confirmed-only" class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500"
checked />
Confirmed Tracks
</label>
</div>
@ -208,6 +209,10 @@
</div>
</aside>
<div id="zoom-panel"
class="hidden fixed bottom-11 right-5 h-[50%] w-1/2 bg-white dark:bg-gray-800 z-20 shadow-2xl border-l-2 border-t-2 border-gray-300 dark:border-gray-600">
<div id="zoom-canvas-container" class="w-full h-full"></div>
</div>
<!-- Open Menu Button -->
<button id="toggle-menu-btn"
class="fixed top-20 left-3 z-20 bg-white dark:bg-gray-700 p-2 rounded-md shadow-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-all">

4
steps/src/dom.js

@ -303,6 +303,8 @@ export function updatePersistentOverlays(currentMediaTime) {
// --- Update Radar Overlay ---
const currentRadarFrame = appState.vizData.radarFrames[appState.currentFrame];
const frameData = appState.vizData.radarFrames[appState.currentFrame];
const motionState = frameData.motionState;
if (currentRadarFrame) {
const absRadarTime = new Date(
appState.videoStartDate.getTime() + currentRadarFrame.timestampMs
@ -315,6 +317,7 @@ export function updatePersistentOverlays(currentMediaTime) {
radarInfoOverlay.innerHTML = `
Frame: ${appState.currentFrame + 1}
Motion State: ${motionState}
| Abs Time: ${formatUTCTime(absRadarTime)}
| Color Mode: <b>${colorMode}</b>
| Drift: <b style="color: ${driftColor};">${driftMs.toFixed(
@ -328,6 +331,7 @@ export function updatePersistentOverlays(currentMediaTime) {
appState.videoStartDate.getTime() + currentMediaTime * 1000
);
const videoFrame = Math.floor(currentMediaTime * VIDEO_FPS);
console.warn('Could not load radarframes ', appState.vizData.radarFrames)
videoInfoOverlay.innerHTML = `
Frame: ${videoFrame}

724
steps/src/drawUtils.js

@ -18,7 +18,8 @@ import {
toggleVelocity,
toggleStationaryColor,
toggleConfirmedOnly,
togglePredictedPos
togglePredictedPos,
toggleTracks,
} from "./dom.js";
// Defines a set of SNR (Signal-to-Noise Ratio) colors.
@ -44,30 +45,29 @@ export const ttcColors = (p) => ({
// Defines a palette of 20 colors for different clusters.
export const clusterColors = (p) => [
// Primary & Secondary Colors
p.color(230, 25, 75), // 1. Red
p.color(60, 180, 75), // 2. Green
p.color(0, 130, 200), // 3. Blue
p.color(245, 130, 48), // 4. Orange
p.color(145, 30, 180), // 5. Purple
p.color(70, 240, 240), // 6. Cyan
p.color(230, 25, 75), // 1. Red
p.color(60, 180, 75), // 2. Green
p.color(0, 130, 200), // 3. Blue
p.color(245, 130, 48), // 4. Orange
p.color(145, 30, 180), // 5. Purple
p.color(70, 240, 240), // 6. Cyan
// Tertiary & Bright Colors
p.color(240, 50, 230), // 7. Magenta
p.color(210, 245, 60), // 8. Lime
p.color(240, 50, 230), // 7. Magenta
p.color(210, 245, 60), // 8. Lime
p.color(250, 190, 212), // 9. Pink
p.color(0, 128, 128), // 10. Teal
p.color(0, 128, 128), // 10. Teal
p.color(220, 190, 255), // 11. Lavender
p.color(170, 110, 40), // 12. Brown
p.color(170, 110, 40), // 12. Brown
p.color(255, 250, 200), // 13. Beige
p.color(128, 0, 0), // 14. Maroon
p.color(128, 0, 0), // 14. Maroon
p.color(170, 255, 195), // 15. Mint
p.color(128, 128, 0), // 16. Olive
p.color(128, 128, 0), // 16. Olive
p.color(255, 215, 180), // 17. Apricot
p.color(0, 0, 128), // 18. Navy
p.color(70, 130, 180), // 19. Steel Blue (Replaced Gray as grey is for unclustered. )
p.color(255, 255, 25), // 20. Yellow
p.color(0, 0, 128), // 18. Navy
p.color(70, 130, 180), // 19. Steel Blue (Replaced Gray as grey is for unclustered. )
p.color(255, 255, 25), // 20. Yellow
];
// Defines colors for stationary and moving objects.
export const stationaryColor = (p) => p.color(218, 165, 32); // Goldenrod
export const movingColor = (p) => p.color(255, 0, 255); // Magenta
@ -517,19 +517,15 @@ export function drawTrackMarkers(p, plotScales) {
}
}
// In src/drawUtils.js
// (Make sure the necessary imports are at the top)
/**
* Handles the display of a comprehensive info tooltip for all elements under the mouse.
* @param {p5} p - The p5 instance.
* @param {object} plotScales - The calculated scales for plotting.
*/
export function handleCloseUpDisplay(p, plotScales) {
// --- Step 1: Gather Hovered Items ---
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (!frameData) return;
if (!frameData) return []; // Return empty array if no data
const hoveredItems = [];
const radius = 10;
const localClusterColors = clusterColors(p); // <-- Get the color palette once
@ -539,95 +535,143 @@ export function handleCloseUpDisplay(p, plotScales) {
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 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 });
hoveredItems.push({ type: "point", data: pt, screenX, screenY });
}
}
}
// Find hovered cluster centroids
if (toggleClusterColor.checked && frameData.clusters) {
const clusters = Array.isArray(frameData.clusters) ? frameData.clusters : [frameData.clusters];
const clusters = Array.isArray(frameData.clusters)
? frameData.clusters
: [frameData.clusters];
for (const cluster of clusters) {
if (cluster.x === null || cluster.y === null) continue;
const screenX = cluster.x * plotScales.plotScaleX + p.width / 2;
const screenY = p.height * 0.95 - (cluster.y * plotScales.plotScaleY);
const screenY = p.height * 0.95 - cluster.y * plotScales.plotScaleY;
const d = p.dist(p.mouseX, p.mouseY, screenX, screenY);
if (d < radius) {
// ======================= CHANGE START =======================
// Get the cluster's color and pass it in the hovered item object
const color = cluster.id > 0
? localClusterColors[(cluster.id - 1) % localClusterColors.length]
: p.color(128);
hoveredItems.push({ type: 'cluster', data: cluster, screenX, screenY, color: color });
// ======================== CHANGE END ========================
const color =
cluster.id > 0
? localClusterColors[(cluster.id - 1) % localClusterColors.length]
: p.color(128);
hoveredItems.push({
type: "cluster",
data: cluster,
screenX,
screenY,
color: color,
});
}
}
}
// ... (Step 1c: Find hovered tracks - no changes here) ...
// Find hovered track markers and predicted positions
if (appState.vizData.tracks) {
for (const track of appState.vizData.tracks) {
const log = track.historyLog.find(log => log.frameIdx === appState.currentFrame + 1);
const log = track.historyLog.find(
(log) => log.frameIdx === appState.currentFrame + 1
);
if (log) {
if (log.correctedPosition && log.correctedPosition[0] !== null) {
const pos = log.correctedPosition;
const screenX = pos[0] * plotScales.plotScaleX + p.width / 2;
const screenY = p.height * 0.95 - (pos[1] * plotScales.plotScaleY);
const screenY = p.height * 0.95 - pos[1] * plotScales.plotScaleY;
const d = p.dist(p.mouseX, p.mouseY, screenX, screenY);
if (d < radius) {
hoveredItems.push({ type: 'track', data: log, trackId: track.id, screenX, screenY });
hoveredItems.push({
type: "track",
data: log,
trackId: track.id,
screenX,
screenY,
});
}
}
if (togglePredictedPos.checked && log.predictedPosition && log.predictedPosition[0] !== null) {
if (
togglePredictedPos.checked &&
log.predictedPosition &&
log.predictedPosition[0] !== null
) {
const pos = log.predictedPosition;
const screenX = pos[0] * plotScales.plotScaleX + p.width / 2;
const screenY = p.height * 0.95 - (pos[1] * plotScales.plotScaleY);
const screenY = p.height * 0.95 - pos[1] * plotScales.plotScaleY;
const d = p.dist(p.mouseX, p.mouseY, screenX, screenY);
if (d < radius) {
hoveredItems.push({ type: 'prediction', data: log, trackId: track.id, screenX, screenY });
hoveredItems.push({
type: "prediction",
data: log,
trackId: track.id,
screenX,
screenY,
});
}
}
}
}
}
if (hoveredItems.length === 0) return;
// Sort items by their vertical screen position to prevent crossed lines.
hoveredItems.sort((a, b) => a.screenY - b.screenY);
// Generate display text (no changes needed in this part)
// If we aren't hovering over anything, draw nothing.
if (hoveredItems.length === 0) {
return hoveredItems; // Return the empty array
}
// --- Step 2 & 3: Generate Text and Render Tooltip ---
const infoStrings = [];
// ... (The text generation logic remains the same) ...
for (const item of hoveredItems) {
let infoText = '';
let infoText = "";
const data = item.data;
switch (item.type) {
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(2)} | V:${vel}, SNR:${snr}`;
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(
2
)} | V:${vel}, SNR:${snr}`;
break;
case 'cluster':
const rs = data.radialSpeed !== null ? data.radialSpeed.toFixed(2) : 'N/A';
const vx = data.vx !== null ? data.vx.toFixed(2) : 'N/A';
const vy = data.vy !== null ? data.vy.toFixed(2) : 'N/A';
infoText = `Cluster ${data.id} | X:${data.x.toFixed(2)}, Y:${data.y.toFixed(2)} | rSpeed:${rs}, vX:${vx}, vY:${vy}`;
case "cluster":
const rs =
data.radialSpeed !== null ? data.radialSpeed.toFixed(2) : "N/A";
const vx = data.vx !== null ? data.vx.toFixed(2) : "N/A";
const vy = data.vy !== null ? data.vy.toFixed(2) : "N/A";
infoText = `Cluster ${data.id} | X:${data.x.toFixed(
2
)}, Y:${data.y.toFixed(2)} | rSpeed:${rs}, vX:${vx}, vY:${vy}`;
break;
case 'track':
infoText = `Track ${item.trackId} | X:${data.correctedPosition[0].toFixed(2)}, Y:${data.correctedPosition[1].toFixed(2)}`;
case "track":
infoText = `Track ${
item.trackId
} | X:${data.correctedPosition[0].toFixed(
2
)}, Y:${data.correctedPosition[1].toFixed(2)}`;
break;
case 'prediction':
const p_vx = data.predictedVelocity[0] !== null ? data.predictedVelocity[0].toFixed(2) : 'N/A';
const p_vy = data.predictedVelocity[1] !== null ? data.predictedVelocity[1].toFixed(2) : 'N/A';
infoText = `Pred. for ${item.trackId} | X:${data.predictedPosition[0].toFixed(2)}, Y:${data.predictedPosition[1].toFixed(2)} | vX:${p_vx}, vY:${p_vy}`;
case "prediction":
const p_vx =
data.predictedVelocity[0] !== null
? data.predictedVelocity[0].toFixed(2)
: "N/A";
const p_vy =
data.predictedVelocity[1] !== null
? data.predictedVelocity[1].toFixed(2)
: "N/A";
infoText = `Pred. for ${
item.trackId
} | X:${data.predictedPosition[0].toFixed(
2
)}, Y:${data.predictedPosition[1].toFixed(2)} | vX:${p_vx}, vY:${p_vy}`;
break;
}
if (infoText) {
infoStrings.push({text: infoText, color: item.color || null});
infoStrings.push({ text: infoText, color: item.color || null });
}
}
// Render the unified tooltip
p.push();
p.textSize(12);
const lineHeight = 15;
@ -637,50 +681,69 @@ export function handleCloseUpDisplay(p, plotScales) {
for (const strInfo of infoStrings) {
boxWidth = Math.max(boxWidth, p.textWidth(strInfo.text));
}
const boxHeight = (infoStrings.length * lineHeight) + (boxPadding * 2);
boxWidth += (boxPadding * 2);
const boxHeight = infoStrings.length * lineHeight + boxPadding * 2;
boxWidth += boxPadding * 2;
const xOffset = 20;
let boxX = p.mouseX + xOffset;
let boxY = p.mouseY - (boxHeight / 2);
if (boxX + boxWidth > p.width) {
let boxX, lineAnchorX;
if (p.mouseX + xOffset + boxWidth > p.width) {
boxX = p.mouseX - boxWidth - xOffset;
lineAnchorX = boxX + boxWidth;
} else {
boxX = p.mouseX + xOffset;
lineAnchorX = boxX;
}
let boxY = p.mouseY - boxHeight / 2;
boxY = p.constrain(boxY, 0, p.height - boxHeight);
// ... (Highlighting logic remains the same) ...
const highlightColor = p.color(46, 204, 113);
for (let i = 0; i < hoveredItems.length; i++) {
const item = hoveredItems[i];
for (const item of hoveredItems) {
p.noFill();
p.stroke(highlightColor);
p.strokeWeight(2);
p.ellipse(item.screenX, item.screenY, 15, 15);
p.strokeWeight(1);
p.line(boxX + boxPadding, boxY + boxPadding + (i * lineHeight) + (lineHeight / 2), item.screenX, item.screenY);
}
const bgColor = document.documentElement.classList.contains('dark') ? p.color(20, 20, 30, 220) : p.color(245, 245, 245, 220);
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);
p.rect(boxX, boxY, boxWidth, boxHeight, 4);
// ======================= CHANGE START =======================
// Draw the text inside the box, applying colors where needed
const defaultTextColor = document.documentElement.classList.contains('dark') ? p.color(230) : p.color(20);
p.noStroke();
p.textAlign(p.LEFT, p.TOP);
const defaultTextColor = document.documentElement.classList.contains("dark")
? p.color(230)
: p.color(20);
const dividerColor = document.documentElement.classList.contains("dark")
? p.color(80)
: p.color(200);
for (let i = 0; i < infoStrings.length; i++) {
const info = infoStrings[i];
// If a color is specified for this line, use it. Otherwise, use the default.
const lineY = boxY + boxPadding + i * lineHeight;
if (i > 0) {
p.stroke(dividerColor);
p.strokeWeight(0.5);
p.line(boxX + 1, lineY, boxX + boxWidth - 1, lineY);
}
p.noStroke();
p.textAlign(p.LEFT, p.TOP);
p.fill(info.color || defaultTextColor);
p.text(info.text, boxX + boxPadding, boxY + boxPadding + (i * lineHeight));
}
// ======================== CHANGE END ========================
p.text(info.text, boxX + boxPadding, lineY);
const item = hoveredItems[i];
const lineAnchorY = lineY + lineHeight / 2;
p.stroke(highlightColor);
p.strokeWeight(1);
p.line(lineAnchorX, lineAnchorY, item.screenX, item.screenY);
}
p.pop();
// Return the list of hovered items for other functions (like the zoom window) to use.
return hoveredItems;
}
export function drawCovarianceEllipse(
@ -780,6 +843,390 @@ export function drawEgoVehicle(p, plotScales) {
p.pop();
}
//OLD_Solid Fill Logic
/**
* Draws the defined regions of interest (ROI) based on dynamic data from the current frame.
* @param {p5} p - The p5 instance to draw on.
* @param {object} frameData - The data for the current radar frame.
* @param {object} plotScales - The calculated scales for plotting.
*/
/**
*/
export function drawRegionsOfInterest(p, frameData, plotScales) {
// --- THIS CHECK IS ESSENTIAL AND MUST NOT BE REMOVED ---
// It gracefully handles frames that do not have the barrier data.
if (!frameData || !frameData.filtered_barrier_x) {
console.warn(
`Skipping bcoz no filtered barrier track in frame ${appState.currentFrame}. `,
frameData
);
return; // Exit the function if the data is missing for this frame.
}
//check here once
const isDark = document.documentElement.classList.contains("dark");
// Using brighter, more visible colors with transparency
const tracksRegionColor = isDark
? p.color(137, 207, 240, 50)
: p.color(173, 216, 230, 80);
const closeRegionColor = isDark
? p.color(255, 182, 193, 60)
: p.color(255, 182, 193, 90);
const [left, right] = frameData.filtered_barrier_x;
p.push();
p.stroke(1);
p.strokeWeight(1);
p.noFill();
p.rectMode(p.CORNERS); // console.warn(`Skipping bcoz no filtered barrier track in frame ${appState.currentFrame}. `, frameData);
// --- Draw Tracks Region ---
p.fill(tracksRegionColor);
p.rect(
left * plotScales.plotScaleX,
ROI_TRACKS_Y_MIN * plotScales.plotScaleY,
right * plotScales.plotScaleX,
ROI_TRACKS_Y_MAX * plotScales.plotScaleY
);
// --- Draw Close Region ---
p.fill(closeRegionColor);
p.rect(
left * plotScales.plotScaleX,
ROI_CLOSE_Y_MIN * plotScales.plotScaleY,
right * plotScales.plotScaleX,
ROI_CLOSE_Y_MAX * plotScales.plotScaleY
);
p.pop();
}
//OLD_Solid Fill Logic
/**
* Draws the cluster centroids on the radar canvas as an asterisk.
* Handles cases where a single cluster is an object instead of an array.
* @param {p5} p - The p5 instance.
* @param {Array|object} clustersInput - The cluster data for the current frame.
* @param {object} plotScales - The calculated scales for plotting.
*/
export function drawClusterCentroids(p, clustersInput, plotScales) {
if (!clustersInput) {
return; // Do nothing if there's no cluster data
}
// --- START: Robustness Fix ---
// This check handles the data inconsistency. If clustersInput is not an array,
// we wrap the single cluster object in an array so the loop works consistently.
const clusters = Array.isArray(clustersInput)
? clustersInput
: [clustersInput];
// --- END: Robustness Fix ---
if (clusters.length === 0) {
return; // Exit if the resulting array is empty
}
const localClusterColors = clusterColors(p);
for (const cluster of clusters) {
if (
cluster &&
typeof cluster.x === "number" &&
typeof cluster.y === "number"
) {
const x = cluster.x * plotScales.plotScaleX;
const y = cluster.y * plotScales.plotScaleY;
const color =
cluster.id > 0
? localClusterColors[(cluster.id - 1) % localClusterColors.length]
: p.color(128);
p.push();
p.stroke(color);
p.strokeWeight(1.5);
const armLength = 5;
p.line(x, y - armLength, x, y + armLength);
p.line(x - armLength, y, x + armLength, y);
p.line(
x - armLength * 0.7,
y - armLength * 0.7,
x + armLength * 0.7,
y + armLength * 0.7
);
p.line(
x + armLength * 0.7,
y - armLength * 0.7,
x - armLength * 0.7,
y + armLength * 0.7
);
p.pop();
}
}
}
//--- drawClusterCentroids function---//
// old trial functions to replace the close up display.
// In src/drawUtils.js
// Replace the ENTIRE 'handleCloseUpDisplay' function with these TWO new functions:
// /**
// * Finds all radar elements (points, tracks, etc.) under the mouse cursor.
// * @param {p5} p - The p5 instance (for mouse coordinates and distance checks).
// * @param {object} plotScales - The calculated scales for plotting.
// * @returns {Array} An array of hovered item objects.
// */
// export function findHoveredItems(p, plotScales) {
// const frameData = appState.vizData.radarFrames[appState.currentFrame];
// if (!frameData) return [];
// const hoveredItems = [];
// const radius = 10;
// const localClusterColors = clusterColors(p);
// // Find hovered points
// 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);
// if (p.dist(p.mouseX, p.mouseY, screenX, screenY) < radius) {
// hoveredItems.push({ type: 'point', data: pt, screenX, screenY });
// }
// }
// }
// // Find hovered cluster centroids
// if (toggleClusterColor.checked && frameData.clusters) {
// const clusters = Array.isArray(frameData.clusters) ? frameData.clusters : [frameData.clusters];
// for (const cluster of clusters) {
// if (cluster.x === null || cluster.y === null) continue;
// const screenX = cluster.x * plotScales.plotScaleX + p.width / 2;
// const screenY = p.height * 0.95 - (cluster.y * plotScales.plotScaleY);
// if (p.dist(p.mouseX, p.mouseY, screenX, screenY) < radius) {
// const color = cluster.id > 0 ? localClusterColors[(cluster.id - 1) % localClusterColors.length] : p.color(128);
// hoveredItems.push({ type: 'cluster', data: cluster, screenX, screenY, color });
// }
// }
// }
// // Find hovered tracks and predictions
// if (appState.vizData.tracks) {
// for (const track of appState.vizData.tracks) {
// const log = track.historyLog.find(log => log.frameIdx === appState.currentFrame + 1);
// if (log) {
// if (log.correctedPosition && log.correctedPosition[0] !== null) {
// const pos = log.correctedPosition;
// const screenX = pos[0] * plotScales.plotScaleX + p.width / 2;
// const screenY = p.height * 0.95 - (pos[1] * plotScales.plotScaleY);
// if (p.dist(p.mouseX, p.mouseY, screenX, screenY) < radius) {
// hoveredItems.push({ type: 'track', data: log, trackId: track.id, screenX, screenY });
// }
// }
// if (togglePredictedPos.checked && log.predictedPosition && log.predictedPosition[0] !== null) {
// const pos = log.predictedPosition;
// const screenX = pos[0] * plotScales.plotScaleX + p.width / 2;
// const screenY = p.height * 0.95 - (pos[1] * plotScales.plotScaleY);
// if (p.dist(p.mouseX, p.mouseY, screenX, screenY) < radius) {
// hoveredItems.push({ type: 'prediction', data: log, trackId: track.id, screenX, screenY });
// }
// }
// }
// }
// }
// hoveredItems.sort((a, b) => a.screenY - b.screenY);
// return hoveredItems;
// }
// /**
// * Draws the visual tooltip and connectors for a given list of hovered items.
// * @param {p5} p - The p5 instance to draw with.
// * @param {Array} hoveredItems - An array of items from findHoveredItems.
// */
// export function drawTooltip(p, hoveredItems) {
// if (hoveredItems.length === 0) return;
// const infoStrings = [];
// // Generate display text
// 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 });
// }
// }
// p.push();
// p.textSize(12);
// const lineHeight = 15;
// const boxPadding = 8;
// let boxWidth = 0;
// infoStrings.forEach(info => {
// boxWidth = Math.max(boxWidth, p.textWidth(info.text));
// });
// const boxHeight = (infoStrings.length * lineHeight) + (boxPadding * 2);
// boxWidth += (boxPadding * 2);
// const xOffset = 20;
// let boxX = p.mouseX + xOffset;
// if (boxX + boxWidth > p.width) {
// boxX = p.mouseX - boxWidth - xOffset;
// }
// let boxY = p.mouseY - (boxHeight / 2);
// boxY = p.constrain(boxY, 0, p.height - boxHeight);
// // Draw highlights and connectors
// const highlightColor = p.color(46, 204, 113);
// hoveredItems.forEach((item, i) => {
// p.noFill();
// p.stroke(highlightColor);
// p.strokeWeight(2);
// p.ellipse(item.screenX, item.screenY, 15, 15);
// p.strokeWeight(1);
// const lineAnchorX = boxX < p.mouseX ? boxX + boxWidth : boxX;
// p.line(lineAnchorX, boxY + boxPadding + (i * lineHeight) + (lineHeight / 2), item.screenX, item.screenY);
// });
// // Draw the box and text
// 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);
// p.rect(boxX, boxY, boxWidth, boxHeight, 4);
// 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));
// });
// p.pop();
// }
// // /**
// // * Renders a high-fidelity, zoomed-in view of the scene around the mouse cursor.
// // * @param {p5} p - The p5 instance.
// // * @param {object} plotScales - The calculated scales for plotting.
// // * @param {Array} hoveredItems - The array of items currently under the mouse.
// // */
// // export function drawZoomWindow(p, plotScales, hoveredItems) {
// // // --- Zoom Window Configuration (easily modifiable) ---
// // // The magnification level. 4.0 means 4x zoom.
// // const zoomFactor = 4.0;
// // // The output size of the zoom window on the screen, in pixels.
// // const zoomWindowWidth = 250;
// // const zoomWindowHeight = 250;
// // // Position the zoom window in the bottom-right of the canvas.
// // const boxX = p.width - zoomWindowWidth - 20;
// // const boxY = p.height - zoomWindowHeight - 20;
// // p.push(); // Save the current global drawing state.
// // // --- Create a "Portal" to the Zoomed View ---
// // // We use a clipping mask to ensure the zoomed content doesn't spill out.
// // p.drawingContext.save();
// // p.drawingContext.rect(boxX, boxY, zoomWindowWidth, zoomWindowHeight);
// // p.drawingContext.clip();
// // // We now transform the entire canvas coordinate system for the redraw.
// // p.translate(boxX, boxY); // 1. Move origin to the zoom box's corner.
// // p.scale(zoomFactor); // 2. Scale everything up.
// // // 3. Translate so the mouse position is in the center of the box.
// // p.translate(-p.mouseX + zoomWindowWidth / (2 * zoomFactor), -p.mouseY + zoomWindowHeight / (2 * zoomFactor));
// // // --- Redraw the Entire Scene in the New Zoomed Coordinate System ---
// // // This provides a high-fidelity, not just pixelated, zoom.
// // p.background(document.documentElement.classList.contains('dark') ? p.color(55, 65, 81) : 255);
// // p.image(p.get(), 0, 0); // A trick to redraw the static background buffer
// // p.push(); // Nested push for the main radar transformations.
// // p.translate(p.width / 2, p.height * 0.95);
// // p.scale(1, -1);
// // const frameData = appState.vizData.radarFrames[appState.currentFrame];
// // drawAxes(p, plotScales);
// // drawEgoVehicle(p, plotScales);
// // if (frameData) {
// // drawRegionsOfInterest(p, frameData, plotScales);
// // if (toggleTracks.checked) {
// // drawTrajectories(p, plotScales);
// // drawTrackMarkers(p, plotScales);
// // }
// // drawPointCloud(p, frameData.pointCloud, plotScales);
// // if (toggleClusterColor.checked) {
// // drawClusterCentroids(p, frameData.clusters, plotScales);
// // }
// // // Redraw predicted positions if toggled
// // if (togglePredictedPos.checked) {
// // for (const track of appState.vizData.tracks) {
// // const log = track.historyLog.find(log => log.frameIdx === appState.currentFrame + 1);
// // 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); 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();
// // }
// // }
// // }
// // }
// // p.pop(); // End of radar transformations.
// // // --- Redraw Tooltip and Connectors ---
// // // We re-run the original tooltip function, which will now draw inside our zoomed view,
// // // making the connector lines perfectly accurate.
// // handleCloseUpDisplay(p, plotScales);
// // // Clean up the clipping mask.
// // p.drawingContext.restore();
// // // --- Draw Border and Crosshairs on Top of Everything ---
// // p.noFill();
// // p.stroke(46, 204, 113); // Highlight green border.
// // p.strokeWeight(2);
// // p.rect(boxX, boxY, zoomWindowWidth, zoomWindowHeight);
// // // Red crosshairs to mark the exact mouse position.
// // const crosshairSize = 10;
// // p.stroke(255, 0, 0, 150);
// // p.strokeWeight(1);
// // p.line(boxX + zoomWindowWidth/2 - crosshairSize, boxY + zoomWindowHeight/2, boxX + zoomWindowWidth/2 + crosshairSize, boxY + zoomWindowHeight/2);
// // p.line(boxX + zoomWindowWidth/2, boxY + zoomWindowHeight/2 - crosshairSize, boxX + zoomWindowWidth/2, boxY + zoomWindowHeight/2 + crosshairSize);
// // p.pop(); // Restore the original global drawing state.
// // }
// OLD HATCH FILL logic
// /**
// * Draws a hatched pattern inside a rectangle defined by corner points.
// * This is a new helper function.
@ -902,114 +1349,3 @@ export function drawEgoVehicle(p, plotScales) {
// b.pop();
// }
//OLD_Solid Fill Logic
/**
* Draws the defined regions of interest (ROI) based on dynamic data from the current frame.
* @param {p5} p - The p5 instance to draw on.
* @param {object} frameData - The data for the current radar frame.
* @param {object} plotScales - The calculated scales for plotting.
*/
/**
*/
export function drawRegionsOfInterest(p, frameData, plotScales) {
// --- THIS CHECK IS ESSENTIAL AND MUST NOT BE REMOVED ---
// It gracefully handles frames that do not have the barrier data.
if (!frameData || !frameData.filtered_barrier_x) {
console.warn(`Skipping bcoz no filtered barrier track in frame ${appState.currentFrame}. `, frameData);
return; // Exit the function if the data is missing for this frame.
}
//check here once
const isDark = document.documentElement.classList.contains("dark");
// Using brighter, more visible colors with transparency
const tracksRegionColor = isDark
? p.color(137, 207, 240, 50)
: p.color(173, 216, 230, 80);
const closeRegionColor = isDark
? p.color(255, 182, 193, 60)
: p.color(255, 182, 193, 90);
const [left, right] = frameData.filtered_barrier_x;
p.push();
p.stroke(1);
p.strokeWeight(1);
p.noFill();
p.rectMode(p.CORNERS);// console.warn(`Skipping bcoz no filtered barrier track in frame ${appState.currentFrame}. `, frameData);
// --- Draw Tracks Region ---
p.fill(tracksRegionColor);
p.rect(
left * plotScales.plotScaleX,
ROI_TRACKS_Y_MIN * plotScales.plotScaleY,
right * plotScales.plotScaleX,
ROI_TRACKS_Y_MAX * plotScales.plotScaleY
);
// --- Draw Close Region ---
p.fill(closeRegionColor);
p.rect(
left * plotScales.plotScaleX,
ROI_CLOSE_Y_MIN * plotScales.plotScaleY,
right * plotScales.plotScaleX,
ROI_CLOSE_Y_MAX * plotScales.plotScaleY
);
p.pop();
}
//OLD_Solid Fill Logic
/**
* Draws the cluster centroids on the radar canvas as an asterisk.
* Handles cases where a single cluster is an object instead of an array.
* @param {p5} p - The p5 instance.
* @param {Array|object} clustersInput - The cluster data for the current frame.
* @param {object} plotScales - The calculated scales for plotting.
*/
export function drawClusterCentroids(p, clustersInput, plotScales) {
if (!clustersInput) {
return; // Do nothing if there's no cluster data
}
// --- START: Robustness Fix ---
// This check handles the data inconsistency. If clustersInput is not an array,
// we wrap the single cluster object in an array so the loop works consistently.
const clusters = Array.isArray(clustersInput) ? clustersInput : [clustersInput];
// --- END: Robustness Fix ---
if (clusters.length === 0) {
return; // Exit if the resulting array is empty
}
const localClusterColors = clusterColors(p);
for (const cluster of clusters) {
if (cluster && typeof cluster.x === 'number' && typeof cluster.y === 'number') {
const x = cluster.x * plotScales.plotScaleX;
const y = cluster.y * plotScales.plotScaleY;
const color = cluster.id > 0
? localClusterColors[(cluster.id - 1) % localClusterColors.length]
: p.color(128);
p.push();
p.stroke(color);
p.strokeWeight(1.5);
const armLength = 5;
p.line(x, y - armLength, x, y + armLength);
p.line(x - armLength, y, x + armLength, y);
p.line(x - armLength * 0.7, y - armLength * 0.7, x + armLength * 0.7, y + armLength * 0.7);
p.line(x + armLength * 0.7, y - armLength * 0.7, x - armLength * 0.7, y + armLength * 0.7);
p.pop();
}
}
}
//--- drawClusterCentroids function---//

27
steps/src/main.js

@ -16,6 +16,7 @@
// - main.js: The main application entry point that wires everything
// ===========================================================================================================
import { zoomSketch } from "./p5/zoomSketch.js";
import { showModal, updateModalProgress } from "./modal.js"; // Modify this import
import { animationLoop } from "./sync.js";
import { radarSketch } from "./p5/radarSketch.js";
@ -93,7 +94,7 @@ import {
fullscreenEnterIcon,
fullscreenExitIcon,
menuScrim,
toggleConfirmedOnly
toggleConfirmedOnly,
} from "./dom.js";
import { initializeTheme } from "./theme.js";
@ -273,7 +274,6 @@ saveSessionBtn.addEventListener("click", () => {
URL.revokeObjectURL(url);
});
/**
* A callback that runs for every new video frame presented to the screen.
* It calculates the time since the last frame to measure video performance.
@ -755,7 +755,6 @@ colorToggles.forEach((t) => {
if (t === toggleDebugOverlay || t === toggleDebug2Overlay) {
updateDebugOverlay(videoPlayer.currentTime);
updatePersistentOverlays(videoPlayer.currentTime);
}
});
});
@ -804,7 +803,7 @@ document.addEventListener("keydown", (event) => {
"4",
"t",
"d",
"c",
"g",
"r",
"p",
"a",
@ -865,7 +864,7 @@ document.addEventListener("keydown", (event) => {
if (key === "d") {
toggleVelocity.click();
}
if (key === "c") {
if (key === "g") {
toggleCloseUp.click();
}
if (key === "r") {
@ -882,13 +881,13 @@ document.addEventListener("keydown", (event) => {
toggleDebugOverlay.click();
toggleDebug2Overlay.click();
if (isDebug1Visible && isDebug2Visible) {
radarInfoOverlay.classList.add("hidden");
videoInfoOverlay.classList.add("hidden");
return;
}
// Otherwise, make sure they are visible.
radarInfoOverlay.classList.remove("hidden");
videoInfoOverlay.classList.remove("hidden");
radarInfoOverlay.classList.add("hidden");
videoInfoOverlay.classList.add("hidden");
return;
}
// Otherwise, make sure they are visible.
radarInfoOverlay.classList.remove("hidden");
videoInfoOverlay.classList.remove("hidden");
}
if (key === "m") {
if (collapsibleMenu.classList.contains("-translate-x-full")) {
@ -1041,7 +1040,11 @@ document.addEventListener("DOMContentLoaded", () => {
if (!appState.p5_instance) {
appState.p5_instance = new p5(radarSketch);
}
if (!appState.zoomSketchInstance) {
appState.zoomSketchInstance = new p5(zoomSketch, 'zoom-canvas-container');
}
}
//document.getElementById("zoom-panel").style.display = "none";
};
if (jsonBlob) {

83
steps/src/p5/radarSketch.js

@ -27,7 +27,7 @@ import {
drawCovarianceEllipse,
ttcColors,
drawRegionsOfInterest,
drawClusterCentroids
drawClusterCentroids,
} from "../drawUtils.js";
export const radarSketch = function (p) {
@ -39,6 +39,10 @@ export const radarSketch = function (p) {
// p5.Graphics buffers for static elements to optimize drawing
let staticBackgroundBuffer, snrLegendBuffer, trackLegendBuffer;
// 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
@ -60,6 +64,41 @@ export const radarSketch = function (p) {
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);
@ -161,9 +200,8 @@ export const radarSketch = function (p) {
// Draw the point cloud for the current frame
drawPointCloud(p, frameData.pointCloud, plotScales);
// Draw cluster centroids if enabled
if(toggleClusterColor.checked){
drawClusterCentroids(p, frameData.clusters, plotScales);
if (toggleClusterColor.checked) {
drawClusterCentroids(p, frameData.clusters, plotScales);
}
}
p.pop();
@ -173,16 +211,37 @@ export const radarSketch = function (p) {
if (toggleTracks.checked) {
p.image(
trackLegendBuffer,
p.width - trackLegendBuffer.width - 0,
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 zoomPanel = document.getElementById("zoom-panel");
if (appState.isCloseUpMode) {
handleCloseUpDisplay(p, plotScales);
const hoveredItems = handleCloseUpDisplay(p, plotScales);
if (hoveredItems.length > 0) {
zoomPanel.style.display = "block"; // show the panel
if (
appState.zoomSketchInstance &&
appState.zoomSketchInstance.updateAndDraw
) {
appState.zoomSketchInstance.updateAndDraw(
p.mouseX,
p.mouseY,
hoveredItems,
plotScales
);
}
} else {
zoomPanel.style.display = "none";
}
} else {
zoomPanel.style.display = "none";
}
// --- Legend Drawing ---
// Draw the SNR legend if enabled
if (toggleSnrColor.checked) {
p.image(snrLegendBuffer, 10, p.height - snrLegendBuffer.height - 10);
@ -244,7 +303,6 @@ export const radarSketch = function (p) {
b.pop();
};
// Handle window resizing event
p.windowResized = function () {
p.resizeCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight);
@ -257,12 +315,11 @@ export const radarSketch = function (p) {
calculatePlotScales();
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
if (appState.vizData) {p.redraw()
};
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
@ -319,6 +376,8 @@ export const radarSketch = function (p) {
p.resizeCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight);
// BUG FIX 2: Re-create the buffer instead of resizing it
staticBackgroundBuffer = p.createGraphics(p.width, p.height);
trackLegendBuffer = p.createGraphics(120, 120);
p.drawTrackLegendToBuffer();
calculatePlotScales();
// Re-draw the static content to the new buffer
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);

416
steps/src/p5/zoomSketch.js

@ -0,0 +1,416 @@
import { appState } from "../state.js";
import {
drawAxes,
drawPointCloud,
drawTrajectories,
drawEgoVehicle,
drawTrackMarkers,
drawClusterCentroids,
drawRegionsOfInterest,
drawCovarianceEllipse,
clusterColors, // We need to import clusterColors for the tooltip
} from "../drawUtils.js";
import {
toggleTracks,
toggleClusterColor,
togglePredictedPos,
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;
// 1. Generate text content
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
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();
// --- Start of Tweakable Parameters ---
const zoomFactor = appState.zoomFactor || 6;
// VISUALS: Adjust these numbers to change the tooltip's appearance
const BASE_FONT_SIZE = 12;
const BASE_LINE_HEIGHT = 15;
const BASE_PADDING = 8;
const BASE_HIGHLIGHT_THICKNESS = 2;
const BASE_LINE_THICKNESS = 1;
const BASE_DISTANCE_OFFSET = 45; // <-- How far the tooltip is from the items
// COLORS
const highlightColor = p.color(46, 204, 113); // Green for border and lines
const bgColor = document.documentElement.classList.contains("dark")
? p.color(20, 20, 30, 220)
: p.color(245, 245, 245, 220);
const defaultTextColor = document.documentElement.classList.contains("dark")
? p.color(230)
: p.color(20);
// --- End of Tweakable Parameters ---
// Compensate for zoom factor
p.textSize(BASE_FONT_SIZE / zoomFactor);
const lineHeight = BASE_LINE_HEIGHT / zoomFactor;
const boxPadding = BASE_PADDING / zoomFactor;
const xOffset = BASE_DISTANCE_OFFSET / 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;
// Smart Positioning Logic
let boxX, connectorAnchorX;
if (mainMouseX > appState.p5_instance.width / 2) {
boxX = avgX - xOffset - boxWidth;
connectorAnchorX = boxX + boxWidth;
} else {
boxX = avgX + xOffset;
connectorAnchorX = boxX;
}
const boxY = avgY - boxHeight / 2;
// Draw highlighting circles
hoveredItems.forEach((item) => {
p.noFill();
p.stroke(highlightColor);
p.strokeWeight(BASE_HIGHLIGHT_THICKNESS / zoomFactor);
p.ellipse(item.screenX, item.screenY, 15 / zoomFactor, 15 / zoomFactor);
});
// Draw the tooltip box
p.fill(bgColor);
p.stroke(highlightColor);
p.strokeWeight(BASE_LINE_THICKNESS / zoomFactor);
p.rect(boxX, boxY, boxWidth, boxHeight, 4 / zoomFactor);
// Draw the text (with italics for prediction)
p.noStroke();
p.textAlign(p.LEFT, p.TOP);
infoStrings.forEach((info, i) => {
p.fill(info.color || defaultTextColor);
if (hoveredItems[i].type === "prediction") {
p.textStyle(p.ITALIC);
}
p.text(info.text, boxX + boxPadding, boxY + boxPadding + i * lineHeight);
p.textStyle(p.NORMAL); // Reset to normal for the next line
});
// Draw individual connector lines
hoveredItems.forEach((item, i) => {
p.stroke(highlightColor);
p.strokeWeight(BASE_LINE_THICKNESS / zoomFactor);
const connectorAnchorY =
boxY + boxPadding + i * lineHeight + lineHeight / 2;
p.line(connectorAnchorX, connectorAnchorY, item.screenX, item.screenY);
});
p.pop();
}
export const zoomSketch = function (p) {
let plotScales = { plotScaleX: 1, plotScaleY: 1 };
let lastUpdate = { mainMouseX: 0, mainMouseY: 0, hoveredItems: [] };
let canvas = null;
const containerId = "zoom-canvas-container";
appState.zoomFactor = 4; // Set a default zoom factor in the global state
p.setup = function () {
p.noLoop();
};
p.updateAndDraw = function (mainMouseX, mainMouseY, hoveredItems, scales) {
lastUpdate = { mainMouseX, mainMouseY, hoveredItems };
plotScales = scales;
if (!canvas) {
const container = document.getElementById(containerId);
if (container && container.offsetWidth > 0) {
canvas = p.createCanvas(container.offsetWidth, container.offsetHeight);
canvas.parent(containerId);
} else {
return;
}
}
p.redraw();
};
p.draw = function () {
if (!appState.vizData || !canvas) return;
p.background(
document.documentElement.classList.contains("dark")
? p.color(55, 65, 81)
: 255
);
const { mainMouseX, mainMouseY, hoveredItems } = lastUpdate;
p.push(); // Start zoom transformations
p.translate(
p.width / 2 - mainMouseX * appState.zoomFactor,
p.height / 2 - mainMouseY * appState.zoomFactor
);
p.scale(appState.zoomFactor);
// --- Redraw the scene from scratch ---
if (appState.p5_instance && appState.p5_instance.getStaticBackground) {
p.image(
appState.p5_instance.getStaticBackground(),
0,
0,
appState.p5_instance.width,
appState.p5_instance.height
);
}
p.push(); // Start radar transformations
p.translate(
appState.p5_instance.width / 2,
appState.p5_instance.height * 0.95
);
p.scale(1, -1);
const frameData = appState.vizData.radarFrames[appState.currentFrame];
drawAxes(p, plotScales);
drawEgoVehicle(p, plotScales);
if (frameData) {
drawTrackMarkers(p, plotScales);
drawRegionsOfInterest(p, frameData, plotScales);
if (toggleTracks.checked) {
drawTrajectories(p, plotScales);
}
drawPointCloud(p, frameData.pointCloud, plotScales);
if (toggleClusterColor.checked) {
drawClusterCentroids(p, frameData.clusters, plotScales);
}
if (togglePredictedPos.checked) {
for (const track of appState.vizData.tracks) {
const log = track.historyLog.find(
(log) => log.frameIdx === appState.currentFrame + 1
);
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();
}
}
}
}
p.pop(); // End radar transformations
// --- Call the new, self-contained tooltip function ---
drawZoomTooltip(p, hoveredItems, mainMouseX);
p.pop(); // End zoom transformations
// --- START: DRAW TITLE OVERLAY ---
// This code runs *after* the zoom transformations have been popped,
// so it draws directly onto the canvas as a fixed UI element.
p.push();
const titleLabel = document.getElementById('toggle-close-up').parentElement;
const titleText = titleLabel ? titleLabel.textContent.trim() : 'Zoom Mode';
const textColor = document.documentElement.classList.contains("dark") ? 220 : 80;
p.fill(textColor);
p.noStroke();
p.textSize(16);
p.textAlign(p.LEFT, p.TOP);
p.textStyle(p.BOLD);
p.text(titleText, 10, 10);
p.pop();
// --- END: DRAW TITLE OVERLAY ---
// --- Draw Crosshairs ---
p.stroke(255, 0, 0, 150);
p.strokeWeight(1.5);
p.line(p.width / 2 - 15, p.height / 2, p.width / 2 + 15, p.height / 2);
p.line(p.width / 2, p.height / 2 - 15, p.width / 2, p.height / 2 + 15);
};
p.windowResized = function () {
if (canvas) {
const container = document.getElementById(containerId);
if (container) {
p.resizeCanvas(container.offsetWidth, container.offsetHeight);
}
}
};
};

1
steps/src/state.js

@ -1,6 +1,7 @@
export const appState = {
// Stores the parsed visualization data (radar frames, tracks, etc.)
vizData: null,
zoomSketchInstance: null, // Add this line
// Stores the processed CAN bus data (speed, time)
videoStartDate: null,
// The timestamp (in milliseconds) of the first radar frame, extracted from the JSON filename

Loading…
Cancel
Save