react-os-shell 0.2.23 → 0.2.25
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.
- package/dist/{Files-XUUCSCHL.js → Files-YHUOR7RM.js} +4 -4
- package/dist/{Files-XUUCSCHL.js.map → Files-YHUOR7RM.js.map} +1 -1
- package/dist/Preview-SLX4ZLUQ.js +6 -0
- package/dist/{Preview-4M4HF2RC.js.map → Preview-SLX4ZLUQ.js.map} +1 -1
- package/dist/apps/index.js +4 -4
- package/dist/{chunk-4ZGTBQWZ.js → chunk-23RBDC2Z.js} +3 -3
- package/dist/{chunk-4ZGTBQWZ.js.map → chunk-23RBDC2Z.js.map} +1 -1
- package/dist/{chunk-HQJRNCTU.js → chunk-6IV6OWF3.js} +510 -200
- package/dist/chunk-6IV6OWF3.js.map +1 -0
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/dist/Preview-4M4HF2RC.js +0 -6
- package/dist/chunk-HQJRNCTU.js.map +0 -1
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { toast_default } from './chunk-WIJ45SYD.js';
|
|
2
2
|
import { WindowTitle, getActiveModalId } from './chunk-QXY6ZHRX.js';
|
|
3
|
-
import {
|
|
3
|
+
import { forwardRef, useRef, useState, useMemo, useEffect, useImperativeHandle, createContext, useContext } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import * as pdfjsLib from 'pdfjs-dist';
|
|
6
|
-
import {
|
|
6
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
7
7
|
|
|
8
8
|
var COLORS = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6", "#8b5cf6", "#000000", "#ffffff"];
|
|
9
|
+
var FONTS = [
|
|
10
|
+
{ id: "system", label: "System", css: "-apple-system, system-ui, sans-serif" },
|
|
11
|
+
{ id: "serif", label: "Serif", css: 'Georgia, "Times New Roman", serif' },
|
|
12
|
+
{ id: "mono", label: "Mono", css: 'ui-monospace, "SF Mono", Menlo, monospace' },
|
|
13
|
+
{ id: "cursive", label: "Cursive", css: '"Brush Script MT", "Comic Sans MS", cursive' }
|
|
14
|
+
];
|
|
9
15
|
var STROKE_DEFAULT = 4;
|
|
16
|
+
var TEXT_SIZE_DEFAULT = 24;
|
|
17
|
+
var RECT_RADIUS_DEFAULT = 12;
|
|
10
18
|
var MOSAIC_BLOCK = 12;
|
|
11
19
|
var ZOOM_MIN = 0.25;
|
|
12
20
|
var ZOOM_MAX = 4;
|
|
13
21
|
var ZOOM_STEP = 0.25;
|
|
14
22
|
var newId = () => Math.random().toString(36).slice(2, 9);
|
|
15
|
-
|
|
23
|
+
var ImageAnnotator = forwardRef(function ImageAnnotator2({ src, filename }, ref) {
|
|
16
24
|
const wrapRef = useRef(null);
|
|
17
25
|
const canvasRef = useRef(null);
|
|
18
26
|
const svgRef = useRef(null);
|
|
@@ -20,6 +28,12 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
20
28
|
const [tool, setTool] = useState("select");
|
|
21
29
|
const [color, setColor] = useState(COLORS[0]);
|
|
22
30
|
const [stroke, setStroke] = useState(STROKE_DEFAULT);
|
|
31
|
+
const [textSize, setTextSize] = useState(TEXT_SIZE_DEFAULT);
|
|
32
|
+
const [textFont, setTextFont] = useState(FONTS[0].id);
|
|
33
|
+
const [textBold, setTextBold] = useState(true);
|
|
34
|
+
const [textItalic, setTextItalic] = useState(false);
|
|
35
|
+
const [textUnderline, setTextUnderline] = useState(false);
|
|
36
|
+
const [rectRadius, setRectRadius] = useState(RECT_RADIUS_DEFAULT);
|
|
23
37
|
const [annotations, setAnnotations] = useState([]);
|
|
24
38
|
const [selectedId, setSelectedId] = useState(null);
|
|
25
39
|
const [imageSize, setImageSize] = useState(null);
|
|
@@ -29,10 +43,30 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
29
43
|
const [pendingText, setPendingText] = useState(null);
|
|
30
44
|
const [pendingCrop, setPendingCrop] = useState(null);
|
|
31
45
|
const dragRef = useRef(null);
|
|
46
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
32
47
|
const displaySize = useMemo(() => {
|
|
33
48
|
if (!fitSize) return null;
|
|
34
49
|
return { w: fitSize.w * zoom, h: fitSize.h * zoom };
|
|
35
50
|
}, [fitSize, zoom]);
|
|
51
|
+
const scale = displaySize && imageSize ? displaySize.w / imageSize.w : 1;
|
|
52
|
+
const selected = useMemo(
|
|
53
|
+
() => annotations.find((a) => a.id === selectedId) ?? null,
|
|
54
|
+
[annotations, selectedId]
|
|
55
|
+
);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (tool === "select" && selected) {
|
|
58
|
+
if ("color" in selected) setColor(selected.color);
|
|
59
|
+
if ("stroke" in selected) setStroke(selected.stroke);
|
|
60
|
+
if (selected.type === "rect") setRectRadius(selected.radius);
|
|
61
|
+
if (selected.type === "text") {
|
|
62
|
+
setTextSize(selected.size);
|
|
63
|
+
setTextFont(selected.font);
|
|
64
|
+
setTextBold(selected.bold);
|
|
65
|
+
setTextItalic(selected.italic);
|
|
66
|
+
setTextUnderline(selected.underline);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}, [selectedId]);
|
|
36
70
|
useEffect(() => {
|
|
37
71
|
const img = new Image();
|
|
38
72
|
img.crossOrigin = "anonymous";
|
|
@@ -55,8 +89,7 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
55
89
|
const availH = Math.max(0, r.height - 32);
|
|
56
90
|
if (availW === 0 || availH === 0) return;
|
|
57
91
|
const ratio = imageSize.w / imageSize.h;
|
|
58
|
-
let w = imageSize.w;
|
|
59
|
-
let h = imageSize.h;
|
|
92
|
+
let w = imageSize.w, h = imageSize.h;
|
|
60
93
|
if (w > availW) {
|
|
61
94
|
w = availW;
|
|
62
95
|
h = w / ratio;
|
|
@@ -85,7 +118,7 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
85
118
|
const ctx = c.getContext("2d");
|
|
86
119
|
ctx.drawImage(img, 0, 0);
|
|
87
120
|
for (const m of mosaicAnnos) applyMosaic(ctx, m);
|
|
88
|
-
}, [imageSize, mosaicAnnos]);
|
|
121
|
+
}, [imageSize, mosaicAnnos, fitSize]);
|
|
89
122
|
const evToImage = (e) => {
|
|
90
123
|
const svg = svgRef.current;
|
|
91
124
|
if (!svg) return { x: 0, y: 0 };
|
|
@@ -97,13 +130,77 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
97
130
|
const t = pt.matrixTransform(ctm.inverse());
|
|
98
131
|
return { x: t.x, y: t.y };
|
|
99
132
|
};
|
|
133
|
+
const beginDrag = (drag) => {
|
|
134
|
+
dragRef.current = drag;
|
|
135
|
+
setIsDragging(true);
|
|
136
|
+
};
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!isDragging) return;
|
|
139
|
+
const onMove = (ev) => {
|
|
140
|
+
const drag = dragRef.current;
|
|
141
|
+
if (!drag) return;
|
|
142
|
+
const p = evToImage(ev);
|
|
143
|
+
if (drag.kind === "draw") {
|
|
144
|
+
setPreview(makeShape(tool, drag.start, p, color, stroke, rectRadius));
|
|
145
|
+
} else if (drag.kind === "pen") {
|
|
146
|
+
const last = drag.points[drag.points.length - 1];
|
|
147
|
+
if (Math.abs(p.x - last.x) > 1 || Math.abs(p.y - last.y) > 1) {
|
|
148
|
+
drag.points.push(p);
|
|
149
|
+
setPreview({ id: "", type: "draw", points: drag.points.slice(), color, stroke });
|
|
150
|
+
}
|
|
151
|
+
} else if (drag.kind === "crop") {
|
|
152
|
+
setPendingCrop(normalizeRect(drag.start, p));
|
|
153
|
+
} else if (drag.kind === "move") {
|
|
154
|
+
const dx = p.x - drag.start.x;
|
|
155
|
+
const dy = p.y - drag.start.y;
|
|
156
|
+
setAnnotations((prev) => prev.map((a) => a.id === drag.id ? translate(drag.original, dx, dy) : a));
|
|
157
|
+
} else if (drag.kind === "resize") {
|
|
158
|
+
setAnnotations((prev) => prev.map((a) => a.id === drag.id ? resize(drag.original, drag.corner, p) : a));
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const onUp = () => {
|
|
162
|
+
const drag = dragRef.current;
|
|
163
|
+
dragRef.current = null;
|
|
164
|
+
setIsDragging(false);
|
|
165
|
+
if (drag?.kind === "draw") {
|
|
166
|
+
setPreview((p) => {
|
|
167
|
+
if (!p || isTrivial(p)) return null;
|
|
168
|
+
const anno = { ...p, id: newId() };
|
|
169
|
+
setAnnotations((prev) => [...prev, anno]);
|
|
170
|
+
setSelectedId(anno.id);
|
|
171
|
+
setTool("select");
|
|
172
|
+
return null;
|
|
173
|
+
});
|
|
174
|
+
} else if (drag?.kind === "pen") {
|
|
175
|
+
if (drag.points.length < 2) {
|
|
176
|
+
setPreview(null);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const anno = { id: newId(), type: "draw", points: drag.points, color, stroke };
|
|
180
|
+
setAnnotations((prev) => [...prev, anno]);
|
|
181
|
+
setSelectedId(anno.id);
|
|
182
|
+
setTool("select");
|
|
183
|
+
setPreview(null);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
window.addEventListener("pointermove", onMove);
|
|
187
|
+
window.addEventListener("pointerup", onUp);
|
|
188
|
+
window.addEventListener("pointercancel", onUp);
|
|
189
|
+
return () => {
|
|
190
|
+
window.removeEventListener("pointermove", onMove);
|
|
191
|
+
window.removeEventListener("pointerup", onUp);
|
|
192
|
+
window.removeEventListener("pointercancel", onUp);
|
|
193
|
+
};
|
|
194
|
+
}, [isDragging, tool, color, stroke, rectRadius]);
|
|
100
195
|
const handleAnnoPointerDown = (e, anno) => {
|
|
101
196
|
if (tool !== "select") return;
|
|
102
197
|
e.stopPropagation();
|
|
103
198
|
setSelectedId(anno.id);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
199
|
+
beginDrag({ kind: "move", id: anno.id, start: evToImage(e), original: anno });
|
|
200
|
+
};
|
|
201
|
+
const handleHandlePointerDown = (e, anno, corner) => {
|
|
202
|
+
e.stopPropagation();
|
|
203
|
+
beginDrag({ kind: "resize", id: anno.id, corner, start: evToImage(e), original: anno });
|
|
107
204
|
};
|
|
108
205
|
const handleSvgPointerDown = (e) => {
|
|
109
206
|
if (tool === "select") {
|
|
@@ -117,44 +214,16 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
117
214
|
}
|
|
118
215
|
const start = evToImage(e);
|
|
119
216
|
if (tool === "crop") {
|
|
120
|
-
|
|
121
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
217
|
+
beginDrag({ kind: "crop", start });
|
|
122
218
|
return;
|
|
123
219
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const handleSvgPointerMove = (e) => {
|
|
129
|
-
const drag = dragRef.current;
|
|
130
|
-
if (!drag) return;
|
|
131
|
-
const p = evToImage(e);
|
|
132
|
-
if (drag.kind === "draw") {
|
|
133
|
-
setPreview(makeShape(tool, drag.start, p, color, stroke));
|
|
134
|
-
} else if (drag.kind === "crop") {
|
|
135
|
-
setPendingCrop(normalizeRect(drag.start, p));
|
|
136
|
-
} else if (drag.kind === "move") {
|
|
137
|
-
const dx = p.x - drag.start.x;
|
|
138
|
-
const dy = p.y - drag.start.y;
|
|
139
|
-
setAnnotations((prev) => prev.map((a) => a.id === drag.id ? translate(drag.original, dx, dy) : a));
|
|
220
|
+
if (tool === "draw") {
|
|
221
|
+
beginDrag({ kind: "pen", points: [start] });
|
|
222
|
+
setPreview({ id: "", type: "draw", points: [start], color, stroke });
|
|
223
|
+
return;
|
|
140
224
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const drag = dragRef.current;
|
|
144
|
-
if (!drag) return;
|
|
145
|
-
dragRef.current = null;
|
|
146
|
-
e.currentTarget.releasePointerCapture?.(e.pointerId);
|
|
147
|
-
if (drag.kind === "draw") {
|
|
148
|
-
const p = preview;
|
|
149
|
-
if (!p) return;
|
|
150
|
-
const tooSmall = isTrivial(p);
|
|
151
|
-
setPreview(null);
|
|
152
|
-
if (tooSmall) return;
|
|
153
|
-
const anno = { ...p, id: newId() };
|
|
154
|
-
setAnnotations((prev) => [...prev, anno]);
|
|
155
|
-
setSelectedId(anno.id);
|
|
156
|
-
setTool("select");
|
|
157
|
-
} else if (drag.kind === "crop") ; else if (drag.kind === "move") ;
|
|
225
|
+
setPreview(makeShape(tool, start, start, color, stroke, rectRadius));
|
|
226
|
+
beginDrag({ kind: "draw", start });
|
|
158
227
|
};
|
|
159
228
|
const handleAnnoDoubleClick = (anno) => {
|
|
160
229
|
if (anno.type !== "text") return;
|
|
@@ -162,8 +231,14 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
162
231
|
};
|
|
163
232
|
useEffect(() => {
|
|
164
233
|
const onKey = (ev) => {
|
|
165
|
-
if (!selectedId) return;
|
|
166
234
|
if (document.activeElement && /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName)) return;
|
|
235
|
+
if ((ev.metaKey || ev.ctrlKey) && (ev.key === "z" || ev.key === "Z") && !ev.shiftKey) {
|
|
236
|
+
ev.preventDefault();
|
|
237
|
+
setAnnotations((prev) => prev.slice(0, -1));
|
|
238
|
+
setSelectedId(null);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (!selectedId) return;
|
|
167
242
|
if (ev.key === "Delete" || ev.key === "Backspace") {
|
|
168
243
|
ev.preventDefault();
|
|
169
244
|
setAnnotations((prev) => prev.filter((a) => a.id !== selectedId));
|
|
@@ -175,17 +250,67 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
175
250
|
window.addEventListener("keydown", onKey);
|
|
176
251
|
return () => window.removeEventListener("keydown", onKey);
|
|
177
252
|
}, [selectedId]);
|
|
253
|
+
const setColorAndApply = (c) => {
|
|
254
|
+
setColor(c);
|
|
255
|
+
if (!selectedId) return;
|
|
256
|
+
setAnnotations((prev) => prev.map((a) => {
|
|
257
|
+
if (a.id !== selectedId) return a;
|
|
258
|
+
if (a.type === "mosaic") return a;
|
|
259
|
+
return { ...a, color: c };
|
|
260
|
+
}));
|
|
261
|
+
};
|
|
262
|
+
const setStrokeAndApply = (s) => {
|
|
263
|
+
setStroke(s);
|
|
264
|
+
if (!selectedId) return;
|
|
265
|
+
setAnnotations((prev) => prev.map((a) => {
|
|
266
|
+
if (a.id !== selectedId) return a;
|
|
267
|
+
if (a.type === "rect" || a.type === "circle" || a.type === "arrow" || a.type === "draw") {
|
|
268
|
+
return { ...a, stroke: s };
|
|
269
|
+
}
|
|
270
|
+
return a;
|
|
271
|
+
}));
|
|
272
|
+
};
|
|
273
|
+
const setRectRadiusAndApply = (r) => {
|
|
274
|
+
setRectRadius(r);
|
|
275
|
+
if (!selectedId) return;
|
|
276
|
+
setAnnotations((prev) => prev.map(
|
|
277
|
+
(a) => a.id === selectedId && a.type === "rect" ? { ...a, radius: r } : a
|
|
278
|
+
));
|
|
279
|
+
};
|
|
280
|
+
const setTextSizeAndApply = (s) => {
|
|
281
|
+
setTextSize(s);
|
|
282
|
+
if (!selectedId) return;
|
|
283
|
+
setAnnotations((prev) => prev.map(
|
|
284
|
+
(a) => a.id === selectedId && a.type === "text" ? { ...a, size: s } : a
|
|
285
|
+
));
|
|
286
|
+
};
|
|
287
|
+
const setTextFontAndApply = (f) => {
|
|
288
|
+
setTextFont(f);
|
|
289
|
+
if (!selectedId) return;
|
|
290
|
+
setAnnotations((prev) => prev.map(
|
|
291
|
+
(a) => a.id === selectedId && a.type === "text" ? { ...a, font: f } : a
|
|
292
|
+
));
|
|
293
|
+
};
|
|
294
|
+
const toggleTextStyleAndApply = (which) => {
|
|
295
|
+
const next = !{ bold: textBold, italic: textItalic, underline: textUnderline }[which];
|
|
296
|
+
if (which === "bold") setTextBold(next);
|
|
297
|
+
if (which === "italic") setTextItalic(next);
|
|
298
|
+
if (which === "underline") setTextUnderline(next);
|
|
299
|
+
if (!selectedId) return;
|
|
300
|
+
setAnnotations((prev) => prev.map(
|
|
301
|
+
(a) => a.id === selectedId && a.type === "text" ? { ...a, [which]: next } : a
|
|
302
|
+
));
|
|
303
|
+
};
|
|
178
304
|
const commitText = () => {
|
|
179
305
|
if (!pendingText) return;
|
|
180
|
-
const value = pendingText.value
|
|
181
|
-
if (!value) {
|
|
306
|
+
const value = pendingText.value;
|
|
307
|
+
if (!value.trim()) {
|
|
182
308
|
if (pendingText.editingId) {
|
|
183
309
|
setAnnotations((prev) => prev.filter((a) => a.id !== pendingText.editingId));
|
|
184
310
|
}
|
|
185
311
|
setPendingText(null);
|
|
186
312
|
return;
|
|
187
313
|
}
|
|
188
|
-
const fontSize = Math.max(16, stroke * 6);
|
|
189
314
|
if (pendingText.editingId) {
|
|
190
315
|
setAnnotations((prev) => prev.map(
|
|
191
316
|
(a) => a.id === pendingText.editingId && a.type === "text" ? { ...a, text: value } : a
|
|
@@ -198,7 +323,11 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
198
323
|
y: pendingText.y,
|
|
199
324
|
text: value,
|
|
200
325
|
color,
|
|
201
|
-
size:
|
|
326
|
+
size: textSize,
|
|
327
|
+
font: textFont,
|
|
328
|
+
bold: textBold,
|
|
329
|
+
italic: textItalic,
|
|
330
|
+
underline: textUnderline
|
|
202
331
|
};
|
|
203
332
|
setAnnotations((prev) => [...prev, anno]);
|
|
204
333
|
setSelectedId(anno.id);
|
|
@@ -236,10 +365,10 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
236
365
|
setAnnotations((prev) => prev.slice(0, -1));
|
|
237
366
|
setSelectedId(null);
|
|
238
367
|
};
|
|
239
|
-
const
|
|
368
|
+
const compositeToCanvas = async () => {
|
|
240
369
|
const c = canvasRef.current;
|
|
241
370
|
const svg = svgRef.current;
|
|
242
|
-
if (!c || !svg || !imageSize) return;
|
|
371
|
+
if (!c || !svg || !imageSize) return null;
|
|
243
372
|
const out = document.createElement("canvas");
|
|
244
373
|
out.width = imageSize.w;
|
|
245
374
|
out.height = imageSize.h;
|
|
@@ -251,30 +380,57 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
251
380
|
const svgBlob = new Blob([xml], { type: "image/svg+xml;charset=utf-8" });
|
|
252
381
|
const svgUrl = URL.createObjectURL(svgBlob);
|
|
253
382
|
const svgImg = new Image();
|
|
254
|
-
await new Promise((resolve
|
|
383
|
+
await new Promise((resolve) => {
|
|
255
384
|
svgImg.onload = () => resolve();
|
|
256
|
-
svgImg.onerror =
|
|
385
|
+
svgImg.onerror = () => resolve();
|
|
257
386
|
svgImg.src = svgUrl;
|
|
258
|
-
}).catch(() => {
|
|
259
387
|
});
|
|
260
388
|
octx.drawImage(svgImg, 0, 0, imageSize.w, imageSize.h);
|
|
261
389
|
URL.revokeObjectURL(svgUrl);
|
|
262
|
-
out
|
|
263
|
-
|
|
264
|
-
|
|
390
|
+
return out;
|
|
391
|
+
};
|
|
392
|
+
useImperativeHandle(ref, () => ({
|
|
393
|
+
save: async () => {
|
|
394
|
+
const out = await compositeToCanvas();
|
|
395
|
+
if (!out) return;
|
|
396
|
+
out.toBlob((blob) => {
|
|
397
|
+
if (!blob) {
|
|
398
|
+
toast_default.error("Failed to export");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const url = URL.createObjectURL(blob);
|
|
402
|
+
const a = document.createElement("a");
|
|
403
|
+
a.href = url;
|
|
404
|
+
const base = filename.replace(/\.[^.]+$/, "");
|
|
405
|
+
a.download = `${base}-annotated.png`;
|
|
406
|
+
a.click();
|
|
407
|
+
URL.revokeObjectURL(url);
|
|
408
|
+
}, "image/png");
|
|
409
|
+
},
|
|
410
|
+
copy: async () => {
|
|
411
|
+
if (!("clipboard" in navigator) || typeof ClipboardItem === "undefined") {
|
|
412
|
+
toast_default.error("Clipboard images not supported in this browser");
|
|
265
413
|
return;
|
|
266
414
|
}
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
415
|
+
const out = await compositeToCanvas();
|
|
416
|
+
if (!out) return;
|
|
417
|
+
out.toBlob(async (blob) => {
|
|
418
|
+
if (!blob) {
|
|
419
|
+
toast_default.error("Failed to copy");
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
|
|
424
|
+
toast_default.success("Copied to clipboard");
|
|
425
|
+
} catch {
|
|
426
|
+
toast_default.error("Copy failed (clipboard permission?)");
|
|
427
|
+
}
|
|
428
|
+
}, "image/png");
|
|
429
|
+
}
|
|
430
|
+
}), [imageSize, filename]);
|
|
276
431
|
const tools = useMemo(() => [
|
|
277
432
|
{ id: "select", label: "Select / Move", icon: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 3l7 17 2-7 7-2L5 3z" }) }) },
|
|
433
|
+
{ id: "draw", label: "Pen / Draw", icon: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487z" }) }) },
|
|
278
434
|
{ id: "rect", label: "Rectangle", icon: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("rect", { x: "4", y: "6", width: "16", height: "12", rx: "3" }) }) },
|
|
279
435
|
{ id: "circle", label: "Ellipse", icon: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "12", rx: "8", ry: "6" }) }) },
|
|
280
436
|
{ id: "arrow", label: "Arrow", icon: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 19L19 5m0 0h-7m7 0v7" }) }) },
|
|
@@ -283,9 +439,12 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
283
439
|
{ id: "crop", label: "Crop", icon: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 2v15a1 1 0 001 1h15M2 6h15a1 1 0 011 1v15" }) }) }
|
|
284
440
|
], []);
|
|
285
441
|
const btnClass = (active) => `p-1.5 rounded transition-colors ${active ? "bg-blue-500 text-white" : "text-gray-700 hover:bg-gray-200"}`;
|
|
286
|
-
const
|
|
442
|
+
const ctxType = selected?.type ?? (tool === "select" ? null : tool);
|
|
443
|
+
const showStrokeControl = ctxType === "rect" || ctxType === "circle" || ctxType === "arrow" || ctxType === "draw";
|
|
444
|
+
const showRectRadius = ctxType === "rect";
|
|
445
|
+
const showTextControls = ctxType === "text";
|
|
287
446
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full bg-gray-100", children: [
|
|
288
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-
|
|
447
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-3 py-2 border-b border-gray-200 bg-white shrink-0 flex-wrap text-[12px]", children: [
|
|
289
448
|
tools.map((t) => /* @__PURE__ */ jsx(
|
|
290
449
|
"button",
|
|
291
450
|
{
|
|
@@ -303,53 +462,55 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
303
462
|
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: COLORS.map((c) => /* @__PURE__ */ jsx(
|
|
304
463
|
"button",
|
|
305
464
|
{
|
|
306
|
-
onClick: () =>
|
|
307
|
-
setColor(c);
|
|
308
|
-
if (selectedId) {
|
|
309
|
-
setAnnotations((prev) => prev.map((a) => a.id === selectedId && a.type !== "mosaic" ? { ...a, color: c } : a));
|
|
310
|
-
}
|
|
311
|
-
},
|
|
465
|
+
onClick: () => setColorAndApply(c),
|
|
312
466
|
title: c,
|
|
313
467
|
className: `h-5 w-5 rounded-full border ${color === c ? "ring-2 ring-blue-500 ring-offset-1" : "border-gray-300"}`,
|
|
314
468
|
style: { background: c }
|
|
315
469
|
},
|
|
316
470
|
c
|
|
317
471
|
)) }),
|
|
318
|
-
/* @__PURE__ */
|
|
319
|
-
|
|
320
|
-
/* @__PURE__ */
|
|
321
|
-
|
|
322
|
-
"input",
|
|
323
|
-
{
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
)
|
|
333
|
-
|
|
472
|
+
showStrokeControl && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
473
|
+
/* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
|
|
474
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
|
|
475
|
+
/* @__PURE__ */ jsx("span", { children: "Weight" }),
|
|
476
|
+
/* @__PURE__ */ jsx("input", { type: "range", min: 1, max: 20, step: 1, value: stroke, onChange: (e) => setStrokeAndApply(Number(e.target.value)), className: "w-16 accent-blue-500" }),
|
|
477
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums w-5 text-right", children: stroke })
|
|
478
|
+
] })
|
|
479
|
+
] }),
|
|
480
|
+
showRectRadius && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
481
|
+
/* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
|
|
482
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
|
|
483
|
+
/* @__PURE__ */ jsx("span", { children: "Radius" }),
|
|
484
|
+
/* @__PURE__ */ jsx("input", { type: "range", min: 0, max: 48, step: 1, value: rectRadius, onChange: (e) => setRectRadiusAndApply(Number(e.target.value)), className: "w-16 accent-blue-500" }),
|
|
485
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums w-6 text-right", children: rectRadius })
|
|
486
|
+
] })
|
|
487
|
+
] }),
|
|
488
|
+
showTextControls && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
489
|
+
/* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
|
|
490
|
+
/* @__PURE__ */ jsx("select", { value: textFont, onChange: (e) => setTextFontAndApply(e.target.value), className: "text-xs border border-gray-300 rounded px-1 py-0.5 bg-white", children: FONTS.map((f) => /* @__PURE__ */ jsx("option", { value: f.id, children: f.label }, f.id)) }),
|
|
491
|
+
/* @__PURE__ */ jsx("button", { onClick: () => toggleTextStyleAndApply("bold"), className: btnClass(textBold), title: "Bold", children: /* @__PURE__ */ jsx("span", { className: "font-bold", children: "B" }) }),
|
|
492
|
+
/* @__PURE__ */ jsx("button", { onClick: () => toggleTextStyleAndApply("italic"), className: btnClass(textItalic), title: "Italic", children: /* @__PURE__ */ jsx("span", { className: "italic", children: "I" }) }),
|
|
493
|
+
/* @__PURE__ */ jsx("button", { onClick: () => toggleTextStyleAndApply("underline"), className: btnClass(textUnderline), title: "Underline", children: /* @__PURE__ */ jsx("span", { className: "underline", children: "U" }) }),
|
|
494
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
|
|
495
|
+
/* @__PURE__ */ jsx("span", { children: "Size" }),
|
|
496
|
+
/* @__PURE__ */ jsx("input", { type: "range", min: 10, max: 96, step: 1, value: textSize, onChange: (e) => setTextSizeAndApply(Number(e.target.value)), className: "w-16 accent-blue-500" }),
|
|
497
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums w-7 text-right", children: textSize })
|
|
498
|
+
] })
|
|
334
499
|
] }),
|
|
335
500
|
/* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
|
|
336
|
-
/* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.max(ZOOM_MIN, Math.round((z - ZOOM_STEP) * 100) / 100)), className: "px-2 py-1
|
|
337
|
-
/* @__PURE__ */ jsxs("span", { className: "text-
|
|
501
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.max(ZOOM_MIN, Math.round((z - ZOOM_STEP) * 100) / 100)), className: "px-2 py-1 rounded hover:bg-gray-200 text-gray-700", title: "Zoom out", children: "\u2212" }),
|
|
502
|
+
/* @__PURE__ */ jsxs("span", { className: "text-gray-600 tabular-nums w-10 text-center", children: [
|
|
338
503
|
Math.round(zoom * 100),
|
|
339
504
|
"%"
|
|
340
505
|
] }),
|
|
341
|
-
/* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.min(ZOOM_MAX, Math.round((z + ZOOM_STEP) * 100) / 100)), className: "px-2 py-1
|
|
342
|
-
/* @__PURE__ */ jsx("button", { onClick: () => setZoom(1), className: "px-2 py-1
|
|
506
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.min(ZOOM_MAX, Math.round((z + ZOOM_STEP) * 100) / 100)), className: "px-2 py-1 rounded hover:bg-gray-200 text-gray-700", title: "Zoom in", children: "+" }),
|
|
507
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setZoom(1), className: "px-2 py-1 rounded hover:bg-gray-200 text-gray-700", title: "Fit to area", children: "Fit" }),
|
|
343
508
|
/* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
|
|
344
|
-
/* @__PURE__ */ jsx("button", { onClick: undoLast, disabled: annotations.length === 0, className: "px-2 py-1
|
|
345
|
-
/* @__PURE__ */ jsx("
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
/* @__PURE__ */ jsx("button", { onClick: cancelCrop, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Cancel" })
|
|
350
|
-
] }),
|
|
351
|
-
/* @__PURE__ */ jsx("button", { onClick: onClose, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Exit" })
|
|
352
|
-
] })
|
|
509
|
+
/* @__PURE__ */ jsx("button", { onClick: undoLast, disabled: annotations.length === 0, className: "px-2 py-1 rounded hover:bg-gray-200 disabled:opacity-30 text-gray-700", children: "Undo" }),
|
|
510
|
+
/* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center gap-2", children: pendingCrop && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
511
|
+
/* @__PURE__ */ jsx("button", { onClick: applyCrop, className: "px-2 py-1 rounded bg-blue-500 text-white hover:bg-blue-600", children: "Apply Crop" }),
|
|
512
|
+
/* @__PURE__ */ jsx("button", { onClick: cancelCrop, className: "px-2 py-1 rounded hover:bg-gray-200 text-gray-700", children: "Cancel" })
|
|
513
|
+
] }) })
|
|
353
514
|
] }),
|
|
354
515
|
/* @__PURE__ */ jsx("div", { ref: wrapRef, className: "flex-1 overflow-auto bg-gray-200 flex items-center justify-center p-4 relative", children: displaySize && imageSize && /* @__PURE__ */ jsxs(
|
|
355
516
|
"div",
|
|
@@ -357,13 +518,7 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
357
518
|
className: "relative shadow-lg rounded overflow-hidden bg-white shrink-0",
|
|
358
519
|
style: { width: displaySize.w, height: displaySize.h },
|
|
359
520
|
children: [
|
|
360
|
-
/* @__PURE__ */ jsx(
|
|
361
|
-
"canvas",
|
|
362
|
-
{
|
|
363
|
-
ref: canvasRef,
|
|
364
|
-
style: { position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }
|
|
365
|
-
}
|
|
366
|
-
),
|
|
521
|
+
/* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: { position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" } }),
|
|
367
522
|
/* @__PURE__ */ jsxs(
|
|
368
523
|
"svg",
|
|
369
524
|
{
|
|
@@ -379,20 +534,16 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
379
534
|
cursor: tool === "select" ? "default" : tool === "text" ? "text" : "crosshair"
|
|
380
535
|
},
|
|
381
536
|
onPointerDown: handleSvgPointerDown,
|
|
382
|
-
onPointerMove: handleSvgPointerMove,
|
|
383
|
-
onPointerUp: handleSvgPointerUp,
|
|
384
|
-
onPointerCancel: () => {
|
|
385
|
-
dragRef.current = null;
|
|
386
|
-
setPreview(null);
|
|
387
|
-
},
|
|
388
537
|
children: [
|
|
389
538
|
annotations.map((a) => /* @__PURE__ */ jsx(
|
|
390
539
|
AnnotationView,
|
|
391
540
|
{
|
|
392
541
|
anno: a,
|
|
393
542
|
selected: selectedId === a.id,
|
|
543
|
+
zoom,
|
|
394
544
|
onPointerDown: (e) => handleAnnoPointerDown(e, a),
|
|
395
|
-
onDoubleClick: () => handleAnnoDoubleClick(a)
|
|
545
|
+
onDoubleClick: () => handleAnnoDoubleClick(a),
|
|
546
|
+
onHandlePointerDown: (e, corner) => handleHandlePointerDown(e, a, corner)
|
|
396
547
|
},
|
|
397
548
|
a.id
|
|
398
549
|
)),
|
|
@@ -402,51 +553,34 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
402
553
|
}
|
|
403
554
|
),
|
|
404
555
|
pendingText && /* @__PURE__ */ jsx(
|
|
405
|
-
|
|
556
|
+
PendingTextEditor,
|
|
406
557
|
{
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
value: pendingText.value,
|
|
419
|
-
onChange: (e) => setPendingText({ ...pendingText, value: e.target.value }),
|
|
420
|
-
onBlur: commitText,
|
|
421
|
-
onKeyDown: (e) => {
|
|
422
|
-
if (e.key === "Escape") {
|
|
423
|
-
setPendingText(null);
|
|
424
|
-
} else if (e.key === "Enter" && !e.shiftKey) {
|
|
425
|
-
e.preventDefault();
|
|
426
|
-
commitText();
|
|
427
|
-
}
|
|
428
|
-
},
|
|
429
|
-
placeholder: "Type then Enter\u2026",
|
|
430
|
-
rows: 1,
|
|
431
|
-
className: "bg-white/95 border border-blue-400 rounded px-1 py-0.5 text-sm outline-none resize-none shadow-md",
|
|
432
|
-
style: { color, fontWeight: 600, minWidth: 80 }
|
|
433
|
-
}
|
|
434
|
-
)
|
|
558
|
+
pendingText,
|
|
559
|
+
color,
|
|
560
|
+
size: textSize,
|
|
561
|
+
font: FONTS.find((f) => f.id === textFont)?.css ?? FONTS[0].css,
|
|
562
|
+
bold: textBold,
|
|
563
|
+
italic: textItalic,
|
|
564
|
+
underline: textUnderline,
|
|
565
|
+
scale,
|
|
566
|
+
onChange: (value) => setPendingText({ ...pendingText, value }),
|
|
567
|
+
onCommit: commitText,
|
|
568
|
+
onCancel: () => setPendingText(null)
|
|
435
569
|
}
|
|
436
570
|
)
|
|
437
571
|
]
|
|
438
572
|
}
|
|
439
573
|
) }),
|
|
440
|
-
/* @__PURE__ */ jsx("div", { className: "px-3 py-1.5 border-t border-gray-200 bg-white text-[11px] text-gray-500 shrink-0", children: tool === "select" ? selectedId ? "Drag to move. Delete / Backspace removes. Click outside to deselect." : "Tap a shape to select
|
|
574
|
+
/* @__PURE__ */ jsx("div", { className: "px-3 py-1.5 border-t border-gray-200 bg-white text-[11px] text-gray-500 shrink-0", children: tool === "select" ? selectedId ? "Drag to move. Drag a corner to resize. Delete / Backspace removes. Click outside to deselect." : "Tap a shape to select. Double-click text to edit." : tool === "text" ? "Click to drop a text label." : tool === "crop" ? "Drag a rectangle to set the crop region." : tool === "draw" ? "Drag to draw freehand." : "Drag on the image to draw." })
|
|
441
575
|
] });
|
|
442
|
-
}
|
|
443
|
-
|
|
576
|
+
});
|
|
577
|
+
var ImageAnnotator_default = ImageAnnotator;
|
|
578
|
+
function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDoubleClick, onHandlePointerDown }) {
|
|
444
579
|
const dim = boundingBox(anno);
|
|
445
|
-
const interactive = onPointerDown ? { onPointerDown, style: { cursor: "move" } } : {};
|
|
580
|
+
const interactive = onPointerDown ? { onPointerDown, pointerEvents: "all", style: { cursor: "move" } } : {};
|
|
446
581
|
const dblc = onDoubleClick ? { onDoubleClick } : {};
|
|
447
582
|
let body;
|
|
448
583
|
if (anno.type === "rect") {
|
|
449
|
-
const radius = Math.min(anno.w, anno.h, 16) * 0.4;
|
|
450
584
|
body = /* @__PURE__ */ jsx(
|
|
451
585
|
"rect",
|
|
452
586
|
{
|
|
@@ -454,8 +588,8 @@ function AnnotationView({ anno, selected, preview, onPointerDown, onDoubleClick
|
|
|
454
588
|
y: anno.y,
|
|
455
589
|
width: anno.w,
|
|
456
590
|
height: anno.h,
|
|
457
|
-
rx: radius,
|
|
458
|
-
ry: radius,
|
|
591
|
+
rx: anno.radius,
|
|
592
|
+
ry: anno.radius,
|
|
459
593
|
fill: "none",
|
|
460
594
|
stroke: anno.color,
|
|
461
595
|
strokeWidth: anno.stroke,
|
|
@@ -480,6 +614,19 @@ function AnnotationView({ anno, selected, preview, onPointerDown, onDoubleClick
|
|
|
480
614
|
);
|
|
481
615
|
} else if (anno.type === "arrow") {
|
|
482
616
|
body = /* @__PURE__ */ jsx(ArrowShape, { anno, interactive });
|
|
617
|
+
} else if (anno.type === "draw") {
|
|
618
|
+
body = /* @__PURE__ */ jsx(
|
|
619
|
+
"path",
|
|
620
|
+
{
|
|
621
|
+
d: pointsToPath(anno.points),
|
|
622
|
+
fill: "none",
|
|
623
|
+
stroke: anno.color,
|
|
624
|
+
strokeWidth: anno.stroke,
|
|
625
|
+
strokeLinecap: "round",
|
|
626
|
+
strokeLinejoin: "round",
|
|
627
|
+
...interactive
|
|
628
|
+
}
|
|
629
|
+
);
|
|
483
630
|
} else if (anno.type === "mosaic") {
|
|
484
631
|
body = /* @__PURE__ */ jsx(
|
|
485
632
|
"rect",
|
|
@@ -496,6 +643,7 @@ function AnnotationView({ anno, selected, preview, onPointerDown, onDoubleClick
|
|
|
496
643
|
}
|
|
497
644
|
);
|
|
498
645
|
} else {
|
|
646
|
+
const font = FONTS.find((f) => f.id === anno.font)?.css ?? FONTS[0].css;
|
|
499
647
|
body = /* @__PURE__ */ jsx(
|
|
500
648
|
"text",
|
|
501
649
|
{
|
|
@@ -503,8 +651,10 @@ function AnnotationView({ anno, selected, preview, onPointerDown, onDoubleClick
|
|
|
503
651
|
y: anno.y + anno.size,
|
|
504
652
|
fill: anno.color,
|
|
505
653
|
fontSize: anno.size,
|
|
506
|
-
fontWeight:
|
|
507
|
-
|
|
654
|
+
fontWeight: anno.bold ? 700 : 400,
|
|
655
|
+
fontStyle: anno.italic ? "italic" : "normal",
|
|
656
|
+
textDecoration: anno.underline ? "underline" : void 0,
|
|
657
|
+
fontFamily: font,
|
|
508
658
|
style: { userSelect: "none" },
|
|
509
659
|
...interactive,
|
|
510
660
|
...dblc,
|
|
@@ -514,21 +664,56 @@ function AnnotationView({ anno, selected, preview, onPointerDown, onDoubleClick
|
|
|
514
664
|
}
|
|
515
665
|
return /* @__PURE__ */ jsxs("g", { children: [
|
|
516
666
|
body,
|
|
517
|
-
selected && !preview && /* @__PURE__ */
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
667
|
+
selected && !preview && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
668
|
+
/* @__PURE__ */ jsx(
|
|
669
|
+
"rect",
|
|
670
|
+
{
|
|
671
|
+
"data-chrome": "selection",
|
|
672
|
+
x: dim.x - 4,
|
|
673
|
+
y: dim.y - 4,
|
|
674
|
+
width: dim.w + 8,
|
|
675
|
+
height: dim.h + 8,
|
|
676
|
+
fill: "none",
|
|
677
|
+
stroke: "#3b82f6",
|
|
678
|
+
strokeWidth: 2 / zoom,
|
|
679
|
+
strokeDasharray: `${6 / zoom},${4 / zoom}`,
|
|
680
|
+
pointerEvents: "none"
|
|
681
|
+
}
|
|
682
|
+
),
|
|
683
|
+
onHandlePointerDown && /* @__PURE__ */ jsx(ResizeHandles, { anno, zoom, onHandlePointerDown })
|
|
684
|
+
] })
|
|
685
|
+
] });
|
|
686
|
+
}
|
|
687
|
+
function ResizeHandles({
|
|
688
|
+
anno,
|
|
689
|
+
zoom,
|
|
690
|
+
onHandlePointerDown
|
|
691
|
+
}) {
|
|
692
|
+
const r = 6 / zoom;
|
|
693
|
+
const sw = 1.5 / zoom;
|
|
694
|
+
const handleProps = (cursor) => ({
|
|
695
|
+
fill: "#fff",
|
|
696
|
+
stroke: "#3b82f6",
|
|
697
|
+
strokeWidth: sw,
|
|
698
|
+
pointerEvents: "all",
|
|
699
|
+
style: { cursor },
|
|
700
|
+
"data-chrome": "handle"
|
|
701
|
+
});
|
|
702
|
+
if (anno.type === "arrow") {
|
|
703
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
704
|
+
/* @__PURE__ */ jsx("circle", { cx: anno.x1, cy: anno.y1, r, ...handleProps("grab"), onPointerDown: (e) => onHandlePointerDown(e, "start") }),
|
|
705
|
+
/* @__PURE__ */ jsx("circle", { cx: anno.x2, cy: anno.y2, r, ...handleProps("grab"), onPointerDown: (e) => onHandlePointerDown(e, "end") })
|
|
706
|
+
] });
|
|
707
|
+
}
|
|
708
|
+
if (anno.type === "text" || anno.type === "draw") {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
const x = anno.x, y = anno.y, w = anno.w, h = anno.h;
|
|
712
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
713
|
+
/* @__PURE__ */ jsx("circle", { cx: x, cy: y, r, ...handleProps("nwse-resize"), onPointerDown: (e) => onHandlePointerDown(e, "nw") }),
|
|
714
|
+
/* @__PURE__ */ jsx("circle", { cx: x + w, cy: y, r, ...handleProps("nesw-resize"), onPointerDown: (e) => onHandlePointerDown(e, "ne") }),
|
|
715
|
+
/* @__PURE__ */ jsx("circle", { cx: x + w, cy: y + h, r, ...handleProps("nwse-resize"), onPointerDown: (e) => onHandlePointerDown(e, "se") }),
|
|
716
|
+
/* @__PURE__ */ jsx("circle", { cx: x, cy: y + h, r, ...handleProps("nesw-resize"), onPointerDown: (e) => onHandlePointerDown(e, "sw") })
|
|
532
717
|
] });
|
|
533
718
|
}
|
|
534
719
|
function ArrowShape({ anno, interactive }) {
|
|
@@ -578,10 +763,79 @@ function CropOverlay({ rect, imageSize }) {
|
|
|
578
763
|
)
|
|
579
764
|
] });
|
|
580
765
|
}
|
|
581
|
-
function
|
|
582
|
-
|
|
766
|
+
function PendingTextEditor({
|
|
767
|
+
pendingText,
|
|
768
|
+
color,
|
|
769
|
+
size,
|
|
770
|
+
font,
|
|
771
|
+
bold,
|
|
772
|
+
italic,
|
|
773
|
+
underline,
|
|
774
|
+
scale,
|
|
775
|
+
onChange,
|
|
776
|
+
onCommit,
|
|
777
|
+
onCancel
|
|
778
|
+
}) {
|
|
779
|
+
const ref = useRef(null);
|
|
780
|
+
useEffect(() => {
|
|
781
|
+
const id = requestAnimationFrame(() => {
|
|
782
|
+
ref.current?.focus();
|
|
783
|
+
ref.current?.select?.();
|
|
784
|
+
});
|
|
785
|
+
return () => cancelAnimationFrame(id);
|
|
786
|
+
}, []);
|
|
787
|
+
return /* @__PURE__ */ jsx(
|
|
788
|
+
"div",
|
|
789
|
+
{
|
|
790
|
+
style: {
|
|
791
|
+
position: "absolute",
|
|
792
|
+
left: `${pendingText.x * scale}px`,
|
|
793
|
+
top: `${pendingText.y * scale}px`,
|
|
794
|
+
transform: "translateY(-2px)",
|
|
795
|
+
zIndex: 5
|
|
796
|
+
},
|
|
797
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
798
|
+
children: /* @__PURE__ */ jsx(
|
|
799
|
+
"textarea",
|
|
800
|
+
{
|
|
801
|
+
ref,
|
|
802
|
+
value: pendingText.value,
|
|
803
|
+
onChange: (e) => onChange(e.target.value),
|
|
804
|
+
onBlur: onCommit,
|
|
805
|
+
onKeyDown: (e) => {
|
|
806
|
+
if (e.key === "Escape") {
|
|
807
|
+
e.preventDefault();
|
|
808
|
+
onCancel();
|
|
809
|
+
} else if (e.key === "Enter" && !e.shiftKey) {
|
|
810
|
+
e.preventDefault();
|
|
811
|
+
onCommit();
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
placeholder: "Type then Enter\u2026",
|
|
815
|
+
rows: 1,
|
|
816
|
+
className: "bg-white/95 border border-blue-400 rounded px-1 py-0.5 outline-none resize-none shadow-md",
|
|
817
|
+
style: {
|
|
818
|
+
color,
|
|
819
|
+
fontSize: `${size * scale}px`,
|
|
820
|
+
fontFamily: font,
|
|
821
|
+
fontWeight: bold ? 700 : 400,
|
|
822
|
+
fontStyle: italic ? "italic" : "normal",
|
|
823
|
+
textDecoration: underline ? "underline" : void 0,
|
|
824
|
+
minWidth: 80
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
function makeShape(tool, start, end, color, stroke, rectRadius) {
|
|
832
|
+
if (tool === "rect") {
|
|
583
833
|
const r = normalizeRect(start, end);
|
|
584
|
-
return { id: "", type:
|
|
834
|
+
return { id: "", type: "rect", x: r.x, y: r.y, w: r.w, h: r.h, color, stroke, radius: rectRadius };
|
|
835
|
+
}
|
|
836
|
+
if (tool === "circle") {
|
|
837
|
+
const r = normalizeRect(start, end);
|
|
838
|
+
return { id: "", type: "circle", x: r.x, y: r.y, w: r.w, h: r.h, color, stroke };
|
|
585
839
|
}
|
|
586
840
|
if (tool === "arrow") {
|
|
587
841
|
return { id: "", type: "arrow", x1: start.x, y1: start.y, x2: end.x, y2: end.y, color, stroke };
|
|
@@ -593,22 +847,47 @@ function makeShape(tool, start, end, color, stroke) {
|
|
|
593
847
|
return null;
|
|
594
848
|
}
|
|
595
849
|
function isTrivial(a) {
|
|
596
|
-
if (a.type === "arrow")
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if ("w" in a && "h" in a) {
|
|
600
|
-
return a.w < 4 || a.h < 4;
|
|
601
|
-
}
|
|
850
|
+
if (a.type === "arrow") return Math.abs(a.x2 - a.x1) < 4 && Math.abs(a.y2 - a.y1) < 4;
|
|
851
|
+
if (a.type === "draw") return a.points.length < 2;
|
|
852
|
+
if ("w" in a && "h" in a) return a.w < 4 || a.h < 4;
|
|
602
853
|
return false;
|
|
603
854
|
}
|
|
604
855
|
function translate(a, dx, dy) {
|
|
605
|
-
if (a.type === "arrow") {
|
|
606
|
-
|
|
856
|
+
if (a.type === "arrow") return { ...a, x1: a.x1 + dx, y1: a.y1 + dy, x2: a.x2 + dx, y2: a.y2 + dy };
|
|
857
|
+
if (a.type === "draw") return { ...a, points: a.points.map((p) => ({ x: p.x + dx, y: p.y + dy })) };
|
|
858
|
+
if (a.type === "text") return { ...a, x: a.x + dx, y: a.y + dy };
|
|
859
|
+
return { ...a, x: a.x + dx, y: a.y + dy };
|
|
860
|
+
}
|
|
861
|
+
function resize(original, corner, p) {
|
|
862
|
+
if (original.type === "arrow") {
|
|
863
|
+
if (corner === "start") return { ...original, x1: p.x, y1: p.y };
|
|
864
|
+
if (corner === "end") return { ...original, x2: p.x, y2: p.y };
|
|
865
|
+
return original;
|
|
607
866
|
}
|
|
608
|
-
if (
|
|
609
|
-
|
|
867
|
+
if (original.type === "text" || original.type === "draw") return original;
|
|
868
|
+
const left = original.x;
|
|
869
|
+
const right = original.x + original.w;
|
|
870
|
+
const top = original.y;
|
|
871
|
+
const bottom = original.y + original.h;
|
|
872
|
+
let x1 = left, y1 = top, x2 = right, y2 = bottom;
|
|
873
|
+
if (corner === "nw") {
|
|
874
|
+
x1 = p.x;
|
|
875
|
+
y1 = p.y;
|
|
610
876
|
}
|
|
611
|
-
|
|
877
|
+
if (corner === "ne") {
|
|
878
|
+
x2 = p.x;
|
|
879
|
+
y1 = p.y;
|
|
880
|
+
}
|
|
881
|
+
if (corner === "se") {
|
|
882
|
+
x2 = p.x;
|
|
883
|
+
y2 = p.y;
|
|
884
|
+
}
|
|
885
|
+
if (corner === "sw") {
|
|
886
|
+
x1 = p.x;
|
|
887
|
+
y2 = p.y;
|
|
888
|
+
}
|
|
889
|
+
const r = normalizeRect({ x: x1, y: y1 }, { x: x2, y: y2 });
|
|
890
|
+
return { ...original, x: r.x, y: r.y, w: r.w, h: r.h };
|
|
612
891
|
}
|
|
613
892
|
function boundingBox(a) {
|
|
614
893
|
if (a.type === "arrow") {
|
|
@@ -616,6 +895,17 @@ function boundingBox(a) {
|
|
|
616
895
|
const y = Math.min(a.y1, a.y2);
|
|
617
896
|
return { x, y, w: Math.abs(a.x2 - a.x1), h: Math.abs(a.y2 - a.y1) };
|
|
618
897
|
}
|
|
898
|
+
if (a.type === "draw") {
|
|
899
|
+
if (a.points.length === 0) return { x: 0, y: 0, w: 0, h: 0 };
|
|
900
|
+
let minX = a.points[0].x, maxX = a.points[0].x, minY = a.points[0].y, maxY = a.points[0].y;
|
|
901
|
+
for (const p of a.points) {
|
|
902
|
+
if (p.x < minX) minX = p.x;
|
|
903
|
+
if (p.x > maxX) maxX = p.x;
|
|
904
|
+
if (p.y < minY) minY = p.y;
|
|
905
|
+
if (p.y > maxY) maxY = p.y;
|
|
906
|
+
}
|
|
907
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
908
|
+
}
|
|
619
909
|
if (a.type === "text") {
|
|
620
910
|
const lines = a.text.split("\n");
|
|
621
911
|
const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
|
|
@@ -634,6 +924,12 @@ function withinBounds(a, w, h) {
|
|
|
634
924
|
const bb = boundingBox(a);
|
|
635
925
|
return bb.x + bb.w > 0 && bb.y + bb.h > 0 && bb.x < w && bb.y < h;
|
|
636
926
|
}
|
|
927
|
+
function pointsToPath(points) {
|
|
928
|
+
if (points.length === 0) return "";
|
|
929
|
+
let d = `M ${points[0].x} ${points[0].y}`;
|
|
930
|
+
for (let i = 1; i < points.length; i++) d += ` L ${points[i].x} ${points[i].y}`;
|
|
931
|
+
return d;
|
|
932
|
+
}
|
|
637
933
|
function applyMosaic(ctx, rect) {
|
|
638
934
|
const x = Math.round(Math.max(0, rect.x));
|
|
639
935
|
const y = Math.round(Math.max(0, rect.y));
|
|
@@ -2053,6 +2349,7 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
|
|
|
2053
2349
|
const [zoom, setZoom] = useState(1);
|
|
2054
2350
|
const [error, setError] = useState(false);
|
|
2055
2351
|
const [annotating, setAnnotating] = useState(false);
|
|
2352
|
+
const annotatorRef = useRef(null);
|
|
2056
2353
|
const handleDefaultDownload = () => {
|
|
2057
2354
|
const a = document.createElement("a");
|
|
2058
2355
|
a.href = url;
|
|
@@ -2060,22 +2357,35 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
|
|
|
2060
2357
|
a.click();
|
|
2061
2358
|
};
|
|
2062
2359
|
const btn = "px-2 py-1 rounded hover:bg-gray-200 transition-colors text-gray-600 flex items-center gap-1";
|
|
2063
|
-
if (annotating) {
|
|
2064
|
-
return /* @__PURE__ */ jsx(ImageAnnotator, { src: url, filename, onClose: () => setAnnotating(false) });
|
|
2065
|
-
}
|
|
2066
2360
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
|
|
2067
2361
|
/* @__PURE__ */ jsxs(PanelActions, { children: [
|
|
2068
|
-
/* @__PURE__ */
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2362
|
+
!annotating && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2363
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.max(0.1, Math.round((z - 0.25) * 100) / 100)), className: btn, children: "\u2212" }),
|
|
2364
|
+
/* @__PURE__ */ jsxs("span", { className: "text-gray-500 w-12 text-center tabular-nums", children: [
|
|
2365
|
+
Math.round(zoom * 100),
|
|
2366
|
+
"%"
|
|
2367
|
+
] }),
|
|
2368
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.min(8, Math.round((z + 0.25) * 100) / 100)), className: btn, children: "+" }),
|
|
2369
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setZoom(1), className: btn, children: "1:1" }),
|
|
2370
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-300 mx-1" }),
|
|
2371
|
+
/* @__PURE__ */ jsxs("button", { onClick: () => setAnnotating(true), className: btn, title: "Annotate this image", children: [
|
|
2372
|
+
/* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" }) }),
|
|
2373
|
+
"Annotate"
|
|
2374
|
+
] })
|
|
2072
2375
|
] }),
|
|
2073
|
-
/* @__PURE__ */
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
"
|
|
2376
|
+
annotating && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2377
|
+
/* @__PURE__ */ jsxs("button", { onClick: () => setAnnotating(false), className: btn, title: "Back to viewer", children: [
|
|
2378
|
+
/* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15.75 19.5L8.25 12l7.5-7.5" }) }),
|
|
2379
|
+
"View"
|
|
2380
|
+
] }),
|
|
2381
|
+
/* @__PURE__ */ jsxs("button", { onClick: () => annotatorRef.current?.copy(), className: btn, children: [
|
|
2382
|
+
/* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" }) }),
|
|
2383
|
+
"Copy"
|
|
2384
|
+
] }),
|
|
2385
|
+
/* @__PURE__ */ jsxs("button", { onClick: () => annotatorRef.current?.save(), className: btn, children: [
|
|
2386
|
+
/* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" }) }),
|
|
2387
|
+
"Save"
|
|
2388
|
+
] })
|
|
2079
2389
|
] }),
|
|
2080
2390
|
/* @__PURE__ */ jsxs("button", { onClick: onDownload ?? handleDefaultDownload, className: btn, children: [
|
|
2081
2391
|
/* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" }) }),
|
|
@@ -2086,7 +2396,7 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
|
|
|
2086
2396
|
"Email"
|
|
2087
2397
|
] })
|
|
2088
2398
|
] }),
|
|
2089
|
-
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto bg-gray-100 flex items-center justify-center p-4", children: error ? /* @__PURE__ */ jsx("div", { className: "text-sm text-red-600", children: "Failed to load image." }) : /* @__PURE__ */ jsx(
|
|
2399
|
+
annotating ? /* @__PURE__ */ jsx(ImageAnnotator_default, { ref: annotatorRef, src: url, filename }) : /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto bg-gray-100 flex items-center justify-center p-4", children: error ? /* @__PURE__ */ jsx("div", { className: "text-sm text-red-600", children: "Failed to load image." }) : /* @__PURE__ */ jsx(
|
|
2090
2400
|
"img",
|
|
2091
2401
|
{
|
|
2092
2402
|
src: url,
|
|
@@ -2100,5 +2410,5 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
|
|
|
2100
2410
|
}
|
|
2101
2411
|
|
|
2102
2412
|
export { Preview, setPdfPreview };
|
|
2103
|
-
//# sourceMappingURL=chunk-
|
|
2104
|
-
//# sourceMappingURL=chunk-
|
|
2413
|
+
//# sourceMappingURL=chunk-6IV6OWF3.js.map
|
|
2414
|
+
//# sourceMappingURL=chunk-6IV6OWF3.js.map
|