@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 CHANGED
@@ -31,9 +31,23 @@ Canvas 기반 이미지 annotation 라이브러리로, **TypeScript Engine + Rea
31
31
  ### 설치
32
32
 
33
33
  ```bash
34
- npm install deepnoid-canvas
34
+ npm install @deepnoid/canvas
35
35
  ```
36
36
 
37
+ ### Claude Code 스킬 설치 (선택)
38
+
39
+ 소비자 프로젝트에서 Claude Code가 `@deepnoid/canvas` API를 정확하게 사용할 수 있도록 스킬을 복사합니다.
40
+
41
+ ```bash
42
+ # .claude/skills 디렉토리가 없으면 생성
43
+ mkdir -p .claude/skills
44
+
45
+ # 스킬 복사
46
+ cp -r node_modules/@deepnoid/canvas/skills/deepnoid-canvas .claude/skills/
47
+ ```
48
+
49
+ 복사 후 Claude Code에서 `@deepnoid/canvas`를 import하면 자동으로 올바른 사용법, Props, applyStyle 작성법 등을 안내합니다.
50
+
37
51
  ### 기본 사용법
38
52
 
39
53
  ```tsx
@@ -22,6 +22,7 @@ export type AnnotationCanvasProps = {
22
22
  };
23
23
  enableHotkeys?: boolean;
24
24
  editable?: boolean;
25
+ keepCrossOnImageChange?: boolean;
25
26
  };
26
27
  export interface AnnotationCanvasHandle {
27
28
  resetImageSize: () => void;
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
3
+ import { useEffect, useRef, useState, forwardRef, useImperativeHandle, } from 'react';
4
4
  import { AnnotationEngine } from '../engine/public/annotationEngine';
5
5
  import useResizeObserver from './hooks/useResizeObserver';
6
6
  import { useDebounce } from './hooks/useDebounce';
@@ -12,7 +12,7 @@ function safeRemove(parent, child) {
12
12
  parent.removeChild(child);
13
13
  }
14
14
  }
15
- const AnnotationCanvas = forwardRef(({ image, annotations = [], setAnnotations, options, drawing, events, enableHotkeys = true, editable = true, }, ref) => {
15
+ const AnnotationCanvas = forwardRef(({ image, annotations = [], setAnnotations, options, drawing, events, enableHotkeys = true, editable = true, keepCrossOnImageChange = false, }, ref) => {
16
16
  const { panZoomEnabled, zoom, ZoomButton, resetOnImageChange = false } = options || {};
17
17
  const { onImageLoadSuccess, onImageLoadError } = events || {};
18
18
  const containerRef = useRef(null);
@@ -20,6 +20,7 @@ const AnnotationCanvas = forwardRef(({ image, annotations = [], setAnnotations,
20
20
  const pendingAnnotationsRef = useRef(null);
21
21
  const imageLoadingRef = useRef(false);
22
22
  const engineIdRef = useRef(0);
23
+ const clientMousePointRef = useRef(null);
23
24
  const [, forceRender] = useState(0);
24
25
  /* ---------- Resize Observer ---------- */
25
26
  useResizeObserver({
@@ -110,6 +111,12 @@ const AnnotationCanvas = forwardRef(({ image, annotations = [], setAnnotations,
110
111
  container.insertBefore(imageCanvas, firstChild);
111
112
  container.insertBefore(annotationsCanvas, firstChild);
112
113
  engine.initImageCanvas(resetOnImageChange);
114
+ if (keepCrossOnImageChange && clientMousePointRef.current) {
115
+ engine.onMouseMove({
116
+ clientX: clientMousePointRef.current.x,
117
+ clientY: clientMousePointRef.current.y,
118
+ });
119
+ }
113
120
  if (prevEngine) {
114
121
  safeRemove(container, prevEngine.getImageCanvas());
115
122
  safeRemove(container, prevEngine.getAnnotationsCanvas());
@@ -164,10 +171,16 @@ const AnnotationCanvas = forwardRef(({ image, annotations = [], setAnnotations,
164
171
  useImperativeHandle(ref, () => ({
165
172
  resetImageSize: () => {
166
173
  engineRef.current?.initImageCanvas(true);
167
- }
174
+ },
168
175
  }));
169
176
  /* ---------- Render ---------- */
170
- return (_jsx("div", { ref: containerRef, style: { width: '100%', height: '100%', position: 'relative', flex: 1, outline: 'none' }, onWheel: (e) => engineRef.current?.onWheel(e.nativeEvent), onMouseDown: (e) => engineRef.current?.onMouseDown(e.nativeEvent), onMouseMove: (e) => engineRef.current?.onMouseMove(e.nativeEvent), onMouseUp: (e) => engineRef.current?.onMouseUp(e.nativeEvent), onMouseLeave: (e) => engineRef.current?.onMouseLeave(e.nativeEvent), onContextMenu: (e) => e.preventDefault(), tabIndex: 0, onKeyDown: (e) => {
177
+ return (_jsx("div", { ref: containerRef, style: { width: '100%', height: '100%', position: 'relative', flex: 1, outline: 'none' }, onWheel: (e) => engineRef.current?.onWheel(e.nativeEvent), onMouseDown: (e) => engineRef.current?.onMouseDown(e.nativeEvent), onMouseMove: (e) => {
178
+ clientMousePointRef.current = { x: e.clientX, y: e.clientY };
179
+ engineRef.current?.onMouseMove(e.nativeEvent);
180
+ }, onMouseUp: (e) => engineRef.current?.onMouseUp(e.nativeEvent), onMouseLeave: (e) => {
181
+ clientMousePointRef.current = null;
182
+ engineRef.current?.onMouseLeave(e.nativeEvent);
183
+ }, onContextMenu: (e) => e.preventDefault(), tabIndex: 0, onKeyDown: (e) => {
171
184
  if (e.key === 'Escape' || e.code === 'Escape') {
172
185
  engineRef.current?.onKeyDown(e.nativeEvent);
173
186
  }
@@ -8,5 +8,9 @@ export declare const AnnotationViewer: import("react").ForwardRefExoticComponent
8
8
  drawing: Pick<AnnotationCanvasProps["drawing"], "lineSize" | "applyStyle">;
9
9
  events: Pick<AnnotationCanvasProps["events"], "onImageLoadSuccess" | "onImageLoadError">;
10
10
  } & import("react").RefAttributes<AnnotationCanvasHandle>>;
11
- export type AnnotationEditorProps = AnnotationCanvasProps;
12
- export declare const AnnotationEditor: import("react").ForwardRefExoticComponent<AnnotationCanvasProps & import("react").RefAttributes<AnnotationCanvasHandle>>;
11
+ export type AnnotationEditorProps = AnnotationCanvasProps & {
12
+ keepCrossOnImageChange?: boolean;
13
+ };
14
+ export declare const AnnotationEditor: import("react").ForwardRefExoticComponent<AnnotationCanvasProps & {
15
+ keepCrossOnImageChange?: boolean;
16
+ } & import("react").RefAttributes<AnnotationCanvasHandle>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepnoid/canvas",
3
- "version": "0.1.84",
3
+ "version": "0.1.86",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",
@@ -14,7 +14,8 @@
14
14
  }
15
15
  },
16
16
  "files": [
17
- "dist"
17
+ "dist",
18
+ "skills"
18
19
  ],
19
20
  "scripts": {
20
21
  "build": "npx tsc -p tsconfig.json",
@@ -0,0 +1,57 @@
1
+ # npm 배포 시 스킬 포함 방법
2
+
3
+ ## 1. package.json 설정
4
+
5
+ `package.json`의 `files` 필드에 `skills`를 추가하여 npm 배포 시 스킬이 포함되도록 합니다.
6
+
7
+ ```json
8
+ {
9
+ "name": "@deepnoid/canvas",
10
+ "files": [
11
+ "dist",
12
+ "skills"
13
+ ]
14
+ }
15
+ ```
16
+
17
+ ## 2. 배포
18
+
19
+ ```bash
20
+ # 빌드
21
+ yarn build
22
+
23
+ # 버전 업
24
+ npm version patch # 또는 minor, major
25
+
26
+ # 배포
27
+ npm publish
28
+ ```
29
+
30
+ ## 3. 소비자 프로젝트에서 설치
31
+
32
+ ```bash
33
+ # 패키지 설치
34
+ yarn add @deepnoid/canvas
35
+
36
+ # 스킬 복사 (최초 1회 또는 업데이트 시)
37
+ cp -r node_modules/@deepnoid/canvas/skills/deepnoid-canvas .claude/skills/
38
+ ```
39
+
40
+ ## 4. 확인
41
+
42
+ 배포 후 패키지에 스킬이 포함되었는지 확인:
43
+
44
+ ```bash
45
+ # 패키지 내용 확인
46
+ npm pack --dry-run
47
+ ```
48
+
49
+ 출력에 `skills/deepnoid-canvas/SKILL.md` 등이 포함되어야 합니다.
50
+
51
+ ## 5. 스킬 업데이트 시
52
+
53
+ 라이브러리 API가 변경되면 스킬도 함께 업데이트해야 합니다:
54
+
55
+ 1. `skills/deepnoid-canvas/` 내 관련 파일 수정
56
+ 2. 패키지 버전 업 후 배포
57
+ 3. 소비자 프로젝트에서 `cp -r` 명령으로 스킬 재복사
@@ -0,0 +1,56 @@
1
+ # @deepnoid/canvas Skill
2
+
3
+ `@deepnoid/canvas` 라이브러리의 Claude Code용 지식 패키지. 소비자 프로젝트에서 Claude Code가 Canvas 어노테이션 API를 정확하게 사용할 수 있도록 합니다.
4
+
5
+ ## 설치
6
+
7
+ ```bash
8
+ # 1. 패키지 설치
9
+ yarn add @deepnoid/canvas
10
+
11
+ # 2. 스킬 복사
12
+ cp -r node_modules/@deepnoid/canvas/skills/deepnoid-canvas .claude/skills/
13
+ ```
14
+
15
+ ## 기능
16
+
17
+ - **SKILL.md**: 핵심 원칙, 컴포넌트/모드 선택 가이드, 기본 사용 패턴
18
+ - **rules/**: 올바른 사용법 vs 잘못된 사용법 코드 예시
19
+ - `rendering.md` — applyStyle 렌더링 규칙
20
+ - `annotations.md` — 어노테이션 데이터 구조 규칙
21
+ - `events.md` — 이벤트 핸들러, 단축키 규칙
22
+ - `layout.md` — 캔버스 레이아웃, 줌/팬 규칙
23
+ - **references/**: 완전한 API 레퍼런스
24
+ - `components.md` — AnnotationEditor, AnnotationViewer Props
25
+ - `types.md` — Annotation, DrawMode, Label 등 전체 타입
26
+ - `apply-style.md` — applyStyle 함수 상세 가이드 + 3가지 실전 예제
27
+ - **setup.md**: 소비자 프로젝트 초기 설정 가이드
28
+ - **DEPLOY.md**: npm 배포 시 스킬 포함 방법
29
+
30
+ ## 파일 구조
31
+
32
+ ```
33
+ skills/deepnoid-canvas/
34
+ ├── SKILL.md ← 핵심 진입점 (트리거 조건 포함)
35
+ ├── README.md ← 이 파일
36
+ ├── DEPLOY.md ← npm 배포 방식 안내
37
+ ├── setup.md ← 소비자 프로젝트 설정 가이드
38
+ ├── rules/
39
+ │ ├── rendering.md ← applyStyle 렌더링 규칙
40
+ │ ├── annotations.md ← 어노테이션 데이터 규칙
41
+ │ ├── events.md ← 이벤트/단축키 규칙
42
+ │ └── layout.md ← 레이아웃/줌/팬 규칙
43
+ └── references/
44
+ ├── components.md ← 컴포넌트 Props 레퍼런스
45
+ ├── types.md ← 타입 레퍼런스
46
+ └── apply-style.md ← applyStyle 함수 레퍼런스
47
+ ```
48
+
49
+ ## 자동 트리거
50
+
51
+ 이 스킬은 다음 상황에서 Claude Code에 의해 자동 활성화됩니다:
52
+
53
+ - import 경로에 `@deepnoid/canvas`가 포함될 때
54
+ - `AnnotationEditor`, `AnnotationViewer`, `DrawMode`, `ApplyAnnotationStyle` 키워드 사용 시
55
+ - Canvas 위 이미지 어노테이션(bounding box, polygon) 관련 질문 시
56
+ - `applyStyle` 콜백 함수를 작성하거나 수정할 때
@@ -0,0 +1,131 @@
1
+ ---
2
+ name: deepnoid-canvas
3
+ description: >
4
+ @deepnoid/canvas 라이브러리의 완전한 사용 가이드.
5
+ 트리거 조건: import 경로에 '@deepnoid/canvas'가 포함될 때,
6
+ AnnotationEditor/AnnotationViewer/DrawMode/ApplyAnnotationStyle 키워드가 사용될 때,
7
+ Canvas 위 이미지 어노테이션(bounding box, polygon) 관련 질문이 있을 때,
8
+ applyStyle 콜백 함수를 작성하거나 수정할 때 자동 활성화.
9
+ ---
10
+
11
+ # @deepnoid/canvas 스킬
12
+
13
+ Canvas 기반 이미지 어노테이션 React 라이브러리. 이미지 위에 사각형(BBox)/폴리곤 어노테이션을 그리고, 편집하고, 표시하는 기능을 제공합니다.
14
+
15
+ ## 핵심 원칙
16
+
17
+ 1. **applyStyle은 필수**: 모든 어노테이션의 시각적 표현은 소비자가 `applyStyle` 콜백으로 완전 제어합니다. 라이브러리는 스타일을 자체 적용하지 않습니다.
18
+ 2. **컨테이너 크기 필수**: `AnnotationEditor`/`AnnotationViewer`를 감싸는 부모 `div`에 반드시 고정된 width/height (또는 flex: 1)를 설정해야 합니다.
19
+ 3. **zoom 보정 필수**: `applyStyle` 내에서 `lineWidth`, `font size`, `setLineDash` 등의 값은 반드시 `/ zoom`으로 나누어야 줌 레벨에 관계없이 일정하게 보입니다.
20
+ 4. **Editor vs Viewer 분리**: 편집이 필요하면 `AnnotationEditor`, 읽기 전용이면 `AnnotationViewer`를 사용합니다. 두 컴포넌트의 props가 다릅니다.
21
+ 5. **좌표는 원본 이미지 기준**: 모든 어노테이션 좌표(x, y, width, height, points)는 원본 이미지의 픽셀 좌표입니다. 캔버스 표시 좌표와 다릅니다.
22
+
23
+ ## Critical Rules
24
+
25
+ | 규칙 파일 | 언제 읽어야 하는지 |
26
+ |-----------|-------------------|
27
+ | [rules/rendering.md](rules/rendering.md) | applyStyle 함수를 작성하거나 수정할 때 |
28
+ | [rules/annotations.md](rules/annotations.md) | 어노테이션 데이터를 생성/변환하거나 상태를 관리할 때 |
29
+ | [rules/events.md](rules/events.md) | 이벤트 핸들러, 단축키, annotationSelected 연동 시 |
30
+ | [rules/layout.md](rules/layout.md) | 캔버스 컨테이너 레이아웃, 줌/팬, 리사이즈 관련 작업 시 |
31
+
32
+ ## 컴포넌트 선택 가이드
33
+
34
+ | 상황 | 사용할 컴포넌트 | 설명 |
35
+ |------|----------------|------|
36
+ | 어노테이션 생성/편집 필요 | `AnnotationEditor` | drawing.mode로 그리기 모드 설정 |
37
+ | 읽기 전용 표시만 필요 | `AnnotationViewer` | mode 불필요, 편집 비활성 |
38
+ | ref로 캔버스 제어 필요 | 둘 다 지원 | `AnnotationCanvasHandle` ref 사용 |
39
+
40
+ ## 그리기 모드 선택 가이드
41
+
42
+ | 상황 | DrawMode | 설명 |
43
+ |------|----------|------|
44
+ | 바운딩 박스 (사각형) | `DrawMode.RECTANGLE` | 드래그로 사각형 그리기 |
45
+ | 다각형 영역 | `DrawMode.POLYGON` | 클릭으로 점 찍기, 더블클릭/시작점 클릭으로 완성 |
46
+
47
+ ## 핵심 사용 패턴
48
+
49
+ ### 기본 Editor 사용
50
+
51
+ ```tsx
52
+ import { useState } from 'react';
53
+ import { AnnotationEditor, DrawMode } from '@deepnoid/canvas';
54
+ import type { Annotation, ApplyAnnotationStyle } from '@deepnoid/canvas';
55
+
56
+ const applyStyle: ApplyAnnotationStyle = ({ variant, context, annotation, drawLineSize, zoom }) => {
57
+ if (variant === 'drawRect') {
58
+ context.strokeStyle = annotation.selected ? '#00FF00' : '#FF4136';
59
+ context.lineWidth = drawLineSize / zoom;
60
+ if (annotation.type === DrawMode.RECTANGLE) {
61
+ context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
62
+ } else if (annotation.type === DrawMode.POLYGON) {
63
+ context.beginPath();
64
+ annotation.points.forEach((p, i) => i === 0 ? context.moveTo(p.x, p.y) : context.lineTo(p.x, p.y));
65
+ context.closePath();
66
+ context.stroke();
67
+ }
68
+ }
69
+ if (variant === 'drawText') {
70
+ if (annotation.label) {
71
+ context.fillStyle = '#FF4136';
72
+ context.font = `bold ${14 / zoom}px sans-serif`;
73
+ const tx = annotation.type === DrawMode.RECTANGLE ? annotation.x : Math.min(...annotation.points.map(p => p.x));
74
+ const ty = annotation.type === DrawMode.RECTANGLE ? annotation.y - 5 : Math.min(...annotation.points.map(p => p.y)) - 5;
75
+ context.fillText(annotation.label.name, tx, ty);
76
+ }
77
+ }
78
+ };
79
+
80
+ function MyEditor() {
81
+ const [annotations, setAnnotations] = useState<Annotation[]>([]);
82
+
83
+ return (
84
+ <div style={{ width: 800, height: 600 }}>
85
+ <AnnotationEditor
86
+ image="https://example.com/image.jpg"
87
+ annotations={annotations}
88
+ setAnnotations={setAnnotations}
89
+ drawing={{
90
+ mode: DrawMode.RECTANGLE,
91
+ color: '#FF4136',
92
+ lineSize: 2,
93
+ applyStyle,
94
+ }}
95
+ events={{}}
96
+ enableHotkeys
97
+ />
98
+ </div>
99
+ );
100
+ }
101
+ ```
102
+
103
+ ### 기본 Viewer 사용
104
+
105
+ ```tsx
106
+ import { AnnotationViewer } from '@deepnoid/canvas';
107
+
108
+ <div style={{ width: 800, height: 600 }}>
109
+ <AnnotationViewer
110
+ image="https://example.com/image.jpg"
111
+ annotations={annotations}
112
+ drawing={{ lineSize: 2, applyStyle }}
113
+ events={{}}
114
+ options={{ panZoomEnabled: true }}
115
+ />
116
+ </div>
117
+ ```
118
+
119
+ ## API 레퍼런스
120
+
121
+ 상세 API 문서는 [references/](references/) 디렉토리를 참조하세요.
122
+
123
+ | 레퍼런스 파일 | 내용 |
124
+ |--------------|------|
125
+ | [references/components.md](references/components.md) | AnnotationEditor, AnnotationViewer 전체 Props |
126
+ | [references/types.md](references/types.md) | Annotation, DrawMode, Label 등 모든 타입 |
127
+ | [references/apply-style.md](references/apply-style.md) | applyStyle 함수 시그니처, variant별 가이드, 예제 |
128
+
129
+ ## 초기 설정
130
+
131
+ 소비자 프로젝트에서의 설치 및 설정 방법은 [setup.md](setup.md)를 참조하세요.
@@ -0,0 +1,189 @@
1
+ # applyStyle 함수 레퍼런스
2
+
3
+ ## 시그니처
4
+
5
+ ```typescript
6
+ import type { ApplyAnnotationStyle } from '@deepnoid/canvas';
7
+
8
+ const applyStyle: ApplyAnnotationStyle = (params) => {
9
+ const { variant, context, annotation, drawLineSize, zoom } = params;
10
+ // ...
11
+ };
12
+ ```
13
+
14
+ ## 파라미터 상세
15
+
16
+ | 파라미터 | 타입 | 설명 |
17
+ |----------|------|------|
18
+ | `variant` | `'drawRect' \| 'drawText'` | 현재 렌더링 단계 |
19
+ | `context` | `CanvasRenderingContext2D` | Canvas 2D 그리기 컨텍스트 |
20
+ | `annotation` | `Annotation` | 렌더링 대상 어노테이션 (type으로 판별 가능) |
21
+ | `drawLineSize` | `number` | 설정된 선 두께 (현재 2px 고정 전달) |
22
+ | `zoom` | `number` | 현재 줌 배율 (1 = 100%) |
23
+
24
+ ## 호출 순서
25
+
26
+ 엔진이 각 어노테이션에 대해 다음 순서로 `applyStyle`을 호출합니다:
27
+
28
+ 1. `variant: 'drawRect'` — 도형 외곽선/채우기 렌더링
29
+ 2. `variant: 'drawText'` — 라벨 텍스트 렌더링
30
+
31
+ **중요**: 두 variant 모두에서 실제 Canvas2D 그리기 API(`strokeRect`, `fillText` 등)를 소비자가 직접 호출해야 합니다.
32
+
33
+ ## 예제 1: 기본 스타일 (Fense 스타일)
34
+
35
+ color 필드에 따라 다른 색상으로 도형을 그리는 패턴.
36
+
37
+ ```typescript
38
+ import { DrawMode } from '@deepnoid/canvas';
39
+ import type { ApplyAnnotationStyle } from '@deepnoid/canvas';
40
+
41
+ const fenseStyle: ApplyAnnotationStyle = ({ variant, context, annotation, zoom, drawLineSize }) => {
42
+ if (variant === 'drawRect') {
43
+ // color 필드로 색상 결정
44
+ const colorMap: Record<string, string> = {
45
+ danger: 'rgba(255, 70, 132, 1)',
46
+ warning: 'rgba(237, 164, 16, 1)',
47
+ success: 'rgba(36, 162, 91, 1)',
48
+ };
49
+ context.strokeStyle = colorMap[annotation.color ?? 'danger'] ?? 'rgba(255, 70, 132, 1)';
50
+ context.lineWidth = drawLineSize * (1 / zoom);
51
+
52
+ if (annotation.type === DrawMode.RECTANGLE) {
53
+ const { x, y, width, height } = annotation;
54
+ context.strokeRect(x, y, width, height);
55
+ } else if (annotation.type === DrawMode.POLYGON) {
56
+ context.beginPath();
57
+ annotation.points.forEach((p, idx) => {
58
+ if (idx === 0) context.moveTo(p.x, p.y);
59
+ else context.lineTo(p.x, p.y);
60
+ });
61
+ context.closePath();
62
+ context.stroke();
63
+ }
64
+ }
65
+ // drawText는 생략 — 라벨 표시 불필요한 경우
66
+ };
67
+ ```
68
+
69
+ ## 예제 2: 고급 스타일 (Seed 스타일)
70
+
71
+ 선택 상태에 따른 색상 변경, 반투명 채우기, 십자 마커, 라벨 텍스트를 포함한 풀 스타일.
72
+
73
+ ```typescript
74
+ const seedStyle: ApplyAnnotationStyle = ({ variant, context, annotation, zoom, drawLineSize }) => {
75
+ const DEFAULT_COLOR = '#A020F0';
76
+ const ACTIVE_COLOR = '#FF4136';
77
+ const baseColor = annotation.selected ? ACTIVE_COLOR : DEFAULT_COLOR;
78
+
79
+ if (variant === 'drawRect') {
80
+ // 스타일 설정
81
+ context.strokeStyle = `${baseColor}cc`; // 80% 불투명
82
+ context.fillStyle = `${baseColor}1a`; // 10% 불투명
83
+ context.lineWidth = drawLineSize * (1 / zoom);
84
+
85
+ if (annotation.type === DrawMode.RECTANGLE) {
86
+ const { x, y, width, height } = annotation;
87
+ context.strokeRect(x, y, width, height);
88
+ context.fillRect(x, y, width, height);
89
+
90
+ // 중심 십자 마커
91
+ const cx = x + width / 2;
92
+ const cy = y + height / 2;
93
+ context.beginPath();
94
+ context.moveTo(cx, cy - 10);
95
+ context.lineTo(cx, cy + 10);
96
+ context.stroke();
97
+ context.beginPath();
98
+ context.moveTo(cx - 10, cy);
99
+ context.lineTo(cx + 10, cy);
100
+ context.stroke();
101
+ } else if (annotation.type === DrawMode.POLYGON) {
102
+ context.beginPath();
103
+ annotation.points.forEach((p, idx) => {
104
+ if (idx === 0) context.moveTo(p.x, p.y);
105
+ else context.lineTo(p.x, p.y);
106
+ });
107
+ context.closePath();
108
+ context.stroke();
109
+ context.fill();
110
+ }
111
+ }
112
+
113
+ if (variant === 'drawText') {
114
+ if (annotation.label) {
115
+ context.fillStyle = baseColor;
116
+ context.font = `bold ${28 / zoom}px arial`;
117
+
118
+ let textX = 0, textY = 0;
119
+ if (annotation.type === DrawMode.RECTANGLE) {
120
+ textX = annotation.x;
121
+ textY = annotation.y - 5;
122
+ } else if (annotation.type === DrawMode.POLYGON && annotation.points.length > 0) {
123
+ textX = Math.min(...annotation.points.map(p => p.x));
124
+ textY = Math.min(...annotation.points.map(p => p.y)) - 5;
125
+ }
126
+ context.fillText(annotation.label.name, textX, textY);
127
+ }
128
+ }
129
+ };
130
+ ```
131
+
132
+ ## 예제 3: 선택 상태 강조 (점선 + 글로우)
133
+
134
+ ```typescript
135
+ const highlightStyle: ApplyAnnotationStyle = ({ variant, context, annotation, drawLineSize, zoom }) => {
136
+ if (variant === 'drawRect') {
137
+ const color = '#00BFFF';
138
+ context.strokeStyle = color;
139
+ context.lineWidth = drawLineSize / zoom;
140
+
141
+ // 선택 시 점선 + 그림자 효과
142
+ if (annotation.selected) {
143
+ context.setLineDash([6 / zoom, 3 / zoom]);
144
+ context.shadowColor = color;
145
+ context.shadowBlur = 8 / zoom;
146
+ } else {
147
+ context.setLineDash([]);
148
+ context.shadowColor = 'transparent';
149
+ context.shadowBlur = 0;
150
+ }
151
+
152
+ if (annotation.type === DrawMode.RECTANGLE) {
153
+ context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
154
+ } else if (annotation.type === DrawMode.POLYGON) {
155
+ context.beginPath();
156
+ annotation.points.forEach((p, i) => i === 0 ? context.moveTo(p.x, p.y) : context.lineTo(p.x, p.y));
157
+ context.closePath();
158
+ context.stroke();
159
+ }
160
+
161
+ // 그림자 초기화 (다음 어노테이션에 영향 방지)
162
+ context.shadowColor = 'transparent';
163
+ context.shadowBlur = 0;
164
+ }
165
+
166
+ if (variant === 'drawText') {
167
+ if (annotation.label) {
168
+ context.setLineDash([]); // 텍스트에 dash 적용 방지
169
+ context.fillStyle = '#00BFFF';
170
+ context.font = `${14 / zoom}px sans-serif`;
171
+ const tx = annotation.type === DrawMode.RECTANGLE ? annotation.x : Math.min(...annotation.points.map(p => p.x));
172
+ const ty = (annotation.type === DrawMode.RECTANGLE ? annotation.y : Math.min(...annotation.points.map(p => p.y))) - 5;
173
+ context.fillText(annotation.label.name, tx, ty);
174
+ }
175
+ }
176
+ };
177
+ ```
178
+
179
+ ## 체크리스트
180
+
181
+ applyStyle 함수 작성 시 확인할 사항:
182
+
183
+ - [ ] `variant === 'drawRect'`와 `variant === 'drawText'` 분기 처리
184
+ - [ ] `annotation.type`으로 RECTANGLE/POLYGON 분기 처리
185
+ - [ ] `lineWidth`, `font`, `setLineDash` 등에 `/ zoom` 보정 적용
186
+ - [ ] `setLineDash` 사용 후 `context.setLineDash([])` 초기화
187
+ - [ ] `shadowBlur` 사용 후 초기화
188
+ - [ ] `annotation.color` 필드를 직접 CSS 색상으로 사용하지 않음 (매핑 필요)
189
+ - [ ] RECTANGLE과 POLYGON 모두에서 실제 도형 그리기 API 호출