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,22 +223,88 @@ export default function ObjectTrackOverlay({
[typeColorMap], [typeColorMap],
); );
const handlePointClick = useCallback(
(timestamp: number) => {
onSeekToTime?.(timestamp);
},
[onSeekToTime],
);
// render bounding box for object at current time if we have a timeline entry
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)
: "rgb(255, 0, 0)";
}, [pathPoints, getObjectColor]);
const objectColorArray = useMemo(() => {
return pathPoints[0]?.label
? getObjectColor(pathPoints[0].label).match(/\d+/g)?.map(Number) || [
255, 0, 0,
]
: [255, 0, 0];
}, [pathPoints, getObjectColor]);
const absolutePositions = useMemo(
() => getAbsolutePositions(),
[getAbsolutePositions],
);
// render any zones for object at current time
const zonePolygons = useMemo(() => {
return zones.map((zone) => {
// Convert zone coordinates from normalized (0-1) to pixel coordinates
const points = zone.coordinates
.split(",")
.map(Number.parseFloat)
.reduce((acc: string[], value, index) => {
const isXCoordinate = index % 2 === 0;
const coordinate = isXCoordinate
? value * videoWidth
: value * videoHeight;
acc.push(coordinate.toString());
return acc;
}, [])
.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) { if (!pathPoints.length || !config) {
return null; return null;
} }
// Get the object color from the first point's label
const objectColor = pathPoints[0]?.label
? getObjectColor(pathPoints[0].label)
: "rgb(255, 0, 0)";
const objectColorArray = pathPoints[0]?.label
? getObjectColor(pathPoints[0].label).match(/\d+/g)?.map(Number) || [
255, 0, 0,
]
: [255, 0, 0];
const absolutePositions = getAbsolutePositions();
return ( return (
<svg <svg
className={cn(className)} className={cn(className)}
@ -246,35 +315,17 @@ export default function ObjectTrackOverlay({
}} }}
preserveAspectRatio="xMidYMid slice" preserveAspectRatio="xMidYMid slice"
> >
{/* Render zones */} {zonePolygons.map((zone) => (
{zones.map((zone) => { <polygon
// Convert zone coordinates from normalized (0-1) to pixel coordinates key={zone.key}
const points = zone.coordinates points={zone.points}
.split(",") fill={zone.fill}
.map(Number.parseFloat) stroke={zone.stroke}
.reduce((acc: string[], value, index) => { strokeWidth="5"
const isXCoordinate = index % 2 === 0; opacity="0.7"
const coordinate = isXCoordinate />
? value * videoWidth ))}
: value * videoHeight;
acc.push(coordinate.toString());
return acc;
}, [])
.join(",");
return (
<polygon
key={zone.name}
points={points}
fill={`rgba(${zone.color.replace("rgb(", "").replace(")", "")}, 0.3)`}
stroke={zone.color}
strokeWidth="5"
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,53 +369,30 @@ export default function ObjectTrackOverlay({
</Tooltip> </Tooltip>
))} ))}
{/* Highlight current position with bounding box */} {currentBoundingBox && (
{(() => { <g>
if (!objectTimeline) return null; <rect
x={currentBoundingBox.left * videoWidth}
y={currentBoundingBox.top * videoHeight}
width={currentBoundingBox.width * videoWidth}
height={currentBoundingBox.height * videoHeight}
fill="none"
stroke={objectColor}
strokeWidth="5"
opacity="0.9"
/>
// Find the most recent timeline event at or before effective current time with a bounding box <circle
const relevantEvents = objectTimeline cx={currentBoundingBox.centerX * videoWidth}
.filter( cy={currentBoundingBox.centerY * videoHeight}
(event) => r="5"
event.timestamp <= effectiveCurrentTime && event.data.box, fill="rgb(255, 255, 0)" // yellow highlight
) stroke={objectColor}
.sort((a, b) => b.timestamp - a.timestamp); // Most recent first strokeWidth="5"
opacity="1"
const currentEvent = relevantEvents[0]; />
</g>
if (currentEvent && currentEvent.data.box) { )}
const [left, top, width, height] = currentEvent.data.box;
const centerX = left + width / 2;
const centerY = top + height;
return (
<g>
{/* Bounding box */}
<rect
x={left * videoWidth}
y={top * videoHeight}
width={width * videoWidth}
height={height * videoHeight}
fill="none"
stroke={objectColor}
strokeWidth="5"
opacity="0.9"
/>
{/* Center point highlight */}
<circle
cx={centerX * videoWidth}
cy={centerY * videoHeight}
r="5"
fill="rgb(255, 255, 0)" // yellow highlight
stroke={objectColor}
strokeWidth="5"
opacity="1"
/>
</g>
);
}
return null;
})()}
</svg> </svg>
); );
} }