react-os-shell 0.2.22 → 0.2.24
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-XKNHOT5T.js → Files-ZPMM53WI.js} +4 -4
- package/dist/{Files-XKNHOT5T.js.map → Files-ZPMM53WI.js.map} +1 -1
- package/dist/Preview-QI4WKYS2.js +6 -0
- package/dist/{Preview-MIILWJVE.js.map → Preview-QI4WKYS2.js.map} +1 -1
- package/dist/apps/index.js +4 -4
- package/dist/{chunk-4DW5YQ7Y.js → chunk-LJ6DLGTY.js} +603 -254
- package/dist/chunk-LJ6DLGTY.js.map +1 -0
- package/dist/{chunk-XYCLLD46.js → chunk-NC7UQF6K.js} +3 -3
- package/dist/{chunk-XYCLLD46.js.map → chunk-NC7UQF6K.js.map} +1 -1
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/dist/Preview-MIILWJVE.js +0 -6
- package/dist/chunk-4DW5YQ7Y.js.map +0 -1
|
@@ -8,53 +8,56 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
|
8
8
|
var COLORS = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6", "#8b5cf6", "#000000", "#ffffff"];
|
|
9
9
|
var STROKE_DEFAULT = 4;
|
|
10
10
|
var MOSAIC_BLOCK = 12;
|
|
11
|
+
var ZOOM_MIN = 0.25;
|
|
12
|
+
var ZOOM_MAX = 4;
|
|
13
|
+
var ZOOM_STEP = 0.25;
|
|
14
|
+
var newId = () => Math.random().toString(36).slice(2, 9);
|
|
11
15
|
function ImageAnnotator({ src, filename, onClose }) {
|
|
12
|
-
const canvasRef = useRef(null);
|
|
13
|
-
const overlayRef = useRef(null);
|
|
14
16
|
const wrapRef = useRef(null);
|
|
15
|
-
const
|
|
17
|
+
const canvasRef = useRef(null);
|
|
18
|
+
const svgRef = useRef(null);
|
|
19
|
+
const imageRef = useRef(null);
|
|
20
|
+
const [tool, setTool] = useState("select");
|
|
16
21
|
const [color, setColor] = useState(COLORS[0]);
|
|
17
22
|
const [stroke, setStroke] = useState(STROKE_DEFAULT);
|
|
18
|
-
const
|
|
19
|
-
const [
|
|
20
|
-
const [
|
|
23
|
+
const [annotations, setAnnotations] = useState([]);
|
|
24
|
+
const [selectedId, setSelectedId] = useState(null);
|
|
25
|
+
const [imageSize, setImageSize] = useState(null);
|
|
26
|
+
const [fitSize, setFitSize] = useState(null);
|
|
27
|
+
const [zoom, setZoom] = useState(1);
|
|
28
|
+
const [preview, setPreview] = useState(null);
|
|
21
29
|
const [pendingText, setPendingText] = useState(null);
|
|
22
30
|
const [pendingCrop, setPendingCrop] = useState(null);
|
|
23
|
-
const
|
|
24
|
-
const [
|
|
31
|
+
const dragRef = useRef(null);
|
|
32
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
33
|
+
const displaySize = useMemo(() => {
|
|
34
|
+
if (!fitSize) return null;
|
|
35
|
+
return { w: fitSize.w * zoom, h: fitSize.h * zoom };
|
|
36
|
+
}, [fitSize, zoom]);
|
|
25
37
|
useEffect(() => {
|
|
26
38
|
const img = new Image();
|
|
27
39
|
img.crossOrigin = "anonymous";
|
|
28
40
|
img.onload = () => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
c.height = img.naturalHeight;
|
|
34
|
-
o.width = img.naturalWidth;
|
|
35
|
-
o.height = img.naturalHeight;
|
|
36
|
-
const ctx = c.getContext("2d");
|
|
37
|
-
ctx.drawImage(img, 0, 0);
|
|
38
|
-
historyRef.current = [];
|
|
39
|
-
setHistoryDepth(0);
|
|
40
|
-
setImageReady(true);
|
|
41
|
+
imageRef.current = img;
|
|
42
|
+
setImageSize({ w: img.naturalWidth, h: img.naturalHeight });
|
|
43
|
+
setAnnotations([]);
|
|
44
|
+
setSelectedId(null);
|
|
41
45
|
};
|
|
42
46
|
img.onerror = () => toast_default.error("Failed to load image");
|
|
43
47
|
img.src = src;
|
|
44
48
|
}, [src]);
|
|
45
49
|
useEffect(() => {
|
|
46
|
-
if (!
|
|
50
|
+
if (!imageSize) return;
|
|
47
51
|
const update = () => {
|
|
48
|
-
const c = canvasRef.current;
|
|
49
52
|
const wrap = wrapRef.current;
|
|
50
|
-
if (!
|
|
51
|
-
const
|
|
52
|
-
const availW = Math.max(0,
|
|
53
|
-
const availH = Math.max(0,
|
|
53
|
+
if (!wrap) return;
|
|
54
|
+
const r = wrap.getBoundingClientRect();
|
|
55
|
+
const availW = Math.max(0, r.width - 32);
|
|
56
|
+
const availH = Math.max(0, r.height - 32);
|
|
54
57
|
if (availW === 0 || availH === 0) return;
|
|
55
|
-
const ratio =
|
|
56
|
-
let w =
|
|
57
|
-
let h =
|
|
58
|
+
const ratio = imageSize.w / imageSize.h;
|
|
59
|
+
let w = imageSize.w;
|
|
60
|
+
let h = imageSize.h;
|
|
58
61
|
if (w > availW) {
|
|
59
62
|
w = availW;
|
|
60
63
|
h = w / ratio;
|
|
@@ -63,171 +66,220 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
63
66
|
h = availH;
|
|
64
67
|
w = h * ratio;
|
|
65
68
|
}
|
|
66
|
-
|
|
67
|
-
setDisplayScale(c.width / w);
|
|
69
|
+
setFitSize({ w, h });
|
|
68
70
|
};
|
|
69
71
|
update();
|
|
70
72
|
const ro = new ResizeObserver(update);
|
|
71
73
|
if (wrapRef.current) ro.observe(wrapRef.current);
|
|
72
74
|
return () => ro.disconnect();
|
|
73
|
-
}, [
|
|
74
|
-
const
|
|
75
|
+
}, [imageSize]);
|
|
76
|
+
const mosaicAnnos = useMemo(
|
|
77
|
+
() => annotations.filter((a) => a.type === "mosaic"),
|
|
78
|
+
[annotations]
|
|
79
|
+
);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const img = imageRef.current;
|
|
75
82
|
const c = canvasRef.current;
|
|
76
|
-
if (!c) return;
|
|
83
|
+
if (!img || !c || !imageSize) return;
|
|
84
|
+
c.width = imageSize.w;
|
|
85
|
+
c.height = imageSize.h;
|
|
77
86
|
const ctx = c.getContext("2d");
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
ctx.drawImage(img, 0, 0);
|
|
88
|
+
for (const m of mosaicAnnos) applyMosaic(ctx, m);
|
|
89
|
+
}, [imageSize, mosaicAnnos, fitSize]);
|
|
90
|
+
const evToImage = (e) => {
|
|
91
|
+
const svg = svgRef.current;
|
|
92
|
+
if (!svg) return { x: 0, y: 0 };
|
|
93
|
+
const pt = svg.createSVGPoint();
|
|
94
|
+
pt.x = e.clientX;
|
|
95
|
+
pt.y = e.clientY;
|
|
96
|
+
const ctm = svg.getScreenCTM();
|
|
97
|
+
if (!ctm) return { x: 0, y: 0 };
|
|
98
|
+
const t = pt.matrixTransform(ctm.inverse());
|
|
99
|
+
return { x: t.x, y: t.y };
|
|
81
100
|
};
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
setHistoryDepth(historyRef.current.length);
|
|
86
|
-
const c = canvasRef.current;
|
|
87
|
-
if (!c) return;
|
|
88
|
-
c.getContext("2d").putImageData(prev, 0, 0);
|
|
89
|
-
setPendingCrop(null);
|
|
90
|
-
clearOverlay();
|
|
101
|
+
const beginDrag = (drag) => {
|
|
102
|
+
dragRef.current = drag;
|
|
103
|
+
setIsDragging(true);
|
|
91
104
|
};
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!isDragging) return;
|
|
107
|
+
const onMove = (ev) => {
|
|
108
|
+
const drag = dragRef.current;
|
|
109
|
+
if (!drag) return;
|
|
110
|
+
const p = evToImage(ev);
|
|
111
|
+
if (drag.kind === "draw") {
|
|
112
|
+
setPreview(makeShape(tool, drag.start, p, color, stroke));
|
|
113
|
+
} else if (drag.kind === "crop") {
|
|
114
|
+
setPendingCrop(normalizeRect(drag.start, p));
|
|
115
|
+
} else if (drag.kind === "move") {
|
|
116
|
+
const dx = p.x - drag.start.x;
|
|
117
|
+
const dy = p.y - drag.start.y;
|
|
118
|
+
setAnnotations((prev) => prev.map((a) => a.id === drag.id ? translate(drag.original, dx, dy) : a));
|
|
119
|
+
} else if (drag.kind === "resize") {
|
|
120
|
+
setAnnotations((prev) => prev.map((a) => a.id === drag.id ? resize(drag.original, drag.corner, p) : a));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const onUp = () => {
|
|
124
|
+
const drag = dragRef.current;
|
|
125
|
+
dragRef.current = null;
|
|
126
|
+
setIsDragging(false);
|
|
127
|
+
if (drag?.kind === "draw") {
|
|
128
|
+
setPreview((p) => {
|
|
129
|
+
if (!p || isTrivial(p)) return null;
|
|
130
|
+
const anno = { ...p, id: newId() };
|
|
131
|
+
setAnnotations((prev) => [...prev, anno]);
|
|
132
|
+
setSelectedId(anno.id);
|
|
133
|
+
setTool("select");
|
|
134
|
+
return null;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
window.addEventListener("pointermove", onMove);
|
|
139
|
+
window.addEventListener("pointerup", onUp);
|
|
140
|
+
window.addEventListener("pointercancel", onUp);
|
|
141
|
+
return () => {
|
|
142
|
+
window.removeEventListener("pointermove", onMove);
|
|
143
|
+
window.removeEventListener("pointerup", onUp);
|
|
144
|
+
window.removeEventListener("pointercancel", onUp);
|
|
145
|
+
};
|
|
146
|
+
}, [isDragging, tool, color, stroke]);
|
|
147
|
+
const handleAnnoPointerDown = (e, anno) => {
|
|
148
|
+
if (tool !== "select") return;
|
|
149
|
+
e.stopPropagation();
|
|
150
|
+
setSelectedId(anno.id);
|
|
151
|
+
beginDrag({ kind: "move", id: anno.id, start: evToImage(e), original: anno });
|
|
102
152
|
};
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (!o) return;
|
|
107
|
-
const ctx = o.getContext("2d");
|
|
108
|
-
ctx.clearRect(0, 0, o.width, o.height);
|
|
109
|
-
ctx.strokeStyle = color;
|
|
110
|
-
ctx.fillStyle = color;
|
|
111
|
-
ctx.lineWidth = stroke;
|
|
112
|
-
ctx.lineCap = "round";
|
|
113
|
-
ctx.lineJoin = "round";
|
|
114
|
-
if (tool === "rect") {
|
|
115
|
-
drawRoundedRect(ctx, start, end, stroke);
|
|
116
|
-
} else if (tool === "circle") {
|
|
117
|
-
drawEllipse(ctx, start, end);
|
|
118
|
-
} else if (tool === "arrow") {
|
|
119
|
-
drawArrow(ctx, start, end, stroke);
|
|
120
|
-
} else if (tool === "mosaic") {
|
|
121
|
-
ctx.fillStyle = `${color}33`;
|
|
122
|
-
ctx.strokeStyle = color;
|
|
123
|
-
ctx.lineWidth = 1.5;
|
|
124
|
-
const r = normalizeRect(start, end);
|
|
125
|
-
ctx.fillRect(r.x, r.y, r.w, r.h);
|
|
126
|
-
ctx.strokeRect(r.x, r.y, r.w, r.h);
|
|
127
|
-
} else if (tool === "crop") {
|
|
128
|
-
ctx.strokeStyle = "#ffffff";
|
|
129
|
-
ctx.lineWidth = 2;
|
|
130
|
-
ctx.setLineDash([8, 6]);
|
|
131
|
-
const r = normalizeRect(start, end);
|
|
132
|
-
ctx.fillStyle = "rgba(0,0,0,0.45)";
|
|
133
|
-
ctx.fillRect(0, 0, o.width, r.y);
|
|
134
|
-
ctx.fillRect(0, r.y + r.h, o.width, o.height - (r.y + r.h));
|
|
135
|
-
ctx.fillRect(0, r.y, r.x, r.h);
|
|
136
|
-
ctx.fillRect(r.x + r.w, r.y, o.width - (r.x + r.w), r.h);
|
|
137
|
-
ctx.strokeRect(r.x, r.y, r.w, r.h);
|
|
138
|
-
ctx.setLineDash([]);
|
|
139
|
-
}
|
|
153
|
+
const handleHandlePointerDown = (e, anno, corner) => {
|
|
154
|
+
e.stopPropagation();
|
|
155
|
+
beginDrag({ kind: "resize", id: anno.id, corner, start: evToImage(e), original: anno });
|
|
140
156
|
};
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
if (tool === "text") {
|
|
145
|
-
setPendingText({ x: pos.x, y: pos.y, value: "" });
|
|
157
|
+
const handleSvgPointerDown = (e) => {
|
|
158
|
+
if (tool === "select") {
|
|
159
|
+
setSelectedId(null);
|
|
146
160
|
return;
|
|
147
161
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
};
|
|
152
|
-
const onPointerMove = (e) => {
|
|
153
|
-
if (!dragRef.current) return;
|
|
154
|
-
const pos = evToCanvas(e);
|
|
155
|
-
dragRef.current.current = pos;
|
|
156
|
-
drawShapePreview(dragRef.current.start, pos);
|
|
157
|
-
};
|
|
158
|
-
const onPointerUp = (e) => {
|
|
159
|
-
if (!dragRef.current) return;
|
|
160
|
-
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
161
|
-
const { start, current } = dragRef.current;
|
|
162
|
-
dragRef.current = null;
|
|
163
|
-
if (Math.abs(current.x - start.x) < 2 && Math.abs(current.y - start.y) < 2) {
|
|
164
|
-
clearOverlay();
|
|
162
|
+
if (tool === "text") {
|
|
163
|
+
const p = evToImage(e);
|
|
164
|
+
setPendingText({ x: p.x, y: p.y, value: "" });
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
|
+
const start = evToImage(e);
|
|
167
168
|
if (tool === "crop") {
|
|
168
|
-
|
|
169
|
-
setPendingCrop(r);
|
|
169
|
+
beginDrag({ kind: "crop", start });
|
|
170
170
|
return;
|
|
171
171
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const ctx = c.getContext("2d");
|
|
175
|
-
ctx.strokeStyle = color;
|
|
176
|
-
ctx.fillStyle = color;
|
|
177
|
-
ctx.lineWidth = stroke;
|
|
178
|
-
ctx.lineCap = "round";
|
|
179
|
-
ctx.lineJoin = "round";
|
|
180
|
-
if (tool === "rect") drawRoundedRect(ctx, start, current, stroke);
|
|
181
|
-
else if (tool === "circle") drawEllipse(ctx, start, current);
|
|
182
|
-
else if (tool === "arrow") drawArrow(ctx, start, current, stroke);
|
|
183
|
-
else if (tool === "mosaic") applyMosaic(ctx, normalizeRect(start, current));
|
|
184
|
-
clearOverlay();
|
|
172
|
+
setPreview(makeShape(tool, start, start, color, stroke));
|
|
173
|
+
beginDrag({ kind: "draw", start });
|
|
185
174
|
};
|
|
175
|
+
const handleAnnoDoubleClick = (anno) => {
|
|
176
|
+
if (anno.type !== "text") return;
|
|
177
|
+
setPendingText({ x: anno.x, y: anno.y, value: anno.text, editingId: anno.id });
|
|
178
|
+
};
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
const onKey = (ev) => {
|
|
181
|
+
if (!selectedId) return;
|
|
182
|
+
if (document.activeElement && /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName)) return;
|
|
183
|
+
if (ev.key === "Delete" || ev.key === "Backspace") {
|
|
184
|
+
ev.preventDefault();
|
|
185
|
+
setAnnotations((prev) => prev.filter((a) => a.id !== selectedId));
|
|
186
|
+
setSelectedId(null);
|
|
187
|
+
} else if (ev.key === "Escape") {
|
|
188
|
+
setSelectedId(null);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
window.addEventListener("keydown", onKey);
|
|
192
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
193
|
+
}, [selectedId]);
|
|
186
194
|
const commitText = () => {
|
|
187
195
|
if (!pendingText) return;
|
|
188
|
-
|
|
196
|
+
const value = pendingText.value.trim();
|
|
197
|
+
if (!value) {
|
|
198
|
+
if (pendingText.editingId) {
|
|
199
|
+
setAnnotations((prev) => prev.filter((a) => a.id !== pendingText.editingId));
|
|
200
|
+
}
|
|
189
201
|
setPendingText(null);
|
|
190
202
|
return;
|
|
191
203
|
}
|
|
192
|
-
pushHistory();
|
|
193
|
-
const c = canvasRef.current;
|
|
194
|
-
const ctx = c.getContext("2d");
|
|
195
204
|
const fontSize = Math.max(16, stroke * 6);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
if (pendingText.editingId) {
|
|
206
|
+
setAnnotations((prev) => prev.map(
|
|
207
|
+
(a) => a.id === pendingText.editingId && a.type === "text" ? { ...a, text: value } : a
|
|
208
|
+
));
|
|
209
|
+
} else {
|
|
210
|
+
const anno = {
|
|
211
|
+
id: newId(),
|
|
212
|
+
type: "text",
|
|
213
|
+
x: pendingText.x,
|
|
214
|
+
y: pendingText.y,
|
|
215
|
+
text: value,
|
|
216
|
+
color,
|
|
217
|
+
size: fontSize
|
|
218
|
+
};
|
|
219
|
+
setAnnotations((prev) => [...prev, anno]);
|
|
220
|
+
setSelectedId(anno.id);
|
|
221
|
+
setTool("select");
|
|
222
|
+
}
|
|
203
223
|
setPendingText(null);
|
|
204
224
|
};
|
|
205
225
|
const applyCrop = () => {
|
|
206
|
-
if (!pendingCrop) return;
|
|
207
|
-
const c = canvasRef.current;
|
|
208
|
-
pushHistory();
|
|
226
|
+
if (!pendingCrop || !imageRef.current || !imageSize) return;
|
|
209
227
|
const r = pendingCrop;
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
228
|
+
const tmp = document.createElement("canvas");
|
|
229
|
+
tmp.width = Math.round(r.w);
|
|
230
|
+
tmp.height = Math.round(r.h);
|
|
231
|
+
const tctx = tmp.getContext("2d");
|
|
232
|
+
const sourceCanvas = document.createElement("canvas");
|
|
233
|
+
sourceCanvas.width = imageSize.w;
|
|
234
|
+
sourceCanvas.height = imageSize.h;
|
|
235
|
+
const sctx = sourceCanvas.getContext("2d");
|
|
236
|
+
sctx.drawImage(imageRef.current, 0, 0);
|
|
237
|
+
for (const m of mosaicAnnos) applyMosaic(sctx, m);
|
|
238
|
+
tctx.drawImage(sourceCanvas, r.x, r.y, r.w, r.h, 0, 0, r.w, r.h);
|
|
239
|
+
const newImg = new Image();
|
|
240
|
+
newImg.onload = () => {
|
|
241
|
+
imageRef.current = newImg;
|
|
242
|
+
setImageSize({ w: newImg.naturalWidth, h: newImg.naturalHeight });
|
|
243
|
+
setAnnotations(
|
|
244
|
+
(prev) => prev.filter((a) => a.type !== "mosaic").map((a) => translate(a, -r.x, -r.y)).filter((a) => withinBounds(a, newImg.naturalWidth, newImg.naturalHeight))
|
|
245
|
+
);
|
|
246
|
+
setPendingCrop(null);
|
|
247
|
+
};
|
|
248
|
+
newImg.src = tmp.toDataURL("image/png");
|
|
222
249
|
};
|
|
223
|
-
const cancelCrop = () =>
|
|
224
|
-
|
|
225
|
-
|
|
250
|
+
const cancelCrop = () => setPendingCrop(null);
|
|
251
|
+
const undoLast = () => {
|
|
252
|
+
setAnnotations((prev) => prev.slice(0, -1));
|
|
253
|
+
setSelectedId(null);
|
|
226
254
|
};
|
|
227
|
-
const
|
|
255
|
+
const compositeToCanvas = async () => {
|
|
228
256
|
const c = canvasRef.current;
|
|
229
|
-
|
|
230
|
-
c
|
|
257
|
+
const svg = svgRef.current;
|
|
258
|
+
if (!c || !svg || !imageSize) return null;
|
|
259
|
+
const out = document.createElement("canvas");
|
|
260
|
+
out.width = imageSize.w;
|
|
261
|
+
out.height = imageSize.h;
|
|
262
|
+
const octx = out.getContext("2d");
|
|
263
|
+
octx.drawImage(c, 0, 0);
|
|
264
|
+
const clone = svg.cloneNode(true);
|
|
265
|
+
clone.querySelectorAll("[data-chrome]").forEach((n) => n.remove());
|
|
266
|
+
const xml = new XMLSerializer().serializeToString(clone);
|
|
267
|
+
const svgBlob = new Blob([xml], { type: "image/svg+xml;charset=utf-8" });
|
|
268
|
+
const svgUrl = URL.createObjectURL(svgBlob);
|
|
269
|
+
const svgImg = new Image();
|
|
270
|
+
await new Promise((resolve) => {
|
|
271
|
+
svgImg.onload = () => resolve();
|
|
272
|
+
svgImg.onerror = () => resolve();
|
|
273
|
+
svgImg.src = svgUrl;
|
|
274
|
+
});
|
|
275
|
+
octx.drawImage(svgImg, 0, 0, imageSize.w, imageSize.h);
|
|
276
|
+
URL.revokeObjectURL(svgUrl);
|
|
277
|
+
return out;
|
|
278
|
+
};
|
|
279
|
+
const downloadAnnotated = async () => {
|
|
280
|
+
const out = await compositeToCanvas();
|
|
281
|
+
if (!out) return;
|
|
282
|
+
out.toBlob((blob) => {
|
|
231
283
|
if (!blob) {
|
|
232
284
|
toast_default.error("Failed to export");
|
|
233
285
|
return;
|
|
@@ -241,7 +293,28 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
241
293
|
URL.revokeObjectURL(url);
|
|
242
294
|
}, "image/png");
|
|
243
295
|
};
|
|
296
|
+
const copyToClipboard = async () => {
|
|
297
|
+
if (!("clipboard" in navigator) || typeof ClipboardItem === "undefined") {
|
|
298
|
+
toast_default.error("Clipboard images are not supported in this browser");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const out = await compositeToCanvas();
|
|
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
|
+
};
|
|
244
316
|
const tools = useMemo(() => [
|
|
317
|
+
{ 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" }) }) },
|
|
245
318
|
{ 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" }) }) },
|
|
246
319
|
{ 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" }) }) },
|
|
247
320
|
{ 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" }) }) },
|
|
@@ -250,12 +323,16 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
250
323
|
{ 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" }) }) }
|
|
251
324
|
], []);
|
|
252
325
|
const btnClass = (active) => `p-1.5 rounded transition-colors ${active ? "bg-blue-500 text-white" : "text-gray-700 hover:bg-gray-200"}`;
|
|
326
|
+
const scale = displaySize && imageSize ? displaySize.w / imageSize.w : 1;
|
|
253
327
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full bg-gray-100", children: [
|
|
254
328
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-b border-gray-200 bg-white shrink-0 flex-wrap", children: [
|
|
255
329
|
tools.map((t) => /* @__PURE__ */ jsx(
|
|
256
330
|
"button",
|
|
257
331
|
{
|
|
258
|
-
onClick: () =>
|
|
332
|
+
onClick: () => {
|
|
333
|
+
setTool(t.id);
|
|
334
|
+
setSelectedId(null);
|
|
335
|
+
},
|
|
259
336
|
title: t.label,
|
|
260
337
|
className: btnClass(tool === t.id),
|
|
261
338
|
children: t.icon
|
|
@@ -266,7 +343,12 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
266
343
|
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: COLORS.map((c) => /* @__PURE__ */ jsx(
|
|
267
344
|
"button",
|
|
268
345
|
{
|
|
269
|
-
onClick: () =>
|
|
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
|
+
},
|
|
270
352
|
title: c,
|
|
271
353
|
className: `h-5 w-5 rounded-full border ${color === c ? "ring-2 ring-blue-500 ring-offset-1" : "border-gray-300"}`,
|
|
272
354
|
style: { background: c }
|
|
@@ -291,7 +373,16 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
291
373
|
/* @__PURE__ */ jsx("span", { className: "tabular-nums w-4 text-right", children: stroke })
|
|
292
374
|
] }),
|
|
293
375
|
/* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
|
|
294
|
-
/* @__PURE__ */ jsx("button", { onClick:
|
|
376
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.max(ZOOM_MIN, Math.round((z - ZOOM_STEP) * 100) / 100)), className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", title: "Zoom out", children: "\u2212" }),
|
|
377
|
+
/* @__PURE__ */ jsxs("span", { className: "text-[11px] text-gray-600 tabular-nums w-10 text-center", children: [
|
|
378
|
+
Math.round(zoom * 100),
|
|
379
|
+
"%"
|
|
380
|
+
] }),
|
|
381
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.min(ZOOM_MAX, Math.round((z + ZOOM_STEP) * 100) / 100)), className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", title: "Zoom in", children: "+" }),
|
|
382
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setZoom(1), className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", title: "Fit to area", children: "Fit" }),
|
|
383
|
+
/* @__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 text-xs rounded hover:bg-gray-200 disabled:opacity-30 text-gray-700", children: "Undo" }),
|
|
385
|
+
/* @__PURE__ */ jsx("button", { onClick: copyToClipboard, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Copy" }),
|
|
295
386
|
/* @__PURE__ */ jsx("button", { onClick: downloadAnnotated, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Save" }),
|
|
296
387
|
/* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
|
|
297
388
|
pendingCrop && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -301,79 +392,372 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
301
392
|
/* @__PURE__ */ jsx("button", { onClick: onClose, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Exit" })
|
|
302
393
|
] })
|
|
303
394
|
] }),
|
|
304
|
-
/* @__PURE__ */ jsx("div", { ref: wrapRef, className: "flex-1 overflow-
|
|
395
|
+
/* @__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(
|
|
305
396
|
"div",
|
|
306
397
|
{
|
|
307
|
-
className: "relative shadow-lg rounded overflow-hidden",
|
|
308
|
-
style:
|
|
398
|
+
className: "relative shadow-lg rounded overflow-hidden bg-white shrink-0",
|
|
399
|
+
style: { width: displaySize.w, height: displaySize.h },
|
|
309
400
|
children: [
|
|
310
401
|
/* @__PURE__ */ jsx(
|
|
311
402
|
"canvas",
|
|
312
403
|
{
|
|
313
404
|
ref: canvasRef,
|
|
314
|
-
|
|
315
|
-
onPointerMove,
|
|
316
|
-
onPointerUp,
|
|
317
|
-
onPointerCancel: () => {
|
|
318
|
-
dragRef.current = null;
|
|
319
|
-
clearOverlay();
|
|
320
|
-
},
|
|
321
|
-
style: { touchAction: "none", display: "block", width: "100%", height: "100%", cursor: tool === "text" ? "text" : "crosshair", background: "#fff" }
|
|
405
|
+
style: { position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }
|
|
322
406
|
}
|
|
323
407
|
),
|
|
324
|
-
/* @__PURE__ */
|
|
325
|
-
"
|
|
408
|
+
/* @__PURE__ */ jsxs(
|
|
409
|
+
"svg",
|
|
326
410
|
{
|
|
327
|
-
ref:
|
|
411
|
+
ref: svgRef,
|
|
412
|
+
viewBox: `0 0 ${imageSize.w} ${imageSize.h}`,
|
|
413
|
+
preserveAspectRatio: "none",
|
|
328
414
|
style: {
|
|
329
415
|
position: "absolute",
|
|
330
|
-
|
|
331
|
-
left: 0,
|
|
416
|
+
inset: 0,
|
|
332
417
|
width: "100%",
|
|
333
418
|
height: "100%",
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
419
|
+
touchAction: "none",
|
|
420
|
+
cursor: tool === "select" ? "default" : tool === "text" ? "text" : "crosshair"
|
|
421
|
+
},
|
|
422
|
+
onPointerDown: handleSvgPointerDown,
|
|
423
|
+
children: [
|
|
424
|
+
annotations.map((a) => /* @__PURE__ */ jsx(
|
|
425
|
+
AnnotationView,
|
|
426
|
+
{
|
|
427
|
+
anno: a,
|
|
428
|
+
selected: selectedId === a.id,
|
|
429
|
+
zoom,
|
|
430
|
+
onPointerDown: (e) => handleAnnoPointerDown(e, a),
|
|
431
|
+
onDoubleClick: () => handleAnnoDoubleClick(a),
|
|
432
|
+
onHandlePointerDown: (e, corner) => handleHandlePointerDown(e, a, corner)
|
|
433
|
+
},
|
|
434
|
+
a.id
|
|
435
|
+
)),
|
|
436
|
+
preview && /* @__PURE__ */ jsx(AnnotationView, { anno: { ...preview, id: "__preview" }, preview: true }),
|
|
437
|
+
pendingCrop && /* @__PURE__ */ jsx(CropOverlay, { rect: pendingCrop, imageSize })
|
|
438
|
+
]
|
|
337
439
|
}
|
|
338
440
|
),
|
|
339
441
|
pendingText && /* @__PURE__ */ jsx(
|
|
340
|
-
|
|
442
|
+
PendingTextEditor,
|
|
341
443
|
{
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
className: "z-10",
|
|
349
|
-
children: /* @__PURE__ */ jsx(
|
|
350
|
-
"textarea",
|
|
351
|
-
{
|
|
352
|
-
autoFocus: true,
|
|
353
|
-
value: pendingText.value,
|
|
354
|
-
onChange: (e) => setPendingText({ ...pendingText, value: e.target.value }),
|
|
355
|
-
onBlur: commitText,
|
|
356
|
-
onKeyDown: (e) => {
|
|
357
|
-
if (e.key === "Escape") {
|
|
358
|
-
setPendingText(null);
|
|
359
|
-
} else if (e.key === "Enter" && !e.shiftKey) {
|
|
360
|
-
e.preventDefault();
|
|
361
|
-
commitText();
|
|
362
|
-
}
|
|
363
|
-
},
|
|
364
|
-
placeholder: "Type then Enter\u2026",
|
|
365
|
-
rows: 1,
|
|
366
|
-
className: "bg-white/95 border border-blue-400 rounded px-1 py-0.5 text-sm outline-none resize-none shadow-md",
|
|
367
|
-
style: { color, fontWeight: 600, minWidth: 80 }
|
|
368
|
-
}
|
|
369
|
-
)
|
|
444
|
+
pendingText,
|
|
445
|
+
color,
|
|
446
|
+
scale,
|
|
447
|
+
onChange: (value) => setPendingText({ ...pendingText, value }),
|
|
448
|
+
onCommit: commitText,
|
|
449
|
+
onCancel: () => setPendingText(null)
|
|
370
450
|
}
|
|
371
451
|
)
|
|
372
452
|
]
|
|
373
453
|
}
|
|
374
|
-
) })
|
|
454
|
+
) }),
|
|
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 it. Double-click text to edit." : tool === "text" ? "Click to drop a text label." : tool === "crop" ? "Drag a rectangle to set the crop region." : "Drag on the image to draw." })
|
|
375
456
|
] });
|
|
376
457
|
}
|
|
458
|
+
function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDoubleClick, onHandlePointerDown }) {
|
|
459
|
+
const dim = boundingBox(anno);
|
|
460
|
+
const interactive = onPointerDown ? { onPointerDown, pointerEvents: "all", style: { cursor: "move" } } : {};
|
|
461
|
+
const dblc = onDoubleClick ? { onDoubleClick } : {};
|
|
462
|
+
let body;
|
|
463
|
+
if (anno.type === "rect") {
|
|
464
|
+
const radius = Math.min(anno.w, anno.h, 16) * 0.4;
|
|
465
|
+
body = /* @__PURE__ */ jsx(
|
|
466
|
+
"rect",
|
|
467
|
+
{
|
|
468
|
+
x: anno.x,
|
|
469
|
+
y: anno.y,
|
|
470
|
+
width: anno.w,
|
|
471
|
+
height: anno.h,
|
|
472
|
+
rx: radius,
|
|
473
|
+
ry: radius,
|
|
474
|
+
fill: "none",
|
|
475
|
+
stroke: anno.color,
|
|
476
|
+
strokeWidth: anno.stroke,
|
|
477
|
+
strokeLinecap: "round",
|
|
478
|
+
strokeLinejoin: "round",
|
|
479
|
+
...interactive
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
} else if (anno.type === "circle") {
|
|
483
|
+
body = /* @__PURE__ */ jsx(
|
|
484
|
+
"ellipse",
|
|
485
|
+
{
|
|
486
|
+
cx: anno.x + anno.w / 2,
|
|
487
|
+
cy: anno.y + anno.h / 2,
|
|
488
|
+
rx: anno.w / 2,
|
|
489
|
+
ry: anno.h / 2,
|
|
490
|
+
fill: "none",
|
|
491
|
+
stroke: anno.color,
|
|
492
|
+
strokeWidth: anno.stroke,
|
|
493
|
+
...interactive
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
} else if (anno.type === "arrow") {
|
|
497
|
+
body = /* @__PURE__ */ jsx(ArrowShape, { anno, interactive });
|
|
498
|
+
} else if (anno.type === "mosaic") {
|
|
499
|
+
body = /* @__PURE__ */ jsx(
|
|
500
|
+
"rect",
|
|
501
|
+
{
|
|
502
|
+
x: anno.x,
|
|
503
|
+
y: anno.y,
|
|
504
|
+
width: anno.w,
|
|
505
|
+
height: anno.h,
|
|
506
|
+
fill: "rgba(0,0,0,0.001)",
|
|
507
|
+
stroke: selected ? "#3b82f6" : "none",
|
|
508
|
+
strokeWidth: selected ? 2 : 0,
|
|
509
|
+
strokeDasharray: selected ? "6,4" : void 0,
|
|
510
|
+
...interactive
|
|
511
|
+
}
|
|
512
|
+
);
|
|
513
|
+
} else {
|
|
514
|
+
body = /* @__PURE__ */ jsx(
|
|
515
|
+
"text",
|
|
516
|
+
{
|
|
517
|
+
x: anno.x,
|
|
518
|
+
y: anno.y + anno.size,
|
|
519
|
+
fill: anno.color,
|
|
520
|
+
fontSize: anno.size,
|
|
521
|
+
fontWeight: 600,
|
|
522
|
+
fontFamily: "-apple-system, system-ui, sans-serif",
|
|
523
|
+
style: { userSelect: "none" },
|
|
524
|
+
...interactive,
|
|
525
|
+
...dblc,
|
|
526
|
+
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))
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
return /* @__PURE__ */ jsxs("g", { children: [
|
|
531
|
+
body,
|
|
532
|
+
selected && !preview && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
533
|
+
/* @__PURE__ */ jsx(
|
|
534
|
+
"rect",
|
|
535
|
+
{
|
|
536
|
+
"data-chrome": "selection",
|
|
537
|
+
x: dim.x - 4,
|
|
538
|
+
y: dim.y - 4,
|
|
539
|
+
width: dim.w + 8,
|
|
540
|
+
height: dim.h + 8,
|
|
541
|
+
fill: "none",
|
|
542
|
+
stroke: "#3b82f6",
|
|
543
|
+
strokeWidth: 2 / zoom,
|
|
544
|
+
strokeDasharray: `${6 / zoom},${4 / zoom}`,
|
|
545
|
+
pointerEvents: "none"
|
|
546
|
+
}
|
|
547
|
+
),
|
|
548
|
+
onHandlePointerDown && /* @__PURE__ */ jsx(ResizeHandles, { anno, zoom, onHandlePointerDown })
|
|
549
|
+
] })
|
|
550
|
+
] });
|
|
551
|
+
}
|
|
552
|
+
function ResizeHandles({
|
|
553
|
+
anno,
|
|
554
|
+
zoom,
|
|
555
|
+
onHandlePointerDown
|
|
556
|
+
}) {
|
|
557
|
+
const r = 6 / zoom;
|
|
558
|
+
const sw = 1.5 / zoom;
|
|
559
|
+
const handleProps = (cursor) => ({
|
|
560
|
+
fill: "#fff",
|
|
561
|
+
stroke: "#3b82f6",
|
|
562
|
+
strokeWidth: sw,
|
|
563
|
+
pointerEvents: "all",
|
|
564
|
+
style: { cursor }
|
|
565
|
+
});
|
|
566
|
+
if (anno.type === "arrow") {
|
|
567
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
568
|
+
/* @__PURE__ */ jsx("circle", { cx: anno.x1, cy: anno.y1, r, ...handleProps("grab"), "data-chrome": "handle", onPointerDown: (e) => onHandlePointerDown(e, "start") }),
|
|
569
|
+
/* @__PURE__ */ jsx("circle", { cx: anno.x2, cy: anno.y2, r, ...handleProps("grab"), "data-chrome": "handle", onPointerDown: (e) => onHandlePointerDown(e, "end") })
|
|
570
|
+
] });
|
|
571
|
+
}
|
|
572
|
+
if (anno.type === "text") {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
const x = anno.x;
|
|
576
|
+
const y = anno.y;
|
|
577
|
+
const w = anno.w;
|
|
578
|
+
const h = anno.h;
|
|
579
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
580
|
+
/* @__PURE__ */ jsx("circle", { cx: x, cy: y, r, ...handleProps("nwse-resize"), "data-chrome": "handle", onPointerDown: (e) => onHandlePointerDown(e, "nw") }),
|
|
581
|
+
/* @__PURE__ */ jsx("circle", { cx: x + w, cy: y, r, ...handleProps("nesw-resize"), "data-chrome": "handle", onPointerDown: (e) => onHandlePointerDown(e, "ne") }),
|
|
582
|
+
/* @__PURE__ */ jsx("circle", { cx: x + w, cy: y + h, r, ...handleProps("nwse-resize"), "data-chrome": "handle", onPointerDown: (e) => onHandlePointerDown(e, "se") }),
|
|
583
|
+
/* @__PURE__ */ jsx("circle", { cx: x, cy: y + h, r, ...handleProps("nesw-resize"), "data-chrome": "handle", onPointerDown: (e) => onHandlePointerDown(e, "sw") })
|
|
584
|
+
] });
|
|
585
|
+
}
|
|
586
|
+
function PendingTextEditor({
|
|
587
|
+
pendingText,
|
|
588
|
+
color,
|
|
589
|
+
scale,
|
|
590
|
+
onChange,
|
|
591
|
+
onCommit,
|
|
592
|
+
onCancel
|
|
593
|
+
}) {
|
|
594
|
+
const ref = useRef(null);
|
|
595
|
+
useEffect(() => {
|
|
596
|
+
const id = requestAnimationFrame(() => {
|
|
597
|
+
ref.current?.focus();
|
|
598
|
+
ref.current?.select?.();
|
|
599
|
+
});
|
|
600
|
+
return () => cancelAnimationFrame(id);
|
|
601
|
+
}, []);
|
|
602
|
+
return /* @__PURE__ */ jsx(
|
|
603
|
+
"div",
|
|
604
|
+
{
|
|
605
|
+
style: {
|
|
606
|
+
position: "absolute",
|
|
607
|
+
left: `${pendingText.x * scale}px`,
|
|
608
|
+
top: `${pendingText.y * scale}px`,
|
|
609
|
+
transform: "translateY(-2px)",
|
|
610
|
+
zIndex: 5
|
|
611
|
+
},
|
|
612
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
613
|
+
children: /* @__PURE__ */ jsx(
|
|
614
|
+
"textarea",
|
|
615
|
+
{
|
|
616
|
+
ref,
|
|
617
|
+
value: pendingText.value,
|
|
618
|
+
onChange: (e) => onChange(e.target.value),
|
|
619
|
+
onBlur: onCommit,
|
|
620
|
+
onKeyDown: (e) => {
|
|
621
|
+
if (e.key === "Escape") {
|
|
622
|
+
e.preventDefault();
|
|
623
|
+
onCancel();
|
|
624
|
+
} else if (e.key === "Enter" && !e.shiftKey) {
|
|
625
|
+
e.preventDefault();
|
|
626
|
+
onCommit();
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
placeholder: "Type then Enter\u2026",
|
|
630
|
+
rows: 1,
|
|
631
|
+
className: "bg-white/95 border border-blue-400 rounded px-1 py-0.5 text-sm outline-none resize-none shadow-md",
|
|
632
|
+
style: { color, fontWeight: 600, minWidth: 80 }
|
|
633
|
+
}
|
|
634
|
+
)
|
|
635
|
+
}
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
function ArrowShape({ anno, interactive }) {
|
|
639
|
+
const headLen = Math.max(12, anno.stroke * 4);
|
|
640
|
+
const angle = Math.atan2(anno.y2 - anno.y1, anno.x2 - anno.x1);
|
|
641
|
+
const a1 = angle + Math.PI - Math.PI / 7;
|
|
642
|
+
const a2 = angle + Math.PI + Math.PI / 7;
|
|
643
|
+
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`;
|
|
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") {
|
|
687
|
+
const r = normalizeRect(start, end);
|
|
688
|
+
return { id: "", type: tool, x: r.x, y: r.y, w: r.w, h: r.h, color, stroke };
|
|
689
|
+
}
|
|
690
|
+
if (tool === "arrow") {
|
|
691
|
+
return { id: "", type: "arrow", x1: start.x, y1: start.y, x2: end.x, y2: end.y, color, stroke };
|
|
692
|
+
}
|
|
693
|
+
if (tool === "mosaic") {
|
|
694
|
+
const r = normalizeRect(start, end);
|
|
695
|
+
return { id: "", type: "mosaic", x: r.x, y: r.y, w: r.w, h: r.h };
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
function isTrivial(a) {
|
|
700
|
+
if (a.type === "arrow") {
|
|
701
|
+
return Math.abs(a.x2 - a.x1) < 4 && Math.abs(a.y2 - a.y1) < 4;
|
|
702
|
+
}
|
|
703
|
+
if ("w" in a && "h" in a) {
|
|
704
|
+
return a.w < 4 || a.h < 4;
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
function translate(a, dx, dy) {
|
|
709
|
+
if (a.type === "arrow") {
|
|
710
|
+
return { ...a, x1: a.x1 + dx, y1: a.y1 + dy, x2: a.x2 + dx, y2: a.y2 + dy };
|
|
711
|
+
}
|
|
712
|
+
if (a.type === "text") {
|
|
713
|
+
return { ...a, x: a.x + dx, y: a.y + dy };
|
|
714
|
+
}
|
|
715
|
+
return { ...a, x: a.x + dx, y: a.y + dy };
|
|
716
|
+
}
|
|
717
|
+
function resize(original, corner, p) {
|
|
718
|
+
if (original.type === "arrow") {
|
|
719
|
+
if (corner === "start") return { ...original, x1: p.x, y1: p.y };
|
|
720
|
+
if (corner === "end") return { ...original, x2: p.x, y2: p.y };
|
|
721
|
+
return original;
|
|
722
|
+
}
|
|
723
|
+
if (original.type === "text") return original;
|
|
724
|
+
const left = original.x;
|
|
725
|
+
const right = original.x + original.w;
|
|
726
|
+
const top = original.y;
|
|
727
|
+
const bottom = original.y + original.h;
|
|
728
|
+
let x1 = left, y1 = top, x2 = right, y2 = bottom;
|
|
729
|
+
if (corner === "nw") {
|
|
730
|
+
x1 = p.x;
|
|
731
|
+
y1 = p.y;
|
|
732
|
+
}
|
|
733
|
+
if (corner === "ne") {
|
|
734
|
+
x2 = p.x;
|
|
735
|
+
y1 = p.y;
|
|
736
|
+
}
|
|
737
|
+
if (corner === "se") {
|
|
738
|
+
x2 = p.x;
|
|
739
|
+
y2 = p.y;
|
|
740
|
+
}
|
|
741
|
+
if (corner === "sw") {
|
|
742
|
+
x1 = p.x;
|
|
743
|
+
y2 = p.y;
|
|
744
|
+
}
|
|
745
|
+
const r = normalizeRect({ x: x1, y: y1 }, { x: x2, y: y2 });
|
|
746
|
+
return { ...original, x: r.x, y: r.y, w: r.w, h: r.h };
|
|
747
|
+
}
|
|
748
|
+
function boundingBox(a) {
|
|
749
|
+
if (a.type === "arrow") {
|
|
750
|
+
const x = Math.min(a.x1, a.x2);
|
|
751
|
+
const y = Math.min(a.y1, a.y2);
|
|
752
|
+
return { x, y, w: Math.abs(a.x2 - a.x1), h: Math.abs(a.y2 - a.y1) };
|
|
753
|
+
}
|
|
754
|
+
if (a.type === "text") {
|
|
755
|
+
const lines = a.text.split("\n");
|
|
756
|
+
const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
|
|
757
|
+
return { x: a.x, y: a.y, w: longest * a.size * 0.55, h: lines.length * a.size * 1.2 };
|
|
758
|
+
}
|
|
759
|
+
return { x: a.x, y: a.y, w: a.w, h: a.h };
|
|
760
|
+
}
|
|
377
761
|
function normalizeRect(a, b) {
|
|
378
762
|
const x = Math.min(a.x, b.x);
|
|
379
763
|
const y = Math.min(a.y, b.y);
|
|
@@ -381,44 +765,9 @@ function normalizeRect(a, b) {
|
|
|
381
765
|
const h = Math.abs(a.y - b.y);
|
|
382
766
|
return { x, y, w, h };
|
|
383
767
|
}
|
|
384
|
-
function
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
ctx.beginPath();
|
|
388
|
-
if (ctx.roundRect) {
|
|
389
|
-
ctx.roundRect(r.x, r.y, r.w, r.h, radius);
|
|
390
|
-
} else {
|
|
391
|
-
ctx.moveTo(r.x + radius, r.y);
|
|
392
|
-
ctx.arcTo(r.x + r.w, r.y, r.x + r.w, r.y + r.h, radius);
|
|
393
|
-
ctx.arcTo(r.x + r.w, r.y + r.h, r.x, r.y + r.h, radius);
|
|
394
|
-
ctx.arcTo(r.x, r.y + r.h, r.x, r.y, radius);
|
|
395
|
-
ctx.arcTo(r.x, r.y, r.x + r.w, r.y, radius);
|
|
396
|
-
ctx.closePath();
|
|
397
|
-
}
|
|
398
|
-
ctx.lineWidth = stroke;
|
|
399
|
-
ctx.stroke();
|
|
400
|
-
}
|
|
401
|
-
function drawEllipse(ctx, start, end) {
|
|
402
|
-
const r = normalizeRect(start, end);
|
|
403
|
-
ctx.beginPath();
|
|
404
|
-
ctx.ellipse(r.x + r.w / 2, r.y + r.h / 2, r.w / 2, r.h / 2, 0, 0, Math.PI * 2);
|
|
405
|
-
ctx.stroke();
|
|
406
|
-
}
|
|
407
|
-
function drawArrow(ctx, start, end, stroke) {
|
|
408
|
-
ctx.beginPath();
|
|
409
|
-
ctx.moveTo(start.x, start.y);
|
|
410
|
-
ctx.lineTo(end.x, end.y);
|
|
411
|
-
ctx.stroke();
|
|
412
|
-
const headLen = Math.max(12, stroke * 4);
|
|
413
|
-
const angle = Math.atan2(end.y - start.y, end.x - start.x);
|
|
414
|
-
const a1 = angle + Math.PI - Math.PI / 7;
|
|
415
|
-
const a2 = angle + Math.PI + Math.PI / 7;
|
|
416
|
-
ctx.beginPath();
|
|
417
|
-
ctx.moveTo(end.x, end.y);
|
|
418
|
-
ctx.lineTo(end.x + headLen * Math.cos(a1), end.y + headLen * Math.sin(a1));
|
|
419
|
-
ctx.lineTo(end.x + headLen * Math.cos(a2), end.y + headLen * Math.sin(a2));
|
|
420
|
-
ctx.closePath();
|
|
421
|
-
ctx.fill();
|
|
768
|
+
function withinBounds(a, w, h) {
|
|
769
|
+
const bb = boundingBox(a);
|
|
770
|
+
return bb.x + bb.w > 0 && bb.y + bb.h > 0 && bb.x < w && bb.y < h;
|
|
422
771
|
}
|
|
423
772
|
function applyMosaic(ctx, rect) {
|
|
424
773
|
const x = Math.round(Math.max(0, rect.x));
|
|
@@ -1886,5 +2235,5 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
|
|
|
1886
2235
|
}
|
|
1887
2236
|
|
|
1888
2237
|
export { Preview, setPdfPreview };
|
|
1889
|
-
//# sourceMappingURL=chunk-
|
|
1890
|
-
//# sourceMappingURL=chunk-
|
|
2238
|
+
//# sourceMappingURL=chunk-LJ6DLGTY.js.map
|
|
2239
|
+
//# sourceMappingURL=chunk-LJ6DLGTY.js.map
|