@deepnoid/canvas 0.1.79 → 0.1.81

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.
Files changed (34) hide show
  1. package/README.md +19 -8
  2. package/dist/engine/annotation/annotationTypes.d.ts +12 -2
  3. package/dist/engine/annotation/annotationTypes.js +1 -2
  4. package/dist/engine/annotation/hitTest.d.ts +5 -0
  5. package/dist/engine/annotation/hitTest.js +63 -0
  6. package/dist/engine/annotation/polygon/polygonController.d.ts +34 -0
  7. package/dist/engine/annotation/polygon/polygonController.js +252 -0
  8. package/dist/engine/annotation/polygon/polygonHitTest.d.ts +6 -0
  9. package/dist/engine/annotation/polygon/polygonHitTest.js +84 -0
  10. package/dist/engine/annotation/polygon/polygonInteraction.d.ts +19 -0
  11. package/dist/engine/annotation/polygon/polygonInteraction.js +36 -0
  12. package/dist/engine/annotation/polygon/polygonMath.d.ts +7 -0
  13. package/dist/engine/annotation/polygon/polygonMath.js +47 -0
  14. package/dist/engine/annotation/polygon/polygonRenderer.d.ts +2 -0
  15. package/dist/engine/annotation/polygon/polygonRenderer.js +79 -0
  16. package/dist/engine/annotation/polygon/polygonTransform.d.ts +14 -0
  17. package/dist/engine/annotation/polygon/polygonTransform.js +68 -0
  18. package/dist/engine/annotation/polygon/polygonTypes.d.ts +9 -0
  19. package/dist/engine/annotation/polygon/polygonTypes.js +1 -0
  20. package/dist/engine/annotation/rectangle/rectangleController.js +27 -16
  21. package/dist/engine/annotation/rectangle/rectangleHitTest.js +5 -4
  22. package/dist/engine/annotation/rectangle/rectangleRenderer.js +8 -0
  23. package/dist/engine/annotation/rectangle/rectangleTransform.js +31 -28
  24. package/dist/engine/interaction/drawModeRouter.js +47 -27
  25. package/dist/engine/interaction/interactionController.d.ts +1 -0
  26. package/dist/engine/interaction/interactionController.js +17 -0
  27. package/dist/engine/interaction/interface.d.ts +1 -0
  28. package/dist/engine/interaction/pointerInteraction.js +7 -2
  29. package/dist/engine/public/annotationEngine.d.ts +3 -1
  30. package/dist/engine/public/annotationEngine.js +19 -1
  31. package/dist/react/AnnotationCanvas.js +6 -1
  32. package/dist/react/hooks/useHotkeys.d.ts +2 -1
  33. package/dist/react/hooks/useHotkeys.js +4 -2
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -11,12 +11,20 @@ Canvas 기반 이미지 annotation 라이브러리로, **TypeScript Engine + Rea
11
11
 
12
12
  ## 주요 기능
13
13
 
14
- - ✏️ Rectangle annotation 그리기/편집
14
+ - ✏️ **Annotation 지원 (그리기 및 편집)**
15
+ - **Rectangle (BBox)**:
16
+ - 모서리/상하좌우 테두리를 통한 직관적 크기 조절
17
+ - 영역 내부 드래그(`move` 커서)를 통한 도형 전체 이동
18
+ - **Polygon**:
19
+ - 다각형 점찍기 및 시작점(또는 더블클릭) 클릭으로 도형 완성
20
+ - **선 분할(Edge Point Insertion)**: 기존 그려진 폴리곤 선(Edge) 위에 마우스를 1초간 올려둘 시, 새로운 꼭짓점(Vertex)을 간편하게 추가 가능 (클릭/이동 오작동 방지)
21
+ - 내부 클릭 후 드래그하여 전체 다각형 이동 지원
22
+ - **스마트 UI 동기화**: 캔버스 내의 도형 클릭 시 선택된 객체 타입(RECTANGLE/POLYGON)에 맞춰 외부 도구(React 상태)가 자동으로 전환되도록 이벤트(`annotationSelected`) 연동
15
23
  - 👁️ Viewer 모드 지원 (읽기 전용)
16
- - 🔍 Pan & Zoom 지원
17
- - ↩️ Undo/Redo 기능
18
- - ⌨️ 단축키 지원
19
- - 🎨 커스터마이징 가능한 스타일링
24
+ - 🔍 Pan & Zoom 지원 (Image Canvas / Annotation Canvas 레이어 완전 분리)
25
+ - ↩️ Undo/Redo 기능 (History 상태망 자체 관리)
26
+ - ⌨️ 단축키 지원 (선택 객체 삭제, 되돌리기, 한 객체만 보기 등)
27
+ - 🎨 커스터마이징 가능한 스타일링 (커스텀 렌더링 함수(`applyStyle`) 주입 가능)
20
28
 
21
29
  ## 빠른 시작
22
30
 
@@ -200,6 +208,9 @@ src/
200
208
 
201
209
  ## 설계 원칙
202
210
 
203
- 1. **관심사 분리**: Engine(로직) ↔ React(UI) 완전 분리
204
- 2. **확장성**: 새로운 annotation 타입 추가 용이
205
- 3. **성능 최적화**: 레이어별 redraw 제어
211
+ 1. **관심사 분리**: Engine(로직, Canvas 렌더링) ↔ React(UI DOM, 상태 연동) 완전 분리
212
+ 2. **확장성**: `Renderer`, `HitTest`, `Controller` 구조로 분리되어 있어, Polygon과 같은 새로운 Annotation 타입을 손쉽게 플러그인처럼 추가 가능합니다.
213
+ 3. **사용자 경험(UX) 중심**:
214
+ - 툴 오버 액션과 클릭/드래그 충돌을 방지하는 스마트 HitTest 우선순위
215
+ - 1초 Hover Timer 등 인간 중심적인 편집 인터랙션 제공
216
+ 4. **성능 최적화**: 레이어별(base image / annotation) 독립된 Canvas를 사용하여 불필요한 이미지 렌더링(Redraw) 비용 원천 차단
@@ -1,6 +1,7 @@
1
1
  import { Label } from '../types';
2
2
  export declare enum DrawMode {
3
- RECTANGLE = "RECTANGLE"
3
+ RECTANGLE = "RECTANGLE",
4
+ POLYGON = "POLYGON"
4
5
  }
5
6
  export type AnnotationBase = {
6
7
  label?: Label;
@@ -18,4 +19,13 @@ export type Rectangle = {
18
19
  export type RectangleAnnotation = AnnotationBase & {
19
20
  type: DrawMode.RECTANGLE;
20
21
  } & Rectangle;
21
- export type Annotation = RectangleAnnotation;
22
+ export type Polygon = {
23
+ points: {
24
+ x: number;
25
+ y: number;
26
+ }[];
27
+ };
28
+ export type PolygonAnnotation = AnnotationBase & {
29
+ type: DrawMode.POLYGON;
30
+ } & Polygon;
31
+ export type Annotation = RectangleAnnotation | PolygonAnnotation;
@@ -1,6 +1,5 @@
1
1
  export var DrawMode;
2
2
  (function (DrawMode) {
3
3
  DrawMode["RECTANGLE"] = "RECTANGLE";
4
- // POLYGON = 'POLYGON',
4
+ DrawMode["POLYGON"] = "POLYGON";
5
5
  })(DrawMode || (DrawMode = {}));
6
- // | PolygonAnnotation
@@ -0,0 +1,5 @@
1
+ import { Point } from '../types';
2
+ import { Annotation, PolygonAnnotation, RectangleAnnotation } from './annotationTypes';
3
+ export declare function isPointInRectangle(zoom: number, mousePoint: Point, a: RectangleAnnotation): boolean;
4
+ export declare function isPointInOrOnPolygon(zoom: number, mousePoint: Point, a: PolygonAnnotation): boolean;
5
+ export declare function selectAnnotationAtPoint(zoom: number, mousePoint: Point, annotations: Annotation[]): Annotation | null;
@@ -0,0 +1,63 @@
1
+ import { DrawMode } from './annotationTypes';
2
+ import { ACTIVE_POINT_SIZE, isInsidePolygon, distToSegment } from './polygon/polygonMath';
3
+ export function isPointInRectangle(zoom, mousePoint, a) {
4
+ const padding = ACTIVE_POINT_SIZE / zoom;
5
+ return (mousePoint.x >= a.x - padding &&
6
+ mousePoint.x <= a.x + a.width + padding &&
7
+ mousePoint.y >= a.y - padding &&
8
+ mousePoint.y <= a.y + a.height + padding);
9
+ }
10
+ export function isPointInOrOnPolygon(zoom, mousePoint, a) {
11
+ if (a.points.length < 3)
12
+ return false;
13
+ if (isInsidePolygon(mousePoint, a.points))
14
+ return true;
15
+ const padding = ACTIVE_POINT_SIZE / zoom;
16
+ for (let i = 0; i < a.points.length; i++) {
17
+ const p1 = a.points[i];
18
+ const p2 = a.points[(i + 1) % a.points.length];
19
+ if (distToSegment(mousePoint, p1, p2) <= padding) {
20
+ return true;
21
+ }
22
+ }
23
+ return false;
24
+ }
25
+ export function selectAnnotationAtPoint(zoom, mousePoint, annotations) {
26
+ let selectedIndex = -1;
27
+ let minArea = Number.MAX_SAFE_INTEGER;
28
+ const currentIndex = annotations.findIndex((a) => a.selected);
29
+ if (currentIndex >= 0) {
30
+ const c = annotations[currentIndex];
31
+ if (c.type === DrawMode.RECTANGLE && isPointInRectangle(zoom, mousePoint, c)) {
32
+ selectedIndex = currentIndex;
33
+ minArea = c.width * c.height;
34
+ }
35
+ else if (c.type === DrawMode.POLYGON && isPointInOrOnPolygon(zoom, mousePoint, c)) {
36
+ selectedIndex = currentIndex;
37
+ }
38
+ }
39
+ if (selectedIndex === -1) {
40
+ for (let i = annotations.length - 1; i >= 0; i--) {
41
+ const a = annotations[i];
42
+ if (a.type === DrawMode.RECTANGLE && isPointInRectangle(zoom, mousePoint, a)) {
43
+ const area = a.width * a.height;
44
+ if (area < minArea) {
45
+ minArea = area;
46
+ selectedIndex = i;
47
+ }
48
+ }
49
+ else if (a.type === DrawMode.POLYGON && isPointInOrOnPolygon(zoom, mousePoint, a)) {
50
+ selectedIndex = i;
51
+ break; // polygon has precedence
52
+ }
53
+ }
54
+ }
55
+ annotations.forEach((a) => (a.selected = false));
56
+ if (selectedIndex >= 0) {
57
+ annotations[selectedIndex].selected = true;
58
+ return annotations[selectedIndex];
59
+ }
60
+ else {
61
+ return null;
62
+ }
63
+ }
@@ -0,0 +1,34 @@
1
+ import { AnnotationEngine } from '../../public/annotationEngine';
2
+ import { MouseAction } from '../../utils/mouseActions';
3
+ import { PointState, Point } from '../../types';
4
+ import { PolygonAnchor } from './polygonTypes';
5
+ export declare class PolygonController {
6
+ private readonly engine;
7
+ private mouseAction;
8
+ private mousePoint;
9
+ private prevMousePoint;
10
+ private drawingPoints;
11
+ private polygonAnchor;
12
+ private needsRedraw;
13
+ private isDrawingNewPolygon;
14
+ private isCompletingPolygon;
15
+ private hasMoved;
16
+ private hoverEdgeTimer;
17
+ private isEdgeInsertReady;
18
+ constructor(engine: AnnotationEngine);
19
+ getRenderState(): {
20
+ mousePoint: PointState;
21
+ drawingPoints: Point[];
22
+ mouseAction: MouseAction | null;
23
+ anchor: PolygonAnchor | null;
24
+ isEdgeInsertReady: boolean;
25
+ };
26
+ shouldRedraw(): boolean;
27
+ begin(event: MouseEvent): true | undefined;
28
+ move(event: MouseEvent): boolean;
29
+ end(event: MouseEvent): true | undefined;
30
+ leave(event: MouseEvent): boolean;
31
+ cancel(): boolean;
32
+ private commitPolygon;
33
+ private reset;
34
+ }
@@ -0,0 +1,252 @@
1
+ import { cloneDeep } from '../../utils/cloneDeep';
2
+ import { isMouseClickAction, isMouseDragAction, MouseAction } from '../../utils/mouseActions';
3
+ import { getCanvasMousePoint } from '../../utils/mousePoint';
4
+ import { updateActivePolygonAnchor } from './polygonHitTest';
5
+ import { selectAnnotationAtPoint } from '../hitTest';
6
+ import { applyPolygonMove, applyPolygonResize } from './polygonTransform';
7
+ import { DrawMode } from '../annotationTypes';
8
+ import { clampPointToImage, clipLineToImage, ACTIVE_POINT_SIZE } from './polygonMath';
9
+ export class PolygonController {
10
+ constructor(engine) {
11
+ this.engine = engine;
12
+ this.mouseAction = null;
13
+ this.mousePoint = { x: 0, y: 0, selected: false };
14
+ this.prevMousePoint = null;
15
+ this.drawingPoints = [];
16
+ this.polygonAnchor = null;
17
+ this.needsRedraw = false;
18
+ this.isDrawingNewPolygon = false;
19
+ this.isCompletingPolygon = false;
20
+ this.hasMoved = false;
21
+ this.hoverEdgeTimer = null;
22
+ this.isEdgeInsertReady = false;
23
+ }
24
+ getRenderState() {
25
+ return {
26
+ mousePoint: this.mousePoint,
27
+ drawingPoints: this.drawingPoints,
28
+ mouseAction: this.mouseAction,
29
+ anchor: this.isEdgeInsertReady
30
+ ? this.polygonAnchor
31
+ : this.polygonAnchor?.type === 'edge'
32
+ ? null
33
+ : this.polygonAnchor,
34
+ isEdgeInsertReady: this.isEdgeInsertReady,
35
+ };
36
+ }
37
+ shouldRedraw() {
38
+ return this.needsRedraw;
39
+ }
40
+ begin(event) {
41
+ if (!isMouseClickAction(event.button, MouseAction.LEFT))
42
+ return;
43
+ const imageCanvasState = this.engine.getImageCanvasState();
44
+ const { zoom, dw, dh } = imageCanvasState;
45
+ const mousePoint = getCanvasMousePoint(event, this.engine.getImageCanvas(), imageCanvasState);
46
+ this.mouseAction = MouseAction.LEFT;
47
+ this.mousePoint = mousePoint;
48
+ this.hasMoved = false;
49
+ // Drawing new polygon logic
50
+ if (this.isDrawingNewPolygon) {
51
+ let nextPoint = mousePoint;
52
+ if (this.drawingPoints.length > 0) {
53
+ nextPoint = clipLineToImage(this.drawingPoints[this.drawingPoints.length - 1], mousePoint, dw, dh);
54
+ }
55
+ else {
56
+ nextPoint = clampPointToImage(mousePoint.x, mousePoint.y, dw, dh);
57
+ }
58
+ if (this.drawingPoints.length >= 3) {
59
+ // Double click or click on start point
60
+ const distToStart = Math.sqrt((nextPoint.x - this.drawingPoints[0].x) ** 2 + (nextPoint.y - this.drawingPoints[0].y) ** 2);
61
+ if (event.detail >= 2 || distToStart <= (ACTIVE_POINT_SIZE * 2) / zoom) {
62
+ this.commitPolygon();
63
+ return true;
64
+ }
65
+ }
66
+ this.drawingPoints.push(nextPoint);
67
+ this.needsRedraw = true;
68
+ return true;
69
+ }
70
+ // Hit test existing polygon
71
+ const annotations = this.engine.getAnnotations();
72
+ const currentSelected = this.engine.getSelectedAnnotation();
73
+ const targetAnnotation = selectAnnotationAtPoint(zoom, mousePoint, annotations);
74
+ if (targetAnnotation !== currentSelected) {
75
+ this.engine.setSelectedAnnotation(targetAnnotation);
76
+ this.engine.setAnnotations(annotations);
77
+ }
78
+ this.polygonAnchor = updateActivePolygonAnchor(zoom, mousePoint, targetAnnotation);
79
+ if (this.polygonAnchor && this.polygonAnchor.type === 'edge') {
80
+ if (this.hoverEdgeTimer) {
81
+ window.clearTimeout(this.hoverEdgeTimer);
82
+ this.hoverEdgeTimer = null;
83
+ }
84
+ if (this.isEdgeInsertReady) {
85
+ const selected = this.engine.getSelectedAnnotation();
86
+ if (selected && selected.type === DrawMode.POLYGON) {
87
+ const poly = selected;
88
+ const insertIdx = this.polygonAnchor.edgeIndex + 1;
89
+ poly.points.splice(insertIdx, 0, this.polygonAnchor.insertPoint);
90
+ this.polygonAnchor = { type: 'point', pointIndex: insertIdx };
91
+ this.engine.commitHistory(true);
92
+ this.isEdgeInsertReady = false;
93
+ this.needsRedraw = true;
94
+ return true;
95
+ }
96
+ }
97
+ }
98
+ if (!this.polygonAnchor && !targetAnnotation) {
99
+ if (mousePoint.x < 0 || mousePoint.y < 0 || mousePoint.x > dw || mousePoint.y > dh) {
100
+ return true;
101
+ }
102
+ // Begin new polygon
103
+ this.isDrawingNewPolygon = true;
104
+ this.drawingPoints = [clampPointToImage(mousePoint.x, mousePoint.y, dw, dh)];
105
+ this.needsRedraw = true;
106
+ }
107
+ return true;
108
+ }
109
+ move(event) {
110
+ this.needsRedraw = false;
111
+ const imageCanvasState = this.engine.getImageCanvasState();
112
+ const { dw, dh, zoom } = imageCanvasState;
113
+ this.prevMousePoint = cloneDeep(this.mousePoint);
114
+ this.mousePoint = getCanvasMousePoint(event, this.engine.getImageCanvas(), imageCanvasState);
115
+ if (this.isDrawingNewPolygon) {
116
+ this.needsRedraw = true;
117
+ return true;
118
+ }
119
+ if (!isMouseDragAction(event.buttons, MouseAction.LEFT)) {
120
+ const currentSelected = this.engine.getSelectedAnnotation();
121
+ if (currentSelected && currentSelected.type === DrawMode.POLYGON) {
122
+ const newAnchor = updateActivePolygonAnchor(zoom, this.mousePoint, currentSelected);
123
+ const isSameAnchorType = this.polygonAnchor?.type === newAnchor?.type;
124
+ const isSameEdge = isSameAnchorType &&
125
+ newAnchor?.type === 'edge' &&
126
+ this.polygonAnchor?.edgeIndex === newAnchor.edgeIndex;
127
+ if (isSameEdge) {
128
+ this.polygonAnchor = newAnchor;
129
+ this.needsRedraw = true;
130
+ }
131
+ else {
132
+ this.polygonAnchor = newAnchor;
133
+ this.isEdgeInsertReady = false;
134
+ if (this.hoverEdgeTimer) {
135
+ window.clearTimeout(this.hoverEdgeTimer);
136
+ this.hoverEdgeTimer = null;
137
+ }
138
+ if (newAnchor && newAnchor.type === 'edge') {
139
+ this.hoverEdgeTimer = window.setTimeout(() => {
140
+ this.isEdgeInsertReady = true;
141
+ this.needsRedraw = true;
142
+ this.engine.redrawAnnotations?.();
143
+ this.engine.getAnnotationsCanvas().style.cursor = 'pointer';
144
+ }, 1000);
145
+ }
146
+ this.needsRedraw = true;
147
+ }
148
+ }
149
+ return true;
150
+ }
151
+ this.hasMoved = true;
152
+ const selectedAnnotation = this.engine.getSelectedAnnotation();
153
+ if (selectedAnnotation && this.prevMousePoint) {
154
+ const params = {
155
+ annotations: this.engine.getAnnotations(),
156
+ dw,
157
+ dh,
158
+ mousePoint: this.mousePoint,
159
+ prevMousePoint: this.prevMousePoint,
160
+ polygonAnchor: this.polygonAnchor,
161
+ };
162
+ if (this.polygonAnchor) {
163
+ applyPolygonResize(params);
164
+ }
165
+ else {
166
+ applyPolygonMove(params);
167
+ }
168
+ this.needsRedraw = true;
169
+ }
170
+ return true;
171
+ }
172
+ end(event) {
173
+ if (!isMouseClickAction(event.button, MouseAction.LEFT))
174
+ return;
175
+ if (this.isDrawingNewPolygon) {
176
+ // Do nothing, wait for double click or close to finish
177
+ return true;
178
+ }
179
+ if (this.isCompletingPolygon) {
180
+ this.isCompletingPolygon = false;
181
+ return true;
182
+ }
183
+ if (this.hasMoved) {
184
+ if (this.engine.getSelectedAnnotation()) {
185
+ this.engine.commitHistory(true);
186
+ }
187
+ }
188
+ else {
189
+ const imageCanvasState = this.engine.getImageCanvasState();
190
+ const mousePoint = getCanvasMousePoint(event, this.engine.getImageCanvas(), imageCanvasState);
191
+ const annotations = this.engine.getAnnotations();
192
+ const selected = selectAnnotationAtPoint(imageCanvasState.zoom, mousePoint, annotations);
193
+ if (selected !== this.engine.getSelectedAnnotation()) {
194
+ this.engine.setSelectedAnnotation(selected);
195
+ }
196
+ }
197
+ this.mouseAction = null;
198
+ this.hasMoved = false;
199
+ if (this.hoverEdgeTimer) {
200
+ window.clearTimeout(this.hoverEdgeTimer);
201
+ this.hoverEdgeTimer = null;
202
+ }
203
+ return true;
204
+ }
205
+ leave(event) {
206
+ this.mousePoint = getCanvasMousePoint(event, this.engine.getImageCanvas(), this.engine.getImageCanvasState());
207
+ return true;
208
+ }
209
+ cancel() {
210
+ if (this.hoverEdgeTimer) {
211
+ window.clearTimeout(this.hoverEdgeTimer);
212
+ this.hoverEdgeTimer = null;
213
+ }
214
+ if (this.isDrawingNewPolygon) {
215
+ this.reset();
216
+ this.needsRedraw = true;
217
+ return true;
218
+ }
219
+ return false;
220
+ }
221
+ commitPolygon() {
222
+ if (this.drawingPoints.length < 3) {
223
+ this.reset();
224
+ return;
225
+ }
226
+ const label = this.engine.getDrawing().label;
227
+ const points = Array.from(this.drawingPoints);
228
+ this.engine.appendAnnotation({
229
+ label,
230
+ type: DrawMode.POLYGON,
231
+ points,
232
+ selected: false,
233
+ });
234
+ this.engine.commitHistory(true);
235
+ this.reset();
236
+ this.isCompletingPolygon = true;
237
+ }
238
+ reset() {
239
+ if (this.hoverEdgeTimer) {
240
+ window.clearTimeout(this.hoverEdgeTimer);
241
+ this.hoverEdgeTimer = null;
242
+ }
243
+ this.mouseAction = null;
244
+ this.polygonAnchor = null;
245
+ this.isDrawingNewPolygon = false;
246
+ this.drawingPoints = [];
247
+ this.hasMoved = false;
248
+ this.needsRedraw = true;
249
+ this.isCompletingPolygon = false;
250
+ this.isEdgeInsertReady = false;
251
+ }
252
+ }
@@ -0,0 +1,6 @@
1
+ import { Point } from '../../types';
2
+ import { Annotation, PolygonAnnotation } from '../annotationTypes';
3
+ import { PolygonAnchor } from './polygonTypes';
4
+ export declare function selectPolygonAtPoint(zoom: number, mousePoint: Point, annotations: Annotation[]): Annotation | null;
5
+ export declare function updateActivePolygonAnchor(zoom: number, mousePoint: Point, selectedAnnotation: Annotation | null): PolygonAnchor | null;
6
+ export declare function getPolygonCursor(mousePoint: Point, annotation: PolygonAnnotation, zoom: number, isEdgeInsertReady?: boolean): string;
@@ -0,0 +1,84 @@
1
+ import { DrawMode } from '../annotationTypes';
2
+ import { ACTIVE_POINT_SIZE, distToSegment, isInsidePolygon, projectOnSegment } from './polygonMath';
3
+ export function selectPolygonAtPoint(zoom, mousePoint, annotations) {
4
+ const padding = ACTIVE_POINT_SIZE / zoom;
5
+ const isPointInOrOnPolygon = (a) => {
6
+ if (a.points.length < 3)
7
+ return false;
8
+ if (isInsidePolygon(mousePoint, a.points))
9
+ return true;
10
+ // Check edges
11
+ for (let i = 0; i < a.points.length; i++) {
12
+ const p1 = a.points[i];
13
+ const p2 = a.points[(i + 1) % a.points.length];
14
+ if (distToSegment(mousePoint, p1, p2) <= padding) {
15
+ return true;
16
+ }
17
+ }
18
+ return false;
19
+ };
20
+ let selectedIndex = -1;
21
+ const currentIndex = annotations.findIndex((a) => a.selected);
22
+ if (currentIndex >= 0) {
23
+ const c = annotations[currentIndex];
24
+ if (c.type === DrawMode.POLYGON && isPointInOrOnPolygon(c)) {
25
+ selectedIndex = currentIndex;
26
+ }
27
+ }
28
+ if (selectedIndex === -1) {
29
+ for (let i = annotations.length - 1; i >= 0; i--) {
30
+ const a = annotations[i];
31
+ if (a.type === DrawMode.POLYGON && isPointInOrOnPolygon(a)) {
32
+ selectedIndex = i;
33
+ break;
34
+ }
35
+ }
36
+ }
37
+ annotations.forEach((a) => (a.selected = false));
38
+ if (selectedIndex >= 0) {
39
+ annotations[selectedIndex].selected = true;
40
+ return annotations[selectedIndex];
41
+ }
42
+ else {
43
+ return null;
44
+ }
45
+ }
46
+ export function updateActivePolygonAnchor(zoom, mousePoint, selectedAnnotation) {
47
+ if (!selectedAnnotation || selectedAnnotation.type !== DrawMode.POLYGON)
48
+ return null;
49
+ const pointSize = ACTIVE_POINT_SIZE / zoom;
50
+ const { points } = selectedAnnotation;
51
+ // Check points first
52
+ for (let i = 0; i < points.length; i++) {
53
+ const p = points[i];
54
+ if (mousePoint.x >= p.x - pointSize / 2 &&
55
+ mousePoint.x <= p.x + pointSize / 2 &&
56
+ mousePoint.y >= p.y - pointSize / 2 &&
57
+ mousePoint.y <= p.y + pointSize / 2) {
58
+ return { type: 'point', pointIndex: i };
59
+ }
60
+ }
61
+ // Check edges
62
+ for (let i = 0; i < points.length; i++) {
63
+ const p1 = points[i];
64
+ const p2 = points[(i + 1) % points.length];
65
+ if (distToSegment(mousePoint, p1, p2) <= pointSize / 2) {
66
+ const projection = projectOnSegment(mousePoint, p1, p2);
67
+ return { type: 'edge', edgeIndex: i, insertPoint: { x: projection.x, y: projection.y, selected: false } };
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ export function getPolygonCursor(mousePoint, annotation, zoom, isEdgeInsertReady) {
73
+ const anchor = updateActivePolygonAnchor(zoom, mousePoint, annotation);
74
+ if (anchor) {
75
+ if (anchor.type === 'point')
76
+ return 'pointer';
77
+ if (anchor.type === 'edge')
78
+ return isEdgeInsertReady ? 'pointer' : 'move';
79
+ }
80
+ if (annotation.points.length >= 3 && isInsidePolygon(mousePoint, annotation.points)) {
81
+ return 'move';
82
+ }
83
+ return 'default';
84
+ }
@@ -0,0 +1,19 @@
1
+ import { RedrawLayers } from '../../interaction/interface';
2
+ import { PolygonController } from './polygonController';
3
+ export declare class PolygonInteraction {
4
+ private readonly controller;
5
+ constructor(controller: PolygonController);
6
+ getRenderState(): {
7
+ mousePoint: import("../../types").PointState;
8
+ drawingPoints: import("../../types").Point[];
9
+ mouseAction: import("../../utils/mouseActions").MouseAction | null;
10
+ anchor: import("./polygonTypes").PolygonAnchor | null;
11
+ isEdgeInsertReady: boolean;
12
+ };
13
+ getRedrawLayers(): RedrawLayers;
14
+ handleMouseDown(e: MouseEvent): boolean;
15
+ handleMouseMove(e: MouseEvent): boolean;
16
+ handleMouseUp(e: MouseEvent): boolean;
17
+ handleMouseLeave(e: MouseEvent): boolean;
18
+ handleKeyDown(e: KeyboardEvent): boolean;
19
+ }
@@ -0,0 +1,36 @@
1
+ export class PolygonInteraction {
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
+ handleKeyDown(e) {
31
+ if (e.key === 'Escape') {
32
+ return this.controller.cancel();
33
+ }
34
+ return false;
35
+ }
36
+ }
@@ -0,0 +1,7 @@
1
+ import { Point } from '../../types';
2
+ export declare const ACTIVE_POINT_SIZE = 8;
3
+ export declare function clampPointToImage(x: number, y: number, dw: number, dh: number): Point;
4
+ export declare function clipLineToImage(start: Point, end: Point, dw: number, dh: number): Point;
5
+ export declare function isInsidePolygon(point: Point, vs: Point[]): boolean;
6
+ export declare function projectOnSegment(p: Point, v: Point, w: Point): Point;
7
+ export declare function distToSegment(p: Point, v: Point, w: Point): number;
@@ -0,0 +1,47 @@
1
+ export const ACTIVE_POINT_SIZE = 8;
2
+ export function clampPointToImage(x, y, dw, dh) {
3
+ return {
4
+ x: Math.max(0, Math.min(x, dw)),
5
+ y: Math.max(0, Math.min(y, dh)),
6
+ };
7
+ }
8
+ export function clipLineToImage(start, end, dw, dh) {
9
+ let t = 1.0;
10
+ const dx = end.x - start.x;
11
+ const dy = end.y - start.y;
12
+ if (end.x < 0)
13
+ t = Math.min(t, (0 - start.x) / dx);
14
+ if (end.x > dw)
15
+ t = Math.min(t, (dw - start.x) / dx);
16
+ if (end.y < 0)
17
+ t = Math.min(t, (0 - start.y) / dy);
18
+ if (end.y > dh)
19
+ t = Math.min(t, (dh - start.y) / dy);
20
+ return {
21
+ x: start.x + t * dx,
22
+ y: start.y + t * dy,
23
+ };
24
+ }
25
+ export function isInsidePolygon(point, vs) {
26
+ let isInside = false;
27
+ for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) {
28
+ const xi = vs[i].x, yi = vs[i].y;
29
+ const xj = vs[j].x, yj = vs[j].y;
30
+ const intersect = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
31
+ if (intersect)
32
+ isInside = !isInside;
33
+ }
34
+ return isInside;
35
+ }
36
+ export function projectOnSegment(p, v, w) {
37
+ const l2 = (w.x - v.x) ** 2 + (w.y - v.y) ** 2;
38
+ if (l2 === 0)
39
+ return v;
40
+ let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
41
+ t = Math.max(0, Math.min(1, t));
42
+ return { x: v.x + t * (w.x - v.x), y: v.y + t * (w.y - v.y) };
43
+ }
44
+ export function distToSegment(p, v, w) {
45
+ const projection = projectOnSegment(p, v, w);
46
+ return Math.sqrt((p.x - projection.x) ** 2 + (p.y - projection.y) ** 2);
47
+ }
@@ -0,0 +1,2 @@
1
+ import { AnnotationsRenderer } from '../../renderer/interface';
2
+ export declare const polygonRenderer: AnnotationsRenderer;