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.
package/dist/native.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import getStroke from 'perfect-freehand';
2
- import { Group, Canvas, Rect, Circle, Path, RoundedRect, Oval, DashPathEffect, Line, vec, matchFont, Text, useImage, Image } from '@shopify/react-native-skia';
3
- import { memo, forwardRef, useState, useRef, useEffect, useCallback, useMemo, useImperativeHandle } from 'react';
2
+ import { Group, Canvas, Rect, Circle, Path, RoundedRect, Oval, DashPathEffect, Line, vec, matchFont, Text, Image } from '@shopify/react-native-skia';
3
+ import { memo, forwardRef, useState, useRef, useCallback, useEffect, useMemo, useImperativeHandle } from 'react';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import { StyleSheet, PanResponder, View, Pressable, Text as Text$1, ScrollView } from 'react-native';
6
6
 
@@ -1354,6 +1354,17 @@ function computeResizeBoundsFixedAspect(bounds, handle, currentWorld, aspect) {
1354
1354
  }
1355
1355
  }
1356
1356
 
1357
+ // src/react/presence/peer-color.ts
1358
+ function defaultPresenceColorForId(id) {
1359
+ let h = 2166136261;
1360
+ for (let i = 0; i < id.length; i++) {
1361
+ h ^= id.charCodeAt(i);
1362
+ h = Math.imul(h, 16777619);
1363
+ }
1364
+ const hue = (h >>> 0) % 360;
1365
+ return `hsl(${hue} 72% 42%)`;
1366
+ }
1367
+
1357
1368
  // src/scene/freehand-path.ts
1358
1369
  function smoothFreehandPointsToPathD(points) {
1359
1370
  const n = points.length;
@@ -1387,6 +1398,141 @@ function smoothFreehandPointsToPathD(points) {
1387
1398
  d += ` Q ${pLast.x} ${pLast.y} ${pEnd.x} ${pEnd.y}`;
1388
1399
  return d;
1389
1400
  }
1401
+ var DEFAULT_NATIVE_IMAGE_CACHE_MAX_ENTRIES = 96;
1402
+ function disposeCachedImage(image) {
1403
+ try {
1404
+ image.dispose?.();
1405
+ } catch {
1406
+ }
1407
+ }
1408
+ function createNativeImageCache({
1409
+ loadImage,
1410
+ maxEntries = DEFAULT_NATIVE_IMAGE_CACHE_MAX_ENTRIES
1411
+ }) {
1412
+ const safeMaxEntries = Math.max(1, Math.round(maxEntries));
1413
+ const entries = /* @__PURE__ */ new Map();
1414
+ let clock = 0;
1415
+ const touchEntry = (entry) => {
1416
+ clock += 1;
1417
+ entry.lastUsed = clock;
1418
+ };
1419
+ const createEntry = () => {
1420
+ clock += 1;
1421
+ return {
1422
+ lastUsed: clock,
1423
+ retainCount: 0
1424
+ };
1425
+ };
1426
+ const prune = () => {
1427
+ const cachedEntries = [...entries.entries()].filter(
1428
+ (entry) => entry[1].image != null && entry[1].retainCount === 0
1429
+ );
1430
+ if (cachedEntries.length <= safeMaxEntries) return;
1431
+ const evictedEntries = cachedEntries.sort(
1432
+ (leftEntry, rightEntry) => leftEntry[1].lastUsed - rightEntry[1].lastUsed
1433
+ ).slice(0, cachedEntries.length - safeMaxEntries);
1434
+ for (const [href, entry] of evictedEntries) {
1435
+ entries.delete(href);
1436
+ disposeCachedImage(entry.image);
1437
+ }
1438
+ };
1439
+ const getCached = (href) => {
1440
+ const entry = entries.get(href);
1441
+ if (!entry?.image) return null;
1442
+ touchEntry(entry);
1443
+ return entry.image;
1444
+ };
1445
+ const load = async (href) => {
1446
+ const cachedImage = getCached(href);
1447
+ if (cachedImage) return cachedImage;
1448
+ const existingEntry = entries.get(href);
1449
+ if (existingEntry?.promise) return await existingEntry.promise;
1450
+ const entry = existingEntry ?? createEntry();
1451
+ const promise = loadImage(href).then((image) => {
1452
+ if (!image) {
1453
+ if (entry.retainCount === 0) entries.delete(href);
1454
+ return null;
1455
+ }
1456
+ entry.image = image;
1457
+ entry.promise = void 0;
1458
+ touchEntry(entry);
1459
+ prune();
1460
+ return image;
1461
+ }).catch((error) => {
1462
+ entries.delete(href);
1463
+ throw error;
1464
+ });
1465
+ entry.promise = promise;
1466
+ entries.set(href, entry);
1467
+ return await promise;
1468
+ };
1469
+ const retain = (href) => {
1470
+ const entry = entries.get(href) ?? createEntry();
1471
+ entry.retainCount += 1;
1472
+ touchEntry(entry);
1473
+ entries.set(href, entry);
1474
+ let released = false;
1475
+ return () => {
1476
+ if (released) return;
1477
+ released = true;
1478
+ entry.retainCount = Math.max(0, entry.retainCount - 1);
1479
+ if (!entry.image && !entry.promise && entry.retainCount === 0) {
1480
+ entries.delete(href);
1481
+ return;
1482
+ }
1483
+ prune();
1484
+ };
1485
+ };
1486
+ const clear = () => {
1487
+ for (const entry of entries.values()) {
1488
+ if (entry.image) disposeCachedImage(entry.image);
1489
+ }
1490
+ entries.clear();
1491
+ };
1492
+ const size = () => [...entries.values()].filter((entry) => entry.image != null).length;
1493
+ return { getCached, load, retain, clear, size };
1494
+ }
1495
+ async function loadSkiaImageFromHref(href) {
1496
+ const { Skia } = await import('@shopify/react-native-skia');
1497
+ const data = await Skia.Data.fromURI(href);
1498
+ return Skia.Image.MakeImageFromEncoded(data);
1499
+ }
1500
+ var nativeSkiaImageCache = createNativeImageCache({
1501
+ loadImage: loadSkiaImageFromHref
1502
+ });
1503
+ function useCachedSkiaImage(href) {
1504
+ const [loadedImage, setLoadedImage] = useState(
1505
+ () => href ? { href, image: nativeSkiaImageCache.getCached(href) } : null
1506
+ );
1507
+ useEffect(() => {
1508
+ if (!href) {
1509
+ setLoadedImage(null);
1510
+ return;
1511
+ }
1512
+ const releaseImage = nativeSkiaImageCache.retain(href);
1513
+ const cachedImage = nativeSkiaImageCache.getCached(href);
1514
+ if (cachedImage) {
1515
+ setLoadedImage({ href, image: cachedImage });
1516
+ return releaseImage;
1517
+ }
1518
+ let active = true;
1519
+ setLoadedImage({ href, image: null });
1520
+ void nativeSkiaImageCache.load(href).then(
1521
+ (loadedImage2) => {
1522
+ if (active) setLoadedImage({ href, image: loadedImage2 });
1523
+ },
1524
+ () => {
1525
+ if (active) setLoadedImage({ href, image: null });
1526
+ }
1527
+ );
1528
+ return () => {
1529
+ active = false;
1530
+ releaseImage();
1531
+ };
1532
+ }, [href]);
1533
+ if (!href || loadedImage?.href !== href) return null;
1534
+ return loadedImage.image;
1535
+ }
1390
1536
 
1391
1537
  // src/native/skia-transform.ts
1392
1538
  function parseNum(s) {
@@ -1651,7 +1797,7 @@ function SvgNodeItem({ node }) {
1651
1797
  function SvgImageNodeItem({
1652
1798
  node
1653
1799
  }) {
1654
- const image = useImage(node.href);
1800
+ const image = useCachedSkiaImage(node.href);
1655
1801
  if (!image) return null;
1656
1802
  return /* @__PURE__ */ jsx(
1657
1803
  Image,
@@ -2272,6 +2418,26 @@ var HANDLE_ORDER = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
2272
2418
  var ERASER_PREVIEW_OPACITY = 0.3;
2273
2419
  var OVERLAY_STROKE_PX = 1.25;
2274
2420
  var MARQUEE_DASH_PX = 4;
2421
+ var REMOTE_CURSOR_SCREEN_PX = 22;
2422
+ var REMOTE_LABEL_SCREEN_PX = 12;
2423
+ function remoteStrokePaint(tool, fallback) {
2424
+ if (tool === "laser") {
2425
+ return { stroke: LASER_TINT, strokeOpacity: 0.92, widthWorld: 4 };
2426
+ }
2427
+ if (tool === "marker") {
2428
+ return { stroke: fallback, strokeOpacity: 0.45, widthWorld: 14 };
2429
+ }
2430
+ if (tool === "brush") {
2431
+ return { stroke: fallback, strokeOpacity: 0.85, widthWorld: 5 };
2432
+ }
2433
+ if (tool === "pencil") {
2434
+ return { stroke: fallback, strokeOpacity: 0.9, widthWorld: 2.5 };
2435
+ }
2436
+ return { stroke: fallback, strokeOpacity: 0.95, widthWorld: 3.5 };
2437
+ }
2438
+ function isRemoteFreehandTool(tool) {
2439
+ return tool === "draw" || tool === "marker" || tool === "pencil" || tool === "brush";
2440
+ }
2275
2441
  function pointsToSmoothPathD(points) {
2276
2442
  if (points.length < 2) return null;
2277
2443
  const d = smoothFreehandPointsToPathD(points);
@@ -2292,7 +2458,8 @@ function NativeInteractionOverlay({
2292
2458
  eraserTrail,
2293
2459
  laserTrail,
2294
2460
  eraserPreviewItems = [],
2295
- previewStrokeStyle
2461
+ previewStrokeStyle,
2462
+ remotePresence = []
2296
2463
  }) {
2297
2464
  const z = camera.zoom;
2298
2465
  const camTransform = skiaCameraTransform(z, camera.x, camera.y);
@@ -2686,6 +2853,136 @@ function NativeInteractionOverlay({
2686
2853
  )
2687
2854
  ] });
2688
2855
  }, [laserTrail, z]);
2856
+ const remotePresenceElements = useMemo(() => {
2857
+ if (remotePresence.length === 0) return null;
2858
+ const labelFont = matchFont({ fontSize: REMOTE_LABEL_SCREEN_PX / z });
2859
+ const cursorSize = REMOTE_CURSOR_SCREEN_PX / z;
2860
+ const labelOffsetX = 14 / z;
2861
+ const labelOffsetY = 18 / z;
2862
+ return /* @__PURE__ */ jsx(Fragment, { children: remotePresence.map((peer) => {
2863
+ const color = peer.color ?? defaultPresenceColorForId(peer.id);
2864
+ const markup = peer.markupStroke;
2865
+ const cursor = peer.cursor;
2866
+ const camera2 = peer.camera;
2867
+ let strokeElement = null;
2868
+ if (markup && markup.points.length > 0) {
2869
+ const fallbackPaint = remoteStrokePaint(markup.tool, color);
2870
+ const paint = {
2871
+ stroke: markup.stroke ?? fallbackPaint.stroke,
2872
+ strokeOpacity: markup.strokeOpacity ?? fallbackPaint.strokeOpacity,
2873
+ widthWorld: markup.strokeWidth ?? fallbackPaint.widthWorld
2874
+ };
2875
+ if (markup.tool === "laser") {
2876
+ const d = markup.points.length >= 2 ? smoothFreehandPointsToPathD([...markup.points]) : null;
2877
+ if (d) {
2878
+ strokeElement = /* @__PURE__ */ jsx(
2879
+ Path,
2880
+ {
2881
+ path: d,
2882
+ color: colorWithOpacity(paint.stroke, paint.strokeOpacity),
2883
+ style: "stroke",
2884
+ strokeWidth: Math.max(paint.widthWorld, OVERLAY_STROKE_PX) / z,
2885
+ strokeCap: "round",
2886
+ strokeJoin: "round",
2887
+ antiAlias: true
2888
+ }
2889
+ );
2890
+ }
2891
+ }
2892
+ if (!strokeElement && isRemoteFreehandTool(markup.tool)) {
2893
+ const payload = computeFreehandSvgPayload(
2894
+ markup.points.map((point) => ({ x: point.x, y: point.y })),
2895
+ {
2896
+ stroke: paint.stroke,
2897
+ strokeWidth: paint.widthWorld,
2898
+ strokeOpacity: paint.strokeOpacity
2899
+ },
2900
+ markup.tool,
2901
+ markup.points.length === 2
2902
+ );
2903
+ if (payload?.kind === "circle") {
2904
+ strokeElement = /* @__PURE__ */ jsx(
2905
+ Circle,
2906
+ {
2907
+ cx: payload.cx,
2908
+ cy: payload.cy,
2909
+ r: payload.r,
2910
+ color: colorWithOpacity(payload.fill, payload.fillOpacity),
2911
+ style: "fill",
2912
+ antiAlias: true
2913
+ }
2914
+ );
2915
+ }
2916
+ if (payload?.kind === "fillPath") {
2917
+ strokeElement = /* @__PURE__ */ jsx(
2918
+ Path,
2919
+ {
2920
+ path: payload.d,
2921
+ color: colorWithOpacity(payload.fill, payload.fillOpacity),
2922
+ style: "fill",
2923
+ fillType: "winding",
2924
+ antiAlias: true
2925
+ }
2926
+ );
2927
+ }
2928
+ if (payload?.kind === "strokePath") {
2929
+ strokeElement = /* @__PURE__ */ jsx(
2930
+ Path,
2931
+ {
2932
+ path: payload.d,
2933
+ color: colorWithOpacity(payload.stroke, payload.strokeOpacity),
2934
+ style: "stroke",
2935
+ strokeWidth: payload.strokeWidth,
2936
+ strokeCap: "round",
2937
+ strokeJoin: "round",
2938
+ antiAlias: true
2939
+ }
2940
+ );
2941
+ }
2942
+ }
2943
+ }
2944
+ const cameraElement = camera2 ? /* @__PURE__ */ jsx(
2945
+ Rect,
2946
+ {
2947
+ x: -camera2.x / camera2.zoom,
2948
+ y: -camera2.y / camera2.zoom,
2949
+ width: camera2.viewportWidth / camera2.zoom,
2950
+ height: camera2.viewportHeight / camera2.zoom,
2951
+ color,
2952
+ style: "stroke",
2953
+ strokeWidth: overlayStrokeWorld,
2954
+ antiAlias: true,
2955
+ children: /* @__PURE__ */ jsx(DashPathEffect, { intervals: [marqueeDashWorld, marqueeDashWorld] })
2956
+ }
2957
+ ) : null;
2958
+ const cursorElement = cursor ? /* @__PURE__ */ jsxs(Fragment, { children: [
2959
+ /* @__PURE__ */ jsx(
2960
+ Path,
2961
+ {
2962
+ 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`,
2963
+ color,
2964
+ style: "fill",
2965
+ antiAlias: true
2966
+ }
2967
+ ),
2968
+ peer.displayName ? /* @__PURE__ */ jsx(
2969
+ Text,
2970
+ {
2971
+ x: cursor.x + labelOffsetX,
2972
+ y: cursor.y + labelOffsetY,
2973
+ text: peer.displayName,
2974
+ color,
2975
+ font: labelFont
2976
+ }
2977
+ ) : null
2978
+ ] }) : null;
2979
+ return /* @__PURE__ */ jsxs(Group, { children: [
2980
+ cameraElement,
2981
+ strokeElement,
2982
+ cursorElement
2983
+ ] }, peer.clientId ?? peer.id);
2984
+ }) });
2985
+ }, [remotePresence, z, overlayStrokeWorld, marqueeDashWorld]);
2689
2986
  if (width <= 0 || height <= 0) return null;
2690
2987
  return /* @__PURE__ */ jsx(
2691
2988
  Canvas,
@@ -2703,6 +3000,7 @@ function NativeInteractionOverlay({
2703
3000
  laserTrailElements,
2704
3001
  eraserTrailElements,
2705
3002
  eraserPreviewElements,
3003
+ remotePresenceElements,
2706
3004
  selectionElements
2707
3005
  ] })
2708
3006
  }
@@ -4205,6 +4503,7 @@ function fitCameraToWorldRect(camera, viewportW, viewportH, worldRect, padding)
4205
4503
  var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4206
4504
  items,
4207
4505
  selectedIds = [],
4506
+ remotePresence = [],
4208
4507
  toolId = "hand",
4209
4508
  toolLocked = false,
4210
4509
  interactive = false,
@@ -4212,6 +4511,9 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4212
4511
  onItemsChange,
4213
4512
  onToolChangeRequest,
4214
4513
  onWorldPointerDown,
4514
+ onWorldPointerMove,
4515
+ onWorldPointerLeave,
4516
+ onPlacementPreviewChange,
4215
4517
  onCameraChange,
4216
4518
  customPlacement,
4217
4519
  customPlacements = [],
@@ -4229,6 +4531,12 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4229
4531
  onToolChangeRequestRef.current = onToolChangeRequest;
4230
4532
  const onWorldPointerDownRef = useRef(onWorldPointerDown);
4231
4533
  onWorldPointerDownRef.current = onWorldPointerDown;
4534
+ const onWorldPointerMoveRef = useRef(onWorldPointerMove);
4535
+ onWorldPointerMoveRef.current = onWorldPointerMove;
4536
+ const onWorldPointerLeaveRef = useRef(onWorldPointerLeave);
4537
+ onWorldPointerLeaveRef.current = onWorldPointerLeave;
4538
+ const onPlacementPreviewChangeRef = useRef(onPlacementPreviewChange);
4539
+ onPlacementPreviewChangeRef.current = onPlacementPreviewChange;
4232
4540
  const onCameraChangeRef = useRef(onCameraChange);
4233
4541
  onCameraChangeRef.current = onCameraChange;
4234
4542
  const onItemsChangeRef = useRef(onItemsChange);
@@ -4244,8 +4552,13 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4244
4552
  const selectedIdsRef = useRef(selectedIds);
4245
4553
  selectedIdsRef.current = selectedIds;
4246
4554
  const dragStateRef = useRef({ kind: "idle" });
4247
- const [placementPreview, setPlacementPreview] = useState(
4248
- null
4555
+ const [placementPreview, setPlacementPreviewState] = useState(null);
4556
+ const setRealtimePlacementPreview = useCallback(
4557
+ (nextPreview) => {
4558
+ setPlacementPreviewState(nextPreview);
4559
+ onPlacementPreviewChangeRef.current?.(nextPreview);
4560
+ },
4561
+ []
4249
4562
  );
4250
4563
  const [eraserTrail, setEraserTrail] = useState([]);
4251
4564
  const [laserTrail, setLaserTrail] = useState([]);
@@ -4339,6 +4652,16 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4339
4652
  },
4340
4653
  []
4341
4654
  );
4655
+ const notifyWorldPointerMove = useCallback(
4656
+ (point) => {
4657
+ const { worldX, worldY } = screenToWorld(point.x, point.y);
4658
+ onWorldPointerMoveRef.current?.({ x: worldX, y: worldY });
4659
+ },
4660
+ [screenToWorld]
4661
+ );
4662
+ const notifyWorldPointerLeave = useCallback(() => {
4663
+ onWorldPointerLeaveRef.current?.();
4664
+ }, []);
4342
4665
  const requestRender = useCallback(() => {
4343
4666
  setCameraTick((n) => n + 1);
4344
4667
  onCameraChangeRef.current?.();
@@ -4378,6 +4701,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4378
4701
  const cam = cameraRef.current;
4379
4702
  if (!cam) return;
4380
4703
  const { worldX, worldY } = screenToWorld(sx, sy);
4704
+ onWorldPointerMoveRef.current?.({ x: worldX, y: worldY });
4381
4705
  if (tool === "hand") {
4382
4706
  dragStateRef.current = { kind: "pan" };
4383
4707
  return;
@@ -4463,7 +4787,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4463
4787
  kind: "marquee",
4464
4788
  startWorld: { x: worldX, y: worldY }
4465
4789
  };
4466
- setPlacementPreview({
4790
+ setRealtimePlacementPreview({
4467
4791
  kind: "marquee",
4468
4792
  rect: { x: worldX, y: worldY, width: 0, height: 0 }
4469
4793
  });
@@ -4483,7 +4807,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4483
4807
  }
4484
4808
  setLaserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
4485
4809
  } else {
4486
- setPlacementPreview({
4810
+ setRealtimePlacementPreview({
4487
4811
  kind: "stroke",
4488
4812
  tool,
4489
4813
  points: [{ x: worldX, y: worldY }],
@@ -4516,7 +4840,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4516
4840
  startWorld: { x: worldX, y: worldY },
4517
4841
  startScreen: { x: sx, y: sy }
4518
4842
  };
4519
- setPlacementPreview(
4843
+ setRealtimePlacementPreview(
4520
4844
  placementPreviewForTool(
4521
4845
  tool,
4522
4846
  { x: worldX, y: worldY },
@@ -4541,7 +4865,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4541
4865
  startWorld: { x: worldX, y: worldY },
4542
4866
  startScreen: { x: sx, y: sy }
4543
4867
  };
4544
- setPlacementPreview({
4868
+ setRealtimePlacementPreview({
4545
4869
  kind: "rect",
4546
4870
  rect: { x: worldX, y: worldY, width: 0, height: 0 }
4547
4871
  });
@@ -4570,7 +4894,13 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4570
4894
  }
4571
4895
  dragStateRef.current = { kind: "pan" };
4572
4896
  },
4573
- [interactive, requestSelectToolAfterUse, screenToWorld, updateToolCursorPoint]
4897
+ [
4898
+ interactive,
4899
+ requestSelectToolAfterUse,
4900
+ screenToWorld,
4901
+ setRealtimePlacementPreview,
4902
+ updateToolCursorPoint
4903
+ ]
4574
4904
  );
4575
4905
  const applyDragMoveAtScreenPoint = useCallback(
4576
4906
  (point, pagePoint) => {
@@ -4578,6 +4908,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4578
4908
  if (!cam) return;
4579
4909
  updateToolCursorPoint(point);
4580
4910
  const { worldX, worldY } = screenToWorld(point.x, point.y);
4911
+ onWorldPointerMoveRef.current?.({ x: worldX, y: worldY });
4581
4912
  const st = dragStateRef.current;
4582
4913
  if (st.kind === "pan") {
4583
4914
  const current = pagePoint ?? point;
@@ -4610,7 +4941,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4610
4941
  }
4611
4942
  return;
4612
4943
  }
4613
- setPlacementPreview({
4944
+ setRealtimePlacementPreview({
4614
4945
  kind: "stroke",
4615
4946
  tool: st.tool,
4616
4947
  points: [...pts],
@@ -4672,7 +5003,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4672
5003
  width: Math.abs(b.x - a.x),
4673
5004
  height: Math.abs(b.y - a.y)
4674
5005
  };
4675
- setPlacementPreview({ kind: "marquee", rect });
5006
+ setRealtimePlacementPreview({ kind: "marquee", rect });
4676
5007
  return;
4677
5008
  }
4678
5009
  if (st.kind === "erase") {
@@ -4690,7 +5021,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4690
5021
  return;
4691
5022
  }
4692
5023
  if (st.kind === "place") {
4693
- setPlacementPreview(
5024
+ setRealtimePlacementPreview(
4694
5025
  placementPreviewForTool(st.tool, st.startWorld, {
4695
5026
  x: worldX,
4696
5027
  y: worldY
@@ -4699,14 +5030,19 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4699
5030
  return;
4700
5031
  }
4701
5032
  if (st.kind === "custom-place") {
4702
- setPlacementPreview({
5033
+ setRealtimePlacementPreview({
4703
5034
  kind: "rect",
4704
5035
  rect: rectFromCorners(st.startWorld, { x: worldX, y: worldY })
4705
5036
  });
4706
5037
  return;
4707
5038
  }
4708
5039
  },
4709
- [requestRender, screenToWorld, updateToolCursorPoint]
5040
+ [
5041
+ requestRender,
5042
+ screenToWorld,
5043
+ setRealtimePlacementPreview,
5044
+ updateToolCursorPoint
5045
+ ]
4710
5046
  );
4711
5047
  const finishDragAtScreenPoint = useCallback(
4712
5048
  (point) => {
@@ -4716,7 +5052,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4716
5052
  const st = dragStateRef.current;
4717
5053
  if (st.kind === "draw") {
4718
5054
  dragStateRef.current = { kind: "idle" };
4719
- setPlacementPreview(null);
5055
+ setRealtimePlacementPreview(null);
4720
5056
  if (st.tool === "laser") {
4721
5057
  if (laserClearTimerRef.current) {
4722
5058
  clearTimeout(laserClearTimerRef.current);
@@ -4754,7 +5090,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4754
5090
  }
4755
5091
  if (st.kind === "marquee") {
4756
5092
  dragStateRef.current = { kind: "idle" };
4757
- setPlacementPreview(null);
5093
+ setRealtimePlacementPreview(null);
4758
5094
  const cam = cameraRef.current;
4759
5095
  if (!cam) return;
4760
5096
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -4785,7 +5121,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4785
5121
  }
4786
5122
  if (st.kind === "place") {
4787
5123
  dragStateRef.current = { kind: "idle" };
4788
- setPlacementPreview(null);
5124
+ setRealtimePlacementPreview(null);
4789
5125
  const change = onItemsChangeRef.current;
4790
5126
  if (!change) return;
4791
5127
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -4842,7 +5178,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4842
5178
  }
4843
5179
  if (st.kind === "custom-place") {
4844
5180
  dragStateRef.current = { kind: "idle" };
4845
- setPlacementPreview(null);
5181
+ setRealtimePlacementPreview(null);
4846
5182
  const change = onItemsChangeRef.current;
4847
5183
  if (!change) return;
4848
5184
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -4917,7 +5253,12 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4917
5253
  }
4918
5254
  dragStateRef.current = { kind: "idle" };
4919
5255
  },
4920
- [requestSelectToolAfterUse, screenToWorld, updateToolCursorPoint]
5256
+ [
5257
+ requestSelectToolAfterUse,
5258
+ screenToWorld,
5259
+ setRealtimePlacementPreview,
5260
+ updateToolCursorPoint
5261
+ ]
4921
5262
  );
4922
5263
  const handlePointerDown = useCallback(
4923
5264
  (event) => {
@@ -4933,9 +5274,10 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4933
5274
  applyDragMoveAtScreenPoint(point, point);
4934
5275
  return;
4935
5276
  }
5277
+ notifyWorldPointerMove(point);
4936
5278
  updateToolCursorPoint(point);
4937
5279
  },
4938
- [applyDragMoveAtScreenPoint, updateToolCursorPoint]
5280
+ [applyDragMoveAtScreenPoint, notifyWorldPointerMove, updateToolCursorPoint]
4939
5281
  );
4940
5282
  const handlePointerUp = useCallback(
4941
5283
  (event) => {
@@ -4954,6 +5296,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4954
5296
  const sy = evt.nativeEvent.locationY;
4955
5297
  if (touches && touches.length >= 2) {
4956
5298
  hideToolCursor();
5299
+ notifyWorldPointerLeave();
4957
5300
  dragStateRef.current = { kind: "pan" };
4958
5301
  return;
4959
5302
  }
@@ -4969,6 +5312,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4969
5312
  const pageY = evt.nativeEvent.pageY;
4970
5313
  if (touches && touches.length >= 2) {
4971
5314
  hideToolCursor();
5315
+ notifyWorldPointerLeave();
4972
5316
  const t0 = touches[0];
4973
5317
  const t1 = touches[1];
4974
5318
  if (t0 && t1) {
@@ -5000,8 +5344,9 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5000
5344
  lastPinchDist.current = null;
5001
5345
  lastPanPoint.current = null;
5002
5346
  hideToolCursor();
5347
+ notifyWorldPointerLeave();
5003
5348
  dragStateRef.current = { kind: "idle" };
5004
- setPlacementPreview(null);
5349
+ setRealtimePlacementPreview(null);
5005
5350
  setLaserTrail([]);
5006
5351
  setEraserTrail([]);
5007
5352
  setEraserPreviewIds([]);
@@ -5013,7 +5358,9 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5013
5358
  beginDragAtScreenPoint,
5014
5359
  finishDragAtScreenPoint,
5015
5360
  requestRender,
5016
- hideToolCursor
5361
+ hideToolCursor,
5362
+ notifyWorldPointerLeave,
5363
+ setRealtimePlacementPreview
5017
5364
  ]
5018
5365
  );
5019
5366
  useImperativeHandle(
@@ -5047,7 +5394,10 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5047
5394
  onPointerMove: handlePointerMove,
5048
5395
  onPointerUp: handlePointerUp,
5049
5396
  onPointerEnter: handlePointerMove,
5050
- onPointerLeave: hideToolCursor,
5397
+ onPointerLeave: () => {
5398
+ hideToolCursor();
5399
+ notifyWorldPointerLeave();
5400
+ },
5051
5401
  ...panResponder.panHandlers,
5052
5402
  children: size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
5053
5403
  /* @__PURE__ */ jsx(
@@ -5073,7 +5423,8 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5073
5423
  eraserPreviewItems: items.filter(
5074
5424
  (it) => eraserPreviewIds.includes(it.id)
5075
5425
  ),
5076
- previewStrokeStyle: strokeStyleState
5426
+ previewStrokeStyle: strokeStyleState,
5427
+ remotePresence
5077
5428
  }
5078
5429
  ),
5079
5430
  interactive && showStyleInspector && activeStyleToolId ? /* @__PURE__ */ jsx(