memoize to prevent unnecessary renders

This commit is contained in:
Josh Hawkins 2025-10-07 14:40:59 -05:00
parent ddd029b7d9
commit 5fdd426e12

View File

@ -103,6 +103,7 @@ export default function ObjectTrackOverlay({
})); }));
}, [config, camera, getZoneColor, currentObjectZones]); }, [config, camera, getZoneColor, currentObjectZones]);
// get saved path points from event
const savedPathPoints = useMemo(() => { const savedPathPoints = useMemo(() => {
return ( return (
eventData?.[0].data?.path_data?.map( eventData?.[0].data?.path_data?.map(
@ -116,6 +117,7 @@ export default function ObjectTrackOverlay({
); );
}, [eventData]); }, [eventData]);
// timeline points for selected event
const eventSequencePoints = useMemo(() => { const eventSequencePoints = useMemo(() => {
return ( return (
objectTimeline objectTimeline
@ -124,8 +126,8 @@ export default function ObjectTrackOverlay({
const [left, top, width, height] = event.data.box!; const [left, top, width, height] = event.data.box!;
return { return {
x: left + width / 2, // Center x-coordinate x: left + width / 2, // Center x
y: top + height, // Bottom y-coordinate y: top + height, // Bottom y
timestamp: event.timestamp, timestamp: event.timestamp,
lifecycle_item: event, lifecycle_item: event,
}; };
@ -152,6 +154,7 @@ export default function ObjectTrackOverlay({
); );
}, [savedPathPoints, eventSequencePoints, config, camera, currentTime]); }, [savedPathPoints, eventSequencePoints, config, camera, currentTime]);
// get absolute positions on the svg canvas for each point
const getAbsolutePositions = useCallback(() => { const getAbsolutePositions = useCallback(() => {
if (!pathPoints) return []; if (!pathPoints) return [];
return pathPoints.map((point) => { return pathPoints.map((point) => {
@ -165,7 +168,7 @@ export default function ObjectTrackOverlay({
timestamp: point.timestamp, timestamp: point.timestamp,
lifecycle_item: lifecycle_item:
timelineEntry || timelineEntry ||
(point.box (point.box // normal path point
? { ? {
timestamp: point.timestamp, timestamp: point.timestamp,
camera: camera, camera: camera,
@ -220,34 +223,61 @@ export default function ObjectTrackOverlay({
[typeColorMap], [typeColorMap],
); );
if (!pathPoints.length || !config) { const handlePointClick = useCallback(
return null; (timestamp: number) => {
} onSeekToTime?.(timestamp);
},
[onSeekToTime],
);
// Get the object color from the first point's label // render bounding box for object at current time if we have a timeline entry
const objectColor = pathPoints[0]?.label const currentBoundingBox = useMemo(() => {
if (!objectTimeline) return null;
// Find the most recent timeline event at or before effective current time with a bounding box
const relevantEvents = objectTimeline
.filter(
(event) => event.timestamp <= effectiveCurrentTime && event.data.box,
)
.sort((a, b) => b.timestamp - a.timestamp); // Most recent first
const currentEvent = relevantEvents[0];
if (!currentEvent?.data.box) return null;
const [left, top, width, height] = currentEvent.data.box;
return {
left,
top,
width,
height,
centerX: left + width / 2,
centerY: top + height,
};
}, [objectTimeline, effectiveCurrentTime]);
const objectColor = useMemo(() => {
return pathPoints[0]?.label
? getObjectColor(pathPoints[0].label) ? getObjectColor(pathPoints[0].label)
: "rgb(255, 0, 0)"; : "rgb(255, 0, 0)";
const objectColorArray = pathPoints[0]?.label }, [pathPoints, getObjectColor]);
const objectColorArray = useMemo(() => {
return pathPoints[0]?.label
? getObjectColor(pathPoints[0].label).match(/\d+/g)?.map(Number) || [ ? getObjectColor(pathPoints[0].label).match(/\d+/g)?.map(Number) || [
255, 0, 0, 255, 0, 0,
] ]
: [255, 0, 0]; : [255, 0, 0];
}, [pathPoints, getObjectColor]);
const absolutePositions = getAbsolutePositions(); const absolutePositions = useMemo(
() => getAbsolutePositions(),
[getAbsolutePositions],
);
return ( // render any zones for object at current time
<svg const zonePolygons = useMemo(() => {
className={cn(className)} return zones.map((zone) => {
viewBox={`0 0 ${videoWidth} ${videoHeight}`}
style={{
width: "100%",
height: "100%",
}}
preserveAspectRatio="xMidYMid slice"
>
{/* Render zones */}
{zones.map((zone) => {
// Convert zone coordinates from normalized (0-1) to pixel coordinates // Convert zone coordinates from normalized (0-1) to pixel coordinates
const points = zone.coordinates const points = zone.coordinates
.split(",") .split(",")
@ -262,19 +292,40 @@ export default function ObjectTrackOverlay({
}, []) }, [])
.join(","); .join(",");
return {
key: zone.name,
points,
fill: `rgba(${zone.color.replace("rgb(", "").replace(")", "")}, 0.3)`,
stroke: zone.color,
};
});
}, [zones, videoWidth, videoHeight]);
if (!pathPoints.length || !config) {
return null;
}
return ( return (
<svg
className={cn(className)}
viewBox={`0 0 ${videoWidth} ${videoHeight}`}
style={{
width: "100%",
height: "100%",
}}
preserveAspectRatio="xMidYMid slice"
>
{zonePolygons.map((zone) => (
<polygon <polygon
key={zone.name} key={zone.key}
points={points} points={zone.points}
fill={`rgba(${zone.color.replace("rgb(", "").replace(")", "")}, 0.3)`} fill={zone.fill}
stroke={zone.color} stroke={zone.stroke}
strokeWidth="5" strokeWidth="5"
opacity="0.7" opacity="0.7"
/> />
); ))}
})}
{/* Draw path connecting the points */}
{absolutePositions.length > 1 && ( {absolutePositions.length > 1 && (
<path <path
d={generateStraightPath(absolutePositions)} d={generateStraightPath(absolutePositions)}
@ -286,7 +337,6 @@ export default function ObjectTrackOverlay({
/> />
)} )}
{/* Draw points with tooltips */}
{absolutePositions.map((pos, index) => ( {absolutePositions.map((pos, index) => (
<Tooltip key={`point-${index}`}> <Tooltip key={`point-${index}`}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@ -301,9 +351,7 @@ export default function ObjectTrackOverlay({
stroke="white" stroke="white"
strokeWidth="3" strokeWidth="3"
style={{ cursor: onSeekToTime ? "pointer" : "default" }} style={{ cursor: onSeekToTime ? "pointer" : "default" }}
onClick={() => { onClick={() => handlePointClick(pos.timestamp)}
onSeekToTime?.(pos.timestamp);
}}
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
@ -321,42 +369,22 @@ export default function ObjectTrackOverlay({
</Tooltip> </Tooltip>
))} ))}
{/* Highlight current position with bounding box */} {currentBoundingBox && (
{(() => {
if (!objectTimeline) return null;
// Find the most recent timeline event at or before effective current time with a bounding box
const relevantEvents = objectTimeline
.filter(
(event) =>
event.timestamp <= effectiveCurrentTime && event.data.box,
)
.sort((a, b) => b.timestamp - a.timestamp); // Most recent first
const currentEvent = relevantEvents[0];
if (currentEvent && currentEvent.data.box) {
const [left, top, width, height] = currentEvent.data.box;
const centerX = left + width / 2;
const centerY = top + height;
return (
<g> <g>
{/* Bounding box */}
<rect <rect
x={left * videoWidth} x={currentBoundingBox.left * videoWidth}
y={top * videoHeight} y={currentBoundingBox.top * videoHeight}
width={width * videoWidth} width={currentBoundingBox.width * videoWidth}
height={height * videoHeight} height={currentBoundingBox.height * videoHeight}
fill="none" fill="none"
stroke={objectColor} stroke={objectColor}
strokeWidth="5" strokeWidth="5"
opacity="0.9" opacity="0.9"
/> />
{/* Center point highlight */}
<circle <circle
cx={centerX * videoWidth} cx={currentBoundingBox.centerX * videoWidth}
cy={centerY * videoHeight} cy={currentBoundingBox.centerY * videoHeight}
r="5" r="5"
fill="rgb(255, 255, 0)" // yellow highlight fill="rgb(255, 255, 0)" // yellow highlight
stroke={objectColor} stroke={objectColor}
@ -364,10 +392,7 @@ export default function ObjectTrackOverlay({
opacity="1" opacity="1"
/> />
</g> </g>
); )}
}
return null;
})()}
</svg> </svg>
); );
} }