Feat: 캔버스 직사각형 렌더링 추가 - S11P21S002-103
This commit is contained in:
parent
ee85f6fdb2
commit
f7cc913e14
104
frontend/package-lock.json
generated
104
frontend/package-lock.json
generated
@ -18,14 +18,17 @@
|
|||||||
"axios": "^1.7.5",
|
"axios": "^1.7.5",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"konva": "^9.3.14",
|
||||||
"lucide-react": "^0.436.0",
|
"lucide-react": "^0.436.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
|
"react-konva": "^18.2.10",
|
||||||
"react-resizable-panels": "^2.1.1",
|
"react-resizable-panels": "^2.1.1",
|
||||||
"react-router-dom": "^6.26.1",
|
"react-router-dom": "^6.26.1",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"use-image": "^1.1.1",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zustand": "^4.5.5"
|
"zustand": "^4.5.5"
|
||||||
},
|
},
|
||||||
@ -5590,7 +5593,6 @@
|
|||||||
"version": "15.7.12",
|
"version": "15.7.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
||||||
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
|
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
@ -5609,7 +5611,6 @@
|
|||||||
"version": "18.3.4",
|
"version": "18.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz",
|
||||||
"integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==",
|
"integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
@ -5626,6 +5627,15 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-reconciler": {
|
||||||
|
"version": "0.28.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.8.tgz",
|
||||||
|
"integrity": "sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.6",
|
"version": "1.20.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
|
||||||
@ -6999,7 +7009,6 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
@ -8924,6 +8933,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/its-fine": {
|
||||||
|
"version": "1.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
|
||||||
|
"integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react-reconciler": "^0.28.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jackspeak": {
|
"node_modules/jackspeak": {
|
||||||
"version": "3.4.3",
|
"version": "3.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||||
@ -9098,6 +9119,26 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/konva": {
|
||||||
|
"version": "9.3.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/konva/-/konva-9.3.14.tgz",
|
||||||
|
"integrity": "sha512-Gmm5lyikGYJyogKQA7Fy6dKkfNh350V6DwfZkid0RVrGYP2cfCsxuMxgF5etKeCv7NjXYpJxKqi1dYkIkX/dcA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/lavrton"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/konva"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/lavrton"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/leven": {
|
"node_modules/leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||||
@ -10642,6 +10683,53 @@
|
|||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/react-konva": {
|
||||||
|
"version": "18.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.10.tgz",
|
||||||
|
"integrity": "sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/lavrton"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/konva"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/lavrton"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react-reconciler": "^0.28.2",
|
||||||
|
"its-fine": "^1.1.1",
|
||||||
|
"react-reconciler": "~0.29.0",
|
||||||
|
"scheduler": "^0.23.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"konva": "^8.0.1 || ^7.2.5 || ^9.0.0",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"react-dom": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-reconciler": {
|
||||||
|
"version": "0.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz",
|
||||||
|
"integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"scheduler": "^0.23.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-remove-scroll": {
|
"node_modules/react-remove-scroll": {
|
||||||
"version": "2.5.7",
|
"version": "2.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz",
|
||||||
@ -12479,6 +12567,16 @@
|
|||||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/use-image": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-image/-/use-image-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n4YO2k8AJG/BcDtxmBx8Aa+47kxY5m335dJiCQA5tTeVU4XdhrhqR6wT0WISRXwdMEOv5CSjqekDZkEMiiWaYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/use-sidecar": {
|
"node_modules/use-sidecar": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
||||||
|
@ -24,14 +24,17 @@
|
|||||||
"axios": "^1.7.5",
|
"axios": "^1.7.5",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"konva": "^9.3.14",
|
||||||
"lucide-react": "^0.436.0",
|
"lucide-react": "^0.436.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
|
"react-konva": "^18.2.10",
|
||||||
"react-resizable-panels": "^2.1.1",
|
"react-resizable-panels": "^2.1.1",
|
||||||
"react-router-dom": "^6.26.1",
|
"react-router-dom": "^6.26.1",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"use-image": "^1.1.1",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zustand": "^4.5.5"
|
"zustand": "^4.5.5"
|
||||||
},
|
},
|
||||||
|
BIN
frontend/public/sample.jpg
Normal file
BIN
frontend/public/sample.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 918 KiB |
40
frontend/src/components/ImageCanvas/LabelRect.tsx
Normal file
40
frontend/src/components/ImageCanvas/LabelRect.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Label } from '@/types';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { Line } from 'react-konva';
|
||||||
|
|
||||||
|
export default function LabelRect({
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
info,
|
||||||
|
}: {
|
||||||
|
isSelected: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
|
info: Label;
|
||||||
|
}) {
|
||||||
|
const rectRef = useRef<Konva.Line>(null);
|
||||||
|
const trRef = useRef<Konva.Transformer>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSelected) {
|
||||||
|
trRef.current?.nodes([rectRef.current as Konva.Node]);
|
||||||
|
trRef.current?.getLayer()?.batchDraw();
|
||||||
|
}
|
||||||
|
}, [isSelected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Line
|
||||||
|
points={info.coordinates.flat()}
|
||||||
|
stroke={info.color}
|
||||||
|
strokeWidth={1}
|
||||||
|
ref={rectRef}
|
||||||
|
onMouseDown={onSelect}
|
||||||
|
onTouchStart={onSelect}
|
||||||
|
strokeScaleEnabled={false}
|
||||||
|
fillAfterStrokeEnabled={false}
|
||||||
|
fill={`${info.color}33`}
|
||||||
|
closed
|
||||||
|
draggable
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
14
frontend/src/components/ImageCanvas/index.stories.tsx
Normal file
14
frontend/src/components/ImageCanvas/index.stories.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Meta } from '@storybook/react';
|
||||||
|
import ImageCanvas from '.';
|
||||||
|
|
||||||
|
const meta: Meta<typeof ImageCanvas> = {
|
||||||
|
title: 'Components/ImageCanvas',
|
||||||
|
component: ImageCanvas,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
export const Default = () => <ImageCanvas />;
|
145
frontend/src/components/ImageCanvas/index.tsx
Normal file
145
frontend/src/components/ImageCanvas/index.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import useCanvasStore from '@/stores/useCanvasStore';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Image, Layer, Stage } from 'react-konva';
|
||||||
|
import useImage from 'use-image';
|
||||||
|
import { Label } from '@/types';
|
||||||
|
import LabelRect from './LabelRect';
|
||||||
|
import { Vector2d } from 'konva/lib/types';
|
||||||
|
|
||||||
|
const mockLabels: Label[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Label 1',
|
||||||
|
color: '#FFaa33',
|
||||||
|
type: 'rect',
|
||||||
|
coordinates: [
|
||||||
|
[100, 100],
|
||||||
|
[200, 100],
|
||||||
|
[200, 200],
|
||||||
|
[100, 200],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Label 3',
|
||||||
|
color: '#aa33ff',
|
||||||
|
type: 'rect',
|
||||||
|
coordinates: [
|
||||||
|
[1200, 200],
|
||||||
|
[1200, 400],
|
||||||
|
[1400, 400],
|
||||||
|
[1400, 200],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Label 3',
|
||||||
|
color: '#aaff33',
|
||||||
|
type: 'polygon',
|
||||||
|
// star shape
|
||||||
|
coordinates: [
|
||||||
|
[500, 375],
|
||||||
|
[523, 232],
|
||||||
|
[600, 232],
|
||||||
|
[535, 175],
|
||||||
|
[560, 100],
|
||||||
|
[500, 150],
|
||||||
|
[440, 100],
|
||||||
|
[465, 175],
|
||||||
|
[400, 232],
|
||||||
|
[477, 232],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ImageCanvas() {
|
||||||
|
const stageWidth = window.innerWidth;
|
||||||
|
const stageHeight = window.innerHeight;
|
||||||
|
const stageRef = useRef<Konva.Stage>(null);
|
||||||
|
const scale = useRef<number>(0);
|
||||||
|
const imageUrl = '/sample.jpg';
|
||||||
|
const labels = useCanvasStore((state) => state.labels) ?? [];
|
||||||
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||||
|
const [image, imageStatus] = useImage(imageUrl);
|
||||||
|
const handleZoom = (e: Konva.KonvaEventObject<WheelEvent>) => {
|
||||||
|
const scaleBy = 1.05;
|
||||||
|
const oldScale = scale.current;
|
||||||
|
const mousePointTo = {
|
||||||
|
x: (stageRef.current?.getPointerPosition()?.x ?? 0) / oldScale - stageRef.current!.x() / oldScale,
|
||||||
|
y: (stageRef.current?.getPointerPosition()?.y ?? 0) / oldScale - stageRef.current!.y() / oldScale,
|
||||||
|
};
|
||||||
|
const newScale = e.evt.deltaY < 0 ? oldScale * scaleBy : oldScale / scaleBy;
|
||||||
|
scale.current = newScale;
|
||||||
|
|
||||||
|
stageRef.current?.scale({ x: newScale, y: newScale });
|
||||||
|
const newPos = {
|
||||||
|
x: -(mousePointTo.x - (stageRef.current?.getPointerPosition()?.x ?? 0) / newScale) * newScale,
|
||||||
|
y: -(mousePointTo.y - (stageRef.current?.getPointerPosition()?.y ?? 0) / newScale) * newScale,
|
||||||
|
};
|
||||||
|
stageRef.current?.position(newPos);
|
||||||
|
stageRef.current?.batchDraw();
|
||||||
|
};
|
||||||
|
const handleScroll = (e: Konva.KonvaEventObject<WheelEvent>) => {
|
||||||
|
const delta = -e.evt.deltaY;
|
||||||
|
const x = stageRef.current?.x();
|
||||||
|
const y = stageRef.current?.y();
|
||||||
|
const newX = e.evt.shiftKey ? x! + delta : x!;
|
||||||
|
const newY = e.evt.shiftKey ? y! : y! + delta;
|
||||||
|
stageRef.current?.position({ x: newX, y: newY });
|
||||||
|
stageRef.current?.batchDraw();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWheel = (e: Konva.KonvaEventObject<WheelEvent>) => {
|
||||||
|
if (stageRef.current === null) return;
|
||||||
|
e.evt.preventDefault();
|
||||||
|
|
||||||
|
e.evt.ctrlKey ? handleZoom(e) : handleScroll(e);
|
||||||
|
};
|
||||||
|
const getScale = (): Vector2d => {
|
||||||
|
if (scale.current) return { x: scale.current, y: scale.current };
|
||||||
|
const widthRatio = stageWidth / image!.width;
|
||||||
|
const heightRatio = stageHeight / image!.height;
|
||||||
|
scale.current = Math.min(widthRatio, heightRatio);
|
||||||
|
|
||||||
|
console.log(scale);
|
||||||
|
|
||||||
|
return { x: scale.current, y: scale.current };
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: remove mock data
|
||||||
|
useEffect(() => {
|
||||||
|
useCanvasStore.setState({ labels: mockLabels });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return imageStatus === 'loaded' ? (
|
||||||
|
<Stage
|
||||||
|
ref={stageRef}
|
||||||
|
width={stageWidth}
|
||||||
|
height={stageHeight}
|
||||||
|
className="overflow-hidden bg-gray-200"
|
||||||
|
draggable
|
||||||
|
onWheel={handleWheel}
|
||||||
|
scale={getScale()}
|
||||||
|
>
|
||||||
|
<Layer>
|
||||||
|
<Image image={image} />
|
||||||
|
</Layer>
|
||||||
|
{labels.map((label) => (
|
||||||
|
<Layer key={label.id}>
|
||||||
|
{label.type === 'rect' ? (
|
||||||
|
<LabelRect
|
||||||
|
isSelected={label.id === selectedId}
|
||||||
|
onSelect={() => setSelectedId(label.id)}
|
||||||
|
info={label}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</Layer>
|
||||||
|
))}
|
||||||
|
</Stage>
|
||||||
|
) : (
|
||||||
|
<div></div>
|
||||||
|
);
|
||||||
|
}
|
@ -65,16 +65,22 @@ export default function WorkspaceLayout() {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: 'Label 1',
|
name: 'Label 1',
|
||||||
color: '#FFaa33',
|
color: '#FFaa33',
|
||||||
|
coordinates: [],
|
||||||
|
type: 'rect',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'Label 2',
|
name: 'Label 2',
|
||||||
color: '#aaFF55',
|
color: '#aaFF55',
|
||||||
|
coordinates: [],
|
||||||
|
type: 'rect',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
name: 'Label 3',
|
name: 'Label 3',
|
||||||
color: '#77aaFF',
|
color: '#77aaFF',
|
||||||
|
coordinates: [],
|
||||||
|
type: 'rect',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -88,7 +94,7 @@ export default function WorkspaceLayout() {
|
|||||||
projects={workspace.projects}
|
projects={workspace.projects}
|
||||||
/>
|
/>
|
||||||
<ResizablePanel className="flex w-full items-center">
|
<ResizablePanel className="flex w-full items-center">
|
||||||
<main className="grow">
|
<main className="h-full grow">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
<WorkspaceLabelBar labels={labels} />
|
<WorkspaceLabelBar labels={labels} />
|
||||||
|
22
frontend/src/stores/useCanvasStore.ts
Normal file
22
frontend/src/stores/useCanvasStore.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Label } from '@/types';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface CanvasState {
|
||||||
|
image: string;
|
||||||
|
labels: Label[];
|
||||||
|
changeImage: (image: string, labels: Label[]) => void;
|
||||||
|
addLabel: (label: Label) => void;
|
||||||
|
removeLabel: (labelId: number) => void;
|
||||||
|
updateLabel: (label: Label) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCanvasStore = create<CanvasState>()((set) => ({
|
||||||
|
image: '',
|
||||||
|
labels: [],
|
||||||
|
changeImage: (image: string, labels: Label[]) => set({ image, labels }),
|
||||||
|
addLabel: (label: Label) => set((state) => ({ labels: [...state.labels, label] })),
|
||||||
|
removeLabel: (labelId: number) => set((state) => ({ labels: state.labels.filter((label) => label.id !== labelId) })),
|
||||||
|
updateLabel: (label: Label) => set((state) => ({ labels: state.labels.map((l) => (l.id === label.id ? label : l)) })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useCanvasStore;
|
@ -30,4 +30,6 @@ export type Label = {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
type: 'polygon' | 'rect';
|
||||||
|
coordinates: Array<[number, number]>;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user