react-os-shell 0.2.22 → 0.2.23
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-XUUCSCHL.js} +4 -4
- package/dist/{Files-XKNHOT5T.js.map → Files-XUUCSCHL.js.map} +1 -1
- package/dist/Preview-4M4HF2RC.js +6 -0
- package/dist/{Preview-MIILWJVE.js.map → Preview-4M4HF2RC.js.map} +1 -1
- package/dist/apps/index.js +4 -4
- package/dist/{chunk-XYCLLD46.js → chunk-4ZGTBQWZ.js} +3 -3
- package/dist/{chunk-XYCLLD46.js.map → chunk-4ZGTBQWZ.js.map} +1 -1
- package/dist/{chunk-4DW5YQ7Y.js → chunk-HQJRNCTU.js} +444 -230
- package/dist/chunk-HQJRNCTU.js.map +1 -0
- 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,55 @@ 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 displaySize = useMemo(() => {
|
|
33
|
+
if (!fitSize) return null;
|
|
34
|
+
return { w: fitSize.w * zoom, h: fitSize.h * zoom };
|
|
35
|
+
}, [fitSize, zoom]);
|
|
25
36
|
useEffect(() => {
|
|
26
37
|
const img = new Image();
|
|
27
38
|
img.crossOrigin = "anonymous";
|
|
28
39
|
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);
|
|
40
|
+
imageRef.current = img;
|
|
41
|
+
setImageSize({ w: img.naturalWidth, h: img.naturalHeight });
|
|
42
|
+
setAnnotations([]);
|
|
43
|
+
setSelectedId(null);
|
|
41
44
|
};
|
|
42
45
|
img.onerror = () => toast_default.error("Failed to load image");
|
|
43
46
|
img.src = src;
|
|
44
47
|
}, [src]);
|
|
45
48
|
useEffect(() => {
|
|
46
|
-
if (!
|
|
49
|
+
if (!imageSize) return;
|
|
47
50
|
const update = () => {
|
|
48
|
-
const c = canvasRef.current;
|
|
49
51
|
const wrap = wrapRef.current;
|
|
50
|
-
if (!
|
|
51
|
-
const
|
|
52
|
-
const availW = Math.max(0,
|
|
53
|
-
const availH = Math.max(0,
|
|
52
|
+
if (!wrap) return;
|
|
53
|
+
const r = wrap.getBoundingClientRect();
|
|
54
|
+
const availW = Math.max(0, r.width - 32);
|
|
55
|
+
const availH = Math.max(0, r.height - 32);
|
|
54
56
|
if (availW === 0 || availH === 0) return;
|
|
55
|
-
const ratio =
|
|
56
|
-
let w =
|
|
57
|
-
let h =
|
|
57
|
+
const ratio = imageSize.w / imageSize.h;
|
|
58
|
+
let w = imageSize.w;
|
|
59
|
+
let h = imageSize.h;
|
|
58
60
|
if (w > availW) {
|
|
59
61
|
w = availW;
|
|
60
62
|
h = w / ratio;
|
|
@@ -63,171 +65,201 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
63
65
|
h = availH;
|
|
64
66
|
w = h * ratio;
|
|
65
67
|
}
|
|
66
|
-
|
|
67
|
-
setDisplayScale(c.width / w);
|
|
68
|
+
setFitSize({ w, h });
|
|
68
69
|
};
|
|
69
70
|
update();
|
|
70
71
|
const ro = new ResizeObserver(update);
|
|
71
72
|
if (wrapRef.current) ro.observe(wrapRef.current);
|
|
72
73
|
return () => ro.disconnect();
|
|
73
|
-
}, [
|
|
74
|
-
const
|
|
74
|
+
}, [imageSize]);
|
|
75
|
+
const mosaicAnnos = useMemo(
|
|
76
|
+
() => annotations.filter((a) => a.type === "mosaic"),
|
|
77
|
+
[annotations]
|
|
78
|
+
);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const img = imageRef.current;
|
|
75
81
|
const c = canvasRef.current;
|
|
76
|
-
if (!c) return;
|
|
82
|
+
if (!img || !c || !imageSize) return;
|
|
83
|
+
c.width = imageSize.w;
|
|
84
|
+
c.height = imageSize.h;
|
|
77
85
|
const ctx = c.getContext("2d");
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
};
|
|
92
|
-
const clearOverlay = () => {
|
|
93
|
-
const o = overlayRef.current;
|
|
94
|
-
if (!o) return;
|
|
95
|
-
o.getContext("2d").clearRect(0, 0, o.width, o.height);
|
|
86
|
+
ctx.drawImage(img, 0, 0);
|
|
87
|
+
for (const m of mosaicAnnos) applyMosaic(ctx, m);
|
|
88
|
+
}, [imageSize, mosaicAnnos]);
|
|
89
|
+
const evToImage = (e) => {
|
|
90
|
+
const svg = svgRef.current;
|
|
91
|
+
if (!svg) return { x: 0, y: 0 };
|
|
92
|
+
const pt = svg.createSVGPoint();
|
|
93
|
+
pt.x = e.clientX;
|
|
94
|
+
pt.y = e.clientY;
|
|
95
|
+
const ctm = svg.getScreenCTM();
|
|
96
|
+
if (!ctm) return { x: 0, y: 0 };
|
|
97
|
+
const t = pt.matrixTransform(ctm.inverse());
|
|
98
|
+
return { x: t.x, y: t.y };
|
|
96
99
|
};
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
const handleAnnoPointerDown = (e, anno) => {
|
|
101
|
+
if (tool !== "select") return;
|
|
102
|
+
e.stopPropagation();
|
|
103
|
+
setSelectedId(anno.id);
|
|
104
|
+
const start = evToImage(e);
|
|
105
|
+
dragRef.current = { kind: "move", id: anno.id, start, original: anno };
|
|
106
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
102
107
|
};
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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([]);
|
|
108
|
+
const handleSvgPointerDown = (e) => {
|
|
109
|
+
if (tool === "select") {
|
|
110
|
+
setSelectedId(null);
|
|
111
|
+
return;
|
|
139
112
|
}
|
|
140
|
-
};
|
|
141
|
-
const onPointerDown = (e) => {
|
|
142
|
-
if (!imageReady) return;
|
|
143
|
-
const pos = evToCanvas(e);
|
|
144
113
|
if (tool === "text") {
|
|
145
|
-
|
|
114
|
+
const p = evToImage(e);
|
|
115
|
+
setPendingText({ x: p.x, y: p.y, value: "" });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const start = evToImage(e);
|
|
119
|
+
if (tool === "crop") {
|
|
120
|
+
dragRef.current = { kind: "crop", start };
|
|
121
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
146
122
|
return;
|
|
147
123
|
}
|
|
124
|
+
dragRef.current = { kind: "draw", start };
|
|
148
125
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
149
|
-
|
|
150
|
-
drawShapePreview(pos, pos);
|
|
126
|
+
setPreview(makeShape(tool, start, start, color, stroke));
|
|
151
127
|
};
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
128
|
+
const handleSvgPointerMove = (e) => {
|
|
129
|
+
const drag = dragRef.current;
|
|
130
|
+
if (!drag) return;
|
|
131
|
+
const p = evToImage(e);
|
|
132
|
+
if (drag.kind === "draw") {
|
|
133
|
+
setPreview(makeShape(tool, drag.start, p, color, stroke));
|
|
134
|
+
} else if (drag.kind === "crop") {
|
|
135
|
+
setPendingCrop(normalizeRect(drag.start, p));
|
|
136
|
+
} else if (drag.kind === "move") {
|
|
137
|
+
const dx = p.x - drag.start.x;
|
|
138
|
+
const dy = p.y - drag.start.y;
|
|
139
|
+
setAnnotations((prev) => prev.map((a) => a.id === drag.id ? translate(drag.original, dx, dy) : a));
|
|
140
|
+
}
|
|
157
141
|
};
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const { start, current } = dragRef.current;
|
|
142
|
+
const handleSvgPointerUp = (e) => {
|
|
143
|
+
const drag = dragRef.current;
|
|
144
|
+
if (!drag) return;
|
|
162
145
|
dragRef.current = null;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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();
|
|
146
|
+
e.currentTarget.releasePointerCapture?.(e.pointerId);
|
|
147
|
+
if (drag.kind === "draw") {
|
|
148
|
+
const p = preview;
|
|
149
|
+
if (!p) return;
|
|
150
|
+
const tooSmall = isTrivial(p);
|
|
151
|
+
setPreview(null);
|
|
152
|
+
if (tooSmall) return;
|
|
153
|
+
const anno = { ...p, id: newId() };
|
|
154
|
+
setAnnotations((prev) => [...prev, anno]);
|
|
155
|
+
setSelectedId(anno.id);
|
|
156
|
+
setTool("select");
|
|
157
|
+
} else if (drag.kind === "crop") ; else if (drag.kind === "move") ;
|
|
185
158
|
};
|
|
159
|
+
const handleAnnoDoubleClick = (anno) => {
|
|
160
|
+
if (anno.type !== "text") return;
|
|
161
|
+
setPendingText({ x: anno.x, y: anno.y, value: anno.text, editingId: anno.id });
|
|
162
|
+
};
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
const onKey = (ev) => {
|
|
165
|
+
if (!selectedId) return;
|
|
166
|
+
if (document.activeElement && /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName)) return;
|
|
167
|
+
if (ev.key === "Delete" || ev.key === "Backspace") {
|
|
168
|
+
ev.preventDefault();
|
|
169
|
+
setAnnotations((prev) => prev.filter((a) => a.id !== selectedId));
|
|
170
|
+
setSelectedId(null);
|
|
171
|
+
} else if (ev.key === "Escape") {
|
|
172
|
+
setSelectedId(null);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
window.addEventListener("keydown", onKey);
|
|
176
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
177
|
+
}, [selectedId]);
|
|
186
178
|
const commitText = () => {
|
|
187
179
|
if (!pendingText) return;
|
|
188
|
-
|
|
180
|
+
const value = pendingText.value.trim();
|
|
181
|
+
if (!value) {
|
|
182
|
+
if (pendingText.editingId) {
|
|
183
|
+
setAnnotations((prev) => prev.filter((a) => a.id !== pendingText.editingId));
|
|
184
|
+
}
|
|
189
185
|
setPendingText(null);
|
|
190
186
|
return;
|
|
191
187
|
}
|
|
192
|
-
pushHistory();
|
|
193
|
-
const c = canvasRef.current;
|
|
194
|
-
const ctx = c.getContext("2d");
|
|
195
188
|
const fontSize = Math.max(16, stroke * 6);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
189
|
+
if (pendingText.editingId) {
|
|
190
|
+
setAnnotations((prev) => prev.map(
|
|
191
|
+
(a) => a.id === pendingText.editingId && a.type === "text" ? { ...a, text: value } : a
|
|
192
|
+
));
|
|
193
|
+
} else {
|
|
194
|
+
const anno = {
|
|
195
|
+
id: newId(),
|
|
196
|
+
type: "text",
|
|
197
|
+
x: pendingText.x,
|
|
198
|
+
y: pendingText.y,
|
|
199
|
+
text: value,
|
|
200
|
+
color,
|
|
201
|
+
size: fontSize
|
|
202
|
+
};
|
|
203
|
+
setAnnotations((prev) => [...prev, anno]);
|
|
204
|
+
setSelectedId(anno.id);
|
|
205
|
+
setTool("select");
|
|
206
|
+
}
|
|
203
207
|
setPendingText(null);
|
|
204
208
|
};
|
|
205
209
|
const applyCrop = () => {
|
|
206
|
-
if (!pendingCrop) return;
|
|
207
|
-
const c = canvasRef.current;
|
|
208
|
-
pushHistory();
|
|
210
|
+
if (!pendingCrop || !imageRef.current || !imageSize) return;
|
|
209
211
|
const r = pendingCrop;
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
212
|
+
const tmp = document.createElement("canvas");
|
|
213
|
+
tmp.width = Math.round(r.w);
|
|
214
|
+
tmp.height = Math.round(r.h);
|
|
215
|
+
const tctx = tmp.getContext("2d");
|
|
216
|
+
const sourceCanvas = document.createElement("canvas");
|
|
217
|
+
sourceCanvas.width = imageSize.w;
|
|
218
|
+
sourceCanvas.height = imageSize.h;
|
|
219
|
+
const sctx = sourceCanvas.getContext("2d");
|
|
220
|
+
sctx.drawImage(imageRef.current, 0, 0);
|
|
221
|
+
for (const m of mosaicAnnos) applyMosaic(sctx, m);
|
|
222
|
+
tctx.drawImage(sourceCanvas, r.x, r.y, r.w, r.h, 0, 0, r.w, r.h);
|
|
223
|
+
const newImg = new Image();
|
|
224
|
+
newImg.onload = () => {
|
|
225
|
+
imageRef.current = newImg;
|
|
226
|
+
setImageSize({ w: newImg.naturalWidth, h: newImg.naturalHeight });
|
|
227
|
+
setAnnotations(
|
|
228
|
+
(prev) => prev.filter((a) => a.type !== "mosaic").map((a) => translate(a, -r.x, -r.y)).filter((a) => withinBounds(a, newImg.naturalWidth, newImg.naturalHeight))
|
|
229
|
+
);
|
|
230
|
+
setPendingCrop(null);
|
|
231
|
+
};
|
|
232
|
+
newImg.src = tmp.toDataURL("image/png");
|
|
222
233
|
};
|
|
223
|
-
const cancelCrop = () =>
|
|
224
|
-
|
|
225
|
-
|
|
234
|
+
const cancelCrop = () => setPendingCrop(null);
|
|
235
|
+
const undoLast = () => {
|
|
236
|
+
setAnnotations((prev) => prev.slice(0, -1));
|
|
237
|
+
setSelectedId(null);
|
|
226
238
|
};
|
|
227
|
-
const downloadAnnotated = () => {
|
|
239
|
+
const downloadAnnotated = async () => {
|
|
228
240
|
const c = canvasRef.current;
|
|
229
|
-
|
|
230
|
-
c
|
|
241
|
+
const svg = svgRef.current;
|
|
242
|
+
if (!c || !svg || !imageSize) return;
|
|
243
|
+
const out = document.createElement("canvas");
|
|
244
|
+
out.width = imageSize.w;
|
|
245
|
+
out.height = imageSize.h;
|
|
246
|
+
const octx = out.getContext("2d");
|
|
247
|
+
octx.drawImage(c, 0, 0);
|
|
248
|
+
const clone = svg.cloneNode(true);
|
|
249
|
+
clone.querySelectorAll("[data-chrome]").forEach((n) => n.remove());
|
|
250
|
+
const xml = new XMLSerializer().serializeToString(clone);
|
|
251
|
+
const svgBlob = new Blob([xml], { type: "image/svg+xml;charset=utf-8" });
|
|
252
|
+
const svgUrl = URL.createObjectURL(svgBlob);
|
|
253
|
+
const svgImg = new Image();
|
|
254
|
+
await new Promise((resolve, reject) => {
|
|
255
|
+
svgImg.onload = () => resolve();
|
|
256
|
+
svgImg.onerror = reject;
|
|
257
|
+
svgImg.src = svgUrl;
|
|
258
|
+
}).catch(() => {
|
|
259
|
+
});
|
|
260
|
+
octx.drawImage(svgImg, 0, 0, imageSize.w, imageSize.h);
|
|
261
|
+
URL.revokeObjectURL(svgUrl);
|
|
262
|
+
out.toBlob((blob) => {
|
|
231
263
|
if (!blob) {
|
|
232
264
|
toast_default.error("Failed to export");
|
|
233
265
|
return;
|
|
@@ -242,6 +274,7 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
242
274
|
}, "image/png");
|
|
243
275
|
};
|
|
244
276
|
const tools = useMemo(() => [
|
|
277
|
+
{ 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
278
|
{ 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
279
|
{ 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
280
|
{ 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 +283,16 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
250
283
|
{ 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
284
|
], []);
|
|
252
285
|
const btnClass = (active) => `p-1.5 rounded transition-colors ${active ? "bg-blue-500 text-white" : "text-gray-700 hover:bg-gray-200"}`;
|
|
286
|
+
const scale = displaySize && imageSize ? displaySize.w / imageSize.w : 1;
|
|
253
287
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full bg-gray-100", children: [
|
|
254
288
|
/* @__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
289
|
tools.map((t) => /* @__PURE__ */ jsx(
|
|
256
290
|
"button",
|
|
257
291
|
{
|
|
258
|
-
onClick: () =>
|
|
292
|
+
onClick: () => {
|
|
293
|
+
setTool(t.id);
|
|
294
|
+
setSelectedId(null);
|
|
295
|
+
},
|
|
259
296
|
title: t.label,
|
|
260
297
|
className: btnClass(tool === t.id),
|
|
261
298
|
children: t.icon
|
|
@@ -266,7 +303,12 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
266
303
|
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: COLORS.map((c) => /* @__PURE__ */ jsx(
|
|
267
304
|
"button",
|
|
268
305
|
{
|
|
269
|
-
onClick: () =>
|
|
306
|
+
onClick: () => {
|
|
307
|
+
setColor(c);
|
|
308
|
+
if (selectedId) {
|
|
309
|
+
setAnnotations((prev) => prev.map((a) => a.id === selectedId && a.type !== "mosaic" ? { ...a, color: c } : a));
|
|
310
|
+
}
|
|
311
|
+
},
|
|
270
312
|
title: c,
|
|
271
313
|
className: `h-5 w-5 rounded-full border ${color === c ? "ring-2 ring-blue-500 ring-offset-1" : "border-gray-300"}`,
|
|
272
314
|
style: { background: c }
|
|
@@ -291,7 +333,15 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
291
333
|
/* @__PURE__ */ jsx("span", { className: "tabular-nums w-4 text-right", children: stroke })
|
|
292
334
|
] }),
|
|
293
335
|
/* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
|
|
294
|
-
/* @__PURE__ */ jsx("button", { onClick:
|
|
336
|
+
/* @__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" }),
|
|
337
|
+
/* @__PURE__ */ jsxs("span", { className: "text-[11px] text-gray-600 tabular-nums w-10 text-center", children: [
|
|
338
|
+
Math.round(zoom * 100),
|
|
339
|
+
"%"
|
|
340
|
+
] }),
|
|
341
|
+
/* @__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: "+" }),
|
|
342
|
+
/* @__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" }),
|
|
343
|
+
/* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
|
|
344
|
+
/* @__PURE__ */ jsx("button", { onClick: undoLast, disabled: annotations.length === 0, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 disabled:opacity-30 text-gray-700", children: "Undo" }),
|
|
295
345
|
/* @__PURE__ */ jsx("button", { onClick: downloadAnnotated, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Save" }),
|
|
296
346
|
/* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
|
|
297
347
|
pendingCrop && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -301,39 +351,54 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
301
351
|
/* @__PURE__ */ jsx("button", { onClick: onClose, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Exit" })
|
|
302
352
|
] })
|
|
303
353
|
] }),
|
|
304
|
-
/* @__PURE__ */ jsx("div", { ref: wrapRef, className: "flex-1 overflow-
|
|
354
|
+
/* @__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
355
|
"div",
|
|
306
356
|
{
|
|
307
|
-
className: "relative shadow-lg rounded overflow-hidden",
|
|
308
|
-
style:
|
|
357
|
+
className: "relative shadow-lg rounded overflow-hidden bg-white shrink-0",
|
|
358
|
+
style: { width: displaySize.w, height: displaySize.h },
|
|
309
359
|
children: [
|
|
310
360
|
/* @__PURE__ */ jsx(
|
|
311
361
|
"canvas",
|
|
312
362
|
{
|
|
313
363
|
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" }
|
|
364
|
+
style: { position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }
|
|
322
365
|
}
|
|
323
366
|
),
|
|
324
|
-
/* @__PURE__ */
|
|
325
|
-
"
|
|
367
|
+
/* @__PURE__ */ jsxs(
|
|
368
|
+
"svg",
|
|
326
369
|
{
|
|
327
|
-
ref:
|
|
370
|
+
ref: svgRef,
|
|
371
|
+
viewBox: `0 0 ${imageSize.w} ${imageSize.h}`,
|
|
372
|
+
preserveAspectRatio: "none",
|
|
328
373
|
style: {
|
|
329
374
|
position: "absolute",
|
|
330
|
-
|
|
331
|
-
left: 0,
|
|
375
|
+
inset: 0,
|
|
332
376
|
width: "100%",
|
|
333
377
|
height: "100%",
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
378
|
+
touchAction: "none",
|
|
379
|
+
cursor: tool === "select" ? "default" : tool === "text" ? "text" : "crosshair"
|
|
380
|
+
},
|
|
381
|
+
onPointerDown: handleSvgPointerDown,
|
|
382
|
+
onPointerMove: handleSvgPointerMove,
|
|
383
|
+
onPointerUp: handleSvgPointerUp,
|
|
384
|
+
onPointerCancel: () => {
|
|
385
|
+
dragRef.current = null;
|
|
386
|
+
setPreview(null);
|
|
387
|
+
},
|
|
388
|
+
children: [
|
|
389
|
+
annotations.map((a) => /* @__PURE__ */ jsx(
|
|
390
|
+
AnnotationView,
|
|
391
|
+
{
|
|
392
|
+
anno: a,
|
|
393
|
+
selected: selectedId === a.id,
|
|
394
|
+
onPointerDown: (e) => handleAnnoPointerDown(e, a),
|
|
395
|
+
onDoubleClick: () => handleAnnoDoubleClick(a)
|
|
396
|
+
},
|
|
397
|
+
a.id
|
|
398
|
+
)),
|
|
399
|
+
preview && /* @__PURE__ */ jsx(AnnotationView, { anno: { ...preview, id: "__preview" }, preview: true }),
|
|
400
|
+
pendingCrop && /* @__PURE__ */ jsx(CropOverlay, { rect: pendingCrop, imageSize })
|
|
401
|
+
]
|
|
337
402
|
}
|
|
338
403
|
),
|
|
339
404
|
pendingText && /* @__PURE__ */ jsx(
|
|
@@ -341,11 +406,11 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
341
406
|
{
|
|
342
407
|
style: {
|
|
343
408
|
position: "absolute",
|
|
344
|
-
left: `${pendingText.x
|
|
345
|
-
top: `${pendingText.y
|
|
346
|
-
transform: "translateY(-2px)"
|
|
409
|
+
left: `${pendingText.x * scale}px`,
|
|
410
|
+
top: `${pendingText.y * scale}px`,
|
|
411
|
+
transform: "translateY(-2px)",
|
|
412
|
+
zIndex: 5
|
|
347
413
|
},
|
|
348
|
-
className: "z-10",
|
|
349
414
|
children: /* @__PURE__ */ jsx(
|
|
350
415
|
"textarea",
|
|
351
416
|
{
|
|
@@ -371,9 +436,193 @@ function ImageAnnotator({ src, filename, onClose }) {
|
|
|
371
436
|
)
|
|
372
437
|
]
|
|
373
438
|
}
|
|
374
|
-
) })
|
|
439
|
+
) }),
|
|
440
|
+
/* @__PURE__ */ jsx("div", { className: "px-3 py-1.5 border-t border-gray-200 bg-white text-[11px] text-gray-500 shrink-0", children: tool === "select" ? selectedId ? "Drag to move. Delete / Backspace removes. Click outside to deselect." : "Tap a shape to select 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." })
|
|
441
|
+
] });
|
|
442
|
+
}
|
|
443
|
+
function AnnotationView({ anno, selected, preview, onPointerDown, onDoubleClick }) {
|
|
444
|
+
const dim = boundingBox(anno);
|
|
445
|
+
const interactive = onPointerDown ? { onPointerDown, style: { cursor: "move" } } : {};
|
|
446
|
+
const dblc = onDoubleClick ? { onDoubleClick } : {};
|
|
447
|
+
let body;
|
|
448
|
+
if (anno.type === "rect") {
|
|
449
|
+
const radius = Math.min(anno.w, anno.h, 16) * 0.4;
|
|
450
|
+
body = /* @__PURE__ */ jsx(
|
|
451
|
+
"rect",
|
|
452
|
+
{
|
|
453
|
+
x: anno.x,
|
|
454
|
+
y: anno.y,
|
|
455
|
+
width: anno.w,
|
|
456
|
+
height: anno.h,
|
|
457
|
+
rx: radius,
|
|
458
|
+
ry: radius,
|
|
459
|
+
fill: "none",
|
|
460
|
+
stroke: anno.color,
|
|
461
|
+
strokeWidth: anno.stroke,
|
|
462
|
+
strokeLinecap: "round",
|
|
463
|
+
strokeLinejoin: "round",
|
|
464
|
+
...interactive
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
} else if (anno.type === "circle") {
|
|
468
|
+
body = /* @__PURE__ */ jsx(
|
|
469
|
+
"ellipse",
|
|
470
|
+
{
|
|
471
|
+
cx: anno.x + anno.w / 2,
|
|
472
|
+
cy: anno.y + anno.h / 2,
|
|
473
|
+
rx: anno.w / 2,
|
|
474
|
+
ry: anno.h / 2,
|
|
475
|
+
fill: "none",
|
|
476
|
+
stroke: anno.color,
|
|
477
|
+
strokeWidth: anno.stroke,
|
|
478
|
+
...interactive
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
} else if (anno.type === "arrow") {
|
|
482
|
+
body = /* @__PURE__ */ jsx(ArrowShape, { anno, interactive });
|
|
483
|
+
} else if (anno.type === "mosaic") {
|
|
484
|
+
body = /* @__PURE__ */ jsx(
|
|
485
|
+
"rect",
|
|
486
|
+
{
|
|
487
|
+
x: anno.x,
|
|
488
|
+
y: anno.y,
|
|
489
|
+
width: anno.w,
|
|
490
|
+
height: anno.h,
|
|
491
|
+
fill: "rgba(0,0,0,0.001)",
|
|
492
|
+
stroke: selected ? "#3b82f6" : "none",
|
|
493
|
+
strokeWidth: selected ? 2 : 0,
|
|
494
|
+
strokeDasharray: selected ? "6,4" : void 0,
|
|
495
|
+
...interactive
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
} else {
|
|
499
|
+
body = /* @__PURE__ */ jsx(
|
|
500
|
+
"text",
|
|
501
|
+
{
|
|
502
|
+
x: anno.x,
|
|
503
|
+
y: anno.y + anno.size,
|
|
504
|
+
fill: anno.color,
|
|
505
|
+
fontSize: anno.size,
|
|
506
|
+
fontWeight: 600,
|
|
507
|
+
fontFamily: "-apple-system, system-ui, sans-serif",
|
|
508
|
+
style: { userSelect: "none" },
|
|
509
|
+
...interactive,
|
|
510
|
+
...dblc,
|
|
511
|
+
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))
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return /* @__PURE__ */ jsxs("g", { children: [
|
|
516
|
+
body,
|
|
517
|
+
selected && !preview && /* @__PURE__ */ jsx(
|
|
518
|
+
"rect",
|
|
519
|
+
{
|
|
520
|
+
"data-chrome": "selection",
|
|
521
|
+
x: dim.x - 4,
|
|
522
|
+
y: dim.y - 4,
|
|
523
|
+
width: dim.w + 8,
|
|
524
|
+
height: dim.h + 8,
|
|
525
|
+
fill: "none",
|
|
526
|
+
stroke: "#3b82f6",
|
|
527
|
+
strokeWidth: 2,
|
|
528
|
+
strokeDasharray: "6,4",
|
|
529
|
+
pointerEvents: "none"
|
|
530
|
+
}
|
|
531
|
+
)
|
|
532
|
+
] });
|
|
533
|
+
}
|
|
534
|
+
function ArrowShape({ anno, interactive }) {
|
|
535
|
+
const headLen = Math.max(12, anno.stroke * 4);
|
|
536
|
+
const angle = Math.atan2(anno.y2 - anno.y1, anno.x2 - anno.x1);
|
|
537
|
+
const a1 = angle + Math.PI - Math.PI / 7;
|
|
538
|
+
const a2 = angle + Math.PI + Math.PI / 7;
|
|
539
|
+
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`;
|
|
540
|
+
return /* @__PURE__ */ jsxs("g", { ...interactive, children: [
|
|
541
|
+
/* @__PURE__ */ jsx(
|
|
542
|
+
"line",
|
|
543
|
+
{
|
|
544
|
+
x1: anno.x1,
|
|
545
|
+
y1: anno.y1,
|
|
546
|
+
x2: anno.x2,
|
|
547
|
+
y2: anno.y2,
|
|
548
|
+
stroke: anno.color,
|
|
549
|
+
strokeWidth: anno.stroke,
|
|
550
|
+
strokeLinecap: "round"
|
|
551
|
+
}
|
|
552
|
+
),
|
|
553
|
+
/* @__PURE__ */ jsx("path", { d: head, fill: anno.color })
|
|
375
554
|
] });
|
|
376
555
|
}
|
|
556
|
+
function CropOverlay({ rect, imageSize }) {
|
|
557
|
+
return /* @__PURE__ */ jsxs("g", { pointerEvents: "none", children: [
|
|
558
|
+
/* @__PURE__ */ jsx(
|
|
559
|
+
"path",
|
|
560
|
+
{
|
|
561
|
+
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`,
|
|
562
|
+
fill: "rgba(0,0,0,0.45)",
|
|
563
|
+
fillRule: "evenodd"
|
|
564
|
+
}
|
|
565
|
+
),
|
|
566
|
+
/* @__PURE__ */ jsx(
|
|
567
|
+
"rect",
|
|
568
|
+
{
|
|
569
|
+
x: rect.x,
|
|
570
|
+
y: rect.y,
|
|
571
|
+
width: rect.w,
|
|
572
|
+
height: rect.h,
|
|
573
|
+
fill: "none",
|
|
574
|
+
stroke: "#fff",
|
|
575
|
+
strokeWidth: 2,
|
|
576
|
+
strokeDasharray: "8,6"
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
] });
|
|
580
|
+
}
|
|
581
|
+
function makeShape(tool, start, end, color, stroke) {
|
|
582
|
+
if (tool === "rect" || tool === "circle") {
|
|
583
|
+
const r = normalizeRect(start, end);
|
|
584
|
+
return { id: "", type: tool, x: r.x, y: r.y, w: r.w, h: r.h, color, stroke };
|
|
585
|
+
}
|
|
586
|
+
if (tool === "arrow") {
|
|
587
|
+
return { id: "", type: "arrow", x1: start.x, y1: start.y, x2: end.x, y2: end.y, color, stroke };
|
|
588
|
+
}
|
|
589
|
+
if (tool === "mosaic") {
|
|
590
|
+
const r = normalizeRect(start, end);
|
|
591
|
+
return { id: "", type: "mosaic", x: r.x, y: r.y, w: r.w, h: r.h };
|
|
592
|
+
}
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
function isTrivial(a) {
|
|
596
|
+
if (a.type === "arrow") {
|
|
597
|
+
return Math.abs(a.x2 - a.x1) < 4 && Math.abs(a.y2 - a.y1) < 4;
|
|
598
|
+
}
|
|
599
|
+
if ("w" in a && "h" in a) {
|
|
600
|
+
return a.w < 4 || a.h < 4;
|
|
601
|
+
}
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
function translate(a, dx, dy) {
|
|
605
|
+
if (a.type === "arrow") {
|
|
606
|
+
return { ...a, x1: a.x1 + dx, y1: a.y1 + dy, x2: a.x2 + dx, y2: a.y2 + dy };
|
|
607
|
+
}
|
|
608
|
+
if (a.type === "text") {
|
|
609
|
+
return { ...a, x: a.x + dx, y: a.y + dy };
|
|
610
|
+
}
|
|
611
|
+
return { ...a, x: a.x + dx, y: a.y + dy };
|
|
612
|
+
}
|
|
613
|
+
function boundingBox(a) {
|
|
614
|
+
if (a.type === "arrow") {
|
|
615
|
+
const x = Math.min(a.x1, a.x2);
|
|
616
|
+
const y = Math.min(a.y1, a.y2);
|
|
617
|
+
return { x, y, w: Math.abs(a.x2 - a.x1), h: Math.abs(a.y2 - a.y1) };
|
|
618
|
+
}
|
|
619
|
+
if (a.type === "text") {
|
|
620
|
+
const lines = a.text.split("\n");
|
|
621
|
+
const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
|
|
622
|
+
return { x: a.x, y: a.y, w: longest * a.size * 0.55, h: lines.length * a.size * 1.2 };
|
|
623
|
+
}
|
|
624
|
+
return { x: a.x, y: a.y, w: a.w, h: a.h };
|
|
625
|
+
}
|
|
377
626
|
function normalizeRect(a, b) {
|
|
378
627
|
const x = Math.min(a.x, b.x);
|
|
379
628
|
const y = Math.min(a.y, b.y);
|
|
@@ -381,44 +630,9 @@ function normalizeRect(a, b) {
|
|
|
381
630
|
const h = Math.abs(a.y - b.y);
|
|
382
631
|
return { x, y, w, h };
|
|
383
632
|
}
|
|
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();
|
|
633
|
+
function withinBounds(a, w, h) {
|
|
634
|
+
const bb = boundingBox(a);
|
|
635
|
+
return bb.x + bb.w > 0 && bb.y + bb.h > 0 && bb.x < w && bb.y < h;
|
|
422
636
|
}
|
|
423
637
|
function applyMosaic(ctx, rect) {
|
|
424
638
|
const x = Math.round(Math.max(0, rect.x));
|
|
@@ -1886,5 +2100,5 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
|
|
|
1886
2100
|
}
|
|
1887
2101
|
|
|
1888
2102
|
export { Preview, setPdfPreview };
|
|
1889
|
-
//# sourceMappingURL=chunk-
|
|
1890
|
-
//# sourceMappingURL=chunk-
|
|
2103
|
+
//# sourceMappingURL=chunk-HQJRNCTU.js.map
|
|
2104
|
+
//# sourceMappingURL=chunk-HQJRNCTU.js.map
|