mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-11 05:35:25 +03:00
motion tuner, edit controls, tooltips
This commit is contained in:
parent
8deec1c9b6
commit
6ae3e24157
@ -6,6 +6,7 @@ type AutoUpdatingCameraImageProps = {
|
|||||||
searchParams?: URLSearchParams;
|
searchParams?: URLSearchParams;
|
||||||
showFps?: boolean;
|
showFps?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
cameraClasses?: string;
|
||||||
reloadInterval?: number;
|
reloadInterval?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ export default function AutoUpdatingCameraImage({
|
|||||||
searchParams = undefined,
|
searchParams = undefined,
|
||||||
showFps = true,
|
showFps = true,
|
||||||
className,
|
className,
|
||||||
|
cameraClasses,
|
||||||
reloadInterval = MIN_LOAD_TIMEOUT_MS,
|
reloadInterval = MIN_LOAD_TIMEOUT_MS,
|
||||||
}: AutoUpdatingCameraImageProps) {
|
}: AutoUpdatingCameraImageProps) {
|
||||||
const [key, setKey] = useState(Date.now());
|
const [key, setKey] = useState(Date.now());
|
||||||
@ -68,6 +70,7 @@ export default function AutoUpdatingCameraImage({
|
|||||||
camera={camera}
|
camera={camera}
|
||||||
onload={handleLoad}
|
onload={handleLoad}
|
||||||
searchParams={`cache=${key}&${searchParams}`}
|
searchParams={`cache=${key}&${searchParams}`}
|
||||||
|
className={cameraClasses}
|
||||||
/>
|
/>
|
||||||
{showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
|
{showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -36,12 +36,7 @@ export default function CameraImage({
|
|||||||
}, [apiHost, name, imgRef, searchParams, config]);
|
}, [apiHost, name, imgRef, searchParams, config]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={className} ref={containerRef}>
|
||||||
className={`relative w-full h-full flex justify-center ${
|
|
||||||
className || ""
|
|
||||||
}`}
|
|
||||||
ref={containerRef}
|
|
||||||
>
|
|
||||||
{enabled ? (
|
{enabled ? (
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
|
|||||||
@ -53,6 +53,7 @@ export default function DebugCameraImage({
|
|||||||
<AutoUpdatingCameraImage
|
<AutoUpdatingCameraImage
|
||||||
camera={cameraConfig.name}
|
camera={cameraConfig.name}
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
|
cameraClasses="relative w-full h-full flex justify-center"
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleToggleSettings} variant="link" size="sm">
|
<Button onClick={handleToggleSettings} variant="link" size="sm">
|
||||||
<span className="w-5 h-5">
|
<span className="w-5 h-5">
|
||||||
|
|||||||
@ -163,6 +163,7 @@ export default function LivePlayer({
|
|||||||
camera={cameraConfig.name}
|
camera={cameraConfig.name}
|
||||||
showFps={false}
|
showFps={false}
|
||||||
reloadInterval={stillReloadInterval}
|
reloadInterval={stillReloadInterval}
|
||||||
|
cameraClasses="relative w-full h-full flex justify-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -5,16 +5,19 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { PolygonCanvas } from "./PolygonCanvas";
|
import { PolygonCanvas } from "./PolygonCanvas";
|
||||||
import { Polygon, PolygonType } from "@/types/canvas";
|
import { Polygon, PolygonType } from "@/types/canvas";
|
||||||
import { interpolatePoints, toRGBColorString } from "@/utils/canvasUtil";
|
import { interpolatePoints, toRGBColorString } from "@/utils/canvasUtil";
|
||||||
import { isDesktop, isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import { Skeleton } from "../ui/skeleton";
|
import { Skeleton } from "../ui/skeleton";
|
||||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
import { LuCopy, LuPencil, LuPlusSquare, LuTrash } from "react-icons/lu";
|
import { LuCopy, LuPencil, LuPlus } from "react-icons/lu";
|
||||||
import { FaDrawPolygon, FaObjectGroup } from "react-icons/fa";
|
import { FaDrawPolygon, FaObjectGroup } from "react-icons/fa";
|
||||||
|
import { BsPersonBoundingBox } from "react-icons/bs";
|
||||||
|
import { HiTrash } from "react-icons/hi";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Toaster } from "../ui/sonner";
|
import { Toaster } from "../ui/sonner";
|
||||||
import { ZoneEditPane } from "./ZoneEditPane";
|
import { ZoneEditPane } from "./ZoneEditPane";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -25,8 +28,7 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "../ui/alert-dialog";
|
} from "../ui/alert-dialog";
|
||||||
import { Separator } from "../ui/separator";
|
import Heading from "../ui/heading";
|
||||||
import { BsPersonBoundingBox } from "react-icons/bs";
|
|
||||||
|
|
||||||
const parseCoordinates = (coordinatesString: string) => {
|
const parseCoordinates = (coordinatesString: string) => {
|
||||||
const coordinates = coordinatesString.split(",");
|
const coordinates = coordinatesString.split(",");
|
||||||
@ -196,7 +198,7 @@ export default function MasksAndZones({
|
|||||||
}, [config, selectedCamera]);
|
}, [config, selectedCamera]);
|
||||||
|
|
||||||
const stretch = true;
|
const stretch = true;
|
||||||
const fitAspect = 1;
|
const fitAspect = 16 / 9;
|
||||||
|
|
||||||
const scaledHeight = useMemo(() => {
|
const scaledHeight = useMemo(() => {
|
||||||
if (containerRef.current && aspectRatio && detectHeight) {
|
if (containerRef.current && aspectRatio && detectHeight) {
|
||||||
@ -247,13 +249,18 @@ export default function MasksAndZones({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
const handleCancel = useCallback(() => {
|
||||||
|
console.log("handling cancel");
|
||||||
setEditPane(undefined);
|
setEditPane(undefined);
|
||||||
setAllPolygons(allPolygons.filter((poly) => !poly.isUnsaved));
|
console.log("all", allPolygons);
|
||||||
|
console.log("editing", editingPolygons);
|
||||||
|
// setAllPolygons(allPolygons.filter((poly) => !poly.isUnsaved));
|
||||||
|
setEditingPolygons([...allPolygons]);
|
||||||
setActivePolygonIndex(undefined);
|
setActivePolygonIndex(undefined);
|
||||||
setHoveredPolygonIndex(null);
|
setHoveredPolygonIndex(null);
|
||||||
}, [allPolygons]);
|
}, [allPolygons, editingPolygons]);
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
|
console.log("handling save");
|
||||||
setAllPolygons([...(editingPolygons ?? [])]);
|
setAllPolygons([...(editingPolygons ?? [])]);
|
||||||
setActivePolygonIndex(undefined);
|
setActivePolygonIndex(undefined);
|
||||||
setEditPane(undefined);
|
setEditPane(undefined);
|
||||||
@ -368,20 +375,27 @@ export default function MasksAndZones({
|
|||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("setting all and editing");
|
||||||
setAllPolygons([
|
setAllPolygons([
|
||||||
...zones,
|
...zones,
|
||||||
...motionMasks,
|
...motionMasks,
|
||||||
...globalObjectMasks,
|
...globalObjectMasks,
|
||||||
...objectMasks,
|
...objectMasks,
|
||||||
]);
|
]);
|
||||||
|
setEditingPolygons([
|
||||||
|
...zones,
|
||||||
|
...motionMasks,
|
||||||
|
...globalObjectMasks,
|
||||||
|
...objectMasks,
|
||||||
|
]);
|
||||||
|
|
||||||
setZoneObjects(
|
// setZoneObjects(
|
||||||
Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
|
// Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
|
||||||
camera: cameraConfig.name,
|
// camera: cameraConfig.name,
|
||||||
zoneName: name,
|
// zoneName: name,
|
||||||
objects: Object.keys(zoneData.filters),
|
// objects: Object.keys(zoneData.filters),
|
||||||
})),
|
// })),
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
// we know that these deps are correct
|
// we know that these deps are correct
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -391,23 +405,25 @@ export default function MasksAndZones({
|
|||||||
if (editPane === undefined) {
|
if (editPane === undefined) {
|
||||||
setEditingPolygons([...allPolygons]);
|
setEditingPolygons([...allPolygons]);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
console.log(allPolygons);
|
console.log("edit pane undefined, all", allPolygons);
|
||||||
} else {
|
} else {
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
}
|
}
|
||||||
}, [setEditingPolygons, setIsEditing, allPolygons, editPane]);
|
// we know that these deps are correct
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [setEditingPolygons, setIsEditing, allPolygons]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
console.log(
|
// console.log(
|
||||||
"config zone objects",
|
// "config zone objects",
|
||||||
Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
|
// Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
|
||||||
camera: cameraConfig.name,
|
// camera: cameraConfig.name,
|
||||||
zoneName: name,
|
// zoneName: name,
|
||||||
objects: Object.keys(zoneData.filters),
|
// objects: Object.keys(zoneData.filters),
|
||||||
})),
|
// })),
|
||||||
);
|
// );
|
||||||
console.log("component zone objects", zoneObjects);
|
// console.log("component zone objects", zoneObjects);
|
||||||
}, [zoneObjects]);
|
// }, [zoneObjects]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedCamera) {
|
if (selectedCamera) {
|
||||||
@ -421,13 +437,14 @@ export default function MasksAndZones({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{cameraConfig && allPolygons && (
|
{cameraConfig && editingPolygons && (
|
||||||
<div className="flex flex-col md:flex-row size-full">
|
<div className="flex flex-col md:flex-row size-full">
|
||||||
<Toaster position="top-center" />
|
<Toaster position="top-center" />
|
||||||
<div className="flex flex-col w-full overflow-y-auto md:w-3/12 order-last md:order-none md:mr-2 rounded-lg border-secondary-foreground border-[1px] p-2 bg-background_alt">
|
<div className="flex flex-col h-full w-full overflow-y-auto mt-2 md:mt-0 md:w-3/12 order-last md:order-none md:mr-2 rounded-lg border-secondary-foreground border-[1px] p-2 bg-background_alt">
|
||||||
{editPane == "zone" && (
|
{editPane == "zone" && (
|
||||||
<ZoneEditPane
|
<ZoneEditPane
|
||||||
polygons={editingPolygons}
|
polygons={editingPolygons}
|
||||||
|
setPolygons={setEditingPolygons}
|
||||||
activePolygonIndex={activePolygonIndex}
|
activePolygonIndex={activePolygonIndex}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
@ -436,6 +453,7 @@ export default function MasksAndZones({
|
|||||||
{editPane == "motion_mask" && (
|
{editPane == "motion_mask" && (
|
||||||
<ZoneEditPane
|
<ZoneEditPane
|
||||||
polygons={editingPolygons}
|
polygons={editingPolygons}
|
||||||
|
setPolygons={setEditingPolygons}
|
||||||
activePolygonIndex={activePolygonIndex}
|
activePolygonIndex={activePolygonIndex}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
@ -444,6 +462,7 @@ export default function MasksAndZones({
|
|||||||
{editPane == "object_mask" && (
|
{editPane == "object_mask" && (
|
||||||
<ZoneEditPane
|
<ZoneEditPane
|
||||||
polygons={editingPolygons}
|
polygons={editingPolygons}
|
||||||
|
setPolygons={setEditingPolygons}
|
||||||
activePolygonIndex={activePolygonIndex}
|
activePolygonIndex={activePolygonIndex}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
@ -451,130 +470,148 @@ export default function MasksAndZones({
|
|||||||
)}
|
)}
|
||||||
{editPane === undefined && (
|
{editPane === undefined && (
|
||||||
<>
|
<>
|
||||||
{(selectedZoneMask === undefined ||
|
<Heading as="h3" className="my-2">
|
||||||
selectedZoneMask.includes("zone" as PolygonType)) && (
|
Masks / Zones
|
||||||
<>
|
</Heading>
|
||||||
<div className="flex flex-row justify-between items-center my-2">
|
<div className="flex flex-col w-full">
|
||||||
<div className="text-md">Zones</div>
|
{(selectedZoneMask === undefined ||
|
||||||
<Button
|
selectedZoneMask.includes("zone" as PolygonType)) && (
|
||||||
variant="ghost"
|
<div className="mt-0 pt-0">
|
||||||
className="h-8 px-0"
|
<div className="flex flex-row justify-between items-center my-3">
|
||||||
onClick={() => {
|
<div className="text-md">Zones</div>
|
||||||
setEditPane("zone");
|
<Tooltip>
|
||||||
handleNewPolygon("zone");
|
<TooltipTrigger asChild>
|
||||||
}}
|
<Button
|
||||||
>
|
variant="secondary"
|
||||||
<LuPlusSquare />
|
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
|
||||||
</Button>
|
onClick={() => {
|
||||||
|
setEditPane("zone");
|
||||||
|
handleNewPolygon("zone");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LuPlus />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Add Zone</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{allPolygons
|
||||||
|
.flatMap((polygon, index) =>
|
||||||
|
polygon.type === "zone" ? [{ polygon, index }] : [],
|
||||||
|
)
|
||||||
|
.map(({ polygon, index }) => (
|
||||||
|
<PolygonItem
|
||||||
|
key={index}
|
||||||
|
polygon={polygon}
|
||||||
|
index={index}
|
||||||
|
activePolygonIndex={activePolygonIndex}
|
||||||
|
hoveredPolygonIndex={hoveredPolygonIndex}
|
||||||
|
setHoveredPolygonIndex={setHoveredPolygonIndex}
|
||||||
|
deleteDialogOpen={deleteDialogOpen}
|
||||||
|
setDeleteDialogOpen={setDeleteDialogOpen}
|
||||||
|
setActivePolygonIndex={setActivePolygonIndex}
|
||||||
|
setEditPane={setEditPane}
|
||||||
|
setAllPolygons={setAllPolygons}
|
||||||
|
handleCopyCoordinates={handleCopyCoordinates}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{allPolygons
|
)}
|
||||||
.flatMap((polygon, index) =>
|
{(selectedZoneMask === undefined ||
|
||||||
polygon.type === "zone" ? [{ polygon, index }] : [],
|
selectedZoneMask.includes(
|
||||||
)
|
"motion_mask" as PolygonType,
|
||||||
.map(({ polygon, index }) => (
|
)) && (
|
||||||
<PolygonItem
|
<div className="first:mt-0 mt-3 first:pt-0 pt-3 border-t-[1px] first:border-transparent border-secondary">
|
||||||
key={index}
|
<div className="flex flex-row justify-between items-center my-3">
|
||||||
polygon={polygon}
|
<div className="text-md">Motion Masks</div>
|
||||||
index={index}
|
<Tooltip>
|
||||||
activePolygonIndex={activePolygonIndex}
|
<TooltipTrigger asChild>
|
||||||
hoveredPolygonIndex={hoveredPolygonIndex}
|
<Button
|
||||||
setHoveredPolygonIndex={setHoveredPolygonIndex}
|
variant="secondary"
|
||||||
deleteDialogOpen={deleteDialogOpen}
|
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
|
||||||
setDeleteDialogOpen={setDeleteDialogOpen}
|
onClick={() => {
|
||||||
setActivePolygonIndex={setActivePolygonIndex}
|
setEditPane("motion_mask");
|
||||||
setEditPane={setEditPane}
|
handleNewPolygon("motion_mask");
|
||||||
setAllPolygons={setAllPolygons}
|
}}
|
||||||
handleCopyCoordinates={handleCopyCoordinates}
|
>
|
||||||
/>
|
<LuPlus />
|
||||||
))}
|
</Button>
|
||||||
</>
|
</TooltipTrigger>
|
||||||
)}
|
<TooltipContent>Add Motion Mask</TooltipContent>
|
||||||
<div className="flex my-2">
|
</Tooltip>
|
||||||
<Separator className="bg-secondary" />
|
</div>
|
||||||
|
{allPolygons
|
||||||
|
.flatMap((polygon, index) =>
|
||||||
|
polygon.type === "motion_mask"
|
||||||
|
? [{ polygon, index }]
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
.map(({ polygon, index }) => (
|
||||||
|
<PolygonItem
|
||||||
|
key={index}
|
||||||
|
polygon={polygon}
|
||||||
|
index={index}
|
||||||
|
activePolygonIndex={activePolygonIndex}
|
||||||
|
hoveredPolygonIndex={hoveredPolygonIndex}
|
||||||
|
setHoveredPolygonIndex={setHoveredPolygonIndex}
|
||||||
|
deleteDialogOpen={deleteDialogOpen}
|
||||||
|
setDeleteDialogOpen={setDeleteDialogOpen}
|
||||||
|
setActivePolygonIndex={setActivePolygonIndex}
|
||||||
|
setEditPane={setEditPane}
|
||||||
|
setAllPolygons={setAllPolygons}
|
||||||
|
handleCopyCoordinates={handleCopyCoordinates}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(selectedZoneMask === undefined ||
|
||||||
|
selectedZoneMask.includes(
|
||||||
|
"object_mask" as PolygonType,
|
||||||
|
)) && (
|
||||||
|
<div className="first:mt-0 mt-3 first:pt-0 pt-3 border-t-[1px] first:border-transparent border-secondary">
|
||||||
|
<div className="flex flex-row justify-between items-center my-3">
|
||||||
|
<div className="text-md">Object Masks</div>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
setEditPane("object_mask");
|
||||||
|
handleNewPolygon("object_mask");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LuPlus />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Add Object Mask</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{allPolygons
|
||||||
|
.flatMap((polygon, index) =>
|
||||||
|
polygon.type === "object_mask"
|
||||||
|
? [{ polygon, index }]
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
.map(({ polygon, index }) => (
|
||||||
|
<PolygonItem
|
||||||
|
key={index}
|
||||||
|
polygon={polygon}
|
||||||
|
index={index}
|
||||||
|
activePolygonIndex={activePolygonIndex}
|
||||||
|
hoveredPolygonIndex={hoveredPolygonIndex}
|
||||||
|
setHoveredPolygonIndex={setHoveredPolygonIndex}
|
||||||
|
deleteDialogOpen={deleteDialogOpen}
|
||||||
|
setDeleteDialogOpen={setDeleteDialogOpen}
|
||||||
|
setActivePolygonIndex={setActivePolygonIndex}
|
||||||
|
setEditPane={setEditPane}
|
||||||
|
setAllPolygons={setAllPolygons}
|
||||||
|
handleCopyCoordinates={handleCopyCoordinates}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(selectedZoneMask === undefined ||
|
|
||||||
selectedZoneMask.includes("motion_mask" as PolygonType)) && (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-row justify-between items-center my-2">
|
|
||||||
<div className="text-md">Motion Masks</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 px-0"
|
|
||||||
onClick={() => {
|
|
||||||
setEditPane("motion_mask");
|
|
||||||
handleNewPolygon("motion_mask");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LuPlusSquare />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{allPolygons
|
|
||||||
.flatMap((polygon, index) =>
|
|
||||||
polygon.type === "motion_mask"
|
|
||||||
? [{ polygon, index }]
|
|
||||||
: [],
|
|
||||||
)
|
|
||||||
.map(({ polygon, index }) => (
|
|
||||||
<PolygonItem
|
|
||||||
key={index}
|
|
||||||
polygon={polygon}
|
|
||||||
index={index}
|
|
||||||
activePolygonIndex={activePolygonIndex}
|
|
||||||
hoveredPolygonIndex={hoveredPolygonIndex}
|
|
||||||
setHoveredPolygonIndex={setHoveredPolygonIndex}
|
|
||||||
deleteDialogOpen={deleteDialogOpen}
|
|
||||||
setDeleteDialogOpen={setDeleteDialogOpen}
|
|
||||||
setActivePolygonIndex={setActivePolygonIndex}
|
|
||||||
setEditPane={setEditPane}
|
|
||||||
setAllPolygons={setAllPolygons}
|
|
||||||
handleCopyCoordinates={handleCopyCoordinates}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="flex my-2">
|
|
||||||
<Separator className="bg-secondary" />
|
|
||||||
</div>
|
|
||||||
{(selectedZoneMask === undefined ||
|
|
||||||
selectedZoneMask.includes("object_mask" as PolygonType)) && (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-row justify-between items-center my-2">
|
|
||||||
<div className="text-md">Object Masks</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 px-0"
|
|
||||||
onClick={() => {
|
|
||||||
setEditPane("object_mask");
|
|
||||||
handleNewPolygon("object_mask");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LuPlusSquare />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{allPolygons
|
|
||||||
.flatMap((polygon, index) =>
|
|
||||||
polygon.type === "object_mask"
|
|
||||||
? [{ polygon, index }]
|
|
||||||
: [],
|
|
||||||
)
|
|
||||||
.map(({ polygon, index }) => (
|
|
||||||
<PolygonItem
|
|
||||||
key={index}
|
|
||||||
polygon={polygon}
|
|
||||||
index={index}
|
|
||||||
activePolygonIndex={activePolygonIndex}
|
|
||||||
hoveredPolygonIndex={hoveredPolygonIndex}
|
|
||||||
setHoveredPolygonIndex={setHoveredPolygonIndex}
|
|
||||||
deleteDialogOpen={deleteDialogOpen}
|
|
||||||
setDeleteDialogOpen={setDeleteDialogOpen}
|
|
||||||
setActivePolygonIndex={setActivePolygonIndex}
|
|
||||||
setEditPane={setEditPane}
|
|
||||||
setAllPolygons={setAllPolygons}
|
|
||||||
handleCopyCoordinates={handleCopyCoordinates}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{/* <Table>
|
{/* <Table>
|
||||||
@ -661,7 +698,7 @@ export default function MasksAndZones({
|
|||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="flex md:w-7/12 md:grow md:h-dvh md:max-h-full"
|
className="flex md:w-7/12 md:grow md:h-dvh md:max-h-full"
|
||||||
>
|
>
|
||||||
<div className="size-full">
|
<div className="flex flex-row justify-center mx-auto size-full">
|
||||||
{cameraConfig &&
|
{cameraConfig &&
|
||||||
scaledWidth &&
|
scaledWidth &&
|
||||||
scaledHeight &&
|
scaledHeight &&
|
||||||
@ -677,7 +714,7 @@ export default function MasksAndZones({
|
|||||||
selectedZoneMask={selectedZoneMask}
|
selectedZoneMask={selectedZoneMask}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton className="w-full h-full" />
|
<Skeleton className="size-full" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -725,7 +762,7 @@ function PolygonItem({
|
|||||||
{isMobile && <></>}
|
{isMobile && <></>}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center ${
|
className={`flex items-center ${
|
||||||
activePolygonIndex === index
|
hoveredPolygonIndex === index
|
||||||
? "text-primary"
|
? "text-primary"
|
||||||
: "text-muted-foreground"
|
: "text-muted-foreground"
|
||||||
}`}
|
}`}
|
||||||
@ -779,37 +816,52 @@ function PolygonItem({
|
|||||||
setEditPane(polygon.type);
|
setEditPane(polygon.type);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LuPencil
|
<Tooltip>
|
||||||
className={`size-4 ${
|
<TooltipTrigger>
|
||||||
activePolygonIndex === index
|
<LuPencil
|
||||||
? "text-primary"
|
className={`size-4 ${
|
||||||
: "text-muted-foreground"
|
hoveredPolygonIndex === index
|
||||||
}`}
|
? "text-primary"
|
||||||
/>
|
: "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Edit</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => handleCopyCoordinates(index)}
|
onClick={() => handleCopyCoordinates(index)}
|
||||||
>
|
>
|
||||||
<LuCopy
|
<Tooltip>
|
||||||
className={`size-4 ${
|
<TooltipTrigger>
|
||||||
activePolygonIndex === index
|
<LuCopy
|
||||||
? "text-primary"
|
className={`size-4 ${
|
||||||
: "text-muted-foreground"
|
hoveredPolygonIndex === index
|
||||||
}`}
|
? "text-primary"
|
||||||
/>
|
: "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Copy coordinates</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => setDeleteDialogOpen(true)}
|
onClick={() => setDeleteDialogOpen(true)}
|
||||||
>
|
>
|
||||||
<LuTrash
|
<Tooltip>
|
||||||
className={`size-4 ${
|
<TooltipTrigger>
|
||||||
activePolygonIndex === index
|
<HiTrash
|
||||||
? "text-primary fill-primary"
|
className={`size-4 ${
|
||||||
: "text-muted-foreground fill-muted-foreground"
|
hoveredPolygonIndex === index
|
||||||
}`}
|
? "text-primary fill-primary"
|
||||||
/>
|
: "text-muted-foreground fill-muted-foreground"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Delete</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,13 +1,4 @@
|
|||||||
import Heading from "@/components/ui/heading";
|
import Heading from "@/components/ui/heading";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -37,13 +28,17 @@ import { Switch } from "../ui/switch";
|
|||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
type MotionTunerProps = {
|
||||||
|
selectedCamera: string;
|
||||||
|
};
|
||||||
|
|
||||||
type MotionSettings = {
|
type MotionSettings = {
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
contour_area?: number;
|
contour_area?: number;
|
||||||
improve_contrast?: boolean;
|
improve_contrast?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MotionTuner() {
|
export default function MotionTuner({ selectedCamera }: MotionTunerProps) {
|
||||||
const { data: config, mutate: updateConfig } =
|
const { data: config, mutate: updateConfig } =
|
||||||
useSWR<FrigateConfig>("config");
|
useSWR<FrigateConfig>("config");
|
||||||
const [changedValue, setChangedValue] = useState(false);
|
const [changedValue, setChangedValue] = useState(false);
|
||||||
@ -60,7 +55,7 @@ export default function MotionTuner() {
|
|||||||
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
const [selectedCamera, setSelectedCamera] = useState(cameras[0]?.name);
|
// const [selectedCamera, setSelectedCamera] = useState(cameras[0]?.name);
|
||||||
const [nextSelectedCamera, setNextSelectedCamera] = useState("");
|
const [nextSelectedCamera, setNextSelectedCamera] = useState("");
|
||||||
|
|
||||||
const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera);
|
const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera);
|
||||||
@ -169,11 +164,11 @@ export default function MotionTuner() {
|
|||||||
setNextSelectedCamera(camera);
|
setNextSelectedCamera(camera);
|
||||||
setConfirmationDialogOpen(true);
|
setConfirmationDialogOpen(true);
|
||||||
} else {
|
} else {
|
||||||
setSelectedCamera(camera);
|
// setSelectedCamera(camera);
|
||||||
setNextSelectedCamera("");
|
setNextSelectedCamera("");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setSelectedCamera, changedValue],
|
[changedValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDialog = useCallback(
|
const handleDialog = useCallback(
|
||||||
@ -181,12 +176,12 @@ export default function MotionTuner() {
|
|||||||
if (save) {
|
if (save) {
|
||||||
saveToConfig();
|
saveToConfig();
|
||||||
}
|
}
|
||||||
setSelectedCamera(nextSelectedCamera);
|
// setSelectedCamera(nextSelectedCamera);
|
||||||
setNextSelectedCamera("");
|
setNextSelectedCamera("");
|
||||||
setConfirmationDialogOpen(false);
|
setConfirmationDialogOpen(false);
|
||||||
setChangedValue(false);
|
setChangedValue(false);
|
||||||
},
|
},
|
||||||
[saveToConfig, setSelectedCamera, nextSelectedCamera],
|
[saveToConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!cameraConfig && !selectedCamera) {
|
if (!cameraConfig && !selectedCamera) {
|
||||||
@ -194,127 +189,113 @@ export default function MotionTuner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col md:flex-row size-full">
|
||||||
<Heading as="h2">Motion Detection Tuner</Heading>
|
<Toaster position="top-center" />
|
||||||
<Toaster />
|
<div className="flex flex-col w-full overflow-y-auto mt-2 md:mt-0 md:w-3/12 order-last md:order-none md:mr-2 rounded-lg border-secondary-foreground border-[1px] p-2 bg-background_alt">
|
||||||
<div className="flex items-center space-x-2 mt-5">
|
<Heading as="h3" className="my-2">
|
||||||
<Select
|
Motion Detection Tuner
|
||||||
value={selectedCamera}
|
</Heading>
|
||||||
onValueChange={handleSelectedCameraChange}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[180px]">
|
|
||||||
<SelectValue placeholder="Camera" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>Choose a camera</SelectLabel>
|
|
||||||
{cameras.map((camera) => (
|
|
||||||
<SelectItem
|
|
||||||
key={camera.name}
|
|
||||||
value={camera.name}
|
|
||||||
className="capitalize"
|
|
||||||
>
|
|
||||||
{camera.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
{cameraConfig ? (
|
|
||||||
<div className="flex flex-col justify-start">
|
|
||||||
<AutoUpdatingCameraImage
|
|
||||||
camera={cameraConfig.name}
|
|
||||||
searchParams={new URLSearchParams([["motion", "1"]])}
|
|
||||||
className="w-[50%]"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-row justify-evenly w-full">
|
|
||||||
<div className="flex flex-row mb-5">
|
|
||||||
<Slider
|
|
||||||
id="motion-threshold"
|
|
||||||
className="w-[300px]"
|
|
||||||
disabled={motionSettings.threshold === undefined}
|
|
||||||
value={[motionSettings.threshold ?? 0]}
|
|
||||||
min={10}
|
|
||||||
max={80}
|
|
||||||
step={1}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
handleMotionConfigChange({ threshold: value[0] });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="motion-threshold" className="px-2">
|
|
||||||
Threshold: {motionSettings.threshold}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<Slider
|
|
||||||
id="motion-contour-area"
|
|
||||||
className="w-[300px]"
|
|
||||||
disabled={motionSettings.contour_area === undefined}
|
|
||||||
value={[motionSettings.contour_area ?? 0]}
|
|
||||||
min={10}
|
|
||||||
max={200}
|
|
||||||
step={5}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
handleMotionConfigChange({ contour_area: value[0] });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="motion-contour-area" className="px-2">
|
|
||||||
Contour Area: {motionSettings.contour_area}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<Switch
|
|
||||||
id="improve-contrast"
|
|
||||||
disabled={motionSettings.improve_contrast === undefined}
|
|
||||||
checked={motionSettings.improve_contrast === true}
|
|
||||||
onCheckedChange={(isChecked) => {
|
|
||||||
handleMotionConfigChange({ improve_contrast: isChecked });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="improve-contrast">Improve Contrast</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex">
|
<div className="flex flex-col w-full space-y-10">
|
||||||
<Button
|
<div className="flex flex-row mb-5">
|
||||||
size="sm"
|
<Slider
|
||||||
variant={isLoading ? "ghost" : "select"}
|
id="motion-threshold"
|
||||||
disabled={!changedValue || isLoading}
|
className="w-[300px]"
|
||||||
onClick={saveToConfig}
|
disabled={motionSettings.threshold === undefined}
|
||||||
>
|
value={[motionSettings.threshold ?? 0]}
|
||||||
{isLoading ? "Saving..." : "Save to Config"}
|
min={10}
|
||||||
</Button>
|
max={80}
|
||||||
</div>
|
step={1}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
handleMotionConfigChange({ threshold: value[0] });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="motion-threshold" className="px-2">
|
||||||
|
Threshold: {motionSettings.threshold}
|
||||||
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{confirmationDialogOpen && (
|
<div className="flex flex-row">
|
||||||
<AlertDialog
|
<Slider
|
||||||
open={confirmationDialogOpen}
|
id="motion-contour-area"
|
||||||
onOpenChange={() => setConfirmationDialogOpen(false)}
|
className="w-[300px]"
|
||||||
|
disabled={motionSettings.contour_area === undefined}
|
||||||
|
value={[motionSettings.contour_area ?? 0]}
|
||||||
|
min={10}
|
||||||
|
max={200}
|
||||||
|
step={5}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
handleMotionConfigChange({ contour_area: value[0] });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="motion-contour-area" className="px-2">
|
||||||
|
Contour Area: {motionSettings.contour_area}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<Switch
|
||||||
|
id="improve-contrast"
|
||||||
|
disabled={motionSettings.improve_contrast === undefined}
|
||||||
|
checked={motionSettings.improve_contrast === true}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
handleMotionConfigChange({ improve_contrast: isChecked });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="improve-contrast">Improve Contrast</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={isLoading ? "ghost" : "select"}
|
||||||
|
disabled={!changedValue || isLoading}
|
||||||
|
onClick={saveToConfig}
|
||||||
>
|
>
|
||||||
<AlertDialogContent>
|
{isLoading ? "Saving..." : "Save to Config"}
|
||||||
<AlertDialogHeader>
|
</Button>
|
||||||
<AlertDialogTitle>
|
</div>
|
||||||
You have unsaved changes on this camera.
|
</div>
|
||||||
</AlertDialogTitle>
|
{confirmationDialogOpen && (
|
||||||
<AlertDialogDescription>
|
<AlertDialog
|
||||||
Do you want to save your changes before continuing?
|
open={confirmationDialogOpen}
|
||||||
</AlertDialogDescription>
|
onOpenChange={() => setConfirmationDialogOpen(false)}
|
||||||
</AlertDialogHeader>
|
>
|
||||||
<AlertDialogFooter>
|
<AlertDialogContent>
|
||||||
<AlertDialogCancel onClick={() => handleDialog(false)}>
|
<AlertDialogHeader>
|
||||||
Cancel
|
<AlertDialogTitle>
|
||||||
</AlertDialogCancel>
|
You have unsaved changes on this camera.
|
||||||
<AlertDialogAction onClick={() => handleDialog(true)}>
|
</AlertDialogTitle>
|
||||||
Save
|
<AlertDialogDescription>
|
||||||
</AlertDialogAction>
|
Do you want to save your changes before continuing?
|
||||||
</AlertDialogFooter>
|
</AlertDialogDescription>
|
||||||
</AlertDialogContent>
|
</AlertDialogHeader>
|
||||||
</AlertDialog>
|
<AlertDialogFooter>
|
||||||
)}
|
<AlertDialogCancel onClick={() => handleDialog(false)}>
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={() => handleDialog(true)}>
|
||||||
|
Save
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{cameraConfig ? (
|
||||||
|
<div className="flex md:w-7/12 md:grow md:h-dvh md:max-h-full">
|
||||||
|
<div className="size-full min-h-10">
|
||||||
|
<AutoUpdatingCameraImage
|
||||||
|
camera={cameraConfig.name}
|
||||||
|
searchParams={new URLSearchParams([["motion", "1"]])}
|
||||||
|
showFps={false}
|
||||||
|
className="size-full"
|
||||||
|
cameraClasses="relative w-full h-full flex flex-col justify-start"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton className="size-full rounded-2xl" />
|
<Skeleton className="size-full rounded-2xl" />
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo, useRef, useState, useEffect } from "react";
|
import React, { useMemo, useRef, useState, useEffect } from "react";
|
||||||
import PolygonDrawer from "./PolygonDrawer";
|
import PolygonDrawer from "./PolygonDrawer";
|
||||||
import { Stage, Layer, Image, Text } from "react-konva";
|
import { Stage, Layer, Image, Text, Circle } from "react-konva";
|
||||||
import Konva from "konva";
|
import Konva from "konva";
|
||||||
import type { KonvaEventObject } from "konva/lib/Node";
|
import type { KonvaEventObject } from "konva/lib/Node";
|
||||||
import { Polygon, PolygonType } from "@/types/canvas";
|
import { Polygon, PolygonType } from "@/types/canvas";
|
||||||
@ -216,23 +216,43 @@ export function PolygonCanvas({
|
|||||||
isHovered={index === hoveredPolygonIndex}
|
isHovered={index === hoveredPolygonIndex}
|
||||||
isFinished={polygon.isFinished}
|
isFinished={polygon.isFinished}
|
||||||
color={polygon.color}
|
color={polygon.color}
|
||||||
|
name={polygon.name}
|
||||||
handlePointDragMove={handlePointDragMove}
|
handlePointDragMove={handlePointDragMove}
|
||||||
handleGroupDragEnd={handleGroupDragEnd}
|
handleGroupDragEnd={handleGroupDragEnd}
|
||||||
handleMouseOverStartPoint={handleMouseOverStartPoint}
|
handleMouseOverStartPoint={handleMouseOverStartPoint}
|
||||||
handleMouseOutStartPoint={handleMouseOutStartPoint}
|
handleMouseOutStartPoint={handleMouseOutStartPoint}
|
||||||
/>
|
/>
|
||||||
{index === hoveredPolygonIndex && (
|
{index === hoveredPolygonIndex && (
|
||||||
<Text
|
<>
|
||||||
text={polygon.name}
|
<Circle
|
||||||
align="left"
|
x={
|
||||||
verticalAlign="top"
|
getAveragePoint(flattenPoints(polygon.points)).x //-
|
||||||
x={
|
//(polygon.name.length * 16 * 0.6) / 2
|
||||||
getAveragePoint(flattenPoints(polygon.points)).x
|
}
|
||||||
// - (polygon.name.length * 16 * 0.6) / 2
|
y={
|
||||||
}
|
getAveragePoint(flattenPoints(polygon.points)).y //-
|
||||||
y={getAveragePoint(flattenPoints(polygon.points)).y} //- 16 / 2}
|
//16 / 2
|
||||||
fontSize={16}
|
}
|
||||||
/>
|
radius={2}
|
||||||
|
fill="red"
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
text={polygon.name}
|
||||||
|
// align="left"
|
||||||
|
// verticalAlign="top"
|
||||||
|
align="center"
|
||||||
|
verticalAlign="middle"
|
||||||
|
x={
|
||||||
|
getAveragePoint(flattenPoints(polygon.points)).x //-
|
||||||
|
//(polygon.name.length * 16 * 0.6) / 2
|
||||||
|
}
|
||||||
|
y={
|
||||||
|
getAveragePoint(flattenPoints(polygon.points)).y //-
|
||||||
|
//16 / 2
|
||||||
|
}
|
||||||
|
fontSize={16}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { Line, Circle, Group } from "react-konva";
|
import { Line, Circle, Group, Text } from "react-konva";
|
||||||
import { minMax, toRGBColorString, dragBoundFunc } from "@/utils/canvasUtil";
|
import {
|
||||||
|
minMax,
|
||||||
|
toRGBColorString,
|
||||||
|
dragBoundFunc,
|
||||||
|
getAveragePoint,
|
||||||
|
} from "@/utils/canvasUtil";
|
||||||
import type { KonvaEventObject } from "konva/lib/Node";
|
import type { KonvaEventObject } from "konva/lib/Node";
|
||||||
import Konva from "konva";
|
import Konva from "konva";
|
||||||
import { Vector2d } from "konva/lib/types";
|
import { Vector2d } from "konva/lib/types";
|
||||||
@ -12,6 +17,7 @@ type PolygonDrawerProps = {
|
|||||||
isHovered: boolean;
|
isHovered: boolean;
|
||||||
isFinished: boolean;
|
isFinished: boolean;
|
||||||
color: number[];
|
color: number[];
|
||||||
|
name: string;
|
||||||
handlePointDragMove: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
|
handlePointDragMove: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
|
||||||
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
|
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
|
||||||
handleMouseOverStartPoint: (
|
handleMouseOverStartPoint: (
|
||||||
@ -28,6 +34,7 @@ export default function PolygonDrawer({
|
|||||||
isActive,
|
isActive,
|
||||||
isHovered,
|
isHovered,
|
||||||
isFinished,
|
isFinished,
|
||||||
|
name,
|
||||||
color,
|
color,
|
||||||
handlePointDragMove,
|
handlePointDragMove,
|
||||||
handleGroupDragEnd,
|
handleGroupDragEnd,
|
||||||
@ -38,6 +45,7 @@ export default function PolygonDrawer({
|
|||||||
const [stage, setStage] = useState<Konva.Stage>();
|
const [stage, setStage] = useState<Konva.Stage>();
|
||||||
const [minMaxX, setMinMaxX] = useState([0, 0]);
|
const [minMaxX, setMinMaxX] = useState([0, 0]);
|
||||||
const [minMaxY, setMinMaxY] = useState([0, 0]);
|
const [minMaxY, setMinMaxY] = useState([0, 0]);
|
||||||
|
const groupRef = useRef<Konva.Group>(null);
|
||||||
|
|
||||||
const handleGroupMouseOver = (
|
const handleGroupMouseOver = (
|
||||||
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
|
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
|
||||||
@ -85,9 +93,12 @@ export default function PolygonDrawer({
|
|||||||
[color],
|
[color],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// console.log(groupRef.current?.height());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group
|
<Group
|
||||||
name="polygon"
|
name="polygon"
|
||||||
|
ref={groupRef}
|
||||||
draggable={isActive && isFinished}
|
draggable={isActive && isFinished}
|
||||||
onDragStart={isActive ? handleGroupDragStart : undefined}
|
onDragStart={isActive ? handleGroupDragStart : undefined}
|
||||||
onDragEnd={isActive ? handleGroupDragEnd : undefined}
|
onDragEnd={isActive ? handleGroupDragEnd : undefined}
|
||||||
@ -145,6 +156,26 @@ export default function PolygonDrawer({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{groupRef.current && (
|
||||||
|
<Text
|
||||||
|
text={name}
|
||||||
|
// align="left"
|
||||||
|
// verticalAlign="top"
|
||||||
|
width={groupRef.current.width()}
|
||||||
|
height={groupRef.current.height()}
|
||||||
|
align="center"
|
||||||
|
verticalAlign="middle"
|
||||||
|
x={
|
||||||
|
getAveragePoint(flattenedPoints).x //-
|
||||||
|
//(polygon.name.length * 16 * 0.6) / 2
|
||||||
|
}
|
||||||
|
y={
|
||||||
|
getAveragePoint(flattenedPoints).y //-
|
||||||
|
//16 / 2
|
||||||
|
}
|
||||||
|
fontSize={16}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { Polygon } from "@/types/canvas";
|
import { Polygon } from "@/types/canvas";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
|
import { MdOutlineRestartAlt, MdUndo } from "react-icons/md";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
type PolygonEditControlsProps = {
|
type PolygonEditControlsProps = {
|
||||||
polygons: Polygon[];
|
polygons: Polygon[];
|
||||||
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
||||||
activePolygonIndex: number | null;
|
activePolygonIndex: number | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PolygonEditControls({
|
export default function PolygonEditControls({
|
||||||
@ -13,39 +15,61 @@ export default function PolygonEditControls({
|
|||||||
activePolygonIndex,
|
activePolygonIndex,
|
||||||
}: PolygonEditControlsProps) {
|
}: PolygonEditControlsProps) {
|
||||||
const undo = () => {
|
const undo = () => {
|
||||||
if (activePolygonIndex !== null && polygons) {
|
if (activePolygonIndex === undefined || !polygons) {
|
||||||
const updatedPolygons = [...polygons];
|
return;
|
||||||
const activePolygon = updatedPolygons[activePolygonIndex];
|
|
||||||
if (activePolygon.points.length > 0) {
|
|
||||||
updatedPolygons[activePolygonIndex] = {
|
|
||||||
...activePolygon,
|
|
||||||
points: activePolygon.points.slice(0, -1),
|
|
||||||
isFinished: false,
|
|
||||||
};
|
|
||||||
setPolygons(updatedPolygons);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatedPolygons = [...polygons];
|
||||||
|
const activePolygon = updatedPolygons[activePolygonIndex];
|
||||||
|
updatedPolygons[activePolygonIndex] = {
|
||||||
|
...activePolygon,
|
||||||
|
points: [...activePolygon.points.slice(0, -1)],
|
||||||
|
isFinished: false,
|
||||||
|
};
|
||||||
|
setPolygons(updatedPolygons);
|
||||||
};
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
if (activePolygonIndex !== null) {
|
if (activePolygonIndex === undefined || !polygons) {
|
||||||
const updatedPolygons = [...polygons];
|
return;
|
||||||
updatedPolygons[activePolygonIndex] = {
|
|
||||||
...updatedPolygons[activePolygonIndex],
|
|
||||||
points: [],
|
|
||||||
};
|
|
||||||
setPolygons(updatedPolygons);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatedPolygons = [...polygons];
|
||||||
|
const activePolygon = updatedPolygons[activePolygonIndex];
|
||||||
|
updatedPolygons[activePolygonIndex] = {
|
||||||
|
...activePolygon,
|
||||||
|
points: [],
|
||||||
|
isFinished: false,
|
||||||
|
};
|
||||||
|
setPolygons(updatedPolygons);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex">
|
<div className="flex flex-row justify-center gap-2">
|
||||||
<Button className="mr-5" variant="secondary" onClick={undo}>
|
<Tooltip>
|
||||||
Undo
|
<TooltipTrigger asChild>
|
||||||
</Button>
|
<Button
|
||||||
<Button variant="secondary" onClick={reset}>
|
variant="secondary"
|
||||||
Reset
|
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
|
||||||
</Button>
|
onClick={undo}
|
||||||
|
>
|
||||||
|
<MdUndo />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Undo</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
|
||||||
|
onClick={reset}
|
||||||
|
>
|
||||||
|
<MdOutlineRestartAlt />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Reset</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,9 +21,11 @@ import { z } from "zod";
|
|||||||
import { Polygon } from "@/types/canvas";
|
import { Polygon } from "@/types/canvas";
|
||||||
import { Switch } from "../ui/switch";
|
import { Switch } from "../ui/switch";
|
||||||
import { Label } from "../ui/label";
|
import { Label } from "../ui/label";
|
||||||
|
import PolygonEditControls from "./PolygonEditControls";
|
||||||
|
|
||||||
type ZoneEditPaneProps = {
|
type ZoneEditPaneProps = {
|
||||||
polygons?: Polygon[];
|
polygons?: Polygon[];
|
||||||
|
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
||||||
activePolygonIndex?: number;
|
activePolygonIndex?: number;
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
@ -31,6 +33,7 @@ type ZoneEditPaneProps = {
|
|||||||
|
|
||||||
export function ZoneEditPane({
|
export function ZoneEditPane({
|
||||||
polygons,
|
polygons,
|
||||||
|
setPolygons,
|
||||||
activePolygonIndex,
|
activePolygonIndex,
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
@ -133,10 +136,26 @@ export function ZoneEditPane({
|
|||||||
<Heading as="h3" className="my-2">
|
<Heading as="h3" className="my-2">
|
||||||
Zone
|
Zone
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="flex my-3">
|
<Separator className="my-3 bg-secondary" />
|
||||||
<Separator className="bg-secondary" />
|
{polygons && activePolygonIndex !== undefined && (
|
||||||
|
<div className="flex flex-row my-2 text-sm w-full justify-between">
|
||||||
|
<div className="my-1">
|
||||||
|
{polygons[activePolygonIndex].points.length} points
|
||||||
|
</div>
|
||||||
|
{polygons[activePolygonIndex].isFinished ? <></> : <></>}
|
||||||
|
<PolygonEditControls
|
||||||
|
polygons={polygons}
|
||||||
|
setPolygons={setPolygons}
|
||||||
|
activePolygonIndex={activePolygonIndex}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mb-3 text-sm text-muted-foreground">
|
||||||
|
Click to draw a polygon on the image.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Separator className="my-3 bg-secondary" />
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
<FormField
|
<FormField
|
||||||
@ -146,8 +165,16 @@ export function ZoneEditPane({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Enter a name..." {...field} />
|
<Input
|
||||||
|
className="w-full p-2 border border-input bg-background text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
|
placeholder="Enter a name..."
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Name must be at least 2 characters and must not be the name of
|
||||||
|
a camera or another zone.
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -162,7 +189,11 @@ export function ZoneEditPane({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Inertia</FormLabel>
|
<FormLabel>Inertia</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="3" {...field} />
|
<Input
|
||||||
|
className="w-full p-2 border border-input bg-background text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
|
placeholder="3"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Specifies how many frames that an object must be in a zone
|
Specifies how many frames that an object must be in a zone
|
||||||
@ -182,7 +213,11 @@ export function ZoneEditPane({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Loitering Time</FormLabel>
|
<FormLabel>Loitering Time</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="0" {...field} />
|
<Input
|
||||||
|
className="w-full p-2 border border-input bg-background text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
|
placeholder="0"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Sets a minimum amount of time in seconds that the object must
|
Sets a minimum amount of time in seconds that the object must
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export default function Settings() {
|
|||||||
page == "masks / zones" ||
|
page == "masks / zones" ||
|
||||||
page == "motion tuner") && (
|
page == "motion tuner") && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{!isEditing && (
|
{page == "masks / zones" && (
|
||||||
<ZoneMaskFilterButton
|
<ZoneMaskFilterButton
|
||||||
selectedZoneMask={filterZoneMask}
|
selectedZoneMask={filterZoneMask}
|
||||||
updateZoneMaskFilter={setFilterZoneMask}
|
updateZoneMaskFilter={setFilterZoneMask}
|
||||||
@ -101,7 +101,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex flex-col items-start w-full h-dvh md:pb-24">
|
<div className="mt-2 flex flex-col items-start w-full h-full md:h-dvh pb-9 md:pb-24">
|
||||||
{page == "general" && <General />}
|
{page == "general" && <General />}
|
||||||
{page == "objects" && <></>}
|
{page == "objects" && <></>}
|
||||||
{page == "masks / zones" && (
|
{page == "masks / zones" && (
|
||||||
@ -114,7 +114,9 @@ export default function Settings() {
|
|||||||
setUnsavedChanges={setUnsavedChanges}
|
setUnsavedChanges={setUnsavedChanges}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{page == "motion tuner" && <MotionTuner />}
|
{page == "motion tuner" && (
|
||||||
|
<MotionTuner selectedCamera={selectedCamera} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -133,8 +133,8 @@
|
|||||||
--secondary-highlight: hsl(0, 0%, 25%);
|
--secondary-highlight: hsl(0, 0%, 25%);
|
||||||
--secondary-highlight: 0 0% 25%;
|
--secondary-highlight: 0 0% 25%;
|
||||||
|
|
||||||
--muted: hsl(0, 0%, 8%);
|
--muted: hsl(0, 0%, 15%);
|
||||||
--muted: 0 0% 8%;
|
--muted: 0 0% 15%;
|
||||||
|
|
||||||
--muted-foreground: hsl(0, 0%, 32%);
|
--muted-foreground: hsl(0, 0%, 32%);
|
||||||
--muted-foreground: 0 0% 32%;
|
--muted-foreground: 0 0% 32%;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user