@deepnoid/canvas 0.1.72 → 0.1.74

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.
@@ -5,6 +5,13 @@ import { AnnotationEngine } from '../engine/public/annotationEngine';
5
5
  import useResizeObserver from './hooks/useResizeObserver';
6
6
  import { useDebounce } from './hooks/useDebounce';
7
7
  import { useHotkeys } from './hooks/useHotkeys';
8
+ function safeRemove(parent, child) {
9
+ if (!child)
10
+ return;
11
+ if (child.parentNode === parent) {
12
+ parent.removeChild(child);
13
+ }
14
+ }
8
15
  const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, drawing, events, enableHotkeys = true, editable = true, }) => {
9
16
  const { panZoomEnabled, zoom, ZoomButton, resetOnImageChange = false } = options || {};
10
17
  const { onImageLoadSuccess, onImageLoadError } = events || {};
@@ -12,7 +19,6 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
12
19
  const engineRef = useRef(null);
13
20
  const pendingAnnotationsRef = useRef(null);
14
21
  const imageLoadingRef = useRef(false);
15
- const currentImageRef = useRef(image);
16
22
  const engineIdRef = useRef(0);
17
23
  const [, forceRender] = useState(0);
18
24
  /* ---------- Resize Observer ---------- */
@@ -31,43 +37,27 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
31
37
  });
32
38
  /* ---------- Image / Engine lifecycle ---------- */
33
39
  useEffect(() => {
34
- if (!containerRef.current)
40
+ const container = containerRef.current;
41
+ if (!container)
35
42
  return;
36
- const prevEngine = engineRef.current;
37
43
  imageLoadingRef.current = true;
44
+ const newEngineId = ++engineIdRef.current;
45
+ const prevEngine = engineRef.current;
38
46
  if (!image?.trim()) {
39
47
  if (prevEngine) {
48
+ safeRemove(container, prevEngine.getImageCanvas());
49
+ safeRemove(container, prevEngine.getAnnotationsCanvas());
40
50
  prevEngine.destroy();
41
- containerRef.current.removeChild(prevEngine.getImageCanvas());
42
- containerRef.current.removeChild(prevEngine.getAnnotationsCanvas());
43
51
  }
44
52
  engineRef.current = null;
45
53
  pendingAnnotationsRef.current = null;
46
54
  imageLoadingRef.current = false;
47
- currentImageRef.current = '';
48
55
  return;
49
56
  }
50
- const newEngineId = engineIdRef.current + 1;
51
- engineIdRef.current = newEngineId;
52
- let cancelled = false;
53
57
  const img = new Image();
54
58
  img.onload = () => {
55
- if (cancelled || !containerRef.current)
59
+ if (engineIdRef.current !== newEngineId || !container)
56
60
  return;
57
- const imageCanvas = document.createElement('canvas');
58
- const annotationsCanvas = document.createElement('canvas');
59
- imageCanvas.style.position = 'absolute';
60
- imageCanvas.style.width = '100%';
61
- imageCanvas.style.height = '100%';
62
- annotationsCanvas.style.position = 'absolute';
63
- annotationsCanvas.style.width = '100%';
64
- annotationsCanvas.style.height = '100%';
65
- const containerRect = containerRef.current.getBoundingClientRect();
66
- const dpr = window.devicePixelRatio || 1;
67
- imageCanvas.width = containerRect.width * dpr;
68
- imageCanvas.height = containerRect.height * dpr;
69
- annotationsCanvas.width = containerRect.width * dpr;
70
- annotationsCanvas.height = containerRect.height * dpr;
71
61
  const prevImageCanvasState = prevEngine?.getImageCanvasState();
72
62
  const imageCanvasState = !resetOnImageChange && prevImageCanvasState
73
63
  ? prevImageCanvasState
@@ -83,15 +73,24 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
83
73
  dh: img.height,
84
74
  initZoom: 1,
85
75
  };
76
+ const imageCanvas = document.createElement('canvas');
77
+ const annotationsCanvas = document.createElement('canvas');
78
+ imageCanvas.style.position = 'absolute';
79
+ imageCanvas.style.width = '100%';
80
+ imageCanvas.style.height = '100%';
81
+ annotationsCanvas.style.position = 'absolute';
82
+ annotationsCanvas.style.width = '100%';
83
+ annotationsCanvas.style.height = '100%';
86
84
  const engine = new AnnotationEngine({
87
85
  imageCanvas,
88
86
  annotationsCanvas,
89
87
  image: img,
90
88
  imageCanvasState,
91
- annotations: pendingAnnotationsRef.current ?? [],
89
+ annotations: pendingAnnotationsRef.current ?? annotations,
92
90
  setAnnotations: (next) => {
93
- if (engineIdRef.current === newEngineId)
91
+ if (engineIdRef.current === newEngineId) {
94
92
  setAnnotations?.(next);
93
+ }
95
94
  },
96
95
  drawing,
97
96
  editable,
@@ -101,43 +100,47 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
101
100
  onChange: () => forceRender((v) => v + 1),
102
101
  });
103
102
  requestAnimationFrame(() => {
104
- containerRef.current.appendChild(imageCanvas);
105
- containerRef.current.appendChild(annotationsCanvas);
103
+ if (engineIdRef.current !== newEngineId) {
104
+ engine.destroy();
105
+ return;
106
+ }
107
+ const firstChild = container.firstChild;
108
+ container.insertBefore(imageCanvas, firstChild);
109
+ container.insertBefore(annotationsCanvas, firstChild);
106
110
  engine.initImageCanvas(resetOnImageChange);
107
111
  if (prevEngine) {
112
+ safeRemove(container, prevEngine.getImageCanvas());
113
+ safeRemove(container, prevEngine.getAnnotationsCanvas());
108
114
  prevEngine.destroy();
109
- containerRef.current.removeChild(prevEngine.getImageCanvas());
110
- containerRef.current.removeChild(prevEngine.getAnnotationsCanvas());
111
115
  }
112
116
  engineRef.current = engine;
113
117
  pendingAnnotationsRef.current = null;
114
118
  imageLoadingRef.current = false;
115
- currentImageRef.current = image;
116
119
  onImageLoadSuccess?.();
117
120
  });
118
121
  };
119
122
  img.onerror = () => {
120
- if (!containerRef.current)
123
+ if (engineIdRef.current !== newEngineId || !container)
121
124
  return;
122
125
  if (prevEngine) {
126
+ safeRemove(container, prevEngine.getImageCanvas());
127
+ safeRemove(container, prevEngine.getAnnotationsCanvas());
123
128
  prevEngine.destroy();
124
- containerRef.current.removeChild(prevEngine.getImageCanvas());
125
- containerRef.current.removeChild(prevEngine.getAnnotationsCanvas());
126
129
  }
127
130
  engineRef.current = null;
128
131
  pendingAnnotationsRef.current = null;
129
132
  imageLoadingRef.current = false;
130
- currentImageRef.current = '';
131
133
  onImageLoadError?.(new Error(`Failed to load image: ${image}`));
132
134
  };
133
135
  img.src = image;
134
136
  return () => {
135
- cancelled = true;
137
+ img.onload = null;
138
+ img.onerror = null;
136
139
  };
137
140
  }, [image, resetOnImageChange]);
138
141
  /* ---------- External annotations ---------- */
139
142
  useEffect(() => {
140
- if (!annotations || imageLoadingRef.current || !engineRef.current) {
143
+ if (imageLoadingRef.current || !engineRef.current) {
141
144
  pendingAnnotationsRef.current = annotations;
142
145
  return;
143
146
  }
@@ -152,6 +155,6 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
152
155
  useEffect(() => engineRef.current?.setEditable(editable), [editable]);
153
156
  useEffect(() => engineRef.current?.setDrawing(drawing), [drawing]);
154
157
  /* ---------- Render ---------- */
155
- return (_jsx("div", { ref: containerRef, style: { width: '100%', height: '100%', position: 'relative', display: 'flex', flex: 1 }, onWheel: (e) => engineRef.current?.onWheel(e.nativeEvent), onMouseDown: (e) => engineRef.current?.onMouseDown(e.nativeEvent), onMouseMove: (e) => engineRef.current?.onMouseMove(e.nativeEvent), onMouseUp: (e) => engineRef.current?.onMouseUp(e.nativeEvent), onMouseLeave: (e) => engineRef.current?.onMouseLeave(e.nativeEvent), onContextMenu: (e) => e.preventDefault(), children: ZoomButton && (_jsx(ZoomButton, { onClick: () => engineRef.current?.initImageCanvas(true), children: engineRef.current?.getZoomRatioLabel() })) }));
158
+ return (_jsx("div", { ref: containerRef, style: { width: '100%', height: '100%', position: 'relative', flex: 1 }, onWheel: (e) => engineRef.current?.onWheel(e.nativeEvent), onMouseDown: (e) => engineRef.current?.onMouseDown(e.nativeEvent), onMouseMove: (e) => engineRef.current?.onMouseMove(e.nativeEvent), onMouseUp: (e) => engineRef.current?.onMouseUp(e.nativeEvent), onMouseLeave: (e) => engineRef.current?.onMouseLeave(e.nativeEvent), onContextMenu: (e) => e.preventDefault(), children: ZoomButton && (_jsx(ZoomButton, { onClick: () => engineRef.current?.initImageCanvas(true), children: engineRef.current?.getZoomRatioLabel() })) }));
156
159
  };
157
160
  export default AnnotationCanvas;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepnoid/canvas",
3
- "version": "0.1.72",
3
+ "version": "0.1.74",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",