This commit is contained in:
Josh Hawkins 2024-04-10 23:08:34 -05:00
parent cf96f7b706
commit 8d0385c991
6 changed files with 203 additions and 97 deletions

View File

@ -67,8 +67,8 @@ export function PolygonCanvas({
return distance < 15;
};
const handleMouseDown = (e: KonvaEventObject<MouseEvent>) => {
if (!activePolygonIndex || !polygons) {
const handleMouseDown = (e: KonvaEventObject<MouseEvent | TouchEvent>) => {
if (activePolygonIndex == null || !polygons) {
return;
}
@ -100,50 +100,63 @@ export function PolygonCanvas({
// }
};
const handleMouseOverStartPoint = (e: KonvaEventObject<MouseEvent>) => {
if (activePolygonIndex !== null && polygons) {
const activePolygon = polygons[activePolygonIndex];
if (!activePolygon.isFinished && activePolygon.points.length >= 3) {
e.currentTarget.scale({ x: 2, y: 2 });
}
const handleMouseOverStartPoint = (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => {
if (activePolygonIndex == null || !polygons) {
return;
}
const activePolygon = polygons[activePolygonIndex];
if (!activePolygon.isFinished && activePolygon.points.length >= 3) {
e.currentTarget.scale({ x: 2, y: 2 });
}
};
const handleMouseOutStartPoint = (e: KonvaEventObject<MouseEvent>) => {
const handleMouseOutStartPoint = (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => {
e.currentTarget.scale({ x: 1, y: 1 });
if (activePolygonIndex !== null && polygons) {
const activePolygon = polygons[activePolygonIndex];
if (
(!activePolygon.isFinished && activePolygon.points.length >= 3) ||
activePolygon.isFinished
) {
e.currentTarget.scale({ x: 1, y: 1 });
}
if (activePolygonIndex == null || !polygons) {
return;
}
const activePolygon = polygons[activePolygonIndex];
if (
(!activePolygon.isFinished && activePolygon.points.length >= 3) ||
activePolygon.isFinished
) {
e.currentTarget.scale({ x: 1, y: 1 });
}
};
const handlePointDragMove = (e: KonvaEventObject<MouseEvent>) => {
if (activePolygonIndex !== null && polygons) {
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
const stage = e.target.getStage();
if (stage) {
const index = e.target.index - 1;
const pos = [e.target._lastPos!.x, e.target._lastPos!.y];
if (pos[0] < 0) pos[0] = 0;
if (pos[1] < 0) pos[1] = 0;
if (pos[0] > stage.width()) pos[0] = stage.width();
if (pos[1] > stage.height()) pos[1] = stage.height();
updatedPolygons[activePolygonIndex] = {
...activePolygon,
points: [
...activePolygon.points.slice(0, index),
pos,
...activePolygon.points.slice(index + 1),
],
};
setPolygons(updatedPolygons);
}
const handlePointDragMove = (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => {
if (activePolygonIndex == null || !polygons) {
return;
}
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
const stage = e.target.getStage();
if (stage) {
const index = e.target.index - 1;
const pos = [e.target._lastPos!.x, e.target._lastPos!.y];
if (pos[0] < 0) pos[0] = 0;
if (pos[1] < 0) pos[1] = 0;
if (pos[0] > stage.width()) pos[0] = stage.width();
if (pos[1] > stage.height()) pos[1] = stage.height();
updatedPolygons[activePolygonIndex] = {
...activePolygon,
points: [
...activePolygon.points.slice(0, index),
pos,
...activePolygon.points.slice(index + 1),
],
};
setPolygons(updatedPolygons);
}
};
@ -151,7 +164,7 @@ export function PolygonCanvas({
return points.reduce((acc, point) => [...acc, ...point], []);
};
const handleGroupDragEnd = (e: KonvaEventObject<MouseEvent>) => {
const handleGroupDragEnd = (e: KonvaEventObject<MouseEvent | TouchEvent>) => {
if (activePolygonIndex !== null && e.target.name() === "polygon") {
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
@ -174,6 +187,7 @@ export function PolygonCanvas({
width={width}
height={height}
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
>
<Layer>
<Image

View File

@ -11,10 +11,14 @@ type PolygonDrawerProps = {
isActive: boolean;
isFinished: boolean;
color: number[];
handlePointDragMove: (e: KonvaEventObject<MouseEvent>) => void;
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent>) => void;
handleMouseOverStartPoint: (e: KonvaEventObject<MouseEvent>) => void;
handleMouseOutStartPoint: (e: KonvaEventObject<MouseEvent>) => void;
handlePointDragMove: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
handleMouseOverStartPoint: (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => void;
handleMouseOutStartPoint: (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => void;
};
export default function PolygonDrawer({
@ -33,13 +37,17 @@ export default function PolygonDrawer({
const [minMaxX, setMinMaxX] = useState([0, 0]);
const [minMaxY, setMinMaxY] = useState([0, 0]);
const handleGroupMouseOver = (e: Konva.KonvaEventObject<MouseEvent>) => {
const handleGroupMouseOver = (
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
) => {
if (!isFinished) return;
e.target.getStage()!.container().style.cursor = "move";
setStage(e.target.getStage()!);
};
const handleGroupMouseOut = (e: Konva.KonvaEventObject<MouseEvent>) => {
const handleGroupMouseOut = (
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
) => {
if (!e.target) return;
e.target.getStage()!.container().style.cursor = "default";
};
@ -87,6 +95,7 @@ export default function PolygonDrawer({
onDragEnd={isActive ? handleGroupDragEnd : undefined}
dragBoundFunc={isActive ? groupDragBound : undefined}
onMouseOver={isActive ? handleGroupMouseOver : undefined}
onTouchStart={isActive ? handleGroupMouseOver : undefined}
onMouseOut={isActive ? handleGroupMouseOut : undefined}
>
<Line

View File

@ -43,11 +43,8 @@ export function ZoneObjectSelector({
if (!cameraConfig || !zoneName) {
return [];
}
console.log(zoneName);
const labels = new Set<string>();
// console.log("zone name", zoneName);
// console.log(cameraConfig.zones[zoneName].objects);
cameraConfig.objects.track.forEach((label) => {
if (!ATTRIBUTES.includes(label)) {
@ -55,9 +52,13 @@ export function ZoneObjectSelector({
}
});
cameraConfig.zones[zoneName].objects.forEach((label) => {
labels.add(label);
});
if (cameraConfig.zones[zoneName]) {
cameraConfig.zones[zoneName].objects.forEach((label) => {
if (!ATTRIBUTES.includes(label)) {
labels.add(label);
}
});
}
return [...labels].sort() || [];
}, [cameraConfig, zoneName]);
@ -166,7 +167,7 @@ export function ZoneControls({
updatedPolygons[activePolygonIndex] = {
points: [],
isFinished: false,
name: "new",
name: updatedPolygons[activePolygonIndex].name,
camera: camera,
color: updatedPolygons[activePolygonIndex].color ?? [220, 0, 0],
};
@ -210,29 +211,31 @@ export function ZoneControls({
}}
>
<DialogContent>
{isMobile && <span tabIndex={0} className="sr-only" />}
<DialogTitle>New Zone</DialogTitle>
<DialogDescription>
Enter a label for your zone. Do not include spaces, and don't use
the name of a camera.
Enter a unique label for your zone. Do not include spaces, and
don't use the name of a camera.
</DialogDescription>
<>
<Input
className="mt-3"
className={`mt-3 ${isMobile && "text-md"}`}
type="search"
value={zoneName ?? ""}
onChange={(e) => {
setInvalidName(
Object.keys(config.cameras).includes(e.target.value) ||
e.target.value.includes(" "),
e.target.value.includes(" ") ||
polygons
.map((item) => item.name)
.includes(e.target.value),
);
setZoneName(e.target.value);
}}
/>
{invalidName && (
<div className="text-danger text-sm">
Zone name is not valid.
</div>
<div className="text-danger text-sm">Invalid zone name.</div>
)}
<DialogFooter>
<Button

View File

@ -20,7 +20,7 @@ import {
import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { PolygonCanvas } from "./PolygonCanvas";
import { Polygon } from "@/types/canvas";
import { interpolatePoints } from "@/utils/canvasUtil";
@ -43,9 +43,16 @@ const parseCoordinates = (coordinatesString: string) => {
return points;
};
export type ZoneObjects = {
camera: string;
zoneName: string;
objects: string[];
};
export default function SettingsZones() {
const { data: config } = useSWR<FrigateConfig>("config");
const [zonePolygons, setZonePolygons] = useState<Polygon[]>([]);
const [zoneObjects, setZoneObjects] = useState<ZoneObjects[]>([]);
const [activePolygonIndex, setActivePolygonIndex] = useState<number | null>(
null,
);
@ -85,6 +92,51 @@ export default function SettingsZones() {
return [...labels].sort();
}, [cameras]);
// const saveZoneObjects = useCallback(
// (camera: string, zoneName: string, newObjects?: string[]) => {
// setZoneObjects((prevZoneObjects) =>
// prevZoneObjects.map((zoneObject) => {
// if (
// zoneObject.camera === camera &&
// zoneObject.zoneName === zoneName
// ) {
// console.log("found", camera, "with", zoneName);
// console.log("new objects", newObjects);
// console.log("new zoneobject", {
// ...zoneObject,
// objects: newObjects ?? [],
// });
// // Replace objects with newObjects if provided
// return {
// ...zoneObject,
// objects: newObjects ?? [],
// };
// }
// return zoneObject; // Keep original object
// }),
// );
// },
// [setZoneObjects],
// );
const saveZoneObjects = useCallback(
(camera: string, zoneName: string, objects?: string[]) => {
setZoneObjects((prevZoneObjects) => {
const updatedZoneObjects = prevZoneObjects.map((zoneObject) => {
if (
zoneObject.camera === camera &&
zoneObject.zoneName === zoneName
) {
return { ...zoneObject, objects: objects || [] };
}
return zoneObject;
});
return updatedZoneObjects;
});
},
[setZoneObjects],
);
const grow = useMemo(() => {
if (!cameraConfig) {
return;
@ -105,6 +157,14 @@ export default function SettingsZones() {
}
}, [cameraConfig]);
const handleCameraChange = useCallback(
(camera: string) => {
setSelectedCamera(camera);
setActivePolygonIndex(null);
},
[setSelectedCamera, setActivePolygonIndex],
);
const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(containerRef);
@ -159,20 +219,40 @@ export default function SettingsZones() {
color: zoneData.color,
})),
);
setZoneObjects(
Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
camera: cameraConfig.name,
zoneName: name,
objects: Object.keys(zoneData.filters),
})),
);
}
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cameraConfig, containerRef]);
useEffect(() => {
console.log(
"config zone objects",
Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
camera: cameraConfig.name,
zoneName: name,
objects: Object.keys(zoneData.filters),
})),
);
console.log("component zone objects", zoneObjects);
}, [zoneObjects]);
if (!cameraConfig && !selectedCamera) {
return <ActivityIndicator />;
}
return (
<>
<div className="overflow-auto">
<Heading as="h2">Zones</Heading>
<div className="flex items-center space-x-2 mt-5">
<Select value={selectedCamera} onValueChange={setSelectedCamera}>
<Select value={selectedCamera} onValueChange={handleCameraChange}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Camera" />
</SelectTrigger>
@ -194,9 +274,9 @@ export default function SettingsZones() {
</div>
{cameraConfig && (
<div className="flex flex-row justify-evenly">
<div className="flex flex-col justify-evenly">
<div
className={`flex flex-col justify-center items-center w-[60%] ${grow}`}
className={`flex flex-col justify-center items-center w-full md:w-[60%] ${grow}`}
>
<div ref={containerRef} className="size-full">
{cameraConfig ? (
@ -213,7 +293,7 @@ export default function SettingsZones() {
)}
</div>
</div>
<div className="w-[30%]">
<div className="w-full md:w-[30%]">
<Table>
<TableHeader>
<TableRow>
@ -254,7 +334,9 @@ export default function SettingsZones() {
camera={polygon.camera}
zoneName={polygon.name}
allLabels={allLabels}
updateLabelFilter={(objects) => console.log(objects)}
updateLabelFilter={(objects) =>
saveZoneObjects(polygon.camera, polygon.name, objects)
}
/>
</TableCell>
</TableRow>
@ -294,6 +376,6 @@ export default function SettingsZones() {
</div>
</div>
)}
</>
</div>
);
}

View File

@ -46,27 +46,31 @@ function General() {
export default function Settings() {
return (
<>
<Tabs defaultValue="general" className="w-auto">
<TabsList>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="objects">Objects</TabsTrigger>
<TabsTrigger value="zones">Zones</TabsTrigger>
<TabsTrigger value="masks">Masks</TabsTrigger>
<TabsTrigger value="motion">Motion</TabsTrigger>
</TabsList>
<TabsContent value="general">
<General />
</TabsContent>
<TabsContent value="objects">Objects</TabsContent>
<TabsContent value="zones">
<SettingsZones />
</TabsContent>
<TabsContent value="masks">Masks</TabsContent>
<TabsContent value="motion">
<MotionTuner />
</TabsContent>
</Tabs>
</>
<div className="w-full h-full">
<div className="flex h-full">
<div className="flex-1 content-start gap-2 overflow-y-auto no-scrollbar mt-4 mr-5">
<Tabs defaultValue="general" className="w-auto">
<TabsList>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="objects">Objects</TabsTrigger>
<TabsTrigger value="zones">Zones</TabsTrigger>
<TabsTrigger value="masks">Masks</TabsTrigger>
<TabsTrigger value="motion">Motion</TabsTrigger>
</TabsList>
<TabsContent value="general">
<General />
</TabsContent>
<TabsContent value="objects">Objects</TabsContent>
<TabsContent value="zones">
<SettingsZones />
</TabsContent>
<TabsContent value="masks">Masks</TabsContent>
<TabsContent value="motion">
<MotionTuner />
</TabsContent>
</Tabs>
</div>
</div>
</div>
);
}

View File

@ -21,13 +21,7 @@ export interface BirdseyeConfig {
width: number;
}
export const ATTRIBUTES = [
"amazon",
"face",
"fedex",
"license_plate",
"ups",
] as const;
export const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
export interface CameraConfig {
audio: {