@deepnoid/canvas 0.1.85 → 0.1.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/package.json +3 -2
- package/skills/deepnoid-canvas/DEPLOY.md +57 -0
- package/skills/deepnoid-canvas/README.md +56 -0
- package/skills/deepnoid-canvas/SKILL.md +131 -0
- package/skills/deepnoid-canvas/references/apply-style.md +189 -0
- package/skills/deepnoid-canvas/references/components.md +197 -0
- package/skills/deepnoid-canvas/references/types.md +231 -0
- package/skills/deepnoid-canvas/rules/annotations.md +152 -0
- package/skills/deepnoid-canvas/rules/events.md +124 -0
- package/skills/deepnoid-canvas/rules/layout.md +171 -0
- package/skills/deepnoid-canvas/rules/rendering.md +142 -0
- package/skills/deepnoid-canvas/setup.md +147 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# 타입 API 레퍼런스
|
|
2
|
+
|
|
3
|
+
모든 public 타입은 `@deepnoid/canvas`에서 직접 import합니다.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { DrawMode } from '@deepnoid/canvas';
|
|
9
|
+
import type {
|
|
10
|
+
Annotation,
|
|
11
|
+
Rectangle,
|
|
12
|
+
Label,
|
|
13
|
+
ApplyAnnotationStyle,
|
|
14
|
+
AnnotationCanvasHandle,
|
|
15
|
+
} from '@deepnoid/canvas';
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## DrawMode (Enum)
|
|
21
|
+
|
|
22
|
+
어노테이션 그리기 모드를 지정하는 enum.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
enum DrawMode {
|
|
26
|
+
RECTANGLE = 'RECTANGLE',
|
|
27
|
+
POLYGON = 'POLYGON',
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 사용 예시
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { DrawMode } from '@deepnoid/canvas';
|
|
35
|
+
|
|
36
|
+
// drawing.mode에 사용
|
|
37
|
+
<AnnotationEditor drawing={{ mode: DrawMode.RECTANGLE, ... }} />
|
|
38
|
+
|
|
39
|
+
// 어노테이션 타입 판별
|
|
40
|
+
if (annotation.type === DrawMode.RECTANGLE) {
|
|
41
|
+
// RectangleAnnotation으로 좁혀짐
|
|
42
|
+
console.log(annotation.x, annotation.y, annotation.width, annotation.height);
|
|
43
|
+
} else if (annotation.type === DrawMode.POLYGON) {
|
|
44
|
+
// PolygonAnnotation으로 좁혀짐
|
|
45
|
+
console.log(annotation.points);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Annotation (Union Type)
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
type Annotation = RectangleAnnotation | PolygonAnnotation;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`type` 필드로 discriminated union 판별.
|
|
58
|
+
|
|
59
|
+
### RectangleAnnotation
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
type RectangleAnnotation = {
|
|
63
|
+
type: DrawMode.RECTANGLE;
|
|
64
|
+
x: number; // 좌상단 X (원본 이미지 px)
|
|
65
|
+
y: number; // 좌상단 Y (원본 이미지 px)
|
|
66
|
+
width: number; // 너비 (원본 이미지 px)
|
|
67
|
+
height: number; // 높이 (원본 이미지 px)
|
|
68
|
+
label?: Label; // 라벨 정보
|
|
69
|
+
color?: 'success' | 'warning' | 'danger'; // 상태 색상
|
|
70
|
+
selected?: boolean; // 선택 상태 (엔진 내부 관리)
|
|
71
|
+
showOnlySelected?: boolean;
|
|
72
|
+
};
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### PolygonAnnotation
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
type PolygonAnnotation = {
|
|
79
|
+
type: DrawMode.POLYGON;
|
|
80
|
+
points: { x: number; y: number }[]; // 꼭짓점 배열 (원본 이미지 px)
|
|
81
|
+
label?: Label;
|
|
82
|
+
color?: 'success' | 'warning' | 'danger';
|
|
83
|
+
selected?: boolean;
|
|
84
|
+
showOnlySelected?: boolean;
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 데이터 예시
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// RectangleAnnotation
|
|
92
|
+
const rect: Annotation = {
|
|
93
|
+
type: DrawMode.RECTANGLE,
|
|
94
|
+
x: 100,
|
|
95
|
+
y: 50,
|
|
96
|
+
width: 200,
|
|
97
|
+
height: 150,
|
|
98
|
+
label: { id: 1, name: 'defect', type: 'bbox' },
|
|
99
|
+
color: 'danger',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// PolygonAnnotation
|
|
103
|
+
const polygon: Annotation = {
|
|
104
|
+
type: DrawMode.POLYGON,
|
|
105
|
+
points: [
|
|
106
|
+
{ x: 100, y: 100 },
|
|
107
|
+
{ x: 250, y: 80 },
|
|
108
|
+
{ x: 300, y: 200 },
|
|
109
|
+
{ x: 150, y: 250 },
|
|
110
|
+
],
|
|
111
|
+
label: { id: 2, name: 'crack', type: 'polygon' },
|
|
112
|
+
color: 'warning',
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Rectangle (Type)
|
|
119
|
+
|
|
120
|
+
사각형의 기하 정보만 담는 순수 타입. 어노테이션이 아닌 좌표 계산 등에 활용.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
type Rectangle = {
|
|
124
|
+
x: number;
|
|
125
|
+
y: number;
|
|
126
|
+
width: number;
|
|
127
|
+
height: number;
|
|
128
|
+
};
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Label (Type)
|
|
134
|
+
|
|
135
|
+
어노테이션에 부여하는 라벨 정보.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
type Label = {
|
|
139
|
+
id: number; // 라벨 고유 ID
|
|
140
|
+
name: string; // 표시 이름 (applyStyle의 drawText에서 사용)
|
|
141
|
+
type: string; // 라벨 분류 (자유 문자열, 예: 'bbox', 'polygon', 'seed')
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 사용 예시
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
const labels: Label[] = [
|
|
149
|
+
{ id: 1, name: '결함', type: 'defect' },
|
|
150
|
+
{ id: 2, name: '균열', type: 'crack' },
|
|
151
|
+
{ id: 3, name: '정상', type: 'normal' },
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
// drawing.label에 설정하면 새로 그리는 어노테이션에 자동 부여
|
|
155
|
+
<AnnotationEditor
|
|
156
|
+
drawing={{
|
|
157
|
+
mode: DrawMode.RECTANGLE,
|
|
158
|
+
label: labels[0],
|
|
159
|
+
lineSize: 2,
|
|
160
|
+
applyStyle,
|
|
161
|
+
}}
|
|
162
|
+
...
|
|
163
|
+
/>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## ApplyAnnotationStyle (Type)
|
|
169
|
+
|
|
170
|
+
어노테이션 렌더링 스타일을 완전 제어하는 콜백 함수 타입.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
type ApplyAnnotationStyle = (params: ApplyAnnotationStyleParams) => void;
|
|
174
|
+
|
|
175
|
+
type ApplyAnnotationStyleParams = {
|
|
176
|
+
variant: 'drawRect' | 'drawText'; // 현재 렌더링 단계
|
|
177
|
+
context: CanvasRenderingContext2D; // Canvas 2D 컨텍스트
|
|
178
|
+
annotation: Annotation; // 렌더링 대상 어노테이션
|
|
179
|
+
drawLineSize: number; // 설정된 선 두께 (현재 2px 고정)
|
|
180
|
+
zoom: number; // 현재 줌 배율
|
|
181
|
+
};
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### variant 설명
|
|
185
|
+
|
|
186
|
+
| variant | 호출 시점 | 설정해야 할 Canvas 속성 |
|
|
187
|
+
|---------|----------|------------------------|
|
|
188
|
+
| `'drawRect'` | 도형 렌더링 | `strokeStyle`, `fillStyle`, `lineWidth`, `setLineDash` + 실제 도형 그리기 (`strokeRect`, `beginPath`/`stroke` 등) |
|
|
189
|
+
| `'drawText'` | 라벨 텍스트 렌더링 | `fillStyle`, `font`, `textBaseline` + 실제 텍스트 그리기 (`fillText`) |
|
|
190
|
+
|
|
191
|
+
상세 예제는 [apply-style.md](apply-style.md) 참조.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## AnnotationCanvasHandle (Interface)
|
|
196
|
+
|
|
197
|
+
`ref`를 통해 캔버스를 프로그래밍 방식으로 제어.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
interface AnnotationCanvasHandle {
|
|
201
|
+
resetImageSize(): void; // 캔버스 크기 및 줌을 초기 상태로 리셋
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 사용 예시
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { useRef } from 'react';
|
|
209
|
+
import type { AnnotationCanvasHandle } from '@deepnoid/canvas';
|
|
210
|
+
|
|
211
|
+
const ref = useRef<AnnotationCanvasHandle>(null);
|
|
212
|
+
|
|
213
|
+
// 줌 리셋 버튼
|
|
214
|
+
<button onClick={() => ref.current?.resetImageSize()}>초기화</button>
|
|
215
|
+
<AnnotationEditor ref={ref} ... />
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 전체 Export 요약
|
|
221
|
+
|
|
222
|
+
| Export | Kind | import 방식 |
|
|
223
|
+
|--------|------|------------|
|
|
224
|
+
| `AnnotationEditor` | Component | `import { AnnotationEditor } from '@deepnoid/canvas'` |
|
|
225
|
+
| `AnnotationViewer` | Component | `import { AnnotationViewer } from '@deepnoid/canvas'` |
|
|
226
|
+
| `DrawMode` | Enum (value) | `import { DrawMode } from '@deepnoid/canvas'` |
|
|
227
|
+
| `Annotation` | Type | `import type { Annotation } from '@deepnoid/canvas'` |
|
|
228
|
+
| `Rectangle` | Type | `import type { Rectangle } from '@deepnoid/canvas'` |
|
|
229
|
+
| `Label` | Type | `import type { Label } from '@deepnoid/canvas'` |
|
|
230
|
+
| `ApplyAnnotationStyle` | Type | `import type { ApplyAnnotationStyle } from '@deepnoid/canvas'` |
|
|
231
|
+
| `AnnotationCanvasHandle` | Interface | `import type { AnnotationCanvasHandle } from '@deepnoid/canvas'` |
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# 어노테이션 데이터 규칙
|
|
2
|
+
|
|
3
|
+
## 규칙 1: 모든 어노테이션에 type 필드가 필수다
|
|
4
|
+
|
|
5
|
+
`Annotation` 타입은 discriminated union이므로, `type` 필드가 없으면 타입 추론이 동작하지 않습니다.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
// ✅ 올바른 사용 — type 필드 포함
|
|
9
|
+
const rect: Annotation = {
|
|
10
|
+
type: DrawMode.RECTANGLE,
|
|
11
|
+
x: 100,
|
|
12
|
+
y: 50,
|
|
13
|
+
width: 200,
|
|
14
|
+
height: 150,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const polygon: Annotation = {
|
|
18
|
+
type: DrawMode.POLYGON,
|
|
19
|
+
points: [
|
|
20
|
+
{ x: 100, y: 100 },
|
|
21
|
+
{ x: 200, y: 100 },
|
|
22
|
+
{ x: 150, y: 200 },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ❌ 잘못된 사용 — type 누락
|
|
27
|
+
const rect = { x: 100, y: 50, width: 200, height: 150 };
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 규칙 2: DrawMode enum을 사용해야 한다
|
|
31
|
+
|
|
32
|
+
문자열 리터럴 대신 반드시 `DrawMode` enum을 사용합니다.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { DrawMode } from '@deepnoid/canvas';
|
|
36
|
+
|
|
37
|
+
// ✅ 올바른 사용
|
|
38
|
+
{ type: DrawMode.RECTANGLE, ... }
|
|
39
|
+
{ type: DrawMode.POLYGON, ... }
|
|
40
|
+
|
|
41
|
+
// ❌ 잘못된 사용 — 문자열 직접 사용 (타입 에러)
|
|
42
|
+
{ type: 'RECTANGLE', ... }
|
|
43
|
+
{ type: 'rectangle', ... }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 규칙 3: 좌표는 원본 이미지 기준 픽셀 좌표다
|
|
47
|
+
|
|
48
|
+
캔버스에 표시된 좌표가 아니라, 원본 이미지의 픽셀 좌표를 사용합니다. 줌/팬 변환은 엔진이 자동 처리합니다.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// ✅ 올바른 사용 — 원본 이미지 좌표
|
|
52
|
+
// 원본 이미지가 1920x1080일 때:
|
|
53
|
+
{ type: DrawMode.RECTANGLE, x: 500, y: 300, width: 200, height: 150 }
|
|
54
|
+
|
|
55
|
+
// ❌ 잘못된 사용 — 화면 표시 좌표를 그대로 사용
|
|
56
|
+
// 캔버스가 50%로 축소되어 있어도 원본 좌표를 사용해야 함
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 규칙 4: RectangleAnnotation의 width/height는 양수여야 한다
|
|
60
|
+
|
|
61
|
+
사용자가 역방향으로 드래그하면 엔진이 자동 정규화하지만, 외부에서 직접 생성 시 양수 값이어야 합니다.
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// ✅ 올바른 사용
|
|
65
|
+
{ type: DrawMode.RECTANGLE, x: 100, y: 100, width: 200, height: 150 }
|
|
66
|
+
|
|
67
|
+
// ❌ 잘못된 사용 — 음수 크기
|
|
68
|
+
{ type: DrawMode.RECTANGLE, x: 300, y: 250, width: -200, height: -150 }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 규칙 5: PolygonAnnotation의 points는 최소 3개여야 한다
|
|
72
|
+
|
|
73
|
+
유효한 폴리곤을 형성하려면 최소 3개의 점이 필요합니다.
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// ✅ 올바른 사용 — 3개 이상의 점
|
|
77
|
+
{
|
|
78
|
+
type: DrawMode.POLYGON,
|
|
79
|
+
points: [
|
|
80
|
+
{ x: 100, y: 100 },
|
|
81
|
+
{ x: 200, y: 100 },
|
|
82
|
+
{ x: 150, y: 200 },
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ❌ 잘못된 사용 — 2개 이하의 점
|
|
87
|
+
{
|
|
88
|
+
type: DrawMode.POLYGON,
|
|
89
|
+
points: [
|
|
90
|
+
{ x: 100, y: 100 },
|
|
91
|
+
{ x: 200, y: 100 },
|
|
92
|
+
],
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 규칙 6: annotations 상태 관리는 React state로 한다
|
|
97
|
+
|
|
98
|
+
`setAnnotations` 콜백으로 어노테이션이 변경될 때 외부 상태와 동기화됩니다.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
// ✅ 올바른 사용 — React state 관리
|
|
102
|
+
const [annotations, setAnnotations] = useState<Annotation[]>([]);
|
|
103
|
+
|
|
104
|
+
<AnnotationEditor
|
|
105
|
+
annotations={annotations}
|
|
106
|
+
setAnnotations={setAnnotations}
|
|
107
|
+
...
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
// ❌ 잘못된 사용 — ref나 외부 변수로 관리
|
|
111
|
+
const annotationsRef = useRef<Annotation[]>([]);
|
|
112
|
+
<AnnotationEditor
|
|
113
|
+
annotations={annotationsRef.current}
|
|
114
|
+
setAnnotations={(a) => { annotationsRef.current = a; }}
|
|
115
|
+
...
|
|
116
|
+
/>
|
|
117
|
+
// → 리렌더링이 발생하지 않아 UI가 동기화되지 않음
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## 규칙 7: selected 필드를 외부에서 직접 조작하지 않는다
|
|
121
|
+
|
|
122
|
+
어노테이션 선택 상태는 엔진이 내부적으로 관리합니다. 외부에서 `selected: true`를 설정해도 동작하지만, 엔진과 충돌할 수 있습니다.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
// ✅ 올바른 사용 — 선택은 캔버스 내 클릭으로
|
|
126
|
+
// 사용자가 캔버스에서 어노테이션을 클릭하면 자동으로 selected 처리됨
|
|
127
|
+
|
|
128
|
+
// ⚠️ 주의 — 외부에서 selected 설정
|
|
129
|
+
setAnnotations(prev => prev.map(a =>
|
|
130
|
+
a === target ? { ...a, selected: true } : { ...a, selected: false }
|
|
131
|
+
));
|
|
132
|
+
// 가능하지만, setAnnotations가 다시 엔진에 전달되면서 불필요한 리렌더링 발생 가능
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## 규칙 8: label은 선택적이지만, 텍스트 표시에 필요하다
|
|
136
|
+
|
|
137
|
+
`label` 필드가 없으면 `drawText` variant에서 표시할 텍스트가 없습니다.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
// ✅ 텍스트 라벨이 필요한 경우
|
|
141
|
+
{
|
|
142
|
+
type: DrawMode.RECTANGLE,
|
|
143
|
+
x: 100, y: 100, width: 200, height: 150,
|
|
144
|
+
label: { id: 1, name: 'defect', type: 'bbox' },
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ✅ 텍스트 라벨이 불필요한 경우 — label 생략 가능
|
|
148
|
+
{
|
|
149
|
+
type: DrawMode.RECTANGLE,
|
|
150
|
+
x: 100, y: 100, width: 200, height: 150,
|
|
151
|
+
}
|
|
152
|
+
```
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# 이벤트 및 단축키 규칙
|
|
2
|
+
|
|
3
|
+
## 규칙 1: annotationSelected 이벤트로 DrawMode 동기화
|
|
4
|
+
|
|
5
|
+
캔버스에서 어노테이션을 클릭하면 `window`에 `annotationSelected` CustomEvent가 발생합니다.
|
|
6
|
+
이를 통해 외부 UI의 그리기 모드를 자동 전환할 수 있습니다.
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
// ✅ 올바른 사용 — annotationSelected 이벤트 리스닝
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const handler = (e: CustomEvent) => {
|
|
12
|
+
if (e.detail?.type) {
|
|
13
|
+
setDrawMode(e.detail.type); // DrawMode.RECTANGLE 또는 DrawMode.POLYGON
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
window.addEventListener('annotationSelected', handler);
|
|
17
|
+
return () => window.removeEventListener('annotationSelected', handler);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
// ❌ 잘못된 사용 — 이벤트 클린업 누락
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
window.addEventListener('annotationSelected', handler);
|
|
23
|
+
// return 없음 → 메모리 누수
|
|
24
|
+
}, []);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 규칙 2: enableHotkeys를 명시적으로 설정한다
|
|
28
|
+
|
|
29
|
+
단축키는 기본적으로 비활성화되어 있습니다 (`enableHotkeys` 기본값: `true` in Editor, 단 명시적 설정 권장).
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// ✅ 올바른 사용 — 명시적 설정
|
|
33
|
+
<AnnotationEditor enableHotkeys {...otherProps} />
|
|
34
|
+
|
|
35
|
+
// ✅ 단축키 충돌이 있는 경우 비활성화
|
|
36
|
+
<AnnotationEditor enableHotkeys={false} {...otherProps} />
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 지원 단축키
|
|
40
|
+
|
|
41
|
+
| 키 | 기능 | 조건 |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `Delete` | 선택된 어노테이션 삭제 | 어노테이션 선택 상태일 때 |
|
|
44
|
+
| `Ctrl+Z` (Mac: `Cmd+Z`) | Undo | 히스토리가 있을 때 |
|
|
45
|
+
| `Ctrl+Shift+Z` (Mac: `Cmd+Shift+Z`) | Redo | undo 후 히스토리가 있을 때 |
|
|
46
|
+
| `X` | 선택 어노테이션만 보기 토글 | 언제든 |
|
|
47
|
+
| `Escape` | 진행 중인 그리기 취소 | 폴리곤 점 찍기 중 등 |
|
|
48
|
+
|
|
49
|
+
## 규칙 3: 이미지 로드 이벤트를 활용한다
|
|
50
|
+
|
|
51
|
+
`events` prop으로 이미지 로드 성공/실패를 감지할 수 있습니다.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// ✅ 올바른 사용 — 로드 상태 관리
|
|
55
|
+
const [imageLoaded, setImageLoaded] = useState(false);
|
|
56
|
+
|
|
57
|
+
<AnnotationEditor
|
|
58
|
+
image={imageUrl}
|
|
59
|
+
events={{
|
|
60
|
+
onImageLoadSuccess: () => setImageLoaded(true),
|
|
61
|
+
onImageLoadError: (error) => {
|
|
62
|
+
console.error('이미지 로드 실패:', error);
|
|
63
|
+
setImageLoaded(false);
|
|
64
|
+
},
|
|
65
|
+
}}
|
|
66
|
+
...
|
|
67
|
+
/>
|
|
68
|
+
|
|
69
|
+
// ❌ 잘못된 사용 — events를 undefined로 전달
|
|
70
|
+
<AnnotationEditor
|
|
71
|
+
events={undefined} // 타입 에러: events는 필수 prop
|
|
72
|
+
...
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
// ✅ 이벤트가 필요 없으면 빈 객체 전달
|
|
76
|
+
<AnnotationEditor events={{}} ... />
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 규칙 4: 단축키와 외부 키 이벤트의 충돌에 주의한다
|
|
80
|
+
|
|
81
|
+
`enableHotkeys`가 켜져 있으면 `window` 레벨에서 키 이벤트를 감지합니다.
|
|
82
|
+
외부에서 같은 키(Delete, X, Ctrl+Z)를 사용하는 경우 충돌이 발생할 수 있습니다.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// ✅ 충돌 방지 — 캔버스 영역에서만 단축키 활성화
|
|
86
|
+
// enableHotkeys를 조건부로 제어
|
|
87
|
+
const [canvasFocused, setCanvasFocused] = useState(false);
|
|
88
|
+
|
|
89
|
+
<div
|
|
90
|
+
onFocus={() => setCanvasFocused(true)}
|
|
91
|
+
onBlur={() => setCanvasFocused(false)}
|
|
92
|
+
>
|
|
93
|
+
<AnnotationEditor
|
|
94
|
+
enableHotkeys={canvasFocused}
|
|
95
|
+
...
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
// ❌ 잘못된 사용 — 폼 입력이 있는 페이지에서 항상 enableHotkeys
|
|
100
|
+
// → Delete 키로 텍스트 삭제 시 어노테이션도 삭제됨
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 규칙 5: setAnnotations 콜백은 비동기적으로 호출된다
|
|
104
|
+
|
|
105
|
+
엔진 내부에서 어노테이션이 변경되면 `setAnnotations` 콜백이 호출됩니다.
|
|
106
|
+
이 콜백 내에서 무거운 로직을 실행하면 성능에 영향을 줄 수 있습니다.
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
// ✅ 올바른 사용 — 가벼운 상태 업데이트만
|
|
110
|
+
<AnnotationEditor
|
|
111
|
+
setAnnotations={setAnnotations}
|
|
112
|
+
...
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
// ⚠️ 주의 — 콜백 내 무거운 로직
|
|
116
|
+
<AnnotationEditor
|
|
117
|
+
setAnnotations={(annotations) => {
|
|
118
|
+
setAnnotations(annotations);
|
|
119
|
+
// 매 마우스 이동마다 호출될 수 있어 성능 이슈
|
|
120
|
+
saveToServer(annotations); // ← debounce 권장
|
|
121
|
+
}}
|
|
122
|
+
...
|
|
123
|
+
/>
|
|
124
|
+
```
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# 레이아웃, 줌/팬 규칙
|
|
2
|
+
|
|
3
|
+
## 규칙 1: 컨테이너에 반드시 고정 크기를 설정한다
|
|
4
|
+
|
|
5
|
+
`AnnotationEditor`/`AnnotationViewer`는 부모 컨테이너의 100% 크기를 차지합니다.
|
|
6
|
+
부모에 크기가 없으면 캔버스가 0px로 렌더링됩니다.
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
// ✅ 올바른 사용 — 고정 크기
|
|
10
|
+
<div style={{ width: 800, height: 600 }}>
|
|
11
|
+
<AnnotationEditor ... />
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
// ✅ flex 레이아웃
|
|
15
|
+
<div style={{ display: 'flex', height: '100vh' }}>
|
|
16
|
+
<aside style={{ width: 200 }}>사이드바</aside>
|
|
17
|
+
<div style={{ flex: 1 }}>
|
|
18
|
+
<AnnotationEditor ... />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
// ❌ 잘못된 사용 — 크기 없는 컨테이너
|
|
23
|
+
<div>
|
|
24
|
+
<AnnotationEditor ... />
|
|
25
|
+
</div>
|
|
26
|
+
// → 캔버스 크기가 0이 되어 아무것도 보이지 않음
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 규칙 2: panZoomEnabled로 줌/팬을 제어한다
|
|
30
|
+
|
|
31
|
+
줌(마우스 휠)과 팬(우클릭 드래그)은 `options.panZoomEnabled`로 제어합니다.
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// ✅ 줌/팬 활성화
|
|
35
|
+
<AnnotationEditor
|
|
36
|
+
options={{ panZoomEnabled: true }}
|
|
37
|
+
...
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
// ✅ 줌/팬 비활성화 (고정 뷰)
|
|
41
|
+
<AnnotationEditor
|
|
42
|
+
options={{ panZoomEnabled: false }}
|
|
43
|
+
...
|
|
44
|
+
/>
|
|
45
|
+
|
|
46
|
+
// ✅ 기본값: panZoomEnabled를 생략하면 옵션 객체 자체가 없어야 함
|
|
47
|
+
<AnnotationEditor options={undefined} ... />
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 규칙 3: zoom 옵션으로 줌 범위를 제한한다
|
|
51
|
+
|
|
52
|
+
줌 배율의 최소/최대/단계를 설정할 수 있습니다.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// ✅ 올바른 사용 — 줌 범위 설정
|
|
56
|
+
<AnnotationEditor
|
|
57
|
+
options={{
|
|
58
|
+
panZoomEnabled: true,
|
|
59
|
+
zoom: {
|
|
60
|
+
min: 0.5, // 최소 50% (기본: 0.1)
|
|
61
|
+
max: 4, // 최대 400% (기본: Infinity)
|
|
62
|
+
step: 0.9, // 한 스텝당 배율 변화 (기본: 0.9)
|
|
63
|
+
},
|
|
64
|
+
}}
|
|
65
|
+
...
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
// ✅ 일부만 설정 가능
|
|
69
|
+
<AnnotationEditor
|
|
70
|
+
options={{
|
|
71
|
+
panZoomEnabled: true,
|
|
72
|
+
zoom: { max: 10 }, // min, step은 기본값 사용
|
|
73
|
+
}}
|
|
74
|
+
...
|
|
75
|
+
/>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 규칙 4: ZoomButton으로 줌 비율 표시 및 리셋
|
|
79
|
+
|
|
80
|
+
커스텀 줌 버튼 컴포넌트를 주입하여 현재 줌 비율 표시 및 클릭 시 줌 리셋이 가능합니다.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
// ✅ 올바른 사용 — 커스텀 ZoomButton
|
|
84
|
+
const ZoomButton = ({ onClick, children }: { onClick: () => void; children: ReactNode }) => (
|
|
85
|
+
<button
|
|
86
|
+
onClick={onClick}
|
|
87
|
+
style={{ position: 'absolute', top: 10, left: 10, zIndex: 10 }}
|
|
88
|
+
>
|
|
89
|
+
{children} {/* "125%" 같은 줌 비율 문자열 */}
|
|
90
|
+
</button>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
<AnnotationEditor
|
|
94
|
+
options={{
|
|
95
|
+
panZoomEnabled: true,
|
|
96
|
+
ZoomButton,
|
|
97
|
+
}}
|
|
98
|
+
...
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
// ❌ 잘못된 사용 — ZoomButton에 잘못된 시그니처
|
|
102
|
+
const ZoomButton = ({ zoom }) => <span>{zoom}</span>; // onClick과 children이 필요
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 규칙 5: resetOnImageChange로 이미지 전환 시 줌 상태를 제어한다
|
|
106
|
+
|
|
107
|
+
이미지가 바뀔 때 줌/팬 상태를 초기화할지 유지할지 선택할 수 있습니다.
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
// ✅ 이미지 전환 시 줌 리셋
|
|
111
|
+
<AnnotationEditor
|
|
112
|
+
options={{ resetOnImageChange: true }}
|
|
113
|
+
...
|
|
114
|
+
/>
|
|
115
|
+
|
|
116
|
+
// ✅ 이미지 전환 시 줌 유지 (기본값: false)
|
|
117
|
+
<AnnotationEditor
|
|
118
|
+
options={{ resetOnImageChange: false }}
|
|
119
|
+
...
|
|
120
|
+
/>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 규칙 6: ref로 캔버스 크기를 수동 리셋할 수 있다
|
|
124
|
+
|
|
125
|
+
`AnnotationCanvasHandle` ref를 통해 프로그래밍 방식으로 캔버스를 리셋합니다.
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import { useRef } from 'react';
|
|
129
|
+
import type { AnnotationCanvasHandle } from '@deepnoid/canvas';
|
|
130
|
+
|
|
131
|
+
// ✅ 올바른 사용
|
|
132
|
+
const canvasRef = useRef<AnnotationCanvasHandle>(null);
|
|
133
|
+
|
|
134
|
+
<button onClick={() => canvasRef.current?.resetImageSize()}>
|
|
135
|
+
크기 초기화
|
|
136
|
+
</button>
|
|
137
|
+
<AnnotationEditor ref={canvasRef} ... />
|
|
138
|
+
|
|
139
|
+
// ❌ 잘못된 사용 — 잘못된 타입
|
|
140
|
+
const canvasRef = useRef<HTMLCanvasElement>(null); // AnnotationCanvasHandle이어야 함
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 규칙 7: keepCrossOnImageChange로 가이드 십자선 유지
|
|
144
|
+
|
|
145
|
+
이미지가 변경될 때 마우스 위치의 가이드 십자선(crosshair)을 유지할 수 있습니다.
|
|
146
|
+
`AnnotationEditor`에서만 사용 가능합니다.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
// ✅ 이미지 전환 시 십자선 유지 (연속 작업 시 편리)
|
|
150
|
+
<AnnotationEditor keepCrossOnImageChange ... />
|
|
151
|
+
|
|
152
|
+
// ✅ 기본 동작 — 이미지 전환 시 십자선 제거
|
|
153
|
+
<AnnotationEditor keepCrossOnImageChange={false} ... />
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 규칙 8: 리사이즈 대응은 자동으로 처리된다
|
|
157
|
+
|
|
158
|
+
컨테이너 크기가 변하면 내부 `ResizeObserver`가 자동으로 캔버스를 재설정합니다 (150ms debounce).
|
|
159
|
+
별도의 리사이즈 처리가 필요 없습니다.
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
// ✅ 리사이즈 대응 — 별도 처리 불필요
|
|
163
|
+
<div style={{ width: '100%', height: '100%' }}>
|
|
164
|
+
<AnnotationEditor ... />
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
// ❌ 불필요한 사용 — 수동 리사이즈 핸들러
|
|
168
|
+
window.addEventListener('resize', () => {
|
|
169
|
+
canvasRef.current?.resetImageSize(); // 불필요, 자동 처리됨
|
|
170
|
+
});
|
|
171
|
+
```
|