diff --git a/web/src/components/filter/ZoneMaskFilter.tsx b/web/src/components/filter/ZoneMaskFilter.tsx new file mode 100644 index 000000000..071d9ece0 --- /dev/null +++ b/web/src/components/filter/ZoneMaskFilter.tsx @@ -0,0 +1,130 @@ +import { Button } from "../ui/button"; +import { FaFilter } from "react-icons/fa"; +import { isMobile } from "react-device-detect"; +import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { PolygonType } from "@/types/canvas"; +import { Label } from "../ui/label"; +import { Switch } from "../ui/switch"; +import { DropdownMenuSeparator } from "../ui/dropdown-menu"; + +type ZoneMaskFilterButtonProps = { + selectedZoneMask?: PolygonType[]; + updateZoneMaskFilter: (labels: PolygonType[] | undefined) => void; +}; +export function ZoneMaskFilterButton({ + selectedZoneMask, + updateZoneMaskFilter, +}: ZoneMaskFilterButtonProps) { + const trigger = ( + + ); + const content = ( + + ); + + if (isMobile) { + return ( + + {trigger} + + {content} + + + ); + } + + return ( + + {trigger} + {content} + + ); +} + +type GeneralFilterContentProps = { + selectedZoneMask: PolygonType[] | undefined; + updateZoneMaskFilter: (labels: PolygonType[] | undefined) => void; +}; +export function GeneralFilterContent({ + selectedZoneMask, + updateZoneMaskFilter, +}: GeneralFilterContentProps) { + return ( + <> +
+
+ + { + if (isChecked) { + updateZoneMaskFilter(undefined); + } + }} + /> +
+ +
+ {["zone", "motion_mask", "object_mask"].map((item) => ( +
+ + { + if (isChecked) { + const updatedLabels = selectedZoneMask + ? [...selectedZoneMask] + : []; + + updatedLabels.push(item as PolygonType); + updateZoneMaskFilter(updatedLabels); + } else { + const updatedLabels = selectedZoneMask + ? [...selectedZoneMask] + : []; + + // can not deselect the last item + if (updatedLabels.length > 1) { + updatedLabels.splice( + updatedLabels.indexOf(item as PolygonType), + 1, + ); + updateZoneMaskFilter(updatedLabels); + } + } + }} + /> +
+ ))} +
+
+ + + ); +} diff --git a/web/src/components/settings/MasksAndZones.tsx b/web/src/components/settings/MasksAndZones.tsx index 4ed6fa818..b1d0e4b69 100644 --- a/web/src/components/settings/MasksAndZones.tsx +++ b/web/src/components/settings/MasksAndZones.tsx @@ -180,9 +180,13 @@ export type ZoneObjects = { type MasksAndZoneProps = { selectedCamera: string; + selectedZoneMask: PolygonType; }; -export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) { +export default function MasksAndZones({ + selectedCamera, + selectedZoneMask, +}: MasksAndZoneProps) { const { data: config } = useSWR("config"); const [allPolygons, setAllPolygons] = useState([]); const [editingPolygons, setEditingPolygons] = useState(); @@ -266,23 +270,6 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) { [setZoneObjects], ); - // 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 [{ width: containerWidth, height: containerHeight }] = useResizeObserver(containerRef); @@ -322,7 +309,6 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) { const scaledHeight = useMemo(() => { if (containerRef.current && aspectRatio && detectHeight) { - console.log("recalc", Date.now()); const scaledHeight = aspectRatio < (fitAspect ?? 0) ? Math.floor( @@ -405,9 +391,9 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) { useEffect(() => { if (cameraConfig && containerRef.current && scaledWidth) { - setAllPolygons([ - ...Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({ - type: "zone" as PolygonType, // Add the type property here + const zones = Object.entries(cameraConfig.zones).map( + ([name, zoneData]) => ({ + type: "zone" as PolygonType, camera: cameraConfig.name, name, points: interpolatePoints( @@ -419,11 +405,14 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) { ), isFinished: true, color: zoneData.color, - })), - ...Object.entries(cameraConfig.motion.mask).map(([, maskData]) => ({ + }), + ); + + const motionMasks = Object.entries(cameraConfig.motion.mask).map( + ([, maskData], index) => ({ type: "motion_mask" as PolygonType, camera: cameraConfig.name, - name: "motion_mask", + name: `Motion Mask ${index + 1}`, points: interpolatePoints( parseCoordinates(maskData), 1, @@ -433,32 +422,59 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) { ), 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], - }, - ] - : [], - ) - : [], - ), + }), + ); + + const globalObjectMasks = Object.entries(cameraConfig.objects.mask).map( + ([, maskData], index) => ({ + type: "object_mask" as PolygonType, + camera: cameraConfig.name, + name: `All Objects Object Mask ${index + 1}`, + points: interpolatePoints( + parseCoordinates(maskData), + 1, + 1, + scaledWidth, + scaledHeight, + ), + isFinished: true, + color: [0, 0, 255], + }), + ); + + const globalObjectMasksCount = globalObjectMasks.length; + + const objectMasks = Object.entries(cameraConfig.objects.filters).flatMap( + ([objectName, { mask }]): Polygon[] => + mask !== null && mask !== undefined + ? mask.flatMap((maskItem, subIndex) => + maskItem !== null && maskItem !== undefined + ? [ + { + type: "object_mask" as PolygonType, + camera: cameraConfig.name, + name: `${objectName.charAt(0).toUpperCase() + objectName.slice(1)} Object Mask ${globalObjectMasksCount + subIndex + 1}`, + points: interpolatePoints( + parseCoordinates(maskItem), + 1, + 1, + scaledWidth, + scaledHeight, + ), + isFinished: true, + color: [128, 128, 128], + }, + ] + : [], + ) + : [], + ); + + setAllPolygons([ + ...zones, + ...motionMasks, + ...globalObjectMasks, + ...objectMasks, ]); setZoneObjects( @@ -502,12 +518,14 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) { return ; } + // console.log(selectedZoneMask); + return ( <> {cameraConfig && allPolygons && (
-
+
{editPane == "zone" && ( )} - {editPane == undefined && ( + {editPane === undefined && ( <> -
-
Zones
- -
- {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 }) => ( - - ))} + {(selectedZoneMask === undefined || + selectedZoneMask.includes("zone" as PolygonType)) && ( + <> +
+
Zones
+ +
+ {allPolygons + .flatMap((polygon, index) => + polygon.type === "zone" ? [{ polygon, index }] : [], + ) + .map(({ polygon, index }) => ( + + ))} + + )} + {(selectedZoneMask === undefined || + selectedZoneMask.includes("motion_mask" as PolygonType)) && ( + <> +
+
Motion Masks
+ +
+ {allPolygons + .flatMap((polygon, index) => + polygon.type === "motion_mask" + ? [{ polygon, index }] + : [], + ) + .map(({ polygon, index }) => ( + + ))} + + )} + {(selectedZoneMask === undefined || + selectedZoneMask.includes("object_mask" as PolygonType)) && ( + <> +
+
Object Masks
+ +
+ {allPolygons + .flatMap((polygon, index) => + polygon.type === "object_mask" + ? [{ polygon, index }] + : [], + ) + .map(({ polygon, index }) => ( + + ))} + + )} )} {/* @@ -728,6 +765,7 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) { setPolygons={setEditingPolygons} activePolygonIndex={activePolygonIndex} hoveredPolygonIndex={hoveredPolygonIndex} + selectedZoneMask={selectedZoneMask} /> ) : ( diff --git a/web/src/components/settings/PolygonCanvas.tsx b/web/src/components/settings/PolygonCanvas.tsx index 666f9b436..c516ceb3a 100644 --- a/web/src/components/settings/PolygonCanvas.tsx +++ b/web/src/components/settings/PolygonCanvas.tsx @@ -3,7 +3,7 @@ import PolygonDrawer from "./PolygonDrawer"; 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 { Polygon, PolygonType } from "@/types/canvas"; import { useApiHost } from "@/api"; import { getAveragePoint } from "@/utils/canvasUtil"; @@ -16,6 +16,7 @@ type PolygonCanvasProps = { setPolygons: React.Dispatch>; activePolygonIndex: number | undefined; hoveredPolygonIndex: number | null; + selectedZoneMask: PolygonType; }; export function PolygonCanvas({ @@ -27,6 +28,7 @@ export function PolygonCanvas({ setPolygons, activePolygonIndex, hoveredPolygonIndex, + selectedZoneMask, }: PolygonCanvasProps) { const [image, setImage] = useState(); const imageRef = useRef(null); @@ -205,36 +207,40 @@ export function PolygonCanvas({ width={width} height={height} /> - {polygons?.map((polygon, index) => ( - - - {index === hoveredPolygonIndex && ( - - )} - - ))} + {polygons?.map( + (polygon, index) => + (selectedZoneMask === undefined || + selectedZoneMask.includes(polygon.type)) && ( + + + {index === hoveredPolygonIndex && ( + + )} + + ), + )} ); diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index c84c8e821..f8677edb1 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -20,6 +20,8 @@ import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; import useSWR from "swr"; import General from "@/components/settings/General"; import FilterCheckBox from "@/components/filter/FilterCheckBox"; +import { ZoneMaskFilterButton } from "@/components/filter/ZoneMaskFilter"; +import { PolygonType } from "@/types/canvas"; type CameraSelectButtonProps = { allCameras: CameraConfig[]; @@ -136,6 +138,8 @@ export default function Settings() { const [selectedCamera, setSelectedCamera] = useState(cameras[0].name); + const [filterZoneMask, setFilterZoneMask] = useState(); + return (
@@ -168,6 +172,10 @@ export default function Settings() { page == "masks / zones" || page == "motion tuner") && (
+ )} {page == "motion tuner" && } diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index 179a40811..25604eb53 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -340,7 +340,7 @@ export interface FrigateConfig { threshold: number; }; }; - mask: string; + mask: string[]; track: string[]; };