frigate/web/src/components/settings/PolygonCanvas.tsx

379 lines
11 KiB
TypeScript
Raw Normal View History

import React, { useMemo, useRef, useState, useEffect, RefObject } from "react";
import PolygonDrawer from "./PolygonDrawer";
import { Stage, Layer, Image } from "react-konva";
import Konva from "konva";
import type { KonvaEventObject } from "konva/lib/Node";
import { Polygon, PolygonType } from "@/types/canvas";
import { useApiHost } from "@/api";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { snapPointToLines } from "@/utils/canvasUtil";
import { usePolygonStates } from "@/hooks/use-polygon-states";
type PolygonCanvasProps = {
Update frontend to React 19 (#22275) * remove unused RecoilRoot and fix implicit ref callback Remove the vestigial recoil dependency (zero consumers) and convert the implicit-return ref callback in SearchView to block form to prevent React 19 interpreting it as a cleanup function. * replace react-transition-group with framer-motion in Chip Replace CSSTransition with framer-motion AnimatePresence + motion.div for React 19 compatibility (react-transition-group uses findDOMNode). framer-motion is already a project dependency. * migrate react-grid-layout v1 to v2 - Replace WidthProvider(Responsive) HOC with useContainerWidth hook - Update types: Layout (single item) → LayoutItem, Layout[] → Layout - Replace isDraggable/isResizable/resizeHandles with dragConfig/resizeConfig - Update EventCallback signature for v2 API - Remove @types/react-grid-layout (v2 includes its own types) * upgrade vaul, next-themes, framer-motion, react-zoom-pan-pinch - vaul: ^0.9.1 → ^1.1.2 - next-themes: ^0.3.0 → ^0.4.6 - framer-motion: ^11.5.4 → ^12.35.0 (React 19 native support) - react-zoom-pan-pinch: 3.4.4 → latest * upgrade to React 19, react-konva v19, eslint-plugin-react-hooks v5 Core React 19 upgrade with all necessary type fixes: - Update RefObject types to accept T | null (React 19 refs always nullable) - Add JSX namespace imports (no longer global in React 19) - Add initial values to useRef calls (required in React 19) - Fix ReactElement.props unknown type in config-form components - Fix IconWrapper interface to use HTMLAttributes instead of index signature - Add monaco-editor as dev dependency for type declarations - Upgrade react-konva to v19, eslint-plugin-react-hooks to v5 * upgrade typescript to 5.9.3 * modernize Context.Provider to React 19 shorthand Replace <Context.Provider value={...}> with <Context value={...}> across all project-owned context providers. External library contexts (react-icons IconContext, radix TooltipPrimitive) left unchanged. * add runtime patches for React 19 compatibility - Patch @radix-ui/react-compose-refs@1.1.2: stabilize useComposedRefs to prevent infinite render loops from unstable ref callbacks https://github.com/radix-ui/primitives/issues/3799 - Patch @radix-ui/react-slot@1.2.4: use useComposedRefs hook in SlotClone instead of inline composeRefs to prevent re-render cycles https://github.com/radix-ui/primitives/pull/3804 - Patch react-use-websocket@4.8.1: remove flushSync wrappers that cause "Maximum update depth exceeded" with React 19 auto-batching https://github.com/facebook/react/issues/27613 - Add npm overrides to ensure single hoisted copies of compose-refs and react-slot across all Radix packages - Add postinstall script for patch-package - Remove leftover react-transition-group dependency * formatting * use availableWidth instead of useContainerWidth for grid layout The useContainerWidth hook from react-grid-layout v2 returns raw container width without accounting for scrollbar width, causing the grid to not fill the full available space. Use the existing availableWidth value from useResizeObserver which already compensates for scrollbar width, matching the working implementation. * remove unused carousel component and fix React 19 peer deps Remove embla-carousel-react and its unused Carousel UI component. Upgrade sonner v1 → v2 for native React 19 support. Remove @types/react-icons stub (react-icons bundles its own types). These changes eliminate all peer dependency conflicts, so npm install works without --legacy-peer-deps. * fix React 19 infinite re-render loop on live dashboard The "Maximum update depth exceeded" error was caused by two issues: 1. useDeferredStreamMetadata returned a new `{}` default on every render when SWR data was undefined, creating an unstable reference that triggered the useEffect in useCameraLiveMode on every render cycle. Fixed by using a stable module-level EMPTY_METADATA constant. 2. useResizeObserver's rest parameter `...refs` created a new array on every render, causing its useEffect to re-run and re-observe elements continuously. Fixed by stabilizing refs with useRef and only reconnecting the observer when actual DOM elements change.
2026-03-05 17:42:38 +03:00
containerRef: RefObject<HTMLDivElement | null>;
camera: string;
width: number;
height: number;
polygons: Polygon[];
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
activePolygonIndex: number | undefined;
hoveredPolygonIndex: number | null;
selectedZoneMask: PolygonType[] | undefined;
Estimated object speed for zones (#16452) * utility functions * backend config * backend object speed tracking * draw speed on debug view * basic frontend zone editor * remove line sorting * fix types * highlight line on canvas when entering value in zone edit pane * rename vars and add validation * ensure speed estimation is disabled when user adds more than 4 points * pixel velocity in debug * unit_system in config * ability to define unit system in config * save max speed to db * frontend * docs * clarify docs * utility functions * backend config * backend object speed tracking * draw speed on debug view * basic frontend zone editor * remove line sorting * fix types * highlight line on canvas when entering value in zone edit pane * rename vars and add validation * ensure speed estimation is disabled when user adds more than 4 points * pixel velocity in debug * unit_system in config * ability to define unit system in config * save max speed to db * frontend * docs * clarify docs * fix duplicates from merge * include max_estimated_speed in api responses * add units to zone edit pane * catch undefined * add average speed * clarify docs * only track average speed when object is active * rename vars * ensure points and distances are ordered clockwise * only store the last 10 speeds like score history * remove max estimated speed * update docs * update docs * fix point ordering * improve readability * docs inertia recommendation * fix point ordering * check object frame time * add velocity angle to frontend * docs clarity * add frontend speed filter * fix mqtt docs * fix mqtt docs * don't try to remove distances if they weren't already defined * don't display estimates on debug view/snapshots if object is not in a speed tracking zone * docs * implement speed_threshold for zone presence * docs for threshold * better ground plane image * improve image zone size * add inertia to speed threshold example
2025-02-10 23:23:42 +03:00
activeLine?: number;
snapPoints: boolean;
};
export function PolygonCanvas({
containerRef,
camera,
width,
height,
polygons,
setPolygons,
activePolygonIndex,
hoveredPolygonIndex,
selectedZoneMask,
Estimated object speed for zones (#16452) * utility functions * backend config * backend object speed tracking * draw speed on debug view * basic frontend zone editor * remove line sorting * fix types * highlight line on canvas when entering value in zone edit pane * rename vars and add validation * ensure speed estimation is disabled when user adds more than 4 points * pixel velocity in debug * unit_system in config * ability to define unit system in config * save max speed to db * frontend * docs * clarify docs * utility functions * backend config * backend object speed tracking * draw speed on debug view * basic frontend zone editor * remove line sorting * fix types * highlight line on canvas when entering value in zone edit pane * rename vars and add validation * ensure speed estimation is disabled when user adds more than 4 points * pixel velocity in debug * unit_system in config * ability to define unit system in config * save max speed to db * frontend * docs * clarify docs * fix duplicates from merge * include max_estimated_speed in api responses * add units to zone edit pane * catch undefined * add average speed * clarify docs * only track average speed when object is active * rename vars * ensure points and distances are ordered clockwise * only store the last 10 speeds like score history * remove max estimated speed * update docs * update docs * fix point ordering * improve readability * docs inertia recommendation * fix point ordering * check object frame time * add velocity angle to frontend * docs clarity * add frontend speed filter * fix mqtt docs * fix mqtt docs * don't try to remove distances if they weren't already defined * don't display estimates on debug view/snapshots if object is not in a speed tracking zone * docs * implement speed_threshold for zone presence * docs for threshold * better ground plane image * improve image zone size * add inertia to speed threshold example
2025-02-10 23:23:42 +03:00
activeLine,
snapPoints,
}: PolygonCanvasProps) {
const [isLoaded, setIsLoaded] = useState(false);
const [image, setImage] = useState<HTMLImageElement | undefined>();
const imageRef = useRef<Konva.Image | null>(null);
const stageRef = useRef<Konva.Stage>(null);
const apiHost = useApiHost();
const getPolygonEnabled = usePolygonStates(polygons);
const videoElement = useMemo(() => {
if (camera && width && height) {
setIsLoaded(false);
const element = new window.Image();
element.width = width;
element.height = height;
element.src = `${apiHost}api/${camera}/latest.webp?cache=${Date.now()}`;
return element;
}
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [camera, apiHost]);
useEffect(() => {
if (!videoElement) {
return;
}
const onload = function () {
setImage(videoElement);
setIsLoaded(true);
};
videoElement.addEventListener("load", onload);
return () => {
videoElement.removeEventListener("load", onload);
};
}, [videoElement]);
const addPointToPolygon = (polygon: Polygon, newPoint: number[]) => {
const points = polygon.points;
const pointsOrder = polygon.pointsOrder;
const [newPointX, newPointY] = newPoint;
const updatedPoints = [...points];
let updatedPointsOrder: number[];
if (!pointsOrder) {
updatedPointsOrder = [];
} else {
updatedPointsOrder = [...pointsOrder];
}
let insertIndex = points.length;
for (let i = 0; i < points.length; i++) {
const [x1, y1] = points[i];
const [x2, y2] = i === points.length - 1 ? points[0] : points[i + 1];
if (
(x1 <= newPointX && newPointX <= x2) ||
(x2 <= newPointX && newPointX <= x1)
) {
if (
(y1 <= newPointY && newPointY <= y2) ||
(y2 <= newPointY && newPointY <= y1)
) {
insertIndex = i + 1;
break;
}
}
}
updatedPoints.splice(insertIndex, 0, [newPointX, newPointY]);
updatedPointsOrder.splice(insertIndex, 0, updatedPoints.length);
return { updatedPoints, updatedPointsOrder };
};
const handleMouseDown = (e: KonvaEventObject<MouseEvent | TouchEvent>) => {
if (activePolygonIndex === undefined || !polygons) {
return;
}
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
const stage = e.target.getStage()!;
const mousePos = stage.getPointerPosition() ?? { x: 0, y: 0 };
const intersection = stage.getIntersection(mousePos);
// right click on desktops to delete a point
if (
e.evt instanceof MouseEvent &&
e.evt.button === 2 &&
intersection?.getClassName() == "Circle"
) {
const pointIndex = parseInt(intersection.name()?.split("-")[1]);
if (!isNaN(pointIndex)) {
const updatedPoints = activePolygon.points.filter(
(_, index) => index !== pointIndex,
);
updatedPolygons[activePolygonIndex] = {
...activePolygon,
points: updatedPoints,
pointsOrder: activePolygon.pointsOrder?.filter(
(_, index) => index !== pointIndex,
),
};
setPolygons(updatedPolygons);
}
return;
}
if (
activePolygon.points.length >= 3 &&
intersection?.getClassName() == "Circle" &&
intersection?.name() == "point-0"
) {
// Close the polygon
updatedPolygons[activePolygonIndex] = {
...activePolygon,
isFinished: true,
};
setPolygons(updatedPolygons);
} else {
if (
(!activePolygon.isFinished &&
intersection?.getClassName() !== "Circle") ||
(activePolygon.isFinished && intersection?.name() == "unfilled-line")
) {
let newPoint = [mousePos.x, mousePos.y];
if (snapPoints) {
// Snap to other polygons' edges
const otherPolygons = polygons.filter(
(_, i) => i !== activePolygonIndex,
);
const snappedPos = snapPointToLines(newPoint, otherPolygons, 10);
if (snappedPos) {
newPoint = snappedPos;
}
}
const { updatedPoints, updatedPointsOrder } = addPointToPolygon(
activePolygon,
newPoint,
);
updatedPolygons[activePolygonIndex] = {
...activePolygon,
points: updatedPoints,
pointsOrder: updatedPointsOrder,
};
setPolygons(updatedPolygons);
}
}
};
const handlePointDragMove = (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => {
if (activePolygonIndex === undefined || !polygons) {
return;
}
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
const stage = e.target.getStage();
if (stage) {
// we add an unfilled line for adding points when finished
const index = e.target.index - (activePolygon.isFinished ? 2 : 1);
let pos = [e.target._lastPos!.x, e.target._lastPos!.y];
if (snapPoints) {
// Snap to other polygons' edges
const otherPolygons = polygons.filter(
(_, i) => i !== activePolygonIndex,
);
const snappedPos = snapPointToLines(pos, otherPolygons, 10); // 10 is the snap threshold
if (snappedPos) {
pos = snappedPos;
}
}
// Constrain to stage boundaries
pos[0] = Math.max(0, Math.min(pos[0], stage.width()));
pos[1] = Math.max(0, Math.min(pos[1], stage.height()));
updatedPolygons[activePolygonIndex] = {
...activePolygon,
points: [
...activePolygon.points.slice(0, index),
pos,
...activePolygon.points.slice(index + 1),
],
};
setPolygons(updatedPolygons);
}
};
const handleGroupDragEnd = (e: KonvaEventObject<MouseEvent | TouchEvent>) => {
if (activePolygonIndex !== undefined && e.target.name() === "polygon") {
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
const result: number[][] = [];
activePolygon.points.map((point: number[]) =>
result.push([point[0] + e.target.x(), point[1] + e.target.y()]),
);
e.target.position({ x: 0, y: 0 });
updatedPolygons[activePolygonIndex] = {
...activePolygon,
points: result,
};
setPolygons(updatedPolygons);
}
};
const handleStageMouseOver = () => {
if (activePolygonIndex === undefined || !polygons) {
return;
}
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
if (containerRef.current && activePolygon && !activePolygon.isFinished) {
containerRef.current.style.cursor = "crosshair";
}
};
useEffect(() => {
if (activePolygonIndex === undefined || !polygons?.length) {
return;
}
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
if (!activePolygon) {
return;
}
// add default points order for already completed polygons
if (!activePolygon.pointsOrder && activePolygon.isFinished) {
updatedPolygons[activePolygonIndex] = {
...activePolygon,
pointsOrder: activePolygon.points.map((_, index) => index),
};
setPolygons(updatedPolygons);
}
}, [activePolygonIndex, polygons, setPolygons]);
if (!isLoaded) {
return <ActivityIndicator />;
}
return (
<Stage
ref={stageRef}
width={width}
height={height}
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
onMouseOver={handleStageMouseOver}
onContextMenu={(e) => {
e.evt.preventDefault();
}}
>
<Layer>
<Image
ref={imageRef}
image={image}
x={0}
y={0}
width={width}
height={height}
/>
{polygons?.map(
(polygon, index) =>
(selectedZoneMask === undefined ||
selectedZoneMask.includes(polygon.type)) &&
index !== activePolygonIndex && (
<PolygonDrawer
stageRef={stageRef}
key={index}
points={polygon.points}
Estimated object speed for zones (#16452) * utility functions * backend config * backend object speed tracking * draw speed on debug view * basic frontend zone editor * remove line sorting * fix types * highlight line on canvas when entering value in zone edit pane * rename vars and add validation * ensure speed estimation is disabled when user adds more than 4 points * pixel velocity in debug * unit_system in config * ability to define unit system in config * save max speed to db * frontend * docs * clarify docs * utility functions * backend config * backend object speed tracking * draw speed on debug view * basic frontend zone editor * remove line sorting * fix types * highlight line on canvas when entering value in zone edit pane * rename vars and add validation * ensure speed estimation is disabled when user adds more than 4 points * pixel velocity in debug * unit_system in config * ability to define unit system in config * save max speed to db * frontend * docs * clarify docs * fix duplicates from merge * include max_estimated_speed in api responses * add units to zone edit pane * catch undefined * add average speed * clarify docs * only track average speed when object is active * rename vars * ensure points and distances are ordered clockwise * only store the last 10 speeds like score history * remove max estimated speed * update docs * update docs * fix point ordering * improve readability * docs inertia recommendation * fix point ordering * check object frame time * add velocity angle to frontend * docs clarity * add frontend speed filter * fix mqtt docs * fix mqtt docs * don't try to remove distances if they weren't already defined * don't display estimates on debug view/snapshots if object is not in a speed tracking zone * docs * implement speed_threshold for zone presence * docs for threshold * better ground plane image * improve image zone size * add inertia to speed threshold example
2025-02-10 23:23:42 +03:00
distances={polygon.distances}
isActive={index === activePolygonIndex}
isHovered={index === hoveredPolygonIndex}
isFinished={polygon.isFinished}
enabled={getPolygonEnabled(polygon)}
color={polygon.color}
handlePointDragMove={handlePointDragMove}
handleGroupDragEnd={handleGroupDragEnd}
Estimated object speed for zones (#16452) * utility functions * backend config * backend object speed tracking * draw speed on debug view * basic frontend zone editor * remove line sorting * fix types * highlight line on canvas when entering value in zone edit pane * rename vars and add validation * ensure speed estimation is disabled when user adds more than 4 points * pixel velocity in debug * unit_system in config * ability to define unit system in config * save max speed to db * frontend * docs * clarify docs * utility functions * backend config * backend object speed tracking * draw speed on debug view * basic frontend zone editor * remove line sorting * fix types * highlight line on canvas when entering value in zone edit pane * rename vars and add validation * ensure speed estimation is disabled when user adds more than 4 points * pixel velocity in debug * unit_system in config * ability to define unit system in config * save max speed to db * frontend * docs * clarify docs * fix duplicates from merge * include max_estimated_speed in api responses * add units to zone edit pane * catch undefined * add average speed * clarify docs * only track average speed when object is active * rename vars * ensure points and distances are ordered clockwise * only store the last 10 speeds like score history * remove max estimated speed * update docs * update docs * fix point ordering * improve readability * docs inertia recommendation * fix point ordering * check object frame time * add velocity angle to frontend * docs clarity * add frontend speed filter * fix mqtt docs * fix mqtt docs * don't try to remove distances if they weren't already defined * don't display estimates on debug view/snapshots if object is not in a speed tracking zone * docs * implement speed_threshold for zone presence * docs for threshold * better ground plane image * improve image zone size * add inertia to speed threshold example
2025-02-10 23:23:42 +03:00
activeLine={activeLine}
snapPoints={snapPoints}
snapToLines={(point) =>
snapPoints
? snapPointToLines(
point,
polygons.filter((_, i) => i !== index),
10,
)
: null
}
/>
),
)}
{activePolygonIndex !== undefined &&
polygons?.[activePolygonIndex] &&
(selectedZoneMask === undefined ||
selectedZoneMask.includes(polygons[activePolygonIndex].type)) && (
<PolygonDrawer
stageRef={stageRef}
key={activePolygonIndex}
points={polygons[activePolygonIndex].points}
Estimated object speed for zones (#16452) * utility functions * backend config * backend object speed tracking * draw speed on debug view * basic frontend zone editor * remove line sorting * fix types * highlight line on canvas when entering value in zone edit pane * rename vars and add validation * ensure speed estimation is disabled when user adds more than 4 points * pixel velocity in debug * unit_system in config * ability to define unit system in config * save max speed to db * frontend * docs * clarify docs * utility functions * backend config * backend object speed tracking * draw speed on debug view * basic frontend zone editor * remove line sorting * fix types * highlight line on canvas when entering value in zone edit pane * rename vars and add validation * ensure speed estimation is disabled when user adds more than 4 points * pixel velocity in debug * unit_system in config * ability to define unit system in config * save max speed to db * frontend * docs * clarify docs * fix duplicates from merge * include max_estimated_speed in api responses * add units to zone edit pane * catch undefined * add average speed * clarify docs * only track average speed when object is active * rename vars * ensure points and distances are ordered clockwise * only store the last 10 speeds like score history * remove max estimated speed * update docs * update docs * fix point ordering * improve readability * docs inertia recommendation * fix point ordering * check object frame time * add velocity angle to frontend * docs clarity * add frontend speed filter * fix mqtt docs * fix mqtt docs * don't try to remove distances if they weren't already defined * don't display estimates on debug view/snapshots if object is not in a speed tracking zone * docs * implement speed_threshold for zone presence * docs for threshold * better ground plane image * improve image zone size * add inertia to speed threshold example
2025-02-10 23:23:42 +03:00
distances={polygons[activePolygonIndex].distances}
isActive={true}
isHovered={activePolygonIndex === hoveredPolygonIndex}
isFinished={polygons[activePolygonIndex].isFinished}
enabled={getPolygonEnabled(polygons[activePolygonIndex])}
color={polygons[activePolygonIndex].color}
handlePointDragMove={handlePointDragMove}
handleGroupDragEnd={handleGroupDragEnd}
Estimated object speed for zones (#16452) * utility functions * backend config * backend object speed tracking * draw speed on debug view * basic frontend zone editor * remove line sorting * fix types * highlight line on canvas when entering value in zone edit pane * rename vars and add validation * ensure speed estimation is disabled when user adds more than 4 points * pixel velocity in debug * unit_system in config * ability to define unit system in config * save max speed to db * frontend * docs * clarify docs * utility functions * backend config * backend object speed tracking * draw speed on debug view * basic frontend zone editor * remove line sorting * fix types * highlight line on canvas when entering value in zone edit pane * rename vars and add validation * ensure speed estimation is disabled when user adds more than 4 points * pixel velocity in debug * unit_system in config * ability to define unit system in config * save max speed to db * frontend * docs * clarify docs * fix duplicates from merge * include max_estimated_speed in api responses * add units to zone edit pane * catch undefined * add average speed * clarify docs * only track average speed when object is active * rename vars * ensure points and distances are ordered clockwise * only store the last 10 speeds like score history * remove max estimated speed * update docs * update docs * fix point ordering * improve readability * docs inertia recommendation * fix point ordering * check object frame time * add velocity angle to frontend * docs clarity * add frontend speed filter * fix mqtt docs * fix mqtt docs * don't try to remove distances if they weren't already defined * don't display estimates on debug view/snapshots if object is not in a speed tracking zone * docs * implement speed_threshold for zone presence * docs for threshold * better ground plane image * improve image zone size * add inertia to speed threshold example
2025-02-10 23:23:42 +03:00
activeLine={activeLine}
snapPoints={snapPoints}
snapToLines={(point) =>
snapPoints
? snapPointToLines(
point,
polygons.filter((_, i) => i !== activePolygonIndex),
10,
)
: null
}
/>
)}
</Layer>
</Stage>
);
}
export default PolygonCanvas;