canvu-react 0.3.8 → 0.3.10

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,5 +1,5 @@
1
- import { P as PlacementPreview, X as RemotePresenceMarkupStroke, Y as RemotePresencePeer, Z as RealtimeConnectionState, J as VectorViewportHandle, b as VectorToolDefinition, K as VectorViewportProps, C as CanvasPlugin, f as CanvasPluginRenderContext } from './types-CW146bKP.cjs';
2
- export { _ as PresenceOverlayRenderContext } from './types-CW146bKP.cjs';
1
+ import { P as PlacementPreview, X as RemotePresenceMarkupStroke, Y as RemotePresencePeer, Z as RemotePresenceCamera, _ as RealtimeConnectionState, J as VectorViewportHandle, b as VectorToolDefinition, K as VectorViewportProps, C as CanvasPlugin, f as CanvasPluginRenderContext } from './types-BtLGGw0r.cjs';
2
+ export { $ as PresenceOverlayRenderContext } from './types-BtLGGw0r.cjs';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { C as Camera2D } from './camera-BwQjm5oh.cjs';
5
5
  import { V as VectorSceneItem, R as Rect } from './types-CB0TZZuk.cjs';
@@ -62,6 +62,7 @@ type RealtimePresencePayload = {
62
62
  readonly y: number;
63
63
  } | null;
64
64
  readonly markupStroke?: RemotePresenceMarkupStroke | null;
65
+ readonly camera?: RemotePresenceCamera | null;
65
66
  readonly activeTool?: string;
66
67
  };
67
68
  type RealtimeClientPeer = {
@@ -191,6 +192,7 @@ type UseRealtimeCommentsResult = {
191
192
  tools: VectorToolDefinition[];
192
193
  overlay: ReactNode;
193
194
  viewport: RealtimeCommentsViewportBindings;
195
+ handleViewportItemsChange: (items: VectorSceneItem[], onItemsChange?: (items: VectorSceneItem[]) => void) => void;
194
196
  isComposerOpen: boolean;
195
197
  closeComposer: () => void;
196
198
  };
@@ -215,16 +217,19 @@ type UseRealtimeSessionOptions = {
215
217
  connectTimeoutMs?: number;
216
218
  onError?: (message: string) => void;
217
219
  };
218
- type RealtimeViewportPresenceBindings = Pick<VectorViewportProps, "remotePresence" | "onWorldPointerMove" | "onWorldPointerLeave" | "onPlacementPreviewChange">;
220
+ type RealtimeViewportPresenceBindings = Pick<VectorViewportProps, "remotePresence" | "onWorldPointerMove" | "onWorldPointerLeave" | "onPlacementPreviewChange" | "onCameraChange">;
221
+ type BindViewportPresenceOptions = {
222
+ activeTool?: string;
223
+ viewportRef?: RefObject<VectorViewportHandle | null>;
224
+ };
219
225
  type UseRealtimeSessionResult = {
220
226
  connection: RealtimeConnectionInfo;
221
227
  sessionPeers: RealtimeSessionPeer[];
222
228
  remotePresence: RealtimeSessionPeer[];
223
229
  remoteAdapter: VectorCanvasRemoteAdapter;
224
230
  document: RealtimeDocumentSnapshot | null;
225
- bindViewportPresence: (options?: {
226
- activeTool?: string;
227
- }) => RealtimeViewportPresenceBindings;
231
+ bindViewportPresence: (options?: BindViewportPresenceOptions) => RealtimeViewportPresenceBindings;
232
+ syncViewportPresence: (options?: BindViewportPresenceOptions) => boolean;
228
233
  disconnect: () => void;
229
234
  reconnectNow: () => void;
230
235
  };
@@ -307,4 +312,12 @@ type RealtimeSessionPluginOptions = RealtimeSessionPanelProps;
307
312
  */
308
313
  declare function realtimeSessionPlugin(options: RealtimeSessionPluginOptions): CanvasPlugin;
309
314
 
310
- export { PresenceRemoteLayer, type PresenceRemoteLayerProps, REALTIME_COMMENT_TOOL, type RealtimeClientMessage, type RealtimeCollaborationPluginOptions, type RealtimeCommentData, type RealtimeCommentsAuthor, RealtimeCommentsOverlay, type RealtimeCommentsOverlayProps, type RealtimeCommentsPluginOptions, type RealtimeCommentsViewportBindings, type RealtimeConnectionInfo, RealtimeConnectionState, type RealtimeDocumentSnapshot, type RealtimePresencePayload, type RealtimeServerMessage, RealtimeSessionPanel, type RealtimeSessionPanelProps, type RealtimeSessionPeer, type RealtimeSessionPluginOptions, type RealtimeViewportPresenceBindings, RemotePresenceMarkupStroke, RemotePresencePeer, type UseRealtimeCommentsOptions, type UseRealtimeCommentsResult, type UseRealtimeSessionOptions, type UseRealtimeSessionResult, createRealtimeCommentAvatarDataUrl, createRealtimeCommentDraftItem, createRealtimeCommentItem, defaultPresenceColorForId, getRealtimeCommentData, isRealtimeCommentDraftItem, isRealtimeCommentItem, parseRealtimeClientMessage, parseRealtimeServerMessage, realtimeCollaborationPlugin, realtimeCommentsPlugin, realtimeSessionPlugin, remoteMarkupStrokeFromPlacementPreview, useRealtimeComments, useRealtimeSession, withRealtimeCommentTool };
315
+ type UseRealtimePeerFollowOptions = {
316
+ viewportRef: RefObject<VectorViewportHandle | null>;
317
+ sessionPeers: RealtimeSessionPeer[];
318
+ followedPeerId: string | null | undefined;
319
+ onFollowEnd?: () => void;
320
+ };
321
+ declare function useRealtimePeerFollow(options: UseRealtimePeerFollowOptions): void;
322
+
323
+ export { PresenceRemoteLayer, type PresenceRemoteLayerProps, REALTIME_COMMENT_TOOL, type RealtimeClientMessage, type RealtimeCollaborationPluginOptions, type RealtimeCommentData, type RealtimeCommentsAuthor, RealtimeCommentsOverlay, type RealtimeCommentsOverlayProps, type RealtimeCommentsPluginOptions, type RealtimeCommentsViewportBindings, type RealtimeConnectionInfo, RealtimeConnectionState, type RealtimeDocumentSnapshot, type RealtimePresencePayload, type RealtimeServerMessage, RealtimeSessionPanel, type RealtimeSessionPanelProps, type RealtimeSessionPeer, type RealtimeSessionPluginOptions, type RealtimeViewportPresenceBindings, RemotePresenceCamera, RemotePresenceMarkupStroke, RemotePresencePeer, type UseRealtimeCommentsOptions, type UseRealtimeCommentsResult, type UseRealtimePeerFollowOptions, type UseRealtimeSessionOptions, type UseRealtimeSessionResult, createRealtimeCommentAvatarDataUrl, createRealtimeCommentDraftItem, createRealtimeCommentItem, defaultPresenceColorForId, getRealtimeCommentData, isRealtimeCommentDraftItem, isRealtimeCommentItem, parseRealtimeClientMessage, parseRealtimeServerMessage, realtimeCollaborationPlugin, realtimeCommentsPlugin, realtimeSessionPlugin, remoteMarkupStrokeFromPlacementPreview, useRealtimeComments, useRealtimePeerFollow, useRealtimeSession, withRealtimeCommentTool };
@@ -1,5 +1,5 @@
1
- import { P as PlacementPreview, X as RemotePresenceMarkupStroke, Y as RemotePresencePeer, Z as RealtimeConnectionState, J as VectorViewportHandle, b as VectorToolDefinition, K as VectorViewportProps, C as CanvasPlugin, f as CanvasPluginRenderContext } from './types-CpqlbbCP.js';
2
- export { _ as PresenceOverlayRenderContext } from './types-CpqlbbCP.js';
1
+ import { P as PlacementPreview, X as RemotePresenceMarkupStroke, Y as RemotePresencePeer, Z as RemotePresenceCamera, _ as RealtimeConnectionState, J as VectorViewportHandle, b as VectorToolDefinition, K as VectorViewportProps, C as CanvasPlugin, f as CanvasPluginRenderContext } from './types-ChnTSRSe.js';
2
+ export { $ as PresenceOverlayRenderContext } from './types-ChnTSRSe.js';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { C as Camera2D } from './camera-KwCYYPhm.js';
5
5
  import { V as VectorSceneItem, R as Rect } from './types-CB0TZZuk.js';
@@ -62,6 +62,7 @@ type RealtimePresencePayload = {
62
62
  readonly y: number;
63
63
  } | null;
64
64
  readonly markupStroke?: RemotePresenceMarkupStroke | null;
65
+ readonly camera?: RemotePresenceCamera | null;
65
66
  readonly activeTool?: string;
66
67
  };
67
68
  type RealtimeClientPeer = {
@@ -191,6 +192,7 @@ type UseRealtimeCommentsResult = {
191
192
  tools: VectorToolDefinition[];
192
193
  overlay: ReactNode;
193
194
  viewport: RealtimeCommentsViewportBindings;
195
+ handleViewportItemsChange: (items: VectorSceneItem[], onItemsChange?: (items: VectorSceneItem[]) => void) => void;
194
196
  isComposerOpen: boolean;
195
197
  closeComposer: () => void;
196
198
  };
@@ -215,16 +217,19 @@ type UseRealtimeSessionOptions = {
215
217
  connectTimeoutMs?: number;
216
218
  onError?: (message: string) => void;
217
219
  };
218
- type RealtimeViewportPresenceBindings = Pick<VectorViewportProps, "remotePresence" | "onWorldPointerMove" | "onWorldPointerLeave" | "onPlacementPreviewChange">;
220
+ type RealtimeViewportPresenceBindings = Pick<VectorViewportProps, "remotePresence" | "onWorldPointerMove" | "onWorldPointerLeave" | "onPlacementPreviewChange" | "onCameraChange">;
221
+ type BindViewportPresenceOptions = {
222
+ activeTool?: string;
223
+ viewportRef?: RefObject<VectorViewportHandle | null>;
224
+ };
219
225
  type UseRealtimeSessionResult = {
220
226
  connection: RealtimeConnectionInfo;
221
227
  sessionPeers: RealtimeSessionPeer[];
222
228
  remotePresence: RealtimeSessionPeer[];
223
229
  remoteAdapter: VectorCanvasRemoteAdapter;
224
230
  document: RealtimeDocumentSnapshot | null;
225
- bindViewportPresence: (options?: {
226
- activeTool?: string;
227
- }) => RealtimeViewportPresenceBindings;
231
+ bindViewportPresence: (options?: BindViewportPresenceOptions) => RealtimeViewportPresenceBindings;
232
+ syncViewportPresence: (options?: BindViewportPresenceOptions) => boolean;
228
233
  disconnect: () => void;
229
234
  reconnectNow: () => void;
230
235
  };
@@ -307,4 +312,12 @@ type RealtimeSessionPluginOptions = RealtimeSessionPanelProps;
307
312
  */
308
313
  declare function realtimeSessionPlugin(options: RealtimeSessionPluginOptions): CanvasPlugin;
309
314
 
310
- export { PresenceRemoteLayer, type PresenceRemoteLayerProps, REALTIME_COMMENT_TOOL, type RealtimeClientMessage, type RealtimeCollaborationPluginOptions, type RealtimeCommentData, type RealtimeCommentsAuthor, RealtimeCommentsOverlay, type RealtimeCommentsOverlayProps, type RealtimeCommentsPluginOptions, type RealtimeCommentsViewportBindings, type RealtimeConnectionInfo, RealtimeConnectionState, type RealtimeDocumentSnapshot, type RealtimePresencePayload, type RealtimeServerMessage, RealtimeSessionPanel, type RealtimeSessionPanelProps, type RealtimeSessionPeer, type RealtimeSessionPluginOptions, type RealtimeViewportPresenceBindings, RemotePresenceMarkupStroke, RemotePresencePeer, type UseRealtimeCommentsOptions, type UseRealtimeCommentsResult, type UseRealtimeSessionOptions, type UseRealtimeSessionResult, createRealtimeCommentAvatarDataUrl, createRealtimeCommentDraftItem, createRealtimeCommentItem, defaultPresenceColorForId, getRealtimeCommentData, isRealtimeCommentDraftItem, isRealtimeCommentItem, parseRealtimeClientMessage, parseRealtimeServerMessage, realtimeCollaborationPlugin, realtimeCommentsPlugin, realtimeSessionPlugin, remoteMarkupStrokeFromPlacementPreview, useRealtimeComments, useRealtimeSession, withRealtimeCommentTool };
315
+ type UseRealtimePeerFollowOptions = {
316
+ viewportRef: RefObject<VectorViewportHandle | null>;
317
+ sessionPeers: RealtimeSessionPeer[];
318
+ followedPeerId: string | null | undefined;
319
+ onFollowEnd?: () => void;
320
+ };
321
+ declare function useRealtimePeerFollow(options: UseRealtimePeerFollowOptions): void;
322
+
323
+ export { PresenceRemoteLayer, type PresenceRemoteLayerProps, REALTIME_COMMENT_TOOL, type RealtimeClientMessage, type RealtimeCollaborationPluginOptions, type RealtimeCommentData, type RealtimeCommentsAuthor, RealtimeCommentsOverlay, type RealtimeCommentsOverlayProps, type RealtimeCommentsPluginOptions, type RealtimeCommentsViewportBindings, type RealtimeConnectionInfo, RealtimeConnectionState, type RealtimeDocumentSnapshot, type RealtimePresencePayload, type RealtimeServerMessage, RealtimeSessionPanel, type RealtimeSessionPanelProps, type RealtimeSessionPeer, type RealtimeSessionPluginOptions, type RealtimeViewportPresenceBindings, RemotePresenceCamera, RemotePresenceMarkupStroke, RemotePresencePeer, type UseRealtimeCommentsOptions, type UseRealtimeCommentsResult, type UseRealtimePeerFollowOptions, type UseRealtimeSessionOptions, type UseRealtimeSessionResult, createRealtimeCommentAvatarDataUrl, createRealtimeCommentDraftItem, createRealtimeCommentItem, defaultPresenceColorForId, getRealtimeCommentData, isRealtimeCommentDraftItem, isRealtimeCommentItem, parseRealtimeClientMessage, parseRealtimeServerMessage, realtimeCollaborationPlugin, realtimeCommentsPlugin, realtimeSessionPlugin, remoteMarkupStrokeFromPlacementPreview, useRealtimeComments, useRealtimePeerFollow, useRealtimeSession, withRealtimeCommentTool };
package/dist/realtime.js CHANGED
@@ -100,7 +100,7 @@ function PresenceRemoteLayer({
100
100
  const rootTransform = formatCameraTransform(camera);
101
101
  const overlayStrokePx = 1.25;
102
102
  const LUCIDE_POINTER_VIEWBOX = 24;
103
- const remoteCursorScreenPx = 15;
103
+ const remoteCursorScreenPx = 22;
104
104
  const iconWorldScale = remoteCursorScreenPx / (LUCIDE_POINTER_VIEWBOX * z);
105
105
  return /* @__PURE__ */ jsx(
106
106
  "svg",
@@ -164,9 +164,9 @@ function PresenceRemoteLayer({
164
164
  let cursorNode = null;
165
165
  if (cur) {
166
166
  const displayName = peer.displayName;
167
- const labelOffsetX = 10 / z;
168
- const labelOffsetY = 10 / z;
169
- const labelFont = 10 / z;
167
+ const labelOffsetX = 14 / z;
168
+ const labelOffsetY = 14 / z;
169
+ const labelFont = 12 / z;
170
170
  cursorNode = /* @__PURE__ */ jsxs("g", { children: [
171
171
  /* @__PURE__ */ jsx(
172
172
  "g",
@@ -233,6 +233,19 @@ function parseCursor(value) {
233
233
  if (x == null || y == null) return void 0;
234
234
  return { x, y };
235
235
  }
236
+ function parseCamera(value) {
237
+ if (value === null) return null;
238
+ if (!isRecord(value)) return void 0;
239
+ const x = getNumber(value.x);
240
+ const y = getNumber(value.y);
241
+ const zoom = getNumber(value.zoom);
242
+ const viewportWidth = getNumber(value.viewportWidth);
243
+ const viewportHeight = getNumber(value.viewportHeight);
244
+ if (x == null || y == null || zoom == null || viewportWidth == null || viewportHeight == null) {
245
+ return void 0;
246
+ }
247
+ return { x, y, zoom, viewportWidth, viewportHeight };
248
+ }
236
249
  function parseMarkupStroke(value) {
237
250
  if (value === null) return null;
238
251
  if (!isRecord(value) || !Array.isArray(value.points)) return void 0;
@@ -256,9 +269,12 @@ function parsePresencePayload(value) {
256
269
  const markupStroke = parseMarkupStroke(value.markupStroke);
257
270
  if (markupStroke === void 0 && value.markupStroke !== void 0)
258
271
  return void 0;
272
+ const camera = parseCamera(value.camera);
273
+ if (camera === void 0 && value.camera !== void 0) return void 0;
259
274
  return {
260
275
  cursor,
261
276
  ...markupStroke !== void 0 ? { markupStroke } : {},
277
+ ...camera !== void 0 ? { camera } : {},
262
278
  ...getString(value.activeTool) ? { activeTool: getString(value.activeTool) } : {}
263
279
  };
264
280
  }
@@ -292,6 +308,8 @@ function parseRealtimeSessionPeer(value) {
292
308
  const markupStroke = parseMarkupStroke(value.markupStroke);
293
309
  if (markupStroke === void 0 && value.markupStroke !== void 0)
294
310
  return void 0;
311
+ const camera = parseCamera(value.camera);
312
+ if (camera === void 0 && value.camera !== void 0) return void 0;
295
313
  const isSelf = value.isSelf === true;
296
314
  const connectionState = getString(value.connectionState);
297
315
  return {
@@ -307,6 +325,7 @@ function parseRealtimeSessionPeer(value) {
307
325
  ...getString(value.color) ? { color: getString(value.color) } : {},
308
326
  ...getString(value.image) ? { image: getString(value.image) } : {},
309
327
  ...markupStroke !== void 0 ? { markupStroke } : {},
328
+ ...camera !== void 0 ? { camera } : {},
310
329
  ...getString(value.activeTool) ? { activeTool: getString(value.activeTool) } : {},
311
330
  ...connectionState ? { connectionState } : {}
312
331
  };
@@ -1518,20 +1537,21 @@ function useRealtimeComments({
1518
1537
  }),
1519
1538
  [author.color]
1520
1539
  );
1521
- const onViewportItemsChange = useCallback(
1522
- (nextItems) => {
1540
+ const handleViewportItemsChange = useCallback(
1541
+ (nextItems, overrideOnItemsChange) => {
1542
+ const applyItemsChange = overrideOnItemsChange ?? onItemsChange;
1523
1543
  const currentIds = new Set(items.map((item) => item.id));
1524
1544
  const draftItem = nextItems.find(
1525
1545
  (item) => !currentIds.has(item.id) && isRealtimeCommentDraftItem(item)
1526
1546
  );
1527
1547
  if (!draftItem) {
1528
- onItemsChange(nextItems);
1548
+ applyItemsChange(nextItems);
1529
1549
  return;
1530
1550
  }
1531
1551
  const filteredItems = nextItems.filter((item) => item.id !== draftItem.id);
1532
1552
  const shouldPersistFiltered = filteredItems.length !== items.length || filteredItems.some((item, index) => items[index]?.id !== item.id);
1533
1553
  if (shouldPersistFiltered) {
1534
- onItemsChange(filteredItems);
1554
+ applyItemsChange(filteredItems);
1535
1555
  }
1536
1556
  setCommentComposer({
1537
1557
  worldX: draftItem.bounds.x + draftItem.bounds.width / 2,
@@ -1631,22 +1651,159 @@ function useRealtimeComments({
1631
1651
  const viewport = useMemo(
1632
1652
  () => ({
1633
1653
  customPlacement,
1634
- onItemsChange: onViewportItemsChange,
1654
+ onItemsChange: (nextItems) => handleViewportItemsChange(nextItems),
1635
1655
  onCameraChange
1636
1656
  }),
1637
- [customPlacement, onCameraChange, onViewportItemsChange]
1657
+ [customPlacement, handleViewportItemsChange, onCameraChange]
1638
1658
  );
1639
1659
  return useMemo(
1640
1660
  () => ({
1641
1661
  tools,
1642
1662
  overlay,
1643
1663
  viewport,
1664
+ handleViewportItemsChange,
1644
1665
  isComposerOpen: commentComposer != null,
1645
1666
  closeComposer
1646
1667
  }),
1647
- [closeComposer, commentComposer, overlay, tools, viewport]
1668
+ [
1669
+ closeComposer,
1670
+ commentComposer,
1671
+ handleViewportItemsChange,
1672
+ overlay,
1673
+ tools,
1674
+ viewport
1675
+ ]
1648
1676
  );
1649
1677
  }
1678
+ var viewportFollowSyncSnapshots = /* @__PURE__ */ new WeakMap();
1679
+ function getFollowedPeer(sessionPeers, followedPeerId) {
1680
+ return sessionPeers.find(
1681
+ (peerState) => peerState.peerId === followedPeerId || peerState.id === followedPeerId
1682
+ );
1683
+ }
1684
+ function getCameraKey(peer) {
1685
+ if (!peer.camera) return null;
1686
+ return [
1687
+ peer.peerId,
1688
+ peer.camera.x,
1689
+ peer.camera.y,
1690
+ peer.camera.zoom,
1691
+ peer.camera.viewportWidth,
1692
+ peer.camera.viewportHeight
1693
+ ].join(":");
1694
+ }
1695
+ function getViewportSizeKey(viewport) {
1696
+ if (!viewport) return null;
1697
+ const viewportSize = viewport.getViewportSize();
1698
+ return [viewportSize.width, viewportSize.height].join(":");
1699
+ }
1700
+ function getFollowedCameraPosition(viewport, peer) {
1701
+ if (!peer.camera) return null;
1702
+ const viewportSize = viewport.getViewportSize();
1703
+ return {
1704
+ x: peer.camera.x + (viewportSize.width - peer.camera.viewportWidth) / 2,
1705
+ y: peer.camera.y + (viewportSize.height - peer.camera.viewportHeight) / 2,
1706
+ zoom: peer.camera.zoom
1707
+ };
1708
+ }
1709
+ function applyPeerCamera(viewport, peer) {
1710
+ const camera = viewport.getCamera();
1711
+ const nextCamera = getFollowedCameraPosition(viewport, peer);
1712
+ const viewportSize = viewport.getViewportSize();
1713
+ if (!camera || !nextCamera) return false;
1714
+ if (camera.x === nextCamera.x && camera.y === nextCamera.y && camera.zoom === nextCamera.zoom) {
1715
+ return true;
1716
+ }
1717
+ markViewportFollowSync(viewport, {
1718
+ x: nextCamera.x,
1719
+ y: nextCamera.y,
1720
+ zoom: nextCamera.zoom,
1721
+ viewportWidth: viewportSize.width,
1722
+ viewportHeight: viewportSize.height
1723
+ });
1724
+ camera.x = nextCamera.x;
1725
+ camera.y = nextCamera.y;
1726
+ camera.zoom = nextCamera.zoom;
1727
+ viewport.requestRender();
1728
+ return true;
1729
+ }
1730
+ function markViewportFollowSync(viewport, snapshot) {
1731
+ viewportFollowSyncSnapshots.set(viewport, snapshot);
1732
+ }
1733
+ function consumeViewportFollowSync(viewport, snapshot) {
1734
+ const currentSnapshot = viewportFollowSyncSnapshots.get(viewport);
1735
+ if (!currentSnapshot || currentSnapshot.x !== snapshot.x || currentSnapshot.y !== snapshot.y || currentSnapshot.zoom !== snapshot.zoom || currentSnapshot.viewportWidth !== snapshot.viewportWidth || currentSnapshot.viewportHeight !== snapshot.viewportHeight) {
1736
+ return false;
1737
+ }
1738
+ viewportFollowSyncSnapshots.delete(viewport);
1739
+ return true;
1740
+ }
1741
+ function useRealtimePeerFollow(options) {
1742
+ const { viewportRef, sessionPeers, followedPeerId, onFollowEnd } = options;
1743
+ const endedPeerIdRef = useRef(null);
1744
+ const lastAppliedCameraKeyRef = useRef(null);
1745
+ const [viewportSizeVersion, setViewportSizeVersion] = useState(0);
1746
+ useEffect(() => {
1747
+ if (!followedPeerId) return;
1748
+ let animationFrameId = 0;
1749
+ let lastViewportSizeKey = getViewportSizeKey(viewportRef.current ?? null);
1750
+ const checkViewportSize = () => {
1751
+ const nextViewportSizeKey = getViewportSizeKey(viewportRef.current ?? null);
1752
+ if (nextViewportSizeKey !== lastViewportSizeKey) {
1753
+ lastViewportSizeKey = nextViewportSizeKey;
1754
+ setViewportSizeVersion((value) => value + 1);
1755
+ }
1756
+ animationFrameId = window.requestAnimationFrame(checkViewportSize);
1757
+ };
1758
+ animationFrameId = window.requestAnimationFrame(checkViewportSize);
1759
+ return () => {
1760
+ window.cancelAnimationFrame(animationFrameId);
1761
+ };
1762
+ }, [followedPeerId, viewportRef]);
1763
+ useEffect(() => {
1764
+ if (!followedPeerId) {
1765
+ endedPeerIdRef.current = null;
1766
+ lastAppliedCameraKeyRef.current = null;
1767
+ return;
1768
+ }
1769
+ const followedPeer = getFollowedPeer(sessionPeers, followedPeerId);
1770
+ if (!followedPeer) {
1771
+ lastAppliedCameraKeyRef.current = null;
1772
+ if (endedPeerIdRef.current === followedPeerId) {
1773
+ return;
1774
+ }
1775
+ endedPeerIdRef.current = followedPeerId;
1776
+ onFollowEnd?.();
1777
+ return;
1778
+ }
1779
+ if (!followedPeer.camera) {
1780
+ endedPeerIdRef.current = null;
1781
+ lastAppliedCameraKeyRef.current = null;
1782
+ return;
1783
+ }
1784
+ endedPeerIdRef.current = null;
1785
+ const viewport = viewportRef.current;
1786
+ const nextCameraKey = [
1787
+ getCameraKey(followedPeer),
1788
+ getViewportSizeKey(viewport ?? null)
1789
+ ].join(":");
1790
+ if (nextCameraKey && nextCameraKey === lastAppliedCameraKeyRef.current) {
1791
+ return;
1792
+ }
1793
+ if (!viewport || !applyPeerCamera(viewport, followedPeer)) {
1794
+ return;
1795
+ }
1796
+ lastAppliedCameraKeyRef.current = nextCameraKey;
1797
+ }, [
1798
+ followedPeerId,
1799
+ onFollowEnd,
1800
+ sessionPeers,
1801
+ viewportRef,
1802
+ viewportSizeVersion
1803
+ ]);
1804
+ }
1805
+
1806
+ // src/react/plugins/realtime/use-realtime-session.ts
1650
1807
  function createClientId() {
1651
1808
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1652
1809
  return crypto.randomUUID();
@@ -1689,6 +1846,19 @@ function sameSerializedItems(left, right) {
1689
1846
  function nowMs() {
1690
1847
  return Date.now();
1691
1848
  }
1849
+ function getViewportCameraSnapshot(viewport) {
1850
+ if (!viewport) return null;
1851
+ const camera = viewport.getCamera();
1852
+ if (!camera) return null;
1853
+ const viewportSize = viewport.getViewportSize();
1854
+ return {
1855
+ x: camera.x,
1856
+ y: camera.y,
1857
+ zoom: camera.zoom,
1858
+ viewportWidth: viewportSize.width,
1859
+ viewportHeight: viewportSize.height
1860
+ };
1861
+ }
1692
1862
  function useRealtimeSession(options) {
1693
1863
  const {
1694
1864
  url,
@@ -1715,6 +1885,9 @@ function useRealtimeSession(options) {
1715
1885
  const subscriberRefs = useRef(/* @__PURE__ */ new Set());
1716
1886
  const lastCursorRef = useRef(null);
1717
1887
  const lastMarkupStrokeRef = useRef(null);
1888
+ const lastCameraRef = useRef(
1889
+ null
1890
+ );
1718
1891
  const lastActiveToolRef = useRef(void 0);
1719
1892
  const latestDocumentRef = useRef(null);
1720
1893
  const connectionStateRef = useRef(
@@ -1839,6 +2012,7 @@ function useRealtimeSession(options) {
1839
2012
  presence: {
1840
2013
  cursor: lastCursorRef.current,
1841
2014
  markupStroke: lastMarkupStrokeRef.current ?? null,
2015
+ camera: lastCameraRef.current ?? null,
1842
2016
  ...lastActiveToolRef.current ? { activeTool: lastActiveToolRef.current } : {}
1843
2017
  }
1844
2018
  });
@@ -2164,6 +2338,18 @@ function useRealtimeSession(options) {
2164
2338
  () => sessionPeers.filter((peerState) => !peerState.isSelf),
2165
2339
  [sessionPeers]
2166
2340
  );
2341
+ const syncViewportPresence = useCallback(
2342
+ (bindingOptions) => {
2343
+ const viewport = bindingOptions?.viewportRef?.current;
2344
+ const cameraSnapshot = getViewportCameraSnapshot(viewport);
2345
+ if (!cameraSnapshot) return false;
2346
+ lastCameraRef.current = cameraSnapshot;
2347
+ lastActiveToolRef.current = bindingOptions?.activeTool;
2348
+ sendPresenceUpdate();
2349
+ return true;
2350
+ },
2351
+ [sendPresenceUpdate]
2352
+ );
2167
2353
  const bindViewportPresence = useCallback(
2168
2354
  (bindingOptions) => ({
2169
2355
  remotePresence,
@@ -2181,6 +2367,18 @@ function useRealtimeSession(options) {
2181
2367
  lastMarkupStrokeRef.current = remoteMarkupStrokeFromPlacementPreview(preview);
2182
2368
  lastActiveToolRef.current = bindingOptions?.activeTool;
2183
2369
  sendPresenceUpdate();
2370
+ },
2371
+ onCameraChange() {
2372
+ const viewport = bindingOptions?.viewportRef?.current;
2373
+ const cameraSnapshot = getViewportCameraSnapshot(viewport);
2374
+ if (!cameraSnapshot) return;
2375
+ if (viewport && consumeViewportFollowSync(viewport, cameraSnapshot)) {
2376
+ lastCameraRef.current = cameraSnapshot;
2377
+ return;
2378
+ }
2379
+ lastCameraRef.current = cameraSnapshot;
2380
+ lastActiveToolRef.current = bindingOptions?.activeTool;
2381
+ sendPresenceUpdate();
2184
2382
  }
2185
2383
  }),
2186
2384
  [remotePresence, sendPresenceUpdate]
@@ -2192,6 +2390,7 @@ function useRealtimeSession(options) {
2192
2390
  remoteAdapter,
2193
2391
  document: document2,
2194
2392
  bindViewportPresence,
2393
+ syncViewportPresence,
2195
2394
  disconnect,
2196
2395
  reconnectNow
2197
2396
  };
@@ -2236,9 +2435,37 @@ function RealtimeCollaborationPluginComponent({
2236
2435
  ...commentOptions ?? {}
2237
2436
  });
2238
2437
  const presenceBindings = useMemo(
2239
- () => session.bindViewportPresence({ activeTool: viewport.toolId }),
2240
- [session.bindViewportPresence, viewport.toolId]
2438
+ () => session.bindViewportPresence({
2439
+ activeTool: viewport.toolId,
2440
+ viewportRef
2441
+ }),
2442
+ [session.bindViewportPresence, viewport.toolId, viewportRef]
2241
2443
  );
2444
+ const onViewportCameraChange = useCallback(() => {
2445
+ presenceBindings.onCameraChange?.();
2446
+ comments.viewport.onCameraChange?.();
2447
+ }, [comments.viewport, presenceBindings]);
2448
+ useEffect(() => {
2449
+ if (!session.connection.connected) return;
2450
+ let animationFrameId = 0;
2451
+ const syncViewportPresence = () => {
2452
+ const didSync = session.syncViewportPresence({
2453
+ activeTool: viewport.toolId,
2454
+ viewportRef
2455
+ });
2456
+ if (didSync) return;
2457
+ animationFrameId = window.requestAnimationFrame(syncViewportPresence);
2458
+ };
2459
+ syncViewportPresence();
2460
+ return () => {
2461
+ window.cancelAnimationFrame(animationFrameId);
2462
+ };
2463
+ }, [
2464
+ session.connection.connected,
2465
+ session.syncViewportPresence,
2466
+ viewport.toolId,
2467
+ viewportRef
2468
+ ]);
2242
2469
  useEffect(() => {
2243
2470
  if (!onItemsChange || !session.document) return;
2244
2471
  if (session.document.updatedByClientId === session.connection.clientId) return;
@@ -2255,11 +2482,14 @@ function RealtimeCollaborationPluginComponent({
2255
2482
  onWorldPointerMove: presenceBindings.onWorldPointerMove,
2256
2483
  onWorldPointerLeave: presenceBindings.onWorldPointerLeave,
2257
2484
  onPlacementPreviewChange: presenceBindings.onPlacementPreviewChange,
2258
- onCameraChange: commentOptions ? comments.viewport.onCameraChange : void 0
2485
+ onCameraChange: onViewportCameraChange
2259
2486
  },
2260
2487
  wrapOnItemsChange: (nextItems, ctx) => {
2261
2488
  if (commentOptions) {
2262
- comments.viewport.onItemsChange?.(nextItems);
2489
+ comments.handleViewportItemsChange(nextItems, (processedItems) => {
2490
+ ctx.next(processedItems);
2491
+ session.remoteAdapter.send?.([...processedItems]);
2492
+ });
2263
2493
  return;
2264
2494
  }
2265
2495
  ctx.next(nextItems);
@@ -2270,6 +2500,8 @@ function RealtimeCollaborationPluginComponent({
2270
2500
  commentOptions,
2271
2501
  comments.tools,
2272
2502
  comments.viewport,
2503
+ comments.handleViewportItemsChange,
2504
+ onViewportCameraChange,
2273
2505
  presenceBindings,
2274
2506
  session.remoteAdapter
2275
2507
  ]
@@ -2312,6 +2544,6 @@ function realtimeSessionPlugin(options) {
2312
2544
  };
2313
2545
  }
2314
2546
 
2315
- export { PresenceRemoteLayer, REALTIME_COMMENT_TOOL, RealtimeCommentsOverlay, RealtimeSessionPanel, createRealtimeCommentAvatarDataUrl, createRealtimeCommentDraftItem, createRealtimeCommentItem, defaultPresenceColorForId, getRealtimeCommentData, isRealtimeCommentDraftItem, isRealtimeCommentItem, parseRealtimeClientMessage, parseRealtimeServerMessage, realtimeCollaborationPlugin, realtimeCommentsPlugin, realtimeSessionPlugin, remoteMarkupStrokeFromPlacementPreview, useRealtimeComments, useRealtimeSession, withRealtimeCommentTool };
2547
+ export { PresenceRemoteLayer, REALTIME_COMMENT_TOOL, RealtimeCommentsOverlay, RealtimeSessionPanel, createRealtimeCommentAvatarDataUrl, createRealtimeCommentDraftItem, createRealtimeCommentItem, defaultPresenceColorForId, getRealtimeCommentData, isRealtimeCommentDraftItem, isRealtimeCommentItem, parseRealtimeClientMessage, parseRealtimeServerMessage, realtimeCollaborationPlugin, realtimeCommentsPlugin, realtimeSessionPlugin, remoteMarkupStrokeFromPlacementPreview, useRealtimeComments, useRealtimePeerFollow, useRealtimeSession, withRealtimeCommentTool };
2316
2548
  //# sourceMappingURL=realtime.js.map
2317
2549
  //# sourceMappingURL=realtime.js.map