mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-10 21:25:24 +03:00
working multi polygons
This commit is contained in:
parent
7556d08e1c
commit
8610b02ddb
@ -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;
|
||||
@ -8,10 +8,10 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import DebugCameraImage from "@/components/camera/DebugCameraImage";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import useSWR from "swr";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@ -105,7 +105,11 @@ export default function MotionTuner() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</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-row mb-5">
|
||||
<Slider
|
||||
|
||||
277
web/src/components/settings/PolygonCanvas.tsx
Normal file
277
web/src/components/settings/PolygonCanvas.tsx
Normal 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;
|
||||
79
web/src/components/settings/PolygonControls.tsx
Normal file
79
web/src/components/settings/PolygonControls.tsx
Normal 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;
|
||||
@ -8,6 +8,7 @@ import { Vector2d } from "konva/lib/types";
|
||||
type PolygonDrawerProps = {
|
||||
points: number[][];
|
||||
flattenedPoints: number[];
|
||||
isActive: boolean;
|
||||
isFinished: boolean;
|
||||
handlePointDragMove: (e: KonvaEventObject<MouseEvent>) => void;
|
||||
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent>) => void;
|
||||
@ -18,6 +19,7 @@ type PolygonDrawerProps = {
|
||||
export default function PolygonDrawer({
|
||||
points,
|
||||
flattenedPoints,
|
||||
isActive,
|
||||
isFinished,
|
||||
handlePointDragMove,
|
||||
handleGroupDragEnd,
|
||||
@ -25,11 +27,9 @@ export default function PolygonDrawer({
|
||||
handleMouseOutStartPoint,
|
||||
}: PolygonDrawerProps) {
|
||||
const vertexRadius = 6;
|
||||
|
||||
const [stage, setStage] = useState<Konva.Stage>();
|
||||
|
||||
const [minMaxX, setMinMaxX] = useState<[number, number]>([0, 0]); //min and max in x axis
|
||||
const [minMaxY, setMinMaxY] = useState<[number, number]>([0, 0]); //min and max in y axis
|
||||
const [minMaxX, setMinMaxX] = useState([0, 0]);
|
||||
const [minMaxY, setMinMaxY] = useState([0, 0]);
|
||||
|
||||
const handleGroupMouseOver = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
if (!isFinished) return;
|
||||
@ -53,39 +53,40 @@ export default function PolygonDrawer({
|
||||
if (!stage) {
|
||||
return pos;
|
||||
}
|
||||
|
||||
let { x, y } = pos;
|
||||
const sw = stage.width();
|
||||
const sh = stage.height();
|
||||
|
||||
if (minMaxY[0] + y < 0) y = -1 * minMaxY[0];
|
||||
if (minMaxX[0] + x < 0) x = -1 * minMaxX[0];
|
||||
if (minMaxY[1] + y > sh) y = sh - minMaxY[1];
|
||||
if (minMaxX[1] + x > sw) x = sw - minMaxX[1];
|
||||
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
// const flattenedPointsAsNumber = useMemo(
|
||||
// () => flattenedPoints.flatMap((point) => [point.x, point.y]),
|
||||
// [flattenedPoints],
|
||||
// );
|
||||
|
||||
return (
|
||||
<Group
|
||||
name="polygon"
|
||||
draggable={isFinished}
|
||||
onDragStart={handleGroupDragStart}
|
||||
onDragEnd={handleGroupDragEnd}
|
||||
dragBoundFunc={groupDragBound}
|
||||
onMouseOver={handleGroupMouseOver}
|
||||
onMouseOut={handleGroupMouseOut}
|
||||
draggable={isActive && isFinished}
|
||||
onDragStart={isActive ? handleGroupDragStart : undefined}
|
||||
onDragEnd={isActive ? handleGroupDragEnd : undefined}
|
||||
dragBoundFunc={isActive ? groupDragBound : undefined}
|
||||
onMouseOver={isActive ? handleGroupMouseOver : undefined}
|
||||
onMouseOut={isActive ? handleGroupMouseOut : undefined}
|
||||
>
|
||||
<Line
|
||||
points={flattenedPoints}
|
||||
stroke="#00F1FF"
|
||||
stroke="#aa0000"
|
||||
strokeWidth={3}
|
||||
closed={isFinished}
|
||||
fill="rgb(140,30,255,0.5)"
|
||||
fill="rgb(220,0,0,0.5)"
|
||||
/>
|
||||
{points.map((point, index) => {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
const x = point[0] - vertexRadius / 2;
|
||||
const y = point[1] - vertexRadius / 2;
|
||||
const startPointAttr =
|
||||
@ -96,17 +97,18 @@ export default function PolygonDrawer({
|
||||
onMouseOut: handleMouseOutStartPoint,
|
||||
}
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Circle
|
||||
key={index}
|
||||
x={x}
|
||||
y={y}
|
||||
radius={vertexRadius}
|
||||
fill="#FF019A"
|
||||
stroke="#00F1FF"
|
||||
fill="#dd0000"
|
||||
stroke="#cccccc"
|
||||
strokeWidth={2}
|
||||
draggable
|
||||
onDragMove={handlePointDragMove}
|
||||
draggable={isActive}
|
||||
onDragMove={isActive ? handlePointDragMove : undefined}
|
||||
dragBoundFunc={(pos) => {
|
||||
if (stage) {
|
||||
return dragBoundFunc(
|
||||
@ -116,7 +118,7 @@ export default function PolygonDrawer({
|
||||
pos,
|
||||
);
|
||||
} else {
|
||||
return pos; // Return original pos if stage is not defined
|
||||
return pos;
|
||||
}
|
||||
}}
|
||||
{...startPointAttr}
|
||||
|
||||
@ -8,18 +8,43 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import DebugCameraImage from "@/components/camera/DebugCameraImage";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import useSWR from "swr";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useMotionContourArea, useMotionThreshold } from "@/api/ws";
|
||||
import DebugCanvas from "./DebugCanvas";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { PolygonCanvas } from "./PolygonCanvas";
|
||||
import { useApiHost } from "@/api";
|
||||
import { Polygon } from "@/types/canvas";
|
||||
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() {
|
||||
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(() => {
|
||||
if (!config) {
|
||||
@ -39,6 +64,119 @@ export default function SettingsZones() {
|
||||
}
|
||||
}, [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) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
@ -46,7 +184,12 @@ export default function SettingsZones() {
|
||||
// console.log("selected camera", selectedCamera);
|
||||
// console.log("threshold", motionThreshold);
|
||||
// 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 (
|
||||
<>
|
||||
@ -72,8 +215,59 @@ export default function SettingsZones() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ function General() {
|
||||
export default function Settings() {
|
||||
return (
|
||||
<>
|
||||
<Tabs defaultValue="account" className="w-auto">
|
||||
<Tabs defaultValue="general" className="w-auto">
|
||||
<TabsList>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="objects">Objects</TabsTrigger>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export type Point = {
|
||||
x: number;
|
||||
y: number;
|
||||
export type Polygon = {
|
||||
name: string;
|
||||
points: number[][];
|
||||
isFinished: boolean;
|
||||
};
|
||||
|
||||
@ -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 totalY = 0;
|
||||
for (let i = 0; i < points.length; i += 2) {
|
||||
@ -22,8 +24,8 @@ export const dragBoundFunc = (
|
||||
stageWidth: number,
|
||||
stageHeight: number,
|
||||
vertexRadius: number,
|
||||
pos: Point,
|
||||
): Point => {
|
||||
pos: Vector2d,
|
||||
): Vector2d => {
|
||||
let x = pos.x;
|
||||
let y = pos.y;
|
||||
if (pos.x + vertexRadius > stageWidth) x = stageWidth;
|
||||
@ -43,3 +45,21 @@ export const minMax = (points: number[]): [number, number] => {
|
||||
[undefined, undefined],
|
||||
) 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;
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user