@deepnoid/canvas 0.1.84 → 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/dist/react/AnnotationCanvas.d.ts +1 -0
- package/dist/react/AnnotationCanvas.js +17 -4
- package/dist/react/index.d.ts +6 -2
- 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,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
|
+
```
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# applyStyle 렌더링 규칙
|
|
2
|
+
|
|
3
|
+
## 규칙 1: applyStyle은 반드시 제공해야 한다
|
|
4
|
+
|
|
5
|
+
`drawing.applyStyle`은 필수 prop입니다. 이 함수 없이는 어노테이션이 화면에 표시되지 않습니다.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
// ✅ 올바른 사용
|
|
9
|
+
<AnnotationEditor
|
|
10
|
+
drawing={{
|
|
11
|
+
lineSize: 2,
|
|
12
|
+
applyStyle: myStyleFunction,
|
|
13
|
+
mode: DrawMode.RECTANGLE,
|
|
14
|
+
color: '#FF4136',
|
|
15
|
+
}}
|
|
16
|
+
...
|
|
17
|
+
/>
|
|
18
|
+
|
|
19
|
+
// ❌ 잘못된 사용 — applyStyle 누락
|
|
20
|
+
<AnnotationEditor
|
|
21
|
+
drawing={{
|
|
22
|
+
lineSize: 2,
|
|
23
|
+
mode: DrawMode.RECTANGLE,
|
|
24
|
+
}}
|
|
25
|
+
...
|
|
26
|
+
/>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 규칙 2: variant별로 분기 처리해야 한다
|
|
30
|
+
|
|
31
|
+
`applyStyle`은 `drawRect`과 `drawText` 두 번 호출됩니다. 각 variant에 맞는 Canvas2D 속성을 설정해야 합니다.
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// ✅ 올바른 사용 — variant별 분기
|
|
35
|
+
const applyStyle: ApplyAnnotationStyle = ({ variant, context, annotation, drawLineSize, zoom }) => {
|
|
36
|
+
if (variant === 'drawRect') {
|
|
37
|
+
context.strokeStyle = '#FF4136';
|
|
38
|
+
context.lineWidth = drawLineSize / zoom;
|
|
39
|
+
if (annotation.type === DrawMode.RECTANGLE) {
|
|
40
|
+
context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
|
|
41
|
+
} else if (annotation.type === DrawMode.POLYGON) {
|
|
42
|
+
context.beginPath();
|
|
43
|
+
annotation.points.forEach((p, i) => i === 0 ? context.moveTo(p.x, p.y) : context.lineTo(p.x, p.y));
|
|
44
|
+
context.closePath();
|
|
45
|
+
context.stroke();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (variant === 'drawText') {
|
|
49
|
+
context.fillStyle = '#FF4136';
|
|
50
|
+
context.font = `bold ${14 / zoom}px sans-serif`;
|
|
51
|
+
// 텍스트 렌더링 로직...
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ❌ 잘못된 사용 — variant 무시
|
|
56
|
+
const applyStyle: ApplyAnnotationStyle = ({ context, annotation }) => {
|
|
57
|
+
context.strokeStyle = '#FF4136';
|
|
58
|
+
context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 규칙 3: 모든 크기 값에 zoom 보정을 적용해야 한다
|
|
63
|
+
|
|
64
|
+
줌 배율이 변해도 어노테이션 선 두께, 폰트 크기, 대시 간격 등이 일정하게 보이려면 반드시 `/ zoom`으로 나누어야 합니다.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// ✅ 올바른 사용 — zoom 보정
|
|
68
|
+
context.lineWidth = drawLineSize / zoom;
|
|
69
|
+
context.font = `bold ${14 / zoom}px sans-serif`;
|
|
70
|
+
context.setLineDash([6 / zoom, 3 / zoom]);
|
|
71
|
+
|
|
72
|
+
// ❌ 잘못된 사용 — zoom 보정 없음 (줌인하면 선이 점점 두꺼워짐)
|
|
73
|
+
context.lineWidth = drawLineSize;
|
|
74
|
+
context.font = `bold 14px sans-serif`;
|
|
75
|
+
context.setLineDash([6, 3]);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 규칙 4: annotation.type으로 분기하여 도형을 직접 그려야 한다
|
|
79
|
+
|
|
80
|
+
`applyStyle`의 `drawRect` variant에서는 스타일 설정뿐 아니라 **실제 도형 그리기**까지 소비자가 담당합니다.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
// ✅ 올바른 사용 — 타입별 도형 그리기
|
|
84
|
+
if (variant === 'drawRect') {
|
|
85
|
+
context.strokeStyle = '#FF4136';
|
|
86
|
+
context.lineWidth = drawLineSize / zoom;
|
|
87
|
+
|
|
88
|
+
if (annotation.type === DrawMode.RECTANGLE) {
|
|
89
|
+
const { x, y, width, height } = annotation;
|
|
90
|
+
context.strokeRect(x, y, width, height);
|
|
91
|
+
} else if (annotation.type === DrawMode.POLYGON) {
|
|
92
|
+
context.beginPath();
|
|
93
|
+
annotation.points.forEach((p, idx) => {
|
|
94
|
+
if (idx === 0) context.moveTo(p.x, p.y);
|
|
95
|
+
else context.lineTo(p.x, p.y);
|
|
96
|
+
});
|
|
97
|
+
context.closePath();
|
|
98
|
+
context.stroke();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ❌ 잘못된 사용 — RECTANGLE만 처리하고 POLYGON 무시
|
|
103
|
+
if (variant === 'drawRect') {
|
|
104
|
+
context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 규칙 5: setLineDash 사용 후 반드시 초기화해야 한다
|
|
109
|
+
|
|
110
|
+
Canvas2D 컨텍스트는 상태를 유지합니다. `setLineDash`로 점선을 설정하면 다음 어노테이션에도 영향을 줍니다.
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
// ✅ 올바른 사용 — 조건부 dash + 초기화
|
|
114
|
+
if (annotation.selected) {
|
|
115
|
+
context.setLineDash([6 / zoom, 3 / zoom]);
|
|
116
|
+
} else {
|
|
117
|
+
context.setLineDash([]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ❌ 잘못된 사용 — selected일 때만 dash 설정, else 빠짐
|
|
121
|
+
if (annotation.selected) {
|
|
122
|
+
context.setLineDash([6 / zoom, 3 / zoom]);
|
|
123
|
+
}
|
|
124
|
+
// → 다음 어노테이션도 점선으로 그려짐
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 규칙 6: annotation.color를 활용한 상태별 스타일링
|
|
128
|
+
|
|
129
|
+
어노테이션의 `color` 필드(`'success' | 'warning' | 'danger'`)를 활용하여 상태별 색상을 적용할 수 있습니다.
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
// ✅ 올바른 사용 — color 필드 매핑
|
|
133
|
+
const colorMap = {
|
|
134
|
+
success: 'rgba(36, 162, 91, 1)',
|
|
135
|
+
warning: 'rgba(237, 164, 16, 1)',
|
|
136
|
+
danger: 'rgba(255, 70, 132, 1)',
|
|
137
|
+
};
|
|
138
|
+
context.strokeStyle = colorMap[annotation.color ?? 'danger'];
|
|
139
|
+
|
|
140
|
+
// ❌ 잘못된 사용 — color 필드를 직접 CSS 색상으로 사용
|
|
141
|
+
context.strokeStyle = annotation.color; // 'danger'는 유효한 CSS 색상이 아님
|
|
142
|
+
```
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# 소비자 프로젝트 설정 가이드
|
|
2
|
+
|
|
3
|
+
## 1. 설치
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# npm
|
|
7
|
+
npm install @deepnoid/canvas
|
|
8
|
+
|
|
9
|
+
# yarn
|
|
10
|
+
yarn add @deepnoid/canvas
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Peer dependency**: React 19.0.0 이상 필수.
|
|
14
|
+
|
|
15
|
+
## 2. Claude Code 스킬 설치
|
|
16
|
+
|
|
17
|
+
패키지에 포함된 스킬을 프로젝트의 `.claude/skills/`에 복사합니다.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cp -r node_modules/@deepnoid/canvas/skills/deepnoid-canvas .claude/skills/
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
이후 Claude Code가 `@deepnoid/canvas` 관련 코드를 작성할 때 자동으로 스킬을 참조합니다.
|
|
24
|
+
|
|
25
|
+
## 3. 기본 설정
|
|
26
|
+
|
|
27
|
+
### 최소 구성 (Editor)
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { useState } from 'react';
|
|
31
|
+
import { AnnotationEditor, DrawMode } from '@deepnoid/canvas';
|
|
32
|
+
import type { Annotation, ApplyAnnotationStyle } from '@deepnoid/canvas';
|
|
33
|
+
|
|
34
|
+
// Step 1: applyStyle 함수 정의 (필수)
|
|
35
|
+
const applyStyle: ApplyAnnotationStyle = ({ variant, context, annotation, drawLineSize, zoom }) => {
|
|
36
|
+
if (variant === 'drawRect') {
|
|
37
|
+
context.strokeStyle = annotation.selected ? '#00FF00' : '#FF4136';
|
|
38
|
+
context.lineWidth = drawLineSize / zoom;
|
|
39
|
+
if (annotation.type === DrawMode.RECTANGLE) {
|
|
40
|
+
context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
|
|
41
|
+
} else if (annotation.type === DrawMode.POLYGON) {
|
|
42
|
+
context.beginPath();
|
|
43
|
+
annotation.points.forEach((p, i) => i === 0 ? context.moveTo(p.x, p.y) : context.lineTo(p.x, p.y));
|
|
44
|
+
context.closePath();
|
|
45
|
+
context.stroke();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Step 2: 컴포넌트 사용
|
|
51
|
+
function AnnotationPage() {
|
|
52
|
+
const [annotations, setAnnotations] = useState<Annotation[]>([]);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
// Step 3: 반드시 고정 크기의 컨테이너로 감싸기
|
|
56
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
57
|
+
<AnnotationEditor
|
|
58
|
+
image="https://example.com/image.jpg"
|
|
59
|
+
annotations={annotations}
|
|
60
|
+
setAnnotations={setAnnotations}
|
|
61
|
+
drawing={{
|
|
62
|
+
mode: DrawMode.RECTANGLE,
|
|
63
|
+
color: '#FF4136',
|
|
64
|
+
lineSize: 2,
|
|
65
|
+
applyStyle,
|
|
66
|
+
}}
|
|
67
|
+
events={{}}
|
|
68
|
+
enableHotkeys
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 최소 구성 (Viewer)
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { AnnotationViewer } from '@deepnoid/canvas';
|
|
79
|
+
|
|
80
|
+
function ViewerPage({ annotations }) {
|
|
81
|
+
return (
|
|
82
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
83
|
+
<AnnotationViewer
|
|
84
|
+
image="https://example.com/image.jpg"
|
|
85
|
+
annotations={annotations}
|
|
86
|
+
drawing={{ lineSize: 2, applyStyle }}
|
|
87
|
+
events={{}}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 4. 권장 설정
|
|
95
|
+
|
|
96
|
+
### 줌/팬 활성화
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<AnnotationEditor
|
|
100
|
+
options={{
|
|
101
|
+
panZoomEnabled: true,
|
|
102
|
+
zoom: { min: 0.5, max: 4, step: 0.9 },
|
|
103
|
+
}}
|
|
104
|
+
...
|
|
105
|
+
/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### annotationSelected 이벤트 연동
|
|
109
|
+
|
|
110
|
+
어노테이션 클릭 시 그리기 모드 자동 전환:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
const [mode, setMode] = useState(DrawMode.RECTANGLE);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
const handler = (e: CustomEvent) => {
|
|
117
|
+
if (e.detail?.type) setMode(e.detail.type);
|
|
118
|
+
};
|
|
119
|
+
window.addEventListener('annotationSelected', handler);
|
|
120
|
+
return () => window.removeEventListener('annotationSelected', handler);
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
<AnnotationEditor drawing={{ mode, ... }} ... />
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### ref를 통한 줌 리셋 버튼
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { useRef } from 'react';
|
|
130
|
+
import type { AnnotationCanvasHandle } from '@deepnoid/canvas';
|
|
131
|
+
|
|
132
|
+
const canvasRef = useRef<AnnotationCanvasHandle>(null);
|
|
133
|
+
|
|
134
|
+
<button onClick={() => canvasRef.current?.resetImageSize()}>초기화</button>
|
|
135
|
+
<AnnotationEditor ref={canvasRef} ... />
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 5. 체크리스트
|
|
139
|
+
|
|
140
|
+
소비자 프로젝트 설정 후 확인 사항:
|
|
141
|
+
|
|
142
|
+
- [ ] React 19+ 설치 확인
|
|
143
|
+
- [ ] `applyStyle` 함수에서 `drawRect`/`drawText` variant 분기 처리
|
|
144
|
+
- [ ] `applyStyle` 내 모든 크기 값에 `/ zoom` 보정 적용
|
|
145
|
+
- [ ] 컨테이너 `div`에 고정 크기(width/height) 설정
|
|
146
|
+
- [ ] RECTANGLE/POLYGON 모두에서 도형 그리기 처리
|
|
147
|
+
- [ ] `events` prop에 최소 빈 객체 `{}` 전달
|