@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.
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 +8 -15
  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 +23 -2
  31. package/dist/react/AnnotationCanvas.js +6 -14
  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 {};
@@ -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 { 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,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 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) {
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 = selectRectangleAtPoint(zoom, mousePoint, annotations);
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 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;
@@ -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();