@deepnoid/canvas 0.1.58 → 0.1.59

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 (84) hide show
  1. package/README.md +177 -3
  2. package/dist/engine/annotation/annotationTypes.d.ts +21 -0
  3. package/dist/engine/annotation/annotationTypes.js +6 -0
  4. package/dist/engine/annotation/rectangle/rectangleController.d.ts +26 -0
  5. package/dist/engine/annotation/rectangle/rectangleController.js +115 -0
  6. package/dist/engine/annotation/rectangle/rectangleHitTest.d.ts +7 -0
  7. package/dist/engine/annotation/rectangle/rectangleHitTest.js +102 -0
  8. package/dist/engine/annotation/rectangle/rectangleInteraction.d.ts +17 -0
  9. package/dist/engine/annotation/rectangle/rectangleInteraction.js +30 -0
  10. package/dist/engine/annotation/rectangle/rectangleMath.d.ts +10 -0
  11. package/dist/engine/annotation/rectangle/rectangleMath.js +29 -0
  12. package/dist/engine/annotation/rectangle/rectangleRenderer.d.ts +5 -0
  13. package/dist/engine/annotation/rectangle/rectangleRenderer.js +91 -0
  14. package/dist/engine/annotation/rectangle/rectangleTransform.d.ts +14 -0
  15. package/dist/engine/annotation/rectangle/rectangleTransform.js +65 -0
  16. package/dist/{enum/common.d.ts → engine/annotation/rectangle/rectangleTypes.d.ts} +0 -3
  17. package/dist/{enum/common.js → engine/annotation/rectangle/rectangleTypes.js} +0 -4
  18. package/dist/engine/history.d.ts +11 -0
  19. package/dist/{components/AnnotationCanvas/_utils/createHistory.js → engine/history.js} +4 -4
  20. package/dist/engine/interaction/drawModeRouter.d.ts +3 -0
  21. package/dist/engine/interaction/drawModeRouter.js +56 -0
  22. package/dist/engine/interaction/interactionController.d.ts +13 -0
  23. package/dist/engine/interaction/interactionController.js +53 -0
  24. package/dist/engine/interaction/interface.d.ts +15 -0
  25. package/dist/engine/interaction/panZoomInteraction.d.ts +3 -0
  26. package/dist/engine/interaction/panZoomInteraction.js +29 -0
  27. package/dist/engine/interaction/pointerInteraction.d.ts +16 -0
  28. package/dist/engine/interaction/pointerInteraction.js +48 -0
  29. package/dist/engine/pan-zoom/panZoomController.d.ts +26 -0
  30. package/dist/engine/pan-zoom/panZoomController.js +148 -0
  31. package/dist/engine/pan-zoom/panZoomUtils.d.ts +10 -0
  32. package/dist/engine/pan-zoom/panZoomUtils.js +24 -0
  33. package/dist/engine/public/annotationEngine.d.ts +75 -0
  34. package/dist/engine/public/annotationEngine.js +257 -0
  35. package/dist/engine/renderer/drawCross.d.ts +2 -0
  36. package/dist/engine/renderer/drawCross.js +19 -0
  37. package/dist/engine/renderer/interface.d.ts +4 -0
  38. package/dist/engine/renderer/interface.js +1 -0
  39. package/dist/{types/index.d.ts → engine/types.d.ts} +12 -21
  40. package/dist/engine/types.js +1 -0
  41. package/dist/engine/utils/mousePoint.d.ts +3 -0
  42. package/dist/engine/utils/mousePoint.js +52 -0
  43. package/dist/index.d.ts +4 -5
  44. package/dist/index.js +2 -2
  45. package/dist/{components/AnnotationCanvas/index.d.ts → react/AnnotationCanvas.d.ts} +7 -6
  46. package/dist/react/AnnotationCanvas.js +110 -0
  47. package/dist/{components → react}/index.d.ts +1 -1
  48. package/dist/{components → react}/index.js +1 -1
  49. package/package.json +1 -1
  50. package/dist/components/AnnotationCanvas/_hooks/useImagePanZoom.d.ts +0 -24
  51. package/dist/components/AnnotationCanvas/_hooks/useImagePanZoom.js +0 -143
  52. package/dist/components/AnnotationCanvas/_utils/createHistory.d.ts +0 -11
  53. package/dist/components/AnnotationCanvas/_utils/panZoom.d.ts +0 -10
  54. package/dist/components/AnnotationCanvas/_utils/panZoom.js +0 -29
  55. package/dist/components/AnnotationCanvas/index.js +0 -96
  56. package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangle.d.ts +0 -5
  57. package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangle.js +0 -88
  58. package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangleUtils.d.ts +0 -28
  59. package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangleUtils.js +0 -204
  60. package/dist/components/AnnotationLayer/_hooks/drawEvents/useDrawEvents.d.ts +0 -25
  61. package/dist/components/AnnotationLayer/_hooks/drawEvents/useDrawEvents.js +0 -43
  62. package/dist/components/AnnotationLayer/_hooks/useCanvasDraw.d.ts +0 -13
  63. package/dist/components/AnnotationLayer/_hooks/useCanvasDraw.js +0 -115
  64. package/dist/components/AnnotationLayer/index.d.ts +0 -14
  65. package/dist/components/AnnotationLayer/index.js +0 -122
  66. package/dist/utils/canvas.d.ts +0 -3
  67. package/dist/utils/canvas.js +0 -37
  68. package/dist/utils/pointTransform.d.ts +0 -2
  69. package/dist/utils/pointTransform.js +0 -46
  70. /package/dist/{types/index.js → engine/interaction/interface.js} +0 -0
  71. /package/dist/{utils/common → engine/utils}/cloneDeep.d.ts +0 -0
  72. /package/dist/{utils/common → engine/utils}/cloneDeep.js +0 -0
  73. /package/dist/{utils/common → engine/utils}/deepEqual.d.ts +0 -0
  74. /package/dist/{utils/common → engine/utils}/deepEqual.js +0 -0
  75. /package/dist/{utils/common → engine/utils}/isEqualWith.d.ts +0 -0
  76. /package/dist/{utils/common → engine/utils}/isEqualWith.js +0 -0
  77. /package/dist/{utils → engine/utils}/mouseActions.d.ts +0 -0
  78. /package/dist/{utils → engine/utils}/mouseActions.js +0 -0
  79. /package/dist/{hooks → react/hooks}/useDebounce.d.ts +0 -0
  80. /package/dist/{hooks → react/hooks}/useDebounce.js +0 -0
  81. /package/dist/{components/AnnotationLayer/_hooks → react/hooks}/useHotkeys.d.ts +0 -0
  82. /package/dist/{components/AnnotationLayer/_hooks → react/hooks}/useHotkeys.js +0 -0
  83. /package/dist/{hooks → react/hooks}/useResizeObserver.d.ts +0 -0
  84. /package/dist/{hooks → react/hooks}/useResizeObserver.js +0 -0
package/README.md CHANGED
@@ -1,6 +1,180 @@
1
1
  # deepnoid-canvas
2
2
 
3
- 이 프로젝트는 본부 내 프로젝트에서 사용되는 공통 CANVAS 라이브러리입니다!!!
3
+ 이 프로젝트는 본부 내 프로젝트에서 사용되는 공통 CANVAS 라이브러리입니다.
4
4
 
5
- AnnotationViewer → 전체 이미지 + UI 컨트롤 포함
6
- AnnotationLayer → 실제 좌표/도형을 그리는 캔버스
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
+ - 🔍 Pan & Zoom 지원
16
+ - ↩️ Undo/Redo 기능
17
+ - ⌨️ 단축키 지원
18
+ - 🎨 커스터마이징 가능한 스타일링
19
+
20
+ ## 빠른 시작
21
+
22
+ ### 설치
23
+
24
+ ```bash
25
+ npm install deepnoid-canvas
26
+ ```
27
+
28
+ ### 기본 사용법
29
+
30
+ ```tsx
31
+ import { AnnotationEditor } from 'deepnoid-canvas';
32
+
33
+ function App() {
34
+ const [annotations, setAnnotations] = useState([]);
35
+
36
+ return (
37
+ <AnnotationEditor
38
+ image='https://example.com/image.jpg'
39
+ annotations={annotations}
40
+ setAnnotations={setAnnotations}
41
+ drawing={{
42
+ mode: 'RECTANGLE',
43
+ color: '#FF4136',
44
+ lineSize: 2,
45
+ }}
46
+ options={{
47
+ panZoomEnabled: true,
48
+ zoom: { min: 0.5, max: 4, step: 0.9 },
49
+ }}
50
+ enableHotkeys
51
+ />
52
+ );
53
+ }
54
+ ```
55
+
56
+ ## Props
57
+
58
+ ### AnnotationEditor
59
+
60
+ Editor 모드로 annotation을 생성하고 편집할 수 있습니다.
61
+
62
+ | Prop | Type | Required | Description |
63
+ | ---------------- | ------------------------------------- | -------- | -------------------------------- |
64
+ | `image` | `string` | ✅ | 이미지 URL |
65
+ | `drawing` | `AnnotationCanvasDrawing` | ✅ | 그리기 모드 및 스타일 설정 |
66
+ | `annotations` | `Annotation[]` | - | Annotation 목록 |
67
+ | `setAnnotations` | `(annotations: Annotation[]) => void` | - | Annotation 변경 시 호출되는 콜백 |
68
+ | `options` | `AnnotationCanvasOptions` | - | 줌/팬 및 기타 옵션 |
69
+ | `events` | `AnnotationCanvasEvents` | - | 이미지 로드 관련 이벤트 핸들러 |
70
+ | `enableHotkeys` | `boolean` | - | 단축키 활성화 여부 |
71
+
72
+ ### AnnotationViewer
73
+
74
+ Viewer 모드로 annotation을 읽기 전용으로 표시합니다.
75
+
76
+ | Prop | Type | Required | Description |
77
+ | --------------- | -------------------------------------------------------------------------- | -------- | ------------------------------ |
78
+ | `image` | `string` | ✅ | 이미지 URL |
79
+ | `drawing` | `Pick<AnnotationCanvasDrawing, 'lineSize' \| 'applyStyle'>` | ✅ | 렌더링 스타일 설정 (mode 제외) |
80
+ | `annotations` | `Annotation[]` | - | 표시할 Annotation 목록 |
81
+ | `options` | `AnnotationCanvasOptions` | - | 줌/팬 및 기타 옵션 |
82
+ | `events` | `Pick<AnnotationCanvasEvents, 'onImageLoadSuccess' \| 'onImageLoadError'>` | - | 이미지 로드 관련 이벤트 핸들러 |
83
+ | `enableHotkeys` | `boolean` | - | 단축키 활성화 여부 |
84
+
85
+ ### Drawing 설정
86
+
87
+ ```typescript
88
+ type AnnotationCanvasDrawing = {
89
+ mode?: 'RECTANGLE' | 'POLYGON' | 'NONE'; // 그리기 모드 (Editor 전용)
90
+ color?: string; // Annotation 색상 (HEX)
91
+ lineSize: number; // 선 두께
92
+ label?: Label; // Annotation에 표시할 라벨
93
+ applyStyle: ApplyAnnotationStyle; // 커스텀 스타일 함수
94
+ };
95
+
96
+ type Label = {
97
+ id: number;
98
+ name: string;
99
+ type: string;
100
+ };
101
+
102
+ type ApplyAnnotationStyle = (params: {
103
+ variant: 'drawRect' | 'drawText';
104
+ context: CanvasRenderingContext2D;
105
+ annotation: Annotation;
106
+ drawLineSize: number;
107
+ zoom: number;
108
+ }) => void;
109
+ ```
110
+
111
+ ### Options 설정
112
+
113
+ ```typescript
114
+ type AnnotationCanvasOptions = {
115
+ panZoomEnabled?: boolean; // 줌/팬 기능 활성화 (기본: false)
116
+ zoom?: {
117
+ min?: number; // 최소 줌 배율 (기본: 0.1)
118
+ max?: number; // 최대 줌 배율 (기본: Infinity)
119
+ step?: number; // 줌 단계 (기본: 0.9)
120
+ };
121
+ ZoomButton?: ComponentType; // 커스텀 줌 버튼 컴포넌트
122
+ resetOnImageChange?: boolean; // 이미지 변경 시 줌 리셋 여부
123
+ };
124
+ ```
125
+
126
+ ### Events 설정
127
+
128
+ ```typescript
129
+ type AnnotationCanvasEvents = {
130
+ onImageLoadSuccess?: () => void; // 이미지 로드 성공 시
131
+ onImageLoadError?: (error: Error) => void; // 이미지 로드 실패 시
132
+ };
133
+ ```
134
+
135
+ ## 단축키
136
+
137
+ | 키 | 기능 |
138
+ | -------------- | ----------------------------- |
139
+ | `Delete` | 선택한 annotation 삭제 |
140
+ | `Ctrl+Z` | Undo |
141
+ | `Ctrl+Shift+Z` | Redo |
142
+ | `X` | 선택한 annotation만 보기 토글 |
143
+
144
+ ## 아키텍처
145
+
146
+ ### Facade 패턴
147
+
148
+ ```
149
+ React Component
150
+
151
+ AnnotationEngine (Facade)
152
+ ↓ ↓ ↓ ↓ ↓
153
+ │ │ │ │ └─ History (Undo/Redo)
154
+ │ │ │ └─── PanZoomController
155
+ │ │ └───── InteractionController
156
+ │ └─────── Renderers
157
+ └───────── Annotation Controllers
158
+ ```
159
+
160
+ ### 디렉토리 구조
161
+
162
+ ```
163
+ src/
164
+ ├─ engine/ # TypeScript Engine (순수 로직)
165
+ │ ├─ annotation/ # Annotation 타입별 로직
166
+ │ ├─ interaction/ # 이벤트 처리
167
+ │ ├─ pan-zoom/ # 줌/이동 기능
168
+ │ ├─ public/ # 외부 공개 API
169
+ │ └─ renderer/ # Canvas 렌더링
170
+
171
+ └─ react/ # React 컴포넌트
172
+ ├─ hooks/
173
+ └─ AnnotationCanvas.tsx
174
+ ```
175
+
176
+ ## 설계 원칙
177
+
178
+ 1. **관심사 분리**: Engine(로직) ↔ React(UI) 완전 분리
179
+ 2. **확장성**: 새로운 annotation 타입 추가 용이
180
+ 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,6 @@
1
+ export var DrawMode;
2
+ (function (DrawMode) {
3
+ DrawMode["RECTANGLE"] = "RECTANGLE";
4
+ // POLYGON = 'POLYGON',
5
+ })(DrawMode || (DrawMode = {}));
6
+ // | PolygonAnnotation
@@ -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,91 @@
1
+ import { ACTIVE_POINT_SIZE } from './rectangleMath';
2
+ import { cloneDeep } from '../../utils/cloneDeep';
3
+ import { MouseAction } from '../../utils/mouseActions';
4
+ import { drawCross } from '../../renderer/drawCross';
5
+ import { getRectangleCorners } from './rectangleHitTest';
6
+ export const rectangleRenderer = {
7
+ draw(context, engine) {
8
+ const showSelectedOnly = engine.getShowSelectedOnly();
9
+ const { zoom, dw, dh } = engine.getImageCanvasState();
10
+ const { applyStyle: applyDrawStyle, lineSize: drawLineSize, color: drawColor, mode } = engine.getDrawing();
11
+ const annotations = engine.getAnnotations();
12
+ const editable = engine.isEditable();
13
+ const renderState = engine.getDrawRenderState();
14
+ const cloneAnnotations = cloneDeep(annotations);
15
+ cloneAnnotations.reverse().forEach((annotation) => {
16
+ if (!showSelectedOnly || annotation.selected) {
17
+ applyDrawStyle({ variant: 'drawRect', context, annotation, drawLineSize, zoom });
18
+ applyDrawStyle({ variant: 'drawText', context, annotation, drawLineSize, zoom });
19
+ drawActiveRect(context, annotation, zoom, drawLineSize);
20
+ }
21
+ });
22
+ if (editable && mode && !engine.getSelectedAnnotation()) {
23
+ if (renderState && renderState.mouseAction === MouseAction.LEFT && renderState.startMousePoint) {
24
+ drawNewRectFromPoints(context, renderState.mousePoint, renderState.startMousePoint, zoom, drawLineSize, drawColor);
25
+ }
26
+ const pointerState = engine.getPointerRenderState();
27
+ if (pointerState?.mousePoint) {
28
+ drawCross(context, pointerState.mousePoint, dw, dh, zoom, drawLineSize);
29
+ }
30
+ }
31
+ },
32
+ };
33
+ function drawActiveRect(ctx, annotation, zoom, drawLineSize) {
34
+ const { x, y, width, height, selected } = annotation;
35
+ if (!selected)
36
+ return;
37
+ ctx.lineWidth = drawLineSize / zoom;
38
+ ctx.fillStyle = '#FFF';
39
+ const pointSize = ACTIVE_POINT_SIZE / zoom;
40
+ const points = getRectangleCorners({ x, y, width, height });
41
+ points.forEach((point) => {
42
+ ctx.fillStyle = '#FFF';
43
+ ctx.fillRect(point.x - pointSize / 2, point.y - pointSize / 2, pointSize, pointSize);
44
+ ctx.strokeStyle = '#000';
45
+ ctx.lineWidth = 1 / zoom;
46
+ ctx.strokeRect(point.x - pointSize / 2, point.y - pointSize / 2, pointSize, pointSize);
47
+ });
48
+ }
49
+ export function getRectangleCursor(mousePoint, annotation, zoom) {
50
+ const pointSize = ACTIVE_POINT_SIZE / zoom;
51
+ const { x, y, width: w, height: h } = annotation;
52
+ if (w <= 0 || h <= 0)
53
+ return 'default';
54
+ const mouseX = mousePoint.x;
55
+ const mouseY = mousePoint.y;
56
+ const points = getRectangleCorners({ x, y, width: w, height: h });
57
+ const hit = (px, py) => mouseX >= px - pointSize / 2 &&
58
+ mouseX <= px + pointSize / 2 &&
59
+ mouseY >= py - pointSize / 2 &&
60
+ mouseY <= py + pointSize / 2;
61
+ if (hit(points[0].x, points[0].y))
62
+ return 'nwse-resize';
63
+ if (hit(points[1].x, points[1].y))
64
+ return 'nesw-resize';
65
+ if (hit(points[2].x, points[2].y))
66
+ return 'nesw-resize';
67
+ if (hit(points[3].x, points[3].y))
68
+ return 'nwse-resize';
69
+ if (mouseX >= x && mouseX <= x + w && Math.abs(mouseY - y) <= pointSize / 2)
70
+ return 'ns-resize';
71
+ if (mouseX >= x && mouseX <= x + w && Math.abs(mouseY - (y + h)) <= pointSize / 2)
72
+ return 'ns-resize';
73
+ if (mouseY >= y && mouseY <= y + h && Math.abs(mouseX - x) <= pointSize / 2)
74
+ return 'ew-resize';
75
+ if (mouseY >= y && mouseY <= y + h && Math.abs(mouseX - (x + w)) <= pointSize / 2)
76
+ return 'ew-resize';
77
+ return 'default';
78
+ }
79
+ function drawNewRectFromPoints(context, startPoint, endPoint, zoom, drawLineSize, color) {
80
+ if (!color)
81
+ return;
82
+ const x = Math.min(endPoint.x, startPoint.x);
83
+ const y = Math.min(endPoint.y, startPoint.y);
84
+ const width = Math.abs(endPoint.x - startPoint.x);
85
+ const height = Math.abs(endPoint.y - startPoint.y);
86
+ context.beginPath();
87
+ context.lineWidth = drawLineSize / zoom;
88
+ context.rect(x, y, width, height);
89
+ context.strokeStyle = color;
90
+ context.stroke();
91
+ }
@@ -0,0 +1,14 @@
1
+ import { Point } from '../../types';
2
+ import { Annotation } from '../annotationTypes';
3
+ import { RectangleAnchor } from './rectangleTypes';
4
+ type RectangleParams = {
5
+ annotations: Annotation[];
6
+ dw: number;
7
+ dh: number;
8
+ mousePoint: Point;
9
+ prevMousePoint: Point;
10
+ rectangleAnchor: RectangleAnchor | null;
11
+ };
12
+ export declare function applyRectangleMove({ annotations, dw, dh, mousePoint, prevMousePoint }: RectangleParams): void;
13
+ export declare function applyRectangleResize({ annotations, dw, dh, mousePoint, prevMousePoint, rectangleAnchor, }: RectangleParams): void;
14
+ export {};