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

416 lines
15 KiB

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 = 2;
const BASE_DISTANCE_OFFSET = 65; // <-- 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);
}
}
};
};