canvu-react 0.4.45 → 0.4.47

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
@@ -1869,6 +1869,63 @@ function smoothFreehandPointsToPathD(points) {
1869
1869
  return d;
1870
1870
  }
1871
1871
 
1872
+ // src/native/native-freehand-payload-cache.ts
1873
+ var MAX_NATIVE_FREEHAND_PAYLOAD_CACHE_SIZE = 800;
1874
+ var pathSignatureCache = /* @__PURE__ */ new WeakMap();
1875
+ var payloadCache = /* @__PURE__ */ new Map();
1876
+ function hashNumber(hash, value) {
1877
+ const normalizedValue = Math.round(value * 100);
1878
+ return Math.imul(hash ^ normalizedValue, 16777619) >>> 0;
1879
+ }
1880
+ function getPathPointsSignature(pathPointsLocal) {
1881
+ const cachedSignature = pathSignatureCache.get(pathPointsLocal);
1882
+ if (cachedSignature) return cachedSignature;
1883
+ const hash = pathPointsLocal.reduce((currentHash, point) => {
1884
+ const withX = hashNumber(currentHash, point.x);
1885
+ const withY = hashNumber(withX, point.y);
1886
+ return hashNumber(withY, point.pressure ?? -1);
1887
+ }, 2166136261);
1888
+ const signature = `${pathPointsLocal.length}:${hash}`;
1889
+ pathSignatureCache.set(pathPointsLocal, signature);
1890
+ return signature;
1891
+ }
1892
+ function getStyleSignature(style) {
1893
+ return [
1894
+ style.stroke,
1895
+ style.strokeWidth,
1896
+ style.strokeOpacity ?? "",
1897
+ style.strokeDash ?? ""
1898
+ ].join(":");
1899
+ }
1900
+ function getPayloadCacheKey(input) {
1901
+ return [
1902
+ input.itemId,
1903
+ input.toolKind,
1904
+ getStyleSignature(input.style),
1905
+ getPathPointsSignature(input.pathPointsLocal)
1906
+ ].join("|");
1907
+ }
1908
+ function storePayload(key, payload) {
1909
+ payloadCache.set(key, payload);
1910
+ if (payloadCache.size > MAX_NATIVE_FREEHAND_PAYLOAD_CACHE_SIZE) {
1911
+ const oldestKey = payloadCache.keys().next().value;
1912
+ if (oldestKey !== void 0) {
1913
+ payloadCache.delete(oldestKey);
1914
+ }
1915
+ }
1916
+ return payload;
1917
+ }
1918
+ function getNativeFreehandSvgPayload(input) {
1919
+ const key = getPayloadCacheKey(input);
1920
+ if (payloadCache.has(key)) {
1921
+ return payloadCache.get(key) ?? null;
1922
+ }
1923
+ return storePayload(
1924
+ key,
1925
+ computeFreehandSvgPayload(input.pathPointsLocal, input.style, input.toolKind)
1926
+ );
1927
+ }
1928
+
1872
1929
  // src/native/native-link-card.ts
1873
1930
  function linkHostname(href) {
1874
1931
  try {
@@ -2871,7 +2928,12 @@ function NativeShapeRenderer({ item }) {
2871
2928
  )) });
2872
2929
  }
2873
2930
  if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item.pathPointsLocal && item.pathPointsLocal.length > 0) {
2874
- const payload = computeFreehandSvgPayload(item.pathPointsLocal, style, k);
2931
+ const payload = getNativeFreehandSvgPayload({
2932
+ itemId: item.id,
2933
+ pathPointsLocal: item.pathPointsLocal,
2934
+ style,
2935
+ toolKind: k
2936
+ });
2875
2937
  if (!payload) return null;
2876
2938
  const color = rgba(style.stroke, style.strokeOpacity);
2877
2939
  if (payload.kind === "circle") {
@@ -2970,6 +3032,35 @@ function resolveNativeStrokePreviewStyle(tool, previewStrokeStyle) {
2970
3032
  };
2971
3033
  }
2972
3034
 
3035
+ // src/native/native-stroke-preview.ts
3036
+ var NATIVE_STROKE_PREVIEW_MAX_POINTS = 160;
3037
+ function sampleNativeStrokePreviewPoints(points, maxPoints = NATIVE_STROKE_PREVIEW_MAX_POINTS) {
3038
+ if (points.length <= maxPoints) return points.map((point) => ({ ...point }));
3039
+ if (maxPoints <= 1) {
3040
+ const last2 = points[points.length - 1];
3041
+ return last2 ? [{ ...last2 }] : [];
3042
+ }
3043
+ const lastIndex = points.length - 1;
3044
+ const last = points[lastIndex];
3045
+ if (!last) return [];
3046
+ const step = lastIndex / (maxPoints - 1);
3047
+ return Array.from({ length: maxPoints }, (_, index) => {
3048
+ const point = points[Math.round(index * step)] ?? last;
3049
+ return { x: point.x, y: point.y };
3050
+ });
3051
+ }
3052
+ function buildNativeStrokePreviewPath(points) {
3053
+ if (points.length < 2) return null;
3054
+ const d = smoothFreehandPointsToPathD(points);
3055
+ return d || null;
3056
+ }
3057
+ function nativeStrokePreviewDashArray(style, tool) {
3058
+ if (style.strokeDash !== "dashed" || tool !== "draw") return void 0;
3059
+ const dash = Math.max(style.strokeWidth * 1.8, 4);
3060
+ const gap = Math.max(style.strokeWidth * 1.4, 3);
3061
+ return `${dash} ${gap}`;
3062
+ }
3063
+
2973
3064
  // src/native/native-vector-interactions.ts
2974
3065
  var NATIVE_SELECTION_HANDLE_HIT_RADIUS_PX = 24;
2975
3066
  function nativeItemPlacementBounds(item) {
@@ -3341,58 +3432,42 @@ function NativeInteractionOverlay({
3341
3432
  p.tool,
3342
3433
  p.style ?? previewStrokeStyle
3343
3434
  );
3344
- const payload = computeFreehandSvgPayload(
3345
- p.points,
3346
- style,
3347
- isLaser ? "draw" : p.tool,
3348
- p.points.length === 2
3435
+ const strokeColor = colorWithOpacity(
3436
+ style.stroke,
3437
+ isLaser ? 0.85 : style.strokeOpacity
3349
3438
  );
3350
- if (!payload) return null;
3351
- if (payload.kind === "circle") {
3439
+ if (p.points.length === 1) {
3440
+ const point = p.points[0];
3441
+ if (!point) return null;
3352
3442
  return /* @__PURE__ */ jsxRuntime.jsx(
3353
3443
  reactNativeSkia.Circle,
3354
3444
  {
3355
- cx: payload.cx,
3356
- cy: payload.cy,
3357
- r: payload.r,
3358
- color: colorWithOpacity(payload.fill, payload.fillOpacity),
3445
+ cx: point.x,
3446
+ cy: point.y,
3447
+ r: Math.max(0.5, style.strokeWidth / 2),
3448
+ color: strokeColor,
3359
3449
  style: "fill",
3360
3450
  antiAlias: true
3361
3451
  }
3362
3452
  );
3363
3453
  }
3364
- if (payload.kind === "fillPath") {
3365
- return /* @__PURE__ */ jsxRuntime.jsx(
3366
- reactNativeSkia.Path,
3367
- {
3368
- path: payload.d,
3369
- color: colorWithOpacity(payload.fill, payload.fillOpacity),
3370
- style: "fill",
3371
- fillType: "winding",
3372
- antiAlias: true
3373
- }
3374
- );
3375
- }
3376
- if (payload.kind === "strokePath") {
3377
- const intervals = dashIntervalsFromStrokeDasharray3(payload.strokeDasharray);
3378
- return /* @__PURE__ */ jsxRuntime.jsx(
3379
- reactNativeSkia.Path,
3380
- {
3381
- path: payload.d,
3382
- color: colorWithOpacity(
3383
- payload.stroke,
3384
- isLaser ? 0.85 : payload.strokeOpacity
3385
- ),
3386
- style: "stroke",
3387
- strokeWidth: payload.strokeWidth,
3388
- strokeCap: "round",
3389
- strokeJoin: "round",
3390
- antiAlias: true,
3391
- children: intervals ? /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.DashPathEffect, { intervals }) : null
3392
- }
3393
- );
3394
- }
3395
- return null;
3454
+ const path = buildNativeStrokePreviewPath(p.points);
3455
+ if (!path) return null;
3456
+ const strokeDasharray = nativeStrokePreviewDashArray(style, p.tool);
3457
+ const intervals = dashIntervalsFromStrokeDasharray3(strokeDasharray);
3458
+ return /* @__PURE__ */ jsxRuntime.jsx(
3459
+ reactNativeSkia.Path,
3460
+ {
3461
+ path,
3462
+ color: strokeColor,
3463
+ style: "stroke",
3464
+ strokeWidth: style.strokeWidth,
3465
+ strokeCap: "round",
3466
+ strokeJoin: "round",
3467
+ antiAlias: true,
3468
+ children: intervals ? /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.DashPathEffect, { intervals }) : null
3469
+ }
3470
+ );
3396
3471
  }
3397
3472
  return null;
3398
3473
  }, [placementPreview, previewStrokeStyle, overlayStrokeWorld, marqueeDashWorld]);
@@ -5175,6 +5250,36 @@ function hitTestNativeRemotePresence(peers, camera, point) {
5175
5250
  }
5176
5251
  return null;
5177
5252
  }
5253
+
5254
+ // src/native/native-transient-items.ts
5255
+ function moveNativeTransientItems({
5256
+ items,
5257
+ snapshots,
5258
+ dx,
5259
+ dy
5260
+ }) {
5261
+ return items.map((item) => {
5262
+ const snapshot = snapshots[item.id];
5263
+ if (!snapshot) return item;
5264
+ return {
5265
+ ...snapshot,
5266
+ x: snapshot.x + dx,
5267
+ y: snapshot.y + dy,
5268
+ bounds: {
5269
+ ...snapshot.bounds,
5270
+ x: snapshot.bounds.x + dx,
5271
+ y: snapshot.bounds.y + dy
5272
+ }
5273
+ };
5274
+ });
5275
+ }
5276
+ function replaceNativeTransientItem({
5277
+ items,
5278
+ itemId,
5279
+ item: replacement
5280
+ }) {
5281
+ return items.map((item) => item.id === itemId ? replacement : item);
5282
+ }
5178
5283
  var DEFAULT_NATIVE_LINK_TOOL_DIALOG_LABELS = {
5179
5284
  title: "Add link",
5180
5285
  description: "Paste the link you want to add to the board.",
@@ -5321,13 +5426,41 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5321
5426
  customPlacementsRef.current = customPlacements;
5322
5427
  const onSelectionChangeRef = react.useRef(onSelectionChange);
5323
5428
  onSelectionChangeRef.current = onSelectionChange;
5324
- const itemsRef = react.useRef(items);
5325
- itemsRef.current = items;
5429
+ const [transientItems, setTransientItems] = react.useState(
5430
+ null
5431
+ );
5432
+ const transientItemsRef = react.useRef(null);
5433
+ const activeItems = transientItems ?? items;
5434
+ const itemsRef = react.useRef(activeItems);
5435
+ itemsRef.current = activeItems;
5326
5436
  const selectedIdsRef = react.useRef(selectedIds);
5327
5437
  selectedIdsRef.current = selectedIds;
5328
5438
  const remotePresenceRef = react.useRef(remotePresence);
5329
5439
  remotePresenceRef.current = remotePresence;
5330
5440
  const dragStateRef = react.useRef({ kind: "idle" });
5441
+ react.useEffect(() => {
5442
+ const committedItems = items;
5443
+ if (transientItemsRef.current === null && itemsRef.current === committedItems) {
5444
+ return;
5445
+ }
5446
+ transientItemsRef.current = null;
5447
+ setTransientItems(null);
5448
+ }, [items]);
5449
+ const setTransientItemsPreview = react.useCallback((nextItems) => {
5450
+ transientItemsRef.current = nextItems;
5451
+ setTransientItems(nextItems);
5452
+ }, []);
5453
+ const clearTransientItemsPreview = react.useCallback(() => {
5454
+ transientItemsRef.current = null;
5455
+ setTransientItems(null);
5456
+ }, []);
5457
+ const commitTransientItemsPreview = react.useCallback(() => {
5458
+ const nextItems = transientItemsRef.current;
5459
+ const change = onItemsChangeRef.current;
5460
+ if (!nextItems || !change) return false;
5461
+ change(nextItems);
5462
+ return true;
5463
+ }, []);
5331
5464
  const [placementPreview, setPlacementPreviewState] = react.useState(null);
5332
5465
  const setRealtimePlacementPreview = react.useCallback(
5333
5466
  (nextPreview) => {
@@ -5468,8 +5601,8 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5468
5601
  const hideToolCursor = react.useCallback(() => {
5469
5602
  }, []);
5470
5603
  const selectedItems = react.useMemo(
5471
- () => items.filter((it) => selectedIds.includes(it.id)),
5472
- [items, selectedIds]
5604
+ () => activeItems.filter((item) => selectedIds.includes(item.id)),
5605
+ [activeItems, selectedIds]
5473
5606
  );
5474
5607
  const selectedStyleInspectorState = react.useMemo(() => {
5475
5608
  const styleableItems = selectedItems.filter(
@@ -5485,10 +5618,10 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5485
5618
  };
5486
5619
  }, [selectedItems]);
5487
5620
  const sceneItems = react.useMemo(() => {
5488
- if (eraserPreviewIds.length === 0) return items;
5621
+ if (eraserPreviewIds.length === 0) return activeItems;
5489
5622
  const hidden = new Set(eraserPreviewIds);
5490
- return items.filter((it) => !hidden.has(it.id));
5491
- }, [items, eraserPreviewIds]);
5623
+ return activeItems.filter((item) => !hidden.has(item.id));
5624
+ }, [activeItems, eraserPreviewIds]);
5492
5625
  const showResizeHandles = interactive && selectedItems.length === 1 && !selectedItems[0]?.locked && supportsNativeResizeHandles(selectedItems[0]);
5493
5626
  const patchSelectedItemsStrokeStyle = react.useCallback(
5494
5627
  (patch) => {
@@ -5776,7 +5909,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5776
5909
  setRealtimePlacementPreview({
5777
5910
  kind: "stroke",
5778
5911
  tool: st.tool,
5779
- points: [...pts],
5912
+ points: sampleNativeStrokePreviewPoints(pts),
5780
5913
  style: { ...strokeStyleRef.current }
5781
5914
  });
5782
5915
  return;
@@ -5786,21 +5919,13 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5786
5919
  const dy = worldY - st.startWorld.y;
5787
5920
  const change = onItemsChangeRef.current;
5788
5921
  if (!change) return;
5789
- const nextList = itemsRef.current.map((it) => {
5790
- const snap = st.snapshots[it.id];
5791
- if (!snap) return it;
5792
- return {
5793
- ...snap,
5794
- x: snap.x + dx,
5795
- y: snap.y + dy,
5796
- bounds: {
5797
- ...snap.bounds,
5798
- x: snap.bounds.x + dx,
5799
- y: snap.bounds.y + dy
5800
- }
5801
- };
5922
+ const nextItems = moveNativeTransientItems({
5923
+ items: itemsRef.current,
5924
+ snapshots: st.snapshots,
5925
+ dx,
5926
+ dy
5802
5927
  });
5803
- change(nextList);
5928
+ setTransientItemsPreview(nextItems);
5804
5929
  return;
5805
5930
  }
5806
5931
  if (st.kind === "rotate") {
@@ -5813,7 +5938,13 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5813
5938
  st.startPointerAngleRad,
5814
5939
  angle
5815
5940
  );
5816
- change(itemsRef.current.map((item) => item.id === st.id ? next : item));
5941
+ setTransientItemsPreview(
5942
+ replaceNativeTransientItem({
5943
+ items: itemsRef.current,
5944
+ itemId: st.id,
5945
+ item: next
5946
+ })
5947
+ );
5817
5948
  return;
5818
5949
  }
5819
5950
  if (st.kind === "resize") {
@@ -5823,7 +5954,13 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5823
5954
  x: worldX,
5824
5955
  y: worldY
5825
5956
  });
5826
- change(itemsRef.current.map((item) => item.id === st.id ? next : item));
5957
+ setTransientItemsPreview(
5958
+ replaceNativeTransientItem({
5959
+ items: itemsRef.current,
5960
+ itemId: st.id,
5961
+ item: next
5962
+ })
5963
+ );
5827
5964
  return;
5828
5965
  }
5829
5966
  if (st.kind === "marquee") {
@@ -5873,6 +6010,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5873
6010
  requestRender,
5874
6011
  screenToWorld,
5875
6012
  setRealtimePlacementPreview,
6013
+ setTransientItemsPreview,
5876
6014
  updateToolCursorPoint
5877
6015
  ]
5878
6016
  );
@@ -5914,10 +6052,12 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5914
6052
  }
5915
6053
  if (st.kind === "move") {
5916
6054
  dragStateRef.current = { kind: "idle" };
6055
+ commitTransientItemsPreview();
5917
6056
  return;
5918
6057
  }
5919
6058
  if (st.kind === "resize" || st.kind === "rotate") {
5920
6059
  dragStateRef.current = { kind: "idle" };
6060
+ commitTransientItemsPreview();
5921
6061
  return;
5922
6062
  }
5923
6063
  if (st.kind === "marquee") {
@@ -6127,6 +6267,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6127
6267
  requestSelectToolAfterUse,
6128
6268
  screenToWorld,
6129
6269
  setRealtimePlacementPreview,
6270
+ commitTransientItemsPreview,
6130
6271
  updateToolCursorPoint
6131
6272
  ]
6132
6273
  );
@@ -6217,6 +6358,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6217
6358
  notifyWorldPointerLeave();
6218
6359
  dragStateRef.current = { kind: "idle" };
6219
6360
  setRealtimePlacementPreview(null);
6361
+ clearTransientItemsPreview();
6220
6362
  setLaserTrail([]);
6221
6363
  setEraserTrail([]);
6222
6364
  setEraserPreviewIds([]);
@@ -6230,7 +6372,8 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6230
6372
  requestRender,
6231
6373
  hideToolCursor,
6232
6374
  notifyWorldPointerLeave,
6233
- setRealtimePlacementPreview
6375
+ setRealtimePlacementPreview,
6376
+ clearTransientItemsPreview
6234
6377
  ]
6235
6378
  );
6236
6379
  react.useImperativeHandle(
@@ -6300,8 +6443,8 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6300
6443
  placementPreview,
6301
6444
  laserTrail,
6302
6445
  eraserTrail,
6303
- eraserPreviewItems: items.filter(
6304
- (it) => eraserPreviewIds.includes(it.id)
6446
+ eraserPreviewItems: activeItems.filter(
6447
+ (item) => eraserPreviewIds.includes(item.id)
6305
6448
  ),
6306
6449
  previewStrokeStyle: strokeStyleState,
6307
6450
  remotePresence