@deepnoid/canvas 0.1.60 → 0.1.61
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.
|
@@ -10,6 +10,9 @@ export declare class RectangleController {
|
|
|
10
10
|
private startMousePoint;
|
|
11
11
|
private rectangleAnchor;
|
|
12
12
|
private needsRedraw;
|
|
13
|
+
private isDrawingNewRectangle;
|
|
14
|
+
private hasMoved;
|
|
15
|
+
private targetIndexOnBegin;
|
|
13
16
|
constructor(engine: AnnotationEngine);
|
|
14
17
|
getRenderState(): {
|
|
15
18
|
mousePoint: PointState;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { cloneDeep } from '../../utils/cloneDeep';
|
|
2
2
|
import { isMouseClickAction, isMouseDragAction, MouseAction } from '../../utils/mouseActions';
|
|
3
|
-
import { isInsideImage, clampBoundingBoxToImage
|
|
3
|
+
import { isInsideImage, clampBoundingBoxToImage } from './rectangleMath';
|
|
4
4
|
import { getCanvasMousePoint } from '../../utils/mousePoint';
|
|
5
5
|
import { selectRectangleAtPoint, updateActiveRectangleAnchor } from './rectangleHitTest';
|
|
6
6
|
import { applyRectangleMove, applyRectangleResize } from './rectangleTransform';
|
|
7
7
|
import { DrawMode } from '../annotationTypes';
|
|
8
|
+
const DRAG_THRESHOLD = 3;
|
|
8
9
|
export class RectangleController {
|
|
9
10
|
constructor(engine) {
|
|
10
11
|
this.engine = engine;
|
|
@@ -14,6 +15,9 @@ export class RectangleController {
|
|
|
14
15
|
this.startMousePoint = null;
|
|
15
16
|
this.rectangleAnchor = null;
|
|
16
17
|
this.needsRedraw = false;
|
|
18
|
+
this.isDrawingNewRectangle = false;
|
|
19
|
+
this.hasMoved = false;
|
|
20
|
+
this.targetIndexOnBegin = -1;
|
|
17
21
|
}
|
|
18
22
|
getRenderState() {
|
|
19
23
|
return {
|
|
@@ -37,64 +41,101 @@ export class RectangleController {
|
|
|
37
41
|
this.mouseAction = MouseAction.LEFT;
|
|
38
42
|
this.mousePoint = mousePoint;
|
|
39
43
|
this.startMousePoint = mousePoint;
|
|
40
|
-
|
|
41
|
-
this.
|
|
42
|
-
|
|
44
|
+
this.isDrawingNewRectangle = false;
|
|
45
|
+
this.hasMoved = false;
|
|
46
|
+
const annotations = this.engine.getAnnotations();
|
|
47
|
+
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));
|
|
61
|
+
this.engine.setAnnotations(annotations);
|
|
62
|
+
}
|
|
43
63
|
return true;
|
|
44
64
|
}
|
|
45
65
|
move(event) {
|
|
46
66
|
this.needsRedraw = false;
|
|
47
|
-
if (!isMouseDragAction(event.buttons, MouseAction.LEFT))
|
|
67
|
+
if (!isMouseDragAction(event.buttons, MouseAction.LEFT) || !this.startMousePoint)
|
|
48
68
|
return;
|
|
49
69
|
const imageCanvasState = this.engine.getImageCanvasState();
|
|
50
70
|
const { dw, dh } = imageCanvasState;
|
|
51
71
|
this.prevMousePoint = cloneDeep(this.mousePoint);
|
|
52
72
|
this.mousePoint = getCanvasMousePoint(event, this.engine.getImageCanvas(), imageCanvasState);
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
else {
|
|
67
|
-
applyRectangleMove(params);
|
|
73
|
+
const movedDistance = Math.sqrt(Math.pow(this.mousePoint.x - this.startMousePoint.x, 2) + Math.pow(this.mousePoint.y - this.startMousePoint.y, 2));
|
|
74
|
+
if (movedDistance > DRAG_THRESHOLD) {
|
|
75
|
+
this.hasMoved = true;
|
|
76
|
+
if (!this.isDrawingNewRectangle) {
|
|
77
|
+
const currentSelected = this.engine.getSelectedAnnotation();
|
|
78
|
+
const currentSelectedIndex = currentSelected
|
|
79
|
+
? this.engine.getAnnotations().findIndex((a) => a === currentSelected)
|
|
80
|
+
: -1;
|
|
81
|
+
const isResizeMode = this.rectangleAnchor && currentSelected;
|
|
82
|
+
const isMoveMode = currentSelectedIndex >= 0 && currentSelectedIndex === this.targetIndexOnBegin;
|
|
83
|
+
if (!isResizeMode && !isMoveMode) {
|
|
84
|
+
this.isDrawingNewRectangle = true;
|
|
85
|
+
}
|
|
68
86
|
}
|
|
87
|
+
}
|
|
88
|
+
if (this.isDrawingNewRectangle) {
|
|
69
89
|
this.needsRedraw = true;
|
|
70
90
|
}
|
|
91
|
+
else {
|
|
92
|
+
const selectedAnnotation = this.engine.getSelectedAnnotation();
|
|
93
|
+
if (selectedAnnotation && this.prevMousePoint && this.hasMoved) {
|
|
94
|
+
const params = {
|
|
95
|
+
annotations: this.engine.getAnnotations(),
|
|
96
|
+
dw,
|
|
97
|
+
dh,
|
|
98
|
+
mousePoint: this.mousePoint,
|
|
99
|
+
prevMousePoint: this.prevMousePoint,
|
|
100
|
+
rectangleAnchor: this.rectangleAnchor,
|
|
101
|
+
};
|
|
102
|
+
if (this.rectangleAnchor) {
|
|
103
|
+
applyRectangleResize(params);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
applyRectangleMove(params);
|
|
107
|
+
}
|
|
108
|
+
this.needsRedraw = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
71
111
|
return true;
|
|
72
112
|
}
|
|
73
113
|
end(event) {
|
|
74
114
|
if (!isMouseClickAction(event.button, MouseAction.LEFT))
|
|
75
115
|
return;
|
|
76
116
|
const imageCanvasState = this.engine.getImageCanvasState();
|
|
117
|
+
const { zoom, dw, dh } = imageCanvasState;
|
|
77
118
|
const mousePoint = getCanvasMousePoint(event, this.engine.getImageCanvas(), imageCanvasState);
|
|
78
119
|
this.mousePoint = mousePoint;
|
|
79
120
|
if (this.startMousePoint) {
|
|
80
121
|
const movedWidth = Math.abs(this.startMousePoint.x - mousePoint.x);
|
|
81
122
|
const movedHeight = Math.abs(this.startMousePoint.y - mousePoint.y);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (this.engine.getSelectedAnnotation()) {
|
|
86
|
-
this.engine.commitHistory(true);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
123
|
+
if (this.hasMoved && movedWidth > DRAG_THRESHOLD && movedHeight > DRAG_THRESHOLD) {
|
|
124
|
+
if (this.isDrawingNewRectangle) {
|
|
125
|
+
const { x, y, width, height } = clampBoundingBoxToImage(Math.min(mousePoint.x, this.startMousePoint.x), Math.min(mousePoint.y, this.startMousePoint.y), movedWidth, movedHeight, dw, dh);
|
|
89
126
|
const label = this.engine.getDrawing().label;
|
|
90
127
|
this.engine.appendAnnotation({ label, type: DrawMode.RECTANGLE, x, y, width, height, selected: false });
|
|
91
128
|
this.engine.commitHistory(true);
|
|
92
129
|
this.engine.setSelectedAnnotation(null);
|
|
93
130
|
}
|
|
131
|
+
else if (this.engine.getSelectedAnnotation()) {
|
|
132
|
+
this.engine.commitHistory(true);
|
|
133
|
+
}
|
|
94
134
|
}
|
|
95
|
-
else {
|
|
135
|
+
else if (!this.hasMoved) {
|
|
96
136
|
const annotations = this.engine.getAnnotations();
|
|
97
|
-
|
|
137
|
+
const selected = selectRectangleAtPoint(zoom, mousePoint, annotations);
|
|
138
|
+
this.engine.setSelectedAnnotation(selected);
|
|
98
139
|
this.engine.setAnnotations(annotations);
|
|
99
140
|
}
|
|
100
141
|
}
|
|
@@ -111,5 +152,8 @@ export class RectangleController {
|
|
|
111
152
|
this.startMousePoint = null;
|
|
112
153
|
this.prevMousePoint = null;
|
|
113
154
|
this.rectangleAnchor = null;
|
|
155
|
+
this.isDrawingNewRectangle = false;
|
|
156
|
+
this.hasMoved = false;
|
|
157
|
+
this.targetIndexOnBegin = -1;
|
|
114
158
|
}
|
|
115
159
|
}
|
|
@@ -9,58 +9,67 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
|
|
|
9
9
|
const { panZoomEnabled, zoom, ZoomButton, resetOnImageChange = false } = options || {};
|
|
10
10
|
const { onImageLoadSuccess, onImageLoadError } = events || {};
|
|
11
11
|
const imageCanvasRef = useRef(null);
|
|
12
|
-
const imageRef = useRef(new Image());
|
|
13
|
-
const engineRef = useRef(null);
|
|
14
12
|
const annotationsCanvasRef = useRef(null);
|
|
15
|
-
const
|
|
13
|
+
const engineRef = useRef(null);
|
|
14
|
+
const pendingAnnotationsRef = useRef(null);
|
|
15
|
+
const [, forceRender] = useState(0);
|
|
16
|
+
/* ---------- Resize ---------- */
|
|
16
17
|
useResizeObserver({
|
|
17
18
|
ref: imageCanvasRef,
|
|
18
19
|
onResize: useDebounce((size) => {
|
|
19
20
|
const engine = engineRef.current;
|
|
20
|
-
const
|
|
21
|
-
if (!engine || !
|
|
21
|
+
const canvas = engine?.getImageCanvas();
|
|
22
|
+
if (!engine || !canvas)
|
|
22
23
|
return;
|
|
23
|
-
|
|
24
|
-
if (needsResize)
|
|
24
|
+
if (canvas.width !== size.width || canvas.height !== size.height) {
|
|
25
25
|
engine.initImageCanvas(true);
|
|
26
|
+
}
|
|
26
27
|
}, 150),
|
|
27
28
|
});
|
|
29
|
+
/* ---------- Hotkeys ---------- */
|
|
28
30
|
useHotkeys({
|
|
29
31
|
onDelete: () => engineRef.current?.deleteSelected(),
|
|
30
32
|
toggleSelectionOnly: () => engineRef.current?.toggleShowSelectedOnly(),
|
|
31
33
|
onUndoRedo: (isRedo) => engineRef.current?.undoRedo(isRedo),
|
|
32
34
|
enabled: enableHotkeys,
|
|
33
35
|
});
|
|
34
|
-
|
|
35
|
-
const img = new Image();
|
|
36
|
-
img.width = img.height = 0;
|
|
37
|
-
return img;
|
|
38
|
-
};
|
|
39
|
-
const resetCanvas = (errorMsg) => {
|
|
40
|
-
engineRef.current?.resetCanvas();
|
|
41
|
-
imageRef.current = createEmptyImage();
|
|
42
|
-
if (errorMsg)
|
|
43
|
-
onImageLoadError?.(new Error(errorMsg));
|
|
44
|
-
};
|
|
36
|
+
/* ---------- Image / Engine lifecycle ---------- */
|
|
45
37
|
useEffect(() => {
|
|
46
|
-
if (!image?.trim())
|
|
47
|
-
|
|
38
|
+
if (!image?.trim()) {
|
|
39
|
+
engineRef.current?.resetCanvas();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
48
42
|
let cancelled = false;
|
|
49
43
|
const img = new Image();
|
|
50
44
|
img.onload = () => {
|
|
51
|
-
if (cancelled || !imageCanvasRef.current || !annotationsCanvasRef.current)
|
|
45
|
+
if (cancelled || !imageCanvasRef.current || !annotationsCanvasRef.current) {
|
|
52
46
|
return;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
}
|
|
48
|
+
const prevEngine = engineRef.current;
|
|
49
|
+
const imageCanvasState = prevEngine && !resetOnImageChange
|
|
50
|
+
? prevEngine.getImageCanvasState()
|
|
51
|
+
: {
|
|
52
|
+
moveX: 0,
|
|
53
|
+
moveY: 0,
|
|
54
|
+
zoomX: 0,
|
|
55
|
+
zoomY: 0,
|
|
56
|
+
zoom: 1,
|
|
57
|
+
dx: 0,
|
|
58
|
+
dy: 0,
|
|
59
|
+
dw: img.width,
|
|
60
|
+
dh: img.height,
|
|
61
|
+
initZoom: 1,
|
|
62
|
+
};
|
|
63
|
+
const initialAnnotations = pendingAnnotationsRef.current ?? [];
|
|
64
|
+
pendingAnnotationsRef.current = null;
|
|
65
|
+
prevEngine?.destroy();
|
|
57
66
|
engineRef.current = new AnnotationEngine({
|
|
58
67
|
imageCanvas: imageCanvasRef.current,
|
|
59
68
|
image: img,
|
|
60
69
|
imageCanvasState,
|
|
61
70
|
annotationsCanvas: annotationsCanvasRef.current,
|
|
62
|
-
annotations,
|
|
63
|
-
setAnnotations: (
|
|
71
|
+
annotations: initialAnnotations,
|
|
72
|
+
setAnnotations: (next) => setAnnotations?.(next),
|
|
64
73
|
drawing,
|
|
65
74
|
editable,
|
|
66
75
|
panZoomEnabled,
|
|
@@ -69,42 +78,51 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
|
|
|
69
78
|
onChange: () => forceRender((v) => v + 1),
|
|
70
79
|
});
|
|
71
80
|
engineRef.current.initImageCanvas(resetOnImageChange);
|
|
81
|
+
onImageLoadSuccess?.();
|
|
82
|
+
};
|
|
83
|
+
img.onerror = () => {
|
|
84
|
+
if (cancelled)
|
|
85
|
+
return;
|
|
86
|
+
pendingAnnotationsRef.current = null;
|
|
87
|
+
engineRef.current?.resetCanvas();
|
|
88
|
+
onImageLoadError?.(new Error(`Failed to load image: ${image}`));
|
|
72
89
|
};
|
|
73
|
-
img.onerror = () => !cancelled && resetCanvas(`Failed to load image: ${image}`);
|
|
74
90
|
img.src = image;
|
|
75
|
-
imageRef.current = img;
|
|
76
|
-
onImageLoadSuccess?.();
|
|
77
91
|
return () => {
|
|
78
92
|
cancelled = true;
|
|
79
93
|
img.onload = null;
|
|
80
94
|
img.onerror = null;
|
|
81
|
-
engineRef.current?.destroy();
|
|
82
95
|
};
|
|
83
|
-
}, [image]);
|
|
84
|
-
|
|
85
|
-
if (!engineRef.current)
|
|
86
|
-
return;
|
|
87
|
-
engineRef.current.setPanZoomEnabled(!!panZoomEnabled);
|
|
88
|
-
}, [panZoomEnabled]);
|
|
96
|
+
}, [image, resetOnImageChange]);
|
|
97
|
+
/* ---------- External annotations ---------- */
|
|
89
98
|
useEffect(() => {
|
|
90
|
-
if (!
|
|
99
|
+
if (!annotations)
|
|
91
100
|
return;
|
|
92
|
-
engineRef.current
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
if (!engineRef.current)
|
|
101
|
+
if (!engineRef.current) {
|
|
102
|
+
pendingAnnotationsRef.current = annotations;
|
|
96
103
|
return;
|
|
104
|
+
}
|
|
97
105
|
const before = engineRef.current.getAnnotations();
|
|
98
106
|
if (JSON.stringify(before) !== JSON.stringify(annotations)) {
|
|
99
107
|
engineRef.current.setAnnotations(annotations);
|
|
100
108
|
engineRef.current.commitHistory();
|
|
101
109
|
}
|
|
102
|
-
}, [
|
|
110
|
+
}, [annotations]);
|
|
111
|
+
/* ---------- Options sync ---------- */
|
|
103
112
|
useEffect(() => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
engineRef.current?.setPanZoomEnabled(!!panZoomEnabled);
|
|
114
|
+
}, [panZoomEnabled]);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
engineRef.current?.setEditable(editable);
|
|
117
|
+
}, [editable]);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
engineRef.current?.setDrawing(drawing);
|
|
120
|
+
}, [drawing]);
|
|
121
|
+
/* ---------- Render ---------- */
|
|
122
|
+
return (_jsx("div", { style: { width: '100%', height: '100%', display: 'flex', flex: 1 }, children: _jsxs("div", { 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(), style: {
|
|
123
|
+
flex: 1,
|
|
124
|
+
position: 'relative',
|
|
125
|
+
cursor: !panZoomEnabled || editable ? 'default' : 'grab',
|
|
126
|
+
}, children: [_jsx("canvas", { ref: imageCanvasRef, style: { position: 'absolute', width: '100%', height: '100%' } }), _jsx("canvas", { ref: annotationsCanvasRef, style: { position: 'absolute', width: '100%', height: '100%' } }), ZoomButton && (_jsx(ZoomButton, { onClick: () => engineRef.current?.initImageCanvas(true), children: engineRef.current?.getZoomRatioLabel() }))] }) }));
|
|
109
127
|
};
|
|
110
128
|
export default AnnotationCanvas;
|