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