working multi polygons

This commit is contained in:
Josh Hawkins 2024-04-07 20:57:15 -05:00
parent 7556d08e1c
commit 8610b02ddb
9 changed files with 617 additions and 237 deletions

View File

@ -1,197 +0,0 @@
import React, { useMemo, useRef, useState, useEffect } from "react";
import PolygonDrawer from "./PolygonDrawer";
import { Stage, Layer, Image } from "react-konva";
import Konva from "konva";
import type { KonvaEventObject } from "konva/lib/Node";
import { Button } from "../ui/button";
const videoSource = "./space_landscape.jpg";
const wrapperStyle: React.CSSProperties = {
display: "flex",
justifyContent: "center",
marginTop: 20,
backgroundColor: "aliceblue",
};
const columnStyle: React.CSSProperties = {
display: "flex",
justifyContent: "center",
flexDirection: "column",
alignItems: "center",
marginTop: 20,
backgroundColor: "aliceblue",
};
export function DebugCanvas() {
const [image, setImage] = useState<HTMLImageElement | undefined>();
const imageRef = useRef<Konva.Image | null>(null);
const dataRef = useRef<HTMLDivElement | null>(null);
const stageRef = useRef<Konva.Stage>(null);
const [points, setPoints] = useState<number[][]>([]);
const [size, setSize] = useState<{ width: number; height: number }>({
width: 0,
height: 0,
});
const [flattenedPoints, setFlattenedPoints] = useState<number[]>([]);
const [position, setPosition] = useState([0, 0]);
const [isMouseOverPoint, setMouseOverPoint] = useState(false);
const [isPolyComplete, setPolyComplete] = useState(false);
const videoElement = useMemo(() => {
const element = new window.Image();
element.width = 650;
element.height = 302;
element.src = videoSource;
return element;
}, []); // No dependency needed here since videoSource is a constant
useEffect(() => {
const onload = function () {
setSize({
width: videoElement.width,
height: videoElement.height,
});
setImage(videoElement);
if (imageRef.current) imageRef.current = videoElement;
};
videoElement.addEventListener("load", onload);
return () => {
videoElement.removeEventListener("load", onload);
};
}, [videoElement]);
const getMousePos = (stage: Konva.Stage) => {
return [stage.getPointerPosition()!.x, stage.getPointerPosition()!.y];
};
const handleMouseDown = (e: KonvaEventObject<MouseEvent>) => {
if (isPolyComplete) return;
const stage = e.target.getStage()!;
const mousePos = getMousePos(stage);
if (isMouseOverPoint && points.length >= 3) {
setPolyComplete(true);
} else {
setPoints([...points, mousePos]);
}
};
const handleMouseMove = (e: KonvaEventObject<MouseEvent>) => {
const stage = e.target.getStage()!;
const mousePos = getMousePos(stage);
setPosition(mousePos);
};
const handleMouseOverStartPoint = (e: KonvaEventObject<MouseEvent>) => {
if (isPolyComplete || points.length < 3) return;
e.currentTarget.scale({ x: 3, y: 3 });
setMouseOverPoint(true);
};
const handleMouseOutStartPoint = (e: KonvaEventObject<MouseEvent>) => {
e.currentTarget.scale({ x: 1, y: 1 });
setMouseOverPoint(false);
};
const handlePointDragMove = (e: KonvaEventObject<MouseEvent>) => {
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();
setPoints([...points.slice(0, index), pos, ...points.slice(index + 1)]);
}
};
useEffect(() => {
setFlattenedPoints(
points
.concat(isPolyComplete ? [] : position)
.reduce((a, b) => a.concat(b), []),
);
}, [points, isPolyComplete, position]);
const undo = () => {
setPoints(points.slice(0, -1));
setPolyComplete(false);
setPosition(points[points.length - 1]);
};
const reset = () => {
setPoints([]);
setPolyComplete(false);
};
const handleGroupDragEnd = (e: KonvaEventObject<MouseEvent>) => {
if (e.target.name() === "polygon") {
const result: number[][] = [];
const copyPoints = [...points];
copyPoints.map((point) =>
result.push([point[0] + e.target.x(), point[1] + e.target.y()]),
);
e.target.position({ x: 0, y: 0 });
setPoints(result);
}
};
return (
<div style={wrapperStyle}>
<div style={columnStyle}>
<Stage
ref={stageRef}
width={size.width || 650}
height={size.height || 302}
onMouseMove={handleMouseMove}
onMouseDown={handleMouseDown}
>
<Layer>
<Image
ref={imageRef}
image={image}
x={0}
y={0}
width={size.width}
height={size.height}
/>
<PolygonDrawer
points={points}
flattenedPoints={flattenedPoints}
handlePointDragMove={handlePointDragMove}
handleGroupDragEnd={handleGroupDragEnd}
handleMouseOverStartPoint={handleMouseOverStartPoint}
handleMouseOutStartPoint={handleMouseOutStartPoint}
isFinished={isPolyComplete}
/>
</Layer>
</Stage>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Button name="Undo" onClick={undo} />
<Button name="Reset" onClick={reset} />
</div>
</div>
<div
ref={dataRef}
style={{
width: 375,
height: 302,
boxShadow: ".5px .5px 5px .4em rgba(0,0,0,.1)",
marginTop: 20,
color: "black",
}}
>
<pre style={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(points)}</pre>
</div>
</div>
);
}
export default DebugCanvas;

View File

@ -8,10 +8,10 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import DebugCameraImage from "@/components/camera/DebugCameraImage";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr"; import useSWR from "swr";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { Slider } from "@/components/ui/slider"; import { Slider } from "@/components/ui/slider";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@ -105,7 +105,11 @@ export default function MotionTuner() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<DebugCameraImage cameraConfig={cameraConfig} className="w-[50%]" /> <AutoUpdatingCameraImage
camera={cameraConfig.name}
searchParams={new URLSearchParams([["motion", "1"]])}
className="w-[50%]"
/>
<div className="flex flex-col justify-evenly w-full"> <div className="flex flex-col justify-evenly w-full">
<div className="flex flex-row mb-5"> <div className="flex flex-row mb-5">
<Slider <Slider

View File

@ -0,0 +1,277 @@
import React, { useMemo, useRef, useState, useEffect } from "react";
import PolygonDrawer from "./PolygonDrawer";
import { Stage, Layer, Image } from "react-konva";
import Konva from "konva";
import type { KonvaEventObject } from "konva/lib/Node";
import { Polygon } from "@/types/canvas";
import { useApiHost } from "@/api";
import { useResizeObserver } from "@/hooks/resize-observer";
type PolygonCanvasProps = {
camera: string;
width: number;
height: number;
polygons: Polygon[];
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
activePolygonIndex: number | null;
setActivePolygonIndex: React.Dispatch<React.SetStateAction<number | null>>;
};
export function PolygonCanvas({
camera,
width,
height,
polygons,
setPolygons,
activePolygonIndex,
setActivePolygonIndex,
}: PolygonCanvasProps) {
const [image, setImage] = useState<HTMLImageElement | undefined>();
const imageRef = useRef<Konva.Image | null>(null);
// const containerRef = useRef<HTMLDivElement | null>(null);
const stageRef = useRef<Konva.Stage>(null);
// const [points, setPoints] = useState<number[][]>([]);
// const [activePolygonIndex, setActivePolygonIndex] = useState<number | null>(
// null,
// );
// const [size, setSize] = useState<{ width: number; height: number }>({
// width: width,
// height: height,
// });
const apiHost = useApiHost();
// const [position, setPosition] = useState([0, 0]);
// const [{ width: windowWidth }] = useResizeObserver(window);
const videoElement = useMemo(() => {
if (camera && width && height) {
// console.log("width:", containerRef.current.clientWidth);
// console.log("width:", containerRef.current.clientHeight);
const element = new window.Image();
element.width = width; //containerRef.current.clientWidth;
element.height = height; //containerRef.current.clientHeight;
element.src = `${apiHost}api/${camera}/latest.jpg`;
// setSize({
// width: width,
// height: height,
// });
return element;
}
}, [camera, width, height, apiHost]);
// const imageScale = scaledWidth / 720;
// console.log("window width", windowWidth);
useEffect(() => {
if (!videoElement) {
return;
}
const onload = function () {
setImage(videoElement);
// if (!imageRef.current) imageRef.current = videoElement;
console.log(videoElement, Date.now());
};
videoElement.addEventListener("load", onload);
return () => {
videoElement.removeEventListener("load", onload);
};
}, [videoElement]);
// use Konva.Animation to redraw a layer
// useEffect(() => {
// //videoElement.play();
// if (!videoElement && !imageRef && !imageRef.current) {
// return;
// }
// const layer = imageRef.current?.getLayer();
// console.log("layer", layer);
// const anim = new Konva.Animation(() => {}, layer);
// anim.start();
// return () => {
// anim.stop();
// };
// }, [videoElement]);
const getMousePos = (stage: Konva.Stage) => {
return [stage.getPointerPosition()!.x, stage.getPointerPosition()!.y];
};
const isMouseOverPoint = (polygon: Polygon, mousePos: number[]) => {
if (!polygon || !polygon.points) {
return false;
}
const [firstPoint] = polygon.points;
console.log("first", firstPoint);
const distance = Math.hypot(
mousePos[0] - firstPoint[0],
mousePos[1] - firstPoint[1],
);
return distance < 15;
};
const handleMouseDown = (e: KonvaEventObject<MouseEvent>) => {
if (activePolygonIndex === null || !polygons) {
return;
}
console.log("mouse down polygons", polygons);
console.log(activePolygonIndex);
if (!polygons[activePolygonIndex].points.length) {
// Start a new polygon
const stage = e.target.getStage()!;
const mousePos = getMousePos(stage);
setPolygons([
...polygons,
{
name: "foo",
points: [mousePos],
isFinished: false,
},
]);
setActivePolygonIndex(polygons.length);
} else {
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
const stage = e.target.getStage()!;
const mousePos = getMousePos(stage);
if (
isMouseOverPoint(activePolygon, mousePos) &&
activePolygon.points.length >= 3
) {
// Close the polygon
updatedPolygons[activePolygonIndex] = {
...activePolygon,
isFinished: true,
};
setPolygons(updatedPolygons);
// setActivePolygonIndex(null);
} else {
if (!activePolygon.isFinished) {
// Add a new point to the active polygon
updatedPolygons[activePolygonIndex] = {
...activePolygon,
points: [...activePolygon.points, mousePos],
};
setPolygons(updatedPolygons);
}
}
}
};
const handleMouseMove = (e: KonvaEventObject<MouseEvent>) => {
const stage = e.target.getStage()!;
const mousePos = getMousePos(stage);
// setPosition(mousePos);
};
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 handleMouseOutStartPoint = (e: KonvaEventObject<MouseEvent>) => {
console.log("active index:", activePolygonIndex);
e.currentTarget.scale({ x: 1, y: 1 });
if (activePolygonIndex !== null && polygons) {
const activePolygon = polygons[activePolygonIndex];
console.log(activePolygon);
if (
(!activePolygon.isFinished && activePolygon.points.length >= 3) ||
activePolygon.isFinished
) {
console.log(e.currentTarget);
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 flattenPoints = (points: number[][]): number[] => {
return points.reduce((acc, point) => [...acc, ...point], []);
};
const handleGroupDragEnd = (e: KonvaEventObject<MouseEvent>) => {
if (activePolygonIndex !== null && e.target.name() === "polygon") {
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
const result: number[][] = [];
activePolygon.points.map((point: number[]) =>
result.push([point[0] + e.target.x(), point[1] + e.target.y()]),
);
e.target.position({ x: 0, y: 0 });
updatedPolygons[activePolygonIndex] = {
...activePolygon,
points: result,
};
setPolygons(updatedPolygons);
}
};
return (
<Stage
ref={stageRef}
width={width}
height={height}
onMouseMove={handleMouseMove}
onMouseDown={handleMouseDown}
>
<Layer>
<Image
ref={imageRef}
image={image}
x={0}
y={0}
width={width}
height={height}
/>
{polygons &&
polygons.map((polygon, index) => (
<PolygonDrawer
key={index}
points={polygon.points}
flattenedPoints={flattenPoints(polygon.points)}
isActive={index === activePolygonIndex}
isFinished={polygon.isFinished}
handlePointDragMove={handlePointDragMove}
handleGroupDragEnd={handleGroupDragEnd}
handleMouseOverStartPoint={handleMouseOverStartPoint}
handleMouseOutStartPoint={handleMouseOutStartPoint}
/>
))}
</Layer>
</Stage>
);
}
export default PolygonCanvas;

View File

@ -0,0 +1,79 @@
import { Button } from "../ui/button";
import { Polygon } from "@/types/canvas";
type PolygonCanvasProps = {
camera: string;
width: number;
height: number;
polygons: Polygon[];
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
activePolygonIndex: number | null;
setActivePolygonIndex: React.Dispatch<React.SetStateAction<number | null>>;
};
export function PolygonControls({
polygons,
setPolygons,
activePolygonIndex,
setActivePolygonIndex,
}: PolygonCanvasProps) {
const undo = () => {
if (activePolygonIndex !== null && polygons) {
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
if (activePolygon.points.length > 0) {
updatedPolygons[activePolygonIndex] = {
...activePolygon,
points: activePolygon.points.slice(0, -1),
isFinished: false,
};
setPolygons(updatedPolygons);
}
}
};
const reset = () => {
if (activePolygonIndex !== null) {
const updatedPolygons = [...polygons];
updatedPolygons[activePolygonIndex] = {
points: [],
isFinished: false,
name: "new",
};
setPolygons(updatedPolygons);
}
};
const handleNewPolygon = () => {
setPolygons([
...(polygons || []),
{
points: [],
isFinished: false,
name: "new",
},
]);
console.log(polygons.length);
console.log(polygons);
console.log("active index", polygons.length);
setActivePolygonIndex(polygons.length);
};
return (
<div className="flex flex-col">
<div className="flex justify-between items-center my-5">
<Button className="mr-5" variant="secondary" onClick={undo}>
Undo
</Button>
<Button variant="secondary" onClick={reset}>
Reset
</Button>
<Button variant="secondary" onClick={handleNewPolygon}>
New Polygon
</Button>
</div>
</div>
);
}
export default PolygonControls;

View File

@ -8,6 +8,7 @@ import { Vector2d } from "konva/lib/types";
type PolygonDrawerProps = { type PolygonDrawerProps = {
points: number[][]; points: number[][];
flattenedPoints: number[]; flattenedPoints: number[];
isActive: boolean;
isFinished: boolean; isFinished: boolean;
handlePointDragMove: (e: KonvaEventObject<MouseEvent>) => void; handlePointDragMove: (e: KonvaEventObject<MouseEvent>) => void;
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent>) => void; handleGroupDragEnd: (e: KonvaEventObject<MouseEvent>) => void;
@ -18,6 +19,7 @@ type PolygonDrawerProps = {
export default function PolygonDrawer({ export default function PolygonDrawer({
points, points,
flattenedPoints, flattenedPoints,
isActive,
isFinished, isFinished,
handlePointDragMove, handlePointDragMove,
handleGroupDragEnd, handleGroupDragEnd,
@ -25,11 +27,9 @@ export default function PolygonDrawer({
handleMouseOutStartPoint, handleMouseOutStartPoint,
}: PolygonDrawerProps) { }: PolygonDrawerProps) {
const vertexRadius = 6; const vertexRadius = 6;
const [stage, setStage] = useState<Konva.Stage>(); const [stage, setStage] = useState<Konva.Stage>();
const [minMaxX, setMinMaxX] = useState([0, 0]);
const [minMaxX, setMinMaxX] = useState<[number, number]>([0, 0]); //min and max in x axis const [minMaxY, setMinMaxY] = useState([0, 0]);
const [minMaxY, setMinMaxY] = useState<[number, number]>([0, 0]); //min and max in y axis
const handleGroupMouseOver = (e: Konva.KonvaEventObject<MouseEvent>) => { const handleGroupMouseOver = (e: Konva.KonvaEventObject<MouseEvent>) => {
if (!isFinished) return; if (!isFinished) return;
@ -53,39 +53,40 @@ export default function PolygonDrawer({
if (!stage) { if (!stage) {
return pos; return pos;
} }
let { x, y } = pos; let { x, y } = pos;
const sw = stage.width(); const sw = stage.width();
const sh = stage.height(); const sh = stage.height();
if (minMaxY[0] + y < 0) y = -1 * minMaxY[0]; if (minMaxY[0] + y < 0) y = -1 * minMaxY[0];
if (minMaxX[0] + x < 0) x = -1 * minMaxX[0]; if (minMaxX[0] + x < 0) x = -1 * minMaxX[0];
if (minMaxY[1] + y > sh) y = sh - minMaxY[1]; if (minMaxY[1] + y > sh) y = sh - minMaxY[1];
if (minMaxX[1] + x > sw) x = sw - minMaxX[1]; if (minMaxX[1] + x > sw) x = sw - minMaxX[1];
return { x, y }; return { x, y };
}; };
// const flattenedPointsAsNumber = useMemo(
// () => flattenedPoints.flatMap((point) => [point.x, point.y]),
// [flattenedPoints],
// );
return ( return (
<Group <Group
name="polygon" name="polygon"
draggable={isFinished} draggable={isActive && isFinished}
onDragStart={handleGroupDragStart} onDragStart={isActive ? handleGroupDragStart : undefined}
onDragEnd={handleGroupDragEnd} onDragEnd={isActive ? handleGroupDragEnd : undefined}
dragBoundFunc={groupDragBound} dragBoundFunc={isActive ? groupDragBound : undefined}
onMouseOver={handleGroupMouseOver} onMouseOver={isActive ? handleGroupMouseOver : undefined}
onMouseOut={handleGroupMouseOut} onMouseOut={isActive ? handleGroupMouseOut : undefined}
> >
<Line <Line
points={flattenedPoints} points={flattenedPoints}
stroke="#00F1FF" stroke="#aa0000"
strokeWidth={3} strokeWidth={3}
closed={isFinished} closed={isFinished}
fill="rgb(140,30,255,0.5)" fill="rgb(220,0,0,0.5)"
/> />
{points.map((point, index) => { {points.map((point, index) => {
if (!isActive) {
return;
}
const x = point[0] - vertexRadius / 2; const x = point[0] - vertexRadius / 2;
const y = point[1] - vertexRadius / 2; const y = point[1] - vertexRadius / 2;
const startPointAttr = const startPointAttr =
@ -96,17 +97,18 @@ export default function PolygonDrawer({
onMouseOut: handleMouseOutStartPoint, onMouseOut: handleMouseOutStartPoint,
} }
: null; : null;
return ( return (
<Circle <Circle
key={index} key={index}
x={x} x={x}
y={y} y={y}
radius={vertexRadius} radius={vertexRadius}
fill="#FF019A" fill="#dd0000"
stroke="#00F1FF" stroke="#cccccc"
strokeWidth={2} strokeWidth={2}
draggable draggable={isActive}
onDragMove={handlePointDragMove} onDragMove={isActive ? handlePointDragMove : undefined}
dragBoundFunc={(pos) => { dragBoundFunc={(pos) => {
if (stage) { if (stage) {
return dragBoundFunc( return dragBoundFunc(
@ -116,7 +118,7 @@ export default function PolygonDrawer({
pos, pos,
); );
} else { } else {
return pos; // Return original pos if stage is not defined return pos;
} }
}} }}
{...startPointAttr} {...startPointAttr}

View File

@ -8,18 +8,43 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import DebugCameraImage from "@/components/camera/DebugCameraImage";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr"; import useSWR from "swr";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Slider } from "@/components/ui/slider"; import { PolygonCanvas } from "./PolygonCanvas";
import { Label } from "@/components/ui/label"; import { useApiHost } from "@/api";
import { useMotionContourArea, useMotionThreshold } from "@/api/ws"; import { Polygon } from "@/types/canvas";
import DebugCanvas from "./DebugCanvas"; import { interpolatePoints } from "@/utils/canvasUtil";
import AutoUpdatingCameraImage from "../camera/AutoUpdatingCameraImage";
import { isDesktop } from "react-device-detect";
import PolygonControls from "./PolygonControls";
import { Skeleton } from "../ui/skeleton";
import { useResizeObserver } from "@/hooks/resize-observer";
const parseCoordinates = (coordinatesString: string) => {
const coordinates = coordinatesString.split(",");
const points = [];
for (let i = 0; i < coordinates.length; i += 2) {
const x = parseInt(coordinates[i], 10);
const y = parseInt(coordinates[i + 1], 10);
points.push([x, y]);
}
return points;
};
export default function SettingsZones() { export default function SettingsZones() {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const [zonePolygons, setZonePolygons] = useState<Polygon[]>([]);
const [activePolygonIndex, setActivePolygonIndex] = useState<number | null>(
null,
);
const imgRef = useRef<HTMLImageElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const apiHost = useApiHost();
// const videoSource = `${apiHost}api/ptzcam/latest.jpg`;
const cameras = useMemo(() => { const cameras = useMemo(() => {
if (!config) { if (!config) {
@ -39,6 +64,119 @@ export default function SettingsZones() {
} }
}, [config, selectedCamera]); }, [config, selectedCamera]);
const cameraAspect = useMemo(() => {
if (!cameraConfig) {
return;
}
const aspectRatio = cameraConfig.detect.width / cameraConfig.detect.height;
console.log("aspect", aspectRatio);
if (!aspectRatio) {
return "normal";
} else if (aspectRatio > 2) {
return "wide";
} else if (aspectRatio < 16 / 9) {
return "tall";
} else {
return "normal";
}
}, [cameraConfig]);
const grow = useMemo(() => {
if (cameraAspect == "wide") {
return "aspect-wide";
} else if (cameraAspect == "tall") {
if (isDesktop) {
return "size-full aspect-tall";
} else {
return "size-full";
}
} else {
return "aspect-video";
}
}, [cameraAspect]);
const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(containerRef);
// Add scrollbar width (when visible) to the available observer width to eliminate screen juddering.
// https://github.com/blakeblackshear/frigate/issues/1657
let scrollBarWidth = 0;
if (window.innerWidth && document.body.offsetWidth) {
scrollBarWidth = window.innerWidth - document.body.offsetWidth;
}
const availableWidth = scrollBarWidth
? containerWidth + scrollBarWidth
: containerWidth;
const { width, height } = cameraConfig
? cameraConfig.detect
: { width: 1, height: 1 };
const aspectRatio = width / height;
const stretch = true;
const fitAspect = 1.8;
const scaledHeight = useMemo(() => {
const scaledHeight =
aspectRatio < (fitAspect ?? 0)
? Math.floor(containerHeight)
: Math.floor(availableWidth / aspectRatio);
const finalHeight = stretch ? scaledHeight : Math.min(scaledHeight, height);
if (finalHeight > 0) {
return finalHeight;
}
return 100;
}, [
availableWidth,
aspectRatio,
containerHeight,
fitAspect,
height,
stretch,
]);
const scaledWidth = useMemo(
() => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth),
[scaledHeight, aspectRatio, scrollBarWidth],
);
useEffect(() => {
if (cameraConfig && containerRef.current) {
setZonePolygons(
Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
name,
points: interpolatePoints(
parseCoordinates(zoneData.coordinates),
cameraConfig.detect.width,
cameraConfig.detect.height,
scaledWidth,
scaledHeight,
),
isFinished: true,
})),
);
}
}, [cameraConfig, containerRef, scaledWidth, scaledHeight]);
// const image = useMemo(() => {
// if (cameraConfig && containerRef && containerRef.current) {
// console.log("width:", containerRef.current.clientWidth);
// const element = new window.Image();
// element.width = containerRef.current.clientWidth;
// element.height = containerRef.current.clientHeight;
// element.src = `${apiHost}api/${cameraConfig.name}/latest.jpg`;
// return element;
// }
// }, [cameraConfig, apiHost, containerRef]);
// useEffect(() => {
// if (image) {
// imgRef.current = image;
// }
// }, [image]);
if (!cameraConfig && !selectedCamera) { if (!cameraConfig && !selectedCamera) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
@ -46,7 +184,12 @@ export default function SettingsZones() {
// console.log("selected camera", selectedCamera); // console.log("selected camera", selectedCamera);
// console.log("threshold", motionThreshold); // console.log("threshold", motionThreshold);
// console.log("contour area", motionContourArea); // console.log("contour area", motionContourArea);
// console.log(cameraConfig); // console.log("zone polygons", zonePolygons);
// console.log("width:", containerRef.current.clientWidth);
// const element = new window.Image();
// element.width = containerRef.current.clientWidth;
// element.height = containerRef.current.clientHeight;
return ( return (
<> <>
@ -72,8 +215,59 @@ export default function SettingsZones() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<DebugCameraImage cameraConfig={cameraConfig} className="w-[50%]" />
<DebugCanvas /> {cameraConfig && (
<div className="flex flex-row justify-evenly">
<div
className={`flex flex-col justify-center items-center w-[50%] ${grow}`}
>
<div ref={containerRef} className="size-full">
{cameraConfig ? (
<PolygonCanvas
camera={cameraConfig.name}
width={scaledWidth}
height={scaledHeight}
polygons={zonePolygons}
setPolygons={setZonePolygons}
activePolygonIndex={activePolygonIndex}
setActivePolygonIndex={setActivePolygonIndex}
/>
) : (
<Skeleton className="w-full h-full" />
)}
</div>
</div>
<div className="w-[30%]">
<PolygonControls
camera={cameraConfig.name}
width={scaledWidth}
height={scaledHeight}
polygons={zonePolygons}
setPolygons={setZonePolygons}
activePolygonIndex={activePolygonIndex}
setActivePolygonIndex={setActivePolygonIndex}
/>
<div className="flex flex-col justify-center items-center m-auto w-[30%] bg-secondary">
<pre style={{ whiteSpace: "pre-wrap" }}>
{JSON.stringify(
zonePolygons &&
zonePolygons.map((polygon) =>
interpolatePoints(
polygon.points,
scaledWidth,
scaledHeight,
cameraConfig.detect.width,
cameraConfig.detect.height,
),
),
null,
0,
)}
</pre>
</div>
</div>
</div>
)}
</> </>
); );
} }

View File

@ -47,7 +47,7 @@ function General() {
export default function Settings() { export default function Settings() {
return ( return (
<> <>
<Tabs defaultValue="account" className="w-auto"> <Tabs defaultValue="general" className="w-auto">
<TabsList> <TabsList>
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="objects">Objects</TabsTrigger> <TabsTrigger value="objects">Objects</TabsTrigger>

View File

@ -1,4 +1,5 @@
export type Point = { export type Polygon = {
x: number; name: string;
y: number; points: number[][];
isFinished: boolean;
}; };

View File

@ -1,4 +1,6 @@
export const getAveragePoint = (points: number[]): number => { import { Vector2d } from "konva/lib/types";
export const getAveragePoint = (points: number[]): Vector2d => {
let totalX = 0; let totalX = 0;
let totalY = 0; let totalY = 0;
for (let i = 0; i < points.length; i += 2) { for (let i = 0; i < points.length; i += 2) {
@ -22,8 +24,8 @@ export const dragBoundFunc = (
stageWidth: number, stageWidth: number,
stageHeight: number, stageHeight: number,
vertexRadius: number, vertexRadius: number,
pos: Point, pos: Vector2d,
): Point => { ): Vector2d => {
let x = pos.x; let x = pos.x;
let y = pos.y; let y = pos.y;
if (pos.x + vertexRadius > stageWidth) x = stageWidth; if (pos.x + vertexRadius > stageWidth) x = stageWidth;
@ -43,3 +45,21 @@ export const minMax = (points: number[]): [number, number] => {
[undefined, undefined], [undefined, undefined],
) as [number, number]; ) as [number, number];
}; };
export const interpolatePoints = (
points: number[][],
width: number,
height: number,
newWidth: number,
newHeight: number,
): number[][] => {
const newPoints: number[][] = [];
for (const [x, y] of points) {
const newX = (x * newWidth) / width;
const newY = (y * newHeight) / height;
newPoints.push([Math.floor(newX), Math.floor(newY)]);
}
return newPoints;
};