canvu-react 0.3.7 → 0.3.9

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/realtime.cjs CHANGED
@@ -235,6 +235,19 @@ function parseCursor(value) {
235
235
  if (x == null || y == null) return void 0;
236
236
  return { x, y };
237
237
  }
238
+ function parseCamera(value) {
239
+ if (value === null) return null;
240
+ if (!isRecord(value)) return void 0;
241
+ const x = getNumber(value.x);
242
+ const y = getNumber(value.y);
243
+ const zoom = getNumber(value.zoom);
244
+ const viewportWidth = getNumber(value.viewportWidth);
245
+ const viewportHeight = getNumber(value.viewportHeight);
246
+ if (x == null || y == null || zoom == null || viewportWidth == null || viewportHeight == null) {
247
+ return void 0;
248
+ }
249
+ return { x, y, zoom, viewportWidth, viewportHeight };
250
+ }
238
251
  function parseMarkupStroke(value) {
239
252
  if (value === null) return null;
240
253
  if (!isRecord(value) || !Array.isArray(value.points)) return void 0;
@@ -258,9 +271,12 @@ function parsePresencePayload(value) {
258
271
  const markupStroke = parseMarkupStroke(value.markupStroke);
259
272
  if (markupStroke === void 0 && value.markupStroke !== void 0)
260
273
  return void 0;
274
+ const camera = parseCamera(value.camera);
275
+ if (camera === void 0 && value.camera !== void 0) return void 0;
261
276
  return {
262
277
  cursor,
263
278
  ...markupStroke !== void 0 ? { markupStroke } : {},
279
+ ...camera !== void 0 ? { camera } : {},
264
280
  ...getString(value.activeTool) ? { activeTool: getString(value.activeTool) } : {}
265
281
  };
266
282
  }
@@ -294,6 +310,8 @@ function parseRealtimeSessionPeer(value) {
294
310
  const markupStroke = parseMarkupStroke(value.markupStroke);
295
311
  if (markupStroke === void 0 && value.markupStroke !== void 0)
296
312
  return void 0;
313
+ const camera = parseCamera(value.camera);
314
+ if (camera === void 0 && value.camera !== void 0) return void 0;
297
315
  const isSelf = value.isSelf === true;
298
316
  const connectionState = getString(value.connectionState);
299
317
  return {
@@ -309,6 +327,7 @@ function parseRealtimeSessionPeer(value) {
309
327
  ...getString(value.color) ? { color: getString(value.color) } : {},
310
328
  ...getString(value.image) ? { image: getString(value.image) } : {},
311
329
  ...markupStroke !== void 0 ? { markupStroke } : {},
330
+ ...camera !== void 0 ? { camera } : {},
312
331
  ...getString(value.activeTool) ? { activeTool: getString(value.activeTool) } : {},
313
332
  ...connectionState ? { connectionState } : {}
314
333
  };
@@ -1520,20 +1539,21 @@ function useRealtimeComments({
1520
1539
  }),
1521
1540
  [author.color]
1522
1541
  );
1523
- const onViewportItemsChange = react.useCallback(
1524
- (nextItems) => {
1542
+ const handleViewportItemsChange = react.useCallback(
1543
+ (nextItems, overrideOnItemsChange) => {
1544
+ const applyItemsChange = overrideOnItemsChange ?? onItemsChange;
1525
1545
  const currentIds = new Set(items.map((item) => item.id));
1526
1546
  const draftItem = nextItems.find(
1527
1547
  (item) => !currentIds.has(item.id) && isRealtimeCommentDraftItem(item)
1528
1548
  );
1529
1549
  if (!draftItem) {
1530
- onItemsChange(nextItems);
1550
+ applyItemsChange(nextItems);
1531
1551
  return;
1532
1552
  }
1533
1553
  const filteredItems = nextItems.filter((item) => item.id !== draftItem.id);
1534
1554
  const shouldPersistFiltered = filteredItems.length !== items.length || filteredItems.some((item, index) => items[index]?.id !== item.id);
1535
1555
  if (shouldPersistFiltered) {
1536
- onItemsChange(filteredItems);
1556
+ applyItemsChange(filteredItems);
1537
1557
  }
1538
1558
  setCommentComposer({
1539
1559
  worldX: draftItem.bounds.x + draftItem.bounds.width / 2,
@@ -1633,22 +1653,159 @@ function useRealtimeComments({
1633
1653
  const viewport = react.useMemo(
1634
1654
  () => ({
1635
1655
  customPlacement,
1636
- onItemsChange: onViewportItemsChange,
1656
+ onItemsChange: (nextItems) => handleViewportItemsChange(nextItems),
1637
1657
  onCameraChange
1638
1658
  }),
1639
- [customPlacement, onCameraChange, onViewportItemsChange]
1659
+ [customPlacement, handleViewportItemsChange, onCameraChange]
1640
1660
  );
1641
1661
  return react.useMemo(
1642
1662
  () => ({
1643
1663
  tools,
1644
1664
  overlay,
1645
1665
  viewport,
1666
+ handleViewportItemsChange,
1646
1667
  isComposerOpen: commentComposer != null,
1647
1668
  closeComposer
1648
1669
  }),
1649
- [closeComposer, commentComposer, overlay, tools, viewport]
1670
+ [
1671
+ closeComposer,
1672
+ commentComposer,
1673
+ handleViewportItemsChange,
1674
+ overlay,
1675
+ tools,
1676
+ viewport
1677
+ ]
1650
1678
  );
1651
1679
  }
1680
+ var viewportFollowSyncSnapshots = /* @__PURE__ */ new WeakMap();
1681
+ function getFollowedPeer(sessionPeers, followedPeerId) {
1682
+ return sessionPeers.find(
1683
+ (peerState) => peerState.peerId === followedPeerId || peerState.id === followedPeerId
1684
+ );
1685
+ }
1686
+ function getCameraKey(peer) {
1687
+ if (!peer.camera) return null;
1688
+ return [
1689
+ peer.peerId,
1690
+ peer.camera.x,
1691
+ peer.camera.y,
1692
+ peer.camera.zoom,
1693
+ peer.camera.viewportWidth,
1694
+ peer.camera.viewportHeight
1695
+ ].join(":");
1696
+ }
1697
+ function getViewportSizeKey(viewport) {
1698
+ if (!viewport) return null;
1699
+ const viewportSize = viewport.getViewportSize();
1700
+ return [viewportSize.width, viewportSize.height].join(":");
1701
+ }
1702
+ function getFollowedCameraPosition(viewport, peer) {
1703
+ if (!peer.camera) return null;
1704
+ const viewportSize = viewport.getViewportSize();
1705
+ return {
1706
+ x: peer.camera.x + (viewportSize.width - peer.camera.viewportWidth) / 2,
1707
+ y: peer.camera.y + (viewportSize.height - peer.camera.viewportHeight) / 2,
1708
+ zoom: peer.camera.zoom
1709
+ };
1710
+ }
1711
+ function applyPeerCamera(viewport, peer) {
1712
+ const camera = viewport.getCamera();
1713
+ const nextCamera = getFollowedCameraPosition(viewport, peer);
1714
+ const viewportSize = viewport.getViewportSize();
1715
+ if (!camera || !nextCamera) return false;
1716
+ if (camera.x === nextCamera.x && camera.y === nextCamera.y && camera.zoom === nextCamera.zoom) {
1717
+ return true;
1718
+ }
1719
+ markViewportFollowSync(viewport, {
1720
+ x: nextCamera.x,
1721
+ y: nextCamera.y,
1722
+ zoom: nextCamera.zoom,
1723
+ viewportWidth: viewportSize.width,
1724
+ viewportHeight: viewportSize.height
1725
+ });
1726
+ camera.x = nextCamera.x;
1727
+ camera.y = nextCamera.y;
1728
+ camera.zoom = nextCamera.zoom;
1729
+ viewport.requestRender();
1730
+ return true;
1731
+ }
1732
+ function markViewportFollowSync(viewport, snapshot) {
1733
+ viewportFollowSyncSnapshots.set(viewport, snapshot);
1734
+ }
1735
+ function consumeViewportFollowSync(viewport, snapshot) {
1736
+ const currentSnapshot = viewportFollowSyncSnapshots.get(viewport);
1737
+ if (!currentSnapshot || currentSnapshot.x !== snapshot.x || currentSnapshot.y !== snapshot.y || currentSnapshot.zoom !== snapshot.zoom || currentSnapshot.viewportWidth !== snapshot.viewportWidth || currentSnapshot.viewportHeight !== snapshot.viewportHeight) {
1738
+ return false;
1739
+ }
1740
+ viewportFollowSyncSnapshots.delete(viewport);
1741
+ return true;
1742
+ }
1743
+ function useRealtimePeerFollow(options) {
1744
+ const { viewportRef, sessionPeers, followedPeerId, onFollowEnd } = options;
1745
+ const endedPeerIdRef = react.useRef(null);
1746
+ const lastAppliedCameraKeyRef = react.useRef(null);
1747
+ const [viewportSizeVersion, setViewportSizeVersion] = react.useState(0);
1748
+ react.useEffect(() => {
1749
+ if (!followedPeerId) return;
1750
+ let animationFrameId = 0;
1751
+ let lastViewportSizeKey = getViewportSizeKey(viewportRef.current ?? null);
1752
+ const checkViewportSize = () => {
1753
+ const nextViewportSizeKey = getViewportSizeKey(viewportRef.current ?? null);
1754
+ if (nextViewportSizeKey !== lastViewportSizeKey) {
1755
+ lastViewportSizeKey = nextViewportSizeKey;
1756
+ setViewportSizeVersion((value) => value + 1);
1757
+ }
1758
+ animationFrameId = window.requestAnimationFrame(checkViewportSize);
1759
+ };
1760
+ animationFrameId = window.requestAnimationFrame(checkViewportSize);
1761
+ return () => {
1762
+ window.cancelAnimationFrame(animationFrameId);
1763
+ };
1764
+ }, [followedPeerId, viewportRef]);
1765
+ react.useEffect(() => {
1766
+ if (!followedPeerId) {
1767
+ endedPeerIdRef.current = null;
1768
+ lastAppliedCameraKeyRef.current = null;
1769
+ return;
1770
+ }
1771
+ const followedPeer = getFollowedPeer(sessionPeers, followedPeerId);
1772
+ if (!followedPeer) {
1773
+ lastAppliedCameraKeyRef.current = null;
1774
+ if (endedPeerIdRef.current === followedPeerId) {
1775
+ return;
1776
+ }
1777
+ endedPeerIdRef.current = followedPeerId;
1778
+ onFollowEnd?.();
1779
+ return;
1780
+ }
1781
+ if (!followedPeer.camera) {
1782
+ endedPeerIdRef.current = null;
1783
+ lastAppliedCameraKeyRef.current = null;
1784
+ return;
1785
+ }
1786
+ endedPeerIdRef.current = null;
1787
+ const viewport = viewportRef.current;
1788
+ const nextCameraKey = [
1789
+ getCameraKey(followedPeer),
1790
+ getViewportSizeKey(viewport ?? null)
1791
+ ].join(":");
1792
+ if (nextCameraKey && nextCameraKey === lastAppliedCameraKeyRef.current) {
1793
+ return;
1794
+ }
1795
+ if (!viewport || !applyPeerCamera(viewport, followedPeer)) {
1796
+ return;
1797
+ }
1798
+ lastAppliedCameraKeyRef.current = nextCameraKey;
1799
+ }, [
1800
+ followedPeerId,
1801
+ onFollowEnd,
1802
+ sessionPeers,
1803
+ viewportRef,
1804
+ viewportSizeVersion
1805
+ ]);
1806
+ }
1807
+
1808
+ // src/react/plugins/realtime/use-realtime-session.ts
1652
1809
  function createClientId() {
1653
1810
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1654
1811
  return crypto.randomUUID();
@@ -1691,6 +1848,19 @@ function sameSerializedItems(left, right) {
1691
1848
  function nowMs() {
1692
1849
  return Date.now();
1693
1850
  }
1851
+ function getViewportCameraSnapshot(viewport) {
1852
+ if (!viewport) return null;
1853
+ const camera = viewport.getCamera();
1854
+ if (!camera) return null;
1855
+ const viewportSize = viewport.getViewportSize();
1856
+ return {
1857
+ x: camera.x,
1858
+ y: camera.y,
1859
+ zoom: camera.zoom,
1860
+ viewportWidth: viewportSize.width,
1861
+ viewportHeight: viewportSize.height
1862
+ };
1863
+ }
1694
1864
  function useRealtimeSession(options) {
1695
1865
  const {
1696
1866
  url,
@@ -1717,6 +1887,9 @@ function useRealtimeSession(options) {
1717
1887
  const subscriberRefs = react.useRef(/* @__PURE__ */ new Set());
1718
1888
  const lastCursorRef = react.useRef(null);
1719
1889
  const lastMarkupStrokeRef = react.useRef(null);
1890
+ const lastCameraRef = react.useRef(
1891
+ null
1892
+ );
1720
1893
  const lastActiveToolRef = react.useRef(void 0);
1721
1894
  const latestDocumentRef = react.useRef(null);
1722
1895
  const connectionStateRef = react.useRef(
@@ -1841,6 +2014,7 @@ function useRealtimeSession(options) {
1841
2014
  presence: {
1842
2015
  cursor: lastCursorRef.current,
1843
2016
  markupStroke: lastMarkupStrokeRef.current ?? null,
2017
+ camera: lastCameraRef.current ?? null,
1844
2018
  ...lastActiveToolRef.current ? { activeTool: lastActiveToolRef.current } : {}
1845
2019
  }
1846
2020
  });
@@ -2166,6 +2340,18 @@ function useRealtimeSession(options) {
2166
2340
  () => sessionPeers.filter((peerState) => !peerState.isSelf),
2167
2341
  [sessionPeers]
2168
2342
  );
2343
+ const syncViewportPresence = react.useCallback(
2344
+ (bindingOptions) => {
2345
+ const viewport = bindingOptions?.viewportRef?.current;
2346
+ const cameraSnapshot = getViewportCameraSnapshot(viewport);
2347
+ if (!cameraSnapshot) return false;
2348
+ lastCameraRef.current = cameraSnapshot;
2349
+ lastActiveToolRef.current = bindingOptions?.activeTool;
2350
+ sendPresenceUpdate();
2351
+ return true;
2352
+ },
2353
+ [sendPresenceUpdate]
2354
+ );
2169
2355
  const bindViewportPresence = react.useCallback(
2170
2356
  (bindingOptions) => ({
2171
2357
  remotePresence,
@@ -2183,6 +2369,18 @@ function useRealtimeSession(options) {
2183
2369
  lastMarkupStrokeRef.current = remoteMarkupStrokeFromPlacementPreview(preview);
2184
2370
  lastActiveToolRef.current = bindingOptions?.activeTool;
2185
2371
  sendPresenceUpdate();
2372
+ },
2373
+ onCameraChange() {
2374
+ const viewport = bindingOptions?.viewportRef?.current;
2375
+ const cameraSnapshot = getViewportCameraSnapshot(viewport);
2376
+ if (!cameraSnapshot) return;
2377
+ if (viewport && consumeViewportFollowSync(viewport, cameraSnapshot)) {
2378
+ lastCameraRef.current = cameraSnapshot;
2379
+ return;
2380
+ }
2381
+ lastCameraRef.current = cameraSnapshot;
2382
+ lastActiveToolRef.current = bindingOptions?.activeTool;
2383
+ sendPresenceUpdate();
2186
2384
  }
2187
2385
  }),
2188
2386
  [remotePresence, sendPresenceUpdate]
@@ -2194,6 +2392,7 @@ function useRealtimeSession(options) {
2194
2392
  remoteAdapter,
2195
2393
  document: document2,
2196
2394
  bindViewportPresence,
2395
+ syncViewportPresence,
2197
2396
  disconnect,
2198
2397
  reconnectNow
2199
2398
  };
@@ -2238,9 +2437,37 @@ function RealtimeCollaborationPluginComponent({
2238
2437
  ...commentOptions ?? {}
2239
2438
  });
2240
2439
  const presenceBindings = react.useMemo(
2241
- () => session.bindViewportPresence({ activeTool: viewport.toolId }),
2242
- [session.bindViewportPresence, viewport.toolId]
2440
+ () => session.bindViewportPresence({
2441
+ activeTool: viewport.toolId,
2442
+ viewportRef
2443
+ }),
2444
+ [session.bindViewportPresence, viewport.toolId, viewportRef]
2243
2445
  );
2446
+ const onViewportCameraChange = react.useCallback(() => {
2447
+ presenceBindings.onCameraChange?.();
2448
+ comments.viewport.onCameraChange?.();
2449
+ }, [comments.viewport, presenceBindings]);
2450
+ react.useEffect(() => {
2451
+ if (!session.connection.connected) return;
2452
+ let animationFrameId = 0;
2453
+ const syncViewportPresence = () => {
2454
+ const didSync = session.syncViewportPresence({
2455
+ activeTool: viewport.toolId,
2456
+ viewportRef
2457
+ });
2458
+ if (didSync) return;
2459
+ animationFrameId = window.requestAnimationFrame(syncViewportPresence);
2460
+ };
2461
+ syncViewportPresence();
2462
+ return () => {
2463
+ window.cancelAnimationFrame(animationFrameId);
2464
+ };
2465
+ }, [
2466
+ session.connection.connected,
2467
+ session.syncViewportPresence,
2468
+ viewport.toolId,
2469
+ viewportRef
2470
+ ]);
2244
2471
  react.useEffect(() => {
2245
2472
  if (!onItemsChange || !session.document) return;
2246
2473
  if (session.document.updatedByClientId === session.connection.clientId) return;
@@ -2257,11 +2484,14 @@ function RealtimeCollaborationPluginComponent({
2257
2484
  onWorldPointerMove: presenceBindings.onWorldPointerMove,
2258
2485
  onWorldPointerLeave: presenceBindings.onWorldPointerLeave,
2259
2486
  onPlacementPreviewChange: presenceBindings.onPlacementPreviewChange,
2260
- onCameraChange: commentOptions ? comments.viewport.onCameraChange : void 0
2487
+ onCameraChange: onViewportCameraChange
2261
2488
  },
2262
2489
  wrapOnItemsChange: (nextItems, ctx) => {
2263
2490
  if (commentOptions) {
2264
- comments.viewport.onItemsChange?.(nextItems);
2491
+ comments.handleViewportItemsChange(nextItems, (processedItems) => {
2492
+ ctx.next(processedItems);
2493
+ session.remoteAdapter.send?.([...processedItems]);
2494
+ });
2265
2495
  return;
2266
2496
  }
2267
2497
  ctx.next(nextItems);
@@ -2272,6 +2502,8 @@ function RealtimeCollaborationPluginComponent({
2272
2502
  commentOptions,
2273
2503
  comments.tools,
2274
2504
  comments.viewport,
2505
+ comments.handleViewportItemsChange,
2506
+ onViewportCameraChange,
2275
2507
  presenceBindings,
2276
2508
  session.remoteAdapter
2277
2509
  ]
@@ -2332,6 +2564,7 @@ exports.realtimeCommentsPlugin = realtimeCommentsPlugin;
2332
2564
  exports.realtimeSessionPlugin = realtimeSessionPlugin;
2333
2565
  exports.remoteMarkupStrokeFromPlacementPreview = remoteMarkupStrokeFromPlacementPreview;
2334
2566
  exports.useRealtimeComments = useRealtimeComments;
2567
+ exports.useRealtimePeerFollow = useRealtimePeerFollow;
2335
2568
  exports.useRealtimeSession = useRealtimeSession;
2336
2569
  exports.withRealtimeCommentTool = withRealtimeCommentTool;
2337
2570
  //# sourceMappingURL=realtime.cjs.map