@deepnoid/canvas 0.1.79 → 0.1.80
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 +19 -8
- package/dist/engine/annotation/annotationTypes.d.ts +12 -2
- package/dist/engine/annotation/annotationTypes.js +1 -2
- package/dist/engine/annotation/hitTest.d.ts +5 -0
- package/dist/engine/annotation/hitTest.js +63 -0
- package/dist/engine/annotation/polygon/polygonController.d.ts +34 -0
- package/dist/engine/annotation/polygon/polygonController.js +252 -0
- package/dist/engine/annotation/polygon/polygonHitTest.d.ts +6 -0
- package/dist/engine/annotation/polygon/polygonHitTest.js +84 -0
- package/dist/engine/annotation/polygon/polygonInteraction.d.ts +19 -0
- package/dist/engine/annotation/polygon/polygonInteraction.js +36 -0
- package/dist/engine/annotation/polygon/polygonMath.d.ts +7 -0
- package/dist/engine/annotation/polygon/polygonMath.js +47 -0
- package/dist/engine/annotation/polygon/polygonRenderer.d.ts +2 -0
- package/dist/engine/annotation/polygon/polygonRenderer.js +79 -0
- package/dist/engine/annotation/polygon/polygonTransform.d.ts +14 -0
- package/dist/engine/annotation/polygon/polygonTransform.js +68 -0
- package/dist/engine/annotation/polygon/polygonTypes.d.ts +9 -0
- package/dist/engine/annotation/polygon/polygonTypes.js +1 -0
- package/dist/engine/annotation/rectangle/rectangleController.js +8 -15
- package/dist/engine/annotation/rectangle/rectangleHitTest.js +5 -4
- package/dist/engine/annotation/rectangle/rectangleRenderer.js +8 -0
- package/dist/engine/annotation/rectangle/rectangleTransform.js +31 -28
- package/dist/engine/interaction/drawModeRouter.js +47 -27
- package/dist/engine/interaction/interactionController.d.ts +1 -0
- package/dist/engine/interaction/interactionController.js +17 -0
- package/dist/engine/interaction/interface.d.ts +1 -0
- package/dist/engine/interaction/pointerInteraction.js +7 -2
- package/dist/engine/public/annotationEngine.d.ts +3 -1
- package/dist/engine/public/annotationEngine.js +19 -1
- package/dist/react/AnnotationCanvas.js +6 -1
- package/dist/react/hooks/useHotkeys.d.ts +2 -1
- package/dist/react/hooks/useHotkeys.js +4 -2
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ACTIVE_POINT_SIZE, clipLineToImage } from './polygonMath';
|
|
2
|
+
import { drawCross } from '../../renderer/drawCross';
|
|
3
|
+
import { DrawMode } from '../annotationTypes';
|
|
4
|
+
export const polygonRenderer = {
|
|
5
|
+
drawAnnotation(context, engine, annotation) {
|
|
6
|
+
if (annotation.type !== DrawMode.POLYGON)
|
|
7
|
+
return;
|
|
8
|
+
const { zoom } = engine.getImageCanvasState();
|
|
9
|
+
const { applyStyle, lineSize } = engine.getDrawing();
|
|
10
|
+
applyStyle({ variant: 'drawRect', context, annotation, drawLineSize: lineSize, zoom });
|
|
11
|
+
applyStyle({ variant: 'drawText', context, annotation, drawLineSize: lineSize, zoom });
|
|
12
|
+
if (annotation.selected && engine.isEditable()) {
|
|
13
|
+
const renderState = engine.getDrawRenderState();
|
|
14
|
+
drawActivePoints(context, annotation, zoom, renderState?.anchor);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
drawEditorUI(context, engine) {
|
|
18
|
+
if (engine.getSelectedAnnotation())
|
|
19
|
+
return;
|
|
20
|
+
const renderState = engine.getDrawRenderState();
|
|
21
|
+
const pointerState = engine.getPointerRenderState();
|
|
22
|
+
const { zoom, dw, dh } = engine.getImageCanvasState();
|
|
23
|
+
const { lineSize, color } = engine.getDrawing();
|
|
24
|
+
if (renderState && Array.isArray(renderState.drawingPoints) && renderState.drawingPoints.length > 0) {
|
|
25
|
+
// Draw already placed points
|
|
26
|
+
drawPolygon(context, renderState.drawingPoints, zoom, lineSize, color || '#00ff00', false);
|
|
27
|
+
// Draw line from last point to current mouse
|
|
28
|
+
if (renderState.mousePoint) {
|
|
29
|
+
context.beginPath();
|
|
30
|
+
context.lineWidth = lineSize / zoom;
|
|
31
|
+
context.strokeStyle = color || '#FF4136';
|
|
32
|
+
const lastPoint = renderState.drawingPoints[renderState.drawingPoints.length - 1];
|
|
33
|
+
context.moveTo(lastPoint.x, lastPoint.y);
|
|
34
|
+
const clampedMouse = clipLineToImage(lastPoint, renderState.mousePoint, dw, dh);
|
|
35
|
+
context.lineTo(clampedMouse.x, clampedMouse.y);
|
|
36
|
+
context.stroke();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (pointerState?.mousePoint && !engine.getSelectedAnnotation()) {
|
|
40
|
+
drawCross(context, pointerState.mousePoint, dw, dh, zoom, lineSize);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
function drawPolygon(context, points, zoom, drawLineSize, color, closePath) {
|
|
45
|
+
if (points.length === 0)
|
|
46
|
+
return;
|
|
47
|
+
context.beginPath();
|
|
48
|
+
context.lineWidth = drawLineSize / zoom;
|
|
49
|
+
context.strokeStyle = color || '#FF4136';
|
|
50
|
+
context.moveTo(points[0].x, points[0].y);
|
|
51
|
+
for (let i = 1; i < points.length; i++) {
|
|
52
|
+
context.lineTo(points[i].x, points[i].y);
|
|
53
|
+
}
|
|
54
|
+
if (closePath && points.length > 2) {
|
|
55
|
+
context.closePath();
|
|
56
|
+
}
|
|
57
|
+
context.stroke();
|
|
58
|
+
}
|
|
59
|
+
function drawActivePoints(ctx, annotation, zoom, anchor) {
|
|
60
|
+
const { points, selected } = annotation;
|
|
61
|
+
if (!selected)
|
|
62
|
+
return;
|
|
63
|
+
const pointSize = ACTIVE_POINT_SIZE / zoom;
|
|
64
|
+
points.forEach((point) => {
|
|
65
|
+
ctx.fillStyle = '#FFF';
|
|
66
|
+
ctx.fillRect(point.x - pointSize / 2, point.y - pointSize / 2, pointSize, pointSize);
|
|
67
|
+
ctx.strokeStyle = '#000';
|
|
68
|
+
ctx.lineWidth = 1 / zoom;
|
|
69
|
+
ctx.strokeRect(point.x - pointSize / 2, point.y - pointSize / 2, pointSize, pointSize);
|
|
70
|
+
});
|
|
71
|
+
if (anchor && anchor.type === 'edge' && anchor.insertPoint) {
|
|
72
|
+
const pt = anchor.insertPoint;
|
|
73
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
|
|
74
|
+
ctx.fillRect(pt.x - pointSize / 2, pt.y - pointSize / 2, pointSize, pointSize);
|
|
75
|
+
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
|
|
76
|
+
ctx.lineWidth = 1 / zoom;
|
|
77
|
+
ctx.strokeRect(pt.x - pointSize / 2, pt.y - pointSize / 2, pointSize, pointSize);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Annotation } from '../annotationTypes';
|
|
2
|
+
import { PolygonAnchor } from './polygonTypes';
|
|
3
|
+
import { Point } from '../../types';
|
|
4
|
+
type Params = {
|
|
5
|
+
annotations: Annotation[];
|
|
6
|
+
dw: number;
|
|
7
|
+
dh: number;
|
|
8
|
+
mousePoint: Point;
|
|
9
|
+
prevMousePoint: Point;
|
|
10
|
+
polygonAnchor: PolygonAnchor | null;
|
|
11
|
+
};
|
|
12
|
+
export declare function applyPolygonMove({ annotations, mousePoint, prevMousePoint, dw, dh }: Params): void;
|
|
13
|
+
export declare function applyPolygonResize({ annotations, mousePoint, prevMousePoint, polygonAnchor, dw, dh }: Params): void;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { DrawMode } from '../annotationTypes';
|
|
2
|
+
export function applyPolygonMove({ annotations, mousePoint, prevMousePoint, dw, dh }) {
|
|
3
|
+
const selected = annotations.find((a) => a.selected);
|
|
4
|
+
if (!selected || selected.type !== DrawMode.POLYGON)
|
|
5
|
+
return;
|
|
6
|
+
let dx = mousePoint.x - prevMousePoint.x;
|
|
7
|
+
let dy = mousePoint.y - prevMousePoint.y;
|
|
8
|
+
let minX = Number.MAX_SAFE_INTEGER, minY = Number.MAX_SAFE_INTEGER, maxX = Number.MIN_SAFE_INTEGER, maxY = Number.MIN_SAFE_INTEGER;
|
|
9
|
+
for (const p of selected.points) {
|
|
10
|
+
if (p.x < minX)
|
|
11
|
+
minX = p.x;
|
|
12
|
+
if (p.x > maxX)
|
|
13
|
+
maxX = p.x;
|
|
14
|
+
if (p.y < minY)
|
|
15
|
+
minY = p.y;
|
|
16
|
+
if (p.y > maxY)
|
|
17
|
+
maxY = p.y;
|
|
18
|
+
}
|
|
19
|
+
if (minX + dx < 0)
|
|
20
|
+
dx = -minX;
|
|
21
|
+
if (maxX + dx > dw)
|
|
22
|
+
dx = dw - maxX;
|
|
23
|
+
if (minY + dy < 0)
|
|
24
|
+
dy = -minY;
|
|
25
|
+
if (maxY + dy > dh)
|
|
26
|
+
dy = dh - maxY;
|
|
27
|
+
const newPoints = selected.points.map((p) => ({
|
|
28
|
+
x: p.x + dx,
|
|
29
|
+
y: p.y + dy,
|
|
30
|
+
}));
|
|
31
|
+
selected.points = newPoints;
|
|
32
|
+
}
|
|
33
|
+
export function applyPolygonResize({ annotations, mousePoint, prevMousePoint, polygonAnchor, dw, dh }) {
|
|
34
|
+
const selected = annotations.find((a) => a.selected);
|
|
35
|
+
if (!selected || selected.type !== DrawMode.POLYGON || !polygonAnchor)
|
|
36
|
+
return;
|
|
37
|
+
if (polygonAnchor.type === 'point') {
|
|
38
|
+
selected.points[polygonAnchor.pointIndex] = {
|
|
39
|
+
x: Math.max(0, Math.min(dw, mousePoint.x)),
|
|
40
|
+
y: Math.max(0, Math.min(dh, mousePoint.y)),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
else if (polygonAnchor.type === 'edge') {
|
|
44
|
+
let dx = mousePoint.x - prevMousePoint.x;
|
|
45
|
+
let dy = mousePoint.y - prevMousePoint.y;
|
|
46
|
+
const p1 = selected.points[polygonAnchor.edgeIndex];
|
|
47
|
+
const p2Offset = (polygonAnchor.edgeIndex + 1) % selected.points.length;
|
|
48
|
+
const p2 = selected.points[p2Offset];
|
|
49
|
+
let allowedDx = dx;
|
|
50
|
+
let allowedDy = dy;
|
|
51
|
+
const minX = Math.min(p1.x, p2.x);
|
|
52
|
+
const maxX = Math.max(p1.x, p2.x);
|
|
53
|
+
const minY = Math.min(p1.y, p2.y);
|
|
54
|
+
const maxY = Math.max(p1.y, p2.y);
|
|
55
|
+
if (minX + allowedDx < 0)
|
|
56
|
+
allowedDx = -minX;
|
|
57
|
+
if (maxX + allowedDx > dw)
|
|
58
|
+
allowedDx = dw - maxX;
|
|
59
|
+
if (minY + allowedDy < 0)
|
|
60
|
+
allowedDy = -minY;
|
|
61
|
+
if (maxY + allowedDy > dh)
|
|
62
|
+
allowedDy = dh - maxY;
|
|
63
|
+
p1.x += allowedDx;
|
|
64
|
+
p1.y += allowedDy;
|
|
65
|
+
p2.x += allowedDx;
|
|
66
|
+
p2.y += allowedDy;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -2,7 +2,8 @@ import { cloneDeep } from '../../utils/cloneDeep';
|
|
|
2
2
|
import { isMouseClickAction, isMouseDragAction, MouseAction } from '../../utils/mouseActions';
|
|
3
3
|
import { isInsideImage, clampBoundingBoxToImage } from './rectangleMath';
|
|
4
4
|
import { getCanvasMousePoint } from '../../utils/mousePoint';
|
|
5
|
-
import {
|
|
5
|
+
import { updateActiveRectangleAnchor } from './rectangleHitTest';
|
|
6
|
+
import { selectAnnotationAtPoint } from '../hitTest';
|
|
6
7
|
import { applyRectangleMove, applyRectangleResize } from './rectangleTransform';
|
|
7
8
|
import { DrawMode } from '../annotationTypes';
|
|
8
9
|
const DRAG_THRESHOLD = 3;
|
|
@@ -45,21 +46,13 @@ export class RectangleController {
|
|
|
45
46
|
this.hasMoved = false;
|
|
46
47
|
const annotations = this.engine.getAnnotations();
|
|
47
48
|
const currentSelected = this.engine.getSelectedAnnotation();
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.targetIndexOnBegin = targetAnnotation
|
|
52
|
-
? annotations.findIndex((a) => a.x === targetAnnotation.x &&
|
|
53
|
-
a.y === targetAnnotation.y &&
|
|
54
|
-
a.width === targetAnnotation.width &&
|
|
55
|
-
a.height === targetAnnotation.height)
|
|
56
|
-
: -1;
|
|
57
|
-
this.rectangleAnchor = updateActiveRectangleAnchor(zoom, mousePoint, currentSelected);
|
|
58
|
-
if (currentSelectedIndex >= 0 && this.targetIndexOnBegin !== currentSelectedIndex) {
|
|
59
|
-
this.engine.setSelectedAnnotation(null);
|
|
60
|
-
annotations.forEach((a) => (a.selected = false));
|
|
49
|
+
const targetAnnotation = selectAnnotationAtPoint(zoom, mousePoint, annotations);
|
|
50
|
+
if (targetAnnotation !== currentSelected) {
|
|
51
|
+
this.engine.setSelectedAnnotation(targetAnnotation);
|
|
61
52
|
this.engine.setAnnotations(annotations);
|
|
62
53
|
}
|
|
54
|
+
this.targetIndexOnBegin = targetAnnotation ? annotations.indexOf(targetAnnotation) : -1;
|
|
55
|
+
this.rectangleAnchor = updateActiveRectangleAnchor(zoom, mousePoint, targetAnnotation);
|
|
63
56
|
return true;
|
|
64
57
|
}
|
|
65
58
|
move(event) {
|
|
@@ -134,7 +127,7 @@ export class RectangleController {
|
|
|
134
127
|
}
|
|
135
128
|
else if (!this.hasMoved) {
|
|
136
129
|
const annotations = this.engine.getAnnotations();
|
|
137
|
-
const selected =
|
|
130
|
+
const selected = selectAnnotationAtPoint(zoom, mousePoint, annotations);
|
|
138
131
|
this.engine.setSelectedAnnotation(selected);
|
|
139
132
|
this.engine.setAnnotations(annotations);
|
|
140
133
|
}
|
|
@@ -48,12 +48,13 @@ export function selectRectangleAtPoint(zoom, mousePoint, annotations) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
export function updateActiveRectangleAnchor(zoom, mousePoint, selectedAnnotation) {
|
|
51
|
-
if (!selectedAnnotation)
|
|
51
|
+
if (!selectedAnnotation || selectedAnnotation.type !== DrawMode.RECTANGLE)
|
|
52
52
|
return null;
|
|
53
|
-
const
|
|
53
|
+
const rect = selectedAnnotation;
|
|
54
|
+
const points = getRectangleCorners(rect);
|
|
54
55
|
const pointSize = ACTIVE_POINT_SIZE / zoom;
|
|
55
56
|
const { x: mouseX, y: mouseY } = mousePoint;
|
|
56
|
-
const { x, y, width: w, height: h } =
|
|
57
|
+
const { x, y, width: w, height: h } = rect;
|
|
57
58
|
if (mouseX >= points[0].x - pointSize / 2 &&
|
|
58
59
|
mouseX <= points[0].x + pointSize / 2 &&
|
|
59
60
|
mouseY >= points[0].y - pointSize / 2 &&
|
|
@@ -96,7 +97,7 @@ export function updateActiveRectangleAnchor(zoom, mousePoint, selectedAnnotation
|
|
|
96
97
|
}
|
|
97
98
|
export function getRectangleCursorByAnnotations(mousePoint, annotations, zoom) {
|
|
98
99
|
const target = annotations.find((a) => a.selected);
|
|
99
|
-
if (!target)
|
|
100
|
+
if (!target || target.type !== DrawMode.RECTANGLE)
|
|
100
101
|
return 'default';
|
|
101
102
|
return getRectangleCursor(mousePoint, target, zoom);
|
|
102
103
|
}
|
|
@@ -2,6 +2,7 @@ import { ACTIVE_POINT_SIZE } from './rectangleMath';
|
|
|
2
2
|
import { MouseAction } from '../../utils/mouseActions';
|
|
3
3
|
import { drawCross } from '../../renderer/drawCross';
|
|
4
4
|
import { getRectangleCorners } from './rectangleHitTest';
|
|
5
|
+
import { DrawMode } from '../annotationTypes';
|
|
5
6
|
export const rectangleRenderer = {
|
|
6
7
|
drawAnnotation(context, engine, annotation) {
|
|
7
8
|
const { zoom } = engine.getImageCanvasState();
|
|
@@ -28,6 +29,8 @@ export const rectangleRenderer = {
|
|
|
28
29
|
},
|
|
29
30
|
};
|
|
30
31
|
function drawActiveRect(ctx, annotation, zoom, drawLineSize) {
|
|
32
|
+
if (annotation.type !== DrawMode.RECTANGLE)
|
|
33
|
+
return;
|
|
31
34
|
const { x, y, width, height, selected } = annotation;
|
|
32
35
|
if (!selected)
|
|
33
36
|
return;
|
|
@@ -44,6 +47,8 @@ function drawActiveRect(ctx, annotation, zoom, drawLineSize) {
|
|
|
44
47
|
});
|
|
45
48
|
}
|
|
46
49
|
export function getRectangleCursor(mousePoint, annotation, zoom) {
|
|
50
|
+
if (annotation.type !== DrawMode.RECTANGLE)
|
|
51
|
+
return 'default';
|
|
47
52
|
const pointSize = ACTIVE_POINT_SIZE / zoom;
|
|
48
53
|
const { x, y, width: w, height: h } = annotation;
|
|
49
54
|
if (w <= 0 || h <= 0)
|
|
@@ -71,6 +76,9 @@ export function getRectangleCursor(mousePoint, annotation, zoom) {
|
|
|
71
76
|
return 'ew-resize';
|
|
72
77
|
if (mouseY >= y && mouseY <= y + h && Math.abs(mouseX - (x + w)) <= pointSize / 2)
|
|
73
78
|
return 'ew-resize';
|
|
79
|
+
if (mouseX > x && mouseX < x + w && mouseY > y && mouseY < y + h) {
|
|
80
|
+
return 'move';
|
|
81
|
+
}
|
|
74
82
|
return 'default';
|
|
75
83
|
}
|
|
76
84
|
function drawNewRectFromPoints(context, startPoint, endPoint, zoom, drawLineSize, color) {
|
|
@@ -1,65 +1,68 @@
|
|
|
1
|
+
import { DrawMode } from '../annotationTypes';
|
|
1
2
|
import { RectangleAnchor } from './rectangleTypes';
|
|
2
3
|
export function applyRectangleMove({ annotations, dw, dh, mousePoint, prevMousePoint }) {
|
|
3
4
|
if (!mousePoint || !prevMousePoint)
|
|
4
5
|
return;
|
|
5
6
|
const findIndex = annotations.findIndex((annotation) => annotation.selected);
|
|
6
7
|
const findAnnotation = annotations[findIndex];
|
|
7
|
-
if (findIndex < 0 || !findAnnotation)
|
|
8
|
+
if (findIndex < 0 || !findAnnotation || findAnnotation.type !== DrawMode.RECTANGLE)
|
|
8
9
|
return;
|
|
10
|
+
const rect = findAnnotation;
|
|
9
11
|
const dx = mousePoint.x - prevMousePoint.x;
|
|
10
12
|
const dy = mousePoint.y - prevMousePoint.y;
|
|
11
|
-
const newX =
|
|
12
|
-
const newY =
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const newX = rect.x + dx;
|
|
14
|
+
const newY = rect.y + dy;
|
|
15
|
+
rect.x = Math.max(0, Math.min(newX, dw - rect.width));
|
|
16
|
+
rect.y = Math.max(0, Math.min(newY, dh - rect.height));
|
|
15
17
|
}
|
|
16
18
|
export function applyRectangleResize({ annotations, dw, dh, mousePoint, prevMousePoint, rectangleAnchor, }) {
|
|
17
19
|
const findIndex = annotations.findIndex((annotation) => annotation.selected);
|
|
18
20
|
const findAnnotation = annotations[findIndex];
|
|
19
|
-
if (findIndex >= 0 && findAnnotation) {
|
|
21
|
+
if (findIndex >= 0 && findAnnotation && findAnnotation.type === DrawMode.RECTANGLE) {
|
|
22
|
+
const rect = findAnnotation;
|
|
20
23
|
const dx = mousePoint.x - prevMousePoint.x;
|
|
21
24
|
const dy = mousePoint.y - prevMousePoint.y;
|
|
22
25
|
switch (rectangleAnchor) {
|
|
23
26
|
case RectangleAnchor.LEFT_TOP:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
rect.x += dx;
|
|
28
|
+
rect.y += dy;
|
|
29
|
+
rect.width -= dx;
|
|
30
|
+
rect.height -= dy;
|
|
28
31
|
break;
|
|
29
32
|
case RectangleAnchor.RIGHT_TOP:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
rect.y += dy;
|
|
34
|
+
rect.width += dx;
|
|
35
|
+
rect.height -= dy;
|
|
33
36
|
break;
|
|
34
37
|
case RectangleAnchor.LEFT_BOTTOM:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
rect.x += dx;
|
|
39
|
+
rect.width -= dx;
|
|
40
|
+
rect.height += dy;
|
|
38
41
|
break;
|
|
39
42
|
case RectangleAnchor.RIGHT_BOTTOM:
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
rect.width += dx;
|
|
44
|
+
rect.height += dy;
|
|
42
45
|
break;
|
|
43
46
|
case RectangleAnchor.TOP:
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
rect.y += dy;
|
|
48
|
+
rect.height -= dy;
|
|
46
49
|
break;
|
|
47
50
|
case RectangleAnchor.BOTTOM:
|
|
48
|
-
|
|
51
|
+
rect.height += dy;
|
|
49
52
|
break;
|
|
50
53
|
case RectangleAnchor.LEFT:
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
rect.x += dx;
|
|
55
|
+
rect.width -= dx;
|
|
53
56
|
break;
|
|
54
57
|
case RectangleAnchor.RIGHT:
|
|
55
|
-
|
|
58
|
+
rect.width += dx;
|
|
56
59
|
break;
|
|
57
60
|
default:
|
|
58
61
|
break;
|
|
59
62
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
rect.x = Math.max(0, Math.min(rect.x, dw - rect.width));
|
|
64
|
+
rect.y = Math.max(0, Math.min(rect.y, dh - rect.height));
|
|
65
|
+
rect.width = Math.max(0, Math.min(rect.width, dw - rect.x));
|
|
66
|
+
rect.height = Math.max(0, Math.min(rect.height, dh - rect.y));
|
|
64
67
|
}
|
|
65
68
|
}
|
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
import { RectangleController } from '../annotation/rectangle/rectangleController';
|
|
2
2
|
import { RectangleInteraction } from '../annotation/rectangle/rectangleInteraction';
|
|
3
|
+
import { PolygonController } from '../annotation/polygon/polygonController';
|
|
4
|
+
import { PolygonInteraction } from '../annotation/polygon/polygonInteraction';
|
|
3
5
|
import { DrawMode } from '../annotation/annotationTypes';
|
|
4
6
|
export function drawModeRouter(engine) {
|
|
5
|
-
const
|
|
7
|
+
const rectangleCtrl = new RectangleController(engine);
|
|
8
|
+
const polygonCtrl = new PolygonController(engine);
|
|
9
|
+
const rectangle = new RectangleInteraction(rectangleCtrl);
|
|
10
|
+
const polygon = new PolygonInteraction(polygonCtrl);
|
|
11
|
+
let currentDragInteraction = null;
|
|
12
|
+
function getActiveInteraction() {
|
|
13
|
+
// If an interaction is mid-draw, keep it stickied (Polygon can take multiple clicks)
|
|
14
|
+
if (polygonCtrl.isDrawingNewPolygon)
|
|
15
|
+
return polygon;
|
|
16
|
+
if (rectangleCtrl.isDrawingNewRectangle)
|
|
17
|
+
return rectangle;
|
|
18
|
+
const selected = engine.getSelectedAnnotation();
|
|
19
|
+
if (selected) {
|
|
20
|
+
if (selected.type === DrawMode.RECTANGLE)
|
|
21
|
+
return rectangle;
|
|
22
|
+
if (selected.type === DrawMode.POLYGON)
|
|
23
|
+
return polygon;
|
|
24
|
+
}
|
|
25
|
+
const drawMode = engine.getDrawing().mode;
|
|
26
|
+
if (drawMode === DrawMode.RECTANGLE)
|
|
27
|
+
return rectangle;
|
|
28
|
+
if (drawMode === DrawMode.POLYGON)
|
|
29
|
+
return polygon;
|
|
30
|
+
return rectangle; // fallback
|
|
31
|
+
}
|
|
6
32
|
return {
|
|
7
33
|
getType() {
|
|
8
34
|
const drawMode = engine.getDrawing().mode;
|
|
@@ -11,44 +37,38 @@ export function drawModeRouter(engine) {
|
|
|
11
37
|
return false;
|
|
12
38
|
},
|
|
13
39
|
getRenderState() {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
return rectangle.getRenderState();
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
40
|
+
const active = getActiveInteraction();
|
|
41
|
+
return active.getRenderState ? active.getRenderState() : null;
|
|
19
42
|
},
|
|
20
43
|
getRedrawLayers() {
|
|
21
|
-
|
|
22
|
-
return rectangle.getRedrawLayers();
|
|
23
|
-
}
|
|
24
|
-
return {
|
|
25
|
-
imageCanvas: false,
|
|
26
|
-
annotationsCanvas: false,
|
|
27
|
-
};
|
|
44
|
+
return getActiveInteraction().getRedrawLayers?.() || { imageCanvas: false, annotationsCanvas: false };
|
|
28
45
|
},
|
|
29
46
|
handleMouseDown(e) {
|
|
30
47
|
if (!engine.isEditable())
|
|
31
48
|
return false;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
return false;
|
|
49
|
+
currentDragInteraction = getActiveInteraction();
|
|
50
|
+
return currentDragInteraction.handleMouseDown?.(e) ?? false;
|
|
36
51
|
},
|
|
37
52
|
handleMouseMove(e) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
53
|
+
const active = currentDragInteraction || getActiveInteraction();
|
|
54
|
+
return active.handleMouseMove?.(e) ?? false;
|
|
42
55
|
},
|
|
43
56
|
handleMouseUp(e) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
57
|
+
const active = currentDragInteraction || getActiveInteraction();
|
|
58
|
+
const result = active.handleMouseUp?.(e) ?? false;
|
|
59
|
+
currentDragInteraction = null;
|
|
60
|
+
return result;
|
|
48
61
|
},
|
|
49
62
|
handleMouseLeave(e) {
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
const active = currentDragInteraction || getActiveInteraction();
|
|
64
|
+
const result = active.handleMouseLeave?.(e) ?? false;
|
|
65
|
+
currentDragInteraction = null;
|
|
66
|
+
return result;
|
|
67
|
+
},
|
|
68
|
+
handleKeyDown(e) {
|
|
69
|
+
const active = getActiveInteraction();
|
|
70
|
+
if (active.handleKeyDown) {
|
|
71
|
+
return active.handleKeyDown(e);
|
|
52
72
|
}
|
|
53
73
|
return false;
|
|
54
74
|
},
|
|
@@ -42,6 +42,23 @@ export class InteractionController {
|
|
|
42
42
|
break;
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
|
+
this.handleKeyDown = (e) => {
|
|
46
|
+
const layers = {
|
|
47
|
+
imageCanvas: false,
|
|
48
|
+
annotationsCanvas: false,
|
|
49
|
+
};
|
|
50
|
+
for (const interaction of this.interactions) {
|
|
51
|
+
const handled = interaction.handleKeyDown?.(e);
|
|
52
|
+
const interactionLayers = interaction.getRedrawLayers?.();
|
|
53
|
+
if (interactionLayers) {
|
|
54
|
+
layers.imageCanvas = layers.imageCanvas || interactionLayers.imageCanvas;
|
|
55
|
+
layers.annotationsCanvas = layers.annotationsCanvas || interactionLayers.annotationsCanvas;
|
|
56
|
+
}
|
|
57
|
+
if (handled)
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
return layers;
|
|
61
|
+
};
|
|
45
62
|
}
|
|
46
63
|
getInteractions() {
|
|
47
64
|
return this.interactions;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DrawMode } from '../annotation/annotationTypes';
|
|
2
2
|
import { getRectangleCursor } from '../annotation/rectangle/rectangleRenderer';
|
|
3
|
+
import { getPolygonCursor } from '../annotation/polygon/polygonHitTest';
|
|
3
4
|
import { getCanvasMousePoint } from '../utils/mousePoint';
|
|
4
5
|
export class PointerInteraction {
|
|
5
6
|
constructor(engine) {
|
|
@@ -37,11 +38,15 @@ export class PointerInteraction {
|
|
|
37
38
|
const canvas = this.engine.getAnnotationsCanvas();
|
|
38
39
|
const annotation = this.engine.getSelectedAnnotation();
|
|
39
40
|
const { zoom } = this.engine.getImageCanvasState();
|
|
40
|
-
const { mode } = this.engine.getDrawing();
|
|
41
41
|
let cursor = 'default';
|
|
42
42
|
if (annotation && this.mousePoint) {
|
|
43
|
-
if (
|
|
43
|
+
if (annotation.type === DrawMode.RECTANGLE) {
|
|
44
44
|
cursor = getRectangleCursor(this.mousePoint, annotation, zoom);
|
|
45
|
+
}
|
|
46
|
+
else if (annotation.type === DrawMode.POLYGON) {
|
|
47
|
+
const renderState = this.engine.getDrawRenderState();
|
|
48
|
+
cursor = getPolygonCursor(this.mousePoint, annotation, zoom, renderState?.isEdgeInsertReady);
|
|
49
|
+
}
|
|
45
50
|
}
|
|
46
51
|
canvas.style.cursor = cursor;
|
|
47
52
|
}
|
|
@@ -51,11 +51,12 @@ export declare class AnnotationEngine {
|
|
|
51
51
|
clearCanvasAll(): void;
|
|
52
52
|
private clearCanvas;
|
|
53
53
|
resetCanvas(): void;
|
|
54
|
-
getAnnotations():
|
|
54
|
+
getAnnotations(): Annotation[];
|
|
55
55
|
setAnnotations(annotation: Annotation[]): void;
|
|
56
56
|
appendAnnotation(annotation: Annotation): void;
|
|
57
57
|
private syncAnnotations;
|
|
58
58
|
private drawAnnotations;
|
|
59
|
+
redrawAnnotations(): void;
|
|
59
60
|
getSelectedAnnotation(): Annotation | null;
|
|
60
61
|
setSelectedAnnotation(target: Annotation | null): void;
|
|
61
62
|
getPointerRenderState(): {
|
|
@@ -67,6 +68,7 @@ export declare class AnnotationEngine {
|
|
|
67
68
|
onMouseUp(e: MouseEvent): void;
|
|
68
69
|
onMouseLeave(e: MouseEvent): void;
|
|
69
70
|
onWheel(e: WheelEvent): void;
|
|
71
|
+
onKeyDown(e: KeyboardEvent): void;
|
|
70
72
|
initHistory(annotations: Annotation[]): void;
|
|
71
73
|
commitHistory(checkInit?: boolean): void;
|
|
72
74
|
deleteSelected(): void;
|
|
@@ -6,13 +6,14 @@ import { panZoomInteraction } from '../interaction/panZoomInteraction';
|
|
|
6
6
|
import { rectangleRenderer } from '../annotation/rectangle/rectangleRenderer';
|
|
7
7
|
import { PointerInteraction } from '../interaction/pointerInteraction';
|
|
8
8
|
import { drawModeRouter } from '../interaction/drawModeRouter';
|
|
9
|
+
import { polygonRenderer } from '../annotation/polygon/polygonRenderer';
|
|
9
10
|
export class AnnotationEngine {
|
|
10
11
|
constructor(params) {
|
|
11
12
|
this.history = null;
|
|
12
13
|
this.editable = true;
|
|
13
14
|
this.renderers = {
|
|
14
15
|
RECTANGLE: rectangleRenderer,
|
|
15
|
-
|
|
16
|
+
POLYGON: polygonRenderer,
|
|
16
17
|
};
|
|
17
18
|
this.imageCanvas = params.imageCanvas;
|
|
18
19
|
this.image = params.image;
|
|
@@ -181,6 +182,9 @@ export class AnnotationEngine {
|
|
|
181
182
|
renderer?.drawEditorUI?.(context, this);
|
|
182
183
|
}
|
|
183
184
|
}
|
|
185
|
+
redrawAnnotations() {
|
|
186
|
+
this.drawAnnotationsCanvas();
|
|
187
|
+
}
|
|
184
188
|
getSelectedAnnotation() {
|
|
185
189
|
return this.annotations.find((a) => a.selected) ?? null;
|
|
186
190
|
}
|
|
@@ -196,6 +200,11 @@ export class AnnotationEngine {
|
|
|
196
200
|
if (changed) {
|
|
197
201
|
this.drawAnnotationsCanvas();
|
|
198
202
|
}
|
|
203
|
+
if (target) {
|
|
204
|
+
// Dispatch custom event so React components can sync their drawMode state
|
|
205
|
+
const event = new CustomEvent('annotationSelected', { detail: { type: target.type } });
|
|
206
|
+
window.dispatchEvent(event);
|
|
207
|
+
}
|
|
199
208
|
}
|
|
200
209
|
// ----------------- Interaction -----------------
|
|
201
210
|
getPointerRenderState() {
|
|
@@ -232,6 +241,15 @@ export class AnnotationEngine {
|
|
|
232
241
|
this.interactionController.handleWheel(e);
|
|
233
242
|
this.drawCanvasAll();
|
|
234
243
|
}
|
|
244
|
+
onKeyDown(e) {
|
|
245
|
+
const layers = this.interactionController.handleKeyDown(e);
|
|
246
|
+
if (layers && layers.imageCanvas) {
|
|
247
|
+
this.drawImageCanvas();
|
|
248
|
+
}
|
|
249
|
+
if (layers && layers.annotationsCanvas) {
|
|
250
|
+
this.drawAnnotationsCanvas();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
235
253
|
// ----------------- History -----------------
|
|
236
254
|
initHistory(annotations) {
|
|
237
255
|
this.history = createHistory();
|
|
@@ -33,6 +33,7 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
|
|
|
33
33
|
onDelete: () => engineRef.current?.deleteSelected(),
|
|
34
34
|
toggleSelectionOnly: () => engineRef.current?.toggleShowSelectedOnly(),
|
|
35
35
|
onUndoRedo: (isRedo) => engineRef.current?.undoRedo(isRedo),
|
|
36
|
+
onEscape: () => engineRef.current?.onKeyDown(new KeyboardEvent('keydown', { key: 'Escape' })),
|
|
36
37
|
enabled: enableHotkeys,
|
|
37
38
|
});
|
|
38
39
|
/* ---------- Image / Engine lifecycle ---------- */
|
|
@@ -160,6 +161,10 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
|
|
|
160
161
|
useEffect(() => engineRef.current?.setEditable(editable), [editable]);
|
|
161
162
|
useEffect(() => engineRef.current?.setDrawing(drawing), [drawing]);
|
|
162
163
|
/* ---------- Render ---------- */
|
|
163
|
-
return (_jsx("div", { ref: containerRef, style: { width: '100%', height: '100%', position: 'relative', flex: 1 }, 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(),
|
|
164
|
+
return (_jsx("div", { ref: containerRef, style: { width: '100%', height: '100%', position: 'relative', flex: 1 }, 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) => {
|
|
165
|
+
if (e.key === 'Escape' || e.code === 'Escape') {
|
|
166
|
+
engineRef.current?.onKeyDown(e.nativeEvent);
|
|
167
|
+
}
|
|
168
|
+
}, children: ZoomButton && (_jsx(ZoomButton, { onClick: () => engineRef.current?.initImageCanvas(true), children: engineRef.current?.getZoomRatioLabel() })) }));
|
|
164
169
|
};
|
|
165
170
|
export default AnnotationCanvas;
|
|
@@ -2,7 +2,8 @@ type Props = {
|
|
|
2
2
|
onDelete: () => void;
|
|
3
3
|
toggleSelectionOnly: () => void;
|
|
4
4
|
onUndoRedo: (isRedo: boolean) => void;
|
|
5
|
+
onEscape?: () => void;
|
|
5
6
|
enabled?: boolean;
|
|
6
7
|
};
|
|
7
|
-
export declare function useHotkeys({ onDelete, toggleSelectionOnly, onUndoRedo, enabled }: Props): void;
|
|
8
|
+
export declare function useHotkeys({ onDelete, toggleSelectionOnly, onUndoRedo, onEscape, enabled }: Props): void;
|
|
8
9
|
export {};
|