Snap points to edges and create object mask from bounding box (#16488)

This commit is contained in:
Josh Hawkins
2025-02-11 10:08:28 -06:00
committed by GitHub
parent b594f198a9
commit a3ede3cf8a
10 changed files with 329 additions and 65 deletions

View File

@@ -33,6 +33,8 @@ type MotionMaskEditPaneProps = {
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
onSave?: () => void;
onCancel?: () => void;
snapPoints: boolean;
setSnapPoints: React.Dispatch<React.SetStateAction<boolean>>;
};
export default function MotionMaskEditPane({
@@ -45,6 +47,8 @@ export default function MotionMaskEditPane({
setIsLoading,
onSave,
onCancel,
snapPoints,
setSnapPoints,
}: MotionMaskEditPaneProps) {
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
@@ -252,6 +256,8 @@ export default function MotionMaskEditPane({
polygons={polygons}
setPolygons={setPolygons}
activePolygonIndex={activePolygonIndex}
snapPoints={snapPoints}
setSnapPoints={setSnapPoints}
/>
</div>
)}

View File

@@ -49,6 +49,8 @@ type ObjectMaskEditPaneProps = {
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
onSave?: () => void;
onCancel?: () => void;
snapPoints: boolean;
setSnapPoints: React.Dispatch<React.SetStateAction<boolean>>;
};
export default function ObjectMaskEditPane({
@@ -61,6 +63,8 @@ export default function ObjectMaskEditPane({
setIsLoading,
onSave,
onCancel,
snapPoints,
setSnapPoints,
}: ObjectMaskEditPaneProps) {
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
@@ -272,6 +276,8 @@ export default function ObjectMaskEditPane({
polygons={polygons}
setPolygons={setPolygons}
activePolygonIndex={activePolygonIndex}
snapPoints={snapPoints}
setSnapPoints={setSnapPoints}
/>
</div>
)}

View File

@@ -6,6 +6,7 @@ import type { KonvaEventObject } from "konva/lib/Node";
import { Polygon, PolygonType } from "@/types/canvas";
import { useApiHost } from "@/api";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { snapPointToLines } from "@/utils/canvasUtil";
type PolygonCanvasProps = {
containerRef: RefObject<HTMLDivElement>;
@@ -18,6 +19,7 @@ type PolygonCanvasProps = {
hoveredPolygonIndex: number | null;
selectedZoneMask: PolygonType[] | undefined;
activeLine?: number;
snapPoints: boolean;
};
export function PolygonCanvas({
@@ -31,6 +33,7 @@ export function PolygonCanvas({
hoveredPolygonIndex,
selectedZoneMask,
activeLine,
snapPoints,
}: PolygonCanvasProps) {
const [isLoaded, setIsLoaded] = useState(false);
const [image, setImage] = useState<HTMLImageElement | undefined>();
@@ -156,9 +159,23 @@ export function PolygonCanvas({
intersection?.getClassName() !== "Circle") ||
(activePolygon.isFinished && intersection?.name() == "unfilled-line")
) {
let newPoint = [mousePos.x, mousePos.y];
if (snapPoints) {
// Snap to other polygons' edges
const otherPolygons = polygons.filter(
(_, i) => i !== activePolygonIndex,
);
const snappedPos = snapPointToLines(newPoint, otherPolygons, 10);
if (snappedPos) {
newPoint = snappedPos;
}
}
const { updatedPoints, updatedPointsOrder } = addPointToPolygon(
activePolygon,
[mousePos.x, mousePos.y],
newPoint,
);
updatedPolygons[activePolygonIndex] = {
@@ -184,11 +201,24 @@ export function PolygonCanvas({
if (stage) {
// we add an unfilled line for adding points when finished
const index = e.target.index - (activePolygon.isFinished ? 2 : 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();
let pos = [e.target._lastPos!.x, e.target._lastPos!.y];
if (snapPoints) {
// Snap to other polygons' edges
const otherPolygons = polygons.filter(
(_, i) => i !== activePolygonIndex,
);
const snappedPos = snapPointToLines(pos, otherPolygons, 10); // 10 is the snap threshold
if (snappedPos) {
pos = snappedPos;
}
}
// Constrain to stage boundaries
pos[0] = Math.max(0, Math.min(pos[0], stage.width()));
pos[1] = Math.max(0, Math.min(pos[1], stage.height()));
updatedPolygons[activePolygonIndex] = {
...activePolygon,
points: [
@@ -291,6 +321,16 @@ export function PolygonCanvas({
handlePointDragMove={handlePointDragMove}
handleGroupDragEnd={handleGroupDragEnd}
activeLine={activeLine}
snapPoints={snapPoints}
snapToLines={(point) =>
snapPoints
? snapPointToLines(
point,
polygons.filter((_, i) => i !== index),
10,
)
: null
}
/>
),
)}
@@ -310,6 +350,16 @@ export function PolygonCanvas({
handlePointDragMove={handlePointDragMove}
handleGroupDragEnd={handleGroupDragEnd}
activeLine={activeLine}
snapPoints={snapPoints}
snapToLines={(point) =>
snapPoints
? snapPointToLines(
point,
polygons.filter((_, i) => i !== activePolygonIndex),
10,
)
: null
}
/>
)}
</Layer>

View File

@@ -28,6 +28,8 @@ type PolygonDrawerProps = {
handlePointDragMove: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
activeLine?: number;
snapToLines: (point: number[]) => number[] | null;
snapPoints: boolean;
};
export default function PolygonDrawer({
@@ -41,6 +43,8 @@ export default function PolygonDrawer({
handlePointDragMove,
handleGroupDragEnd,
activeLine,
snapToLines,
snapPoints,
}: PolygonDrawerProps) {
const vertexRadius = 6;
const flattenedPoints = useMemo(() => flattenPoints(points), [points]);
@@ -218,15 +222,32 @@ export default function PolygonDrawer({
onMouseOver={handleMouseOverPoint}
onMouseOut={handleMouseOutPoint}
draggable={isActive}
onDragMove={isActive ? handlePointDragMove : undefined}
onDragMove={(e) => {
if (isActive) {
if (snapPoints) {
const snappedPos = snapToLines([e.target.x(), e.target.y()]);
if (snappedPos) {
e.target.position({ x: snappedPos[0], y: snappedPos[1] });
}
}
handlePointDragMove(e);
}
}}
dragBoundFunc={(pos) => {
if (stageRef.current) {
return dragBoundFunc(
const boundPos = dragBoundFunc(
stageRef.current.width(),
stageRef.current.height(),
vertexRadius,
pos,
);
if (snapPoints) {
const snappedPos = snapToLines([boundPos.x, boundPos.y]);
return snappedPos
? { x: snappedPos[0], y: snappedPos[1] }
: boundPos;
}
return boundPos;
} else {
return pos;
}

View File

@@ -2,17 +2,23 @@ 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 { TbPolygon, TbPolygonOff } from "react-icons/tb";
import { cn } from "@/lib/utils";
type PolygonEditControlsProps = {
polygons: Polygon[];
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
activePolygonIndex: number | undefined;
snapPoints: boolean;
setSnapPoints: React.Dispatch<React.SetStateAction<boolean>>;
};
export default function PolygonEditControls({
polygons,
setPolygons,
activePolygonIndex,
snapPoints,
setSnapPoints,
}: PolygonEditControlsProps) {
const undo = () => {
if (activePolygonIndex === undefined || !polygons) {
@@ -97,6 +103,25 @@ export default function PolygonEditControls({
</TooltipTrigger>
<TooltipContent>Reset</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={snapPoints ? "select" : "default"}
className={cn("size-6 rounded-md p-1")}
aria-label="Snap points"
onClick={() => setSnapPoints((prev) => !prev)}
>
{snapPoints ? (
<TbPolygon className="text-primary" />
) : (
<TbPolygonOff className="text-secondary-foreground" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
{snapPoints ? "Don't snap points" : "Snap points"}
</TooltipContent>
</Tooltip>
</div>
);
}

View File

@@ -41,6 +41,8 @@ type ZoneEditPaneProps = {
onSave?: () => void;
onCancel?: () => void;
setActiveLine: React.Dispatch<React.SetStateAction<number | undefined>>;
snapPoints: boolean;
setSnapPoints: React.Dispatch<React.SetStateAction<boolean>>;
};
export default function ZoneEditPane({
@@ -54,6 +56,8 @@ export default function ZoneEditPane({
onSave,
onCancel,
setActiveLine,
snapPoints,
setSnapPoints,
}: ZoneEditPaneProps) {
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
@@ -483,6 +487,8 @@ export default function ZoneEditPane({
polygons={polygons}
setPolygons={setPolygons}
activePolygonIndex={activePolygonIndex}
snapPoints={snapPoints}
setSnapPoints={setSnapPoints}
/>
</div>
)}