@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
@@ -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,9 @@
1
+ import { PointState } from '../../types';
2
+ export type PolygonAnchor = {
3
+ type: 'point';
4
+ pointIndex: number;
5
+ } | {
6
+ type: 'edge';
7
+ edgeIndex: number;
8
+ insertPoint: PointState;
9
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,9 @@
1
1
  import { cloneDeep } from '../../utils/cloneDeep';
2
2
  import { isMouseClickAction, isMouseDragAction, MouseAction } from '../../utils/mouseActions';
3
- import { isInsideImage, clampBoundingBoxToImage } from './rectangleMath';
3
+ import { isInsideImage, clampBoundingBoxToImage, ACTIVE_POINT_SIZE } from './rectangleMath';
4
4
  import { getCanvasMousePoint } from '../../utils/mousePoint';
5
- import { selectRectangleAtPoint, updateActiveRectangleAnchor } from './rectangleHitTest';
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,20 +46,30 @@ export class RectangleController {
45
46
  this.hasMoved = false;
46
47
  const annotations = this.engine.getAnnotations();
47
48
  const currentSelected = this.engine.getSelectedAnnotation();
48
- const currentSelectedIndex = currentSelected ? annotations.findIndex((a) => a === currentSelected) : -1;
49
- const tempAnnotations = annotations.map((a) => ({ ...a }));
50
- const targetAnnotation = selectRectangleAtPoint(zoom, mousePoint, tempAnnotations);
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) {
49
+ // Check if we hit the currently selected annotation first
50
+ let targetAnnotation = null;
51
+ if (currentSelected && currentSelected.type === DrawMode.RECTANGLE) {
52
+ const { x, y, width, height } = currentSelected;
53
+ const padding = ACTIVE_POINT_SIZE / zoom;
54
+ const isInsideSelected = mousePoint.x >= x - padding &&
55
+ mousePoint.x <= x + width + padding &&
56
+ mousePoint.y >= y - padding &&
57
+ mousePoint.y <= y + height + padding;
58
+ if (isInsideSelected) {
59
+ targetAnnotation = currentSelected;
60
+ }
61
+ }
62
+ if (targetAnnotation) {
63
+ this.targetIndexOnBegin = annotations.indexOf(targetAnnotation);
64
+ this.rectangleAnchor = updateActiveRectangleAnchor(zoom, mousePoint, targetAnnotation);
65
+ }
66
+ else {
67
+ this.targetIndexOnBegin = -1;
68
+ this.rectangleAnchor = null;
69
+ // We explicitly DO NOT call selectAnnotationAtPoint here.
70
+ // If the user drags, we draw a new rectangle.
71
+ // If the user clicks (releases without moving), end() will handle selecting whatever is under the mouse.
59
72
  this.engine.setSelectedAnnotation(null);
60
- annotations.forEach((a) => (a.selected = false));
61
- this.engine.setAnnotations(annotations);
62
73
  }
63
74
  return true;
64
75
  }
@@ -134,7 +145,7 @@ export class RectangleController {
134
145
  }
135
146
  else if (!this.hasMoved) {
136
147
  const annotations = this.engine.getAnnotations();
137
- const selected = selectRectangleAtPoint(zoom, mousePoint, annotations);
148
+ const selected = selectAnnotationAtPoint(zoom, mousePoint, annotations);
138
149
  this.engine.setSelectedAnnotation(selected);
139
150
  this.engine.setAnnotations(annotations);
140
151
  }
@@ -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 points = getRectangleCorners(selectedAnnotation);
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 } = selectedAnnotation;
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 = findAnnotation.x + dx;
12
- const newY = findAnnotation.y + dy;
13
- findAnnotation.x = Math.max(0, Math.min(newX, dw - findAnnotation.width));
14
- findAnnotation.y = Math.max(0, Math.min(newY, dh - findAnnotation.height));
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
- findAnnotation.x += dx;
25
- findAnnotation.y += dy;
26
- findAnnotation.width -= dx;
27
- findAnnotation.height -= dy;
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
- findAnnotation.y += dy;
31
- findAnnotation.width += dx;
32
- findAnnotation.height -= dy;
33
+ rect.y += dy;
34
+ rect.width += dx;
35
+ rect.height -= dy;
33
36
  break;
34
37
  case RectangleAnchor.LEFT_BOTTOM:
35
- findAnnotation.x += dx;
36
- findAnnotation.width -= dx;
37
- findAnnotation.height += dy;
38
+ rect.x += dx;
39
+ rect.width -= dx;
40
+ rect.height += dy;
38
41
  break;
39
42
  case RectangleAnchor.RIGHT_BOTTOM:
40
- findAnnotation.width += dx;
41
- findAnnotation.height += dy;
43
+ rect.width += dx;
44
+ rect.height += dy;
42
45
  break;
43
46
  case RectangleAnchor.TOP:
44
- findAnnotation.y += dy;
45
- findAnnotation.height -= dy;
47
+ rect.y += dy;
48
+ rect.height -= dy;
46
49
  break;
47
50
  case RectangleAnchor.BOTTOM:
48
- findAnnotation.height += dy;
51
+ rect.height += dy;
49
52
  break;
50
53
  case RectangleAnchor.LEFT:
51
- findAnnotation.x += dx;
52
- findAnnotation.width -= dx;
54
+ rect.x += dx;
55
+ rect.width -= dx;
53
56
  break;
54
57
  case RectangleAnchor.RIGHT:
55
- findAnnotation.width += dx;
58
+ rect.width += dx;
56
59
  break;
57
60
  default:
58
61
  break;
59
62
  }
60
- findAnnotation.x = Math.max(0, Math.min(findAnnotation.x, dw - findAnnotation.width));
61
- findAnnotation.y = Math.max(0, Math.min(findAnnotation.y, dh - findAnnotation.height));
62
- findAnnotation.width = Math.max(0, Math.min(findAnnotation.width, dw - findAnnotation.x));
63
- findAnnotation.height = Math.max(0, Math.min(findAnnotation.height, dh - findAnnotation.y));
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 rectangle = new RectangleInteraction(new RectangleController(engine));
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 drawMode = engine.getDrawing().mode;
15
- if (drawMode === DrawMode.RECTANGLE) {
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
- if (engine.getDrawing().mode === DrawMode.RECTANGLE) {
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
- if (engine.getDrawing().mode === DrawMode.RECTANGLE) {
33
- return rectangle.handleMouseDown(e);
34
- }
35
- return false;
49
+ currentDragInteraction = getActiveInteraction();
50
+ return currentDragInteraction.handleMouseDown?.(e) ?? false;
36
51
  },
37
52
  handleMouseMove(e) {
38
- if (engine.getDrawing().mode === DrawMode.RECTANGLE) {
39
- return rectangle.handleMouseMove(e);
40
- }
41
- return false;
53
+ const active = currentDragInteraction || getActiveInteraction();
54
+ return active.handleMouseMove?.(e) ?? false;
42
55
  },
43
56
  handleMouseUp(e) {
44
- if (engine.getDrawing().mode === DrawMode.RECTANGLE) {
45
- return rectangle.handleMouseUp(e);
46
- }
47
- return false;
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
- if (engine.getDrawing().mode === DrawMode.RECTANGLE) {
51
- return rectangle.handleMouseLeave(e);
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
  },
@@ -10,4 +10,5 @@ export declare class InteractionController {
10
10
  handleMouseUp: (e: MouseEvent) => void;
11
11
  handleMouseLeave: (e: MouseEvent) => void;
12
12
  handleWheel: (e: WheelEvent) => void;
13
+ handleKeyDown: (e: KeyboardEvent) => RedrawLayers;
13
14
  }
@@ -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;
@@ -12,4 +12,5 @@ export interface Interaction {
12
12
  handleMouseUp?(e: MouseEvent): boolean | void;
13
13
  handleMouseLeave?(e: MouseEvent): boolean | void;
14
14
  handleWheel?(e: WheelEvent): boolean | void;
15
+ handleKeyDown?(e: KeyboardEvent): boolean | void;
15
16
  }
@@ -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 (mode === DrawMode.RECTANGLE)
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(): import("../annotation/annotationTypes").RectangleAnnotation[];
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
- // POLYGON: polygonRenderer,
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();