react-os-shell 0.2.24 → 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-ZPMM53WI.js → Files-YHUOR7RM.js} +4 -4
- package/dist/{Files-ZPMM53WI.js.map → Files-YHUOR7RM.js.map} +1 -1
- package/dist/Preview-SLX4ZLUQ.js +6 -0
- package/dist/{Preview-QI4WKYS2.js.map → Preview-SLX4ZLUQ.js.map} +1 -1
- package/dist/apps/index.js +4 -4
- package/dist/{chunk-NC7UQF6K.js → chunk-23RBDC2Z.js} +3 -3
- package/dist/{chunk-NC7UQF6K.js.map → chunk-23RBDC2Z.js.map} +1 -1
- package/dist/{chunk-LJ6DLGTY.js → chunk-6IV6OWF3.js} +367 -192
- package/dist/chunk-6IV6OWF3.js.map +1 -0
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/dist/Preview-QI4WKYS2.js +0 -6
- package/dist/chunk-LJ6DLGTY.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);
|
|
@@ -34,6 +48,25 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
34
48
|
if (!fitSize) return null;
|
|
35
49
|
return { w: fitSize.w * zoom, h: fitSize.h * zoom };
|
|
36
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]);
|
|
37
70
|
useEffect(() => {
|
|
38
71
|
const img = new Image();
|
|
39
72
|
img.crossOrigin = "anonymous";
|
|
@@ -56,8 +89,7 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
56
89
|
const availH = Math.max(0, r.height - 32);
|
|
57
90
|
if (availW === 0 || availH === 0) return;
|
|
58
91
|
const ratio = imageSize.w / imageSize.h;
|
|
59
|
-
let w = imageSize.w;
|
|
60
|
-
let h = imageSize.h;
|
|
92
|
+
let w = imageSize.w, h = imageSize.h;
|
|
61
93
|
if (w > availW) {
|
|
62
94
|
w = availW;
|
|
63
95
|
h = w / ratio;
|
|
@@ -109,7 +141,13 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
109
141
|
if (!drag) return;
|
|
110
142
|
const p = evToImage(ev);
|
|
111
143
|
if (drag.kind === "draw") {
|
|
112
|
-
setPreview(makeShape(tool, drag.start, p, color, stroke));
|
|
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
|
+
}
|
|
113
151
|
} else if (drag.kind === "crop") {
|
|
114
152
|
setPendingCrop(normalizeRect(drag.start, p));
|
|
115
153
|
} else if (drag.kind === "move") {
|
|
@@ -133,6 +171,16 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
133
171
|
setTool("select");
|
|
134
172
|
return null;
|
|
135
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);
|
|
136
184
|
}
|
|
137
185
|
};
|
|
138
186
|
window.addEventListener("pointermove", onMove);
|
|
@@ -143,7 +191,7 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
143
191
|
window.removeEventListener("pointerup", onUp);
|
|
144
192
|
window.removeEventListener("pointercancel", onUp);
|
|
145
193
|
};
|
|
146
|
-
}, [isDragging, tool, color, stroke]);
|
|
194
|
+
}, [isDragging, tool, color, stroke, rectRadius]);
|
|
147
195
|
const handleAnnoPointerDown = (e, anno) => {
|
|
148
196
|
if (tool !== "select") return;
|
|
149
197
|
e.stopPropagation();
|
|
@@ -169,7 +217,12 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
169
217
|
beginDrag({ kind: "crop", start });
|
|
170
218
|
return;
|
|
171
219
|
}
|
|
172
|
-
|
|
220
|
+
if (tool === "draw") {
|
|
221
|
+
beginDrag({ kind: "pen", points: [start] });
|
|
222
|
+
setPreview({ id: "", type: "draw", points: [start], color, stroke });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
setPreview(makeShape(tool, start, start, color, stroke, rectRadius));
|
|
173
226
|
beginDrag({ kind: "draw", start });
|
|
174
227
|
};
|
|
175
228
|
const handleAnnoDoubleClick = (anno) => {
|
|
@@ -178,8 +231,14 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
178
231
|
};
|
|
179
232
|
useEffect(() => {
|
|
180
233
|
const onKey = (ev) => {
|
|
181
|
-
if (!selectedId) return;
|
|
182
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;
|
|
183
242
|
if (ev.key === "Delete" || ev.key === "Backspace") {
|
|
184
243
|
ev.preventDefault();
|
|
185
244
|
setAnnotations((prev) => prev.filter((a) => a.id !== selectedId));
|
|
@@ -191,17 +250,67 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
191
250
|
window.addEventListener("keydown", onKey);
|
|
192
251
|
return () => window.removeEventListener("keydown", onKey);
|
|
193
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
|
+
};
|
|
194
304
|
const commitText = () => {
|
|
195
305
|
if (!pendingText) return;
|
|
196
|
-
const value = pendingText.value
|
|
197
|
-
if (!value) {
|
|
306
|
+
const value = pendingText.value;
|
|
307
|
+
if (!value.trim()) {
|
|
198
308
|
if (pendingText.editingId) {
|
|
199
309
|
setAnnotations((prev) => prev.filter((a) => a.id !== pendingText.editingId));
|
|
200
310
|
}
|
|
201
311
|
setPendingText(null);
|
|
202
312
|
return;
|
|
203
313
|
}
|
|
204
|
-
const fontSize = Math.max(16, stroke * 6);
|
|
205
314
|
if (pendingText.editingId) {
|
|
206
315
|
setAnnotations((prev) => prev.map(
|
|
207
316
|
(a) => a.id === pendingText.editingId && a.type === "text" ? { ...a, text: value } : a
|
|
@@ -214,7 +323,11 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
214
323
|
y: pendingText.y,
|
|
215
324
|
text: value,
|
|
216
325
|
color,
|
|
217
|
-
size:
|
|
326
|
+
size: textSize,
|
|
327
|
+
font: textFont,
|
|
328
|
+
bold: textBold,
|
|
329
|
+
italic: textItalic,
|
|
330
|
+
underline: textUnderline
|
|
218
331
|
};
|
|
219
332
|
setAnnotations((prev) => [...prev, anno]);
|
|
220
333
|
setSelectedId(anno.id);
|
|
@@ -276,45 +389,48 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
276
389
|
URL.revokeObjectURL(svgUrl);
|
|
277
390
|
return out;
|
|
278
391
|
};
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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");
|
|
285
413
|
return;
|
|
286
414
|
}
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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");
|
|
300
429
|
}
|
|
301
|
-
|
|
302
|
-
if (!out) return;
|
|
303
|
-
out.toBlob(async (blob) => {
|
|
304
|
-
if (!blob) {
|
|
305
|
-
toast_default.error("Failed to copy");
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
try {
|
|
309
|
-
await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
|
|
310
|
-
toast_default.success("Copied to clipboard");
|
|
311
|
-
} catch {
|
|
312
|
-
toast_default.error("Copy failed (clipboard permission?)");
|
|
313
|
-
}
|
|
314
|
-
}, "image/png");
|
|
315
|
-
};
|
|
430
|
+
}), [imageSize, filename]);
|
|
316
431
|
const tools = useMemo(() => [
|
|
317
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" }) }) },
|
|
318
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" }) }) },
|
|
319
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" }) }) },
|
|
320
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" }) }) },
|
|
@@ -323,9 +439,12 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
323
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" }) }) }
|
|
324
440
|
], []);
|
|
325
441
|
const btnClass = (active) => `p-1.5 rounded transition-colors ${active ? "bg-blue-500 text-white" : "text-gray-700 hover:bg-gray-200"}`;
|
|
326
|
-
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";
|
|
327
446
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full bg-gray-100", children: [
|
|
328
|
-
/* @__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: [
|
|
329
448
|
tools.map((t) => /* @__PURE__ */ jsx(
|
|
330
449
|
"button",
|
|
331
450
|
{
|
|
@@ -343,54 +462,55 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
343
462
|
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: COLORS.map((c) => /* @__PURE__ */ jsx(
|
|
344
463
|
"button",
|
|
345
464
|
{
|
|
346
|
-
onClick: () =>
|
|
347
|
-
setColor(c);
|
|
348
|
-
if (selectedId) {
|
|
349
|
-
setAnnotations((prev) => prev.map((a) => a.id === selectedId && a.type !== "mosaic" ? { ...a, color: c } : a));
|
|
350
|
-
}
|
|
351
|
-
},
|
|
465
|
+
onClick: () => setColorAndApply(c),
|
|
352
466
|
title: c,
|
|
353
467
|
className: `h-5 w-5 rounded-full border ${color === c ? "ring-2 ring-blue-500 ring-offset-1" : "border-gray-300"}`,
|
|
354
468
|
style: { background: c }
|
|
355
469
|
},
|
|
356
470
|
c
|
|
357
471
|
)) }),
|
|
358
|
-
/* @__PURE__ */
|
|
359
|
-
|
|
360
|
-
/* @__PURE__ */
|
|
361
|
-
|
|
362
|
-
"input",
|
|
363
|
-
{
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
)
|
|
373
|
-
|
|
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
|
+
] })
|
|
374
499
|
] }),
|
|
375
500
|
/* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
|
|
376
|
-
/* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.max(ZOOM_MIN, Math.round((z - ZOOM_STEP) * 100) / 100)), className: "px-2 py-1
|
|
377
|
-
/* @__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: [
|
|
378
503
|
Math.round(zoom * 100),
|
|
379
504
|
"%"
|
|
380
505
|
] }),
|
|
381
|
-
/* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.min(ZOOM_MAX, Math.round((z + ZOOM_STEP) * 100) / 100)), className: "px-2 py-1
|
|
382
|
-
/* @__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" }),
|
|
383
508
|
/* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
|
|
384
|
-
/* @__PURE__ */ jsx("button", { onClick: undoLast, disabled: annotations.length === 0, className: "px-2 py-1
|
|
385
|
-
/* @__PURE__ */ jsx("
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
/* @__PURE__ */ jsx("button", { onClick: applyCrop, className: "px-2 py-1 text-xs rounded bg-blue-500 text-white hover:bg-blue-600", children: "Apply Crop" }),
|
|
390
|
-
/* @__PURE__ */ jsx("button", { onClick: cancelCrop, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Cancel" })
|
|
391
|
-
] }),
|
|
392
|
-
/* @__PURE__ */ jsx("button", { onClick: onClose, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Exit" })
|
|
393
|
-
] })
|
|
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
|
+
] }) })
|
|
394
514
|
] }),
|
|
395
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(
|
|
396
516
|
"div",
|
|
@@ -398,13 +518,7 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
398
518
|
className: "relative shadow-lg rounded overflow-hidden bg-white shrink-0",
|
|
399
519
|
style: { width: displaySize.w, height: displaySize.h },
|
|
400
520
|
children: [
|
|
401
|
-
/* @__PURE__ */ jsx(
|
|
402
|
-
"canvas",
|
|
403
|
-
{
|
|
404
|
-
ref: canvasRef,
|
|
405
|
-
style: { position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }
|
|
406
|
-
}
|
|
407
|
-
),
|
|
521
|
+
/* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: { position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" } }),
|
|
408
522
|
/* @__PURE__ */ jsxs(
|
|
409
523
|
"svg",
|
|
410
524
|
{
|
|
@@ -443,6 +557,11 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
443
557
|
{
|
|
444
558
|
pendingText,
|
|
445
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,
|
|
446
565
|
scale,
|
|
447
566
|
onChange: (value) => setPendingText({ ...pendingText, value }),
|
|
448
567
|
onCommit: commitText,
|
|
@@ -452,16 +571,16 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
452
571
|
]
|
|
453
572
|
}
|
|
454
573
|
) }),
|
|
455
|
-
/* @__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." })
|
|
456
575
|
] });
|
|
457
|
-
}
|
|
576
|
+
});
|
|
577
|
+
var ImageAnnotator_default = ImageAnnotator;
|
|
458
578
|
function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDoubleClick, onHandlePointerDown }) {
|
|
459
579
|
const dim = boundingBox(anno);
|
|
460
580
|
const interactive = onPointerDown ? { onPointerDown, pointerEvents: "all", style: { cursor: "move" } } : {};
|
|
461
581
|
const dblc = onDoubleClick ? { onDoubleClick } : {};
|
|
462
582
|
let body;
|
|
463
583
|
if (anno.type === "rect") {
|
|
464
|
-
const radius = Math.min(anno.w, anno.h, 16) * 0.4;
|
|
465
584
|
body = /* @__PURE__ */ jsx(
|
|
466
585
|
"rect",
|
|
467
586
|
{
|
|
@@ -469,8 +588,8 @@ function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDo
|
|
|
469
588
|
y: anno.y,
|
|
470
589
|
width: anno.w,
|
|
471
590
|
height: anno.h,
|
|
472
|
-
rx: radius,
|
|
473
|
-
ry: radius,
|
|
591
|
+
rx: anno.radius,
|
|
592
|
+
ry: anno.radius,
|
|
474
593
|
fill: "none",
|
|
475
594
|
stroke: anno.color,
|
|
476
595
|
strokeWidth: anno.stroke,
|
|
@@ -495,6 +614,19 @@ function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDo
|
|
|
495
614
|
);
|
|
496
615
|
} else if (anno.type === "arrow") {
|
|
497
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
|
+
);
|
|
498
630
|
} else if (anno.type === "mosaic") {
|
|
499
631
|
body = /* @__PURE__ */ jsx(
|
|
500
632
|
"rect",
|
|
@@ -511,6 +643,7 @@ function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDo
|
|
|
511
643
|
}
|
|
512
644
|
);
|
|
513
645
|
} else {
|
|
646
|
+
const font = FONTS.find((f) => f.id === anno.font)?.css ?? FONTS[0].css;
|
|
514
647
|
body = /* @__PURE__ */ jsx(
|
|
515
648
|
"text",
|
|
516
649
|
{
|
|
@@ -518,8 +651,10 @@ function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDo
|
|
|
518
651
|
y: anno.y + anno.size,
|
|
519
652
|
fill: anno.color,
|
|
520
653
|
fontSize: anno.size,
|
|
521
|
-
fontWeight:
|
|
522
|
-
|
|
654
|
+
fontWeight: anno.bold ? 700 : 400,
|
|
655
|
+
fontStyle: anno.italic ? "italic" : "normal",
|
|
656
|
+
textDecoration: anno.underline ? "underline" : void 0,
|
|
657
|
+
fontFamily: font,
|
|
523
658
|
style: { userSelect: "none" },
|
|
524
659
|
...interactive,
|
|
525
660
|
...dblc,
|
|
@@ -561,31 +696,81 @@ function ResizeHandles({
|
|
|
561
696
|
stroke: "#3b82f6",
|
|
562
697
|
strokeWidth: sw,
|
|
563
698
|
pointerEvents: "all",
|
|
564
|
-
style: { cursor }
|
|
699
|
+
style: { cursor },
|
|
700
|
+
"data-chrome": "handle"
|
|
565
701
|
});
|
|
566
702
|
if (anno.type === "arrow") {
|
|
567
703
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
568
|
-
/* @__PURE__ */ jsx("circle", { cx: anno.x1, cy: anno.y1, r, ...handleProps("grab"),
|
|
569
|
-
/* @__PURE__ */ jsx("circle", { cx: anno.x2, cy: anno.y2, r, ...handleProps("grab"),
|
|
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") })
|
|
570
706
|
] });
|
|
571
707
|
}
|
|
572
|
-
if (anno.type === "text") {
|
|
708
|
+
if (anno.type === "text" || anno.type === "draw") {
|
|
573
709
|
return null;
|
|
574
710
|
}
|
|
575
|
-
const x = anno.x;
|
|
576
|
-
const y = anno.y;
|
|
577
|
-
const w = anno.w;
|
|
578
|
-
const h = anno.h;
|
|
711
|
+
const x = anno.x, y = anno.y, w = anno.w, h = anno.h;
|
|
579
712
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
580
|
-
/* @__PURE__ */ jsx("circle", { cx: x, cy: y, r, ...handleProps("nwse-resize"),
|
|
581
|
-
/* @__PURE__ */ jsx("circle", { cx: x + w, cy: y, r, ...handleProps("nesw-resize"),
|
|
582
|
-
/* @__PURE__ */ jsx("circle", { cx: x + w, cy: y + h, r, ...handleProps("nwse-resize"),
|
|
583
|
-
/* @__PURE__ */ jsx("circle", { cx: x, cy: y + h, r, ...handleProps("nesw-resize"),
|
|
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") })
|
|
717
|
+
] });
|
|
718
|
+
}
|
|
719
|
+
function ArrowShape({ anno, interactive }) {
|
|
720
|
+
const headLen = Math.max(12, anno.stroke * 4);
|
|
721
|
+
const angle = Math.atan2(anno.y2 - anno.y1, anno.x2 - anno.x1);
|
|
722
|
+
const a1 = angle + Math.PI - Math.PI / 7;
|
|
723
|
+
const a2 = angle + Math.PI + Math.PI / 7;
|
|
724
|
+
const head = `M${anno.x2},${anno.y2} L${anno.x2 + headLen * Math.cos(a1)},${anno.y2 + headLen * Math.sin(a1)} L${anno.x2 + headLen * Math.cos(a2)},${anno.y2 + headLen * Math.sin(a2)} Z`;
|
|
725
|
+
return /* @__PURE__ */ jsxs("g", { ...interactive, children: [
|
|
726
|
+
/* @__PURE__ */ jsx(
|
|
727
|
+
"line",
|
|
728
|
+
{
|
|
729
|
+
x1: anno.x1,
|
|
730
|
+
y1: anno.y1,
|
|
731
|
+
x2: anno.x2,
|
|
732
|
+
y2: anno.y2,
|
|
733
|
+
stroke: anno.color,
|
|
734
|
+
strokeWidth: anno.stroke,
|
|
735
|
+
strokeLinecap: "round"
|
|
736
|
+
}
|
|
737
|
+
),
|
|
738
|
+
/* @__PURE__ */ jsx("path", { d: head, fill: anno.color })
|
|
739
|
+
] });
|
|
740
|
+
}
|
|
741
|
+
function CropOverlay({ rect, imageSize }) {
|
|
742
|
+
return /* @__PURE__ */ jsxs("g", { pointerEvents: "none", children: [
|
|
743
|
+
/* @__PURE__ */ jsx(
|
|
744
|
+
"path",
|
|
745
|
+
{
|
|
746
|
+
d: `M0,0 H${imageSize.w} V${imageSize.h} H0 Z M${rect.x},${rect.y} V${rect.y + rect.h} H${rect.x + rect.w} V${rect.y} Z`,
|
|
747
|
+
fill: "rgba(0,0,0,0.45)",
|
|
748
|
+
fillRule: "evenodd"
|
|
749
|
+
}
|
|
750
|
+
),
|
|
751
|
+
/* @__PURE__ */ jsx(
|
|
752
|
+
"rect",
|
|
753
|
+
{
|
|
754
|
+
x: rect.x,
|
|
755
|
+
y: rect.y,
|
|
756
|
+
width: rect.w,
|
|
757
|
+
height: rect.h,
|
|
758
|
+
fill: "none",
|
|
759
|
+
stroke: "#fff",
|
|
760
|
+
strokeWidth: 2,
|
|
761
|
+
strokeDasharray: "8,6"
|
|
762
|
+
}
|
|
763
|
+
)
|
|
584
764
|
] });
|
|
585
765
|
}
|
|
586
766
|
function PendingTextEditor({
|
|
587
767
|
pendingText,
|
|
588
768
|
color,
|
|
769
|
+
size,
|
|
770
|
+
font,
|
|
771
|
+
bold,
|
|
772
|
+
italic,
|
|
773
|
+
underline,
|
|
589
774
|
scale,
|
|
590
775
|
onChange,
|
|
591
776
|
onCommit,
|
|
@@ -628,64 +813,29 @@ function PendingTextEditor({
|
|
|
628
813
|
},
|
|
629
814
|
placeholder: "Type then Enter\u2026",
|
|
630
815
|
rows: 1,
|
|
631
|
-
className: "bg-white/95 border border-blue-400 rounded px-1 py-0.5
|
|
632
|
-
style: {
|
|
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
|
+
}
|
|
633
826
|
}
|
|
634
827
|
)
|
|
635
828
|
}
|
|
636
829
|
);
|
|
637
830
|
}
|
|
638
|
-
function
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
return /* @__PURE__ */ jsxs("g", { ...interactive, children: [
|
|
645
|
-
/* @__PURE__ */ jsx(
|
|
646
|
-
"line",
|
|
647
|
-
{
|
|
648
|
-
x1: anno.x1,
|
|
649
|
-
y1: anno.y1,
|
|
650
|
-
x2: anno.x2,
|
|
651
|
-
y2: anno.y2,
|
|
652
|
-
stroke: anno.color,
|
|
653
|
-
strokeWidth: anno.stroke,
|
|
654
|
-
strokeLinecap: "round"
|
|
655
|
-
}
|
|
656
|
-
),
|
|
657
|
-
/* @__PURE__ */ jsx("path", { d: head, fill: anno.color })
|
|
658
|
-
] });
|
|
659
|
-
}
|
|
660
|
-
function CropOverlay({ rect, imageSize }) {
|
|
661
|
-
return /* @__PURE__ */ jsxs("g", { pointerEvents: "none", children: [
|
|
662
|
-
/* @__PURE__ */ jsx(
|
|
663
|
-
"path",
|
|
664
|
-
{
|
|
665
|
-
d: `M0,0 H${imageSize.w} V${imageSize.h} H0 Z M${rect.x},${rect.y} V${rect.y + rect.h} H${rect.x + rect.w} V${rect.y} Z`,
|
|
666
|
-
fill: "rgba(0,0,0,0.45)",
|
|
667
|
-
fillRule: "evenodd"
|
|
668
|
-
}
|
|
669
|
-
),
|
|
670
|
-
/* @__PURE__ */ jsx(
|
|
671
|
-
"rect",
|
|
672
|
-
{
|
|
673
|
-
x: rect.x,
|
|
674
|
-
y: rect.y,
|
|
675
|
-
width: rect.w,
|
|
676
|
-
height: rect.h,
|
|
677
|
-
fill: "none",
|
|
678
|
-
stroke: "#fff",
|
|
679
|
-
strokeWidth: 2,
|
|
680
|
-
strokeDasharray: "8,6"
|
|
681
|
-
}
|
|
682
|
-
)
|
|
683
|
-
] });
|
|
684
|
-
}
|
|
685
|
-
function makeShape(tool, start, end, color, stroke) {
|
|
686
|
-
if (tool === "rect" || tool === "circle") {
|
|
831
|
+
function makeShape(tool, start, end, color, stroke, rectRadius) {
|
|
832
|
+
if (tool === "rect") {
|
|
833
|
+
const r = normalizeRect(start, end);
|
|
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") {
|
|
687
837
|
const r = normalizeRect(start, end);
|
|
688
|
-
return { id: "", type:
|
|
838
|
+
return { id: "", type: "circle", x: r.x, y: r.y, w: r.w, h: r.h, color, stroke };
|
|
689
839
|
}
|
|
690
840
|
if (tool === "arrow") {
|
|
691
841
|
return { id: "", type: "arrow", x1: start.x, y1: start.y, x2: end.x, y2: end.y, color, stroke };
|
|
@@ -697,21 +847,15 @@ function makeShape(tool, start, end, color, stroke) {
|
|
|
697
847
|
return null;
|
|
698
848
|
}
|
|
699
849
|
function isTrivial(a) {
|
|
700
|
-
if (a.type === "arrow")
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if ("w" in a && "h" in a) {
|
|
704
|
-
return a.w < 4 || a.h < 4;
|
|
705
|
-
}
|
|
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;
|
|
706
853
|
return false;
|
|
707
854
|
}
|
|
708
855
|
function translate(a, dx, dy) {
|
|
709
|
-
if (a.type === "arrow") {
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
if (a.type === "text") {
|
|
713
|
-
return { ...a, x: a.x + dx, y: a.y + dy };
|
|
714
|
-
}
|
|
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 };
|
|
715
859
|
return { ...a, x: a.x + dx, y: a.y + dy };
|
|
716
860
|
}
|
|
717
861
|
function resize(original, corner, p) {
|
|
@@ -720,7 +864,7 @@ function resize(original, corner, p) {
|
|
|
720
864
|
if (corner === "end") return { ...original, x2: p.x, y2: p.y };
|
|
721
865
|
return original;
|
|
722
866
|
}
|
|
723
|
-
if (original.type === "text") return original;
|
|
867
|
+
if (original.type === "text" || original.type === "draw") return original;
|
|
724
868
|
const left = original.x;
|
|
725
869
|
const right = original.x + original.w;
|
|
726
870
|
const top = original.y;
|
|
@@ -751,6 +895,17 @@ function boundingBox(a) {
|
|
|
751
895
|
const y = Math.min(a.y1, a.y2);
|
|
752
896
|
return { x, y, w: Math.abs(a.x2 - a.x1), h: Math.abs(a.y2 - a.y1) };
|
|
753
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
|
+
}
|
|
754
909
|
if (a.type === "text") {
|
|
755
910
|
const lines = a.text.split("\n");
|
|
756
911
|
const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
|
|
@@ -769,6 +924,12 @@ function withinBounds(a, w, h) {
|
|
|
769
924
|
const bb = boundingBox(a);
|
|
770
925
|
return bb.x + bb.w > 0 && bb.y + bb.h > 0 && bb.x < w && bb.y < h;
|
|
771
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
|
+
}
|
|
772
933
|
function applyMosaic(ctx, rect) {
|
|
773
934
|
const x = Math.round(Math.max(0, rect.x));
|
|
774
935
|
const y = Math.round(Math.max(0, rect.y));
|
|
@@ -2188,6 +2349,7 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
|
|
|
2188
2349
|
const [zoom, setZoom] = useState(1);
|
|
2189
2350
|
const [error, setError] = useState(false);
|
|
2190
2351
|
const [annotating, setAnnotating] = useState(false);
|
|
2352
|
+
const annotatorRef = useRef(null);
|
|
2191
2353
|
const handleDefaultDownload = () => {
|
|
2192
2354
|
const a = document.createElement("a");
|
|
2193
2355
|
a.href = url;
|
|
@@ -2195,22 +2357,35 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
|
|
|
2195
2357
|
a.click();
|
|
2196
2358
|
};
|
|
2197
2359
|
const btn = "px-2 py-1 rounded hover:bg-gray-200 transition-colors text-gray-600 flex items-center gap-1";
|
|
2198
|
-
if (annotating) {
|
|
2199
|
-
return /* @__PURE__ */ jsx(ImageAnnotator, { src: url, filename, onClose: () => setAnnotating(false) });
|
|
2200
|
-
}
|
|
2201
2360
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
|
|
2202
2361
|
/* @__PURE__ */ jsxs(PanelActions, { children: [
|
|
2203
|
-
/* @__PURE__ */
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
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
|
+
] })
|
|
2207
2375
|
] }),
|
|
2208
|
-
/* @__PURE__ */
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
"
|
|
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
|
+
] })
|
|
2214
2389
|
] }),
|
|
2215
2390
|
/* @__PURE__ */ jsxs("button", { onClick: onDownload ?? handleDefaultDownload, className: btn, children: [
|
|
2216
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" }) }),
|
|
@@ -2221,7 +2396,7 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
|
|
|
2221
2396
|
"Email"
|
|
2222
2397
|
] })
|
|
2223
2398
|
] }),
|
|
2224
|
-
/* @__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(
|
|
2225
2400
|
"img",
|
|
2226
2401
|
{
|
|
2227
2402
|
src: url,
|
|
@@ -2235,5 +2410,5 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
|
|
|
2235
2410
|
}
|
|
2236
2411
|
|
|
2237
2412
|
export { Preview, setPdfPreview };
|
|
2238
|
-
//# sourceMappingURL=chunk-
|
|
2239
|
-
//# sourceMappingURL=chunk-
|
|
2413
|
+
//# sourceMappingURL=chunk-6IV6OWF3.js.map
|
|
2414
|
+
//# sourceMappingURL=chunk-6IV6OWF3.js.map
|