@deepnoid/canvas 0.1.72 → 0.1.73

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,46 @@ 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
+ container.appendChild(imageCanvas);
108
+ container.appendChild(annotationsCanvas);
106
109
  engine.initImageCanvas(resetOnImageChange);
107
110
  if (prevEngine) {
111
+ safeRemove(container, prevEngine.getImageCanvas());
112
+ safeRemove(container, prevEngine.getAnnotationsCanvas());
108
113
  prevEngine.destroy();
109
- containerRef.current.removeChild(prevEngine.getImageCanvas());
110
- containerRef.current.removeChild(prevEngine.getAnnotationsCanvas());
111
114
  }
112
115
  engineRef.current = engine;
113
116
  pendingAnnotationsRef.current = null;
114
117
  imageLoadingRef.current = false;
115
- currentImageRef.current = image;
116
118
  onImageLoadSuccess?.();
117
119
  });
118
120
  };
119
121
  img.onerror = () => {
120
- if (!containerRef.current)
122
+ if (engineIdRef.current !== newEngineId || !container)
121
123
  return;
122
124
  if (prevEngine) {
125
+ safeRemove(container, prevEngine.getImageCanvas());
126
+ safeRemove(container, prevEngine.getAnnotationsCanvas());
123
127
  prevEngine.destroy();
124
- containerRef.current.removeChild(prevEngine.getImageCanvas());
125
- containerRef.current.removeChild(prevEngine.getAnnotationsCanvas());
126
128
  }
127
129
  engineRef.current = null;
128
130
  pendingAnnotationsRef.current = null;
129
131
  imageLoadingRef.current = false;
130
- currentImageRef.current = '';
131
132
  onImageLoadError?.(new Error(`Failed to load image: ${image}`));
132
133
  };
133
134
  img.src = image;
134
135
  return () => {
135
- cancelled = true;
136
+ img.onload = null;
137
+ img.onerror = null;
136
138
  };
137
139
  }, [image, resetOnImageChange]);
138
140
  /* ---------- External annotations ---------- */
139
141
  useEffect(() => {
140
- if (!annotations || imageLoadingRef.current || !engineRef.current) {
142
+ if (imageLoadingRef.current || !engineRef.current) {
141
143
  pendingAnnotationsRef.current = annotations;
142
144
  return;
143
145
  }
@@ -152,6 +154,6 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
152
154
  useEffect(() => engineRef.current?.setEditable(editable), [editable]);
153
155
  useEffect(() => engineRef.current?.setDrawing(drawing), [drawing]);
154
156
  /* ---------- 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() })) }));
157
+ 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
158
  };
157
159
  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.73",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",