canvu-react 0.4.33 → 0.4.35

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.
@@ -1,9 +1,10 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { C as CanvasPlugin } from './types-BS-YG8Hx.cjs';
2
+ import { C as CanvasPlugin } from './types-NBYvslB-.cjs';
3
3
  import 'react';
4
4
  import './types-BCCvY6ie.cjs';
5
5
  import './shape-builders-CKEMjivV.cjs';
6
6
  import './link-item-Dncuz2d_.cjs';
7
+ import './types-BQUbxMgz.cjs';
7
8
 
8
9
  type ChatbotPluginPanelProps = {
9
10
  /**
package/dist/chatbot.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { C as CanvasPlugin } from './types-UZYYwK-v.js';
2
+ import { C as CanvasPlugin } from './types-DeDm865m.js';
3
3
  import 'react';
4
4
  import './types-BCCvY6ie.js';
5
5
  import './shape-builders-Cyh8zvDG.js';
6
6
  import './link-item-voRU0Up9.js';
7
+ import './types-B82WiQQh.js';
7
8
 
8
9
  type ChatbotPluginPanelProps = {
9
10
  /**
package/dist/native.cjs CHANGED
@@ -1360,6 +1360,17 @@ function computeResizeBoundsFixedAspect(bounds, handle, currentWorld, aspect) {
1360
1360
  }
1361
1361
  }
1362
1362
 
1363
+ // src/react/presence/peer-color.ts
1364
+ function defaultPresenceColorForId(id) {
1365
+ let h = 2166136261;
1366
+ for (let i = 0; i < id.length; i++) {
1367
+ h ^= id.charCodeAt(i);
1368
+ h = Math.imul(h, 16777619);
1369
+ }
1370
+ const hue = (h >>> 0) % 360;
1371
+ return `hsl(${hue} 72% 42%)`;
1372
+ }
1373
+
1363
1374
  // src/scene/freehand-path.ts
1364
1375
  function smoothFreehandPointsToPathD(points) {
1365
1376
  const n = points.length;
@@ -1393,6 +1404,141 @@ function smoothFreehandPointsToPathD(points) {
1393
1404
  d += ` Q ${pLast.x} ${pLast.y} ${pEnd.x} ${pEnd.y}`;
1394
1405
  return d;
1395
1406
  }
1407
+ var DEFAULT_NATIVE_IMAGE_CACHE_MAX_ENTRIES = 96;
1408
+ function disposeCachedImage(image) {
1409
+ try {
1410
+ image.dispose?.();
1411
+ } catch {
1412
+ }
1413
+ }
1414
+ function createNativeImageCache({
1415
+ loadImage,
1416
+ maxEntries = DEFAULT_NATIVE_IMAGE_CACHE_MAX_ENTRIES
1417
+ }) {
1418
+ const safeMaxEntries = Math.max(1, Math.round(maxEntries));
1419
+ const entries = /* @__PURE__ */ new Map();
1420
+ let clock = 0;
1421
+ const touchEntry = (entry) => {
1422
+ clock += 1;
1423
+ entry.lastUsed = clock;
1424
+ };
1425
+ const createEntry = () => {
1426
+ clock += 1;
1427
+ return {
1428
+ lastUsed: clock,
1429
+ retainCount: 0
1430
+ };
1431
+ };
1432
+ const prune = () => {
1433
+ const cachedEntries = [...entries.entries()].filter(
1434
+ (entry) => entry[1].image != null && entry[1].retainCount === 0
1435
+ );
1436
+ if (cachedEntries.length <= safeMaxEntries) return;
1437
+ const evictedEntries = cachedEntries.sort(
1438
+ (leftEntry, rightEntry) => leftEntry[1].lastUsed - rightEntry[1].lastUsed
1439
+ ).slice(0, cachedEntries.length - safeMaxEntries);
1440
+ for (const [href, entry] of evictedEntries) {
1441
+ entries.delete(href);
1442
+ disposeCachedImage(entry.image);
1443
+ }
1444
+ };
1445
+ const getCached = (href) => {
1446
+ const entry = entries.get(href);
1447
+ if (!entry?.image) return null;
1448
+ touchEntry(entry);
1449
+ return entry.image;
1450
+ };
1451
+ const load = async (href) => {
1452
+ const cachedImage = getCached(href);
1453
+ if (cachedImage) return cachedImage;
1454
+ const existingEntry = entries.get(href);
1455
+ if (existingEntry?.promise) return await existingEntry.promise;
1456
+ const entry = existingEntry ?? createEntry();
1457
+ const promise = loadImage(href).then((image) => {
1458
+ if (!image) {
1459
+ if (entry.retainCount === 0) entries.delete(href);
1460
+ return null;
1461
+ }
1462
+ entry.image = image;
1463
+ entry.promise = void 0;
1464
+ touchEntry(entry);
1465
+ prune();
1466
+ return image;
1467
+ }).catch((error) => {
1468
+ entries.delete(href);
1469
+ throw error;
1470
+ });
1471
+ entry.promise = promise;
1472
+ entries.set(href, entry);
1473
+ return await promise;
1474
+ };
1475
+ const retain = (href) => {
1476
+ const entry = entries.get(href) ?? createEntry();
1477
+ entry.retainCount += 1;
1478
+ touchEntry(entry);
1479
+ entries.set(href, entry);
1480
+ let released = false;
1481
+ return () => {
1482
+ if (released) return;
1483
+ released = true;
1484
+ entry.retainCount = Math.max(0, entry.retainCount - 1);
1485
+ if (!entry.image && !entry.promise && entry.retainCount === 0) {
1486
+ entries.delete(href);
1487
+ return;
1488
+ }
1489
+ prune();
1490
+ };
1491
+ };
1492
+ const clear = () => {
1493
+ for (const entry of entries.values()) {
1494
+ if (entry.image) disposeCachedImage(entry.image);
1495
+ }
1496
+ entries.clear();
1497
+ };
1498
+ const size = () => [...entries.values()].filter((entry) => entry.image != null).length;
1499
+ return { getCached, load, retain, clear, size };
1500
+ }
1501
+ async function loadSkiaImageFromHref(href) {
1502
+ const { Skia } = await import('@shopify/react-native-skia');
1503
+ const data = await Skia.Data.fromURI(href);
1504
+ return Skia.Image.MakeImageFromEncoded(data);
1505
+ }
1506
+ var nativeSkiaImageCache = createNativeImageCache({
1507
+ loadImage: loadSkiaImageFromHref
1508
+ });
1509
+ function useCachedSkiaImage(href) {
1510
+ const [loadedImage, setLoadedImage] = react.useState(
1511
+ () => href ? { href, image: nativeSkiaImageCache.getCached(href) } : null
1512
+ );
1513
+ react.useEffect(() => {
1514
+ if (!href) {
1515
+ setLoadedImage(null);
1516
+ return;
1517
+ }
1518
+ const releaseImage = nativeSkiaImageCache.retain(href);
1519
+ const cachedImage = nativeSkiaImageCache.getCached(href);
1520
+ if (cachedImage) {
1521
+ setLoadedImage({ href, image: cachedImage });
1522
+ return releaseImage;
1523
+ }
1524
+ let active = true;
1525
+ setLoadedImage({ href, image: null });
1526
+ void nativeSkiaImageCache.load(href).then(
1527
+ (loadedImage2) => {
1528
+ if (active) setLoadedImage({ href, image: loadedImage2 });
1529
+ },
1530
+ () => {
1531
+ if (active) setLoadedImage({ href, image: null });
1532
+ }
1533
+ );
1534
+ return () => {
1535
+ active = false;
1536
+ releaseImage();
1537
+ };
1538
+ }, [href]);
1539
+ if (!href || loadedImage?.href !== href) return null;
1540
+ return loadedImage.image;
1541
+ }
1396
1542
 
1397
1543
  // src/native/skia-transform.ts
1398
1544
  function parseNum(s) {
@@ -1657,7 +1803,7 @@ function SvgNodeItem({ node }) {
1657
1803
  function SvgImageNodeItem({
1658
1804
  node
1659
1805
  }) {
1660
- const image = reactNativeSkia.useImage(node.href);
1806
+ const image = useCachedSkiaImage(node.href);
1661
1807
  if (!image) return null;
1662
1808
  return /* @__PURE__ */ jsxRuntime.jsx(
1663
1809
  reactNativeSkia.Image,
@@ -2278,6 +2424,26 @@ var HANDLE_ORDER = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
2278
2424
  var ERASER_PREVIEW_OPACITY = 0.3;
2279
2425
  var OVERLAY_STROKE_PX = 1.25;
2280
2426
  var MARQUEE_DASH_PX = 4;
2427
+ var REMOTE_CURSOR_SCREEN_PX = 22;
2428
+ var REMOTE_LABEL_SCREEN_PX = 12;
2429
+ function remoteStrokePaint(tool, fallback) {
2430
+ if (tool === "laser") {
2431
+ return { stroke: LASER_TINT, strokeOpacity: 0.92, widthWorld: 4 };
2432
+ }
2433
+ if (tool === "marker") {
2434
+ return { stroke: fallback, strokeOpacity: 0.45, widthWorld: 14 };
2435
+ }
2436
+ if (tool === "brush") {
2437
+ return { stroke: fallback, strokeOpacity: 0.85, widthWorld: 5 };
2438
+ }
2439
+ if (tool === "pencil") {
2440
+ return { stroke: fallback, strokeOpacity: 0.9, widthWorld: 2.5 };
2441
+ }
2442
+ return { stroke: fallback, strokeOpacity: 0.95, widthWorld: 3.5 };
2443
+ }
2444
+ function isRemoteFreehandTool(tool) {
2445
+ return tool === "draw" || tool === "marker" || tool === "pencil" || tool === "brush";
2446
+ }
2281
2447
  function pointsToSmoothPathD(points) {
2282
2448
  if (points.length < 2) return null;
2283
2449
  const d = smoothFreehandPointsToPathD(points);
@@ -2298,7 +2464,8 @@ function NativeInteractionOverlay({
2298
2464
  eraserTrail,
2299
2465
  laserTrail,
2300
2466
  eraserPreviewItems = [],
2301
- previewStrokeStyle
2467
+ previewStrokeStyle,
2468
+ remotePresence = []
2302
2469
  }) {
2303
2470
  const z = camera.zoom;
2304
2471
  const camTransform = skiaCameraTransform(z, camera.x, camera.y);
@@ -2692,6 +2859,136 @@ function NativeInteractionOverlay({
2692
2859
  )
2693
2860
  ] });
2694
2861
  }, [laserTrail, z]);
2862
+ const remotePresenceElements = react.useMemo(() => {
2863
+ if (remotePresence.length === 0) return null;
2864
+ const labelFont = reactNativeSkia.matchFont({ fontSize: REMOTE_LABEL_SCREEN_PX / z });
2865
+ const cursorSize = REMOTE_CURSOR_SCREEN_PX / z;
2866
+ const labelOffsetX = 14 / z;
2867
+ const labelOffsetY = 18 / z;
2868
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: remotePresence.map((peer) => {
2869
+ const color = peer.color ?? defaultPresenceColorForId(peer.id);
2870
+ const markup = peer.markupStroke;
2871
+ const cursor = peer.cursor;
2872
+ const camera2 = peer.camera;
2873
+ let strokeElement = null;
2874
+ if (markup && markup.points.length > 0) {
2875
+ const fallbackPaint = remoteStrokePaint(markup.tool, color);
2876
+ const paint = {
2877
+ stroke: markup.stroke ?? fallbackPaint.stroke,
2878
+ strokeOpacity: markup.strokeOpacity ?? fallbackPaint.strokeOpacity,
2879
+ widthWorld: markup.strokeWidth ?? fallbackPaint.widthWorld
2880
+ };
2881
+ if (markup.tool === "laser") {
2882
+ const d = markup.points.length >= 2 ? smoothFreehandPointsToPathD([...markup.points]) : null;
2883
+ if (d) {
2884
+ strokeElement = /* @__PURE__ */ jsxRuntime.jsx(
2885
+ reactNativeSkia.Path,
2886
+ {
2887
+ path: d,
2888
+ color: colorWithOpacity(paint.stroke, paint.strokeOpacity),
2889
+ style: "stroke",
2890
+ strokeWidth: Math.max(paint.widthWorld, OVERLAY_STROKE_PX) / z,
2891
+ strokeCap: "round",
2892
+ strokeJoin: "round",
2893
+ antiAlias: true
2894
+ }
2895
+ );
2896
+ }
2897
+ }
2898
+ if (!strokeElement && isRemoteFreehandTool(markup.tool)) {
2899
+ const payload = computeFreehandSvgPayload(
2900
+ markup.points.map((point) => ({ x: point.x, y: point.y })),
2901
+ {
2902
+ stroke: paint.stroke,
2903
+ strokeWidth: paint.widthWorld,
2904
+ strokeOpacity: paint.strokeOpacity
2905
+ },
2906
+ markup.tool,
2907
+ markup.points.length === 2
2908
+ );
2909
+ if (payload?.kind === "circle") {
2910
+ strokeElement = /* @__PURE__ */ jsxRuntime.jsx(
2911
+ reactNativeSkia.Circle,
2912
+ {
2913
+ cx: payload.cx,
2914
+ cy: payload.cy,
2915
+ r: payload.r,
2916
+ color: colorWithOpacity(payload.fill, payload.fillOpacity),
2917
+ style: "fill",
2918
+ antiAlias: true
2919
+ }
2920
+ );
2921
+ }
2922
+ if (payload?.kind === "fillPath") {
2923
+ strokeElement = /* @__PURE__ */ jsxRuntime.jsx(
2924
+ reactNativeSkia.Path,
2925
+ {
2926
+ path: payload.d,
2927
+ color: colorWithOpacity(payload.fill, payload.fillOpacity),
2928
+ style: "fill",
2929
+ fillType: "winding",
2930
+ antiAlias: true
2931
+ }
2932
+ );
2933
+ }
2934
+ if (payload?.kind === "strokePath") {
2935
+ strokeElement = /* @__PURE__ */ jsxRuntime.jsx(
2936
+ reactNativeSkia.Path,
2937
+ {
2938
+ path: payload.d,
2939
+ color: colorWithOpacity(payload.stroke, payload.strokeOpacity),
2940
+ style: "stroke",
2941
+ strokeWidth: payload.strokeWidth,
2942
+ strokeCap: "round",
2943
+ strokeJoin: "round",
2944
+ antiAlias: true
2945
+ }
2946
+ );
2947
+ }
2948
+ }
2949
+ }
2950
+ const cameraElement = camera2 ? /* @__PURE__ */ jsxRuntime.jsx(
2951
+ reactNativeSkia.Rect,
2952
+ {
2953
+ x: -camera2.x / camera2.zoom,
2954
+ y: -camera2.y / camera2.zoom,
2955
+ width: camera2.viewportWidth / camera2.zoom,
2956
+ height: camera2.viewportHeight / camera2.zoom,
2957
+ color,
2958
+ style: "stroke",
2959
+ strokeWidth: overlayStrokeWorld,
2960
+ antiAlias: true,
2961
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.DashPathEffect, { intervals: [marqueeDashWorld, marqueeDashWorld] })
2962
+ }
2963
+ ) : null;
2964
+ const cursorElement = cursor ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2965
+ /* @__PURE__ */ jsxRuntime.jsx(
2966
+ reactNativeSkia.Path,
2967
+ {
2968
+ path: `M ${cursor.x} ${cursor.y} L ${cursor.x + cursorSize} ${cursor.y + cursorSize * 0.34} L ${cursor.x + cursorSize * 0.42} ${cursor.y + cursorSize * 0.44} L ${cursor.x + cursorSize * 0.58} ${cursor.y + cursorSize} Z`,
2969
+ color,
2970
+ style: "fill",
2971
+ antiAlias: true
2972
+ }
2973
+ ),
2974
+ peer.displayName ? /* @__PURE__ */ jsxRuntime.jsx(
2975
+ reactNativeSkia.Text,
2976
+ {
2977
+ x: cursor.x + labelOffsetX,
2978
+ y: cursor.y + labelOffsetY,
2979
+ text: peer.displayName,
2980
+ color,
2981
+ font: labelFont
2982
+ }
2983
+ ) : null
2984
+ ] }) : null;
2985
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNativeSkia.Group, { children: [
2986
+ cameraElement,
2987
+ strokeElement,
2988
+ cursorElement
2989
+ ] }, peer.clientId ?? peer.id);
2990
+ }) });
2991
+ }, [remotePresence, z, overlayStrokeWorld, marqueeDashWorld]);
2695
2992
  if (width <= 0 || height <= 0) return null;
2696
2993
  return /* @__PURE__ */ jsxRuntime.jsx(
2697
2994
  reactNativeSkia.Canvas,
@@ -2709,6 +3006,7 @@ function NativeInteractionOverlay({
2709
3006
  laserTrailElements,
2710
3007
  eraserTrailElements,
2711
3008
  eraserPreviewElements,
3009
+ remotePresenceElements,
2712
3010
  selectionElements
2713
3011
  ] })
2714
3012
  }
@@ -4211,6 +4509,7 @@ function fitCameraToWorldRect(camera, viewportW, viewportH, worldRect, padding)
4211
4509
  var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4212
4510
  items,
4213
4511
  selectedIds = [],
4512
+ remotePresence = [],
4214
4513
  toolId = "hand",
4215
4514
  toolLocked = false,
4216
4515
  interactive = false,
@@ -4218,6 +4517,9 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4218
4517
  onItemsChange,
4219
4518
  onToolChangeRequest,
4220
4519
  onWorldPointerDown,
4520
+ onWorldPointerMove,
4521
+ onWorldPointerLeave,
4522
+ onPlacementPreviewChange,
4221
4523
  onCameraChange,
4222
4524
  customPlacement,
4223
4525
  customPlacements = [],
@@ -4235,6 +4537,12 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4235
4537
  onToolChangeRequestRef.current = onToolChangeRequest;
4236
4538
  const onWorldPointerDownRef = react.useRef(onWorldPointerDown);
4237
4539
  onWorldPointerDownRef.current = onWorldPointerDown;
4540
+ const onWorldPointerMoveRef = react.useRef(onWorldPointerMove);
4541
+ onWorldPointerMoveRef.current = onWorldPointerMove;
4542
+ const onWorldPointerLeaveRef = react.useRef(onWorldPointerLeave);
4543
+ onWorldPointerLeaveRef.current = onWorldPointerLeave;
4544
+ const onPlacementPreviewChangeRef = react.useRef(onPlacementPreviewChange);
4545
+ onPlacementPreviewChangeRef.current = onPlacementPreviewChange;
4238
4546
  const onCameraChangeRef = react.useRef(onCameraChange);
4239
4547
  onCameraChangeRef.current = onCameraChange;
4240
4548
  const onItemsChangeRef = react.useRef(onItemsChange);
@@ -4250,8 +4558,13 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4250
4558
  const selectedIdsRef = react.useRef(selectedIds);
4251
4559
  selectedIdsRef.current = selectedIds;
4252
4560
  const dragStateRef = react.useRef({ kind: "idle" });
4253
- const [placementPreview, setPlacementPreview] = react.useState(
4254
- null
4561
+ const [placementPreview, setPlacementPreviewState] = react.useState(null);
4562
+ const setRealtimePlacementPreview = react.useCallback(
4563
+ (nextPreview) => {
4564
+ setPlacementPreviewState(nextPreview);
4565
+ onPlacementPreviewChangeRef.current?.(nextPreview);
4566
+ },
4567
+ []
4255
4568
  );
4256
4569
  const [eraserTrail, setEraserTrail] = react.useState([]);
4257
4570
  const [laserTrail, setLaserTrail] = react.useState([]);
@@ -4345,6 +4658,16 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4345
4658
  },
4346
4659
  []
4347
4660
  );
4661
+ const notifyWorldPointerMove = react.useCallback(
4662
+ (point) => {
4663
+ const { worldX, worldY } = screenToWorld(point.x, point.y);
4664
+ onWorldPointerMoveRef.current?.({ x: worldX, y: worldY });
4665
+ },
4666
+ [screenToWorld]
4667
+ );
4668
+ const notifyWorldPointerLeave = react.useCallback(() => {
4669
+ onWorldPointerLeaveRef.current?.();
4670
+ }, []);
4348
4671
  const requestRender = react.useCallback(() => {
4349
4672
  setCameraTick((n) => n + 1);
4350
4673
  onCameraChangeRef.current?.();
@@ -4384,6 +4707,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4384
4707
  const cam = cameraRef.current;
4385
4708
  if (!cam) return;
4386
4709
  const { worldX, worldY } = screenToWorld(sx, sy);
4710
+ onWorldPointerMoveRef.current?.({ x: worldX, y: worldY });
4387
4711
  if (tool === "hand") {
4388
4712
  dragStateRef.current = { kind: "pan" };
4389
4713
  return;
@@ -4469,7 +4793,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4469
4793
  kind: "marquee",
4470
4794
  startWorld: { x: worldX, y: worldY }
4471
4795
  };
4472
- setPlacementPreview({
4796
+ setRealtimePlacementPreview({
4473
4797
  kind: "marquee",
4474
4798
  rect: { x: worldX, y: worldY, width: 0, height: 0 }
4475
4799
  });
@@ -4489,7 +4813,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4489
4813
  }
4490
4814
  setLaserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
4491
4815
  } else {
4492
- setPlacementPreview({
4816
+ setRealtimePlacementPreview({
4493
4817
  kind: "stroke",
4494
4818
  tool,
4495
4819
  points: [{ x: worldX, y: worldY }],
@@ -4522,7 +4846,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4522
4846
  startWorld: { x: worldX, y: worldY },
4523
4847
  startScreen: { x: sx, y: sy }
4524
4848
  };
4525
- setPlacementPreview(
4849
+ setRealtimePlacementPreview(
4526
4850
  placementPreviewForTool(
4527
4851
  tool,
4528
4852
  { x: worldX, y: worldY },
@@ -4547,7 +4871,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4547
4871
  startWorld: { x: worldX, y: worldY },
4548
4872
  startScreen: { x: sx, y: sy }
4549
4873
  };
4550
- setPlacementPreview({
4874
+ setRealtimePlacementPreview({
4551
4875
  kind: "rect",
4552
4876
  rect: { x: worldX, y: worldY, width: 0, height: 0 }
4553
4877
  });
@@ -4576,7 +4900,13 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4576
4900
  }
4577
4901
  dragStateRef.current = { kind: "pan" };
4578
4902
  },
4579
- [interactive, requestSelectToolAfterUse, screenToWorld, updateToolCursorPoint]
4903
+ [
4904
+ interactive,
4905
+ requestSelectToolAfterUse,
4906
+ screenToWorld,
4907
+ setRealtimePlacementPreview,
4908
+ updateToolCursorPoint
4909
+ ]
4580
4910
  );
4581
4911
  const applyDragMoveAtScreenPoint = react.useCallback(
4582
4912
  (point, pagePoint) => {
@@ -4584,6 +4914,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4584
4914
  if (!cam) return;
4585
4915
  updateToolCursorPoint(point);
4586
4916
  const { worldX, worldY } = screenToWorld(point.x, point.y);
4917
+ onWorldPointerMoveRef.current?.({ x: worldX, y: worldY });
4587
4918
  const st = dragStateRef.current;
4588
4919
  if (st.kind === "pan") {
4589
4920
  const current = pagePoint ?? point;
@@ -4616,7 +4947,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4616
4947
  }
4617
4948
  return;
4618
4949
  }
4619
- setPlacementPreview({
4950
+ setRealtimePlacementPreview({
4620
4951
  kind: "stroke",
4621
4952
  tool: st.tool,
4622
4953
  points: [...pts],
@@ -4678,7 +5009,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4678
5009
  width: Math.abs(b.x - a.x),
4679
5010
  height: Math.abs(b.y - a.y)
4680
5011
  };
4681
- setPlacementPreview({ kind: "marquee", rect });
5012
+ setRealtimePlacementPreview({ kind: "marquee", rect });
4682
5013
  return;
4683
5014
  }
4684
5015
  if (st.kind === "erase") {
@@ -4696,7 +5027,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4696
5027
  return;
4697
5028
  }
4698
5029
  if (st.kind === "place") {
4699
- setPlacementPreview(
5030
+ setRealtimePlacementPreview(
4700
5031
  placementPreviewForTool(st.tool, st.startWorld, {
4701
5032
  x: worldX,
4702
5033
  y: worldY
@@ -4705,14 +5036,19 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4705
5036
  return;
4706
5037
  }
4707
5038
  if (st.kind === "custom-place") {
4708
- setPlacementPreview({
5039
+ setRealtimePlacementPreview({
4709
5040
  kind: "rect",
4710
5041
  rect: rectFromCorners(st.startWorld, { x: worldX, y: worldY })
4711
5042
  });
4712
5043
  return;
4713
5044
  }
4714
5045
  },
4715
- [requestRender, screenToWorld, updateToolCursorPoint]
5046
+ [
5047
+ requestRender,
5048
+ screenToWorld,
5049
+ setRealtimePlacementPreview,
5050
+ updateToolCursorPoint
5051
+ ]
4716
5052
  );
4717
5053
  const finishDragAtScreenPoint = react.useCallback(
4718
5054
  (point) => {
@@ -4722,7 +5058,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4722
5058
  const st = dragStateRef.current;
4723
5059
  if (st.kind === "draw") {
4724
5060
  dragStateRef.current = { kind: "idle" };
4725
- setPlacementPreview(null);
5061
+ setRealtimePlacementPreview(null);
4726
5062
  if (st.tool === "laser") {
4727
5063
  if (laserClearTimerRef.current) {
4728
5064
  clearTimeout(laserClearTimerRef.current);
@@ -4760,7 +5096,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4760
5096
  }
4761
5097
  if (st.kind === "marquee") {
4762
5098
  dragStateRef.current = { kind: "idle" };
4763
- setPlacementPreview(null);
5099
+ setRealtimePlacementPreview(null);
4764
5100
  const cam = cameraRef.current;
4765
5101
  if (!cam) return;
4766
5102
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -4791,7 +5127,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4791
5127
  }
4792
5128
  if (st.kind === "place") {
4793
5129
  dragStateRef.current = { kind: "idle" };
4794
- setPlacementPreview(null);
5130
+ setRealtimePlacementPreview(null);
4795
5131
  const change = onItemsChangeRef.current;
4796
5132
  if (!change) return;
4797
5133
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -4848,7 +5184,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4848
5184
  }
4849
5185
  if (st.kind === "custom-place") {
4850
5186
  dragStateRef.current = { kind: "idle" };
4851
- setPlacementPreview(null);
5187
+ setRealtimePlacementPreview(null);
4852
5188
  const change = onItemsChangeRef.current;
4853
5189
  if (!change) return;
4854
5190
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -4923,7 +5259,12 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4923
5259
  }
4924
5260
  dragStateRef.current = { kind: "idle" };
4925
5261
  },
4926
- [requestSelectToolAfterUse, screenToWorld, updateToolCursorPoint]
5262
+ [
5263
+ requestSelectToolAfterUse,
5264
+ screenToWorld,
5265
+ setRealtimePlacementPreview,
5266
+ updateToolCursorPoint
5267
+ ]
4927
5268
  );
4928
5269
  const handlePointerDown = react.useCallback(
4929
5270
  (event) => {
@@ -4939,9 +5280,10 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4939
5280
  applyDragMoveAtScreenPoint(point, point);
4940
5281
  return;
4941
5282
  }
5283
+ notifyWorldPointerMove(point);
4942
5284
  updateToolCursorPoint(point);
4943
5285
  },
4944
- [applyDragMoveAtScreenPoint, updateToolCursorPoint]
5286
+ [applyDragMoveAtScreenPoint, notifyWorldPointerMove, updateToolCursorPoint]
4945
5287
  );
4946
5288
  const handlePointerUp = react.useCallback(
4947
5289
  (event) => {
@@ -4960,6 +5302,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4960
5302
  const sy = evt.nativeEvent.locationY;
4961
5303
  if (touches && touches.length >= 2) {
4962
5304
  hideToolCursor();
5305
+ notifyWorldPointerLeave();
4963
5306
  dragStateRef.current = { kind: "pan" };
4964
5307
  return;
4965
5308
  }
@@ -4975,6 +5318,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4975
5318
  const pageY = evt.nativeEvent.pageY;
4976
5319
  if (touches && touches.length >= 2) {
4977
5320
  hideToolCursor();
5321
+ notifyWorldPointerLeave();
4978
5322
  const t0 = touches[0];
4979
5323
  const t1 = touches[1];
4980
5324
  if (t0 && t1) {
@@ -5006,8 +5350,9 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5006
5350
  lastPinchDist.current = null;
5007
5351
  lastPanPoint.current = null;
5008
5352
  hideToolCursor();
5353
+ notifyWorldPointerLeave();
5009
5354
  dragStateRef.current = { kind: "idle" };
5010
- setPlacementPreview(null);
5355
+ setRealtimePlacementPreview(null);
5011
5356
  setLaserTrail([]);
5012
5357
  setEraserTrail([]);
5013
5358
  setEraserPreviewIds([]);
@@ -5019,7 +5364,9 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5019
5364
  beginDragAtScreenPoint,
5020
5365
  finishDragAtScreenPoint,
5021
5366
  requestRender,
5022
- hideToolCursor
5367
+ hideToolCursor,
5368
+ notifyWorldPointerLeave,
5369
+ setRealtimePlacementPreview
5023
5370
  ]
5024
5371
  );
5025
5372
  react.useImperativeHandle(
@@ -5053,7 +5400,10 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5053
5400
  onPointerMove: handlePointerMove,
5054
5401
  onPointerUp: handlePointerUp,
5055
5402
  onPointerEnter: handlePointerMove,
5056
- onPointerLeave: hideToolCursor,
5403
+ onPointerLeave: () => {
5404
+ hideToolCursor();
5405
+ notifyWorldPointerLeave();
5406
+ },
5057
5407
  ...panResponder.panHandlers,
5058
5408
  children: size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5059
5409
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -5079,7 +5429,8 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5079
5429
  eraserPreviewItems: items.filter(
5080
5430
  (it) => eraserPreviewIds.includes(it.id)
5081
5431
  ),
5082
- previewStrokeStyle: strokeStyleState
5432
+ previewStrokeStyle: strokeStyleState,
5433
+ remotePresence
5083
5434
  }
5084
5435
  ),
5085
5436
  interactive && showStyleInspector && activeStyleToolId ? /* @__PURE__ */ jsxRuntime.jsx(