canvu-react 0.4.32 → 0.4.34

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.cjs CHANGED
@@ -40,7 +40,7 @@ var LINK_PLUGIN_KEY = "canvuLink";
40
40
  var DEFAULT_LINK_CARD_WIDTH = 320;
41
41
  var DEFAULT_LINK_CARD_HEIGHT = 70;
42
42
  var LINK_CARD_MIN_SCALE = 0.6;
43
- var LINK_CARD_MAX_SCALE = 2.5;
43
+ var LINK_CARD_MAX_SCALE = 6;
44
44
  var LINK_CARD_ASPECT = DEFAULT_LINK_CARD_WIDTH / DEFAULT_LINK_CARD_HEIGHT;
45
45
  var LINK_CARD_BORDER = "oklch(0.918 0.008 255)";
46
46
  var LINK_CARD_BORDER_STRONG = "oklch(0.86 0.012 255)";
@@ -53,7 +53,6 @@ var formatNumber = (value) => {
53
53
  const rounded = Math.round(value * 100) / 100;
54
54
  return Object.is(rounded, -0) ? "0" : String(rounded);
55
55
  };
56
- var escapeXmlAttribute = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
57
56
  var escapeHtmlText = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
58
57
  var getLinkHostname = (href) => {
59
58
  try {
@@ -78,7 +77,6 @@ var getLinkInitial = (hostname) => {
78
77
  const first = hostname.trim().charAt(0).toUpperCase();
79
78
  return first || "L";
80
79
  };
81
- var buildGoogleFaviconUrl = (hostname) => hostname ? `https://www.google.com/s2/favicons?domain=${encodeURIComponent(hostname)}&sz=64` : null;
82
80
  var getStableLinkIdSuffix = (value) => {
83
81
  let hash = 0;
84
82
  for (const char of value) {
@@ -101,16 +99,13 @@ function buildLinkCardSvg(width, _height, link) {
101
99
  const title = link.title?.trim() || hostname || "Link";
102
100
  const protocol = getLinkProtocol(link.href);
103
101
  const subtitle = hostname || link.href;
104
- const favicon = link.favicon?.trim() || buildGoogleFaviconUrl(hostname);
105
102
  const idSuffix = getStableLinkIdSuffix(`${hostname}:${link.href}`);
106
- const clipId = `canvu-link-favicon-${idSuffix}`;
107
103
  const gradientId = `canvu-link-favicon-gradient-${idSuffix}`;
108
104
  const buttonX = contentWidth - padding - buttonSize;
109
105
  const buttonY = (contentHeight - buttonSize) / 2;
110
106
  const isSecure = protocol === "https:";
111
107
  const subtitleX = isSecure ? textX + 13 : textX;
112
108
  const subtitleWidth = isSecure ? textWidth - 13 : textWidth;
113
- const faviconImage = favicon ? `<image class="canvu-link-favicon-img" href="${escapeXmlAttribute(favicon)}" x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" preserveAspectRatio="xMidYMid slice" clip-path="url(#${clipId})" />` : "";
114
109
  return `
115
110
  <style>
116
111
  .canvu-link-card-root .canvu-link-card { transition: transform .18s ease, filter .18s ease, stroke .18s ease; }
@@ -125,13 +120,9 @@ function buildLinkCardSvg(width, _height, link) {
125
120
  <stop stop-color="${LINK_CARD_ACCENT}" />
126
121
  <stop offset="1" stop-color="${LINK_CARD_ACCENT_DEEP}" />
127
122
  </linearGradient>
128
- <clipPath id="${clipId}">
129
- <rect x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" rx="11" />
130
- </clipPath>
131
123
  </defs>
132
124
  <rect x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" rx="11" fill="url(#${gradientId})" />
133
125
  <text x="${formatNumber(padding + badgeSize / 2)}" y="${formatNumber(padding + badgeSize / 2 + 5)}" text-anchor="middle" font-family="system-ui,sans-serif" font-size="17" font-weight="700" fill="#ffffff">${escapeHtmlText(getLinkInitial(hostname))}</text>
134
- ${faviconImage}
135
126
  ${buildLinkTextBand({ x: textX, y: 16, width: textWidth, height: 19, text: title, fontSize: 14.5, color: LINK_CARD_TITLE_COLOR, fontWeight: 700 })}
136
127
  ${isSecure ? `<g transform="translate(${formatNumber(textX)},40)" stroke="${LINK_CARD_TEXT_COLOR}" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" fill="none"><rect x="1.5" y="4.5" width="7" height="6" rx="1" /><path d="M3 4.5 V3 a2 2 0 0 1 4 0 v1.5" /></g>` : ""}
137
128
  ${buildLinkTextBand({ x: subtitleX, y: 36, width: subtitleWidth, height: 17, text: subtitle, fontSize: 12.5, color: LINK_CARD_TEXT_COLOR })}
@@ -1369,6 +1360,17 @@ function computeResizeBoundsFixedAspect(bounds, handle, currentWorld, aspect) {
1369
1360
  }
1370
1361
  }
1371
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
+
1372
1374
  // src/scene/freehand-path.ts
1373
1375
  function smoothFreehandPointsToPathD(points) {
1374
1376
  const n = points.length;
@@ -2287,6 +2289,26 @@ var HANDLE_ORDER = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
2287
2289
  var ERASER_PREVIEW_OPACITY = 0.3;
2288
2290
  var OVERLAY_STROKE_PX = 1.25;
2289
2291
  var MARQUEE_DASH_PX = 4;
2292
+ var REMOTE_CURSOR_SCREEN_PX = 22;
2293
+ var REMOTE_LABEL_SCREEN_PX = 12;
2294
+ function remoteStrokePaint(tool, fallback) {
2295
+ if (tool === "laser") {
2296
+ return { stroke: LASER_TINT, strokeOpacity: 0.92, widthWorld: 4 };
2297
+ }
2298
+ if (tool === "marker") {
2299
+ return { stroke: fallback, strokeOpacity: 0.45, widthWorld: 14 };
2300
+ }
2301
+ if (tool === "brush") {
2302
+ return { stroke: fallback, strokeOpacity: 0.85, widthWorld: 5 };
2303
+ }
2304
+ if (tool === "pencil") {
2305
+ return { stroke: fallback, strokeOpacity: 0.9, widthWorld: 2.5 };
2306
+ }
2307
+ return { stroke: fallback, strokeOpacity: 0.95, widthWorld: 3.5 };
2308
+ }
2309
+ function isRemoteFreehandTool(tool) {
2310
+ return tool === "draw" || tool === "marker" || tool === "pencil" || tool === "brush";
2311
+ }
2290
2312
  function pointsToSmoothPathD(points) {
2291
2313
  if (points.length < 2) return null;
2292
2314
  const d = smoothFreehandPointsToPathD(points);
@@ -2307,7 +2329,8 @@ function NativeInteractionOverlay({
2307
2329
  eraserTrail,
2308
2330
  laserTrail,
2309
2331
  eraserPreviewItems = [],
2310
- previewStrokeStyle
2332
+ previewStrokeStyle,
2333
+ remotePresence = []
2311
2334
  }) {
2312
2335
  const z = camera.zoom;
2313
2336
  const camTransform = skiaCameraTransform(z, camera.x, camera.y);
@@ -2701,6 +2724,136 @@ function NativeInteractionOverlay({
2701
2724
  )
2702
2725
  ] });
2703
2726
  }, [laserTrail, z]);
2727
+ const remotePresenceElements = react.useMemo(() => {
2728
+ if (remotePresence.length === 0) return null;
2729
+ const labelFont = reactNativeSkia.matchFont({ fontSize: REMOTE_LABEL_SCREEN_PX / z });
2730
+ const cursorSize = REMOTE_CURSOR_SCREEN_PX / z;
2731
+ const labelOffsetX = 14 / z;
2732
+ const labelOffsetY = 18 / z;
2733
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: remotePresence.map((peer) => {
2734
+ const color = peer.color ?? defaultPresenceColorForId(peer.id);
2735
+ const markup = peer.markupStroke;
2736
+ const cursor = peer.cursor;
2737
+ const camera2 = peer.camera;
2738
+ let strokeElement = null;
2739
+ if (markup && markup.points.length > 0) {
2740
+ const fallbackPaint = remoteStrokePaint(markup.tool, color);
2741
+ const paint = {
2742
+ stroke: markup.stroke ?? fallbackPaint.stroke,
2743
+ strokeOpacity: markup.strokeOpacity ?? fallbackPaint.strokeOpacity,
2744
+ widthWorld: markup.strokeWidth ?? fallbackPaint.widthWorld
2745
+ };
2746
+ if (markup.tool === "laser") {
2747
+ const d = markup.points.length >= 2 ? smoothFreehandPointsToPathD([...markup.points]) : null;
2748
+ if (d) {
2749
+ strokeElement = /* @__PURE__ */ jsxRuntime.jsx(
2750
+ reactNativeSkia.Path,
2751
+ {
2752
+ path: d,
2753
+ color: colorWithOpacity(paint.stroke, paint.strokeOpacity),
2754
+ style: "stroke",
2755
+ strokeWidth: Math.max(paint.widthWorld, OVERLAY_STROKE_PX) / z,
2756
+ strokeCap: "round",
2757
+ strokeJoin: "round",
2758
+ antiAlias: true
2759
+ }
2760
+ );
2761
+ }
2762
+ }
2763
+ if (!strokeElement && isRemoteFreehandTool(markup.tool)) {
2764
+ const payload = computeFreehandSvgPayload(
2765
+ markup.points.map((point) => ({ x: point.x, y: point.y })),
2766
+ {
2767
+ stroke: paint.stroke,
2768
+ strokeWidth: paint.widthWorld,
2769
+ strokeOpacity: paint.strokeOpacity
2770
+ },
2771
+ markup.tool,
2772
+ markup.points.length === 2
2773
+ );
2774
+ if (payload?.kind === "circle") {
2775
+ strokeElement = /* @__PURE__ */ jsxRuntime.jsx(
2776
+ reactNativeSkia.Circle,
2777
+ {
2778
+ cx: payload.cx,
2779
+ cy: payload.cy,
2780
+ r: payload.r,
2781
+ color: colorWithOpacity(payload.fill, payload.fillOpacity),
2782
+ style: "fill",
2783
+ antiAlias: true
2784
+ }
2785
+ );
2786
+ }
2787
+ if (payload?.kind === "fillPath") {
2788
+ strokeElement = /* @__PURE__ */ jsxRuntime.jsx(
2789
+ reactNativeSkia.Path,
2790
+ {
2791
+ path: payload.d,
2792
+ color: colorWithOpacity(payload.fill, payload.fillOpacity),
2793
+ style: "fill",
2794
+ fillType: "winding",
2795
+ antiAlias: true
2796
+ }
2797
+ );
2798
+ }
2799
+ if (payload?.kind === "strokePath") {
2800
+ strokeElement = /* @__PURE__ */ jsxRuntime.jsx(
2801
+ reactNativeSkia.Path,
2802
+ {
2803
+ path: payload.d,
2804
+ color: colorWithOpacity(payload.stroke, payload.strokeOpacity),
2805
+ style: "stroke",
2806
+ strokeWidth: payload.strokeWidth,
2807
+ strokeCap: "round",
2808
+ strokeJoin: "round",
2809
+ antiAlias: true
2810
+ }
2811
+ );
2812
+ }
2813
+ }
2814
+ }
2815
+ const cameraElement = camera2 ? /* @__PURE__ */ jsxRuntime.jsx(
2816
+ reactNativeSkia.Rect,
2817
+ {
2818
+ x: -camera2.x / camera2.zoom,
2819
+ y: -camera2.y / camera2.zoom,
2820
+ width: camera2.viewportWidth / camera2.zoom,
2821
+ height: camera2.viewportHeight / camera2.zoom,
2822
+ color,
2823
+ style: "stroke",
2824
+ strokeWidth: overlayStrokeWorld,
2825
+ antiAlias: true,
2826
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.DashPathEffect, { intervals: [marqueeDashWorld, marqueeDashWorld] })
2827
+ }
2828
+ ) : null;
2829
+ const cursorElement = cursor ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2830
+ /* @__PURE__ */ jsxRuntime.jsx(
2831
+ reactNativeSkia.Path,
2832
+ {
2833
+ 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`,
2834
+ color,
2835
+ style: "fill",
2836
+ antiAlias: true
2837
+ }
2838
+ ),
2839
+ peer.displayName ? /* @__PURE__ */ jsxRuntime.jsx(
2840
+ reactNativeSkia.Text,
2841
+ {
2842
+ x: cursor.x + labelOffsetX,
2843
+ y: cursor.y + labelOffsetY,
2844
+ text: peer.displayName,
2845
+ color,
2846
+ font: labelFont
2847
+ }
2848
+ ) : null
2849
+ ] }) : null;
2850
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNativeSkia.Group, { children: [
2851
+ cameraElement,
2852
+ strokeElement,
2853
+ cursorElement
2854
+ ] }, peer.clientId ?? peer.id);
2855
+ }) });
2856
+ }, [remotePresence, z, overlayStrokeWorld, marqueeDashWorld]);
2704
2857
  if (width <= 0 || height <= 0) return null;
2705
2858
  return /* @__PURE__ */ jsxRuntime.jsx(
2706
2859
  reactNativeSkia.Canvas,
@@ -2718,6 +2871,7 @@ function NativeInteractionOverlay({
2718
2871
  laserTrailElements,
2719
2872
  eraserTrailElements,
2720
2873
  eraserPreviewElements,
2874
+ remotePresenceElements,
2721
2875
  selectionElements
2722
2876
  ] })
2723
2877
  }
@@ -4220,6 +4374,7 @@ function fitCameraToWorldRect(camera, viewportW, viewportH, worldRect, padding)
4220
4374
  var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4221
4375
  items,
4222
4376
  selectedIds = [],
4377
+ remotePresence = [],
4223
4378
  toolId = "hand",
4224
4379
  toolLocked = false,
4225
4380
  interactive = false,
@@ -4227,6 +4382,9 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4227
4382
  onItemsChange,
4228
4383
  onToolChangeRequest,
4229
4384
  onWorldPointerDown,
4385
+ onWorldPointerMove,
4386
+ onWorldPointerLeave,
4387
+ onPlacementPreviewChange,
4230
4388
  onCameraChange,
4231
4389
  customPlacement,
4232
4390
  customPlacements = [],
@@ -4244,6 +4402,12 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4244
4402
  onToolChangeRequestRef.current = onToolChangeRequest;
4245
4403
  const onWorldPointerDownRef = react.useRef(onWorldPointerDown);
4246
4404
  onWorldPointerDownRef.current = onWorldPointerDown;
4405
+ const onWorldPointerMoveRef = react.useRef(onWorldPointerMove);
4406
+ onWorldPointerMoveRef.current = onWorldPointerMove;
4407
+ const onWorldPointerLeaveRef = react.useRef(onWorldPointerLeave);
4408
+ onWorldPointerLeaveRef.current = onWorldPointerLeave;
4409
+ const onPlacementPreviewChangeRef = react.useRef(onPlacementPreviewChange);
4410
+ onPlacementPreviewChangeRef.current = onPlacementPreviewChange;
4247
4411
  const onCameraChangeRef = react.useRef(onCameraChange);
4248
4412
  onCameraChangeRef.current = onCameraChange;
4249
4413
  const onItemsChangeRef = react.useRef(onItemsChange);
@@ -4259,8 +4423,13 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4259
4423
  const selectedIdsRef = react.useRef(selectedIds);
4260
4424
  selectedIdsRef.current = selectedIds;
4261
4425
  const dragStateRef = react.useRef({ kind: "idle" });
4262
- const [placementPreview, setPlacementPreview] = react.useState(
4263
- null
4426
+ const [placementPreview, setPlacementPreviewState] = react.useState(null);
4427
+ const setRealtimePlacementPreview = react.useCallback(
4428
+ (nextPreview) => {
4429
+ setPlacementPreviewState(nextPreview);
4430
+ onPlacementPreviewChangeRef.current?.(nextPreview);
4431
+ },
4432
+ []
4264
4433
  );
4265
4434
  const [eraserTrail, setEraserTrail] = react.useState([]);
4266
4435
  const [laserTrail, setLaserTrail] = react.useState([]);
@@ -4354,6 +4523,16 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4354
4523
  },
4355
4524
  []
4356
4525
  );
4526
+ const notifyWorldPointerMove = react.useCallback(
4527
+ (point) => {
4528
+ const { worldX, worldY } = screenToWorld(point.x, point.y);
4529
+ onWorldPointerMoveRef.current?.({ x: worldX, y: worldY });
4530
+ },
4531
+ [screenToWorld]
4532
+ );
4533
+ const notifyWorldPointerLeave = react.useCallback(() => {
4534
+ onWorldPointerLeaveRef.current?.();
4535
+ }, []);
4357
4536
  const requestRender = react.useCallback(() => {
4358
4537
  setCameraTick((n) => n + 1);
4359
4538
  onCameraChangeRef.current?.();
@@ -4393,6 +4572,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4393
4572
  const cam = cameraRef.current;
4394
4573
  if (!cam) return;
4395
4574
  const { worldX, worldY } = screenToWorld(sx, sy);
4575
+ onWorldPointerMoveRef.current?.({ x: worldX, y: worldY });
4396
4576
  if (tool === "hand") {
4397
4577
  dragStateRef.current = { kind: "pan" };
4398
4578
  return;
@@ -4478,7 +4658,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4478
4658
  kind: "marquee",
4479
4659
  startWorld: { x: worldX, y: worldY }
4480
4660
  };
4481
- setPlacementPreview({
4661
+ setRealtimePlacementPreview({
4482
4662
  kind: "marquee",
4483
4663
  rect: { x: worldX, y: worldY, width: 0, height: 0 }
4484
4664
  });
@@ -4498,7 +4678,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4498
4678
  }
4499
4679
  setLaserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
4500
4680
  } else {
4501
- setPlacementPreview({
4681
+ setRealtimePlacementPreview({
4502
4682
  kind: "stroke",
4503
4683
  tool,
4504
4684
  points: [{ x: worldX, y: worldY }],
@@ -4531,7 +4711,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4531
4711
  startWorld: { x: worldX, y: worldY },
4532
4712
  startScreen: { x: sx, y: sy }
4533
4713
  };
4534
- setPlacementPreview(
4714
+ setRealtimePlacementPreview(
4535
4715
  placementPreviewForTool(
4536
4716
  tool,
4537
4717
  { x: worldX, y: worldY },
@@ -4556,7 +4736,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4556
4736
  startWorld: { x: worldX, y: worldY },
4557
4737
  startScreen: { x: sx, y: sy }
4558
4738
  };
4559
- setPlacementPreview({
4739
+ setRealtimePlacementPreview({
4560
4740
  kind: "rect",
4561
4741
  rect: { x: worldX, y: worldY, width: 0, height: 0 }
4562
4742
  });
@@ -4585,7 +4765,13 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4585
4765
  }
4586
4766
  dragStateRef.current = { kind: "pan" };
4587
4767
  },
4588
- [interactive, requestSelectToolAfterUse, screenToWorld, updateToolCursorPoint]
4768
+ [
4769
+ interactive,
4770
+ requestSelectToolAfterUse,
4771
+ screenToWorld,
4772
+ setRealtimePlacementPreview,
4773
+ updateToolCursorPoint
4774
+ ]
4589
4775
  );
4590
4776
  const applyDragMoveAtScreenPoint = react.useCallback(
4591
4777
  (point, pagePoint) => {
@@ -4593,6 +4779,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4593
4779
  if (!cam) return;
4594
4780
  updateToolCursorPoint(point);
4595
4781
  const { worldX, worldY } = screenToWorld(point.x, point.y);
4782
+ onWorldPointerMoveRef.current?.({ x: worldX, y: worldY });
4596
4783
  const st = dragStateRef.current;
4597
4784
  if (st.kind === "pan") {
4598
4785
  const current = pagePoint ?? point;
@@ -4625,7 +4812,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4625
4812
  }
4626
4813
  return;
4627
4814
  }
4628
- setPlacementPreview({
4815
+ setRealtimePlacementPreview({
4629
4816
  kind: "stroke",
4630
4817
  tool: st.tool,
4631
4818
  points: [...pts],
@@ -4687,7 +4874,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4687
4874
  width: Math.abs(b.x - a.x),
4688
4875
  height: Math.abs(b.y - a.y)
4689
4876
  };
4690
- setPlacementPreview({ kind: "marquee", rect });
4877
+ setRealtimePlacementPreview({ kind: "marquee", rect });
4691
4878
  return;
4692
4879
  }
4693
4880
  if (st.kind === "erase") {
@@ -4705,7 +4892,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4705
4892
  return;
4706
4893
  }
4707
4894
  if (st.kind === "place") {
4708
- setPlacementPreview(
4895
+ setRealtimePlacementPreview(
4709
4896
  placementPreviewForTool(st.tool, st.startWorld, {
4710
4897
  x: worldX,
4711
4898
  y: worldY
@@ -4714,14 +4901,19 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4714
4901
  return;
4715
4902
  }
4716
4903
  if (st.kind === "custom-place") {
4717
- setPlacementPreview({
4904
+ setRealtimePlacementPreview({
4718
4905
  kind: "rect",
4719
4906
  rect: rectFromCorners(st.startWorld, { x: worldX, y: worldY })
4720
4907
  });
4721
4908
  return;
4722
4909
  }
4723
4910
  },
4724
- [requestRender, screenToWorld, updateToolCursorPoint]
4911
+ [
4912
+ requestRender,
4913
+ screenToWorld,
4914
+ setRealtimePlacementPreview,
4915
+ updateToolCursorPoint
4916
+ ]
4725
4917
  );
4726
4918
  const finishDragAtScreenPoint = react.useCallback(
4727
4919
  (point) => {
@@ -4731,7 +4923,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4731
4923
  const st = dragStateRef.current;
4732
4924
  if (st.kind === "draw") {
4733
4925
  dragStateRef.current = { kind: "idle" };
4734
- setPlacementPreview(null);
4926
+ setRealtimePlacementPreview(null);
4735
4927
  if (st.tool === "laser") {
4736
4928
  if (laserClearTimerRef.current) {
4737
4929
  clearTimeout(laserClearTimerRef.current);
@@ -4769,7 +4961,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4769
4961
  }
4770
4962
  if (st.kind === "marquee") {
4771
4963
  dragStateRef.current = { kind: "idle" };
4772
- setPlacementPreview(null);
4964
+ setRealtimePlacementPreview(null);
4773
4965
  const cam = cameraRef.current;
4774
4966
  if (!cam) return;
4775
4967
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -4800,7 +4992,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4800
4992
  }
4801
4993
  if (st.kind === "place") {
4802
4994
  dragStateRef.current = { kind: "idle" };
4803
- setPlacementPreview(null);
4995
+ setRealtimePlacementPreview(null);
4804
4996
  const change = onItemsChangeRef.current;
4805
4997
  if (!change) return;
4806
4998
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -4857,7 +5049,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4857
5049
  }
4858
5050
  if (st.kind === "custom-place") {
4859
5051
  dragStateRef.current = { kind: "idle" };
4860
- setPlacementPreview(null);
5052
+ setRealtimePlacementPreview(null);
4861
5053
  const change = onItemsChangeRef.current;
4862
5054
  if (!change) return;
4863
5055
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -4932,7 +5124,12 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4932
5124
  }
4933
5125
  dragStateRef.current = { kind: "idle" };
4934
5126
  },
4935
- [requestSelectToolAfterUse, screenToWorld, updateToolCursorPoint]
5127
+ [
5128
+ requestSelectToolAfterUse,
5129
+ screenToWorld,
5130
+ setRealtimePlacementPreview,
5131
+ updateToolCursorPoint
5132
+ ]
4936
5133
  );
4937
5134
  const handlePointerDown = react.useCallback(
4938
5135
  (event) => {
@@ -4948,9 +5145,10 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4948
5145
  applyDragMoveAtScreenPoint(point, point);
4949
5146
  return;
4950
5147
  }
5148
+ notifyWorldPointerMove(point);
4951
5149
  updateToolCursorPoint(point);
4952
5150
  },
4953
- [applyDragMoveAtScreenPoint, updateToolCursorPoint]
5151
+ [applyDragMoveAtScreenPoint, notifyWorldPointerMove, updateToolCursorPoint]
4954
5152
  );
4955
5153
  const handlePointerUp = react.useCallback(
4956
5154
  (event) => {
@@ -4969,6 +5167,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4969
5167
  const sy = evt.nativeEvent.locationY;
4970
5168
  if (touches && touches.length >= 2) {
4971
5169
  hideToolCursor();
5170
+ notifyWorldPointerLeave();
4972
5171
  dragStateRef.current = { kind: "pan" };
4973
5172
  return;
4974
5173
  }
@@ -4984,6 +5183,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4984
5183
  const pageY = evt.nativeEvent.pageY;
4985
5184
  if (touches && touches.length >= 2) {
4986
5185
  hideToolCursor();
5186
+ notifyWorldPointerLeave();
4987
5187
  const t0 = touches[0];
4988
5188
  const t1 = touches[1];
4989
5189
  if (t0 && t1) {
@@ -5015,8 +5215,9 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5015
5215
  lastPinchDist.current = null;
5016
5216
  lastPanPoint.current = null;
5017
5217
  hideToolCursor();
5218
+ notifyWorldPointerLeave();
5018
5219
  dragStateRef.current = { kind: "idle" };
5019
- setPlacementPreview(null);
5220
+ setRealtimePlacementPreview(null);
5020
5221
  setLaserTrail([]);
5021
5222
  setEraserTrail([]);
5022
5223
  setEraserPreviewIds([]);
@@ -5028,7 +5229,9 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5028
5229
  beginDragAtScreenPoint,
5029
5230
  finishDragAtScreenPoint,
5030
5231
  requestRender,
5031
- hideToolCursor
5232
+ hideToolCursor,
5233
+ notifyWorldPointerLeave,
5234
+ setRealtimePlacementPreview
5032
5235
  ]
5033
5236
  );
5034
5237
  react.useImperativeHandle(
@@ -5062,7 +5265,10 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5062
5265
  onPointerMove: handlePointerMove,
5063
5266
  onPointerUp: handlePointerUp,
5064
5267
  onPointerEnter: handlePointerMove,
5065
- onPointerLeave: hideToolCursor,
5268
+ onPointerLeave: () => {
5269
+ hideToolCursor();
5270
+ notifyWorldPointerLeave();
5271
+ },
5066
5272
  ...panResponder.panHandlers,
5067
5273
  children: size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5068
5274
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -5088,7 +5294,8 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5088
5294
  eraserPreviewItems: items.filter(
5089
5295
  (it) => eraserPreviewIds.includes(it.id)
5090
5296
  ),
5091
- previewStrokeStyle: strokeStyleState
5297
+ previewStrokeStyle: strokeStyleState,
5298
+ remotePresence
5092
5299
  }
5093
5300
  ),
5094
5301
  interactive && showStyleInspector && activeStyleToolId ? /* @__PURE__ */ jsxRuntime.jsx(