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.d.cts CHANGED
@@ -2,6 +2,7 @@ import { C as Camera2D, S as StrokeStyle } from './shape-builders-CKEMjivV.cjs';
2
2
  export { o as createFreehandStrokeItem, q as createImageItem, t as createShapeId } from './shape-builders-CKEMjivV.cjs';
3
3
  import { V as VectorSceneItem, R as Rect } from './types-BCCvY6ie.cjs';
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
+ import { R as RemotePresencePeer } from './types-BQUbxMgz.cjs';
5
6
  import { StyleProp, ViewStyle, TextStyle } from 'react-native';
6
7
  import * as react from 'react';
7
8
  import { ReactNode } from 'react';
@@ -58,8 +59,9 @@ type NativeInteractionOverlayProps = {
58
59
  readonly laserTrail?: readonly TimedTrailPoint[];
59
60
  readonly eraserPreviewItems?: readonly VectorSceneItem[];
60
61
  readonly previewStrokeStyle?: StrokeStyle;
62
+ readonly remotePresence?: readonly RemotePresencePeer[];
61
63
  };
62
- declare function NativeInteractionOverlay({ camera, width, height, selectedItems, showResizeHandles, placementPreview, eraserTrail, laserTrail, eraserPreviewItems, previewStrokeStyle, }: NativeInteractionOverlayProps): react_jsx_runtime.JSX.Element | null;
64
+ declare function NativeInteractionOverlay({ camera, width, height, selectedItems, showResizeHandles, placementPreview, eraserTrail, laserTrail, eraserPreviewItems, previewStrokeStyle, remotePresence, }: NativeInteractionOverlayProps): react_jsx_runtime.JSX.Element | null;
63
65
 
64
66
  type NativeSceneRendererProps = {
65
67
  readonly items: readonly VectorSceneItem[];
@@ -214,6 +216,7 @@ type NativeWorldPointerDownDetail = {
214
216
  type NativeVectorViewportProps = {
215
217
  readonly items: readonly VectorSceneItem[];
216
218
  readonly selectedIds?: readonly string[];
219
+ readonly remotePresence?: readonly RemotePresencePeer[];
217
220
  readonly toolId?: string;
218
221
  readonly toolLocked?: boolean;
219
222
  readonly interactive?: boolean;
@@ -221,6 +224,12 @@ type NativeVectorViewportProps = {
221
224
  readonly onItemsChange?: (items: VectorSceneItem[]) => void;
222
225
  readonly onToolChangeRequest?: (toolId: string) => void;
223
226
  readonly onWorldPointerDown?: (detail: NativeWorldPointerDownDetail) => void;
227
+ readonly onWorldPointerMove?: (world: {
228
+ readonly x: number;
229
+ readonly y: number;
230
+ }) => void;
231
+ readonly onWorldPointerLeave?: () => void;
232
+ readonly onPlacementPreviewChange?: (preview: PlacementPreview | null) => void;
224
233
  readonly onCameraChange?: () => void;
225
234
  readonly customPlacement?: NativeCustomShapePlacementOptions;
226
235
  readonly customPlacements?: readonly NativeCustomShapePlacementOptions[];
package/dist/native.d.ts CHANGED
@@ -2,6 +2,7 @@ import { C as Camera2D, S as StrokeStyle } from './shape-builders-Cyh8zvDG.js';
2
2
  export { o as createFreehandStrokeItem, q as createImageItem, t as createShapeId } from './shape-builders-Cyh8zvDG.js';
3
3
  import { V as VectorSceneItem, R as Rect } from './types-BCCvY6ie.js';
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
+ import { R as RemotePresencePeer } from './types-B82WiQQh.js';
5
6
  import { StyleProp, ViewStyle, TextStyle } from 'react-native';
6
7
  import * as react from 'react';
7
8
  import { ReactNode } from 'react';
@@ -58,8 +59,9 @@ type NativeInteractionOverlayProps = {
58
59
  readonly laserTrail?: readonly TimedTrailPoint[];
59
60
  readonly eraserPreviewItems?: readonly VectorSceneItem[];
60
61
  readonly previewStrokeStyle?: StrokeStyle;
62
+ readonly remotePresence?: readonly RemotePresencePeer[];
61
63
  };
62
- declare function NativeInteractionOverlay({ camera, width, height, selectedItems, showResizeHandles, placementPreview, eraserTrail, laserTrail, eraserPreviewItems, previewStrokeStyle, }: NativeInteractionOverlayProps): react_jsx_runtime.JSX.Element | null;
64
+ declare function NativeInteractionOverlay({ camera, width, height, selectedItems, showResizeHandles, placementPreview, eraserTrail, laserTrail, eraserPreviewItems, previewStrokeStyle, remotePresence, }: NativeInteractionOverlayProps): react_jsx_runtime.JSX.Element | null;
63
65
 
64
66
  type NativeSceneRendererProps = {
65
67
  readonly items: readonly VectorSceneItem[];
@@ -214,6 +216,7 @@ type NativeWorldPointerDownDetail = {
214
216
  type NativeVectorViewportProps = {
215
217
  readonly items: readonly VectorSceneItem[];
216
218
  readonly selectedIds?: readonly string[];
219
+ readonly remotePresence?: readonly RemotePresencePeer[];
217
220
  readonly toolId?: string;
218
221
  readonly toolLocked?: boolean;
219
222
  readonly interactive?: boolean;
@@ -221,6 +224,12 @@ type NativeVectorViewportProps = {
221
224
  readonly onItemsChange?: (items: VectorSceneItem[]) => void;
222
225
  readonly onToolChangeRequest?: (toolId: string) => void;
223
226
  readonly onWorldPointerDown?: (detail: NativeWorldPointerDownDetail) => void;
227
+ readonly onWorldPointerMove?: (world: {
228
+ readonly x: number;
229
+ readonly y: number;
230
+ }) => void;
231
+ readonly onWorldPointerLeave?: () => void;
232
+ readonly onPlacementPreviewChange?: (preview: PlacementPreview | null) => void;
224
233
  readonly onCameraChange?: () => void;
225
234
  readonly customPlacement?: NativeCustomShapePlacementOptions;
226
235
  readonly customPlacements?: readonly NativeCustomShapePlacementOptions[];
package/dist/native.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import getStroke from 'perfect-freehand';
2
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';
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
 
@@ -34,7 +34,7 @@ var LINK_PLUGIN_KEY = "canvuLink";
34
34
  var DEFAULT_LINK_CARD_WIDTH = 320;
35
35
  var DEFAULT_LINK_CARD_HEIGHT = 70;
36
36
  var LINK_CARD_MIN_SCALE = 0.6;
37
- var LINK_CARD_MAX_SCALE = 2.5;
37
+ var LINK_CARD_MAX_SCALE = 6;
38
38
  var LINK_CARD_ASPECT = DEFAULT_LINK_CARD_WIDTH / DEFAULT_LINK_CARD_HEIGHT;
39
39
  var LINK_CARD_BORDER = "oklch(0.918 0.008 255)";
40
40
  var LINK_CARD_BORDER_STRONG = "oklch(0.86 0.012 255)";
@@ -47,7 +47,6 @@ var formatNumber = (value) => {
47
47
  const rounded = Math.round(value * 100) / 100;
48
48
  return Object.is(rounded, -0) ? "0" : String(rounded);
49
49
  };
50
- var escapeXmlAttribute = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
51
50
  var escapeHtmlText = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
52
51
  var getLinkHostname = (href) => {
53
52
  try {
@@ -72,7 +71,6 @@ var getLinkInitial = (hostname) => {
72
71
  const first = hostname.trim().charAt(0).toUpperCase();
73
72
  return first || "L";
74
73
  };
75
- var buildGoogleFaviconUrl = (hostname) => hostname ? `https://www.google.com/s2/favicons?domain=${encodeURIComponent(hostname)}&sz=64` : null;
76
74
  var getStableLinkIdSuffix = (value) => {
77
75
  let hash = 0;
78
76
  for (const char of value) {
@@ -95,16 +93,13 @@ function buildLinkCardSvg(width, _height, link) {
95
93
  const title = link.title?.trim() || hostname || "Link";
96
94
  const protocol = getLinkProtocol(link.href);
97
95
  const subtitle = hostname || link.href;
98
- const favicon = link.favicon?.trim() || buildGoogleFaviconUrl(hostname);
99
96
  const idSuffix = getStableLinkIdSuffix(`${hostname}:${link.href}`);
100
- const clipId = `canvu-link-favicon-${idSuffix}`;
101
97
  const gradientId = `canvu-link-favicon-gradient-${idSuffix}`;
102
98
  const buttonX = contentWidth - padding - buttonSize;
103
99
  const buttonY = (contentHeight - buttonSize) / 2;
104
100
  const isSecure = protocol === "https:";
105
101
  const subtitleX = isSecure ? textX + 13 : textX;
106
102
  const subtitleWidth = isSecure ? textWidth - 13 : textWidth;
107
- 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})" />` : "";
108
103
  return `
109
104
  <style>
110
105
  .canvu-link-card-root .canvu-link-card { transition: transform .18s ease, filter .18s ease, stroke .18s ease; }
@@ -119,13 +114,9 @@ function buildLinkCardSvg(width, _height, link) {
119
114
  <stop stop-color="${LINK_CARD_ACCENT}" />
120
115
  <stop offset="1" stop-color="${LINK_CARD_ACCENT_DEEP}" />
121
116
  </linearGradient>
122
- <clipPath id="${clipId}">
123
- <rect x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" rx="11" />
124
- </clipPath>
125
117
  </defs>
126
118
  <rect x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" rx="11" fill="url(#${gradientId})" />
127
119
  <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>
128
- ${faviconImage}
129
120
  ${buildLinkTextBand({ x: textX, y: 16, width: textWidth, height: 19, text: title, fontSize: 14.5, color: LINK_CARD_TITLE_COLOR, fontWeight: 700 })}
130
121
  ${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>` : ""}
131
122
  ${buildLinkTextBand({ x: subtitleX, y: 36, width: subtitleWidth, height: 17, text: subtitle, fontSize: 12.5, color: LINK_CARD_TEXT_COLOR })}
@@ -1363,6 +1354,17 @@ function computeResizeBoundsFixedAspect(bounds, handle, currentWorld, aspect) {
1363
1354
  }
1364
1355
  }
1365
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
+
1366
1368
  // src/scene/freehand-path.ts
1367
1369
  function smoothFreehandPointsToPathD(points) {
1368
1370
  const n = points.length;
@@ -2281,6 +2283,26 @@ var HANDLE_ORDER = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
2281
2283
  var ERASER_PREVIEW_OPACITY = 0.3;
2282
2284
  var OVERLAY_STROKE_PX = 1.25;
2283
2285
  var MARQUEE_DASH_PX = 4;
2286
+ var REMOTE_CURSOR_SCREEN_PX = 22;
2287
+ var REMOTE_LABEL_SCREEN_PX = 12;
2288
+ function remoteStrokePaint(tool, fallback) {
2289
+ if (tool === "laser") {
2290
+ return { stroke: LASER_TINT, strokeOpacity: 0.92, widthWorld: 4 };
2291
+ }
2292
+ if (tool === "marker") {
2293
+ return { stroke: fallback, strokeOpacity: 0.45, widthWorld: 14 };
2294
+ }
2295
+ if (tool === "brush") {
2296
+ return { stroke: fallback, strokeOpacity: 0.85, widthWorld: 5 };
2297
+ }
2298
+ if (tool === "pencil") {
2299
+ return { stroke: fallback, strokeOpacity: 0.9, widthWorld: 2.5 };
2300
+ }
2301
+ return { stroke: fallback, strokeOpacity: 0.95, widthWorld: 3.5 };
2302
+ }
2303
+ function isRemoteFreehandTool(tool) {
2304
+ return tool === "draw" || tool === "marker" || tool === "pencil" || tool === "brush";
2305
+ }
2284
2306
  function pointsToSmoothPathD(points) {
2285
2307
  if (points.length < 2) return null;
2286
2308
  const d = smoothFreehandPointsToPathD(points);
@@ -2301,7 +2323,8 @@ function NativeInteractionOverlay({
2301
2323
  eraserTrail,
2302
2324
  laserTrail,
2303
2325
  eraserPreviewItems = [],
2304
- previewStrokeStyle
2326
+ previewStrokeStyle,
2327
+ remotePresence = []
2305
2328
  }) {
2306
2329
  const z = camera.zoom;
2307
2330
  const camTransform = skiaCameraTransform(z, camera.x, camera.y);
@@ -2695,6 +2718,136 @@ function NativeInteractionOverlay({
2695
2718
  )
2696
2719
  ] });
2697
2720
  }, [laserTrail, z]);
2721
+ const remotePresenceElements = useMemo(() => {
2722
+ if (remotePresence.length === 0) return null;
2723
+ const labelFont = matchFont({ fontSize: REMOTE_LABEL_SCREEN_PX / z });
2724
+ const cursorSize = REMOTE_CURSOR_SCREEN_PX / z;
2725
+ const labelOffsetX = 14 / z;
2726
+ const labelOffsetY = 18 / z;
2727
+ return /* @__PURE__ */ jsx(Fragment, { children: remotePresence.map((peer) => {
2728
+ const color = peer.color ?? defaultPresenceColorForId(peer.id);
2729
+ const markup = peer.markupStroke;
2730
+ const cursor = peer.cursor;
2731
+ const camera2 = peer.camera;
2732
+ let strokeElement = null;
2733
+ if (markup && markup.points.length > 0) {
2734
+ const fallbackPaint = remoteStrokePaint(markup.tool, color);
2735
+ const paint = {
2736
+ stroke: markup.stroke ?? fallbackPaint.stroke,
2737
+ strokeOpacity: markup.strokeOpacity ?? fallbackPaint.strokeOpacity,
2738
+ widthWorld: markup.strokeWidth ?? fallbackPaint.widthWorld
2739
+ };
2740
+ if (markup.tool === "laser") {
2741
+ const d = markup.points.length >= 2 ? smoothFreehandPointsToPathD([...markup.points]) : null;
2742
+ if (d) {
2743
+ strokeElement = /* @__PURE__ */ jsx(
2744
+ Path,
2745
+ {
2746
+ path: d,
2747
+ color: colorWithOpacity(paint.stroke, paint.strokeOpacity),
2748
+ style: "stroke",
2749
+ strokeWidth: Math.max(paint.widthWorld, OVERLAY_STROKE_PX) / z,
2750
+ strokeCap: "round",
2751
+ strokeJoin: "round",
2752
+ antiAlias: true
2753
+ }
2754
+ );
2755
+ }
2756
+ }
2757
+ if (!strokeElement && isRemoteFreehandTool(markup.tool)) {
2758
+ const payload = computeFreehandSvgPayload(
2759
+ markup.points.map((point) => ({ x: point.x, y: point.y })),
2760
+ {
2761
+ stroke: paint.stroke,
2762
+ strokeWidth: paint.widthWorld,
2763
+ strokeOpacity: paint.strokeOpacity
2764
+ },
2765
+ markup.tool,
2766
+ markup.points.length === 2
2767
+ );
2768
+ if (payload?.kind === "circle") {
2769
+ strokeElement = /* @__PURE__ */ jsx(
2770
+ Circle,
2771
+ {
2772
+ cx: payload.cx,
2773
+ cy: payload.cy,
2774
+ r: payload.r,
2775
+ color: colorWithOpacity(payload.fill, payload.fillOpacity),
2776
+ style: "fill",
2777
+ antiAlias: true
2778
+ }
2779
+ );
2780
+ }
2781
+ if (payload?.kind === "fillPath") {
2782
+ strokeElement = /* @__PURE__ */ jsx(
2783
+ Path,
2784
+ {
2785
+ path: payload.d,
2786
+ color: colorWithOpacity(payload.fill, payload.fillOpacity),
2787
+ style: "fill",
2788
+ fillType: "winding",
2789
+ antiAlias: true
2790
+ }
2791
+ );
2792
+ }
2793
+ if (payload?.kind === "strokePath") {
2794
+ strokeElement = /* @__PURE__ */ jsx(
2795
+ Path,
2796
+ {
2797
+ path: payload.d,
2798
+ color: colorWithOpacity(payload.stroke, payload.strokeOpacity),
2799
+ style: "stroke",
2800
+ strokeWidth: payload.strokeWidth,
2801
+ strokeCap: "round",
2802
+ strokeJoin: "round",
2803
+ antiAlias: true
2804
+ }
2805
+ );
2806
+ }
2807
+ }
2808
+ }
2809
+ const cameraElement = camera2 ? /* @__PURE__ */ jsx(
2810
+ Rect,
2811
+ {
2812
+ x: -camera2.x / camera2.zoom,
2813
+ y: -camera2.y / camera2.zoom,
2814
+ width: camera2.viewportWidth / camera2.zoom,
2815
+ height: camera2.viewportHeight / camera2.zoom,
2816
+ color,
2817
+ style: "stroke",
2818
+ strokeWidth: overlayStrokeWorld,
2819
+ antiAlias: true,
2820
+ children: /* @__PURE__ */ jsx(DashPathEffect, { intervals: [marqueeDashWorld, marqueeDashWorld] })
2821
+ }
2822
+ ) : null;
2823
+ const cursorElement = cursor ? /* @__PURE__ */ jsxs(Fragment, { children: [
2824
+ /* @__PURE__ */ jsx(
2825
+ Path,
2826
+ {
2827
+ 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`,
2828
+ color,
2829
+ style: "fill",
2830
+ antiAlias: true
2831
+ }
2832
+ ),
2833
+ peer.displayName ? /* @__PURE__ */ jsx(
2834
+ Text,
2835
+ {
2836
+ x: cursor.x + labelOffsetX,
2837
+ y: cursor.y + labelOffsetY,
2838
+ text: peer.displayName,
2839
+ color,
2840
+ font: labelFont
2841
+ }
2842
+ ) : null
2843
+ ] }) : null;
2844
+ return /* @__PURE__ */ jsxs(Group, { children: [
2845
+ cameraElement,
2846
+ strokeElement,
2847
+ cursorElement
2848
+ ] }, peer.clientId ?? peer.id);
2849
+ }) });
2850
+ }, [remotePresence, z, overlayStrokeWorld, marqueeDashWorld]);
2698
2851
  if (width <= 0 || height <= 0) return null;
2699
2852
  return /* @__PURE__ */ jsx(
2700
2853
  Canvas,
@@ -2712,6 +2865,7 @@ function NativeInteractionOverlay({
2712
2865
  laserTrailElements,
2713
2866
  eraserTrailElements,
2714
2867
  eraserPreviewElements,
2868
+ remotePresenceElements,
2715
2869
  selectionElements
2716
2870
  ] })
2717
2871
  }
@@ -4214,6 +4368,7 @@ function fitCameraToWorldRect(camera, viewportW, viewportH, worldRect, padding)
4214
4368
  var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4215
4369
  items,
4216
4370
  selectedIds = [],
4371
+ remotePresence = [],
4217
4372
  toolId = "hand",
4218
4373
  toolLocked = false,
4219
4374
  interactive = false,
@@ -4221,6 +4376,9 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4221
4376
  onItemsChange,
4222
4377
  onToolChangeRequest,
4223
4378
  onWorldPointerDown,
4379
+ onWorldPointerMove,
4380
+ onWorldPointerLeave,
4381
+ onPlacementPreviewChange,
4224
4382
  onCameraChange,
4225
4383
  customPlacement,
4226
4384
  customPlacements = [],
@@ -4238,6 +4396,12 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4238
4396
  onToolChangeRequestRef.current = onToolChangeRequest;
4239
4397
  const onWorldPointerDownRef = useRef(onWorldPointerDown);
4240
4398
  onWorldPointerDownRef.current = onWorldPointerDown;
4399
+ const onWorldPointerMoveRef = useRef(onWorldPointerMove);
4400
+ onWorldPointerMoveRef.current = onWorldPointerMove;
4401
+ const onWorldPointerLeaveRef = useRef(onWorldPointerLeave);
4402
+ onWorldPointerLeaveRef.current = onWorldPointerLeave;
4403
+ const onPlacementPreviewChangeRef = useRef(onPlacementPreviewChange);
4404
+ onPlacementPreviewChangeRef.current = onPlacementPreviewChange;
4241
4405
  const onCameraChangeRef = useRef(onCameraChange);
4242
4406
  onCameraChangeRef.current = onCameraChange;
4243
4407
  const onItemsChangeRef = useRef(onItemsChange);
@@ -4253,8 +4417,13 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4253
4417
  const selectedIdsRef = useRef(selectedIds);
4254
4418
  selectedIdsRef.current = selectedIds;
4255
4419
  const dragStateRef = useRef({ kind: "idle" });
4256
- const [placementPreview, setPlacementPreview] = useState(
4257
- null
4420
+ const [placementPreview, setPlacementPreviewState] = useState(null);
4421
+ const setRealtimePlacementPreview = useCallback(
4422
+ (nextPreview) => {
4423
+ setPlacementPreviewState(nextPreview);
4424
+ onPlacementPreviewChangeRef.current?.(nextPreview);
4425
+ },
4426
+ []
4258
4427
  );
4259
4428
  const [eraserTrail, setEraserTrail] = useState([]);
4260
4429
  const [laserTrail, setLaserTrail] = useState([]);
@@ -4348,6 +4517,16 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4348
4517
  },
4349
4518
  []
4350
4519
  );
4520
+ const notifyWorldPointerMove = useCallback(
4521
+ (point) => {
4522
+ const { worldX, worldY } = screenToWorld(point.x, point.y);
4523
+ onWorldPointerMoveRef.current?.({ x: worldX, y: worldY });
4524
+ },
4525
+ [screenToWorld]
4526
+ );
4527
+ const notifyWorldPointerLeave = useCallback(() => {
4528
+ onWorldPointerLeaveRef.current?.();
4529
+ }, []);
4351
4530
  const requestRender = useCallback(() => {
4352
4531
  setCameraTick((n) => n + 1);
4353
4532
  onCameraChangeRef.current?.();
@@ -4387,6 +4566,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4387
4566
  const cam = cameraRef.current;
4388
4567
  if (!cam) return;
4389
4568
  const { worldX, worldY } = screenToWorld(sx, sy);
4569
+ onWorldPointerMoveRef.current?.({ x: worldX, y: worldY });
4390
4570
  if (tool === "hand") {
4391
4571
  dragStateRef.current = { kind: "pan" };
4392
4572
  return;
@@ -4472,7 +4652,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4472
4652
  kind: "marquee",
4473
4653
  startWorld: { x: worldX, y: worldY }
4474
4654
  };
4475
- setPlacementPreview({
4655
+ setRealtimePlacementPreview({
4476
4656
  kind: "marquee",
4477
4657
  rect: { x: worldX, y: worldY, width: 0, height: 0 }
4478
4658
  });
@@ -4492,7 +4672,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4492
4672
  }
4493
4673
  setLaserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
4494
4674
  } else {
4495
- setPlacementPreview({
4675
+ setRealtimePlacementPreview({
4496
4676
  kind: "stroke",
4497
4677
  tool,
4498
4678
  points: [{ x: worldX, y: worldY }],
@@ -4525,7 +4705,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4525
4705
  startWorld: { x: worldX, y: worldY },
4526
4706
  startScreen: { x: sx, y: sy }
4527
4707
  };
4528
- setPlacementPreview(
4708
+ setRealtimePlacementPreview(
4529
4709
  placementPreviewForTool(
4530
4710
  tool,
4531
4711
  { x: worldX, y: worldY },
@@ -4550,7 +4730,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4550
4730
  startWorld: { x: worldX, y: worldY },
4551
4731
  startScreen: { x: sx, y: sy }
4552
4732
  };
4553
- setPlacementPreview({
4733
+ setRealtimePlacementPreview({
4554
4734
  kind: "rect",
4555
4735
  rect: { x: worldX, y: worldY, width: 0, height: 0 }
4556
4736
  });
@@ -4579,7 +4759,13 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4579
4759
  }
4580
4760
  dragStateRef.current = { kind: "pan" };
4581
4761
  },
4582
- [interactive, requestSelectToolAfterUse, screenToWorld, updateToolCursorPoint]
4762
+ [
4763
+ interactive,
4764
+ requestSelectToolAfterUse,
4765
+ screenToWorld,
4766
+ setRealtimePlacementPreview,
4767
+ updateToolCursorPoint
4768
+ ]
4583
4769
  );
4584
4770
  const applyDragMoveAtScreenPoint = useCallback(
4585
4771
  (point, pagePoint) => {
@@ -4587,6 +4773,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4587
4773
  if (!cam) return;
4588
4774
  updateToolCursorPoint(point);
4589
4775
  const { worldX, worldY } = screenToWorld(point.x, point.y);
4776
+ onWorldPointerMoveRef.current?.({ x: worldX, y: worldY });
4590
4777
  const st = dragStateRef.current;
4591
4778
  if (st.kind === "pan") {
4592
4779
  const current = pagePoint ?? point;
@@ -4619,7 +4806,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4619
4806
  }
4620
4807
  return;
4621
4808
  }
4622
- setPlacementPreview({
4809
+ setRealtimePlacementPreview({
4623
4810
  kind: "stroke",
4624
4811
  tool: st.tool,
4625
4812
  points: [...pts],
@@ -4681,7 +4868,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4681
4868
  width: Math.abs(b.x - a.x),
4682
4869
  height: Math.abs(b.y - a.y)
4683
4870
  };
4684
- setPlacementPreview({ kind: "marquee", rect });
4871
+ setRealtimePlacementPreview({ kind: "marquee", rect });
4685
4872
  return;
4686
4873
  }
4687
4874
  if (st.kind === "erase") {
@@ -4699,7 +4886,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4699
4886
  return;
4700
4887
  }
4701
4888
  if (st.kind === "place") {
4702
- setPlacementPreview(
4889
+ setRealtimePlacementPreview(
4703
4890
  placementPreviewForTool(st.tool, st.startWorld, {
4704
4891
  x: worldX,
4705
4892
  y: worldY
@@ -4708,14 +4895,19 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4708
4895
  return;
4709
4896
  }
4710
4897
  if (st.kind === "custom-place") {
4711
- setPlacementPreview({
4898
+ setRealtimePlacementPreview({
4712
4899
  kind: "rect",
4713
4900
  rect: rectFromCorners(st.startWorld, { x: worldX, y: worldY })
4714
4901
  });
4715
4902
  return;
4716
4903
  }
4717
4904
  },
4718
- [requestRender, screenToWorld, updateToolCursorPoint]
4905
+ [
4906
+ requestRender,
4907
+ screenToWorld,
4908
+ setRealtimePlacementPreview,
4909
+ updateToolCursorPoint
4910
+ ]
4719
4911
  );
4720
4912
  const finishDragAtScreenPoint = useCallback(
4721
4913
  (point) => {
@@ -4725,7 +4917,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4725
4917
  const st = dragStateRef.current;
4726
4918
  if (st.kind === "draw") {
4727
4919
  dragStateRef.current = { kind: "idle" };
4728
- setPlacementPreview(null);
4920
+ setRealtimePlacementPreview(null);
4729
4921
  if (st.tool === "laser") {
4730
4922
  if (laserClearTimerRef.current) {
4731
4923
  clearTimeout(laserClearTimerRef.current);
@@ -4763,7 +4955,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4763
4955
  }
4764
4956
  if (st.kind === "marquee") {
4765
4957
  dragStateRef.current = { kind: "idle" };
4766
- setPlacementPreview(null);
4958
+ setRealtimePlacementPreview(null);
4767
4959
  const cam = cameraRef.current;
4768
4960
  if (!cam) return;
4769
4961
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -4794,7 +4986,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4794
4986
  }
4795
4987
  if (st.kind === "place") {
4796
4988
  dragStateRef.current = { kind: "idle" };
4797
- setPlacementPreview(null);
4989
+ setRealtimePlacementPreview(null);
4798
4990
  const change = onItemsChangeRef.current;
4799
4991
  if (!change) return;
4800
4992
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -4851,7 +5043,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4851
5043
  }
4852
5044
  if (st.kind === "custom-place") {
4853
5045
  dragStateRef.current = { kind: "idle" };
4854
- setPlacementPreview(null);
5046
+ setRealtimePlacementPreview(null);
4855
5047
  const change = onItemsChangeRef.current;
4856
5048
  if (!change) return;
4857
5049
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -4926,7 +5118,12 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4926
5118
  }
4927
5119
  dragStateRef.current = { kind: "idle" };
4928
5120
  },
4929
- [requestSelectToolAfterUse, screenToWorld, updateToolCursorPoint]
5121
+ [
5122
+ requestSelectToolAfterUse,
5123
+ screenToWorld,
5124
+ setRealtimePlacementPreview,
5125
+ updateToolCursorPoint
5126
+ ]
4930
5127
  );
4931
5128
  const handlePointerDown = useCallback(
4932
5129
  (event) => {
@@ -4942,9 +5139,10 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4942
5139
  applyDragMoveAtScreenPoint(point, point);
4943
5140
  return;
4944
5141
  }
5142
+ notifyWorldPointerMove(point);
4945
5143
  updateToolCursorPoint(point);
4946
5144
  },
4947
- [applyDragMoveAtScreenPoint, updateToolCursorPoint]
5145
+ [applyDragMoveAtScreenPoint, notifyWorldPointerMove, updateToolCursorPoint]
4948
5146
  );
4949
5147
  const handlePointerUp = useCallback(
4950
5148
  (event) => {
@@ -4963,6 +5161,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4963
5161
  const sy = evt.nativeEvent.locationY;
4964
5162
  if (touches && touches.length >= 2) {
4965
5163
  hideToolCursor();
5164
+ notifyWorldPointerLeave();
4966
5165
  dragStateRef.current = { kind: "pan" };
4967
5166
  return;
4968
5167
  }
@@ -4978,6 +5177,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4978
5177
  const pageY = evt.nativeEvent.pageY;
4979
5178
  if (touches && touches.length >= 2) {
4980
5179
  hideToolCursor();
5180
+ notifyWorldPointerLeave();
4981
5181
  const t0 = touches[0];
4982
5182
  const t1 = touches[1];
4983
5183
  if (t0 && t1) {
@@ -5009,8 +5209,9 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5009
5209
  lastPinchDist.current = null;
5010
5210
  lastPanPoint.current = null;
5011
5211
  hideToolCursor();
5212
+ notifyWorldPointerLeave();
5012
5213
  dragStateRef.current = { kind: "idle" };
5013
- setPlacementPreview(null);
5214
+ setRealtimePlacementPreview(null);
5014
5215
  setLaserTrail([]);
5015
5216
  setEraserTrail([]);
5016
5217
  setEraserPreviewIds([]);
@@ -5022,7 +5223,9 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5022
5223
  beginDragAtScreenPoint,
5023
5224
  finishDragAtScreenPoint,
5024
5225
  requestRender,
5025
- hideToolCursor
5226
+ hideToolCursor,
5227
+ notifyWorldPointerLeave,
5228
+ setRealtimePlacementPreview
5026
5229
  ]
5027
5230
  );
5028
5231
  useImperativeHandle(
@@ -5056,7 +5259,10 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5056
5259
  onPointerMove: handlePointerMove,
5057
5260
  onPointerUp: handlePointerUp,
5058
5261
  onPointerEnter: handlePointerMove,
5059
- onPointerLeave: hideToolCursor,
5262
+ onPointerLeave: () => {
5263
+ hideToolCursor();
5264
+ notifyWorldPointerLeave();
5265
+ },
5060
5266
  ...panResponder.panHandlers,
5061
5267
  children: size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
5062
5268
  /* @__PURE__ */ jsx(
@@ -5082,7 +5288,8 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5082
5288
  eraserPreviewItems: items.filter(
5083
5289
  (it) => eraserPreviewIds.includes(it.id)
5084
5290
  ),
5085
- previewStrokeStyle: strokeStyleState
5291
+ previewStrokeStyle: strokeStyleState,
5292
+ remotePresence
5086
5293
  }
5087
5294
  ),
5088
5295
  interactive && showStyleInspector && activeStyleToolId ? /* @__PURE__ */ jsx(