react-os-shell 2.9.4 → 3.0.0

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.
Files changed (37) hide show
  1. package/dist/{Browser-M5FCUZ3B.js → Browser-3SDJFEZW.js} +4 -4
  2. package/dist/{Browser-M5FCUZ3B.js.map → Browser-3SDJFEZW.js.map} +1 -1
  3. package/dist/{Documents-ZBSGZ6IS.js → Documents-HCHGAFPB.js} +3 -3
  4. package/dist/{Documents-ZBSGZ6IS.js.map → Documents-HCHGAFPB.js.map} +1 -1
  5. package/dist/Files-FYWUNLBY.js +12 -0
  6. package/dist/{Files-RCBB7L6F.js.map → Files-FYWUNLBY.js.map} +1 -1
  7. package/dist/{Notepad-MBYDR7WZ.js → Notepad-WRS7EMCK.js} +3 -3
  8. package/dist/{Notepad-MBYDR7WZ.js.map → Notepad-WRS7EMCK.js.map} +1 -1
  9. package/dist/Preview-HOLQVFSU.js +8 -0
  10. package/dist/{Preview-7AIWQDP7.js.map → Preview-HOLQVFSU.js.map} +1 -1
  11. package/dist/{Spreadsheet-USNVGYAH.js → Spreadsheet-MRUQ4LJR.js} +4 -4
  12. package/dist/{Spreadsheet-USNVGYAH.js.map → Spreadsheet-MRUQ4LJR.js.map} +1 -1
  13. package/dist/apps/index.js +12 -13
  14. package/dist/apps/index.js.map +1 -1
  15. package/dist/{chunk-JZKFHLA6.js → chunk-D7QQTFM3.js} +3 -3
  16. package/dist/{chunk-JZKFHLA6.js.map → chunk-D7QQTFM3.js.map} +1 -1
  17. package/dist/{chunk-XJFI35IK.js → chunk-QS2MEW6R.js} +4 -4
  18. package/dist/{chunk-XJFI35IK.js.map → chunk-QS2MEW6R.js.map} +1 -1
  19. package/dist/{chunk-3HIKRXNT.js → chunk-SKNOGIDC.js} +996 -6
  20. package/dist/chunk-SKNOGIDC.js.map +1 -0
  21. package/dist/{chunk-5ICRXHJ7.js → chunk-X4MB43G2.js} +4 -4
  22. package/dist/{chunk-5ICRXHJ7.js.map → chunk-X4MB43G2.js.map} +1 -1
  23. package/dist/{chunk-AJFUAG54.js → chunk-XRCABX52.js} +3 -3
  24. package/dist/{chunk-AJFUAG54.js.map → chunk-XRCABX52.js.map} +1 -1
  25. package/dist/{chunk-2XERB3XN.js → chunk-XVCEFF6M.js} +3 -3
  26. package/dist/{chunk-2XERB3XN.js.map → chunk-XVCEFF6M.js.map} +1 -1
  27. package/dist/index.d.ts +1 -125
  28. package/dist/index.js +15 -510
  29. package/dist/index.js.map +1 -1
  30. package/package.json +1 -1
  31. package/dist/Files-RCBB7L6F.js +0 -13
  32. package/dist/ImageAnnotator-55NPWJ2R.js +0 -4
  33. package/dist/ImageAnnotator-55NPWJ2R.js.map +0 -1
  34. package/dist/Preview-7AIWQDP7.js +0 -9
  35. package/dist/chunk-3HIKRXNT.js.map +0 -1
  36. package/dist/chunk-NUPYEVU4.js +0 -999
  37. package/dist/chunk-NUPYEVU4.js.map +0 -1
@@ -1,12 +1,1002 @@
1
- import { ImageAnnotator_default } from './chunk-NUPYEVU4.js';
2
1
  import { toast_default } from './chunk-VENYVK3L.js';
3
- import { AboutApp } from './chunk-AJFUAG54.js';
2
+ import { AboutApp } from './chunk-XRCABX52.js';
4
3
  import { WindowTitle, registerModalEscapeInterceptor, getActiveModalId } from './chunk-6OWYD4O6.js';
5
- import { createContext, useRef, useEffect, useState, useContext } from 'react';
4
+ import { forwardRef, useRef, useState, useMemo, useEffect, useImperativeHandle, createContext, useContext } from 'react';
6
5
  import { createPortal } from 'react-dom';
7
6
  import * as pdfjsLib from 'pdfjs-dist';
8
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
8
 
9
+ var COLORS = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6", "#8b5cf6", "#000000", "#ffffff"];
10
+ var FONTS = [
11
+ { id: "system", label: "System", css: "-apple-system, system-ui, sans-serif" },
12
+ { id: "serif", label: "Serif", css: 'Georgia, "Times New Roman", serif' },
13
+ { id: "mono", label: "Mono", css: 'ui-monospace, "SF Mono", Menlo, monospace' },
14
+ { id: "cursive", label: "Cursive", css: '"Brush Script MT", "Comic Sans MS", cursive' }
15
+ ];
16
+ var STROKE_DEFAULT = 4;
17
+ var TEXT_SIZE_DEFAULT = 24;
18
+ var RECT_RADIUS_DEFAULT = 12;
19
+ var MOSAIC_BLOCK = 12;
20
+ var ZOOM_MIN = 0.25;
21
+ var ZOOM_MAX = 4;
22
+ var ZOOM_STEP = 0.25;
23
+ var newId = () => Math.random().toString(36).slice(2, 9);
24
+ var ImageAnnotator = forwardRef(function ImageAnnotator2({ src, filename, onApply, onCancel }, ref) {
25
+ const wrapRef = useRef(null);
26
+ const canvasRef = useRef(null);
27
+ const svgRef = useRef(null);
28
+ const imageRef = useRef(null);
29
+ const [tool, setTool] = useState("select");
30
+ const [color, setColor] = useState(COLORS[0]);
31
+ const [stroke, setStroke] = useState(STROKE_DEFAULT);
32
+ const [textSize, setTextSize] = useState(TEXT_SIZE_DEFAULT);
33
+ const [textFont, setTextFont] = useState(FONTS[0].id);
34
+ const [textBold, setTextBold] = useState(true);
35
+ const [textItalic, setTextItalic] = useState(false);
36
+ const [textUnderline, setTextUnderline] = useState(false);
37
+ const [rectRadius, setRectRadius] = useState(RECT_RADIUS_DEFAULT);
38
+ const [annotations, setAnnotations] = useState([]);
39
+ const [selectedId, setSelectedId] = useState(null);
40
+ const [imageSize, setImageSize] = useState(null);
41
+ const [fitSize, setFitSize] = useState(null);
42
+ const [zoom, setZoom] = useState(1);
43
+ const [preview, setPreview] = useState(null);
44
+ const [pendingText, setPendingText] = useState(null);
45
+ const [pendingCrop, setPendingCrop] = useState(null);
46
+ const dragRef = useRef(null);
47
+ const [isDragging, setIsDragging] = useState(false);
48
+ const displaySize = useMemo(() => {
49
+ if (!fitSize) return null;
50
+ return { w: fitSize.w * zoom, h: fitSize.h * zoom };
51
+ }, [fitSize, zoom]);
52
+ const scale = displaySize && imageSize ? displaySize.w / imageSize.w : 1;
53
+ const selected = useMemo(
54
+ () => annotations.find((a) => a.id === selectedId) ?? null,
55
+ [annotations, selectedId]
56
+ );
57
+ useEffect(() => {
58
+ if (tool === "select" && selected) {
59
+ if ("color" in selected) setColor(selected.color);
60
+ if ("stroke" in selected) setStroke(selected.stroke);
61
+ if (selected.type === "rect") setRectRadius(selected.radius);
62
+ if (selected.type === "text") {
63
+ setTextSize(selected.size);
64
+ setTextFont(selected.font);
65
+ setTextBold(selected.bold);
66
+ setTextItalic(selected.italic);
67
+ setTextUnderline(selected.underline);
68
+ }
69
+ }
70
+ }, [selectedId]);
71
+ useEffect(() => {
72
+ const img = new Image();
73
+ img.crossOrigin = "anonymous";
74
+ img.onload = () => {
75
+ imageRef.current = img;
76
+ setImageSize({ w: img.naturalWidth, h: img.naturalHeight });
77
+ setAnnotations([]);
78
+ setSelectedId(null);
79
+ };
80
+ img.onerror = () => toast_default.error("Failed to load image");
81
+ img.src = src;
82
+ }, [src]);
83
+ useEffect(() => {
84
+ if (!imageSize) return;
85
+ const update = () => {
86
+ const wrap = wrapRef.current;
87
+ if (!wrap) return;
88
+ const r = wrap.getBoundingClientRect();
89
+ const availW = Math.max(0, r.width - 32);
90
+ const availH = Math.max(0, r.height - 32);
91
+ if (availW === 0 || availH === 0) return;
92
+ const ratio = imageSize.w / imageSize.h;
93
+ let w = imageSize.w, h = imageSize.h;
94
+ if (w > availW) {
95
+ w = availW;
96
+ h = w / ratio;
97
+ }
98
+ if (h > availH) {
99
+ h = availH;
100
+ w = h * ratio;
101
+ }
102
+ setFitSize({ w, h });
103
+ };
104
+ update();
105
+ const ro = new ResizeObserver(update);
106
+ if (wrapRef.current) ro.observe(wrapRef.current);
107
+ return () => ro.disconnect();
108
+ }, [imageSize]);
109
+ const mosaicAnnos = useMemo(
110
+ () => annotations.filter((a) => a.type === "mosaic"),
111
+ [annotations]
112
+ );
113
+ useEffect(() => {
114
+ const img = imageRef.current;
115
+ const c = canvasRef.current;
116
+ if (!img || !c || !imageSize) return;
117
+ c.width = imageSize.w;
118
+ c.height = imageSize.h;
119
+ const ctx = c.getContext("2d");
120
+ ctx.drawImage(img, 0, 0);
121
+ for (const m of mosaicAnnos) applyMosaic(ctx, m);
122
+ }, [imageSize, mosaicAnnos, fitSize]);
123
+ const evToImage = (e) => {
124
+ const svg = svgRef.current;
125
+ if (!svg) return { x: 0, y: 0 };
126
+ const pt = svg.createSVGPoint();
127
+ pt.x = e.clientX;
128
+ pt.y = e.clientY;
129
+ const ctm = svg.getScreenCTM();
130
+ if (!ctm) return { x: 0, y: 0 };
131
+ const t = pt.matrixTransform(ctm.inverse());
132
+ return { x: t.x, y: t.y };
133
+ };
134
+ const beginDrag = (drag) => {
135
+ dragRef.current = drag;
136
+ setIsDragging(true);
137
+ };
138
+ useEffect(() => {
139
+ if (!isDragging) return;
140
+ const onMove = (ev) => {
141
+ const drag = dragRef.current;
142
+ if (!drag) return;
143
+ const p = evToImage(ev);
144
+ if (drag.kind === "draw") {
145
+ setPreview(makeShape(tool, drag.start, p, color, stroke, rectRadius));
146
+ } else if (drag.kind === "pen") {
147
+ const last = drag.points[drag.points.length - 1];
148
+ if (Math.abs(p.x - last.x) > 1 || Math.abs(p.y - last.y) > 1) {
149
+ drag.points.push(p);
150
+ setPreview({ id: "", type: "draw", points: drag.points.slice(), color, stroke });
151
+ }
152
+ } else if (drag.kind === "crop") {
153
+ setPendingCrop(normalizeRect(drag.start, p));
154
+ } else if (drag.kind === "move") {
155
+ const dx = p.x - drag.start.x;
156
+ const dy = p.y - drag.start.y;
157
+ setAnnotations((prev) => prev.map((a) => a.id === drag.id ? translate(drag.original, dx, dy) : a));
158
+ } else if (drag.kind === "resize") {
159
+ setAnnotations((prev) => prev.map((a) => a.id === drag.id ? resize(drag.original, drag.corner, p) : a));
160
+ }
161
+ };
162
+ const onUp = () => {
163
+ const drag = dragRef.current;
164
+ dragRef.current = null;
165
+ setIsDragging(false);
166
+ if (drag?.kind === "draw") {
167
+ setPreview((p) => {
168
+ if (!p || isTrivial(p)) return null;
169
+ const anno = { ...p, id: newId() };
170
+ setAnnotations((prev) => [...prev, anno]);
171
+ setSelectedId(anno.id);
172
+ setTool("select");
173
+ return null;
174
+ });
175
+ } else if (drag?.kind === "pen") {
176
+ if (drag.points.length < 2) {
177
+ setPreview(null);
178
+ return;
179
+ }
180
+ const anno = { id: newId(), type: "draw", points: drag.points, color, stroke };
181
+ setAnnotations((prev) => [...prev, anno]);
182
+ setSelectedId(anno.id);
183
+ setTool("select");
184
+ setPreview(null);
185
+ }
186
+ };
187
+ window.addEventListener("pointermove", onMove);
188
+ window.addEventListener("pointerup", onUp);
189
+ window.addEventListener("pointercancel", onUp);
190
+ return () => {
191
+ window.removeEventListener("pointermove", onMove);
192
+ window.removeEventListener("pointerup", onUp);
193
+ window.removeEventListener("pointercancel", onUp);
194
+ };
195
+ }, [isDragging, tool, color, stroke, rectRadius]);
196
+ const handleAnnoPointerDown = (e, anno) => {
197
+ if (tool !== "select") return;
198
+ e.stopPropagation();
199
+ setSelectedId(anno.id);
200
+ beginDrag({ kind: "move", id: anno.id, start: evToImage(e), original: anno });
201
+ };
202
+ const handleHandlePointerDown = (e, anno, corner) => {
203
+ e.stopPropagation();
204
+ beginDrag({ kind: "resize", id: anno.id, corner, start: evToImage(e), original: anno });
205
+ };
206
+ const handleSvgPointerDown = (e) => {
207
+ if (tool === "select") {
208
+ setSelectedId(null);
209
+ return;
210
+ }
211
+ if (tool === "text") {
212
+ const p = evToImage(e);
213
+ setPendingText({ x: p.x, y: p.y, value: "" });
214
+ return;
215
+ }
216
+ const start = evToImage(e);
217
+ if (tool === "crop") {
218
+ beginDrag({ kind: "crop", start });
219
+ return;
220
+ }
221
+ if (tool === "draw") {
222
+ beginDrag({ kind: "pen", points: [start] });
223
+ setPreview({ id: "", type: "draw", points: [start], color, stroke });
224
+ return;
225
+ }
226
+ setPreview(makeShape(tool, start, start, color, stroke, rectRadius));
227
+ beginDrag({ kind: "draw", start });
228
+ };
229
+ const handleAnnoDoubleClick = (anno) => {
230
+ if (anno.type !== "text") return;
231
+ setPendingText({ x: anno.x, y: anno.y, value: anno.text, editingId: anno.id });
232
+ };
233
+ useEffect(() => {
234
+ const onKey = (ev) => {
235
+ if (document.activeElement && /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName)) return;
236
+ if ((ev.metaKey || ev.ctrlKey) && (ev.key === "z" || ev.key === "Z") && !ev.shiftKey) {
237
+ ev.preventDefault();
238
+ setAnnotations((prev) => prev.slice(0, -1));
239
+ setSelectedId(null);
240
+ return;
241
+ }
242
+ if (!selectedId) return;
243
+ if (ev.key === "Delete" || ev.key === "Backspace") {
244
+ ev.preventDefault();
245
+ setAnnotations((prev) => prev.filter((a) => a.id !== selectedId));
246
+ setSelectedId(null);
247
+ } else if (ev.key === "Escape") {
248
+ setSelectedId(null);
249
+ }
250
+ };
251
+ window.addEventListener("keydown", onKey);
252
+ return () => window.removeEventListener("keydown", onKey);
253
+ }, [selectedId]);
254
+ const setColorAndApply = (c) => {
255
+ setColor(c);
256
+ if (!selectedId) return;
257
+ setAnnotations((prev) => prev.map((a) => {
258
+ if (a.id !== selectedId) return a;
259
+ if (a.type === "mosaic") return a;
260
+ return { ...a, color: c };
261
+ }));
262
+ };
263
+ const setStrokeAndApply = (s) => {
264
+ setStroke(s);
265
+ if (!selectedId) return;
266
+ setAnnotations((prev) => prev.map((a) => {
267
+ if (a.id !== selectedId) return a;
268
+ if (a.type === "rect" || a.type === "circle" || a.type === "arrow" || a.type === "draw") {
269
+ return { ...a, stroke: s };
270
+ }
271
+ return a;
272
+ }));
273
+ };
274
+ const setRectRadiusAndApply = (r) => {
275
+ setRectRadius(r);
276
+ if (!selectedId) return;
277
+ setAnnotations((prev) => prev.map(
278
+ (a) => a.id === selectedId && a.type === "rect" ? { ...a, radius: r } : a
279
+ ));
280
+ };
281
+ const setTextSizeAndApply = (s) => {
282
+ setTextSize(s);
283
+ if (!selectedId) return;
284
+ setAnnotations((prev) => prev.map(
285
+ (a) => a.id === selectedId && a.type === "text" ? { ...a, size: s } : a
286
+ ));
287
+ };
288
+ const setTextFontAndApply = (f) => {
289
+ setTextFont(f);
290
+ if (!selectedId) return;
291
+ setAnnotations((prev) => prev.map(
292
+ (a) => a.id === selectedId && a.type === "text" ? { ...a, font: f } : a
293
+ ));
294
+ };
295
+ const toggleTextStyleAndApply = (which) => {
296
+ const next = !{ bold: textBold, italic: textItalic, underline: textUnderline }[which];
297
+ if (which === "bold") setTextBold(next);
298
+ if (which === "italic") setTextItalic(next);
299
+ if (which === "underline") setTextUnderline(next);
300
+ if (!selectedId) return;
301
+ setAnnotations((prev) => prev.map(
302
+ (a) => a.id === selectedId && a.type === "text" ? { ...a, [which]: next } : a
303
+ ));
304
+ };
305
+ const commitText = () => {
306
+ if (!pendingText) return;
307
+ const value = pendingText.value;
308
+ if (!value.trim()) {
309
+ if (pendingText.editingId) {
310
+ setAnnotations((prev) => prev.filter((a) => a.id !== pendingText.editingId));
311
+ }
312
+ setPendingText(null);
313
+ return;
314
+ }
315
+ if (pendingText.editingId) {
316
+ setAnnotations((prev) => prev.map(
317
+ (a) => a.id === pendingText.editingId && a.type === "text" ? { ...a, text: value } : a
318
+ ));
319
+ } else {
320
+ const anno = {
321
+ id: newId(),
322
+ type: "text",
323
+ x: pendingText.x,
324
+ y: pendingText.y,
325
+ text: value,
326
+ color,
327
+ size: textSize,
328
+ font: textFont,
329
+ bold: textBold,
330
+ italic: textItalic,
331
+ underline: textUnderline
332
+ };
333
+ setAnnotations((prev) => [...prev, anno]);
334
+ setSelectedId(anno.id);
335
+ setTool("select");
336
+ }
337
+ setPendingText(null);
338
+ };
339
+ const applyCrop = () => {
340
+ if (!pendingCrop || !imageRef.current || !imageSize) return;
341
+ const r = pendingCrop;
342
+ const tmp = document.createElement("canvas");
343
+ tmp.width = Math.round(r.w);
344
+ tmp.height = Math.round(r.h);
345
+ const tctx = tmp.getContext("2d");
346
+ const sourceCanvas = document.createElement("canvas");
347
+ sourceCanvas.width = imageSize.w;
348
+ sourceCanvas.height = imageSize.h;
349
+ const sctx = sourceCanvas.getContext("2d");
350
+ sctx.drawImage(imageRef.current, 0, 0);
351
+ for (const m of mosaicAnnos) applyMosaic(sctx, m);
352
+ tctx.drawImage(sourceCanvas, r.x, r.y, r.w, r.h, 0, 0, r.w, r.h);
353
+ const newImg = new Image();
354
+ newImg.onload = () => {
355
+ imageRef.current = newImg;
356
+ setImageSize({ w: newImg.naturalWidth, h: newImg.naturalHeight });
357
+ setAnnotations(
358
+ (prev) => prev.filter((a) => a.type !== "mosaic").map((a) => translate(a, -r.x, -r.y)).filter((a) => withinBounds(a, newImg.naturalWidth, newImg.naturalHeight))
359
+ );
360
+ setPendingCrop(null);
361
+ };
362
+ newImg.src = tmp.toDataURL("image/png");
363
+ };
364
+ const cancelCrop = () => setPendingCrop(null);
365
+ const undoLast = () => {
366
+ setAnnotations((prev) => prev.slice(0, -1));
367
+ setSelectedId(null);
368
+ };
369
+ const compositeToCanvas = async () => {
370
+ const c = canvasRef.current;
371
+ const svg = svgRef.current;
372
+ if (!c || !svg || !imageSize) return null;
373
+ const out = document.createElement("canvas");
374
+ out.width = imageSize.w;
375
+ out.height = imageSize.h;
376
+ const octx = out.getContext("2d");
377
+ octx.drawImage(c, 0, 0);
378
+ const clone = svg.cloneNode(true);
379
+ clone.querySelectorAll("[data-chrome]").forEach((n) => n.remove());
380
+ const xml = new XMLSerializer().serializeToString(clone);
381
+ const svgBlob = new Blob([xml], { type: "image/svg+xml;charset=utf-8" });
382
+ const svgUrl = URL.createObjectURL(svgBlob);
383
+ const svgImg = new Image();
384
+ await new Promise((resolve) => {
385
+ svgImg.onload = () => resolve();
386
+ svgImg.onerror = () => resolve();
387
+ svgImg.src = svgUrl;
388
+ });
389
+ octx.drawImage(svgImg, 0, 0, imageSize.w, imageSize.h);
390
+ URL.revokeObjectURL(svgUrl);
391
+ return out;
392
+ };
393
+ useImperativeHandle(ref, () => ({
394
+ save: async () => {
395
+ const out = await compositeToCanvas();
396
+ if (!out) return;
397
+ out.toBlob((blob) => {
398
+ if (!blob) {
399
+ toast_default.error("Failed to export");
400
+ return;
401
+ }
402
+ const url = URL.createObjectURL(blob);
403
+ const a = document.createElement("a");
404
+ a.href = url;
405
+ const base = filename.replace(/\.[^.]+$/, "");
406
+ a.download = `${base}-annotated.png`;
407
+ a.click();
408
+ URL.revokeObjectURL(url);
409
+ }, "image/png");
410
+ },
411
+ copy: async () => {
412
+ if (!("clipboard" in navigator) || typeof ClipboardItem === "undefined") {
413
+ toast_default.error("Clipboard images not supported in this browser");
414
+ return;
415
+ }
416
+ const out = await compositeToCanvas();
417
+ if (!out) return;
418
+ out.toBlob(async (blob) => {
419
+ if (!blob) {
420
+ toast_default.error("Failed to copy");
421
+ return;
422
+ }
423
+ try {
424
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
425
+ toast_default.success("Copied to clipboard");
426
+ } catch {
427
+ toast_default.error("Copy failed (clipboard permission?)");
428
+ }
429
+ }, "image/png");
430
+ }
431
+ }), [imageSize, filename]);
432
+ const tools = useMemo(() => [
433
+ { 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" }) }) },
434
+ { 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" }) }) },
435
+ { 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" }) }) },
436
+ { 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" }) }) },
437
+ { 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" }) }) },
438
+ { id: "mosaic", label: "Mosaic", 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: "M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z" }) }) },
439
+ { id: "text", label: "Text", 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: "M4 6h16M12 6v14M9 20h6" }) }) },
440
+ { 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" }) }) }
441
+ ], []);
442
+ const btnClass = (active) => `p-1.5 rounded transition-colors ${active ? "bg-blue-500 text-white" : "text-gray-700 hover:bg-gray-200"}`;
443
+ const ctxType = selected?.type ?? (tool === "select" ? null : tool);
444
+ const showStrokeControl = ctxType === "rect" || ctxType === "circle" || ctxType === "arrow" || ctxType === "draw";
445
+ const showRectRadius = ctxType === "rect";
446
+ const showTextControls = ctxType === "text";
447
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full bg-gray-100", children: [
448
+ /* @__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: [
449
+ tools.map((t) => /* @__PURE__ */ jsx(
450
+ "button",
451
+ {
452
+ onClick: () => {
453
+ setTool(t.id);
454
+ setSelectedId(null);
455
+ },
456
+ title: t.label,
457
+ className: btnClass(tool === t.id),
458
+ children: t.icon
459
+ },
460
+ t.id
461
+ )),
462
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
463
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: COLORS.map((c) => /* @__PURE__ */ jsx(
464
+ "button",
465
+ {
466
+ onClick: () => setColorAndApply(c),
467
+ title: c,
468
+ className: `h-5 w-5 rounded-full border ${color === c ? "ring-2 ring-blue-500 ring-offset-1" : "border-gray-300"}`,
469
+ style: { background: c }
470
+ },
471
+ c
472
+ )) }),
473
+ showStrokeControl && /* @__PURE__ */ jsxs(Fragment, { children: [
474
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
475
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
476
+ /* @__PURE__ */ jsx("span", { children: "Weight" }),
477
+ /* @__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" }),
478
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums w-5 text-right", children: stroke })
479
+ ] })
480
+ ] }),
481
+ showRectRadius && /* @__PURE__ */ jsxs(Fragment, { children: [
482
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
483
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
484
+ /* @__PURE__ */ jsx("span", { children: "Radius" }),
485
+ /* @__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" }),
486
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums w-6 text-right", children: rectRadius })
487
+ ] })
488
+ ] }),
489
+ showTextControls && /* @__PURE__ */ jsxs(Fragment, { children: [
490
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
491
+ /* @__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)) }),
492
+ /* @__PURE__ */ jsx("button", { onClick: () => toggleTextStyleAndApply("bold"), className: btnClass(textBold), title: "Bold", children: /* @__PURE__ */ jsx("span", { className: "font-bold", children: "B" }) }),
493
+ /* @__PURE__ */ jsx("button", { onClick: () => toggleTextStyleAndApply("italic"), className: btnClass(textItalic), title: "Italic", children: /* @__PURE__ */ jsx("span", { className: "italic", children: "I" }) }),
494
+ /* @__PURE__ */ jsx("button", { onClick: () => toggleTextStyleAndApply("underline"), className: btnClass(textUnderline), title: "Underline", children: /* @__PURE__ */ jsx("span", { className: "underline", children: "U" }) }),
495
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
496
+ /* @__PURE__ */ jsx("span", { children: "Size" }),
497
+ /* @__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" }),
498
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums w-7 text-right", children: textSize })
499
+ ] })
500
+ ] }),
501
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
502
+ /* @__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" }),
503
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-600 tabular-nums w-10 text-center", children: [
504
+ Math.round(zoom * 100),
505
+ "%"
506
+ ] }),
507
+ /* @__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: "+" }),
508
+ /* @__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" }),
509
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
510
+ /* @__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" }),
511
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
512
+ pendingCrop && /* @__PURE__ */ jsxs(Fragment, { children: [
513
+ /* @__PURE__ */ jsx("button", { onClick: applyCrop, className: "px-2 py-1 rounded bg-blue-500 text-white hover:bg-blue-600", children: "Apply Crop" }),
514
+ /* @__PURE__ */ jsx("button", { onClick: cancelCrop, className: "px-2 py-1 rounded hover:bg-gray-200 text-gray-700", children: "Cancel" })
515
+ ] }),
516
+ onApply && !pendingCrop && /* @__PURE__ */ jsxs(Fragment, { children: [
517
+ onCancel && /* @__PURE__ */ jsx("button", { onClick: onCancel, className: "px-3 py-1 rounded hover:bg-gray-200 text-gray-700 text-xs font-medium", children: "Cancel" }),
518
+ /* @__PURE__ */ jsx(
519
+ "button",
520
+ {
521
+ onClick: async () => {
522
+ const out = await compositeToCanvas();
523
+ if (!out) {
524
+ toast_default.error("Failed to export");
525
+ return;
526
+ }
527
+ out.toBlob((blob) => {
528
+ if (!blob) {
529
+ toast_default.error("Failed to export");
530
+ return;
531
+ }
532
+ onApply(blob);
533
+ }, "image/png");
534
+ },
535
+ className: "px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 text-xs font-medium",
536
+ children: "Apply"
537
+ }
538
+ )
539
+ ] })
540
+ ] })
541
+ ] }),
542
+ /* @__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(
543
+ "div",
544
+ {
545
+ className: "relative shadow-lg rounded overflow-hidden bg-white shrink-0",
546
+ style: { width: displaySize.w, height: displaySize.h },
547
+ children: [
548
+ /* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: { position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" } }),
549
+ /* @__PURE__ */ jsxs(
550
+ "svg",
551
+ {
552
+ ref: svgRef,
553
+ viewBox: `0 0 ${imageSize.w} ${imageSize.h}`,
554
+ preserveAspectRatio: "none",
555
+ style: {
556
+ position: "absolute",
557
+ inset: 0,
558
+ width: "100%",
559
+ height: "100%",
560
+ touchAction: "none",
561
+ cursor: tool === "select" ? "default" : tool === "text" ? "text" : "crosshair"
562
+ },
563
+ onPointerDown: handleSvgPointerDown,
564
+ children: [
565
+ annotations.map((a) => /* @__PURE__ */ jsx(
566
+ AnnotationView,
567
+ {
568
+ anno: a,
569
+ selected: selectedId === a.id,
570
+ zoom,
571
+ onPointerDown: (e) => handleAnnoPointerDown(e, a),
572
+ onDoubleClick: () => handleAnnoDoubleClick(a),
573
+ onHandlePointerDown: (e, corner) => handleHandlePointerDown(e, a, corner)
574
+ },
575
+ a.id
576
+ )),
577
+ preview && /* @__PURE__ */ jsx(AnnotationView, { anno: { ...preview, id: "__preview" }, preview: true }),
578
+ pendingCrop && /* @__PURE__ */ jsx(CropOverlay, { rect: pendingCrop, imageSize })
579
+ ]
580
+ }
581
+ ),
582
+ pendingText && /* @__PURE__ */ jsx(
583
+ PendingTextEditor,
584
+ {
585
+ pendingText,
586
+ color,
587
+ size: textSize,
588
+ font: FONTS.find((f) => f.id === textFont)?.css ?? FONTS[0].css,
589
+ bold: textBold,
590
+ italic: textItalic,
591
+ underline: textUnderline,
592
+ scale,
593
+ onChange: (value) => setPendingText({ ...pendingText, value }),
594
+ onCommit: commitText,
595
+ onCancel: () => setPendingText(null)
596
+ }
597
+ )
598
+ ]
599
+ }
600
+ ) }),
601
+ /* @__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." })
602
+ ] });
603
+ });
604
+ var ImageAnnotator_default = ImageAnnotator;
605
+ function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDoubleClick, onHandlePointerDown }) {
606
+ const dim = boundingBox(anno);
607
+ const interactive = onPointerDown ? { onPointerDown, pointerEvents: "all", style: { cursor: "move" } } : {};
608
+ const dblc = onDoubleClick ? { onDoubleClick } : {};
609
+ let body;
610
+ if (anno.type === "rect") {
611
+ body = /* @__PURE__ */ jsx(
612
+ "rect",
613
+ {
614
+ x: anno.x,
615
+ y: anno.y,
616
+ width: anno.w,
617
+ height: anno.h,
618
+ rx: anno.radius,
619
+ ry: anno.radius,
620
+ fill: "none",
621
+ stroke: anno.color,
622
+ strokeWidth: anno.stroke,
623
+ strokeLinecap: "round",
624
+ strokeLinejoin: "round",
625
+ ...interactive
626
+ }
627
+ );
628
+ } else if (anno.type === "circle") {
629
+ body = /* @__PURE__ */ jsx(
630
+ "ellipse",
631
+ {
632
+ cx: anno.x + anno.w / 2,
633
+ cy: anno.y + anno.h / 2,
634
+ rx: anno.w / 2,
635
+ ry: anno.h / 2,
636
+ fill: "none",
637
+ stroke: anno.color,
638
+ strokeWidth: anno.stroke,
639
+ ...interactive
640
+ }
641
+ );
642
+ } else if (anno.type === "arrow") {
643
+ body = /* @__PURE__ */ jsx(ArrowShape, { anno, interactive });
644
+ } else if (anno.type === "draw") {
645
+ body = /* @__PURE__ */ jsx(
646
+ "path",
647
+ {
648
+ d: pointsToPath(anno.points),
649
+ fill: "none",
650
+ stroke: anno.color,
651
+ strokeWidth: anno.stroke,
652
+ strokeLinecap: "round",
653
+ strokeLinejoin: "round",
654
+ ...interactive
655
+ }
656
+ );
657
+ } else if (anno.type === "mosaic") {
658
+ body = /* @__PURE__ */ jsx(
659
+ "rect",
660
+ {
661
+ x: anno.x,
662
+ y: anno.y,
663
+ width: anno.w,
664
+ height: anno.h,
665
+ fill: "rgba(0,0,0,0.001)",
666
+ stroke: selected ? "#3b82f6" : "none",
667
+ strokeWidth: selected ? 2 : 0,
668
+ strokeDasharray: selected ? "6,4" : void 0,
669
+ ...interactive
670
+ }
671
+ );
672
+ } else {
673
+ const font = FONTS.find((f) => f.id === anno.font)?.css ?? FONTS[0].css;
674
+ body = /* @__PURE__ */ jsx(
675
+ "text",
676
+ {
677
+ x: anno.x,
678
+ y: anno.y + anno.size,
679
+ fill: anno.color,
680
+ fontSize: anno.size,
681
+ fontWeight: anno.bold ? 700 : 400,
682
+ fontStyle: anno.italic ? "italic" : "normal",
683
+ textDecoration: anno.underline ? "underline" : void 0,
684
+ fontFamily: font,
685
+ style: { userSelect: "none" },
686
+ ...interactive,
687
+ ...dblc,
688
+ children: anno.text.split("\n").map((line, i) => /* @__PURE__ */ jsx("tspan", { x: anno.x, dy: i === 0 ? 0 : anno.size * 1.2, children: line }, i))
689
+ }
690
+ );
691
+ }
692
+ return /* @__PURE__ */ jsxs("g", { children: [
693
+ body,
694
+ selected && !preview && /* @__PURE__ */ jsxs(Fragment, { children: [
695
+ /* @__PURE__ */ jsx(
696
+ "rect",
697
+ {
698
+ "data-chrome": "selection",
699
+ x: dim.x - 4,
700
+ y: dim.y - 4,
701
+ width: dim.w + 8,
702
+ height: dim.h + 8,
703
+ fill: "none",
704
+ stroke: "#3b82f6",
705
+ strokeWidth: 2 / zoom,
706
+ strokeDasharray: `${6 / zoom},${4 / zoom}`,
707
+ pointerEvents: "none"
708
+ }
709
+ ),
710
+ onHandlePointerDown && /* @__PURE__ */ jsx(ResizeHandles, { anno, zoom, onHandlePointerDown })
711
+ ] })
712
+ ] });
713
+ }
714
+ function ResizeHandles({
715
+ anno,
716
+ zoom,
717
+ onHandlePointerDown
718
+ }) {
719
+ const r = 6 / zoom;
720
+ const sw = 1.5 / zoom;
721
+ const handleProps = (cursor) => ({
722
+ fill: "#fff",
723
+ stroke: "#3b82f6",
724
+ strokeWidth: sw,
725
+ pointerEvents: "all",
726
+ style: { cursor },
727
+ "data-chrome": "handle"
728
+ });
729
+ if (anno.type === "arrow") {
730
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
731
+ /* @__PURE__ */ jsx("circle", { cx: anno.x1, cy: anno.y1, r, ...handleProps("grab"), onPointerDown: (e) => onHandlePointerDown(e, "start") }),
732
+ /* @__PURE__ */ jsx("circle", { cx: anno.x2, cy: anno.y2, r, ...handleProps("grab"), onPointerDown: (e) => onHandlePointerDown(e, "end") })
733
+ ] });
734
+ }
735
+ if (anno.type === "text" || anno.type === "draw") {
736
+ return null;
737
+ }
738
+ const x = anno.x, y = anno.y, w = anno.w, h = anno.h;
739
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
740
+ /* @__PURE__ */ jsx("circle", { cx: x, cy: y, r, ...handleProps("nwse-resize"), onPointerDown: (e) => onHandlePointerDown(e, "nw") }),
741
+ /* @__PURE__ */ jsx("circle", { cx: x + w, cy: y, r, ...handleProps("nesw-resize"), onPointerDown: (e) => onHandlePointerDown(e, "ne") }),
742
+ /* @__PURE__ */ jsx("circle", { cx: x + w, cy: y + h, r, ...handleProps("nwse-resize"), onPointerDown: (e) => onHandlePointerDown(e, "se") }),
743
+ /* @__PURE__ */ jsx("circle", { cx: x, cy: y + h, r, ...handleProps("nesw-resize"), onPointerDown: (e) => onHandlePointerDown(e, "sw") })
744
+ ] });
745
+ }
746
+ function ArrowShape({ anno, interactive }) {
747
+ const headLen = Math.max(12, anno.stroke * 4);
748
+ const angle = Math.atan2(anno.y2 - anno.y1, anno.x2 - anno.x1);
749
+ const a1 = angle + Math.PI - Math.PI / 7;
750
+ const a2 = angle + Math.PI + Math.PI / 7;
751
+ 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`;
752
+ return /* @__PURE__ */ jsxs("g", { ...interactive, children: [
753
+ /* @__PURE__ */ jsx(
754
+ "line",
755
+ {
756
+ x1: anno.x1,
757
+ y1: anno.y1,
758
+ x2: anno.x2,
759
+ y2: anno.y2,
760
+ stroke: anno.color,
761
+ strokeWidth: anno.stroke,
762
+ strokeLinecap: "round"
763
+ }
764
+ ),
765
+ /* @__PURE__ */ jsx("path", { d: head, fill: anno.color })
766
+ ] });
767
+ }
768
+ function CropOverlay({ rect, imageSize }) {
769
+ return /* @__PURE__ */ jsxs("g", { pointerEvents: "none", children: [
770
+ /* @__PURE__ */ jsx(
771
+ "path",
772
+ {
773
+ 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`,
774
+ fill: "rgba(0,0,0,0.45)",
775
+ fillRule: "evenodd"
776
+ }
777
+ ),
778
+ /* @__PURE__ */ jsx(
779
+ "rect",
780
+ {
781
+ x: rect.x,
782
+ y: rect.y,
783
+ width: rect.w,
784
+ height: rect.h,
785
+ fill: "none",
786
+ stroke: "#fff",
787
+ strokeWidth: 2,
788
+ strokeDasharray: "8,6"
789
+ }
790
+ )
791
+ ] });
792
+ }
793
+ function PendingTextEditor({
794
+ pendingText,
795
+ color,
796
+ size,
797
+ font,
798
+ bold,
799
+ italic,
800
+ underline,
801
+ scale,
802
+ onChange,
803
+ onCommit,
804
+ onCancel
805
+ }) {
806
+ const ref = useRef(null);
807
+ useEffect(() => {
808
+ const id = requestAnimationFrame(() => {
809
+ ref.current?.focus();
810
+ ref.current?.select?.();
811
+ });
812
+ return () => cancelAnimationFrame(id);
813
+ }, []);
814
+ return /* @__PURE__ */ jsx(
815
+ "div",
816
+ {
817
+ style: {
818
+ position: "absolute",
819
+ left: `${pendingText.x * scale}px`,
820
+ top: `${pendingText.y * scale}px`,
821
+ transform: "translateY(-2px)",
822
+ zIndex: 5
823
+ },
824
+ onPointerDown: (e) => e.stopPropagation(),
825
+ children: /* @__PURE__ */ jsx(
826
+ "textarea",
827
+ {
828
+ ref,
829
+ value: pendingText.value,
830
+ onChange: (e) => onChange(e.target.value),
831
+ onBlur: onCommit,
832
+ onKeyDown: (e) => {
833
+ if (e.key === "Escape") {
834
+ e.preventDefault();
835
+ onCancel();
836
+ } else if (e.key === "Enter" && !e.shiftKey) {
837
+ e.preventDefault();
838
+ onCommit();
839
+ }
840
+ },
841
+ placeholder: "Type then Enter\u2026",
842
+ rows: 1,
843
+ className: "bg-white/95 border border-blue-400 rounded px-1 py-0.5 outline-none resize-none shadow-md",
844
+ style: {
845
+ color,
846
+ fontSize: `${size * scale}px`,
847
+ fontFamily: font,
848
+ fontWeight: bold ? 700 : 400,
849
+ fontStyle: italic ? "italic" : "normal",
850
+ textDecoration: underline ? "underline" : void 0,
851
+ minWidth: 80
852
+ }
853
+ }
854
+ )
855
+ }
856
+ );
857
+ }
858
+ function makeShape(tool, start, end, color, stroke, rectRadius) {
859
+ if (tool === "rect") {
860
+ const r = normalizeRect(start, end);
861
+ return { id: "", type: "rect", x: r.x, y: r.y, w: r.w, h: r.h, color, stroke, radius: rectRadius };
862
+ }
863
+ if (tool === "circle") {
864
+ const r = normalizeRect(start, end);
865
+ return { id: "", type: "circle", x: r.x, y: r.y, w: r.w, h: r.h, color, stroke };
866
+ }
867
+ if (tool === "arrow") {
868
+ return { id: "", type: "arrow", x1: start.x, y1: start.y, x2: end.x, y2: end.y, color, stroke };
869
+ }
870
+ if (tool === "mosaic") {
871
+ const r = normalizeRect(start, end);
872
+ return { id: "", type: "mosaic", x: r.x, y: r.y, w: r.w, h: r.h };
873
+ }
874
+ return null;
875
+ }
876
+ function isTrivial(a) {
877
+ if (a.type === "arrow") return Math.abs(a.x2 - a.x1) < 4 && Math.abs(a.y2 - a.y1) < 4;
878
+ if (a.type === "draw") return a.points.length < 2;
879
+ if ("w" in a && "h" in a) return a.w < 4 || a.h < 4;
880
+ return false;
881
+ }
882
+ function translate(a, dx, dy) {
883
+ if (a.type === "arrow") return { ...a, x1: a.x1 + dx, y1: a.y1 + dy, x2: a.x2 + dx, y2: a.y2 + dy };
884
+ if (a.type === "draw") return { ...a, points: a.points.map((p) => ({ x: p.x + dx, y: p.y + dy })) };
885
+ if (a.type === "text") return { ...a, x: a.x + dx, y: a.y + dy };
886
+ return { ...a, x: a.x + dx, y: a.y + dy };
887
+ }
888
+ function resize(original, corner, p) {
889
+ if (original.type === "arrow") {
890
+ if (corner === "start") return { ...original, x1: p.x, y1: p.y };
891
+ if (corner === "end") return { ...original, x2: p.x, y2: p.y };
892
+ return original;
893
+ }
894
+ if (original.type === "text" || original.type === "draw") return original;
895
+ const left = original.x;
896
+ const right = original.x + original.w;
897
+ const top = original.y;
898
+ const bottom = original.y + original.h;
899
+ let x1 = left, y1 = top, x2 = right, y2 = bottom;
900
+ if (corner === "nw") {
901
+ x1 = p.x;
902
+ y1 = p.y;
903
+ }
904
+ if (corner === "ne") {
905
+ x2 = p.x;
906
+ y1 = p.y;
907
+ }
908
+ if (corner === "se") {
909
+ x2 = p.x;
910
+ y2 = p.y;
911
+ }
912
+ if (corner === "sw") {
913
+ x1 = p.x;
914
+ y2 = p.y;
915
+ }
916
+ const r = normalizeRect({ x: x1, y: y1 }, { x: x2, y: y2 });
917
+ return { ...original, x: r.x, y: r.y, w: r.w, h: r.h };
918
+ }
919
+ function boundingBox(a) {
920
+ if (a.type === "arrow") {
921
+ const x = Math.min(a.x1, a.x2);
922
+ const y = Math.min(a.y1, a.y2);
923
+ return { x, y, w: Math.abs(a.x2 - a.x1), h: Math.abs(a.y2 - a.y1) };
924
+ }
925
+ if (a.type === "draw") {
926
+ if (a.points.length === 0) return { x: 0, y: 0, w: 0, h: 0 };
927
+ let minX = a.points[0].x, maxX = a.points[0].x, minY = a.points[0].y, maxY = a.points[0].y;
928
+ for (const p of a.points) {
929
+ if (p.x < minX) minX = p.x;
930
+ if (p.x > maxX) maxX = p.x;
931
+ if (p.y < minY) minY = p.y;
932
+ if (p.y > maxY) maxY = p.y;
933
+ }
934
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
935
+ }
936
+ if (a.type === "text") {
937
+ const lines = a.text.split("\n");
938
+ const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
939
+ return { x: a.x, y: a.y, w: longest * a.size * 0.55, h: lines.length * a.size * 1.2 };
940
+ }
941
+ return { x: a.x, y: a.y, w: a.w, h: a.h };
942
+ }
943
+ function normalizeRect(a, b) {
944
+ const x = Math.min(a.x, b.x);
945
+ const y = Math.min(a.y, b.y);
946
+ const w = Math.abs(a.x - b.x);
947
+ const h = Math.abs(a.y - b.y);
948
+ return { x, y, w, h };
949
+ }
950
+ function withinBounds(a, w, h) {
951
+ const bb = boundingBox(a);
952
+ return bb.x + bb.w > 0 && bb.y + bb.h > 0 && bb.x < w && bb.y < h;
953
+ }
954
+ function pointsToPath(points) {
955
+ if (points.length === 0) return "";
956
+ let d = `M ${points[0].x} ${points[0].y}`;
957
+ for (let i = 1; i < points.length; i++) d += ` L ${points[i].x} ${points[i].y}`;
958
+ return d;
959
+ }
960
+ function applyMosaic(ctx, rect) {
961
+ const x = Math.round(Math.max(0, rect.x));
962
+ const y = Math.round(Math.max(0, rect.y));
963
+ const w = Math.round(Math.min(ctx.canvas.width - x, rect.w));
964
+ const h = Math.round(Math.min(ctx.canvas.height - y, rect.h));
965
+ if (w < 2 || h < 2) return;
966
+ const data = ctx.getImageData(x, y, w, h);
967
+ const block = MOSAIC_BLOCK;
968
+ for (let by = 0; by < h; by += block) {
969
+ for (let bx = 0; bx < w; bx += block) {
970
+ let r = 0, g = 0, b = 0, a = 0, n = 0;
971
+ const blockW = Math.min(block, w - bx);
972
+ const blockH = Math.min(block, h - by);
973
+ for (let yy = 0; yy < blockH; yy++) {
974
+ for (let xx = 0; xx < blockW; xx++) {
975
+ const i = ((by + yy) * w + (bx + xx)) * 4;
976
+ r += data.data[i];
977
+ g += data.data[i + 1];
978
+ b += data.data[i + 2];
979
+ a += data.data[i + 3];
980
+ n++;
981
+ }
982
+ }
983
+ r = Math.round(r / n);
984
+ g = Math.round(g / n);
985
+ b = Math.round(b / n);
986
+ a = Math.round(a / n);
987
+ for (let yy = 0; yy < blockH; yy++) {
988
+ for (let xx = 0; xx < blockW; xx++) {
989
+ const i = ((by + yy) * w + (bx + xx)) * 4;
990
+ data.data[i] = r;
991
+ data.data[i + 1] = g;
992
+ data.data[i + 2] = b;
993
+ data.data[i + 3] = a;
994
+ }
995
+ }
996
+ }
997
+ }
998
+ ctx.putImageData(data, x, y);
999
+ }
10
1000
  var ToolbarSlotContext = createContext(null);
11
1001
  function PanelActions({ children }) {
12
1002
  const slot = useContext(ToolbarSlotContext);
@@ -3299,5 +4289,5 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
3299
4289
  }
3300
4290
 
3301
4291
  export { Preview, setPdfPreview };
3302
- //# sourceMappingURL=chunk-3HIKRXNT.js.map
3303
- //# sourceMappingURL=chunk-3HIKRXNT.js.map
4292
+ //# sourceMappingURL=chunk-SKNOGIDC.js.map
4293
+ //# sourceMappingURL=chunk-SKNOGIDC.js.map