@deepnoid/canvas 0.1.58 → 0.1.60
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 +202 -3
- package/dist/engine/annotation/annotationTypes.d.ts +21 -0
- package/dist/engine/annotation/annotationTypes.js +6 -0
- package/dist/engine/annotation/rectangle/rectangleController.d.ts +26 -0
- package/dist/engine/annotation/rectangle/rectangleController.js +115 -0
- package/dist/engine/annotation/rectangle/rectangleHitTest.d.ts +7 -0
- package/dist/engine/annotation/rectangle/rectangleHitTest.js +102 -0
- package/dist/engine/annotation/rectangle/rectangleInteraction.d.ts +17 -0
- package/dist/engine/annotation/rectangle/rectangleInteraction.js +30 -0
- package/dist/engine/annotation/rectangle/rectangleMath.d.ts +10 -0
- package/dist/engine/annotation/rectangle/rectangleMath.js +29 -0
- package/dist/engine/annotation/rectangle/rectangleRenderer.d.ts +5 -0
- package/dist/engine/annotation/rectangle/rectangleRenderer.js +88 -0
- package/dist/engine/annotation/rectangle/rectangleTransform.d.ts +14 -0
- package/dist/engine/annotation/rectangle/rectangleTransform.js +65 -0
- package/dist/{enum/common.d.ts → engine/annotation/rectangle/rectangleTypes.d.ts} +0 -3
- package/dist/{enum/common.js → engine/annotation/rectangle/rectangleTypes.js} +0 -4
- package/dist/engine/history.d.ts +11 -0
- package/dist/{components/AnnotationCanvas/_utils/createHistory.js → engine/history.js} +4 -4
- package/dist/engine/interaction/drawModeRouter.d.ts +3 -0
- package/dist/engine/interaction/drawModeRouter.js +56 -0
- package/dist/engine/interaction/interactionController.d.ts +13 -0
- package/dist/engine/interaction/interactionController.js +53 -0
- package/dist/engine/interaction/interface.d.ts +15 -0
- package/dist/engine/interaction/panZoomInteraction.d.ts +3 -0
- package/dist/engine/interaction/panZoomInteraction.js +29 -0
- package/dist/engine/interaction/pointerInteraction.d.ts +16 -0
- package/dist/engine/interaction/pointerInteraction.js +48 -0
- package/dist/engine/pan-zoom/panZoomController.d.ts +26 -0
- package/dist/engine/pan-zoom/panZoomController.js +148 -0
- package/dist/engine/pan-zoom/panZoomUtils.d.ts +10 -0
- package/dist/engine/pan-zoom/panZoomUtils.js +24 -0
- package/dist/engine/public/annotationEngine.d.ts +75 -0
- package/dist/engine/public/annotationEngine.js +263 -0
- package/dist/engine/renderer/drawCross.d.ts +2 -0
- package/dist/engine/renderer/drawCross.js +19 -0
- package/dist/engine/renderer/interface.d.ts +6 -0
- package/dist/engine/renderer/interface.js +1 -0
- package/dist/{types/index.d.ts → engine/types.d.ts} +12 -21
- package/dist/engine/types.js +1 -0
- package/dist/engine/utils/mousePoint.d.ts +3 -0
- package/dist/engine/utils/mousePoint.js +52 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +2 -2
- package/dist/{components/AnnotationCanvas/index.d.ts → react/AnnotationCanvas.d.ts} +7 -6
- package/dist/react/AnnotationCanvas.js +110 -0
- package/dist/{components → react}/index.d.ts +1 -1
- package/dist/{components → react}/index.js +1 -1
- package/package.json +1 -1
- package/dist/components/AnnotationCanvas/_hooks/useImagePanZoom.d.ts +0 -24
- package/dist/components/AnnotationCanvas/_hooks/useImagePanZoom.js +0 -143
- package/dist/components/AnnotationCanvas/_utils/createHistory.d.ts +0 -11
- package/dist/components/AnnotationCanvas/_utils/panZoom.d.ts +0 -10
- package/dist/components/AnnotationCanvas/_utils/panZoom.js +0 -29
- package/dist/components/AnnotationCanvas/index.js +0 -96
- package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangle.d.ts +0 -5
- package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangle.js +0 -88
- package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangleUtils.d.ts +0 -28
- package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangleUtils.js +0 -204
- package/dist/components/AnnotationLayer/_hooks/drawEvents/useDrawEvents.d.ts +0 -25
- package/dist/components/AnnotationLayer/_hooks/drawEvents/useDrawEvents.js +0 -43
- package/dist/components/AnnotationLayer/_hooks/useCanvasDraw.d.ts +0 -13
- package/dist/components/AnnotationLayer/_hooks/useCanvasDraw.js +0 -115
- package/dist/components/AnnotationLayer/index.d.ts +0 -14
- package/dist/components/AnnotationLayer/index.js +0 -122
- package/dist/utils/canvas.d.ts +0 -3
- package/dist/utils/canvas.js +0 -37
- package/dist/utils/pointTransform.d.ts +0 -2
- package/dist/utils/pointTransform.js +0 -46
- /package/dist/{types/index.js → engine/interaction/interface.js} +0 -0
- /package/dist/{utils/common → engine/utils}/cloneDeep.d.ts +0 -0
- /package/dist/{utils/common → engine/utils}/cloneDeep.js +0 -0
- /package/dist/{utils/common → engine/utils}/deepEqual.d.ts +0 -0
- /package/dist/{utils/common → engine/utils}/deepEqual.js +0 -0
- /package/dist/{utils/common → engine/utils}/isEqualWith.d.ts +0 -0
- /package/dist/{utils/common → engine/utils}/isEqualWith.js +0 -0
- /package/dist/{utils → engine/utils}/mouseActions.d.ts +0 -0
- /package/dist/{utils → engine/utils}/mouseActions.js +0 -0
- /package/dist/{hooks → react/hooks}/useDebounce.d.ts +0 -0
- /package/dist/{hooks → react/hooks}/useDebounce.js +0 -0
- /package/dist/{components/AnnotationLayer/_hooks → react/hooks}/useHotkeys.d.ts +0 -0
- /package/dist/{components/AnnotationLayer/_hooks → react/hooks}/useHotkeys.js +0 -0
- /package/dist/{hooks → react/hooks}/useResizeObserver.d.ts +0 -0
- /package/dist/{hooks → react/hooks}/useResizeObserver.js +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,205 @@
|
|
|
1
1
|
# deepnoid-canvas
|
|
2
2
|
|
|
3
|
-
이 프로젝트는 본부 내 프로젝트에서 사용되는 공통 CANVAS
|
|
3
|
+
이 프로젝트는 본부 내 프로젝트에서 사용되는 공통 CANVAS 라이브러리입니다.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
## 개요
|
|
6
|
+
|
|
7
|
+
Canvas 기반 이미지 annotation 라이브러리로, **TypeScript Engine + React Host** 구조로 설계되었습니다.
|
|
8
|
+
|
|
9
|
+
- **Engine (TS)**: Canvas 조작, 상태 관리, 이벤트 처리 등 핵심 로직
|
|
10
|
+
- **React**: UI 렌더링, DOM ref 관리, Engine 생명주기 관리
|
|
11
|
+
|
|
12
|
+
## 주요 기능
|
|
13
|
+
|
|
14
|
+
- ✏️ Rectangle annotation 그리기/편집
|
|
15
|
+
- 👁️ Viewer 모드 지원 (읽기 전용)
|
|
16
|
+
- 🔍 Pan & Zoom 지원
|
|
17
|
+
- ↩️ Undo/Redo 기능
|
|
18
|
+
- ⌨️ 단축키 지원
|
|
19
|
+
- 🎨 커스터마이징 가능한 스타일링
|
|
20
|
+
|
|
21
|
+
## 빠른 시작
|
|
22
|
+
|
|
23
|
+
### 설치
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install deepnoid-canvas
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 기본 사용법
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { AnnotationEditor } from 'deepnoid-canvas';
|
|
33
|
+
|
|
34
|
+
function App() {
|
|
35
|
+
const [annotations, setAnnotations] = useState([]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<AnnotationEditor
|
|
39
|
+
image='https://example.com/image.jpg'
|
|
40
|
+
annotations={annotations}
|
|
41
|
+
setAnnotations={setAnnotations}
|
|
42
|
+
drawing={{
|
|
43
|
+
mode: 'RECTANGLE',
|
|
44
|
+
color: '#FF4136',
|
|
45
|
+
lineSize: 2,
|
|
46
|
+
}}
|
|
47
|
+
options={{
|
|
48
|
+
panZoomEnabled: true,
|
|
49
|
+
zoom: { min: 0.5, max: 4, step: 0.9 },
|
|
50
|
+
}}
|
|
51
|
+
enableHotkeys
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Props
|
|
58
|
+
|
|
59
|
+
### AnnotationEditor
|
|
60
|
+
|
|
61
|
+
Editor 모드로 annotation을 생성하고 편집할 수 있습니다.
|
|
62
|
+
|
|
63
|
+
| Prop | Type | Required | Description |
|
|
64
|
+
| ---------------- | ------------------------------------- | -------- | -------------------------------- |
|
|
65
|
+
| `image` | `string` | ✅ | 이미지 URL |
|
|
66
|
+
| `drawing` | `AnnotationCanvasDrawing` | ✅ | 그리기 모드 및 스타일 설정 |
|
|
67
|
+
| `annotations` | `Annotation[]` | - | Annotation 목록 |
|
|
68
|
+
| `setAnnotations` | `(annotations: Annotation[]) => void` | - | Annotation 변경 시 호출되는 콜백 |
|
|
69
|
+
| `options` | `AnnotationCanvasOptions` | - | 줌/팬 및 기타 옵션 |
|
|
70
|
+
| `events` | `AnnotationCanvasEvents` | - | 이미지 로드 관련 이벤트 핸들러 |
|
|
71
|
+
| `enableHotkeys` | `boolean` | - | 단축키 활성화 여부 |
|
|
72
|
+
|
|
73
|
+
### AnnotationViewer
|
|
74
|
+
|
|
75
|
+
Viewer 모드로 annotation을 읽기 전용으로 표시합니다.
|
|
76
|
+
|
|
77
|
+
| Prop | Type | Required | Description |
|
|
78
|
+
| --------------- | -------------------------------------------------------------------------- | -------- | ------------------------------ |
|
|
79
|
+
| `image` | `string` | ✅ | 이미지 URL |
|
|
80
|
+
| `drawing` | `Pick<AnnotationCanvasDrawing, 'lineSize' \| 'applyStyle'>` | ✅ | 렌더링 스타일 설정 (mode 제외) |
|
|
81
|
+
| `annotations` | `Annotation[]` | - | 표시할 Annotation 목록 |
|
|
82
|
+
| `options` | `AnnotationCanvasOptions` | - | 줌/팬 및 기타 옵션 |
|
|
83
|
+
| `events` | `Pick<AnnotationCanvasEvents, 'onImageLoadSuccess' \| 'onImageLoadError'>` | - | 이미지 로드 관련 이벤트 핸들러 |
|
|
84
|
+
| `enableHotkeys` | `boolean` | - | 단축키 활성화 여부 |
|
|
85
|
+
|
|
86
|
+
```jsx
|
|
87
|
+
import { AnnotationViewer } from 'deepnoid-canvas';
|
|
88
|
+
|
|
89
|
+
function App() {
|
|
90
|
+
const [annotations] = useState([{ type: 'RECTANGLE', x: 100, y: 100, width: 200, height: 150 }]);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<AnnotationViewer
|
|
94
|
+
image='https://example.com/image.jpg'
|
|
95
|
+
annotations={annotations}
|
|
96
|
+
drawing={{
|
|
97
|
+
lineSize: 2,
|
|
98
|
+
applyStyle: (params) => {
|
|
99
|
+
// 커스텀 스타일 적용
|
|
100
|
+
},
|
|
101
|
+
}}
|
|
102
|
+
options={{
|
|
103
|
+
panZoomEnabled: true,
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Drawing 설정
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
type AnnotationCanvasDrawing = {
|
|
114
|
+
mode?: 'RECTANGLE' | 'POLYGON' | 'NONE'; // 그리기 모드 (Editor 전용)
|
|
115
|
+
color?: string; // Annotation 색상 (HEX)
|
|
116
|
+
lineSize: number; // 선 두께
|
|
117
|
+
label?: Label; // Annotation에 표시할 라벨
|
|
118
|
+
applyStyle: ApplyAnnotationStyle; // 커스텀 스타일 함수
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type Label = {
|
|
122
|
+
id: number;
|
|
123
|
+
name: string;
|
|
124
|
+
type: string;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
type ApplyAnnotationStyle = (params: {
|
|
128
|
+
variant: 'drawRect' | 'drawText';
|
|
129
|
+
context: CanvasRenderingContext2D;
|
|
130
|
+
annotation: Annotation;
|
|
131
|
+
drawLineSize: number;
|
|
132
|
+
zoom: number;
|
|
133
|
+
}) => void;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Options 설정
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
type AnnotationCanvasOptions = {
|
|
140
|
+
panZoomEnabled?: boolean; // 줌/팬 기능 활성화 (기본: false)
|
|
141
|
+
zoom?: {
|
|
142
|
+
min?: number; // 최소 줌 배율 (기본: 0.1)
|
|
143
|
+
max?: number; // 최대 줌 배율 (기본: Infinity)
|
|
144
|
+
step?: number; // 줌 단계 (기본: 0.9)
|
|
145
|
+
};
|
|
146
|
+
ZoomButton?: ComponentType; // 커스텀 줌 버튼 컴포넌트
|
|
147
|
+
resetOnImageChange?: boolean; // 이미지 변경 시 줌 리셋 여부
|
|
148
|
+
};
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Events 설정
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
type AnnotationCanvasEvents = {
|
|
155
|
+
onImageLoadSuccess?: () => void; // 이미지 로드 성공 시
|
|
156
|
+
onImageLoadError?: (error: Error) => void; // 이미지 로드 실패 시
|
|
157
|
+
};
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 단축키
|
|
161
|
+
|
|
162
|
+
| 키 | 기능 |
|
|
163
|
+
| -------------- | ----------------------------- |
|
|
164
|
+
| `Delete` | 선택한 annotation 삭제 |
|
|
165
|
+
| `Ctrl+Z` | Undo |
|
|
166
|
+
| `Ctrl+Shift+Z` | Redo |
|
|
167
|
+
| `X` | 선택한 annotation만 보기 토글 |
|
|
168
|
+
|
|
169
|
+
## 아키텍처
|
|
170
|
+
|
|
171
|
+
### Facade 패턴
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
React Component
|
|
175
|
+
↓
|
|
176
|
+
AnnotationEngine (Facade)
|
|
177
|
+
↓ ↓ ↓ ↓ ↓
|
|
178
|
+
│ │ │ │ └─ History (Undo/Redo)
|
|
179
|
+
│ │ │ └─── PanZoomController
|
|
180
|
+
│ │ └───── InteractionController
|
|
181
|
+
│ └─────── Renderers
|
|
182
|
+
└───────── Annotation Controllers
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 디렉토리 구조
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
src/
|
|
189
|
+
├─ engine/ # TypeScript Engine (순수 로직)
|
|
190
|
+
│ ├─ annotation/ # Annotation 타입별 로직
|
|
191
|
+
│ ├─ interaction/ # 이벤트 처리
|
|
192
|
+
│ ├─ pan-zoom/ # 줌/이동 기능
|
|
193
|
+
│ ├─ public/ # 외부 공개 API
|
|
194
|
+
│ └─ renderer/ # Canvas 렌더링
|
|
195
|
+
│
|
|
196
|
+
└─ react/ # React 컴포넌트
|
|
197
|
+
├─ hooks/
|
|
198
|
+
└─ AnnotationCanvas.tsx
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## 설계 원칙
|
|
202
|
+
|
|
203
|
+
1. **관심사 분리**: Engine(로직) ↔ React(UI) 완전 분리
|
|
204
|
+
2. **확장성**: 새로운 annotation 타입 추가 용이
|
|
205
|
+
3. **성능 최적화**: 레이어별 redraw 제어
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Label } from '../types';
|
|
2
|
+
export declare enum DrawMode {
|
|
3
|
+
RECTANGLE = "RECTANGLE"
|
|
4
|
+
}
|
|
5
|
+
export type AnnotationBase = {
|
|
6
|
+
label?: Label;
|
|
7
|
+
type?: DrawMode;
|
|
8
|
+
color?: 'success' | 'warning' | 'danger';
|
|
9
|
+
selected?: boolean;
|
|
10
|
+
showOnlySelected?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type Rectangle = {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
18
|
+
export type RectangleAnnotation = AnnotationBase & {
|
|
19
|
+
type: DrawMode.RECTANGLE;
|
|
20
|
+
} & Rectangle;
|
|
21
|
+
export type Annotation = RectangleAnnotation;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AnnotationEngine } from '../../public/annotationEngine';
|
|
2
|
+
import { MouseAction } from '../../utils/mouseActions';
|
|
3
|
+
import { PointState } from '../../types';
|
|
4
|
+
import { RectangleAnchor } from './rectangleTypes';
|
|
5
|
+
export declare class RectangleController {
|
|
6
|
+
private readonly engine;
|
|
7
|
+
private mouseAction;
|
|
8
|
+
private mousePoint;
|
|
9
|
+
private prevMousePoint;
|
|
10
|
+
private startMousePoint;
|
|
11
|
+
private rectangleAnchor;
|
|
12
|
+
private needsRedraw;
|
|
13
|
+
constructor(engine: AnnotationEngine);
|
|
14
|
+
getRenderState(): {
|
|
15
|
+
mousePoint: PointState;
|
|
16
|
+
startMousePoint: PointState | null;
|
|
17
|
+
mouseAction: MouseAction | null;
|
|
18
|
+
anchor: RectangleAnchor | null;
|
|
19
|
+
};
|
|
20
|
+
shouldRedraw(): boolean;
|
|
21
|
+
begin(event: MouseEvent): true | undefined;
|
|
22
|
+
move(event: MouseEvent): true | undefined;
|
|
23
|
+
end(event: MouseEvent): true | undefined;
|
|
24
|
+
leave(event: MouseEvent): boolean;
|
|
25
|
+
private reset;
|
|
26
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { cloneDeep } from '../../utils/cloneDeep';
|
|
2
|
+
import { isMouseClickAction, isMouseDragAction, MouseAction } from '../../utils/mouseActions';
|
|
3
|
+
import { isInsideImage, clampBoundingBoxToImage, ACTIVE_POINT_SIZE } from './rectangleMath';
|
|
4
|
+
import { getCanvasMousePoint } from '../../utils/mousePoint';
|
|
5
|
+
import { selectRectangleAtPoint, updateActiveRectangleAnchor } from './rectangleHitTest';
|
|
6
|
+
import { applyRectangleMove, applyRectangleResize } from './rectangleTransform';
|
|
7
|
+
import { DrawMode } from '../annotationTypes';
|
|
8
|
+
export class RectangleController {
|
|
9
|
+
constructor(engine) {
|
|
10
|
+
this.engine = engine;
|
|
11
|
+
this.mouseAction = null;
|
|
12
|
+
this.mousePoint = { x: 0, y: 0, selected: false };
|
|
13
|
+
this.prevMousePoint = null;
|
|
14
|
+
this.startMousePoint = null;
|
|
15
|
+
this.rectangleAnchor = null;
|
|
16
|
+
this.needsRedraw = false;
|
|
17
|
+
}
|
|
18
|
+
getRenderState() {
|
|
19
|
+
return {
|
|
20
|
+
mousePoint: this.mousePoint,
|
|
21
|
+
startMousePoint: this.startMousePoint,
|
|
22
|
+
mouseAction: this.mouseAction,
|
|
23
|
+
anchor: this.rectangleAnchor,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
shouldRedraw() {
|
|
27
|
+
return this.needsRedraw;
|
|
28
|
+
}
|
|
29
|
+
begin(event) {
|
|
30
|
+
if (!isMouseClickAction(event.button, MouseAction.LEFT))
|
|
31
|
+
return;
|
|
32
|
+
const imageCanvasState = this.engine.getImageCanvasState();
|
|
33
|
+
const { zoom, dw, dh } = imageCanvasState;
|
|
34
|
+
const mousePoint = getCanvasMousePoint(event, this.engine.getImageCanvas(), imageCanvasState);
|
|
35
|
+
if (!isInsideImage(mousePoint, dw, dh))
|
|
36
|
+
return;
|
|
37
|
+
this.mouseAction = MouseAction.LEFT;
|
|
38
|
+
this.mousePoint = mousePoint;
|
|
39
|
+
this.startMousePoint = mousePoint;
|
|
40
|
+
const selectedAnnotation = selectRectangleAtPoint(zoom, this.mousePoint, this.engine.getAnnotations());
|
|
41
|
+
this.engine.setSelectedAnnotation(selectedAnnotation);
|
|
42
|
+
this.rectangleAnchor = updateActiveRectangleAnchor(zoom, this.mousePoint, selectedAnnotation);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
move(event) {
|
|
46
|
+
this.needsRedraw = false;
|
|
47
|
+
if (!isMouseDragAction(event.buttons, MouseAction.LEFT))
|
|
48
|
+
return;
|
|
49
|
+
const imageCanvasState = this.engine.getImageCanvasState();
|
|
50
|
+
const { dw, dh } = imageCanvasState;
|
|
51
|
+
this.prevMousePoint = cloneDeep(this.mousePoint);
|
|
52
|
+
this.mousePoint = getCanvasMousePoint(event, this.engine.getImageCanvas(), imageCanvasState);
|
|
53
|
+
const selectedAnnotation = this.engine.getSelectedAnnotation();
|
|
54
|
+
if (this.mouseAction === MouseAction.LEFT && selectedAnnotation && this.prevMousePoint && this.mousePoint) {
|
|
55
|
+
const params = {
|
|
56
|
+
annotations: this.engine.getAnnotations(),
|
|
57
|
+
dw,
|
|
58
|
+
dh,
|
|
59
|
+
mousePoint: this.mousePoint,
|
|
60
|
+
prevMousePoint: this.prevMousePoint,
|
|
61
|
+
rectangleAnchor: this.rectangleAnchor,
|
|
62
|
+
};
|
|
63
|
+
if (this.rectangleAnchor) {
|
|
64
|
+
applyRectangleResize(params);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
applyRectangleMove(params);
|
|
68
|
+
}
|
|
69
|
+
this.needsRedraw = true;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
end(event) {
|
|
74
|
+
if (!isMouseClickAction(event.button, MouseAction.LEFT))
|
|
75
|
+
return;
|
|
76
|
+
const imageCanvasState = this.engine.getImageCanvasState();
|
|
77
|
+
const mousePoint = getCanvasMousePoint(event, this.engine.getImageCanvas(), imageCanvasState);
|
|
78
|
+
this.mousePoint = mousePoint;
|
|
79
|
+
if (this.startMousePoint) {
|
|
80
|
+
const movedWidth = Math.abs(this.startMousePoint.x - mousePoint.x);
|
|
81
|
+
const movedHeight = Math.abs(this.startMousePoint.y - mousePoint.y);
|
|
82
|
+
const { zoom, dw, dh } = imageCanvasState;
|
|
83
|
+
if (movedWidth > ACTIVE_POINT_SIZE && movedHeight > ACTIVE_POINT_SIZE) {
|
|
84
|
+
const { x, y, width, height } = clampBoundingBoxToImage(Math.min(mousePoint.x, this.startMousePoint.x), Math.min(mousePoint.y, this.startMousePoint.y), movedWidth, movedHeight, dw, dh);
|
|
85
|
+
if (this.engine.getSelectedAnnotation()) {
|
|
86
|
+
this.engine.commitHistory(true);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const label = this.engine.getDrawing().label;
|
|
90
|
+
this.engine.appendAnnotation({ label, type: DrawMode.RECTANGLE, x, y, width, height, selected: false });
|
|
91
|
+
this.engine.commitHistory(true);
|
|
92
|
+
this.engine.setSelectedAnnotation(null);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
const annotations = this.engine.getAnnotations();
|
|
97
|
+
this.engine.setSelectedAnnotation(selectRectangleAtPoint(zoom, this.mousePoint, annotations));
|
|
98
|
+
this.engine.setAnnotations(annotations);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
this.reset();
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
leave(event) {
|
|
105
|
+
this.mousePoint = getCanvasMousePoint(event, this.engine.getImageCanvas(), this.engine.getImageCanvasState());
|
|
106
|
+
this.reset();
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
reset() {
|
|
110
|
+
this.mouseAction = null;
|
|
111
|
+
this.startMousePoint = null;
|
|
112
|
+
this.prevMousePoint = null;
|
|
113
|
+
this.rectangleAnchor = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Point, PointState } from '../../types';
|
|
2
|
+
import { Annotation, Rectangle } from '../annotationTypes';
|
|
3
|
+
import { RectangleAnchor } from './rectangleTypes';
|
|
4
|
+
export declare function getRectangleCorners({ x, y, width, height }: Rectangle): PointState[];
|
|
5
|
+
export declare function selectRectangleAtPoint(zoom: number, mousePoint: Point, annotations: Annotation[]): Annotation | null;
|
|
6
|
+
export declare function updateActiveRectangleAnchor(zoom: number, mousePoint: Point, selectedAnnotation: Annotation | null): RectangleAnchor | null;
|
|
7
|
+
export declare function getRectangleCursorByAnnotations(mousePoint: Point, annotations: Annotation[], zoom: number): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { DrawMode } from '../annotationTypes';
|
|
2
|
+
import { ACTIVE_POINT_SIZE } from './rectangleMath';
|
|
3
|
+
import { getRectangleCursor } from './rectangleRenderer';
|
|
4
|
+
import { RectangleAnchor } from './rectangleTypes';
|
|
5
|
+
export function getRectangleCorners({ x, y, width, height }) {
|
|
6
|
+
return [
|
|
7
|
+
{ x, y },
|
|
8
|
+
{ x: x + width, y },
|
|
9
|
+
{ x, y: y + height },
|
|
10
|
+
{ x: x + width, y: y + height },
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
export function selectRectangleAtPoint(zoom, mousePoint, annotations) {
|
|
14
|
+
const { x: mouseX, y: mouseY } = mousePoint;
|
|
15
|
+
const padding = ACTIVE_POINT_SIZE / zoom;
|
|
16
|
+
const isPointInRectangle = (a) => mouseX >= a.x - padding &&
|
|
17
|
+
mouseX <= a.x + a.width + padding &&
|
|
18
|
+
mouseY >= a.y - padding &&
|
|
19
|
+
mouseY <= a.y + a.height + padding;
|
|
20
|
+
let selectedIndex = -1;
|
|
21
|
+
let minArea = Number.MAX_SAFE_INTEGER;
|
|
22
|
+
const currentIndex = annotations.findIndex((a) => a.selected);
|
|
23
|
+
if (currentIndex >= 0) {
|
|
24
|
+
const c = annotations[currentIndex];
|
|
25
|
+
if (c.type === DrawMode.RECTANGLE && isPointInRectangle(c)) {
|
|
26
|
+
selectedIndex = currentIndex;
|
|
27
|
+
minArea = c.width * c.height;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (selectedIndex === -1) {
|
|
31
|
+
annotations.forEach((a, idx) => {
|
|
32
|
+
if (a.type !== DrawMode.RECTANGLE || !isPointInRectangle(a))
|
|
33
|
+
return;
|
|
34
|
+
const area = a.width * a.height;
|
|
35
|
+
if (area < minArea) {
|
|
36
|
+
minArea = area;
|
|
37
|
+
selectedIndex = idx;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
annotations.forEach((a) => (a.selected = false));
|
|
42
|
+
if (selectedIndex >= 0) {
|
|
43
|
+
annotations[selectedIndex].selected = true;
|
|
44
|
+
return annotations[selectedIndex];
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function updateActiveRectangleAnchor(zoom, mousePoint, selectedAnnotation) {
|
|
51
|
+
if (!selectedAnnotation)
|
|
52
|
+
return null;
|
|
53
|
+
const points = getRectangleCorners(selectedAnnotation);
|
|
54
|
+
const pointSize = ACTIVE_POINT_SIZE / zoom;
|
|
55
|
+
const { x: mouseX, y: mouseY } = mousePoint;
|
|
56
|
+
const { x, y, width: w, height: h } = selectedAnnotation;
|
|
57
|
+
if (mouseX >= points[0].x - pointSize / 2 &&
|
|
58
|
+
mouseX <= points[0].x + pointSize / 2 &&
|
|
59
|
+
mouseY >= points[0].y - pointSize / 2 &&
|
|
60
|
+
mouseY <= points[0].y + pointSize / 2) {
|
|
61
|
+
return RectangleAnchor.LEFT_TOP;
|
|
62
|
+
}
|
|
63
|
+
else if (mouseX >= points[1].x - pointSize / 2 &&
|
|
64
|
+
mouseX <= points[1].x + pointSize / 2 &&
|
|
65
|
+
mouseY >= points[1].y - pointSize / 2 &&
|
|
66
|
+
mouseY <= points[1].y + pointSize / 2) {
|
|
67
|
+
return RectangleAnchor.RIGHT_TOP;
|
|
68
|
+
}
|
|
69
|
+
else if (mouseX >= points[2].x - pointSize / 2 &&
|
|
70
|
+
mouseX <= points[2].x + pointSize / 2 &&
|
|
71
|
+
mouseY >= points[2].y - pointSize / 2 &&
|
|
72
|
+
mouseY <= points[2].y + pointSize / 2) {
|
|
73
|
+
return RectangleAnchor.LEFT_BOTTOM;
|
|
74
|
+
}
|
|
75
|
+
else if (mouseX >= points[3].x - pointSize / 2 &&
|
|
76
|
+
mouseX <= points[3].x + pointSize / 2 &&
|
|
77
|
+
mouseY >= points[3].y - pointSize / 2 &&
|
|
78
|
+
mouseY <= points[3].y + pointSize / 2) {
|
|
79
|
+
return RectangleAnchor.RIGHT_BOTTOM;
|
|
80
|
+
}
|
|
81
|
+
else if (mouseX >= x && mouseX <= x + w && mouseY >= y - pointSize / 2 && mouseY <= y + pointSize / 2) {
|
|
82
|
+
return RectangleAnchor.TOP;
|
|
83
|
+
}
|
|
84
|
+
else if (mouseX >= x && mouseX <= x + w && mouseY >= y + h - pointSize / 2 && mouseY <= y + h + pointSize / 2) {
|
|
85
|
+
return RectangleAnchor.BOTTOM;
|
|
86
|
+
}
|
|
87
|
+
else if (mouseY >= y && mouseY <= y + h && mouseX >= x - pointSize / 2 && mouseX <= x + pointSize / 2) {
|
|
88
|
+
return RectangleAnchor.LEFT;
|
|
89
|
+
}
|
|
90
|
+
else if (mouseY >= y && mouseY <= y + h && mouseX >= x + w - pointSize / 2 && mouseX <= x + w + pointSize / 2) {
|
|
91
|
+
return RectangleAnchor.RIGHT;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function getRectangleCursorByAnnotations(mousePoint, annotations, zoom) {
|
|
98
|
+
const target = annotations.find((a) => a.selected);
|
|
99
|
+
if (!target)
|
|
100
|
+
return 'default';
|
|
101
|
+
return getRectangleCursor(mousePoint, target, zoom);
|
|
102
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { RedrawLayers } from '../../interaction/interface';
|
|
2
|
+
import { RectangleController } from './rectangleController';
|
|
3
|
+
export declare class RectangleInteraction {
|
|
4
|
+
private readonly controller;
|
|
5
|
+
constructor(controller: RectangleController);
|
|
6
|
+
getRenderState(): {
|
|
7
|
+
mousePoint: import("../../types").PointState;
|
|
8
|
+
startMousePoint: import("../../types").PointState | null;
|
|
9
|
+
mouseAction: import("../../utils/mouseActions").MouseAction | null;
|
|
10
|
+
anchor: import("./rectangleTypes").RectangleAnchor | null;
|
|
11
|
+
};
|
|
12
|
+
getRedrawLayers(): RedrawLayers;
|
|
13
|
+
handleMouseDown(e: MouseEvent): boolean;
|
|
14
|
+
handleMouseMove(e: MouseEvent): boolean;
|
|
15
|
+
handleMouseUp(e: MouseEvent): boolean;
|
|
16
|
+
handleMouseLeave(e: MouseEvent): boolean;
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class RectangleInteraction {
|
|
2
|
+
constructor(controller) {
|
|
3
|
+
this.controller = controller;
|
|
4
|
+
}
|
|
5
|
+
getRenderState() {
|
|
6
|
+
return this.controller.getRenderState();
|
|
7
|
+
}
|
|
8
|
+
getRedrawLayers() {
|
|
9
|
+
return {
|
|
10
|
+
imageCanvas: false,
|
|
11
|
+
annotationsCanvas: this.controller.shouldRedraw(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
handleMouseDown(e) {
|
|
15
|
+
this.controller.begin(e);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
handleMouseMove(e) {
|
|
19
|
+
this.controller.move(e);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
handleMouseUp(e) {
|
|
23
|
+
this.controller.end(e);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
handleMouseLeave(e) {
|
|
27
|
+
this.controller.leave(e);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Point } from '../../types';
|
|
2
|
+
export declare const ACTIVE_POINT_SIZE = 10;
|
|
3
|
+
export declare function isInsideImage(point: Point, imageWidth: number, imageHeight: number): boolean;
|
|
4
|
+
export declare function clampBoundingBoxToImage(x: number, y: number, width: number, height: number, dw: number, dh: number): {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
selected: boolean;
|
|
10
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const ACTIVE_POINT_SIZE = 10;
|
|
2
|
+
export function isInsideImage(point, imageWidth, imageHeight) {
|
|
3
|
+
return point.x >= 0 && point.y >= 0 && point.x <= imageWidth && point.y <= imageHeight;
|
|
4
|
+
}
|
|
5
|
+
export function clampBoundingBoxToImage(x, y, width, height, dw, dh) {
|
|
6
|
+
let correctedX = x;
|
|
7
|
+
let correctedY = y;
|
|
8
|
+
let correctedWidth = width;
|
|
9
|
+
let correctedHeight = height;
|
|
10
|
+
if (x + width > dw)
|
|
11
|
+
correctedWidth = dw - x;
|
|
12
|
+
if (y + height > dh)
|
|
13
|
+
correctedHeight = dh - y;
|
|
14
|
+
if (x < 0) {
|
|
15
|
+
correctedWidth = width + x;
|
|
16
|
+
correctedX = 0;
|
|
17
|
+
}
|
|
18
|
+
if (y < 0) {
|
|
19
|
+
correctedHeight = height + y;
|
|
20
|
+
correctedY = 0;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
x: correctedX,
|
|
24
|
+
y: correctedY,
|
|
25
|
+
width: Math.max(0, correctedWidth),
|
|
26
|
+
height: Math.max(0, correctedHeight),
|
|
27
|
+
selected: false,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Point } from '../../types';
|
|
2
|
+
import { AnnotationsRenderer } from '../../renderer/interface';
|
|
3
|
+
import { Annotation } from '../annotationTypes';
|
|
4
|
+
export declare const rectangleRenderer: AnnotationsRenderer;
|
|
5
|
+
export declare function getRectangleCursor(mousePoint: Point, annotation: Annotation, zoom: number): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ACTIVE_POINT_SIZE } from './rectangleMath';
|
|
2
|
+
import { MouseAction } from '../../utils/mouseActions';
|
|
3
|
+
import { drawCross } from '../../renderer/drawCross';
|
|
4
|
+
import { getRectangleCorners } from './rectangleHitTest';
|
|
5
|
+
export const rectangleRenderer = {
|
|
6
|
+
drawAnnotation(context, engine, annotation) {
|
|
7
|
+
const { zoom } = engine.getImageCanvasState();
|
|
8
|
+
const { applyStyle, lineSize } = engine.getDrawing();
|
|
9
|
+
applyStyle({ variant: 'drawRect', context, annotation, drawLineSize: lineSize, zoom });
|
|
10
|
+
applyStyle({ variant: 'drawText', context, annotation, drawLineSize: lineSize, zoom });
|
|
11
|
+
if (annotation.selected && engine.isEditable()) {
|
|
12
|
+
drawActiveRect(context, annotation, zoom, lineSize);
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
drawEditorUI(context, engine) {
|
|
16
|
+
if (engine.getSelectedAnnotation())
|
|
17
|
+
return;
|
|
18
|
+
const renderState = engine.getDrawRenderState();
|
|
19
|
+
const pointerState = engine.getPointerRenderState();
|
|
20
|
+
const { zoom, dw, dh } = engine.getImageCanvasState();
|
|
21
|
+
const { lineSize, color } = engine.getDrawing();
|
|
22
|
+
if (renderState?.mouseAction === MouseAction.LEFT && renderState.startMousePoint) {
|
|
23
|
+
drawNewRectFromPoints(context, renderState.mousePoint, renderState.startMousePoint, zoom, lineSize, color);
|
|
24
|
+
}
|
|
25
|
+
if (pointerState?.mousePoint && !engine.getSelectedAnnotation()) {
|
|
26
|
+
drawCross(context, pointerState.mousePoint, dw, dh, zoom, lineSize);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
function drawActiveRect(ctx, annotation, zoom, drawLineSize) {
|
|
31
|
+
const { x, y, width, height, selected } = annotation;
|
|
32
|
+
if (!selected)
|
|
33
|
+
return;
|
|
34
|
+
ctx.lineWidth = drawLineSize / zoom;
|
|
35
|
+
ctx.fillStyle = '#FFF';
|
|
36
|
+
const pointSize = ACTIVE_POINT_SIZE / zoom;
|
|
37
|
+
const points = getRectangleCorners({ x, y, width, height });
|
|
38
|
+
points.forEach((point) => {
|
|
39
|
+
ctx.fillStyle = '#FFF';
|
|
40
|
+
ctx.fillRect(point.x - pointSize / 2, point.y - pointSize / 2, pointSize, pointSize);
|
|
41
|
+
ctx.strokeStyle = '#000';
|
|
42
|
+
ctx.lineWidth = 1 / zoom;
|
|
43
|
+
ctx.strokeRect(point.x - pointSize / 2, point.y - pointSize / 2, pointSize, pointSize);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function getRectangleCursor(mousePoint, annotation, zoom) {
|
|
47
|
+
const pointSize = ACTIVE_POINT_SIZE / zoom;
|
|
48
|
+
const { x, y, width: w, height: h } = annotation;
|
|
49
|
+
if (w <= 0 || h <= 0)
|
|
50
|
+
return 'default';
|
|
51
|
+
const mouseX = mousePoint.x;
|
|
52
|
+
const mouseY = mousePoint.y;
|
|
53
|
+
const points = getRectangleCorners({ x, y, width: w, height: h });
|
|
54
|
+
const hit = (px, py) => mouseX >= px - pointSize / 2 &&
|
|
55
|
+
mouseX <= px + pointSize / 2 &&
|
|
56
|
+
mouseY >= py - pointSize / 2 &&
|
|
57
|
+
mouseY <= py + pointSize / 2;
|
|
58
|
+
if (hit(points[0].x, points[0].y))
|
|
59
|
+
return 'nwse-resize';
|
|
60
|
+
if (hit(points[1].x, points[1].y))
|
|
61
|
+
return 'nesw-resize';
|
|
62
|
+
if (hit(points[2].x, points[2].y))
|
|
63
|
+
return 'nesw-resize';
|
|
64
|
+
if (hit(points[3].x, points[3].y))
|
|
65
|
+
return 'nwse-resize';
|
|
66
|
+
if (mouseX >= x && mouseX <= x + w && Math.abs(mouseY - y) <= pointSize / 2)
|
|
67
|
+
return 'ns-resize';
|
|
68
|
+
if (mouseX >= x && mouseX <= x + w && Math.abs(mouseY - (y + h)) <= pointSize / 2)
|
|
69
|
+
return 'ns-resize';
|
|
70
|
+
if (mouseY >= y && mouseY <= y + h && Math.abs(mouseX - x) <= pointSize / 2)
|
|
71
|
+
return 'ew-resize';
|
|
72
|
+
if (mouseY >= y && mouseY <= y + h && Math.abs(mouseX - (x + w)) <= pointSize / 2)
|
|
73
|
+
return 'ew-resize';
|
|
74
|
+
return 'default';
|
|
75
|
+
}
|
|
76
|
+
function drawNewRectFromPoints(context, startPoint, endPoint, zoom, drawLineSize, color) {
|
|
77
|
+
if (!color)
|
|
78
|
+
return;
|
|
79
|
+
const x = Math.min(endPoint.x, startPoint.x);
|
|
80
|
+
const y = Math.min(endPoint.y, startPoint.y);
|
|
81
|
+
const width = Math.abs(endPoint.x - startPoint.x);
|
|
82
|
+
const height = Math.abs(endPoint.y - startPoint.y);
|
|
83
|
+
context.beginPath();
|
|
84
|
+
context.lineWidth = drawLineSize / zoom;
|
|
85
|
+
context.rect(x, y, width, height);
|
|
86
|
+
context.strokeStyle = color;
|
|
87
|
+
context.stroke();
|
|
88
|
+
}
|