canvu-react 0.3.5

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/camera-BwQjm5oh.d.cts +50 -0
  4. package/dist/camera-KwCYYPhm.d.ts +50 -0
  5. package/dist/chatbot.cjs +221 -0
  6. package/dist/chatbot.cjs.map +1 -0
  7. package/dist/chatbot.d.cts +36 -0
  8. package/dist/chatbot.d.ts +36 -0
  9. package/dist/chatbot.js +218 -0
  10. package/dist/chatbot.js.map +1 -0
  11. package/dist/index.cjs +1920 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.cts +276 -0
  14. package/dist/index.d.ts +276 -0
  15. package/dist/index.js +1867 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/native.cjs +2572 -0
  18. package/dist/native.cjs.map +1 -0
  19. package/dist/native.d.cts +217 -0
  20. package/dist/native.d.ts +217 -0
  21. package/dist/native.js +2562 -0
  22. package/dist/native.js.map +1 -0
  23. package/dist/react.cjs +8540 -0
  24. package/dist/react.cjs.map +1 -0
  25. package/dist/react.d.cts +481 -0
  26. package/dist/react.d.ts +481 -0
  27. package/dist/react.js +8492 -0
  28. package/dist/react.js.map +1 -0
  29. package/dist/realtime.cjs +2338 -0
  30. package/dist/realtime.cjs.map +1 -0
  31. package/dist/realtime.d.cts +309 -0
  32. package/dist/realtime.d.ts +309 -0
  33. package/dist/realtime.js +2317 -0
  34. package/dist/realtime.js.map +1 -0
  35. package/dist/shape-builders-DTYvub8W.d.ts +93 -0
  36. package/dist/shape-builders-DxPoOecg.d.cts +93 -0
  37. package/dist/tldraw.cjs +1948 -0
  38. package/dist/tldraw.cjs.map +1 -0
  39. package/dist/tldraw.d.cts +98 -0
  40. package/dist/tldraw.d.ts +98 -0
  41. package/dist/tldraw.js +1941 -0
  42. package/dist/tldraw.js.map +1 -0
  43. package/dist/types--ALu1mF-.d.ts +356 -0
  44. package/dist/types-B58i5k-u.d.cts +35 -0
  45. package/dist/types-CB0TZZuk.d.cts +157 -0
  46. package/dist/types-CB0TZZuk.d.ts +157 -0
  47. package/dist/types-D1ftVsOQ.d.cts +356 -0
  48. package/dist/types-DgEArHkA.d.ts +35 -0
  49. package/package.json +103 -0
package/dist/native.js ADDED
@@ -0,0 +1,2562 @@
1
+ import { Group, RoundedRect, Circle, Line, vec, Path, matchFont, Text, Canvas, Rect, Oval } from '@shopify/react-native-skia';
2
+ import { memo, forwardRef, useState, useRef, useCallback, useMemo, useImperativeHandle } from 'react';
3
+ import getStroke from 'perfect-freehand';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
+ import { PanResponder, View } from 'react-native';
6
+
7
+ // src/native/NativeInteractionOverlay.tsx
8
+
9
+ // src/math/rect.ts
10
+ function rectsIntersect(a, b) {
11
+ return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
12
+ }
13
+ function normalizeRect(r) {
14
+ const x0 = r.width >= 0 ? r.x : r.x + r.width;
15
+ const y0 = r.height >= 0 ? r.y : r.y + r.height;
16
+ return {
17
+ x: x0,
18
+ y: y0,
19
+ width: Math.abs(r.width),
20
+ height: Math.abs(r.height)
21
+ };
22
+ }
23
+
24
+ // src/math/item-transform.ts
25
+ function getItemRotationRad(item) {
26
+ return item.rotation ?? 0;
27
+ }
28
+ function itemLocalToWorld(lx, ly, itemX, itemY, w, h, rotationRad) {
29
+ const c = { x: w / 2, y: h / 2 };
30
+ const dlx = lx - c.x;
31
+ const dly = ly - c.y;
32
+ const cos = Math.cos(rotationRad);
33
+ const sin = Math.sin(rotationRad);
34
+ return {
35
+ x: itemX + c.x + cos * dlx - sin * dly,
36
+ y: itemY + c.y + sin * dlx + cos * dly
37
+ };
38
+ }
39
+ function worldToItemLocal(wx, wy, itemX, itemY, w, h, rotationRad) {
40
+ const c = { x: w / 2, y: h / 2 };
41
+ const vx = wx - itemX;
42
+ const vy = wy - itemY;
43
+ const dx = vx - c.x;
44
+ const dy = vy - c.y;
45
+ const cos = Math.cos(-rotationRad);
46
+ const sin = Math.sin(-rotationRad);
47
+ const lx = cos * dx - sin * dy;
48
+ const ly = sin * dx + cos * dy;
49
+ return { x: c.x + lx, y: c.y + ly };
50
+ }
51
+ function boundsAabbForRotatedItem(item) {
52
+ const rot = getItemRotationRad(item);
53
+ if (Math.abs(rot) < 1e-12 && item.bounds.width >= 0 && item.bounds.height >= 0) {
54
+ return item.bounds;
55
+ }
56
+ const r = normalizeRect(item.bounds);
57
+ if (Math.abs(rot) < 1e-12) {
58
+ return r;
59
+ }
60
+ const corners = [
61
+ [0, 0],
62
+ [r.width, 0],
63
+ [r.width, r.height],
64
+ [0, r.height]
65
+ ];
66
+ let minX = Infinity;
67
+ let minY = Infinity;
68
+ let maxX = -Infinity;
69
+ let maxY = -Infinity;
70
+ for (const [lx, ly] of corners) {
71
+ const p = itemLocalToWorld(lx, ly, item.x, item.y, r.width, r.height, rot);
72
+ minX = Math.min(minX, p.x);
73
+ minY = Math.min(minY, p.y);
74
+ maxX = Math.max(maxX, p.x);
75
+ maxY = Math.max(maxY, p.y);
76
+ }
77
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
78
+ }
79
+
80
+ // src/interaction/resize-handles.ts
81
+ function getHandleWorldPosition(bounds, id) {
82
+ const r = normalizeRect(bounds);
83
+ const cx = r.x + r.width / 2;
84
+ const cy = r.y + r.height / 2;
85
+ switch (id) {
86
+ case "nw":
87
+ return { x: r.x, y: r.y };
88
+ case "n":
89
+ return { x: cx, y: r.y };
90
+ case "ne":
91
+ return { x: r.x + r.width, y: r.y };
92
+ case "e":
93
+ return { x: r.x + r.width, y: cy };
94
+ case "se":
95
+ return { x: r.x + r.width, y: r.y + r.height };
96
+ case "s":
97
+ return { x: cx, y: r.y + r.height };
98
+ case "sw":
99
+ return { x: r.x, y: r.y + r.height };
100
+ case "w":
101
+ return { x: r.x, y: cy };
102
+ }
103
+ }
104
+ function getHandleWorldPositionRotated(bounds, handle, rotationRad) {
105
+ const r = normalizeRect(bounds);
106
+ const p = getHandleWorldPosition(
107
+ { x: 0, y: 0, width: r.width, height: r.height },
108
+ handle
109
+ );
110
+ return itemLocalToWorld(p.x, p.y, r.x, r.y, r.width, r.height, rotationRad);
111
+ }
112
+ function getRotationHandleWorldPosition(bounds, rotationRad, handleOffsetWorld) {
113
+ const r = normalizeRect(bounds);
114
+ return itemLocalToWorld(
115
+ r.width / 2,
116
+ -handleOffsetWorld,
117
+ r.x,
118
+ r.y,
119
+ r.width,
120
+ r.height,
121
+ rotationRad
122
+ );
123
+ }
124
+
125
+ // src/scene/freehand-path.ts
126
+ function dedupeFreehandPoints(points, minDist) {
127
+ if (points.length <= 2) {
128
+ return points.map((p) => ({ ...p }));
129
+ }
130
+ const minSq = minDist * minDist;
131
+ const first = points[0];
132
+ if (!first) return [];
133
+ const out = [{ ...first }];
134
+ for (let i = 1; i < points.length - 1; i++) {
135
+ const p = points[i];
136
+ const last = out[out.length - 1];
137
+ if (!p || !last) continue;
138
+ const dx = p.x - last.x;
139
+ const dy = p.y - last.y;
140
+ if (dx * dx + dy * dy >= minSq) {
141
+ out.push({ ...p });
142
+ }
143
+ }
144
+ const end = points[points.length - 1];
145
+ const lastKept = out[out.length - 1];
146
+ if (!end || !lastKept) return out;
147
+ if ((end.x - lastKept.x) ** 2 + (end.y - lastKept.y) ** 2 > 1e-12) {
148
+ out.push({ ...end });
149
+ }
150
+ return out;
151
+ }
152
+ function smoothFreehandPointsToPathD(points) {
153
+ const n = points.length;
154
+ if (n === 0) return "";
155
+ if (n === 1) {
156
+ const p = points[0];
157
+ if (!p) return "";
158
+ return `M ${p.x} ${p.y}`;
159
+ }
160
+ if (n === 2) {
161
+ const a = points[0];
162
+ const b = points[1];
163
+ if (!a || !b) return "";
164
+ return `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
165
+ }
166
+ const p0 = points[0];
167
+ if (!p0) return "";
168
+ let d = `M ${p0.x} ${p0.y}`;
169
+ let i = 1;
170
+ for (; i < n - 2; i++) {
171
+ const pi = points[i];
172
+ const pi1 = points[i + 1];
173
+ if (!pi || !pi1) continue;
174
+ const xc = (pi.x + pi1.x) / 2;
175
+ const yc = (pi.y + pi1.y) / 2;
176
+ d += ` Q ${pi.x} ${pi.y} ${xc} ${yc}`;
177
+ }
178
+ const pLast = points[i];
179
+ const pEnd = points[i + 1];
180
+ if (!pLast || !pEnd) return d;
181
+ d += ` Q ${pLast.x} ${pLast.y} ${pEnd.x} ${pEnd.y}`;
182
+ return d;
183
+ }
184
+ function outlineStrokeToClosedPathD(outline) {
185
+ const len = outline.length;
186
+ if (len === 0) return "";
187
+ const first = outline[0];
188
+ if (!first) return "";
189
+ if (len < 3) {
190
+ let d2 = `M ${first[0]} ${first[1]}`;
191
+ for (let i = 1; i < len; i++) {
192
+ const pt = outline[i];
193
+ if (!pt) continue;
194
+ d2 += ` L ${pt[0]} ${pt[1]}`;
195
+ }
196
+ return `${d2} Z`;
197
+ }
198
+ let d = `M ${first[0]} ${first[1]} Q`;
199
+ for (let i = 0; i < len; i++) {
200
+ const p0 = outline[i];
201
+ const p1 = outline[(i + 1) % len];
202
+ if (!p0 || !p1) continue;
203
+ d += ` ${p0[0]} ${p0[1]} ${(p0[0] + p1[0]) / 2} ${(p0[1] + p1[1]) / 2}`;
204
+ }
205
+ return `${d} Z`;
206
+ }
207
+
208
+ // src/scene/custom-shape.ts
209
+ function buildCustomShapeChildrenSvg(inner, intrinsic, bounds) {
210
+ const b = normalizeRect(bounds);
211
+ const sx = b.width / intrinsic.width;
212
+ const sy = b.height / intrinsic.height;
213
+ return `<g transform="scale(${sx},${sy})">${inner}</g>`;
214
+ }
215
+
216
+ // src/scene/text-svg.ts
217
+ function escapeSvgTextContent(s) {
218
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
219
+ }
220
+ function escapeHtmlText(s) {
221
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
222
+ }
223
+ var DEFAULT_TEXT_FONT_SIZE = 18;
224
+ var LINE_HEIGHT_RATIO = 22 / 18;
225
+ var FIRST_LINE_BASELINE_RATIO = 24 / 18;
226
+ var PLACEHOLDER = "Tap to type";
227
+ var MIN_TEXT_BOX_W = 40;
228
+ var MIN_TEXT_BOX_H = 36;
229
+ var TEXT_PAD_X = 4;
230
+ var MAX_TEXT_MEASURE_CACHE_ENTRIES = 2e3;
231
+ var sharedMeasureContext;
232
+ var textMeasureCache = /* @__PURE__ */ new Map();
233
+ function getSharedMeasureContext() {
234
+ if (sharedMeasureContext !== void 0) {
235
+ return sharedMeasureContext;
236
+ }
237
+ if (typeof document === "undefined") {
238
+ sharedMeasureContext = null;
239
+ return sharedMeasureContext;
240
+ }
241
+ const canvas = document.createElement("canvas");
242
+ sharedMeasureContext = canvas.getContext("2d");
243
+ return sharedMeasureContext;
244
+ }
245
+ function textMeasureCacheKey(content, fontSize) {
246
+ return `${fontSize}
247
+ ${content}`;
248
+ }
249
+ function cacheMeasuredBounds(key, bounds) {
250
+ if (textMeasureCache.size >= MAX_TEXT_MEASURE_CACHE_ENTRIES) {
251
+ textMeasureCache.clear();
252
+ }
253
+ textMeasureCache.set(key, bounds);
254
+ }
255
+ function lineHeightFor(fontSize) {
256
+ return fontSize * LINE_HEIGHT_RATIO;
257
+ }
258
+ function firstLineBaselineY(fontSize) {
259
+ return fontSize * FIRST_LINE_BASELINE_RATIO;
260
+ }
261
+ function measureTextBoundsLocal(content, fontSize = DEFAULT_TEXT_FONT_SIZE) {
262
+ const cacheKey = textMeasureCacheKey(content, fontSize);
263
+ const cached = textMeasureCache.get(cacheKey);
264
+ if (cached) {
265
+ return cached;
266
+ }
267
+ const lh = lineHeightFor(fontSize);
268
+ const baselineY = firstLineBaselineY(fontSize);
269
+ const trimmed = content.trim();
270
+ const lines = trimmed.length === 0 ? [PLACEHOLDER] : content.split("\n");
271
+ let maxInnerW = 0;
272
+ const ctx = getSharedMeasureContext();
273
+ if (ctx) {
274
+ ctx.font = `${fontSize}px system-ui, sans-serif`;
275
+ for (const line of lines) {
276
+ const toMeasure = trimmed.length === 0 ? PLACEHOLDER : line.length === 0 ? " " : line;
277
+ maxInnerW = Math.max(maxInnerW, ctx.measureText(toMeasure).width);
278
+ }
279
+ }
280
+ if (maxInnerW === 0) {
281
+ for (const line of lines) {
282
+ const toMeasure = trimmed.length === 0 ? PLACEHOLDER : line.length === 0 ? " " : line;
283
+ maxInnerW = Math.max(maxInnerW, toMeasure.length * fontSize * 0.52);
284
+ }
285
+ }
286
+ const minW = Math.max(MIN_TEXT_BOX_W, TEXT_PAD_X * 2 + fontSize);
287
+ const width = Math.max(minW, TEXT_PAD_X * 2 + maxInnerW);
288
+ const height = Math.max(
289
+ MIN_TEXT_BOX_H,
290
+ baselineY + (lines.length - 1) * lh + Math.max(8, fontSize * 0.35)
291
+ );
292
+ const measured = { width, height };
293
+ cacheMeasuredBounds(cacheKey, measured);
294
+ return measured;
295
+ }
296
+ function buildTextSvg(content, _width, _height, fillColor = "#2563eb", fontSize = DEFAULT_TEXT_FONT_SIZE) {
297
+ const lh = lineHeightFor(fontSize);
298
+ const y0 = firstLineBaselineY(fontSize);
299
+ const trimmed = content.trim();
300
+ if (trimmed.length === 0) {
301
+ return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="system-ui,sans-serif" fill="#94a3b8" font-style="italic">${escapeSvgTextContent(PLACEHOLDER)}</text>`;
302
+ }
303
+ const lines = content.split("\n");
304
+ if (lines.length === 1) {
305
+ return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="system-ui,sans-serif" fill="${fillColor}">${escapeSvgTextContent(lines[0] ?? "")}</text>`;
306
+ }
307
+ const parts = [];
308
+ for (let i = 0; i < lines.length; i++) {
309
+ const line = lines[i] ?? "";
310
+ if (i === 0) {
311
+ parts.push(`<tspan x="4">${escapeSvgTextContent(line)}</tspan>`);
312
+ } else {
313
+ parts.push(`<tspan x="4" dy="${lh}">${escapeSvgTextContent(line)}</tspan>`);
314
+ }
315
+ }
316
+ return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="system-ui,sans-serif" fill="${fillColor}">${parts.join("")}</text>`;
317
+ }
318
+ function buildTextFixedBoundsSvg(content, width, height, fillColor = "#2563eb", fontSize = DEFAULT_TEXT_FONT_SIZE) {
319
+ const w = Math.max(1, width);
320
+ const h = Math.max(1, height);
321
+ const lh = lineHeightFor(fontSize);
322
+ const trimmed = content.trim();
323
+ if (trimmed.length === 0) {
324
+ return `<foreignObject width="${w}" height="${h}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:2px 4px;font-size:${fontSize}px;line-height:${lh}px;font-family:system-ui,sans-serif;white-space:pre-wrap;word-wrap:break-word;overflow:hidden;color:#94a3b8;font-style:italic">${escapeHtmlText(PLACEHOLDER)}</div></foreignObject>`;
325
+ }
326
+ const body = escapeHtmlText(content);
327
+ return `<foreignObject width="${w}" height="${h}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:2px 4px;font-size:${fontSize}px;line-height:${lh}px;font-family:system-ui,sans-serif;white-space:pre-wrap;word-wrap:break-word;overflow:hidden;color:${fillColor}">${body}</div></foreignObject>`;
328
+ }
329
+
330
+ // src/scene/shape-builders.ts
331
+ var DEFAULT_STROKE_STYLE = {
332
+ stroke: "#2563eb",
333
+ strokeWidth: 2
334
+ };
335
+ var TOOL_FREEHAND_DEFAULTS = {
336
+ draw: { strokeWidth: 3 },
337
+ pencil: { strokeWidth: 3 },
338
+ brush: { strokeWidth: 10 },
339
+ marker: { stroke: "#fde047", strokeWidth: 16, strokeOpacity: 0.5 }
340
+ };
341
+ function perfectFreehandOptions(toolKind, style, strokeComplete, pressureAware = false) {
342
+ const sw = style.strokeWidth;
343
+ const base = {
344
+ last: strokeComplete,
345
+ simulatePressure: true
346
+ };
347
+ if (toolKind === "draw" || toolKind === "pencil") {
348
+ if (pressureAware && toolKind === "draw") {
349
+ return {
350
+ ...base,
351
+ size: Math.max(2, sw * 1.05),
352
+ thinning: 0.42,
353
+ smoothing: 0.56,
354
+ streamline: 0.18,
355
+ simulatePressure: false
356
+ };
357
+ }
358
+ return {
359
+ ...base,
360
+ size: Math.max(2, sw * 1.18),
361
+ thinning: 0.12,
362
+ smoothing: 0.72,
363
+ streamline: 0.42,
364
+ simulatePressure: false
365
+ };
366
+ }
367
+ if (toolKind === "brush") {
368
+ return {
369
+ ...base,
370
+ size: Math.max(4, sw * 1.22),
371
+ thinning: 0.52,
372
+ smoothing: 0.64,
373
+ streamline: 0.68
374
+ };
375
+ }
376
+ return {
377
+ ...base,
378
+ size: Math.max(6, sw * 1.08),
379
+ thinning: 0.08,
380
+ smoothing: 0.88,
381
+ streamline: 0.84,
382
+ simulatePressure: false
383
+ };
384
+ }
385
+ function resolveStrokeStyle(item) {
386
+ return {
387
+ stroke: item.stroke ?? DEFAULT_STROKE_STYLE.stroke,
388
+ strokeWidth: item.strokeWidth ?? DEFAULT_STROKE_STYLE.strokeWidth,
389
+ strokeOpacity: item.strokeOpacity
390
+ };
391
+ }
392
+ function strokeOpacityAttr(style) {
393
+ return style.strokeOpacity != null ? ` stroke-opacity="${style.strokeOpacity}"` : "";
394
+ }
395
+ function buildRectSvg(width, height, style = DEFAULT_STROKE_STYLE) {
396
+ return `<rect width="${width}" height="${height}" fill="none" stroke="${style.stroke}" stroke-width="${style.strokeWidth}" rx="4"${strokeOpacityAttr(style)} />`;
397
+ }
398
+ function buildEllipseSvg(width, height, style = DEFAULT_STROKE_STYLE) {
399
+ const rx = width / 2;
400
+ const ry = height / 2;
401
+ return `<ellipse cx="${rx}" cy="${ry}" rx="${rx}" ry="${ry}" fill="none" stroke="${style.stroke}" stroke-width="${style.strokeWidth}"${strokeOpacityAttr(style)} />`;
402
+ }
403
+ function buildLineSvg(line, style = DEFAULT_STROKE_STYLE) {
404
+ return `<line x1="${line.x1}" y1="${line.y1}" x2="${line.x2}" y2="${line.y2}" stroke="${style.stroke}" stroke-width="${style.strokeWidth}"${strokeOpacityAttr(style)} />`;
405
+ }
406
+ function computeStraightArrowGeometry(line, strokeWidth) {
407
+ const dx = line.x2 - line.x1;
408
+ const dy = line.y2 - line.y1;
409
+ const len = Math.hypot(dx, dy);
410
+ if (len < 1e-6) return null;
411
+ const ux = dx / len;
412
+ const uy = dy / len;
413
+ const headLength = Math.min(Math.max(strokeWidth * 4.2, 12), len * 0.38);
414
+ const headAngle = Math.PI / 6;
415
+ const cos = Math.cos(headAngle);
416
+ const sin = Math.sin(headAngle);
417
+ const rx1 = ux * cos - uy * sin;
418
+ const ry1 = ux * sin + uy * cos;
419
+ const rx2 = ux * cos + uy * sin;
420
+ const ry2 = -ux * sin + uy * cos;
421
+ return {
422
+ shaftEndX: line.x2,
423
+ shaftEndY: line.y2,
424
+ headTipX: line.x2,
425
+ headTipY: line.y2,
426
+ headLeftX: line.x2 - rx1 * headLength,
427
+ headLeftY: line.y2 - ry1 * headLength,
428
+ headRightX: line.x2 - rx2 * headLength,
429
+ headRightY: line.y2 - ry2 * headLength
430
+ };
431
+ }
432
+ function buildArrowSvg(itemId, line, style = DEFAULT_STROKE_STYLE) {
433
+ const geometry = computeStraightArrowGeometry(line, style.strokeWidth);
434
+ if (!geometry) {
435
+ return buildLineSvg(line, style);
436
+ }
437
+ const op = strokeOpacityAttr(style);
438
+ return `
439
+ <line x1="${line.x1}" y1="${line.y1}" x2="${geometry.shaftEndX}" y2="${geometry.shaftEndY}" stroke="${style.stroke}" stroke-width="${style.strokeWidth}" stroke-linecap="round"${op} />
440
+ <path d="M ${geometry.headLeftX} ${geometry.headLeftY} L ${geometry.headTipX} ${geometry.headTipY} L ${geometry.headRightX} ${geometry.headRightY}" fill="none" stroke="${style.stroke}" stroke-width="${style.strokeWidth}" stroke-linecap="round" stroke-linejoin="round" shape-rendering="geometricPrecision"${op} />
441
+ `;
442
+ }
443
+ function buildDrawDotSvg(r, style = DEFAULT_STROKE_STYLE) {
444
+ const op = style.strokeOpacity != null ? ` fill-opacity="${style.strokeOpacity}"` : "";
445
+ return `<circle cx="${r}" cy="${r}" r="${r}" fill="${style.stroke}" shape-rendering="geometricPrecision"${op} />`;
446
+ }
447
+ function computeFreehandSvgPayload(pathPointsLocal, style, toolKind, strokeComplete = true) {
448
+ if (pathPointsLocal.length === 0) return null;
449
+ if (pathPointsLocal.length === 1) {
450
+ const p = pathPointsLocal[0];
451
+ if (!p) return null;
452
+ const r = Math.max(0.5, style.strokeWidth / 2);
453
+ return {
454
+ kind: "circle",
455
+ cx: p.x,
456
+ cy: p.y,
457
+ r,
458
+ fill: style.stroke,
459
+ fillOpacity: style.strokeOpacity
460
+ };
461
+ }
462
+ const minDist = Math.min(0.25, Math.max(0.02, style.strokeWidth * 0.02));
463
+ const pts = dedupeFreehandPoints(pathPointsLocal, minDist);
464
+ if (pts.length === 0) return null;
465
+ if (pts.length === 1) {
466
+ const p = pts[0];
467
+ if (!p) return null;
468
+ const r = Math.max(0.5, style.strokeWidth / 2);
469
+ return {
470
+ kind: "circle",
471
+ cx: p.x,
472
+ cy: p.y,
473
+ r,
474
+ fill: style.stroke,
475
+ fillOpacity: style.strokeOpacity
476
+ };
477
+ }
478
+ const hasPressure = toolKind === "draw" && pts.some((p) => p.pressure != null && Number.isFinite(p.pressure));
479
+ if (toolKind === "draw" && !hasPressure) {
480
+ const d2 = smoothFreehandPointsToPathD(pts);
481
+ return {
482
+ kind: "strokePath",
483
+ d: d2,
484
+ stroke: style.stroke,
485
+ strokeWidth: style.strokeWidth,
486
+ strokeOpacity: style.strokeOpacity
487
+ };
488
+ }
489
+ const input = hasPressure ? pts.map(
490
+ (p) => [p.x, p.y, Math.min(1, Math.max(0, p.pressure ?? 0.5))]
491
+ ) : pts.map((p) => [p.x, p.y]);
492
+ const opts = perfectFreehandOptions(toolKind, style, strokeComplete, hasPressure);
493
+ let outline = [];
494
+ try {
495
+ const raw = getStroke(input, opts);
496
+ outline = raw.map(([x, y]) => [x, y]);
497
+ } catch {
498
+ outline = [];
499
+ }
500
+ if (outline.length >= 3) {
501
+ const d2 = outlineStrokeToClosedPathD(outline);
502
+ return {
503
+ kind: "fillPath",
504
+ d: d2,
505
+ fill: style.stroke,
506
+ fillOpacity: style.strokeOpacity
507
+ };
508
+ }
509
+ const d = smoothFreehandPointsToPathD(pts);
510
+ return {
511
+ kind: "strokePath",
512
+ d,
513
+ stroke: style.stroke,
514
+ strokeWidth: style.strokeWidth,
515
+ strokeOpacity: style.strokeOpacity
516
+ };
517
+ }
518
+ function freehandPayloadToSvgString(payload) {
519
+ if (payload.kind === "circle") {
520
+ const op2 = payload.fillOpacity != null ? ` fill-opacity="${payload.fillOpacity}"` : "";
521
+ return `<circle cx="${payload.cx}" cy="${payload.cy}" r="${payload.r}" fill="${payload.fill}" shape-rendering="geometricPrecision"${op2} />`;
522
+ }
523
+ if (payload.kind === "fillPath") {
524
+ const op2 = payload.fillOpacity != null ? ` fill-opacity="${payload.fillOpacity}"` : "";
525
+ return `<path d="${payload.d}" fill="${payload.fill}" fill-rule="nonzero" stroke="none"${op2} shape-rendering="geometricPrecision" />`;
526
+ }
527
+ const op = strokeOpacityAttr({
528
+ stroke: payload.stroke,
529
+ strokeWidth: payload.strokeWidth,
530
+ strokeOpacity: payload.strokeOpacity
531
+ });
532
+ return `<path d="${payload.d}" fill="none" stroke="${payload.stroke}" stroke-width="${payload.strokeWidth}" stroke-linecap="round" stroke-linejoin="round" shape-rendering="geometricPrecision"${op} />`;
533
+ }
534
+ function buildFreehandPathSvg(pathPointsLocal, style, toolKind, strokeComplete = true) {
535
+ const payload = computeFreehandSvgPayload(
536
+ pathPointsLocal,
537
+ style,
538
+ toolKind,
539
+ strokeComplete
540
+ );
541
+ if (!payload) return "";
542
+ return freehandPayloadToSvgString(payload);
543
+ }
544
+ function createShapeId() {
545
+ const uid = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : String(Date.now());
546
+ return `user-shape-${uid}`;
547
+ }
548
+ function rebuildItemSvg(item) {
549
+ const style = resolveStrokeStyle(item);
550
+ const k = item.toolKind;
551
+ if (k === "rect") {
552
+ const b = normalizeRect(item.bounds);
553
+ return {
554
+ ...item,
555
+ childrenSvg: buildRectSvg(b.width, b.height, style)
556
+ };
557
+ }
558
+ if (k === "ellipse") {
559
+ const b = normalizeRect(item.bounds);
560
+ return {
561
+ ...item,
562
+ childrenSvg: buildEllipseSvg(b.width, b.height, style)
563
+ };
564
+ }
565
+ if ((k === "line" || k === "arrow") && item.line) {
566
+ const line = item.line;
567
+ const childrenSvg = k === "arrow" ? buildArrowSvg(item.id, line, style) : buildLineSvg(line, style);
568
+ return { ...item, childrenSvg };
569
+ }
570
+ if (k === "text" && item.text !== void 0) {
571
+ const fs = item.textFontSize ?? DEFAULT_TEXT_FONT_SIZE;
572
+ if (item.textFixedBounds) {
573
+ const b2 = normalizeRect(item.bounds);
574
+ return {
575
+ ...item,
576
+ x: b2.x,
577
+ y: b2.y,
578
+ bounds: b2,
579
+ childrenSvg: buildTextFixedBoundsSvg(
580
+ item.text,
581
+ b2.width,
582
+ b2.height,
583
+ style.stroke,
584
+ fs
585
+ )
586
+ };
587
+ }
588
+ const m = measureTextBoundsLocal(item.text, fs);
589
+ const b = normalizeRect({
590
+ x: item.x,
591
+ y: item.y,
592
+ width: m.width,
593
+ height: m.height
594
+ });
595
+ return {
596
+ ...item,
597
+ x: b.x,
598
+ y: b.y,
599
+ bounds: b,
600
+ childrenSvg: buildTextSvg(item.text, b.width, b.height, style.stroke, fs)
601
+ };
602
+ }
603
+ if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item.pathPointsLocal && item.pathPointsLocal.length > 0) {
604
+ return {
605
+ ...item,
606
+ childrenSvg: buildFreehandPathSvg(item.pathPointsLocal, style, k)
607
+ };
608
+ }
609
+ if (k === "draw") {
610
+ const b = normalizeRect(item.bounds);
611
+ const r = Math.min(b.width, b.height) / 2;
612
+ return {
613
+ ...item,
614
+ childrenSvg: buildDrawDotSvg(r, style)
615
+ };
616
+ }
617
+ if (k === "image" && item.imageRasterHref && item.imageIntrinsicSize) {
618
+ const b = normalizeRect(item.bounds);
619
+ return {
620
+ ...item,
621
+ childrenSvg: buildRasterImageChildrenSvg(
622
+ item.imageRasterHref,
623
+ item.imageIntrinsicSize,
624
+ b
625
+ )
626
+ };
627
+ }
628
+ if (k === "custom" && item.customIntrinsicSize && item.customInnerSvg) {
629
+ const b = normalizeRect(item.bounds);
630
+ return {
631
+ ...item,
632
+ x: b.x,
633
+ y: b.y,
634
+ bounds: b,
635
+ childrenSvg: buildCustomShapeChildrenSvg(
636
+ item.customInnerSvg,
637
+ item.customIntrinsicSize,
638
+ b
639
+ )
640
+ };
641
+ }
642
+ return item;
643
+ }
644
+ function createFreehandStrokeItem(id, pointsWorld, toolKind, style) {
645
+ if (pointsWorld.length === 0) return null;
646
+ const merged = {
647
+ ...DEFAULT_STROKE_STYLE,
648
+ ...TOOL_FREEHAND_DEFAULTS[toolKind],
649
+ ...style
650
+ };
651
+ const sw = merged.strokeWidth;
652
+ const pad = Math.max(sw * 0.75 + 4, sw / 2 + 6);
653
+ const xs = pointsWorld.map((p) => p.x);
654
+ const ys = pointsWorld.map((p) => p.y);
655
+ const minX = Math.min(...xs);
656
+ const maxX = Math.max(...xs);
657
+ const minY = Math.min(...ys);
658
+ const maxY = Math.max(...ys);
659
+ const x = minX - pad;
660
+ const y = minY - pad;
661
+ const w = Math.max(maxX - minX + 2 * pad, sw);
662
+ const h = Math.max(maxY - minY + 2 * pad, sw);
663
+ const pathPointsLocal = pointsWorld.map((p) => ({
664
+ x: p.x - x,
665
+ y: p.y - y,
666
+ ...p.pressure != null ? { pressure: p.pressure } : {}
667
+ }));
668
+ return rebuildItemSvg({
669
+ id,
670
+ x,
671
+ y,
672
+ bounds: { x, y, width: w, height: h },
673
+ toolKind,
674
+ stroke: merged.stroke,
675
+ strokeWidth: merged.strokeWidth,
676
+ ...merged.strokeOpacity != null ? { strokeOpacity: merged.strokeOpacity } : {},
677
+ pathPointsLocal,
678
+ childrenSvg: ""
679
+ });
680
+ }
681
+ function escapeSvgAttr(s) {
682
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
683
+ }
684
+ function buildRasterImageChildrenSvg(dataUrl, intrinsic, bounds) {
685
+ const r = normalizeRect(bounds);
686
+ const iw = Math.max(1e-6, intrinsic.width);
687
+ const ih = Math.max(1e-6, intrinsic.height);
688
+ const href = escapeSvgAttr(dataUrl);
689
+ const arB = r.width / Math.max(1e-9, r.height);
690
+ const arI = iw / ih;
691
+ if (Math.abs(arB - arI) < 1e-3) {
692
+ const s2 = r.width / iw;
693
+ return `<g transform="scale(${s2})"><image href="${href}" x="0" y="0" width="${iw}" height="${ih}" /></g>`;
694
+ }
695
+ const s = Math.min(r.width / iw, r.height / ih);
696
+ const tx = (r.width - iw * s) / 2;
697
+ const ty = (r.height - ih * s) / 2;
698
+ return `<g transform="translate(${tx}, ${ty}) scale(${s})"><image href="${href}" x="0" y="0" width="${iw}" height="${ih}" /></g>`;
699
+ }
700
+
701
+ // src/native/skia-transform.ts
702
+ function parseNum(s) {
703
+ return Number(s);
704
+ }
705
+ function parseSvgTransform(s) {
706
+ if (!s || s.trim().length === 0) return [];
707
+ const clean = s.trim();
708
+ const result = [];
709
+ const parts = clean.match(/[a-z]+\([^)]*\)/gi);
710
+ if (!parts) return [];
711
+ const reversed = [...parts].reverse();
712
+ for (const part of reversed) {
713
+ const m = part.match(/^([a-z]+)\(([^)]*)\)$/i);
714
+ if (!m) continue;
715
+ const fn = (m[1] ?? "").toLowerCase();
716
+ const args = (m[2] ?? "").split(/[\s,]+/).filter(Boolean).map(parseNum);
717
+ switch (fn) {
718
+ case "matrix": {
719
+ const [a, b, c, d, e, f] = args;
720
+ if (a !== void 0 && d !== void 0 && Math.abs(b ?? 0) < 1e-12 && Math.abs(c ?? 0) < 1e-12 && Math.abs(a - (d ?? 0)) < 1e-12) {
721
+ result.push({ scale: a });
722
+ } else if (a !== void 0 && d !== void 0 && Math.abs(b ?? 0) < 1e-12 && Math.abs(c ?? 0) < 1e-12) {
723
+ result.push({ scaleX: a, scaleY: d });
724
+ }
725
+ if (e !== void 0 && !Number.isNaN(e)) result.push({ translateX: e });
726
+ if (f !== void 0 && !Number.isNaN(f)) result.push({ translateY: f });
727
+ break;
728
+ }
729
+ case "translate": {
730
+ const [tx, ty] = args;
731
+ if (tx !== void 0 && !Number.isNaN(tx)) result.push({ translateX: tx });
732
+ if (ty !== void 0 && !Number.isNaN(ty)) result.push({ translateY: ty });
733
+ break;
734
+ }
735
+ case "scale": {
736
+ const [sx, sy] = args;
737
+ if (sx === void 0 || Number.isNaN(sx)) break;
738
+ if (sy === void 0 || Number.isNaN(sy)) {
739
+ result.push({ scale: sx });
740
+ } else {
741
+ if (Math.abs(sx - sy) < 1e-12) {
742
+ result.push({ scale: sx });
743
+ } else {
744
+ result.push({ scaleX: sx, scaleY: sy });
745
+ }
746
+ }
747
+ break;
748
+ }
749
+ case "rotate": {
750
+ const [deg, cx, cy] = args;
751
+ if (deg === void 0 || Number.isNaN(deg)) break;
752
+ const rad = deg * Math.PI / 180;
753
+ if (cx !== void 0 && cy !== void 0 && !Number.isNaN(cx) && !Number.isNaN(cy)) {
754
+ result.push({ rotate: rad, origin: { x: cx, y: cy } });
755
+ } else {
756
+ result.push({ rotate: rad });
757
+ }
758
+ break;
759
+ }
760
+ }
761
+ }
762
+ return result;
763
+ }
764
+ function skiaCameraTransform(zoom, panX, panY) {
765
+ return [{ translateX: panX }, { translateY: panY }, { scale: zoom }];
766
+ }
767
+ function skiaItemPlacementTransform(x, y, cx, cy, rotationRad) {
768
+ const result = [];
769
+ if (Math.abs(rotationRad) > 1e-12) {
770
+ result.push({
771
+ rotate: rotationRad,
772
+ origin: { x: cx, y: cy }
773
+ });
774
+ }
775
+ result.push({ translateX: x }, { translateY: y });
776
+ return result;
777
+ }
778
+ var HANDLE_ORDER = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
779
+ function pointsToSmoothPathD(points) {
780
+ if (points.length < 2) return null;
781
+ const d = smoothFreehandPointsToPathD(points);
782
+ return d || null;
783
+ }
784
+ function renderEraserSkeleton(item, overlayStrokePx) {
785
+ const b = normalizeRect(item.bounds);
786
+ const sw = Math.max(item.strokeWidth ?? 2, overlayStrokePx);
787
+ const common = {
788
+ color: "#cbd5e1",
789
+ style: "stroke",
790
+ strokeWidth: sw,
791
+ strokeCap: "round",
792
+ strokeJoin: "round",
793
+ antiAlias: true
794
+ };
795
+ const k = item.toolKind;
796
+ if (k === "rect") {
797
+ return /* @__PURE__ */ jsx(Rect, { x: 0, y: 0, width: b.width, height: b.height, ...common });
798
+ }
799
+ if (k === "ellipse") {
800
+ const rx = Math.max(0, b.width / 2);
801
+ const ry = Math.max(0, b.height / 2);
802
+ if (Math.abs(rx - ry) < 1e-9) {
803
+ return /* @__PURE__ */ jsx(Circle, { cx: rx, cy: ry, r: rx, ...common });
804
+ }
805
+ return /* @__PURE__ */ jsx(Circle, { cx: rx, cy: ry, r: Math.min(rx, ry), ...common });
806
+ }
807
+ if ((k === "line" || k === "arrow") && item.line) {
808
+ const ln = item.line;
809
+ const geometry = k === "arrow" ? computeStraightArrowGeometry(
810
+ ln,
811
+ Math.max(item.strokeWidth ?? 2, overlayStrokePx)
812
+ ) : null;
813
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
814
+ /* @__PURE__ */ jsx(
815
+ Line,
816
+ {
817
+ p1: vec(ln.x1, ln.y1),
818
+ p2: vec(geometry?.shaftEndX ?? ln.x2, geometry?.shaftEndY ?? ln.y2),
819
+ ...common
820
+ }
821
+ ),
822
+ k === "arrow" && geometry ? /* @__PURE__ */ jsx(
823
+ Path,
824
+ {
825
+ path: `M ${geometry.headLeftX} ${geometry.headLeftY} L ${geometry.headTipX} ${geometry.headTipY} L ${geometry.headRightX} ${geometry.headRightY}`,
826
+ ...common
827
+ }
828
+ ) : null
829
+ ] });
830
+ }
831
+ if ((k === "draw" || k === "marker" || k === "pencil" || k === "brush") && item.pathPointsLocal) {
832
+ const pts = item.pathPointsLocal;
833
+ if (pts.length === 1 && pts[0]) {
834
+ const p = pts[0];
835
+ const dotR = Math.max((item.strokeWidth ?? 2) / 2, 2);
836
+ return /* @__PURE__ */ jsx(
837
+ Circle,
838
+ {
839
+ cx: p.x,
840
+ cy: p.y,
841
+ r: dotR,
842
+ color: "#cbd5e1",
843
+ style: "fill",
844
+ antiAlias: true
845
+ }
846
+ );
847
+ }
848
+ const d = pointsToSmoothPathD(pts);
849
+ if (d) {
850
+ return /* @__PURE__ */ jsx(Path, { path: d, ...common });
851
+ }
852
+ }
853
+ return /* @__PURE__ */ jsx(Path, { path: `M0 0 h${b.width} v${b.height} h${-b.width} Z`, ...common });
854
+ }
855
+ function NativeInteractionOverlay({
856
+ camera,
857
+ width,
858
+ height,
859
+ selectedItems,
860
+ showResizeHandles,
861
+ placementPreview,
862
+ eraserTrail,
863
+ laserTrail,
864
+ eraserPreviewItems = []
865
+ }) {
866
+ const z = camera.zoom;
867
+ const camTransform = skiaCameraTransform(z, camera.x, camera.y);
868
+ const handleR = 5 / z;
869
+ const overlayStrokePx = 1.25;
870
+ const rotateOffsetWorld = 24 / z;
871
+ const selectionElements = useMemo(() => {
872
+ if (selectedItems.length === 0) return null;
873
+ const single = selectedItems.length === 1 ? selectedItems[0] : void 0;
874
+ const bSingle = single ? normalizeRect(single.bounds) : null;
875
+ const rotSingle = single?.rotation ?? 0;
876
+ const rotHandlePos = showResizeHandles && bSingle && single ? getRotationHandleWorldPosition(bSingle, rotSingle, rotateOffsetWorld) : null;
877
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
878
+ selectedItems.map((it) => {
879
+ const b = normalizeRect(it.bounds);
880
+ const cx = b.width / 2;
881
+ const cy = b.height / 2;
882
+ const t = skiaItemPlacementTransform(it.x, it.y, cx, cy, it.rotation ?? 0);
883
+ return /* @__PURE__ */ jsx(Group, { transform: t, children: /* @__PURE__ */ jsx(
884
+ Rect,
885
+ {
886
+ x: 0,
887
+ y: 0,
888
+ width: b.width,
889
+ height: b.height,
890
+ color: "#3b82f6",
891
+ style: "stroke",
892
+ strokeWidth: overlayStrokePx,
893
+ antiAlias: true
894
+ }
895
+ ) }, it.id);
896
+ }),
897
+ showResizeHandles && bSingle && single && /* @__PURE__ */ jsxs(Fragment, { children: [
898
+ HANDLE_ORDER.map((hid) => {
899
+ const p = getHandleWorldPositionRotated(bSingle, hid, rotSingle);
900
+ return /* @__PURE__ */ jsx(
901
+ Circle,
902
+ {
903
+ cx: p.x,
904
+ cy: p.y,
905
+ r: handleR,
906
+ color: "#ffffff",
907
+ style: "fill",
908
+ antiAlias: true
909
+ },
910
+ hid
911
+ );
912
+ }),
913
+ rotHandlePos && /* @__PURE__ */ jsx(
914
+ Circle,
915
+ {
916
+ cx: rotHandlePos.x,
917
+ cy: rotHandlePos.y,
918
+ r: handleR * 1.5,
919
+ color: "#3b82f6",
920
+ style: "stroke",
921
+ strokeWidth: overlayStrokePx * 1.2,
922
+ antiAlias: true
923
+ }
924
+ )
925
+ ] })
926
+ ] });
927
+ }, [selectedItems, showResizeHandles, rotateOffsetWorld, handleR]);
928
+ const previewElements = useMemo(() => {
929
+ if (!placementPreview) return null;
930
+ const p = placementPreview;
931
+ if (p.kind === "rect" || p.kind === "ellipse") {
932
+ const r = normalizeRect(p.rect);
933
+ return p.kind === "rect" ? /* @__PURE__ */ jsx(
934
+ Rect,
935
+ {
936
+ x: r.x,
937
+ y: r.y,
938
+ width: r.width,
939
+ height: r.height,
940
+ color: "#64748b",
941
+ style: "stroke",
942
+ strokeWidth: overlayStrokePx,
943
+ antiAlias: true
944
+ }
945
+ ) : /* @__PURE__ */ jsx(
946
+ Circle,
947
+ {
948
+ cx: r.x + r.width / 2,
949
+ cy: r.y + r.height / 2,
950
+ r: Math.max(0, r.width / 2),
951
+ color: "#64748b",
952
+ style: "stroke",
953
+ strokeWidth: overlayStrokePx,
954
+ antiAlias: true
955
+ }
956
+ );
957
+ }
958
+ if (p.kind === "marquee") {
959
+ const r = normalizeRect(p.rect);
960
+ return /* @__PURE__ */ jsx(
961
+ Rect,
962
+ {
963
+ x: r.x,
964
+ y: r.y,
965
+ width: r.width,
966
+ height: r.height,
967
+ color: "rgba(59, 130, 246, 0.12)",
968
+ style: "fill",
969
+ antiAlias: true
970
+ }
971
+ );
972
+ }
973
+ if (p.kind === "line" || p.kind === "arrow") {
974
+ const geometry = p.kind === "arrow" ? computeStraightArrowGeometry(
975
+ { x1: p.start.x, y1: p.start.y, x2: p.end.x, y2: p.end.y },
976
+ overlayStrokePx
977
+ ) : null;
978
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
979
+ /* @__PURE__ */ jsx(
980
+ Line,
981
+ {
982
+ p1: vec(p.start.x, p.start.y),
983
+ p2: vec(geometry?.shaftEndX ?? p.end.x, geometry?.shaftEndY ?? p.end.y),
984
+ color: "#64748b",
985
+ style: "stroke",
986
+ strokeWidth: overlayStrokePx,
987
+ strokeCap: "round",
988
+ antiAlias: true
989
+ }
990
+ ),
991
+ p.kind === "arrow" && geometry ? /* @__PURE__ */ jsx(
992
+ Path,
993
+ {
994
+ path: `M ${geometry.headLeftX} ${geometry.headLeftY} L ${geometry.headTipX} ${geometry.headTipY} L ${geometry.headRightX} ${geometry.headRightY}`,
995
+ color: "#64748b",
996
+ style: "stroke",
997
+ strokeWidth: overlayStrokePx,
998
+ strokeCap: "round",
999
+ strokeJoin: "round",
1000
+ antiAlias: true
1001
+ }
1002
+ ) : null
1003
+ ] });
1004
+ }
1005
+ if (p.kind === "stroke" && p.points.length >= 1) {
1006
+ const payload = computeFreehandSvgPayload(
1007
+ p.points,
1008
+ { stroke: "#64748b", strokeWidth: 3 },
1009
+ "draw",
1010
+ false
1011
+ );
1012
+ if (!payload) return null;
1013
+ if (payload.kind === "circle") {
1014
+ return /* @__PURE__ */ jsx(
1015
+ Circle,
1016
+ {
1017
+ cx: payload.cx,
1018
+ cy: payload.cy,
1019
+ r: payload.r,
1020
+ color: payload.fill,
1021
+ style: "fill",
1022
+ antiAlias: true
1023
+ }
1024
+ );
1025
+ }
1026
+ if (payload.kind === "fillPath") {
1027
+ return /* @__PURE__ */ jsx(Path, { path: payload.d, color: payload.fill, style: "fill", antiAlias: true });
1028
+ }
1029
+ if (payload.kind === "strokePath") {
1030
+ return /* @__PURE__ */ jsx(
1031
+ Path,
1032
+ {
1033
+ path: payload.d,
1034
+ color: "#64748b",
1035
+ style: "stroke",
1036
+ strokeWidth: 3,
1037
+ strokeCap: "round",
1038
+ strokeJoin: "round",
1039
+ antiAlias: true
1040
+ }
1041
+ );
1042
+ }
1043
+ return null;
1044
+ }
1045
+ return null;
1046
+ }, [placementPreview]);
1047
+ const eraserPreviewElements = useMemo(() => {
1048
+ if (eraserPreviewItems.length === 0) return null;
1049
+ return /* @__PURE__ */ jsx(Fragment, { children: eraserPreviewItems.map((it) => {
1050
+ const b = normalizeRect(it.bounds);
1051
+ const cx = b.width / 2;
1052
+ const cy = b.height / 2;
1053
+ const t = skiaItemPlacementTransform(it.x, it.y, cx, cy, it.rotation ?? 0);
1054
+ return /* @__PURE__ */ jsx(Group, { transform: t, children: renderEraserSkeleton(it, overlayStrokePx) }, `erase-${it.id}`);
1055
+ }) });
1056
+ }, [eraserPreviewItems]);
1057
+ const eraserTrailElements = useMemo(() => {
1058
+ if (!eraserTrail || eraserTrail.length < 1) return null;
1059
+ const d = pointsToSmoothPathD(eraserTrail);
1060
+ if (!d)
1061
+ return eraserTrail[0] ? /* @__PURE__ */ jsx(
1062
+ Circle,
1063
+ {
1064
+ cx: eraserTrail[0].x,
1065
+ cy: eraserTrail[0].y,
1066
+ r: Math.max(5 / z, 3),
1067
+ color: "#cbd5e1",
1068
+ style: "fill",
1069
+ antiAlias: true
1070
+ }
1071
+ ) : null;
1072
+ return /* @__PURE__ */ jsx(
1073
+ Path,
1074
+ {
1075
+ path: d,
1076
+ color: "#cbd5e1",
1077
+ style: "stroke",
1078
+ strokeWidth: Math.max(3.5 / z, overlayStrokePx),
1079
+ strokeCap: "round",
1080
+ strokeJoin: "round",
1081
+ antiAlias: true
1082
+ }
1083
+ );
1084
+ }, [eraserTrail, z]);
1085
+ const laserTrailElements = useMemo(() => {
1086
+ if (!laserTrail || laserTrail.length < 1) return null;
1087
+ const d = pointsToSmoothPathD(laserTrail);
1088
+ if (!d)
1089
+ return laserTrail[0] ? /* @__PURE__ */ jsx(
1090
+ Circle,
1091
+ {
1092
+ cx: laserTrail[0].x,
1093
+ cy: laserTrail[0].y,
1094
+ r: Math.max(5 / z, 3),
1095
+ color: "#f43f5e",
1096
+ style: "fill",
1097
+ antiAlias: true
1098
+ }
1099
+ ) : null;
1100
+ return /* @__PURE__ */ jsx(
1101
+ Path,
1102
+ {
1103
+ path: d,
1104
+ color: "#f43f5e",
1105
+ style: "stroke",
1106
+ strokeWidth: Math.max(4 / z, overlayStrokePx),
1107
+ strokeCap: "round",
1108
+ strokeJoin: "round",
1109
+ antiAlias: true
1110
+ }
1111
+ );
1112
+ }, [laserTrail, z]);
1113
+ if (width <= 0 || height <= 0) return null;
1114
+ return /* @__PURE__ */ jsx(
1115
+ Canvas,
1116
+ {
1117
+ style: {
1118
+ position: "absolute",
1119
+ top: 0,
1120
+ left: 0,
1121
+ width,
1122
+ height
1123
+ },
1124
+ pointerEvents: "none",
1125
+ children: /* @__PURE__ */ jsxs(Group, { transform: camTransform, children: [
1126
+ previewElements,
1127
+ laserTrailElements,
1128
+ eraserTrailElements,
1129
+ eraserPreviewElements,
1130
+ selectionElements
1131
+ ] })
1132
+ }
1133
+ );
1134
+ }
1135
+
1136
+ // src/scene/spatial-cull.ts
1137
+ var spatialIndexCache = /* @__PURE__ */ new WeakMap();
1138
+ function cellKey(ix, iy) {
1139
+ return `${ix},${iy}`;
1140
+ }
1141
+ function buildSpatialIndex(items, cellSize) {
1142
+ const cached = spatialIndexCache.get(items);
1143
+ if (cached && cached.cellSize === cellSize) {
1144
+ return cached;
1145
+ }
1146
+ const aabbs = new Array(items.length);
1147
+ const buckets = /* @__PURE__ */ new Map();
1148
+ for (let index = 0; index < items.length; index += 1) {
1149
+ const item = items[index];
1150
+ if (!item) continue;
1151
+ const aabb = boundsAabbForRotatedItem(item);
1152
+ aabbs[index] = aabb;
1153
+ const { minIx, maxIx, minIy, maxIy } = cellRangeForRect(aabb, cellSize);
1154
+ for (let ix = minIx; ix <= maxIx; ix += 1) {
1155
+ for (let iy = minIy; iy <= maxIy; iy += 1) {
1156
+ const key = cellKey(ix, iy);
1157
+ let bucket = buckets.get(key);
1158
+ if (!bucket) {
1159
+ bucket = [];
1160
+ buckets.set(key, bucket);
1161
+ }
1162
+ bucket.push(index);
1163
+ }
1164
+ }
1165
+ }
1166
+ const next = {
1167
+ cellSize,
1168
+ aabbs,
1169
+ buckets
1170
+ };
1171
+ spatialIndexCache.set(items, next);
1172
+ return next;
1173
+ }
1174
+ function cellRangeForRect(r, cellSize) {
1175
+ const n = normalizeRect(r);
1176
+ const x1 = n.x + n.width;
1177
+ const y1 = n.y + n.height;
1178
+ const minIx = Math.floor(n.x / cellSize);
1179
+ const maxIx = Math.max(minIx, Math.ceil(x1 / cellSize) - 1);
1180
+ const minIy = Math.floor(n.y / cellSize);
1181
+ const maxIy = Math.max(minIy, Math.ceil(y1 / cellSize) - 1);
1182
+ return { minIx, maxIx, minIy, maxIy };
1183
+ }
1184
+ function cullItemsByViewportSpatial(items, visibleWorld, cellSize) {
1185
+ const { aabbs, buckets } = buildSpatialIndex(items, cellSize);
1186
+ const vr = cellRangeForRect(visibleWorld, cellSize);
1187
+ const seen = /* @__PURE__ */ new Set();
1188
+ const outIndices = [];
1189
+ for (let ix = vr.minIx; ix <= vr.maxIx; ix++) {
1190
+ for (let iy = vr.minIy; iy <= vr.maxIy; iy++) {
1191
+ const bucket = buckets.get(cellKey(ix, iy));
1192
+ if (!bucket) {
1193
+ continue;
1194
+ }
1195
+ for (const index of bucket) {
1196
+ if (seen.has(index)) {
1197
+ continue;
1198
+ }
1199
+ seen.add(index);
1200
+ const aabb = aabbs[index];
1201
+ if (aabb && rectsIntersect(aabb, visibleWorld)) {
1202
+ outIndices.push(index);
1203
+ }
1204
+ }
1205
+ }
1206
+ }
1207
+ outIndices.sort((a, b) => a - b);
1208
+ return outIndices.map((index) => items[index]).filter((item) => item != null);
1209
+ }
1210
+
1211
+ // src/scene/cull.ts
1212
+ var SPATIAL_MIN_ITEMS = 400;
1213
+ var SPATIAL_CELL_SIZE = 256;
1214
+ function cullItemsLinear(items, visibleWorld) {
1215
+ return items.filter(
1216
+ (item) => rectsIntersect(boundsAabbForCull(item), visibleWorld)
1217
+ );
1218
+ }
1219
+ function boundsAabbForCull(item) {
1220
+ return boundsAabbForRotatedItem(item);
1221
+ }
1222
+ function cullItemsByViewport(items, visibleWorld) {
1223
+ if (items.length < SPATIAL_MIN_ITEMS) {
1224
+ return cullItemsLinear(items, visibleWorld);
1225
+ }
1226
+ return cullItemsByViewportSpatial(items, visibleWorld, SPATIAL_CELL_SIZE);
1227
+ }
1228
+ function rgbaFromHexAndOpacity(hex, opacity) {
1229
+ if (!hex) return hex;
1230
+ if (opacity == null || opacity >= 1) return hex;
1231
+ const r = parseInt(hex.slice(1, 3), 16);
1232
+ const g = parseInt(hex.slice(3, 5), 16);
1233
+ const b = parseInt(hex.slice(5, 7), 16);
1234
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return hex;
1235
+ return `rgba(${r},${g},${b},${opacity})`;
1236
+ }
1237
+ function toNum(v) {
1238
+ return typeof v === "number" ? v : Number(v) || 0;
1239
+ }
1240
+ function SvgNodeRenderer({ nodes }) {
1241
+ return /* @__PURE__ */ jsx(Fragment, { children: nodes.map((node, i) => /* @__PURE__ */ jsx(SvgNodeItem, { node }, i)) });
1242
+ }
1243
+ function SvgNodeItem({ node }) {
1244
+ const isFill = node.fill !== "none";
1245
+ const hasStroke = node.stroke != null && node.stroke !== "none";
1246
+ switch (node.kind) {
1247
+ case "rect": {
1248
+ const stroke = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
1249
+ const fill = isFill ? rgbaFromHexAndOpacity(node.fill, node.fillOpacity) ?? node.fill : void 0;
1250
+ const r = node.rx != null ? toNum(node.rx) : 0;
1251
+ const w = toNum(node.width);
1252
+ const h = toNum(node.height);
1253
+ if (r > 0) {
1254
+ return /* @__PURE__ */ jsx(
1255
+ RoundedRect,
1256
+ {
1257
+ x: 0,
1258
+ y: 0,
1259
+ width: w,
1260
+ height: h,
1261
+ r,
1262
+ color: fill ?? stroke,
1263
+ style: fill ? "fill" : hasStroke ? "stroke" : "fill",
1264
+ strokeWidth: hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
1265
+ antiAlias: true
1266
+ }
1267
+ );
1268
+ }
1269
+ return /* @__PURE__ */ jsx(
1270
+ Rect,
1271
+ {
1272
+ x: 0,
1273
+ y: 0,
1274
+ width: w,
1275
+ height: h,
1276
+ color: fill ?? stroke,
1277
+ style: fill ? "fill" : hasStroke ? "stroke" : "fill",
1278
+ strokeWidth: hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
1279
+ antiAlias: true
1280
+ }
1281
+ );
1282
+ }
1283
+ case "ellipse": {
1284
+ const stroke = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
1285
+ const fill = isFill ? rgbaFromHexAndOpacity(node.fill, node.fillOpacity) ?? node.fill : void 0;
1286
+ const cx = toNum(node.cx);
1287
+ const cy = toNum(node.cy);
1288
+ const rx = toNum(node.rx);
1289
+ const ry = toNum(node.ry);
1290
+ if (Math.abs(rx - ry) < 1e-6) {
1291
+ return /* @__PURE__ */ jsx(
1292
+ Circle,
1293
+ {
1294
+ cx,
1295
+ cy,
1296
+ r: rx,
1297
+ color: fill ?? stroke,
1298
+ style: fill ? "fill" : hasStroke ? "stroke" : "fill",
1299
+ strokeWidth: hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
1300
+ antiAlias: true
1301
+ }
1302
+ );
1303
+ }
1304
+ return /* @__PURE__ */ jsx(
1305
+ Oval,
1306
+ {
1307
+ x: cx - rx,
1308
+ y: cy - ry,
1309
+ width: rx * 2,
1310
+ height: ry * 2,
1311
+ color: fill ?? stroke,
1312
+ style: fill ? "fill" : hasStroke ? "stroke" : "fill",
1313
+ strokeWidth: hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
1314
+ antiAlias: true
1315
+ }
1316
+ );
1317
+ }
1318
+ case "circle": {
1319
+ const fill = rgbaFromHexAndOpacity(node.fill, node.fillOpacity) ?? node.fill;
1320
+ const isCircleFill = fill && fill !== "none";
1321
+ const strokeColor = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
1322
+ return /* @__PURE__ */ jsx(
1323
+ Circle,
1324
+ {
1325
+ cx: toNum(node.cx),
1326
+ cy: toNum(node.cy),
1327
+ r: toNum(node.r),
1328
+ color: isCircleFill ? fill : strokeColor ?? "black",
1329
+ style: isCircleFill ? "fill" : "stroke",
1330
+ strokeWidth: !isCircleFill && hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
1331
+ antiAlias: true
1332
+ }
1333
+ );
1334
+ }
1335
+ case "line": {
1336
+ const stroke = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
1337
+ return /* @__PURE__ */ jsx(
1338
+ Line,
1339
+ {
1340
+ p1: vec(toNum(node.x1), toNum(node.y1)),
1341
+ p2: vec(toNum(node.x2), toNum(node.y2)),
1342
+ color: stroke ?? "black",
1343
+ style: "stroke",
1344
+ strokeWidth: toNum(node.strokeWidth ?? "1"),
1345
+ strokeCap: node.strokeLinecap === "round" ? "round" : "butt",
1346
+ antiAlias: true
1347
+ }
1348
+ );
1349
+ }
1350
+ case "path": {
1351
+ const stroke = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
1352
+ const fill = isFill ? rgbaFromHexAndOpacity(node.fill, node.fillOpacity) ?? node.fill : void 0;
1353
+ const style = fill && fill !== "none" ? "fill" : "stroke";
1354
+ return /* @__PURE__ */ jsx(
1355
+ Path,
1356
+ {
1357
+ path: node.d,
1358
+ color: style === "fill" ? fill ?? stroke : stroke ?? "black",
1359
+ style,
1360
+ strokeWidth: style === "stroke" ? toNum(node.strokeWidth ?? "1") : void 0,
1361
+ strokeCap: node.strokeLinecap === "round" ? "round" : "butt",
1362
+ strokeJoin: node.strokeLinejoin === "round" ? "round" : "miter",
1363
+ fillType: node.fillRule === "evenodd" ? "evenOdd" : "winding",
1364
+ antiAlias: true
1365
+ }
1366
+ );
1367
+ }
1368
+ case "text": {
1369
+ const fill = node.fill ?? "black";
1370
+ const fs = node.fontSize != null ? toNum(node.fontSize) : 16;
1371
+ const font = matchFont({ fontSize: fs });
1372
+ return /* @__PURE__ */ jsx(Fragment, { children: node.children.map((tspan, i) => /* @__PURE__ */ jsx(
1373
+ Text,
1374
+ {
1375
+ x: tspan.x != null ? toNum(tspan.x) : node.x != null ? toNum(node.x) : 0,
1376
+ y: tspan.dy != null ? (node.y != null ? toNum(node.y) : fs) + toNum(tspan.dy) : (node.y != null ? toNum(node.y) : fs) + i * fs * 1.2,
1377
+ text: tspan.text,
1378
+ color: fill,
1379
+ font
1380
+ },
1381
+ i
1382
+ )) });
1383
+ }
1384
+ case "polygon": {
1385
+ const fill = node.fill ?? "black";
1386
+ return /* @__PURE__ */ jsx(
1387
+ Path,
1388
+ {
1389
+ path: polygonPointsToPath(node.points),
1390
+ color: fill,
1391
+ style: "fill",
1392
+ antiAlias: true
1393
+ }
1394
+ );
1395
+ }
1396
+ case "g": {
1397
+ const transform = node.transform ? parseSvgTransform(node.transform) : void 0;
1398
+ return /* @__PURE__ */ jsx(Group, { transform, children: node.children.map((child, i) => /* @__PURE__ */ jsx(SvgNodeItem, { node: child }, i)) });
1399
+ }
1400
+ case "defs": {
1401
+ return null;
1402
+ }
1403
+ default:
1404
+ return null;
1405
+ }
1406
+ }
1407
+ function polygonPointsToPath(points) {
1408
+ const nums = points.split(/[\s,]+/).filter(Boolean).map(Number);
1409
+ if (nums.length < 4) return "";
1410
+ let d = `M${nums[0]} ${nums[1]}`;
1411
+ for (let i = 2; i < nums.length; i += 2) {
1412
+ d += ` L${nums[i]} ${nums[i + 1]}`;
1413
+ }
1414
+ d += " Z";
1415
+ return d;
1416
+ }
1417
+
1418
+ // src/native/svg-parser.ts
1419
+ var SELF_CLOSING = /* @__PURE__ */ new Set([
1420
+ "rect",
1421
+ "ellipse",
1422
+ "circle",
1423
+ "line",
1424
+ "path",
1425
+ "polygon",
1426
+ "image"
1427
+ ]);
1428
+ var ATTR_RE = /([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*"([^"]*)"/g;
1429
+ function parseAttrs(raw) {
1430
+ const attrs = {};
1431
+ let m = ATTR_RE.exec(raw);
1432
+ while (m !== null) {
1433
+ const key = m[1];
1434
+ const val = m[2];
1435
+ if (key !== void 0 && val !== void 0) {
1436
+ attrs[key] = val;
1437
+ }
1438
+ m = ATTR_RE.exec(raw);
1439
+ }
1440
+ return attrs;
1441
+ }
1442
+ function parseNum2(val) {
1443
+ if (val === void 0) return void 0;
1444
+ const n = Number(val);
1445
+ if (Number.isNaN(n)) return val;
1446
+ return n;
1447
+ }
1448
+ function parseNumR(val, fallback) {
1449
+ return parseNum2(val) ?? fallback;
1450
+ }
1451
+ function parseNumOpt(val) {
1452
+ if (val === void 0) return void 0;
1453
+ const n = Number(val);
1454
+ return Number.isNaN(n) ? void 0 : n;
1455
+ }
1456
+ function parseRectNode(attrs) {
1457
+ return {
1458
+ kind: "rect",
1459
+ width: parseNumR(attrs.width, "0"),
1460
+ height: parseNumR(attrs.height, "0"),
1461
+ fill: attrs.fill,
1462
+ fillOpacity: parseNumOpt(attrs["fill-opacity"]),
1463
+ stroke: attrs.stroke,
1464
+ strokeWidth: parseNum2(attrs["stroke-width"]),
1465
+ strokeOpacity: parseNumOpt(attrs["stroke-opacity"]),
1466
+ rx: parseNum2(attrs.rx),
1467
+ shapeRendering: attrs["shape-rendering"]
1468
+ };
1469
+ }
1470
+ function parseEllipseNode(attrs) {
1471
+ return {
1472
+ kind: "ellipse",
1473
+ cx: parseNumR(attrs.cx, "0"),
1474
+ cy: parseNumR(attrs.cy, "0"),
1475
+ rx: parseNumR(attrs.rx, "0"),
1476
+ ry: parseNumR(attrs.ry, "0"),
1477
+ fill: attrs.fill,
1478
+ fillOpacity: parseNumOpt(attrs["fill-opacity"]),
1479
+ stroke: attrs.stroke,
1480
+ strokeWidth: parseNum2(attrs["stroke-width"]),
1481
+ strokeOpacity: parseNumOpt(attrs["stroke-opacity"]),
1482
+ shapeRendering: attrs["shape-rendering"]
1483
+ };
1484
+ }
1485
+ function parseCircleNode(attrs) {
1486
+ return {
1487
+ kind: "circle",
1488
+ cx: parseNumR(attrs.cx, "0"),
1489
+ cy: parseNumR(attrs.cy, "0"),
1490
+ r: parseNumR(attrs.r, "0"),
1491
+ fill: attrs.fill,
1492
+ fillOpacity: parseNumOpt(attrs["fill-opacity"]),
1493
+ stroke: attrs.stroke,
1494
+ strokeWidth: parseNum2(attrs["stroke-width"]),
1495
+ strokeOpacity: parseNumOpt(attrs["stroke-opacity"]),
1496
+ shapeRendering: attrs["shape-rendering"]
1497
+ };
1498
+ }
1499
+ function parseLineNode(attrs) {
1500
+ return {
1501
+ kind: "line",
1502
+ x1: parseNumR(attrs.x1, "0"),
1503
+ y1: parseNumR(attrs.y1, "0"),
1504
+ x2: parseNumR(attrs.x2, "0"),
1505
+ y2: parseNumR(attrs.y2, "0"),
1506
+ stroke: attrs.stroke,
1507
+ strokeWidth: parseNum2(attrs["stroke-width"]),
1508
+ strokeOpacity: parseNumOpt(attrs["stroke-opacity"]),
1509
+ strokeLinecap: attrs["stroke-linecap"],
1510
+ strokeLinejoin: attrs["stroke-linejoin"],
1511
+ markerEnd: attrs["marker-end"],
1512
+ shapeRendering: attrs["shape-rendering"]
1513
+ };
1514
+ }
1515
+ function parsePathNode(attrs) {
1516
+ return {
1517
+ kind: "path",
1518
+ d: attrs.d ?? "",
1519
+ fill: attrs.fill,
1520
+ fillOpacity: parseNumOpt(attrs["fill-opacity"]),
1521
+ fillRule: attrs["fill-rule"],
1522
+ stroke: attrs.stroke,
1523
+ strokeWidth: parseNum2(attrs["stroke-width"]),
1524
+ strokeOpacity: parseNumOpt(attrs["stroke-opacity"]),
1525
+ strokeLinecap: attrs["stroke-linecap"],
1526
+ strokeLinejoin: attrs["stroke-linejoin"],
1527
+ shapeRendering: attrs["shape-rendering"],
1528
+ vectorEffect: attrs["vector-effect"]
1529
+ };
1530
+ }
1531
+ function parsePolygonNode(attrs) {
1532
+ return {
1533
+ kind: "polygon",
1534
+ points: attrs.points ?? "",
1535
+ fill: attrs.fill
1536
+ };
1537
+ }
1538
+ function parseImageNode(attrs) {
1539
+ return {
1540
+ kind: "image",
1541
+ href: attrs.href ?? "",
1542
+ x: parseNumR(attrs.x, "0"),
1543
+ y: parseNumR(attrs.y, "0"),
1544
+ width: parseNumR(attrs.width, "0"),
1545
+ height: parseNumR(attrs.height, "0")
1546
+ };
1547
+ }
1548
+ function parseTextNode(attrs, children) {
1549
+ return {
1550
+ kind: "text",
1551
+ x: parseNum2(attrs.x),
1552
+ y: parseNum2(attrs.y),
1553
+ fill: attrs.fill,
1554
+ textAnchor: attrs["text-anchor"],
1555
+ fontSize: parseNum2(attrs["font-size"]),
1556
+ fontFamily: attrs["font-family"],
1557
+ children
1558
+ };
1559
+ }
1560
+ function parseTspanNode(attrs, textContent) {
1561
+ return {
1562
+ kind: "tspan",
1563
+ x: parseNum2(attrs.x),
1564
+ dy: parseNum2(attrs.dy),
1565
+ text: textContent
1566
+ };
1567
+ }
1568
+ function parseSingleElement(tag, attrs, children, innerText) {
1569
+ switch (tag) {
1570
+ case "rect":
1571
+ return parseRectNode(attrs);
1572
+ case "ellipse":
1573
+ return parseEllipseNode(attrs);
1574
+ case "circle":
1575
+ return parseCircleNode(attrs);
1576
+ case "line":
1577
+ return parseLineNode(attrs);
1578
+ case "path":
1579
+ return parsePathNode(attrs);
1580
+ case "polygon":
1581
+ return parsePolygonNode(attrs);
1582
+ case "image":
1583
+ return parseImageNode(attrs);
1584
+ case "text":
1585
+ return parseTextNode(attrs, parseTspanChildren(innerText));
1586
+ case "g":
1587
+ return { kind: "g", transform: attrs.transform, children };
1588
+ case "defs":
1589
+ return { kind: "defs", children };
1590
+ case "marker":
1591
+ return parseMarkerNode(attrs, children);
1592
+ default:
1593
+ return null;
1594
+ }
1595
+ }
1596
+ function parseTspanChildren(innerXml) {
1597
+ const result = [];
1598
+ if (!innerXml.includes("<tspan")) {
1599
+ const trimmed = innerXml.trim();
1600
+ if (trimmed) {
1601
+ result.push({ kind: "tspan", text: trimmed });
1602
+ }
1603
+ return result;
1604
+ }
1605
+ const tagRe = /<tspan\b([^>]*)>([\s\S]*?)<\/tspan>/g;
1606
+ let m = tagRe.exec(innerXml);
1607
+ while (m !== null) {
1608
+ const rawAttrs = m[1] ?? "";
1609
+ const text = (m[2] ?? "").trim();
1610
+ result.push(parseTspanNode(parseAttrs(rawAttrs), text));
1611
+ m = tagRe.exec(innerXml);
1612
+ }
1613
+ return result;
1614
+ }
1615
+ function parseMarkerNode(attrs, children) {
1616
+ return {
1617
+ kind: "marker",
1618
+ id: attrs.id ?? "",
1619
+ markerWidth: parseNumR(attrs.markerWidth, "8"),
1620
+ markerHeight: parseNumR(attrs.markerHeight, "8"),
1621
+ refX: parseNumR(attrs.refX, "6"),
1622
+ refY: parseNumR(attrs.refY, "4"),
1623
+ orient: attrs.orient ?? "auto",
1624
+ children: children.filter((c) => c.kind === "polygon")
1625
+ };
1626
+ }
1627
+ function stripComments(xml) {
1628
+ return xml.replace(/<!--[\s\S]*?-->/g, "");
1629
+ }
1630
+ function parseSvgFragment(xml) {
1631
+ if (!xml || xml.trim().length === 0) return [];
1632
+ const clean = stripComments(xml.trim());
1633
+ const nodes = [];
1634
+ let pos = 0;
1635
+ while (pos < clean.length) {
1636
+ const openStart = clean.indexOf("<", pos);
1637
+ if (openStart === -1) break;
1638
+ if (clean[openStart + 1] === "/") {
1639
+ pos = openStart + 1;
1640
+ continue;
1641
+ }
1642
+ const openEnd = clean.indexOf(">", openStart);
1643
+ if (openEnd === -1) break;
1644
+ const openTag = clean.slice(openStart + 1, openEnd);
1645
+ const spaceIdx = openTag.indexOf(" ");
1646
+ openTag.indexOf("/");
1647
+ const tagName = (spaceIdx === -1 ? openTag : openTag.slice(0, spaceIdx)).replace(/\/$/, "").trim();
1648
+ const hasCloseSlash = openTag.endsWith("/");
1649
+ if (SELF_CLOSING.has(tagName) || hasCloseSlash) {
1650
+ const attrs2 = spaceIdx === -1 ? {} : parseAttrs(openTag.slice(spaceIdx + 1).replace(/\/$/, ""));
1651
+ const node2 = parseSingleElement(tagName, attrs2, [], "");
1652
+ if (node2) nodes.push(node2);
1653
+ pos = openEnd + 1;
1654
+ continue;
1655
+ }
1656
+ const closeTag = `</${tagName}>`;
1657
+ const closeIdx = clean.indexOf(closeTag, openEnd);
1658
+ if (closeIdx === -1) {
1659
+ pos = openEnd + 1;
1660
+ continue;
1661
+ }
1662
+ const innerXml = clean.slice(openEnd + 1, closeIdx);
1663
+ const attrs = spaceIdx === -1 ? {} : parseAttrs(openTag.slice(spaceIdx + 1));
1664
+ const children = parseSvgFragment(innerXml);
1665
+ const node = parseSingleElement(tagName, attrs, children, innerXml);
1666
+ if (node) nodes.push(node);
1667
+ pos = closeIdx + closeTag.length;
1668
+ }
1669
+ return nodes;
1670
+ }
1671
+ function rgba(hex, alpha) {
1672
+ if (alpha == null || alpha >= 1) return hex;
1673
+ const r = parseInt(hex.slice(1, 3), 16);
1674
+ const g = parseInt(hex.slice(3, 5), 16);
1675
+ const b = parseInt(hex.slice(5, 7), 16);
1676
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return hex;
1677
+ return `rgba(${r},${g},${b},${alpha})`;
1678
+ }
1679
+ function localBounds(bounds) {
1680
+ const b = normalizeRect(bounds);
1681
+ return { w: Math.max(0, b.width), h: Math.max(0, b.height) };
1682
+ }
1683
+ function NativeShapeRenderer({ item }) {
1684
+ const style = resolveStrokeStyle(item);
1685
+ const k = item.toolKind;
1686
+ if (k === "rect") {
1687
+ const { w, h } = localBounds(item.bounds);
1688
+ return /* @__PURE__ */ jsx(
1689
+ RoundedRect,
1690
+ {
1691
+ x: 0,
1692
+ y: 0,
1693
+ width: w,
1694
+ height: h,
1695
+ r: 4,
1696
+ color: style.stroke,
1697
+ style: "stroke",
1698
+ strokeWidth: style.strokeWidth,
1699
+ antiAlias: true
1700
+ }
1701
+ );
1702
+ }
1703
+ if (k === "ellipse") {
1704
+ const { w, h } = localBounds(item.bounds);
1705
+ const rx = Math.max(0, w / 2);
1706
+ const ry = Math.max(0, h / 2);
1707
+ if (Math.abs(rx - ry) < 1e-9) {
1708
+ return /* @__PURE__ */ jsx(
1709
+ Circle,
1710
+ {
1711
+ cx: rx,
1712
+ cy: ry,
1713
+ r: rx,
1714
+ color: style.stroke,
1715
+ style: "stroke",
1716
+ strokeWidth: style.strokeWidth,
1717
+ antiAlias: true
1718
+ }
1719
+ );
1720
+ }
1721
+ return /* @__PURE__ */ jsx(
1722
+ Circle,
1723
+ {
1724
+ cx: rx,
1725
+ cy: ry,
1726
+ r: Math.min(rx, ry),
1727
+ color: style.stroke,
1728
+ style: "stroke",
1729
+ strokeWidth: style.strokeWidth,
1730
+ antiAlias: true
1731
+ }
1732
+ );
1733
+ }
1734
+ if ((k === "line" || k === "arrow") && item.line) {
1735
+ const ln = item.line;
1736
+ const color = rgba(style.stroke, style.strokeOpacity);
1737
+ if (k === "line") {
1738
+ return /* @__PURE__ */ jsx(
1739
+ Line,
1740
+ {
1741
+ p1: vec(ln.x1, ln.y1),
1742
+ p2: vec(ln.x2, ln.y2),
1743
+ color,
1744
+ style: "stroke",
1745
+ strokeWidth: style.strokeWidth,
1746
+ strokeCap: "round",
1747
+ antiAlias: true
1748
+ }
1749
+ );
1750
+ }
1751
+ const dx = ln.x2 - ln.x1;
1752
+ const dy = ln.y2 - ln.y1;
1753
+ const len = Math.hypot(dx, dy);
1754
+ if (len < 1e-6) {
1755
+ return /* @__PURE__ */ jsx(
1756
+ Line,
1757
+ {
1758
+ p1: vec(ln.x1, ln.y1),
1759
+ p2: vec(ln.x2, ln.y2),
1760
+ color,
1761
+ style: "stroke",
1762
+ strokeWidth: style.strokeWidth,
1763
+ strokeCap: "round",
1764
+ antiAlias: true
1765
+ }
1766
+ );
1767
+ }
1768
+ const geometry = computeStraightArrowGeometry(ln, style.strokeWidth);
1769
+ if (!geometry) {
1770
+ return /* @__PURE__ */ jsx(
1771
+ Line,
1772
+ {
1773
+ p1: vec(ln.x1, ln.y1),
1774
+ p2: vec(ln.x2, ln.y2),
1775
+ color,
1776
+ style: "stroke",
1777
+ strokeWidth: style.strokeWidth,
1778
+ strokeCap: "round",
1779
+ antiAlias: true
1780
+ }
1781
+ );
1782
+ }
1783
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1784
+ /* @__PURE__ */ jsx(
1785
+ Line,
1786
+ {
1787
+ p1: vec(ln.x1, ln.y1),
1788
+ p2: vec(geometry.shaftEndX, geometry.shaftEndY),
1789
+ color,
1790
+ style: "stroke",
1791
+ strokeWidth: style.strokeWidth,
1792
+ strokeCap: "round",
1793
+ antiAlias: true
1794
+ }
1795
+ ),
1796
+ /* @__PURE__ */ jsx(
1797
+ Path,
1798
+ {
1799
+ path: `M ${geometry.headLeftX} ${geometry.headLeftY} L ${geometry.headTipX} ${geometry.headTipY} L ${geometry.headRightX} ${geometry.headRightY}`,
1800
+ color,
1801
+ style: "stroke",
1802
+ strokeWidth: style.strokeWidth,
1803
+ strokeCap: "round",
1804
+ strokeJoin: "round",
1805
+ antiAlias: true
1806
+ }
1807
+ )
1808
+ ] });
1809
+ }
1810
+ if (k === "text" && item.text !== void 0) {
1811
+ const fs = item.textFontSize ?? 16;
1812
+ const color = rgba(style.stroke, style.strokeOpacity);
1813
+ const lines = item.text.split("\n");
1814
+ const font = matchFont({ fontSize: fs });
1815
+ return /* @__PURE__ */ jsx(Fragment, { children: lines.map((line, i) => /* @__PURE__ */ jsx(
1816
+ Text,
1817
+ {
1818
+ x: 0,
1819
+ y: fs + i * fs * 1.2,
1820
+ text: line,
1821
+ color,
1822
+ font
1823
+ },
1824
+ i
1825
+ )) });
1826
+ }
1827
+ if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item.pathPointsLocal && item.pathPointsLocal.length > 0) {
1828
+ const payload = computeFreehandSvgPayload(item.pathPointsLocal, style, k);
1829
+ if (!payload) return null;
1830
+ const color = rgba(style.stroke, style.strokeOpacity);
1831
+ if (payload.kind === "circle") {
1832
+ return /* @__PURE__ */ jsx(
1833
+ Circle,
1834
+ {
1835
+ cx: payload.cx,
1836
+ cy: payload.cy,
1837
+ r: payload.r,
1838
+ color: rgba(payload.fill, payload.fillOpacity),
1839
+ style: "fill",
1840
+ antiAlias: true
1841
+ }
1842
+ );
1843
+ }
1844
+ if (payload.kind === "fillPath") {
1845
+ return /* @__PURE__ */ jsx(
1846
+ Path,
1847
+ {
1848
+ path: payload.d,
1849
+ color: rgba(payload.fill, payload.fillOpacity),
1850
+ style: "fill",
1851
+ fillType: "winding",
1852
+ antiAlias: true
1853
+ }
1854
+ );
1855
+ }
1856
+ return /* @__PURE__ */ jsx(
1857
+ Path,
1858
+ {
1859
+ path: payload.d,
1860
+ color,
1861
+ style: "stroke",
1862
+ strokeWidth: payload.strokeWidth,
1863
+ strokeCap: "round",
1864
+ strokeJoin: "round",
1865
+ antiAlias: true
1866
+ }
1867
+ );
1868
+ }
1869
+ if (k === "draw") {
1870
+ const { w, h } = localBounds(item.bounds);
1871
+ const r = Math.min(w, h) / 2;
1872
+ return /* @__PURE__ */ jsx(
1873
+ Circle,
1874
+ {
1875
+ cx: Math.max(0, w) / 2,
1876
+ cy: Math.max(0, h) / 2,
1877
+ r: Math.max(0, r),
1878
+ color: rgba(style.stroke, style.strokeOpacity),
1879
+ style: "fill",
1880
+ antiAlias: true
1881
+ }
1882
+ );
1883
+ }
1884
+ if (k === "image" || k === "custom" || item.childrenSvg) {
1885
+ const nodes = parseSvgFragment(item.childrenSvg);
1886
+ if (nodes.length > 0) {
1887
+ return /* @__PURE__ */ jsx(SvgNodeRenderer, { nodes });
1888
+ }
1889
+ }
1890
+ return null;
1891
+ }
1892
+ var MemoShape = memo(function MemoShape2({
1893
+ item
1894
+ }) {
1895
+ const b = normalizeRect(item.bounds);
1896
+ const cx = b.width / 2;
1897
+ const cy = b.height / 2;
1898
+ const itemTransform = skiaItemPlacementTransform(
1899
+ item.x,
1900
+ item.y,
1901
+ cx,
1902
+ cy,
1903
+ item.rotation ?? 0
1904
+ );
1905
+ return /* @__PURE__ */ jsx(Group, { transform: itemTransform, children: /* @__PURE__ */ jsx(NativeShapeRenderer, { item }) });
1906
+ });
1907
+ function NativeSceneRenderer({
1908
+ items,
1909
+ camera,
1910
+ width,
1911
+ height
1912
+ }) {
1913
+ const cameraTransform = skiaCameraTransform(camera.zoom, camera.x, camera.y);
1914
+ const visible = cullItemsByViewport(
1915
+ items,
1916
+ camera.getVisibleWorldRect(width, height)
1917
+ );
1918
+ if (width <= 0 || height <= 0) return null;
1919
+ return /* @__PURE__ */ jsx(Canvas, { style: { width, height }, children: /* @__PURE__ */ jsx(Group, { transform: cameraTransform, children: visible.map((item) => /* @__PURE__ */ jsx(MemoShape, { item }, item.id)) }) });
1920
+ }
1921
+
1922
+ // src/camera/camera.ts
1923
+ var Camera2D = class {
1924
+ x = 0;
1925
+ y = 0;
1926
+ /** Scale: world units per CSS pixel (larger = more zoomed in). */
1927
+ zoom = 1;
1928
+ minZoom;
1929
+ maxZoom;
1930
+ constructor(options = {}) {
1931
+ this.minZoom = options.minZoom ?? 0.05;
1932
+ this.maxZoom = options.maxZoom ?? 32;
1933
+ }
1934
+ /**
1935
+ * Converts a point from world coordinates to CSS pixel coordinates relative to the viewport top-left.
1936
+ */
1937
+ worldToScreen(worldX, worldY) {
1938
+ return {
1939
+ screenX: worldX * this.zoom + this.x,
1940
+ screenY: worldY * this.zoom + this.y
1941
+ };
1942
+ }
1943
+ /**
1944
+ * Converts a point from CSS pixel coordinates (viewport-relative) to world coordinates.
1945
+ */
1946
+ screenToWorld(screenX, screenY) {
1947
+ const z = this.zoom;
1948
+ if (z === 0) {
1949
+ return { worldX: 0, worldY: 0 };
1950
+ }
1951
+ return {
1952
+ worldX: (screenX - this.x) / z,
1953
+ worldY: (screenY - this.y) / z
1954
+ };
1955
+ }
1956
+ /**
1957
+ * Sets zoom, clamped to `[minZoom, maxZoom]`, optionally anchoring a screen point so it stays under the cursor.
1958
+ */
1959
+ setZoom(nextZoom, anchorScreen) {
1960
+ const clamped = Math.min(this.maxZoom, Math.max(this.minZoom, nextZoom));
1961
+ if (!anchorScreen) {
1962
+ this.zoom = clamped;
1963
+ return;
1964
+ }
1965
+ const anchorWorld = this.screenToWorld(anchorScreen.x, anchorScreen.y);
1966
+ this.zoom = clamped;
1967
+ this.x = anchorScreen.x - anchorWorld.worldX * this.zoom;
1968
+ this.y = anchorScreen.y - anchorWorld.worldY * this.zoom;
1969
+ }
1970
+ /**
1971
+ * Returns the world-space rectangle visible in a viewport of `viewportWidth` x `viewportHeight` CSS pixels.
1972
+ */
1973
+ getVisibleWorldRect(viewportWidth, viewportHeight) {
1974
+ const topLeft = this.screenToWorld(0, 0);
1975
+ const bottomRight = this.screenToWorld(viewportWidth, viewportHeight);
1976
+ return normalizeRect({
1977
+ x: topLeft.worldX,
1978
+ y: topLeft.worldY,
1979
+ width: bottomRight.worldX - topLeft.worldX,
1980
+ height: bottomRight.worldY - topLeft.worldY
1981
+ });
1982
+ }
1983
+ };
1984
+
1985
+ // src/interaction/hit-test.ts
1986
+ function pointInLocalRect(lx, ly, w, h) {
1987
+ return lx >= 0 && lx <= w && ly >= 0 && ly <= h;
1988
+ }
1989
+ function pointInLocalEllipse(lx, ly, w, h) {
1990
+ const rx = w / 2;
1991
+ const ry = h / 2;
1992
+ if (rx < 1e-9 || ry < 1e-9) {
1993
+ return false;
1994
+ }
1995
+ const cx = w / 2;
1996
+ const cy = h / 2;
1997
+ const nx = (lx - cx) / rx;
1998
+ const ny = (ly - cy) / ry;
1999
+ return nx * nx + ny * ny <= 1;
2000
+ }
2001
+ function distancePointToSegment(px, py, ax, ay, bx, by) {
2002
+ const abx = bx - ax;
2003
+ const aby = by - ay;
2004
+ const apx = px - ax;
2005
+ const apy = py - ay;
2006
+ const abLenSq = abx * abx + aby * aby;
2007
+ if (abLenSq < 1e-12) {
2008
+ return Math.hypot(px - ax, py - ay);
2009
+ }
2010
+ let t = (apx * abx + apy * aby) / abLenSq;
2011
+ t = Math.max(0, Math.min(1, t));
2012
+ const qx = ax + t * abx;
2013
+ const qy = ay + t * aby;
2014
+ return Math.hypot(px - qx, py - qy);
2015
+ }
2016
+ function hitTestFilledShape(item, worldX, worldY) {
2017
+ const b = normalizeRect(item.bounds);
2018
+ const w = b.width;
2019
+ const h = b.height;
2020
+ const rot = getItemRotationRad(item);
2021
+ const pl = worldToItemLocal(worldX, worldY, item.x, item.y, w, h, rot);
2022
+ if (item.toolKind === "ellipse") {
2023
+ return pointInLocalEllipse(pl.x, pl.y, w, h);
2024
+ }
2025
+ return pointInLocalRect(pl.x, pl.y, w, h);
2026
+ }
2027
+ function itemHitTestWorldPoint(item, worldX, worldY, options) {
2028
+ const lineHit = options?.lineHitWorld ?? 8;
2029
+ const lineHitSq = lineHit * lineHit;
2030
+ const b = normalizeRect(item.bounds);
2031
+ const w = b.width;
2032
+ const h = b.height;
2033
+ const rot = getItemRotationRad(item);
2034
+ if (item.toolKind === "line" || item.toolKind === "arrow") {
2035
+ const ln = item.line;
2036
+ if (ln) {
2037
+ const a = itemLocalToWorld(ln.x1, ln.y1, item.x, item.y, w, h, rot);
2038
+ const p2 = itemLocalToWorld(ln.x2, ln.y2, item.x, item.y, w, h, rot);
2039
+ const d = distancePointToSegment(worldX, worldY, a.x, a.y, p2.x, p2.y);
2040
+ if (d * d <= lineHitSq) {
2041
+ return true;
2042
+ }
2043
+ } else if (hitTestFilledShape(item, worldX, worldY)) {
2044
+ return true;
2045
+ }
2046
+ return false;
2047
+ }
2048
+ if (item.toolKind === "draw" || item.toolKind === "pencil" || item.toolKind === "brush" || item.toolKind === "marker") {
2049
+ const pts = item.pathPointsLocal;
2050
+ const halfW = Math.max((item.strokeWidth ?? 2) / 2, lineHit * 0.5);
2051
+ const tol = Math.max(lineHit, halfW);
2052
+ const tolSq = tol * tol;
2053
+ if (pts && pts.length >= 2) {
2054
+ for (let j = 0; j < pts.length - 1; j++) {
2055
+ const a = pts[j];
2056
+ const p2 = pts[j + 1];
2057
+ if (!a || !p2) continue;
2058
+ const aw = itemLocalToWorld(a.x, a.y, item.x, item.y, w, h, rot);
2059
+ const bw = itemLocalToWorld(p2.x, p2.y, item.x, item.y, w, h, rot);
2060
+ const d = distancePointToSegment(worldX, worldY, aw.x, aw.y, bw.x, bw.y);
2061
+ if (d * d <= tolSq) {
2062
+ return true;
2063
+ }
2064
+ }
2065
+ }
2066
+ if (pts?.length === 1) {
2067
+ const p = pts[0];
2068
+ if (p) {
2069
+ const cw = itemLocalToWorld(p.x, p.y, item.x, item.y, w, h, rot);
2070
+ const dsq = (worldX - cw.x) ** 2 + (worldY - cw.y) ** 2;
2071
+ if (dsq <= tolSq) {
2072
+ return true;
2073
+ }
2074
+ }
2075
+ }
2076
+ return hitTestFilledShape(item, worldX, worldY);
2077
+ }
2078
+ return hitTestFilledShape(item, worldX, worldY);
2079
+ }
2080
+ function hitTestWorldPoint(items, worldX, worldY, options) {
2081
+ for (let i = items.length - 1; i >= 0; i--) {
2082
+ const item = items[i];
2083
+ if (item === void 0) continue;
2084
+ if (options?.ignoreLocked && item.locked) continue;
2085
+ if (itemHitTestWorldPoint(item, worldX, worldY, options)) {
2086
+ return item;
2087
+ }
2088
+ }
2089
+ return null;
2090
+ }
2091
+ function collectEraserTargetsAtWorldPoint(items, worldX, worldY, options) {
2092
+ const topHit = hitTestWorldPoint(items, worldX, worldY, {
2093
+ ...options,
2094
+ ignoreLocked: true
2095
+ });
2096
+ if (!topHit) return [];
2097
+ const ids = [];
2098
+ const seen = /* @__PURE__ */ new Set();
2099
+ for (let i = 0; i < items.length; i++) {
2100
+ const item = items[i];
2101
+ if (item === void 0 || item.locked) continue;
2102
+ if (!itemHitTestWorldPoint(item, worldX, worldY, options)) continue;
2103
+ if (!seen.has(item.id)) {
2104
+ seen.add(item.id);
2105
+ ids.push(item.id);
2106
+ }
2107
+ }
2108
+ return ids;
2109
+ }
2110
+ function collectIdsInRect(items, marquee) {
2111
+ const m = normalizeRect(marquee);
2112
+ const out = [];
2113
+ for (const it of items) {
2114
+ if (it.locked) continue;
2115
+ if (rectsIntersect(normalizeRect(boundsAabbForRotatedItem(it)), m)) {
2116
+ out.push(it.id);
2117
+ }
2118
+ }
2119
+ return out;
2120
+ }
2121
+ function fitCameraToWorldRect(camera, viewportW, viewportH, worldRect, padding) {
2122
+ const r = normalizeRect(worldRect);
2123
+ if (r.width <= 0 || r.height <= 0 || viewportW <= 0 || viewportH <= 0) return;
2124
+ const pad = Math.max(0, padding);
2125
+ const neededW = r.width * (1 + 2 * pad);
2126
+ const neededH = r.height * (1 + 2 * pad);
2127
+ const zoomX = viewportW / neededW;
2128
+ const zoomY = viewportH / neededH;
2129
+ let z = Math.min(zoomX, zoomY);
2130
+ z = Math.min(camera.maxZoom, Math.max(camera.minZoom, z));
2131
+ const cx = r.x + r.width / 2;
2132
+ const cy = r.y + r.height / 2;
2133
+ camera.zoom = z;
2134
+ camera.x = viewportW / 2 - cx * z;
2135
+ camera.y = viewportH / 2 - cy * z;
2136
+ }
2137
+ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
2138
+ items,
2139
+ selectedIds = [],
2140
+ toolId = "hand",
2141
+ interactive = false,
2142
+ onSelectionChange,
2143
+ onItemsChange,
2144
+ onCameraChange,
2145
+ toolbar
2146
+ }, ref) {
2147
+ const [size, setSize] = useState({ width: 0, height: 0 });
2148
+ const cameraRef = useRef(null);
2149
+ const toolIdRef = useRef(toolId);
2150
+ toolIdRef.current = toolId;
2151
+ const onCameraChangeRef = useRef(onCameraChange);
2152
+ onCameraChangeRef.current = onCameraChange;
2153
+ const onItemsChangeRef = useRef(onItemsChange);
2154
+ onItemsChangeRef.current = onItemsChange;
2155
+ const onSelectionChangeRef = useRef(onSelectionChange);
2156
+ onSelectionChangeRef.current = onSelectionChange;
2157
+ const itemsRef = useRef(items);
2158
+ itemsRef.current = items;
2159
+ const selectedIdsRef = useRef(selectedIds);
2160
+ selectedIdsRef.current = selectedIds;
2161
+ const dragStateRef = useRef({ kind: "idle" });
2162
+ const [placementPreview, setPlacementPreview] = useState(
2163
+ null
2164
+ );
2165
+ const [eraserTrail, setEraserTrail] = useState([]);
2166
+ const [eraserPreviewIds, setEraserPreviewIds] = useState([]);
2167
+ const eraserPreviewIdSetRef = useRef(/* @__PURE__ */ new Set());
2168
+ if (!cameraRef.current) {
2169
+ cameraRef.current = new Camera2D({ minZoom: 0.05, maxZoom: 32 });
2170
+ }
2171
+ const camera = cameraRef.current;
2172
+ const [cameraTick, setCameraTick] = useState(0);
2173
+ const screenToWorld = useCallback(
2174
+ (sx, sy) => {
2175
+ const cam = cameraRef.current;
2176
+ if (!cam) return { worldX: 0, worldY: 0 };
2177
+ return cam.screenToWorld(sx, sy);
2178
+ },
2179
+ []
2180
+ );
2181
+ const requestRender = useCallback(() => {
2182
+ setCameraTick((n) => n + 1);
2183
+ onCameraChangeRef.current?.();
2184
+ }, []);
2185
+ const onLayout = useCallback((e) => {
2186
+ const { width, height } = e.nativeEvent.layout;
2187
+ setSize({ width, height });
2188
+ }, []);
2189
+ const selectedItems = useMemo(
2190
+ () => items.filter((it) => selectedIds.includes(it.id)),
2191
+ [items, selectedIds]
2192
+ );
2193
+ const showResizeHandles = interactive && selectedItems.length === 1 && !selectedItems[0]?.locked;
2194
+ const lastPinchDist = useRef(null);
2195
+ const lastPanPoint = useRef(null);
2196
+ const panResponder = useMemo(
2197
+ () => PanResponder.create({
2198
+ onStartShouldSetPanResponder: () => true,
2199
+ onMoveShouldSetPanResponder: () => true,
2200
+ onPanResponderGrant: (evt) => {
2201
+ lastPinchDist.current = null;
2202
+ lastPanPoint.current = null;
2203
+ const touches = evt.nativeEvent.touches;
2204
+ const sx = evt.nativeEvent.locationX;
2205
+ const sy = evt.nativeEvent.locationY;
2206
+ if (touches && touches.length >= 2) {
2207
+ dragStateRef.current = { kind: "pan" };
2208
+ return;
2209
+ }
2210
+ if (!interactive) {
2211
+ dragStateRef.current = { kind: "pan" };
2212
+ return;
2213
+ }
2214
+ const tool = toolIdRef.current;
2215
+ const cam = cameraRef.current;
2216
+ if (!cam) return;
2217
+ const { worldX, worldY } = screenToWorld(sx, sy);
2218
+ if (tool === "hand") {
2219
+ dragStateRef.current = { kind: "pan" };
2220
+ return;
2221
+ }
2222
+ if (tool === "select") {
2223
+ const hit = hitTestWorldPoint(itemsRef.current, worldX, worldY, {
2224
+ lineHitWorld: 10 / cam.zoom,
2225
+ ignoreLocked: true
2226
+ });
2227
+ if (hit) {
2228
+ const cur = selectedIdsRef.current;
2229
+ const ids = cur.includes(hit.id) ? [...cur] : [hit.id];
2230
+ const snapshots = {};
2231
+ for (const id of ids) {
2232
+ const it = itemsRef.current.find((i) => i.id === id);
2233
+ if (it) snapshots[id] = it;
2234
+ }
2235
+ dragStateRef.current = {
2236
+ kind: "move",
2237
+ ids,
2238
+ snapshots,
2239
+ startWorld: { x: worldX, y: worldY }
2240
+ };
2241
+ if (!cur.includes(hit.id)) {
2242
+ onSelectionChangeRef.current?.([hit.id]);
2243
+ }
2244
+ } else {
2245
+ onSelectionChangeRef.current?.([]);
2246
+ dragStateRef.current = {
2247
+ kind: "marquee",
2248
+ startWorld: { x: worldX, y: worldY }
2249
+ };
2250
+ setPlacementPreview({
2251
+ kind: "marquee",
2252
+ rect: { x: worldX, y: worldY, width: 0, height: 0 }
2253
+ });
2254
+ }
2255
+ return;
2256
+ }
2257
+ if (tool === "draw" || tool === "marker") {
2258
+ dragStateRef.current = {
2259
+ kind: "draw",
2260
+ tool,
2261
+ points: [{ x: worldX, y: worldY }]
2262
+ };
2263
+ return;
2264
+ }
2265
+ if (tool === "eraser") {
2266
+ dragStateRef.current = { kind: "erase" };
2267
+ eraserPreviewIdSetRef.current = /* @__PURE__ */ new Set();
2268
+ setEraserPreviewIds([]);
2269
+ setEraserTrail([{ x: worldX, y: worldY }]);
2270
+ const toErase = collectEraserTargetsAtWorldPoint(
2271
+ itemsRef.current,
2272
+ worldX,
2273
+ worldY,
2274
+ { lineHitWorld: 10 / cam.zoom, ignoreLocked: true }
2275
+ );
2276
+ for (const id of toErase) {
2277
+ eraserPreviewIdSetRef.current.add(id);
2278
+ }
2279
+ setEraserPreviewIds(Array.from(eraserPreviewIdSetRef.current));
2280
+ return;
2281
+ }
2282
+ if (tool === "note" || tool === "text") {
2283
+ dragStateRef.current = {
2284
+ kind: "tap",
2285
+ startWorld: { x: worldX, y: worldY }
2286
+ };
2287
+ return;
2288
+ }
2289
+ dragStateRef.current = { kind: "pan" };
2290
+ },
2291
+ onPanResponderMove: (evt) => {
2292
+ const cam = cameraRef.current;
2293
+ if (!cam) return;
2294
+ const touches = evt.nativeEvent.touches;
2295
+ const sx = evt.nativeEvent.locationX;
2296
+ const sy = evt.nativeEvent.locationY;
2297
+ const pageX = evt.nativeEvent.pageX;
2298
+ const pageY = evt.nativeEvent.pageY;
2299
+ if (touches && touches.length >= 2) {
2300
+ const t0 = touches[0];
2301
+ const t1 = touches[1];
2302
+ if (t0 && t1) {
2303
+ const dx = t1.pageX - t0.pageX;
2304
+ const dy = t1.pageY - t0.pageY;
2305
+ const dist = Math.hypot(dx, dy);
2306
+ if (lastPinchDist.current != null) {
2307
+ const scale = dist / lastPinchDist.current;
2308
+ const cx = (t0.pageX + t1.pageX) / 2;
2309
+ const cy = (t0.pageY + t1.pageY) / 2;
2310
+ cam.setZoom(cam.zoom * scale, { x: cx, y: cy });
2311
+ requestRender();
2312
+ }
2313
+ lastPinchDist.current = dist;
2314
+ lastPanPoint.current = null;
2315
+ }
2316
+ return;
2317
+ }
2318
+ lastPinchDist.current = null;
2319
+ const { worldX, worldY } = screenToWorld(sx, sy);
2320
+ const st = dragStateRef.current;
2321
+ if (st.kind === "pan") {
2322
+ const current = { x: pageX, y: pageY };
2323
+ if (lastPanPoint.current) {
2324
+ const dx = current.x - lastPanPoint.current.x;
2325
+ const dy = current.y - lastPanPoint.current.y;
2326
+ cam.x += dx;
2327
+ cam.y += dy;
2328
+ requestRender();
2329
+ }
2330
+ lastPanPoint.current = current;
2331
+ return;
2332
+ }
2333
+ lastPanPoint.current = null;
2334
+ if (st.kind === "draw") {
2335
+ const pts = st.points;
2336
+ const last = pts[pts.length - 1];
2337
+ const dx = worldX - (last?.x ?? worldX);
2338
+ const dy = worldY - (last?.y ?? worldY);
2339
+ if (Math.hypot(dx, dy) > 0.5 / cam.zoom) {
2340
+ pts.push({ x: worldX, y: worldY });
2341
+ }
2342
+ setPlacementPreview({
2343
+ kind: "stroke",
2344
+ tool: st.tool,
2345
+ points: [...pts]
2346
+ });
2347
+ return;
2348
+ }
2349
+ if (st.kind === "move") {
2350
+ const dx = worldX - st.startWorld.x;
2351
+ const dy = worldY - st.startWorld.y;
2352
+ const change = onItemsChangeRef.current;
2353
+ if (!change) return;
2354
+ const nextList = itemsRef.current.map((it) => {
2355
+ const snap = st.snapshots[it.id];
2356
+ if (!snap) return it;
2357
+ return {
2358
+ ...snap,
2359
+ x: snap.x + dx,
2360
+ y: snap.y + dy,
2361
+ bounds: {
2362
+ ...snap.bounds,
2363
+ x: snap.bounds.x + dx,
2364
+ y: snap.bounds.y + dy
2365
+ }
2366
+ };
2367
+ });
2368
+ change(nextList);
2369
+ return;
2370
+ }
2371
+ if (st.kind === "marquee") {
2372
+ const a = st.startWorld;
2373
+ const b = { x: worldX, y: worldY };
2374
+ const rect = {
2375
+ x: Math.min(a.x, b.x),
2376
+ y: Math.min(a.y, b.y),
2377
+ width: Math.abs(b.x - a.x),
2378
+ height: Math.abs(b.y - a.y)
2379
+ };
2380
+ setPlacementPreview({ kind: "marquee", rect });
2381
+ return;
2382
+ }
2383
+ if (st.kind === "erase") {
2384
+ setEraserTrail((prev) => [...prev, { x: worldX, y: worldY }]);
2385
+ const toErase = collectEraserTargetsAtWorldPoint(
2386
+ itemsRef.current,
2387
+ worldX,
2388
+ worldY,
2389
+ { lineHitWorld: 10 / cam.zoom, ignoreLocked: true }
2390
+ );
2391
+ for (const id of toErase) {
2392
+ eraserPreviewIdSetRef.current.add(id);
2393
+ }
2394
+ setEraserPreviewIds(Array.from(eraserPreviewIdSetRef.current));
2395
+ return;
2396
+ }
2397
+ },
2398
+ onPanResponderRelease: (evt) => {
2399
+ lastPinchDist.current = null;
2400
+ lastPanPoint.current = null;
2401
+ const st = dragStateRef.current;
2402
+ if (st.kind === "draw") {
2403
+ dragStateRef.current = { kind: "idle" };
2404
+ setPlacementPreview(null);
2405
+ if (st.points.length < 1) return;
2406
+ const change = onItemsChangeRef.current;
2407
+ if (!change) return;
2408
+ const id = createShapeId();
2409
+ const item = createFreehandStrokeItem(id, st.points, st.tool);
2410
+ if (item) {
2411
+ change([...itemsRef.current, item]);
2412
+ }
2413
+ return;
2414
+ }
2415
+ if (st.kind === "move") {
2416
+ dragStateRef.current = { kind: "idle" };
2417
+ return;
2418
+ }
2419
+ if (st.kind === "marquee") {
2420
+ dragStateRef.current = { kind: "idle" };
2421
+ setPlacementPreview(null);
2422
+ const cam = cameraRef.current;
2423
+ if (!cam) return;
2424
+ const { worldX, worldY } = screenToWorld(
2425
+ evt.nativeEvent.locationX,
2426
+ evt.nativeEvent.locationY
2427
+ );
2428
+ const a = st.startWorld;
2429
+ if (typeof worldX !== "number" || typeof worldY !== "number") return;
2430
+ const raw = {
2431
+ x: Math.min(a.x, worldX),
2432
+ y: Math.min(a.y, worldY),
2433
+ width: Math.abs(worldX - a.x),
2434
+ height: Math.abs(worldY - a.y)
2435
+ };
2436
+ const picked = collectIdsInRect(itemsRef.current, raw);
2437
+ onSelectionChangeRef.current?.(picked);
2438
+ return;
2439
+ }
2440
+ if (st.kind === "erase") {
2441
+ const change = onItemsChangeRef.current;
2442
+ if (change && eraserPreviewIdSetRef.current.size > 0) {
2443
+ const idSet = new Set(eraserPreviewIdSetRef.current);
2444
+ change(itemsRef.current.filter((i) => !idSet.has(i.id)));
2445
+ }
2446
+ eraserPreviewIdSetRef.current.clear();
2447
+ setEraserPreviewIds([]);
2448
+ setEraserTrail([]);
2449
+ dragStateRef.current = { kind: "idle" };
2450
+ return;
2451
+ }
2452
+ if (st.kind === "tap") {
2453
+ dragStateRef.current = { kind: "idle" };
2454
+ const change = onItemsChangeRef.current;
2455
+ if (!change) return;
2456
+ const tool = toolIdRef.current;
2457
+ if (tool === "note") {
2458
+ const id = createShapeId();
2459
+ const note = {
2460
+ id,
2461
+ x: st.startWorld.x - 70,
2462
+ y: st.startWorld.y - 40,
2463
+ bounds: {
2464
+ x: st.startWorld.x - 70,
2465
+ y: st.startWorld.y - 40,
2466
+ width: 140,
2467
+ height: 80
2468
+ },
2469
+ childrenSvg: `<rect width="140" height="80" rx="8" fill="#fef08a" stroke="#facc15" stroke-width="1" /><text x="10" y="22" fill="#1f2937" font-size="12" font-family="system-ui">Nota</text>`,
2470
+ toolKind: "custom"
2471
+ };
2472
+ change([...itemsRef.current, note]);
2473
+ }
2474
+ return;
2475
+ }
2476
+ dragStateRef.current = { kind: "idle" };
2477
+ },
2478
+ onPanResponderTerminate: () => {
2479
+ lastPinchDist.current = null;
2480
+ lastPanPoint.current = null;
2481
+ dragStateRef.current = { kind: "idle" };
2482
+ setPlacementPreview(null);
2483
+ }
2484
+ }),
2485
+ [screenToWorld, requestRender, interactive]
2486
+ );
2487
+ useImperativeHandle(
2488
+ ref,
2489
+ () => ({
2490
+ getCamera: () => cameraRef.current,
2491
+ requestRender,
2492
+ getViewportSize: () => size,
2493
+ fitWorldRect: (worldRect, options) => {
2494
+ const cam = cameraRef.current;
2495
+ if (!cam || size.width <= 0 || size.height <= 0) return;
2496
+ fitCameraToWorldRect(
2497
+ cam,
2498
+ size.width,
2499
+ size.height,
2500
+ worldRect,
2501
+ options?.padding ?? 0.08
2502
+ );
2503
+ requestRender();
2504
+ }
2505
+ }),
2506
+ [requestRender, size]
2507
+ );
2508
+ return /* @__PURE__ */ jsx(
2509
+ View,
2510
+ {
2511
+ style: { flex: 1, overflow: "hidden" },
2512
+ onLayout,
2513
+ ...panResponder.panHandlers,
2514
+ children: size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
2515
+ /* @__PURE__ */ jsx(
2516
+ NativeSceneRenderer,
2517
+ {
2518
+ items,
2519
+ camera,
2520
+ width: size.width,
2521
+ height: size.height
2522
+ }
2523
+ ),
2524
+ interactive && /* @__PURE__ */ jsx(
2525
+ NativeInteractionOverlay,
2526
+ {
2527
+ camera,
2528
+ width: size.width,
2529
+ height: size.height,
2530
+ selectedItems,
2531
+ showResizeHandles,
2532
+ placementPreview,
2533
+ eraserTrail,
2534
+ eraserPreviewItems: items.filter(
2535
+ (it) => eraserPreviewIds.includes(it.id)
2536
+ )
2537
+ }
2538
+ ),
2539
+ toolbar && /* @__PURE__ */ jsx(
2540
+ View,
2541
+ {
2542
+ style: {
2543
+ position: "absolute",
2544
+ bottom: 16,
2545
+ left: 16,
2546
+ right: 16,
2547
+ flexDirection: "row",
2548
+ justifyContent: "center",
2549
+ alignItems: "center"
2550
+ },
2551
+ pointerEvents: "box-none",
2552
+ children: toolbar
2553
+ }
2554
+ )
2555
+ ] })
2556
+ }
2557
+ );
2558
+ });
2559
+
2560
+ export { NativeInteractionOverlay, NativeSceneRenderer, NativeShapeRenderer, NativeVectorViewport, parseSvgFragment };
2561
+ //# sourceMappingURL=native.js.map
2562
+ //# sourceMappingURL=native.js.map