Merge branch 'fe/feat/136-canvas-draw-polygon' into 'fe/develop'

Feat: 캔버스 폴리곤 그리기 기능 구현

See merge request s11-s-project/S11P21S002!50
This commit is contained in:
정현조 2024-09-05 12:59:21 +09:00
commit 635db2c7d5

View File

@ -1,7 +1,7 @@
import useCanvasStore from '@/stores/useCanvasStore'; import useCanvasStore from '@/stores/useCanvasStore';
import Konva from 'konva'; import Konva from 'konva';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Image, Layer, Rect, Stage } from 'react-konva'; import { Circle, Image, Layer, Line, Rect, Stage } from 'react-konva';
import useImage from 'use-image'; import useImage from 'use-image';
import { Label } from '@/types'; import { Label } from '@/types';
import LabelRect from './LabelRect'; import LabelRect from './LabelRect';
@ -47,7 +47,9 @@ export default function ImageCanvas() {
const [selectedId, setSelectedId] = useState<number | null>(null); const [selectedId, setSelectedId] = useState<number | null>(null);
const [image, imageStatus] = useImage(imageUrl); const [image, imageStatus] = useImage(imageUrl);
const [rectPoints, setRectPoints] = useState<[number, number][]>([]); const [rectPoints, setRectPoints] = useState<[number, number][]>([]);
const [polygonPoints, setPolygonPoints] = useState<[number, number][]>([]);
const drawState = useCanvasStore((state) => state.drawState); const drawState = useCanvasStore((state) => state.drawState);
const setDrawState = useCanvasStore((state) => state.setDrawState);
const addLabel = useCanvasStore((state) => state.addLabel); const addLabel = useCanvasStore((state) => state.addLabel);
const startDrawRect = () => { const startDrawRect = () => {
const { x, y } = stageRef.current!.getRelativePointerPosition()!; const { x, y } = stageRef.current!.getRelativePointerPosition()!;
@ -56,6 +58,59 @@ export default function ImageCanvas() {
[x, y], [x, y],
]); ]);
}; };
const addPointToPolygon = () => {
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
if (polygonPoints.length === 0) {
setPolygonPoints([
[x, y],
[x, y],
]);
return;
}
const diff = Math.max(Math.abs(x - polygonPoints[0][0]), Math.abs(y - polygonPoints[0][1]));
if (diff === 0) return;
const scale = stageRef.current!.getAbsoluteScale().x;
const clickedFirstPoint = polygonPoints.length > 1 && diff * scale < 5;
if (clickedFirstPoint) {
endDrawPolygon();
return;
}
setPolygonPoints([...polygonPoints, [x, y]]);
};
const removeLastPointOfPolygon = (e: MouseEvent) => {
e.preventDefault();
if (polygonPoints.length === 0) return;
setPolygonPoints(polygonPoints.slice(0, -1));
};
const moveLastPointOfPolygon = () => {
if (polygonPoints.length < 2) return;
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
setPolygonPoints([...polygonPoints.slice(0, -1), [x, y]]);
};
const endDrawPolygon = () => {
if (drawState !== 'pen' || polygonPoints.length === 0) return;
setDrawState('pointer');
setPolygonPoints([]);
if (polygonPoints.length < 4) return;
const color = Math.floor(Math.random() * 65535)
.toString(16)
.padStart(6, '0');
const id = labels.length;
addLabel({
id: id,
name: 'label',
type: 'polygon',
color: `#${color}`,
coordinates: polygonPoints.slice(0, -1),
});
setDrawState('pointer');
setSelectedId(id);
};
const updateDrawingRect = () => { const updateDrawingRect = () => {
if (rectPoints.length === 0) return; if (rectPoints.length === 0) return;
@ -71,23 +126,32 @@ export default function ImageCanvas() {
setRectPoints([]); setRectPoints([]);
const color = Math.floor(Math.random() * 65535) const color = Math.floor(Math.random() * 65535)
.toString(16) .toString(16)
.padStart(4, '0'); .padStart(6, '0');
const id = labels.length;
addLabel({ addLabel({
id: labels.length, id: id,
name: 'label', name: 'label',
type: 'rect', type: 'rect',
color: `#${color}ff`, color: `#${color}`,
coordinates: rectPoints, coordinates: rectPoints,
}); });
setDrawState('pointer');
setSelectedId(id);
}; };
const handleClick = (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => { const handleClick = (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => {
const isLeftMouseClicked = e.evt.type === 'mousedown' && (e.evt as MouseEvent).button === 0; e.evt.preventDefault();
e.evt.stopPropagation();
const isLeftClicked = e.evt.type === 'mousedown' && (e.evt as MouseEvent).button === 0;
const isRightClicked = e.evt.type === 'mousedown' && (e.evt as MouseEvent).button === 2;
if (drawState !== 'pointer' && isLeftMouseClicked) { if (drawState !== 'pointer' && (isLeftClicked || isRightClicked)) {
stageRef.current?.stopDrag(); stageRef.current?.stopDrag();
if (drawState === 'rect') { if (drawState === 'rect') {
startDrawRect(); startDrawRect();
} }
if (drawState === 'pen') {
isRightClicked ? removeLastPointOfPolygon(e.evt as MouseEvent) : addPointToPolygon();
}
return; return;
} }
if (e.target === e.target.getStage() || e.target.getClassName() === 'Image') { if (e.target === e.target.getStage() || e.target.getClassName() === 'Image') {
@ -98,6 +162,9 @@ export default function ImageCanvas() {
if (drawState === 'rect' && rectPoints.length) { if (drawState === 'rect' && rectPoints.length) {
updateDrawingRect(); updateDrawingRect();
} }
if (drawState === 'pen' && polygonPoints.length) {
moveLastPointOfPolygon();
}
}; };
const handleZoom = (e: Konva.KonvaEventObject<WheelEvent>) => { const handleZoom = (e: Konva.KonvaEventObject<WheelEvent>) => {
const scaleBy = 1.05; const scaleBy = 1.05;
@ -172,6 +239,7 @@ export default function ImageCanvas() {
onMouseUp={endDrawRect} onMouseUp={endDrawRect}
onTouchEnd={endDrawRect} onTouchEnd={endDrawRect}
scale={getScale()} scale={getScale()}
onContextMenu={(e) => e.evt.preventDefault()}
> >
<Layer> <Layer>
<Image image={image} /> <Image image={image} />
@ -203,15 +271,47 @@ export default function ImageCanvas() {
y={rectPoints[0][1]} y={rectPoints[0][1]}
width={rectPoints[1][0] - rectPoints[0][0]} width={rectPoints[1][0] - rectPoints[0][0]}
height={rectPoints[1][1] - rectPoints[0][1]} height={rectPoints[1][1] - rectPoints[0][1]}
stroke={'#ff0000'} stroke={'#00a1ff'}
strokeWidth={1} strokeWidth={1}
strokeScaleEnabled={false} strokeScaleEnabled={false}
fillAfterStrokeEnabled={false} fillAfterStrokeEnabled={false}
fill="#ff000033" fill="#00a1ff33"
shadowForStrokeEnabled={false} shadowForStrokeEnabled={false}
listening={false} listening={false}
/> />
) : null} ) : null}
{polygonPoints.length ? (
<>
<Line
points={polygonPoints.flat()}
stroke={'#00a1ff'}
strokeWidth={1}
strokeScaleEnabled={false}
fill="#00a1ff33"
closed
listening={false}
/>
{polygonPoints.map((point, index) => (
<Circle
key={index}
x={point[0]}
y={point[1]}
radius={5}
stroke="#00a1ff"
strokeWidth={1}
fill="white"
strokeScaleEnabled={false}
listening={false}
scale={{
x: 1 / stageRef.current!.getAbsoluteScale().x,
y: 1 / stageRef.current!.getAbsoluteScale().y,
}}
perfectDrawEnabled={false}
shadowForStrokeEnabled={false}
/>
))}
</>
) : null}
</Layer> </Layer>
<Layer ref={dragLayerRef} /> <Layer ref={dragLayerRef} />