From 52af3cef9bca1e6f1fba6c1226c9c87e6b17e350 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 12 Apr 2024 23:38:42 -0500 Subject: [PATCH] motion and object masks --- web/src/components/settings/MasksAndZones.tsx | 628 ++++++++++++------ web/src/components/settings/NewZoneButton.tsx | 1 + web/src/components/settings/PolygonCanvas.tsx | 59 +- web/src/components/settings/PolygonDrawer.tsx | 4 +- web/src/components/settings/ZoneEditPane.tsx | 145 +++- web/src/types/canvas.ts | 4 + web/src/types/frigateConfig.ts | 5 +- 7 files changed, 600 insertions(+), 246 deletions(-) diff --git a/web/src/components/settings/MasksAndZones.tsx b/web/src/components/settings/MasksAndZones.tsx index 2fc7e8fa1..4ed6fa818 100644 --- a/web/src/components/settings/MasksAndZones.tsx +++ b/web/src/components/settings/MasksAndZones.tsx @@ -1,24 +1,30 @@ -import { Separator } from "@/components/ui/separator"; - import { FrigateConfig } from "@/types/frigateConfig"; import useSWR from "swr"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { PolygonCanvas } from "./PolygonCanvas"; -import { Polygon } from "@/types/canvas"; +import { Polygon, PolygonType } from "@/types/canvas"; import { interpolatePoints, toRGBColorString } from "@/utils/canvasUtil"; -import { isDesktop } from "react-device-detect"; -import { NewZoneButton } from "./NewZoneButton"; +import { isDesktop, isMobile } from "react-device-detect"; import { Skeleton } from "../ui/skeleton"; import { useResizeObserver } from "@/hooks/resize-observer"; -import { LuCopy, LuPencil, LuTrash } from "react-icons/lu"; +import { LuCopy, LuPencil, LuPlusSquare, LuTrash } from "react-icons/lu"; import { FaDrawPolygon } from "react-icons/fa"; import copy from "copy-to-clipboard"; import { toast } from "sonner"; import { Toaster } from "../ui/sonner"; -import Heading from "../ui/heading"; -import { Input } from "../ui/input"; import { ZoneEditPane } from "./ZoneEditPane"; +import { Button } from "../ui/button"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog"; const parseCoordinates = (coordinatesString: string) => { const coordinates = coordinatesString.split(","); @@ -33,6 +39,139 @@ const parseCoordinates = (coordinatesString: string) => { return points; }; +type PolygonItemProps = { + polygon: Polygon; + setAllPolygons: React.Dispatch>; + index: number; + activePolygonIndex: number | undefined; + hoveredPolygonIndex: number | null; + setHoveredPolygonIndex: (index: number | null) => void; + deleteDialogOpen: boolean; + setDeleteDialogOpen: (open: boolean) => void; + setActivePolygonIndex: (index: number | undefined) => void; + setEditPane: (type: PolygonType) => void; + handleCopyCoordinates: (index: number) => void; +}; + +function PolygonItem({ + polygon, + setAllPolygons, + index, + activePolygonIndex, + hoveredPolygonIndex, + setHoveredPolygonIndex, + deleteDialogOpen, + setDeleteDialogOpen, + setActivePolygonIndex, + setEditPane, + handleCopyCoordinates, +}: PolygonItemProps) { + return ( +
setHoveredPolygonIndex(index)} + onMouseLeave={() => setHoveredPolygonIndex(null)} + style={{ + backgroundColor: + hoveredPolygonIndex === index + ? toRGBColorString(polygon.color, false) + : "", + }} + > + {isMobile && <>} +
+ +

{polygon.name}

+
+ {deleteDialogOpen && ( + setDeleteDialogOpen(!deleteDialogOpen)} + > + + + Confirm Delete + + + Are you sure you want to delete this{" "} + {polygon.type.replace("_", " ")}? + + + Cancel + { + setAllPolygons((oldPolygons) => { + return oldPolygons.filter((_, i) => i !== index); + }); + setActivePolygonIndex(undefined); + }} + > + Delete + + + + + )} + {hoveredPolygonIndex === index && ( +
+
{ + setActivePolygonIndex(index); + setEditPane(polygon.type); + }} + > + +
+
handleCopyCoordinates(index)} + > + +
+
setDeleteDialogOpen(true)} + > + +
+
+ )} +
+ ); +} + export type ZoneObjects = { camera: string; zoneName: string; @@ -41,24 +180,30 @@ export type ZoneObjects = { type MasksAndZoneProps = { selectedCamera: string; - setSelectedCamera: React.Dispatch>; }; -export default function MasksAndZones({ - selectedCamera, - setSelectedCamera, -}: MasksAndZoneProps) { +export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) { const { data: config } = useSWR("config"); - const [zonePolygons, setZonePolygons] = useState([]); + const [allPolygons, setAllPolygons] = useState([]); + const [editingPolygons, setEditingPolygons] = useState(); const [zoneObjects, setZoneObjects] = useState([]); const [activePolygonIndex, setActivePolygonIndex] = useState< number | undefined >(undefined); + const [hoveredPolygonIndex, setHoveredPolygonIndex] = useState( + null, + ); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const containerRef = useRef(null); - const editViews = ["zone", "motion_mask", "object_mask", undefined] as const; + // const polygonTypes = [ + // "zone", + // "motion_mask", + // "object_mask", + // undefined, + // ] as const; - type EditPaneType = (typeof editViews)[number]; - const [editPane, setEditPane] = useState(undefined); + // type EditPaneType = (typeof polygonTypes)[number]; + const [editPane, setEditPane] = useState(undefined); const cameras = useMemo(() => { if (!config) { @@ -121,94 +266,76 @@ export default function MasksAndZones({ [setZoneObjects], ); - const growe = useMemo(() => { - if (!cameraConfig) { - return; - } + // const getCameraAspect = useCallback( + // (cam: string) => { + // if (!config) { + // return undefined; + // } - const aspectRatio = cameraConfig.detect.width / cameraConfig.detect.height; + // const camera = config.cameras[cam]; - if (aspectRatio > 2) { - return "aspect-wide"; - } else if (aspectRatio < 16 / 9) { - if (isDesktop) { - return "size-full aspect-tall"; - } else { - return "size-full"; - } - } else { - return "size-full aspect-video"; - } - }, [cameraConfig]); + // if (!camera) { + // return undefined; + // } - const getCameraAspect = useCallback( - (cam: string) => { - if (!config) { - return undefined; - } - - const camera = config.cameras[cam]; - - if (!camera) { - return undefined; - } - - return camera.detect.width / camera.detect.height; - }, - [config], - ); - - const mainCameraAspect = useMemo(() => { - const aspectRatio = getCameraAspect(selectedCamera); - - if (!aspectRatio) { - return "normal"; - } else if (aspectRatio > 2) { - return "wide"; - } else if (aspectRatio < 16 / 9) { - return "tall"; - } else { - return "normal"; - } - }, [getCameraAspect, selectedCamera]); - - const grow = useMemo(() => { - if (mainCameraAspect == "wide") { - return "w-full aspect-wide"; - } else if (mainCameraAspect == "tall") { - if (isDesktop) { - return "size-full aspect-tall flex flex-col justify-center"; - } else { - return "size-full"; - } - } else { - return "w-full aspect-video"; - } - }, [mainCameraAspect]); + // return camera.detect.width / camera.detect.height; + // }, + // [config], + // ); const [{ width: containerWidth, height: containerHeight }] = useResizeObserver(containerRef); - const { width, height } = cameraConfig - ? cameraConfig.detect - : { width: 1, height: 1 }; - const aspectRatio = width / height; + // const { width: detectWidth, height: detectHeight } = cameraConfig + // ? cameraConfig.detect + // : { width: 1, height: 1 }; + const aspectRatio = useMemo(() => { + if (!config) { + return undefined; + } + + const camera = config.cameras[selectedCamera]; + + if (!camera) { + return undefined; + } + + return camera.detect.width / camera.detect.height; + }, [config, selectedCamera]); + + const detectHeight = useMemo(() => { + if (!config) { + return undefined; + } + + const camera = config.cameras[selectedCamera]; + + if (!camera) { + return undefined; + } + + return camera.detect.height; + }, [config, selectedCamera]); const stretch = true; - const fitAspect = 16 / 9; - // console.log(containerRef.current?.clientHeight); + const fitAspect = 1; const scaledHeight = useMemo(() => { - const scaledHeight = - aspectRatio < (fitAspect ?? 0) - ? Math.floor( - Math.min(containerHeight, containerRef.current?.clientHeight), - ) - : Math.floor(containerWidth / aspectRatio); - const finalHeight = stretch ? scaledHeight : Math.min(scaledHeight, height); + if (containerRef.current && aspectRatio && detectHeight) { + console.log("recalc", Date.now()); + const scaledHeight = + aspectRatio < (fitAspect ?? 0) + ? Math.floor( + Math.min(containerHeight, containerRef.current?.clientHeight), + ) + : Math.floor(containerWidth / aspectRatio); + const finalHeight = stretch + ? scaledHeight + : Math.min(scaledHeight, detectHeight); - if (finalHeight > 0) { - return finalHeight; + if (finalHeight > 0) { + return finalHeight; + } } return 100; @@ -217,19 +344,52 @@ export default function MasksAndZones({ containerWidth, containerHeight, fitAspect, - height, + detectHeight, stretch, ]); - const scaledWidth = useMemo( - () => Math.ceil(scaledHeight * aspectRatio), - [scaledHeight, aspectRatio], - ); + const scaledWidth = useMemo(() => { + if (aspectRatio && scaledHeight) { + return Math.ceil(scaledHeight * aspectRatio); + } + + return 100; + }, [scaledHeight, aspectRatio]); + + const handleNewPolygon = (type: PolygonType) => { + setAllPolygons([ + ...(allPolygons || []), + { + points: [], + isFinished: false, + // isUnsaved: true, + type, + name: "", + camera: selectedCamera, + color: [0, 0, 220], + }, + ]); + setActivePolygonIndex(allPolygons.length); + }; + + const handleCancel = useCallback(() => { + setEditPane(undefined); + // setAllPolygons(allPolygons.filter((poly) => !poly.isUnsaved)); + setActivePolygonIndex(undefined); + setHoveredPolygonIndex(null); + }, []); + + const handleSave = useCallback(() => { + setAllPolygons([...(editingPolygons ?? [])]); + setActivePolygonIndex(undefined); + setEditPane(undefined); + setHoveredPolygonIndex(null); + }, [editingPolygons]); const handleCopyCoordinates = useCallback( (index: number) => { - if (zonePolygons) { - const poly = zonePolygons[index]; + if (allPolygons && scaledWidth) { + const poly = allPolygons[index]; copy( interpolatePoints(poly.points, scaledWidth, scaledHeight, 1, 1) .map((point) => `${point[0]},${point[1]}`) @@ -240,13 +400,14 @@ export default function MasksAndZones({ toast.error("Could not copy coordinates to clipboard."); } }, - [zonePolygons, scaledHeight, scaledWidth], + [allPolygons, scaledHeight, scaledWidth], ); useEffect(() => { - if (cameraConfig && containerRef.current) { - setZonePolygons( - Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({ + if (cameraConfig && containerRef.current && scaledWidth) { + setAllPolygons([ + ...Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({ + type: "zone" as PolygonType, // Add the type property here camera: cameraConfig.name, name, points: interpolatePoints( @@ -259,7 +420,46 @@ export default function MasksAndZones({ isFinished: true, color: zoneData.color, })), - ); + ...Object.entries(cameraConfig.motion.mask).map(([, maskData]) => ({ + type: "motion_mask" as PolygonType, + camera: cameraConfig.name, + name: "motion_mask", + points: interpolatePoints( + parseCoordinates(maskData), + 1, + 1, + scaledWidth, + scaledHeight, + ), + isFinished: true, + color: [0, 0, 255], + })), + ...Object.entries(cameraConfig.objects.filters).flatMap( + ([objectName, { mask }]): Polygon[] => + mask !== null && mask !== undefined + ? mask.flatMap((maskItem) => + maskItem !== null && maskItem !== undefined + ? [ + { + type: "object_mask" as PolygonType, + camera: cameraConfig.name, + name: objectName, + points: interpolatePoints( + parseCoordinates(maskItem), + 1, + 1, + scaledWidth, + scaledHeight, + ), + isFinished: true, + color: [128, 128, 128], + }, + ] + : [], + ) + : [], + ), + ]); setZoneObjects( Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({ @@ -273,6 +473,13 @@ export default function MasksAndZones({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [cameraConfig, containerRef]); + useEffect(() => { + if (editPane === undefined) { + setEditingPolygons([...allPolygons]); + console.log(allPolygons); + } + }, [setEditingPolygons, allPolygons, editPane]); + // useEffect(() => { // console.log( // "config zone objects", @@ -297,120 +504,133 @@ export default function MasksAndZones({ return ( <> - {cameraConfig && zonePolygons && ( + {cameraConfig && allPolygons && (
- {/*
- -
*/} {editPane == "zone" && ( { - setEditPane(undefined); - setActivePolygonIndex(undefined); - }} + onCancel={handleCancel} + onSave={handleSave} /> )} {editPane == "motion_mask" && ( { - setEditPane(undefined); - setActivePolygonIndex(undefined); - }} + onCancel={handleCancel} /> )} {editPane == "object_mask" && ( { - setEditPane(undefined); - setActivePolygonIndex(undefined); - }} + onCancel={handleCancel} /> )} {editPane == undefined && ( <>
Zones
- -
- {zonePolygons.map((polygon, index) => ( -
{ + setEditPane("zone"); + handleNewPolygon("zone"); + }} > -
- -

{polygon.name}

-
-
-
{ - setActivePolygonIndex(index); - setEditPane("zone"); - // if (activePolygonIndex == index) { - // setActivePolygonIndex(null); - - // } else { - // setActivePolygonIndex(index); - // } - }} - > - -
-
handleCopyCoordinates(index)} - > - -
-
{ - setZonePolygons((oldPolygons) => { - return oldPolygons.filter((_, i) => i !== index); - }); - setActivePolygonIndex(undefined); - }} - > - -
-
-
- ))} + + +
+ {allPolygons + .flatMap((polygon, index) => + polygon.type === "zone" ? [{ polygon, index }] : [], + ) + .map(({ polygon, index }) => ( + + ))} +
+
Motion Masks
+ +
+ {allPolygons + .flatMap((polygon, index) => + polygon.type === "motion_mask" ? [{ polygon, index }] : [], + ) + .map(({ polygon, index }) => ( + + ))} +
+
Object Masks
+ +
+ {allPolygons + .flatMap((polygon, index) => + polygon.type === "object_mask" ? [{ polygon, index }] : [], + ) + .map(({ polygon, index }) => ( + + ))} )} {/* @@ -422,7 +642,7 @@ export default function MasksAndZones({ - {zonePolygons.map((polygon, index) => ( + {allPolygons.map((polygon, index) => ( {polygon.name} @@ -469,16 +689,16 @@ export default function MasksAndZones({
                 {JSON.stringify(
-                  zonePolygons &&
-                    zonePolygons.map((polygon) =>
+                  allPolygons &&
+                    allPolygons.map((polygon) =>
                       interpolatePoints(
                         polygon.points,
                         scaledWidth,
@@ -503,9 +723,11 @@ export default function MasksAndZones({
                   camera={cameraConfig.name}
                   width={scaledWidth}
                   height={scaledHeight}
-                  polygons={zonePolygons}
-                  setPolygons={setZonePolygons}
+                  scale={1}
+                  polygons={editingPolygons}
+                  setPolygons={setEditingPolygons}
                   activePolygonIndex={activePolygonIndex}
+                  hoveredPolygonIndex={hoveredPolygonIndex}
                 />
               ) : (
                 
diff --git a/web/src/components/settings/NewZoneButton.tsx b/web/src/components/settings/NewZoneButton.tsx
index 26ee2ccc0..0374b23f5 100644
--- a/web/src/components/settings/NewZoneButton.tsx
+++ b/web/src/components/settings/NewZoneButton.tsx
@@ -73,6 +73,7 @@ export function NewZoneButton({
       {
         points: [],
         isFinished: false,
+        // isUnsaved: true,
         name: zoneName,
         camera: camera,
         color: [220, 0, 0],
diff --git a/web/src/components/settings/PolygonCanvas.tsx b/web/src/components/settings/PolygonCanvas.tsx
index c8a2d325e..666f9b436 100644
--- a/web/src/components/settings/PolygonCanvas.tsx
+++ b/web/src/components/settings/PolygonCanvas.tsx
@@ -1,27 +1,32 @@
 import React, { useMemo, useRef, useState, useEffect } from "react";
 import PolygonDrawer from "./PolygonDrawer";
-import { Stage, Layer, Image } from "react-konva";
+import { Stage, Layer, Image, Text } from "react-konva";
 import Konva from "konva";
 import type { KonvaEventObject } from "konva/lib/Node";
 import { Polygon } from "@/types/canvas";
 import { useApiHost } from "@/api";
+import { getAveragePoint } from "@/utils/canvasUtil";
 
 type PolygonCanvasProps = {
   camera: string;
   width: number;
   height: number;
+  scale: number;
   polygons: Polygon[];
   setPolygons: React.Dispatch>;
   activePolygonIndex: number | undefined;
+  hoveredPolygonIndex: number | null;
 };
 
 export function PolygonCanvas({
   camera,
   width,
   height,
+  scale,
   polygons,
   setPolygons,
   activePolygonIndex,
+  hoveredPolygonIndex,
 }: PolygonCanvasProps) {
   const [image, setImage] = useState();
   const imageRef = useRef(null);
@@ -68,7 +73,7 @@ export function PolygonCanvas({
   };
 
   const handleMouseDown = (e: KonvaEventObject) => {
-    if (!activePolygonIndex || !polygons) {
+    if (activePolygonIndex === undefined || !polygons) {
       return;
     }
 
@@ -103,7 +108,7 @@ export function PolygonCanvas({
   const handleMouseOverStartPoint = (
     e: KonvaEventObject,
   ) => {
-    if (!activePolygonIndex || !polygons) {
+    if (activePolygonIndex === undefined || !polygons) {
       return;
     }
 
@@ -118,7 +123,7 @@ export function PolygonCanvas({
   ) => {
     e.currentTarget.scale({ x: 1, y: 1 });
 
-    if (!activePolygonIndex || !polygons) {
+    if (activePolygonIndex === undefined || !polygons) {
       return;
     }
 
@@ -134,7 +139,7 @@ export function PolygonCanvas({
   const handlePointDragMove = (
     e: KonvaEventObject,
   ) => {
-    if (!activePolygonIndex || !polygons) {
+    if (activePolygonIndex === undefined || !polygons) {
       return;
     }
 
@@ -165,7 +170,7 @@ export function PolygonCanvas({
   };
 
   const handleGroupDragEnd = (e: KonvaEventObject) => {
-    if (activePolygonIndex && e.target.name() === "polygon") {
+    if (activePolygonIndex !== undefined && e.target.name() === "polygon") {
       const updatedPolygons = [...polygons];
       const activePolygon = updatedPolygons[activePolygonIndex];
       const result: number[][] = [];
@@ -186,6 +191,8 @@ export function PolygonCanvas({
       ref={stageRef}
       width={width}
       height={height}
+      scaleX={scale}
+      scaleY={scale}
       onMouseDown={handleMouseDown}
       onTouchStart={handleMouseDown}
     >
@@ -199,18 +206,34 @@ export function PolygonCanvas({
           height={height}
         />
         {polygons?.map((polygon, index) => (
-          
+          
+            
+            {index === hoveredPolygonIndex && (
+              
+            )}
+          
         ))}
       
     
diff --git a/web/src/components/settings/PolygonDrawer.tsx b/web/src/components/settings/PolygonDrawer.tsx
index 83fef8677..708dbbc22 100644
--- a/web/src/components/settings/PolygonDrawer.tsx
+++ b/web/src/components/settings/PolygonDrawer.tsx
@@ -9,6 +9,7 @@ type PolygonDrawerProps = {
   points: number[][];
   flattenedPoints: number[];
   isActive: boolean;
+  isHovered: boolean;
   isFinished: boolean;
   color: number[];
   handlePointDragMove: (e: KonvaEventObject) => void;
@@ -25,6 +26,7 @@ export default function PolygonDrawer({
   points,
   flattenedPoints,
   isActive,
+  isHovered,
   isFinished,
   color,
   handlePointDragMove,
@@ -99,7 +101,7 @@ export default function PolygonDrawer({
         stroke={colorString(true)}
         strokeWidth={3}
         closed={isFinished}
-        fill={colorString(isActive ? true : false)}
+        fill={colorString(isActive || isHovered ? true : false)}
       />
       {points.map((point, index) => {
         if (!isActive) {
diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx
index b0272702d..3c04938b9 100644
--- a/web/src/components/settings/ZoneEditPane.tsx
+++ b/web/src/components/settings/ZoneEditPane.tsx
@@ -16,7 +16,7 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
 import { useEffect, useMemo, useState } from "react";
 import { GeneralFilterContent } from "../filter/ReviewFilterGroup";
 import { FaObjectGroup } from "react-icons/fa";
-import { ATTRIBUTES, FrigateConfig } from "@/types/frigateConfig";
+import { ATTRIBUTES, CameraConfig, FrigateConfig } from "@/types/frigateConfig";
 import useSWR from "swr";
 import { isMobile } from "react-device-detect";
 import { zodResolver } from "@hookform/resolvers/zod";
@@ -88,7 +88,9 @@ export function ZoneObjectSelector({
   }, [cameraConfig, zoneName]);
 
   const [currentLabels, setCurrentLabels] = useState(
-    zoneLabels,
+    zoneLabels.every((label, index) => label === allLabels[index])
+      ? undefined
+      : zoneLabels,
   );
 
   useEffect(() => {
@@ -103,7 +105,7 @@ export function ZoneObjectSelector({
             className="mx-2 text-primary cursor-pointer"
             htmlFor="allLabels"
           >
-            All Labels
+            All Objects
           
            void;
+  onSave?: () => void;
+  onCancel?: () => void;
 };
 
 export function ZoneEditPane({
   polygons,
   activePolygonIndex,
+  onSave,
   onCancel,
 }: ZoneEditPaneProps) {
+  const { data: config } = useSWR("config");
+
+  const cameras = useMemo(() => {
+    if (!config) {
+      return [];
+    }
+
+    return Object.values(config.cameras)
+      .filter((conf) => conf.ui.dashboard && conf.enabled)
+      .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
+  }, [config]);
+
+  const formSchema = z.object({
+    name: z
+      .string()
+      .min(2, {
+        message: "Zone name must be at least 2 characters.",
+      })
+      .transform((val: string) => val.trim().replace(/\s+/g, "_"))
+      .refine(
+        (value: string) => {
+          return !cameras.map((cam) => cam.name).includes(value);
+        },
+        {
+          message: "Zone name must not be the name of a camera.",
+        },
+      )
+      .refine(
+        (value: string) => {
+          return !polygons
+            .filter((polygon, index) => index !== activePolygonIndex)
+            .map((polygon) => polygon.name)
+            .includes(value);
+        },
+        {
+          message: "Zone name already exists on this camera.",
+        },
+      ),
+    inertia: z.coerce.number().min(1, {
+      message: "Inertia must be above 0.",
+    }),
+    loitering_time: z.coerce.number().min(0, {
+      message: "Loitering time must be greater than or equal to 0.",
+    }),
+  });
+
   const polygon = useMemo(() => {
     if (polygons && activePolygonIndex !== undefined) {
       return polygons[activePolygonIndex];
@@ -189,15 +231,27 @@ export function ZoneEditPane({
 
   const form = useForm>({
     resolver: zodResolver(formSchema),
+    mode: "onChange",
     defaultValues: {
-      name: "",
-      inertia: 3,
-      loitering_time: 10,
+      name: polygon?.name ?? "",
+      inertia:
+        ((polygon &&
+          polygon.camera &&
+          polygon.name &&
+          config?.cameras[polygon.camera]?.zones[polygon.name]
+            ?.inertia) as number) ?? 3,
+      loitering_time:
+        ((polygon &&
+          polygon.camera &&
+          polygon.name &&
+          config?.cameras[polygon.camera]?.zones[polygon.name]
+            ?.loitering_time) as number) ?? 0,
     },
   });
 
   function onSubmit(values: z.infer) {
-    console.log(values);
+    console.log(values, polygons[activePolygonIndex]);
+    onSave();
   }
 
   if (!polygon) {
@@ -206,7 +260,7 @@ export function ZoneEditPane({
 
   return (
     <>
-      Edit Zone
+      Zone
       
@@ -220,10 +274,7 @@ export function ZoneEditPane({ Name - + @@ -285,7 +336,57 @@ export function ZoneEditPane({ }} /> -
+
+ +
+ + Alerts and Detections + + When an object enters this zone, ensure it is marked as an alert + or detection. + + +
+
+ + { + if (isChecked) { + return; + } + }} + /> +
+
+ + { + if (isChecked) { + return; + } + }} + /> +
+
+
+
+
diff --git a/web/src/types/canvas.ts b/web/src/types/canvas.ts index 65389172b..66a58680f 100644 --- a/web/src/types/canvas.ts +++ b/web/src/types/canvas.ts @@ -1,7 +1,11 @@ +export type PolygonType = "zone" | "motion_mask" | "object_mask"; + export type Polygon = { camera: string; name: string; + type: PolygonType; points: number[][]; isFinished: boolean; + // isUnsaved: boolean; color: number[]; }; diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index 0c41c15fa..179a40811 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -108,7 +108,7 @@ export interface CameraConfig { objects: { filters: { [objectName: string]: { - mask: string | null; + mask: string[] | null; max_area: number; max_ratio: number; min_area: number; @@ -201,6 +201,7 @@ export interface CameraConfig { coordinates: string; filters: Record; inertia: number; + loitering_time: number; objects: string[]; color: number[]; }; @@ -330,7 +331,7 @@ export interface FrigateConfig { objects: { filters: { [objectName: string]: { - mask: string | null; + mask: string[] | null; max_area: number; max_ratio: number; min_area: number;