Zone/mask editor improvements (#11236)

* add points to completed polygons in zone/mask editor

* change line order so edges are more easily clickable
This commit is contained in:
Josh Hawkins 2024-05-04 09:37:35 -05:00 committed by GitHub
parent f0054ceba4
commit 51dcdd6f4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 95 additions and 175 deletions

View File

@ -629,6 +629,7 @@ export default function MasksAndZones({
scaledHeight && scaledHeight &&
editingPolygons ? ( editingPolygons ? (
<PolygonCanvas <PolygonCanvas
containerRef={containerRef}
camera={cameraConfig.name} camera={cameraConfig.name}
width={scaledWidth} width={scaledWidth}
height={scaledHeight} height={scaledHeight}

View File

@ -1,4 +1,4 @@
import React, { useMemo, useRef, useState, useEffect } from "react"; import React, { useMemo, useRef, useState, useEffect, RefObject } from "react";
import PolygonDrawer from "./PolygonDrawer"; import PolygonDrawer from "./PolygonDrawer";
import { Stage, Layer, Image } from "react-konva"; import { Stage, Layer, Image } from "react-konva";
import Konva from "konva"; import Konva from "konva";
@ -7,6 +7,7 @@ import { Polygon, PolygonType } from "@/types/canvas";
import { useApiHost } from "@/api"; import { useApiHost } from "@/api";
type PolygonCanvasProps = { type PolygonCanvasProps = {
containerRef: RefObject<HTMLDivElement>;
camera: string; camera: string;
width: number; width: number;
height: number; height: number;
@ -18,6 +19,7 @@ type PolygonCanvasProps = {
}; };
export function PolygonCanvas({ export function PolygonCanvas({
containerRef,
camera, camera,
width, width,
height, height,
@ -55,10 +57,6 @@ export function PolygonCanvas({
}; };
}, [videoElement]); }, [videoElement]);
const getMousePos = (stage: Konva.Stage) => {
return [stage.getPointerPosition()!.x, stage.getPointerPosition()!.y];
};
const addPointToPolygon = (polygon: Polygon, newPoint: number[]) => { const addPointToPolygon = (polygon: Polygon, newPoint: number[]) => {
const points = polygon.points; const points = polygon.points;
const pointsOrder = polygon.pointsOrder; const pointsOrder = polygon.pointsOrder;
@ -99,37 +97,6 @@ export function PolygonCanvas({
return { updatedPoints, updatedPointsOrder }; return { updatedPoints, updatedPointsOrder };
}; };
const isMouseOverFirstPoint = (polygon: Polygon, mousePos: number[]) => {
if (!polygon || !polygon.points || polygon.points.length < 1) {
return false;
}
const [firstPoint] = polygon.points;
const distance = Math.hypot(
mousePos[0] - firstPoint[0],
mousePos[1] - firstPoint[1],
);
return distance < 10;
};
const isMouseOverAnyPoint = (polygon: Polygon, mousePos: number[]) => {
if (!polygon || !polygon.points || polygon.points.length === 0) {
return false;
}
for (let i = 1; i < polygon.points.length; i++) {
const point = polygon.points[i];
const distance = Math.hypot(
mousePos[0] - point[0],
mousePos[1] - point[1],
);
if (distance < 10) {
return true;
}
}
return false;
};
const handleMouseDown = (e: KonvaEventObject<MouseEvent | TouchEvent>) => { const handleMouseDown = (e: KonvaEventObject<MouseEvent | TouchEvent>) => {
if (activePolygonIndex === undefined || !polygons) { if (activePolygonIndex === undefined || !polygons) {
return; return;
@ -138,11 +105,13 @@ export function PolygonCanvas({
const updatedPolygons = [...polygons]; const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex]; const activePolygon = updatedPolygons[activePolygonIndex];
const stage = e.target.getStage()!; const stage = e.target.getStage()!;
const mousePos = getMousePos(stage); const mousePos = stage.getPointerPosition() ?? { x: 0, y: 0 };
const intersection = stage.getIntersection(mousePos);
if ( if (
activePolygon.points.length >= 3 && activePolygon.points.length >= 3 &&
isMouseOverFirstPoint(activePolygon, mousePos) intersection?.getClassName() == "Circle" &&
intersection?.name() == "point-0"
) { ) {
// Close the polygon // Close the polygon
updatedPolygons[activePolygonIndex] = { updatedPolygons[activePolygonIndex] = {
@ -152,12 +121,13 @@ export function PolygonCanvas({
setPolygons(updatedPolygons); setPolygons(updatedPolygons);
} else { } else {
if ( if (
!activePolygon.isFinished && (!activePolygon.isFinished &&
!isMouseOverAnyPoint(activePolygon, mousePos) intersection?.getClassName() !== "Circle") ||
(activePolygon.isFinished && intersection?.name() == "unfilled-line")
) { ) {
const { updatedPoints, updatedPointsOrder } = addPointToPolygon( const { updatedPoints, updatedPointsOrder } = addPointToPolygon(
activePolygon, activePolygon,
mousePos, [mousePos.x, mousePos.y],
); );
updatedPolygons[activePolygonIndex] = { updatedPolygons[activePolygonIndex] = {
@ -168,62 +138,6 @@ export function PolygonCanvas({
setPolygons(updatedPolygons); setPolygons(updatedPolygons);
} }
} }
// }
};
const handleMouseOverStartPoint = (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => {
if (activePolygonIndex === undefined || !polygons) {
return;
}
const activePolygon = polygons[activePolygonIndex];
if (!activePolygon.isFinished && activePolygon.points.length >= 3) {
e.target.getStage()!.container().style.cursor = "default";
e.currentTarget.scale({ x: 2, y: 2 });
}
};
const handleMouseOutStartPoint = (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => {
e.currentTarget.scale({ x: 1, y: 1 });
if (activePolygonIndex === undefined || !polygons) {
return;
}
const activePolygon = polygons[activePolygonIndex];
if (
(!activePolygon.isFinished && activePolygon.points.length >= 3) ||
activePolygon.isFinished
) {
e.currentTarget.scale({ x: 1, y: 1 });
}
};
const handleMouseOverAnyPoint = (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => {
if (!polygons) {
return;
}
e.target.getStage()!.container().style.cursor = "move";
};
const handleMouseOutAnyPoint = (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => {
if (activePolygonIndex === undefined || !polygons) {
return;
}
const activePolygon = polygons[activePolygonIndex];
if (activePolygon.isFinished) {
e.target.getStage()!.container().style.cursor = "default";
} else {
e.target.getStage()!.container().style.cursor = "crosshair";
}
}; };
const handlePointDragMove = ( const handlePointDragMove = (
@ -237,7 +151,8 @@ export function PolygonCanvas({
const activePolygon = updatedPolygons[activePolygonIndex]; const activePolygon = updatedPolygons[activePolygonIndex];
const stage = e.target.getStage(); const stage = e.target.getStage();
if (stage) { if (stage) {
const index = e.target.index - 1; // 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]; const pos = [e.target._lastPos!.x, e.target._lastPos!.y];
if (pos[0] < 0) pos[0] = 0; if (pos[0] < 0) pos[0] = 0;
if (pos[1] < 0) pos[1] = 0; if (pos[1] < 0) pos[1] = 0;
@ -272,26 +187,17 @@ export function PolygonCanvas({
} }
}; };
const handleStageMouseOver = ( const handleStageMouseOver = () => {
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
) => {
if (activePolygonIndex === undefined || !polygons) { if (activePolygonIndex === undefined || !polygons) {
return; return;
} }
const updatedPolygons = [...polygons]; const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex]; const activePolygon = updatedPolygons[activePolygonIndex];
const stage = e.target.getStage()!;
const mousePos = getMousePos(stage);
if ( if (containerRef.current && !activePolygon.isFinished) {
activePolygon.isFinished || containerRef.current.style.cursor = "crosshair";
isMouseOverAnyPoint(activePolygon, mousePos) || }
isMouseOverFirstPoint(activePolygon, mousePos)
)
return;
e.target.getStage()!.container().style.cursor = "crosshair";
}; };
useEffect(() => { useEffect(() => {
@ -336,6 +242,7 @@ export function PolygonCanvas({
selectedZoneMask.includes(polygon.type)) && selectedZoneMask.includes(polygon.type)) &&
index !== activePolygonIndex && ( index !== activePolygonIndex && (
<PolygonDrawer <PolygonDrawer
stageRef={stageRef}
key={index} key={index}
points={polygon.points} points={polygon.points}
isActive={index === activePolygonIndex} isActive={index === activePolygonIndex}
@ -344,10 +251,6 @@ export function PolygonCanvas({
color={polygon.color} color={polygon.color}
handlePointDragMove={handlePointDragMove} handlePointDragMove={handlePointDragMove}
handleGroupDragEnd={handleGroupDragEnd} handleGroupDragEnd={handleGroupDragEnd}
handleMouseOverStartPoint={handleMouseOverStartPoint}
handleMouseOutStartPoint={handleMouseOutStartPoint}
handleMouseOverAnyPoint={handleMouseOverAnyPoint}
handleMouseOutAnyPoint={handleMouseOutAnyPoint}
/> />
), ),
)} )}
@ -356,6 +259,7 @@ export function PolygonCanvas({
(selectedZoneMask === undefined || (selectedZoneMask === undefined ||
selectedZoneMask.includes(polygons[activePolygonIndex].type)) && ( selectedZoneMask.includes(polygons[activePolygonIndex].type)) && (
<PolygonDrawer <PolygonDrawer
stageRef={stageRef}
key={activePolygonIndex} key={activePolygonIndex}
points={polygons[activePolygonIndex].points} points={polygons[activePolygonIndex].points}
isActive={true} isActive={true}
@ -364,10 +268,6 @@ export function PolygonCanvas({
color={polygons[activePolygonIndex].color} color={polygons[activePolygonIndex].color}
handlePointDragMove={handlePointDragMove} handlePointDragMove={handlePointDragMove}
handleGroupDragEnd={handleGroupDragEnd} handleGroupDragEnd={handleGroupDragEnd}
handleMouseOverStartPoint={handleMouseOverStartPoint}
handleMouseOutStartPoint={handleMouseOutStartPoint}
handleMouseOverAnyPoint={handleMouseOverAnyPoint}
handleMouseOutAnyPoint={handleMouseOutAnyPoint}
/> />
)} )}
</Layer> </Layer>

View File

@ -1,4 +1,11 @@
import { useCallback, useMemo, useRef, useState } from "react"; import {
RefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Line, Circle, Group } from "react-konva"; import { Line, Circle, Group } from "react-konva";
import { import {
minMax, minMax,
@ -9,9 +16,9 @@ import {
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";
import { isMobileOnly } from "react-device-detect";
type PolygonDrawerProps = { type PolygonDrawerProps = {
stageRef: RefObject<Konva.Stage>;
points: number[][]; points: number[][];
isActive: boolean; isActive: boolean;
isHovered: boolean; isHovered: boolean;
@ -19,21 +26,10 @@ type PolygonDrawerProps = {
color: number[]; color: number[];
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: (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => void;
handleMouseOutStartPoint: (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => void;
handleMouseOverAnyPoint: (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => void;
handleMouseOutAnyPoint: (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => void;
}; };
export default function PolygonDrawer({ export default function PolygonDrawer({
stageRef,
points, points,
isActive, isActive,
isHovered, isHovered,
@ -41,31 +37,41 @@ export default function PolygonDrawer({
color, color,
handlePointDragMove, handlePointDragMove,
handleGroupDragEnd, handleGroupDragEnd,
handleMouseOverStartPoint,
handleMouseOutStartPoint,
handleMouseOverAnyPoint,
handleMouseOutAnyPoint,
}: PolygonDrawerProps) { }: PolygonDrawerProps) {
const vertexRadius = isMobileOnly ? 12 : 6; const vertexRadius = 6;
const flattenedPoints = useMemo(() => flattenPoints(points), [points]); const flattenedPoints = useMemo(() => flattenPoints(points), [points]);
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 groupRef = useRef<Konva.Group>(null);
const [cursor, setCursor] = useState("default");
const handleGroupMouseOver = ( const handleMouseOverPoint = (
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>, e: KonvaEventObject<MouseEvent | TouchEvent>,
) => { ) => {
if (!isFinished) return; if (!e.target) return;
e.target.getStage()!.container().style.cursor = "move";
setStage(e.target.getStage()!); if (!isFinished && points.length >= 3 && e.target.name() === "point-0") {
e.target.scale({ x: 2, y: 2 });
setCursor("crosshair");
} else {
setCursor("move");
}
}; };
const handleGroupMouseOut = ( const handleMouseOutPoint = (
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>, e: KonvaEventObject<MouseEvent | TouchEvent>,
) => { ) => {
if (!e.target || !isFinished) return; if (!e.target) return;
e.target.getStage()!.container().style.cursor = "default";
if (isFinished) {
setCursor("default");
} else {
setCursor("crosshair");
}
if (e.target.name() === "point-0") {
e.target.scale({ x: 1, y: 1 });
}
}; };
const handleGroupDragStart = () => { const handleGroupDragStart = () => {
@ -76,13 +82,13 @@ export default function PolygonDrawer({
}; };
const groupDragBound = (pos: Vector2d) => { const groupDragBound = (pos: Vector2d) => {
if (!stage) { if (!stageRef.current) {
return pos; return pos;
} }
let { x, y } = pos; let { x, y } = pos;
const sw = stage.width(); const sw = stageRef.current.width();
const sh = stage.height(); const sh = stageRef.current.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];
@ -99,6 +105,14 @@ export default function PolygonDrawer({
[color], [color],
); );
useEffect(() => {
if (!stageRef.current) {
return;
}
stageRef.current.container().style.cursor = cursor;
}, [stageRef, cursor]);
return ( return (
<Group <Group
name="polygon" name="polygon"
@ -107,55 +121,62 @@ export default function PolygonDrawer({
onDragStart={isActive ? handleGroupDragStart : undefined} onDragStart={isActive ? handleGroupDragStart : undefined}
onDragEnd={isActive ? handleGroupDragEnd : undefined} onDragEnd={isActive ? handleGroupDragEnd : undefined}
dragBoundFunc={isActive ? groupDragBound : undefined} dragBoundFunc={isActive ? groupDragBound : undefined}
onMouseOver={isActive ? handleGroupMouseOver : undefined}
onTouchStart={isActive ? handleGroupMouseOver : undefined}
onMouseOut={isActive ? handleGroupMouseOut : undefined}
> >
<Line <Line
name="filled-line"
points={flattenedPoints} points={flattenedPoints}
stroke={colorString(true)} stroke={colorString(true)}
strokeWidth={3} strokeWidth={3}
hitStrokeWidth={12}
closed={isFinished} closed={isFinished}
fill={colorString(isActive || isHovered ? true : false)} fill={colorString(isActive || isHovered ? true : false)}
onMouseOver={() =>
isFinished ? setCursor("move") : setCursor("crosshair")
}
onMouseOut={() =>
isFinished ? setCursor("default") : setCursor("crosshair")
}
/> />
{isFinished && isActive && (
<Line
name="unfilled-line"
points={flattenedPoints}
hitStrokeWidth={12}
closed={isFinished}
fillEnabled={false}
onMouseOver={() => setCursor("crosshair")}
onMouseOut={() =>
isFinished ? setCursor("default") : setCursor("crosshair")
}
/>
)}
{points.map((point, index) => { {points.map((point, index) => {
if (!isActive) { if (!isActive) {
return; return;
} }
const x = point[0]; const x = point[0];
const y = point[1]; const y = point[1];
const startPointAttr =
index === 0
? {
hitStrokeWidth: 12,
onMouseOver: handleMouseOverStartPoint,
onMouseOut: handleMouseOutStartPoint,
}
: null;
const otherPointsAttr =
index !== 0
? {
onMouseOver: handleMouseOverAnyPoint,
onMouseOut: handleMouseOutAnyPoint,
}
: null;
return ( return (
<Circle <Circle
key={index} key={index}
name={`point-${index}`}
x={x} x={x}
y={y} y={y}
radius={vertexRadius} radius={vertexRadius}
stroke={colorString(true)} stroke={colorString(true)}
fill="#ffffff" fill="#ffffff"
strokeWidth={3} strokeWidth={3}
hitStrokeWidth={index === 0 ? 12 : 9}
onMouseOver={handleMouseOverPoint}
onMouseOut={handleMouseOutPoint}
draggable={isActive} draggable={isActive}
onDragMove={isActive ? handlePointDragMove : undefined} onDragMove={isActive ? handlePointDragMove : undefined}
dragBoundFunc={(pos) => { dragBoundFunc={(pos) => {
if (stage) { if (stageRef.current) {
return dragBoundFunc( return dragBoundFunc(
stage.width(), stageRef.current.width(),
stage.height(), stageRef.current.height(),
vertexRadius, vertexRadius,
pos, pos,
); );
@ -163,8 +184,6 @@ export default function PolygonDrawer({
return pos; return pos;
} }
}} }}
{...startPointAttr}
{...otherPointsAttr}
/> />
); );
})} })}

View File

@ -41,7 +41,7 @@ export default function PolygonEditControls({
...activePolygon.pointsOrder.slice(0, lastPointOrderIndex), ...activePolygon.pointsOrder.slice(0, lastPointOrderIndex),
...activePolygon.pointsOrder.slice(lastPointOrderIndex + 1), ...activePolygon.pointsOrder.slice(lastPointOrderIndex + 1),
], ],
isFinished: false, isFinished: activePolygon.isFinished && activePolygon.points.length > 3,
}; };
setPolygons(updatedPolygons); setPolygons(updatedPolygons);