@deepnoid/canvas 0.1.78 → 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 +23 -2
- package/dist/react/AnnotationCanvas.js +6 -14
- 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;
|
|
@@ -114,11 +115,14 @@ export class AnnotationEngine {
|
|
|
114
115
|
this.drawCanvas(this.annotationsCanvas, undefined);
|
|
115
116
|
}
|
|
116
117
|
drawCanvas(canvas, image) {
|
|
117
|
-
const { moveX, moveY, zoomX, zoomY, dx, dy, zoom } = this.getImageCanvasState();
|
|
118
118
|
const context = canvas.getContext('2d');
|
|
119
119
|
if (!context)
|
|
120
120
|
return;
|
|
121
|
+
context.save();
|
|
122
|
+
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
121
123
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
124
|
+
context.restore();
|
|
125
|
+
const { moveX, moveY, zoomX, zoomY, dx, dy, zoom } = this.getImageCanvasState();
|
|
122
126
|
context.save();
|
|
123
127
|
context.translate(dx, dy);
|
|
124
128
|
context.translate(moveX, moveY);
|
|
@@ -178,6 +182,9 @@ export class AnnotationEngine {
|
|
|
178
182
|
renderer?.drawEditorUI?.(context, this);
|
|
179
183
|
}
|
|
180
184
|
}
|
|
185
|
+
redrawAnnotations() {
|
|
186
|
+
this.drawAnnotationsCanvas();
|
|
187
|
+
}
|
|
181
188
|
getSelectedAnnotation() {
|
|
182
189
|
return this.annotations.find((a) => a.selected) ?? null;
|
|
183
190
|
}
|
|
@@ -193,6 +200,11 @@ export class AnnotationEngine {
|
|
|
193
200
|
if (changed) {
|
|
194
201
|
this.drawAnnotationsCanvas();
|
|
195
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
|
+
}
|
|
196
208
|
}
|
|
197
209
|
// ----------------- Interaction -----------------
|
|
198
210
|
getPointerRenderState() {
|
|
@@ -229,6 +241,15 @@ export class AnnotationEngine {
|
|
|
229
241
|
this.interactionController.handleWheel(e);
|
|
230
242
|
this.drawCanvasAll();
|
|
231
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
|
+
}
|
|
232
253
|
// ----------------- History -----------------
|
|
233
254
|
initHistory(annotations) {
|
|
234
255
|
this.history = createHistory();
|