@reekon-tools/boldr-utils 1.13.1 → 1.14.0

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.
Files changed (83) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.js +2 -2
  2. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +100 -34
  3. package/dist/annotation/canvas/Tool.d.ts +4 -3
  4. package/dist/annotation/canvas/measurementGeometry.d.ts +1 -1
  5. package/dist/annotation/canvas/measurementGeometry.js +5 -4
  6. package/dist/annotation/canvas/stampLayout.d.ts +2 -1
  7. package/dist/annotation/canvas/stampLayout.js +18 -2
  8. package/dist/annotation/canvas/tools/panTool.js +1 -1
  9. package/dist/annotation/canvas/tools/selectTool.js +14 -10
  10. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +2 -0
  11. package/dist/annotation/canvas/useAnnotationCanvasState.js +24 -3
  12. package/dist/calculator/schema.d.ts +7 -0
  13. package/dist/exports.d.ts +1 -1
  14. package/dist/exports.js +1 -1
  15. package/dist/theme/index.d.ts +3 -0
  16. package/dist/theme/index.js +3 -0
  17. package/dist/theme/preset.d.ts +120 -0
  18. package/dist/theme/preset.js +25 -0
  19. package/dist/theme/semantic.d.ts +136 -0
  20. package/dist/theme/semantic.js +116 -0
  21. package/dist/types/annotation.d.ts +4 -0
  22. package/dist/types/annotation.js +25 -8
  23. package/package.json +1 -1
  24. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  25. package/dist/canvas/AnnotationCanvas.js +0 -10
  26. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  27. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  28. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  29. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  30. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  31. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  32. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  33. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  34. package/dist/canvas/Tool.d.ts +0 -38
  35. package/dist/canvas/Tool.js +0 -1
  36. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  37. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  38. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  39. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  40. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  41. package/dist/canvas/elements/ShapeElement.js +0 -62
  42. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  43. package/dist/canvas/elements/StrokeElement.js +0 -18
  44. package/dist/canvas/measurementPicker.d.ts +0 -10
  45. package/dist/canvas/measurementPicker.js +0 -1
  46. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  47. package/dist/canvas/measurementStampOverlay.js +0 -1
  48. package/dist/canvas/pointerAdapter.d.ts +0 -3
  49. package/dist/canvas/pointerAdapter.js +0 -19
  50. package/dist/canvas/stampLayout.d.ts +0 -5
  51. package/dist/canvas/stampLayout.js +0 -14
  52. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  53. package/dist/canvas/tools/measurementStampTool.js +0 -37
  54. package/dist/canvas/tools/panTool.d.ts +0 -5
  55. package/dist/canvas/tools/panTool.js +0 -25
  56. package/dist/canvas/tools/penTool.d.ts +0 -13
  57. package/dist/canvas/tools/penTool.js +0 -68
  58. package/dist/canvas/tools/selectTool.d.ts +0 -2
  59. package/dist/canvas/tools/selectTool.js +0 -182
  60. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  61. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  62. package/dist/canvas/viewport.d.ts +0 -16
  63. package/dist/canvas/viewport.js +0 -54
  64. package/dist/data/AnnotationDataContext.d.ts +0 -8
  65. package/dist/data/AnnotationDataContext.js +0 -11
  66. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  67. package/dist/data/AnnotationDataProvider.js +0 -4
  68. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  69. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  70. package/dist/data/canvasPersistence.d.ts +0 -3
  71. package/dist/data/canvasPersistence.js +0 -26
  72. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  73. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  74. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  75. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  76. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  77. package/dist/data/hooks/useAnnotationList.js +0 -26
  78. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  79. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  80. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  81. package/dist/hooks/useParseMeasurement.js +0 -14
  82. package/dist/utils/evaluateFormula.d.ts +0 -20
  83. package/dist/utils/evaluateFormula.js +0 -31
@@ -284,7 +284,7 @@ export const AnnotationCanvasInner = (props) => {
284
284
  const world = state.ctx.viewport.screenToWorld(screen);
285
285
  const placed = [...state.effectiveCanvas.placedMeasurements]
286
286
  .reverse()
287
- .find((m) => hitPlacedMeasurement(m, world, state.viewport.zoom, state.tileScaleFactor, state.tileViewportScale));
287
+ .find((m) => hitPlacedMeasurement(m, world, state.viewport.zoom, state.tileScaleFactor, state.tileViewportScale, state.headerTileScaleFactor));
288
288
  if (placed)
289
289
  onMeasurementStampDoubleTap(placed);
290
290
  }, [onMeasurementStampDoubleTap, state]);
@@ -320,7 +320,7 @@ export const AnnotationCanvasInner = (props) => {
320
320
  }, children: state.effectiveCanvas.placedMeasurements.map((placed) => {
321
321
  // Square tile or wide group-header pill — one footprint source of
322
322
  // truth shared with the hit-test (stampTileDims).
323
- const { width, height } = stampTileDims(placed, state.tileScaleFactor, state.tileViewportScale);
323
+ const { width, height } = stampTileDims(placed, state.tileScaleFactor, state.tileViewportScale, state.headerTileScaleFactor);
324
324
  const cx = (placed.anchor.x - state.viewport.pan.x) * state.viewport.zoom;
325
325
  const cy = (placed.anchor.y - state.viewport.pan.y) * state.viewport.zoom;
326
326
  const isSelected = selection?.ids.includes(placed.id) ?? false;
@@ -121,7 +121,11 @@ export const AnnotationCanvasInner = (props) => {
121
121
  const zoom = useSharedValue(state.viewport.zoom);
122
122
  const panX = useSharedValue(state.viewport.pan.x);
123
123
  const panY = useSharedValue(state.viewport.pan.y);
124
- const pinchStartZoom = useSharedValue(state.viewport.zoom);
124
+ // Set by the viewport gestures' onChange, consumed by their onFinalize: the
125
+ // post-gesture sync to React state happens iff a gesture actually moved the
126
+ // viewport, and in onFinalize rather than onEnd because on Android the
127
+ // viewport gestures routinely exit through fail()/cancel() (see the pan).
128
+ const viewportDirty = useSharedValue(false);
125
129
  // Skia consumes this directly and re-renders the transform on the UI thread.
126
130
  const worldTransform = useDerivedValue(() => {
127
131
  'worklet';
@@ -748,8 +752,17 @@ export const AnnotationCanvasInner = (props) => {
748
752
  world: stateRef.current.ctx.viewport.screenToWorld(screen),
749
753
  });
750
754
  // Commit the final viewport to the JS snapshot once a gesture ends, so
751
- // tools' screenToWorld/worldToScreen reflect the new pan/zoom.
755
+ // tools' screenToWorld/worldToScreen reflect the new pan/zoom. Skips the
756
+ // no-op commit: the pan and the pinch can finalize on the same lift, and
757
+ // the second sync would re-render (and re-settle any background layer)
758
+ // for values React already has.
752
759
  const syncViewport = (next) => {
760
+ const current = stateRef.current.ctx.viewport.state;
761
+ if (current.zoom === next.zoom &&
762
+ current.pan.x === next.pan.x &&
763
+ current.pan.y === next.pan.y) {
764
+ return;
765
+ }
753
766
  stateRef.current.setViewport(next);
754
767
  };
755
768
  const beginViewportGesture = () => {
@@ -862,7 +875,7 @@ export const AnnotationCanvasInner = (props) => {
862
875
  return;
863
876
  const world = st.ctx.viewport.screenToWorld(screen);
864
877
  const zoomNow = st.ctx.viewport.state.zoom;
865
- const hit = dragSelection.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor);
878
+ const hit = dragSelection.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor);
866
879
  if (!hit || hit.kind !== 'measurement' || hit.id !== prevSelectedId) {
867
880
  return;
868
881
  }
@@ -873,9 +886,17 @@ export const AnnotationCanvasInner = (props) => {
873
886
  // Viewport pan — runs on the UI thread (no runOnJS), mutating the shared
874
887
  // viewport directly. Mirrors viewport.ts `panBy`. Used for both the
875
888
  // two-finger pan (always available) and the one-finger Hand tool.
889
+ //
890
+ // averageTouches(true) is load-bearing on Android: PanGestureHandler
891
+ // defaults to tracking the LAST-LANDED pointer, not the centroid (iOS
892
+ // always averages). Running simultaneously with the pinch, a non-averaged
893
+ // two-finger pan translates the canvas by finger 2's whole travel — during
894
+ // a spread that is half the spread distance of sideways drift, which reads
895
+ // as "the zoom runs away from where I aimed".
876
896
  const buildViewportPan = (minP, maxP) => Gesture.Pan()
877
897
  .minPointers(minP)
878
898
  .maxPointers(maxP)
899
+ .averageTouches(true)
879
900
  .onBegin(() => {
880
901
  'worklet';
881
902
  runOnJS(beginViewportGesture)();
@@ -884,47 +905,91 @@ export const AnnotationCanvasInner = (props) => {
884
905
  'worklet';
885
906
  panX.value -= e.changeX / zoom.value;
886
907
  panY.value -= e.changeY / zoom.value;
908
+ viewportDirty.value = true;
887
909
  })
888
- .onEnd(() => {
889
- 'worklet';
890
- runOnJS(syncViewport)({
891
- zoom: zoom.value,
892
- pan: { x: panX.value, y: panY.value },
893
- });
894
- })
910
+ // onFinalize, not onEnd: on Android this pan normally exits through
911
+ // fail() — a (2,2) pan the moment the first finger lifts (pointer
912
+ // count below minPointers), and a pure two-finger translate whose
913
+ // pinch never activated at the final lift. onEnd never runs on those
914
+ // paths, and the movement would never reach React state: hit-tests go
915
+ // stale, a background layer never re-rasterizes for the new view, and
916
+ // the next JS-side viewport write snaps the canvas back.
895
917
  .onFinalize(() => {
896
918
  'worklet';
919
+ if (viewportDirty.value) {
920
+ viewportDirty.value = false;
921
+ runOnJS(syncViewport)({
922
+ zoom: zoom.value,
923
+ pan: { x: panX.value, y: panY.value },
924
+ });
925
+ }
897
926
  runOnJS(endViewportGesture)();
898
927
  });
899
- const viewportPan = buildViewportPan(2, 2);
928
+ // With the Hand tool the viewport pan spans one AND two fingers as a
929
+ // single gesture. Two separate gestures (a one-finger pan racing the
930
+ // two-finger pair) cannot express a staggered pinch: finger 1 panning
931
+ // past the slop resolves the Race before finger 2 lands, the pinch is
932
+ // cancelled with it, and finger 2's touch then kills the one-finger pan
933
+ // too (pointer count over maxPointers cancels an active Android pan) —
934
+ // a dead canvas until every finger lifts. One pan spanning both counts
935
+ // stays active across 1→2→1, and its translation is continuous across
936
+ // pointer transitions (the handler rebases its origin on every
937
+ // POINTER_DOWN/UP).
938
+ const viewportPan = buildViewportPan(panViewport ? 1 : 2, 2);
900
939
  // Pinch-to-zoom about the focal point — UI thread. Mirrors viewport.ts
901
940
  // `zoomAt`: keep the world point under the focal screen point fixed.
941
+ //
942
+ // Driven by the per-event ratio (`scaleChange`), not by cumulative
943
+ // `e.scale` against a zoom snapshotted in onBegin. On Android onBegin
944
+ // fires at the FIRST finger down — before a pinch provably exists — so a
945
+ // snapshot there can go stale against anything else driving the shared
946
+ // values (the glide animation, the simultaneous pan). The native
947
+ // cumulative scale also freezes rather than rebases across pointer-set
948
+ // changes (2→1→2, a third finger) and sticks at 0 for the rest of the
949
+ // gesture if the fingers ever collapse to a single point, so only ratios
950
+ // between consecutive events are trustworthy. The live focal needs no
951
+ // snapshot either: a focal that merely moves leaves this update a no-op
952
+ // (translation is the pan's job), which is also what makes Android's
953
+ // wrong-focal BEGAN event (it reports the first finger's touch-down
954
+ // position, not a centroid) harmless here.
902
955
  const pinch = Gesture.Pinch()
903
956
  .onBegin(() => {
904
957
  'worklet';
905
- pinchStartZoom.value = zoom.value;
906
958
  runOnJS(beginViewportGesture)();
907
959
  })
908
- .onUpdate((e) => {
960
+ .onChange((e) => {
909
961
  'worklet';
910
- const clamped = Math.min(50, Math.max(0.05, pinchStartZoom.value * e.scale));
962
+ // Drop corrupt ratios instead of applying them: a native span
963
+ // collapse yields scale 0, and the ratio of the frame after that is
964
+ // 0/0. Applying either would slam the zoom to a bound (or NaN it).
965
+ const ratio = e.scaleChange;
966
+ if (!Number.isFinite(ratio) || ratio <= 0)
967
+ return;
968
+ const next = Math.min(50, Math.max(0.05, zoom.value * ratio));
969
+ if (next === zoom.value)
970
+ return; // clamped at a bound, or ratio 1
911
971
  // World point currently under the focal screen point (using the live
912
972
  // zoom/pan), then re-anchor pan so it stays put at the new zoom.
913
973
  const focalWorldX = e.focalX / zoom.value + panX.value;
914
974
  const focalWorldY = e.focalY / zoom.value + panY.value;
915
- panX.value = focalWorldX - e.focalX / clamped;
916
- panY.value = focalWorldY - e.focalY / clamped;
917
- zoom.value = clamped;
918
- })
919
- .onEnd(() => {
920
- 'worklet';
921
- runOnJS(syncViewport)({
922
- zoom: zoom.value,
923
- pan: { x: panX.value, y: panY.value },
924
- });
975
+ panX.value = focalWorldX - e.focalX / next;
976
+ panY.value = focalWorldY - e.focalY / next;
977
+ zoom.value = next;
978
+ viewportDirty.value = true;
925
979
  })
980
+ // onFinalize, not onEnd, for the same reason as the pan: an Android
981
+ // pinch that never activated fails at the final lift, and a cancelled
982
+ // gesture never reaches onEnd — the dirty check keeps the no-gesture
983
+ // paths from committing anything.
926
984
  .onFinalize(() => {
927
985
  'worklet';
986
+ if (viewportDirty.value) {
987
+ viewportDirty.value = false;
988
+ runOnJS(syncViewport)({
989
+ zoom: zoom.value,
990
+ pan: { x: panX.value, y: panY.value },
991
+ });
992
+ }
928
993
  runOnJS(endViewportGesture)();
929
994
  });
930
995
  // Freehand drawing (pen/marker/highlighter) — one finger, UI thread.
@@ -1128,7 +1193,7 @@ export const AnnotationCanvasInner = (props) => {
1128
1193
  // so a selected tile isn't stuck until you deselect it).
1129
1194
  const selId = st.ctx.selection?.ids[0];
1130
1195
  if (selId &&
1131
- !cfg.isSelectedTileGrab?.(st.ctx.document, selId, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor)) {
1196
+ !cfg.isSelectedTileGrab?.(st.ctx.document, selId, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor)) {
1132
1197
  const handle = cfg.hitTestHandle?.(st.ctx.document, selId, world, zoomNow);
1133
1198
  if (handle) {
1134
1199
  const m = st.ctx.document.placedMeasurements.find((x) => x.id === selId);
@@ -1228,7 +1293,7 @@ export const AnnotationCanvasInner = (props) => {
1228
1293
  return;
1229
1294
  }
1230
1295
  }
1231
- const hit = cfg.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor);
1296
+ const hit = cfg.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor);
1232
1297
  if (!hit) {
1233
1298
  // Backgrounds can cover the whole document, so they only get the
1234
1299
  // drag once every annotation element misses, and only when ALREADY
@@ -1248,7 +1313,7 @@ export const AnnotationCanvasInner = (props) => {
1248
1313
  st.ctx.setSelection({ ids: [hit.id] });
1249
1314
  // Grabbing a line annotation's tile slides it; otherwise group-move.
1250
1315
  const grab = hit.kind === 'measurement'
1251
- ? cfg.classifyMeasurementGrab?.(st.ctx.document, hit.id, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor)
1316
+ ? cfg.classifyMeasurementGrab?.(st.ctx.document, hit.id, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor)
1252
1317
  : 'move';
1253
1318
  if (grab === 'slide') {
1254
1319
  const m = st.ctx.document.placedMeasurements.find((x) => x.id === hit.id);
@@ -1425,7 +1490,8 @@ export const AnnotationCanvasInner = (props) => {
1425
1490
  // One finger, by active tool:
1426
1491
  // - freehand (pen/marker/highlighter) → draw on the UI thread
1427
1492
  // - shape tools (line/rect/…) → rubber-band on the UI thread
1428
- // - Hand → pan the viewport on the UI thread (live, no JS round-trip)
1493
+ // - Hand → no separate gesture: viewportPan above already spans one
1494
+ // finger (min 1), so panning and pinching stay one uncancelled stream
1429
1495
  // - select → drag the hit element on the UI thread
1430
1496
  // - everything else → dispatch pointer events on the JS thread
1431
1497
  const oneFinger = freehand
@@ -1433,18 +1499,18 @@ export const AnnotationCanvasInner = (props) => {
1433
1499
  : shapeDraw
1434
1500
  ? buildShapeDrawPan(shapeDraw)
1435
1501
  : panViewport
1436
- ? buildViewportPan(1, 1)
1502
+ ? null
1437
1503
  : dragSelection
1438
1504
  ? buildSelectDragPan(dragSelection)
1439
1505
  : toolPan;
1440
1506
  // Long-press only joins the race when the active tool acts on it, so it
1441
1507
  // never pre-empts a one-finger drag/draw on tools that ignore holds.
1442
- return Gesture.Race(tap, ...(longPressEnabled ? [longPress] : []), Gesture.Simultaneous(viewportPan, pinch), oneFinger);
1508
+ return Gesture.Race(tap, ...(longPressEnabled ? [longPress] : []), Gesture.Simultaneous(viewportPan, pinch), ...(oneFinger ? [oneFinger] : []));
1443
1509
  }, [
1444
1510
  zoom,
1445
1511
  panX,
1446
1512
  panY,
1447
- pinchStartZoom,
1513
+ viewportDirty,
1448
1514
  livePoints,
1449
1515
  handoffStrokes,
1450
1516
  removeHandoffStrokes,
@@ -1555,7 +1621,7 @@ export const AnnotationCanvasInner = (props) => {
1555
1621
  // non-fullscreen canvas (e.g. a diagram strip) escape into surrounding UI.
1556
1622
  _jsxs(GestureHandlerRootView, { style: [{ width, height, overflow: 'hidden' }, style], children: [backgroundLayers.map((layer) => (_jsx(BackgroundSkImageLoader, { layer: layer, resolveUrl: resolveImageUrl, onImage: onBackgroundSkImage }, layer.id))), _jsx(GestureDetector, { gesture: gesture, children: _jsx(View, { style: { width, height }, collapsable: false, children: AnnotationCanvasSkia(skiaProps) }) }), renderMeasurementStamp && (_jsx(View, { pointerEvents: "box-none", style: StyleSheet.absoluteFill, children: state.effectiveCanvas.placedMeasurements.map((placed) => (_jsx(MeasurementStampOverlayItem, { placed: placed, measurement: placed.measurementId
1557
1623
  ? (state.measurementsById.get(placed.measurementId) ?? null)
1558
- : (resolveStampMeasurement?.(placed) ?? null), selected: selection?.ids.includes(placed.id) ?? false, dragging: draggingId === placed.id, sliding: slidingId === placed.id, endpointDragging: epDragId === placed.id, rectResizing: rectDragId === placed.id, zoomSnapshot: state.viewport.zoom, zoom: zoom, panX: panX, panY: panY, dragX: dragX, dragY: dragY, slideCtx: slideCtx, epCtx: epCtx, rectCtx: rectCtx, renderMeasurementStamp: renderMeasurementStamp, tileScaleFactor: state.tileScaleFactor, tileViewportScale: state.tileViewportScale, onStampPress: onMeasurementStampPress, onStampLongPress: onMeasurementStampLongPress, onRemove: onMeasurementStampRemove
1624
+ : (resolveStampMeasurement?.(placed) ?? null), selected: selection?.ids.includes(placed.id) ?? false, dragging: draggingId === placed.id, sliding: slidingId === placed.id, endpointDragging: epDragId === placed.id, rectResizing: rectDragId === placed.id, zoomSnapshot: state.viewport.zoom, zoom: zoom, panX: panX, panY: panY, dragX: dragX, dragY: dragY, slideCtx: slideCtx, epCtx: epCtx, rectCtx: rectCtx, renderMeasurementStamp: renderMeasurementStamp, tileScaleFactor: state.tileScaleFactor, headerTileScaleFactor: state.headerTileScaleFactor, tileViewportScale: state.tileViewportScale, onStampPress: onMeasurementStampPress, onStampLongPress: onMeasurementStampLongPress, onRemove: onMeasurementStampRemove
1559
1625
  ? () => {
1560
1626
  const defaultRemove = () => {
1561
1627
  const { ops, keepSelection } = buildRemoveMeasurementOps(placed);
@@ -1571,10 +1637,10 @@ export const AnnotationCanvasInner = (props) => {
1571
1637
  wrapContent: loupeWrap,
1572
1638
  }) }))] }));
1573
1639
  };
1574
- const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, rectCtx, renderMeasurementStamp, tileScaleFactor, tileViewportScale, onStampPress, onStampLongPress, onRemove, }) => {
1640
+ const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, rectCtx, renderMeasurementStamp, tileScaleFactor, headerTileScaleFactor, tileViewportScale, onStampPress, onStampLongPress, onRemove, }) => {
1575
1641
  // Square tile or wide group-header pill — one footprint source of truth
1576
1642
  // shared with the hit-test (stampTileDims).
1577
- const { width, height } = stampTileDims(placed, tileScaleFactor, tileViewportScale);
1643
+ const { width, height } = stampTileDims(placed, tileScaleFactor, tileViewportScale, headerTileScaleFactor ?? tileScaleFactor);
1578
1644
  const halfW = width / 2;
1579
1645
  const halfH = height / 2;
1580
1646
  // Remove-"X" touch target, sized as a fraction of the tile and corner-pinned
@@ -24,6 +24,7 @@ export interface ToolContext {
24
24
  viewport: ViewportApi;
25
25
  tileViewportScale: number;
26
26
  tileScaleFactor: number;
27
+ headerTileScaleFactor: number;
27
28
  preview(patch: AnnotationDocumentPatch): void;
28
29
  commit(patch: AnnotationDocumentPatch): void;
29
30
  setSelection(selection: Selection | null): void;
@@ -54,12 +55,12 @@ export interface ShapeDrawConfig {
54
55
  }
55
56
  export type DragElementKind = 'stroke' | 'shape' | 'measurement' | 'background';
56
57
  export interface DragSelectionConfig {
57
- hitTest(doc: AnnotationCanvasState, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number): {
58
+ hitTest(doc: AnnotationCanvasState, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number, headerTileScaleFactor?: number): {
58
59
  id: AnnotationElementId;
59
60
  kind: DragElementKind;
60
61
  } | null;
61
62
  buildTranslatePatch(doc: AnnotationCanvasState, id: AnnotationElementId, kind: DragElementKind, delta: Vec2): AnnotationDocumentPatch | null;
62
- classifyMeasurementGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number): 'slide' | 'move';
63
+ classifyMeasurementGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number, headerTileScaleFactor?: number): 'slide' | 'move';
63
64
  buildSlidePatch?(doc: AnnotationCanvasState, id: AnnotationElementId, delta: Vec2, zoom: number): AnnotationDocumentPatch | null;
64
65
  hitTestHandle?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): 'a' | 'b' | null;
65
66
  buildEndpointPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, handle: 'a' | 'b', delta: Vec2): AnnotationDocumentPatch | null;
@@ -77,7 +78,7 @@ export interface DragSelectionConfig {
77
78
  fixed: Vec2;
78
79
  } | null;
79
80
  buildShapeCornerPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, corner: RectCorner, delta: Vec2): AnnotationDocumentPatch | null;
80
- isSelectedTileGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number): boolean;
81
+ isSelectedTileGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number, headerTileScaleFactor?: number): boolean;
81
82
  hitTestBackground?(doc: AnnotationCanvasState, world: Vec2): {
82
83
  id: AnnotationElementId;
83
84
  } | null;
@@ -36,7 +36,7 @@ export declare const rectCornerPoint: (rect: {
36
36
  b: Vec2;
37
37
  }, corner: RectCorner) => Vec2;
38
38
  export declare const oppositeRectCorner: (corner: RectCorner) => RectCorner;
39
- export declare const hitPlacedMeasurement: (m: PlacedMeasurementRef, p: Vec2, zoom?: number, tileScaleFactor?: number, viewportScale?: number) => boolean;
39
+ export declare const hitPlacedMeasurement: (m: PlacedMeasurementRef, p: Vec2, zoom?: number, tileScaleFactor?: number, viewportScale?: number, headerTileScaleFactor?: number) => boolean;
40
40
  export interface RemoveMeasurementResult {
41
41
  ops: AnnotationPatchOp[];
42
42
  keepSelection: boolean;
@@ -110,14 +110,15 @@ const segmentDistSq = (p, a, b) => {
110
110
  // around the anchor, the line body of a line annotation, or the
111
111
  // border ring of a rectangle annotation (interiors stay transparent to hits so
112
112
  // elements inside remain reachable).
113
- export const hitPlacedMeasurement = (m, p, zoom = 1, tileScaleFactor = 1, viewportScale = 1) => {
113
+ export const hitPlacedMeasurement = (m, p, zoom = 1, tileScaleFactor = 1, viewportScale = 1, headerTileScaleFactor = tileScaleFactor) => {
114
114
  // stampTileDims yields the tile's CURRENT screen-space box (the
115
115
  // zoom-relative multiplier arrives via `viewportScale`); converting the
116
116
  // half-extents (+ padding) back to doc space via the zoom keeps the hit box
117
117
  // matching what's drawn — a doc-constant footprint (blank and populated
118
- // stamps share the square footprint; group-header chips are a wide pill
119
- // see stampTileDims in stampLayout).
120
- const dims = stampTileDims(m, tileScaleFactor, viewportScale);
118
+ // stamps share the square footprint; group-header chips are a wide pill
119
+ // sized by their OWN document-wide knob, `headerTileScaleFactor` — see
120
+ // stampTileDims in stampLayout).
121
+ const dims = stampTileDims(m, tileScaleFactor, viewportScale, headerTileScaleFactor);
121
122
  const halfW = (dims.width / 2 + STAMP_HIT_PADDING) / zoom;
122
123
  const halfH = (dims.height / 2 + STAMP_HIT_PADDING) / zoom;
123
124
  const dx = Math.abs(p.x - m.anchor.x);
@@ -19,6 +19,7 @@ export declare const DEFAULT_TILE_SCALE = 1;
19
19
  export declare const TILE_SCALE_MIN = 0.4;
20
20
  export declare const TILE_SCALE_MAX = 2;
21
21
  export declare const resolveTileScaleFactor: (canvas: Pick<AnnotationCanvasState, "tileScaleFactor" | "tileScaleFactorMobile">, platform: TileScalePlatform) => number;
22
+ export declare const resolveHeaderTileScaleFactor: (canvas: Pick<AnnotationCanvasState, "tileScaleFactor" | "tileScaleFactorMobile" | "headerTileScaleFactor" | "headerTileScaleFactorMobile">, platform: TileScalePlatform) => number;
22
23
  export declare const clampTileScale: (v: number) => number;
23
24
  export declare const tileDocSizeForFactor: (factor: number, zoom: number) => number;
24
25
  export declare const tileScaleFromDocSize: (docSize: number, zoom: number) => number;
@@ -28,4 +29,4 @@ export interface StampTileDims {
28
29
  width: number;
29
30
  height: number;
30
31
  }
31
- export declare const stampTileDims: (m: Pick<PlacedMeasurementRef, "scale" | "isGroupHeader" | "collapsed">, tileScaleFactor?: number, viewportScale?: number) => StampTileDims;
32
+ export declare const stampTileDims: (m: Pick<PlacedMeasurementRef, "scale" | "isGroupHeader" | "collapsed">, tileScaleFactor?: number, viewportScale?: number, headerTileScaleFactor?: number) => StampTileDims;
@@ -80,6 +80,18 @@ export const TILE_SCALE_MAX = 2;
80
80
  export const resolveTileScaleFactor = (canvas, platform) => (platform === 'mobile'
81
81
  ? (canvas.tileScaleFactorMobile ?? canvas.tileScaleFactor)
82
82
  : canvas.tileScaleFactor) ?? DEFAULT_TILE_SCALE;
83
+ // Resolve the document-wide HEADER-scale knob for a platform — the group/form
84
+ // header counterpart of resolveTileScaleFactor (Asana 1217570500895543).
85
+ // Same platform fallback (mobile → web), then one more step: a document with
86
+ // no header knob at all falls back to the MEASUREMENT factor, the pre-split
87
+ // behavior where one knob drove every tile — so existing documents render
88
+ // identically until a user first moves the header slider. The ONE read path
89
+ // for the header fields — renderers, hit-testing, and the slider UIs must all
90
+ // resolve through here so they agree.
91
+ export const resolveHeaderTileScaleFactor = (canvas, platform) => (platform === 'mobile'
92
+ ? (canvas.headerTileScaleFactorMobile ?? canvas.headerTileScaleFactor)
93
+ : canvas.headerTileScaleFactor) ??
94
+ resolveTileScaleFactor(canvas, platform);
83
95
  // Clamp a tile-scale-factor candidate to the supported range. The single guard
84
96
  // for the value before it lands in the document (slider input, restored docs).
85
97
  export const clampTileScale = (v) => v < TILE_SCALE_MIN ? TILE_SCALE_MIN : v > TILE_SCALE_MAX ? TILE_SCALE_MAX : v;
@@ -136,11 +148,15 @@ export const isUnassociatedStamp = (m) => !m.isGroupHeader && !m.measurementId &
136
148
  // canvas's content-fit reference zoom (see the note above), so tiles scale
137
149
  // with the drawing as the user zooms.
138
150
  export const stampTileSize = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1) => STAMP_TILE_SIZE * (m.scale ?? 1) * tileScaleFactor * viewportScale;
139
- export const stampTileDims = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1) => {
140
- const k = (m.scale ?? 1) * tileScaleFactor * viewportScale;
151
+ // `headerTileScaleFactor` is the header chips' own document-wide knob
152
+ // (resolveHeaderTileScaleFactor); it defaults to `tileScaleFactor` so callers
153
+ // that predate the header/measurement split keep their one-knob behavior.
154
+ export const stampTileDims = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1, headerTileScaleFactor = tileScaleFactor) => {
141
155
  if (!m.isGroupHeader) {
156
+ const k = (m.scale ?? 1) * tileScaleFactor * viewportScale;
142
157
  return { width: STAMP_TILE_SIZE * k, height: STAMP_TILE_SIZE * k };
143
158
  }
159
+ const k = (m.scale ?? 1) * headerTileScaleFactor * viewportScale;
144
160
  const base = groupHeaderBaseDims(m.collapsed);
145
161
  return { width: base.width * k, height: base.height * k };
146
162
  };
@@ -51,7 +51,7 @@ export const createPanTool = (options = {}) => ({
51
51
  const measurements = ctx.document.placedMeasurements;
52
52
  for (let i = measurements.length - 1; i >= 0; i--) {
53
53
  const m = measurements[i];
54
- if (hitPlacedMeasurement(m, event.world, zoom, ctx.tileScaleFactor, ctx.tileViewportScale)) {
54
+ if (hitPlacedMeasurement(m, event.world, zoom, ctx.tileScaleFactor, ctx.tileViewportScale, ctx.headerTileScaleFactor)) {
55
55
  ctx.setSelection({ ids: [m.id] });
56
56
  return;
57
57
  }
@@ -66,11 +66,15 @@ const segmentDistanceSq = (px, py, ax, ay, bx, by) => {
66
66
  const findHit = (doc, world, zoom, viewportTileScale = 1,
67
67
  // Platform-resolved tile-scale knob (ctx.tileScaleFactor). Defaults to the
68
68
  // doc's web/legacy field so un-threaded callers keep the pre-split behavior.
69
- tileScaleFactor = doc.tileScaleFactor) => {
69
+ tileScaleFactor = doc.tileScaleFactor,
70
+ // Platform-resolved header-scale knob (ctx.headerTileScaleFactor). Defaults
71
+ // to the doc's web header field, falling back to the measurement knob the
72
+ // way resolveHeaderTileScaleFactor does.
73
+ headerTileScaleFactor = doc.headerTileScaleFactor ?? tileScaleFactor) => {
70
74
  // Hit-test in z-order (top first): measurements > shapes > strokes.
71
75
  for (let i = doc.placedMeasurements.length - 1; i >= 0; i--) {
72
76
  const m = doc.placedMeasurements[i];
73
- if (hitPlacedMeasurement(m, world, zoom, tileScaleFactor, viewportTileScale)) {
77
+ if (hitPlacedMeasurement(m, world, zoom, tileScaleFactor, viewportTileScale, headerTileScaleFactor)) {
74
78
  return { id: m.id, kind: 'measurement' };
75
79
  }
76
80
  }
@@ -179,11 +183,11 @@ const translatePatch = (elementKind, id, doc, delta) => {
179
183
  // is selected; without this, a handle sitting under the tile (a rectangle
180
184
  // tile at the rect center, a line tile slid onto an endpoint) hijacks the grab
181
185
  // and the tile becomes unmovable until deselected.
182
- const isOnMeasurementTile = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor) => {
186
+ const isOnMeasurementTile = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor, headerTileScaleFactor = doc.headerTileScaleFactor ?? tileScaleFactor) => {
183
187
  const m = doc.placedMeasurements.find((x) => x.id === id);
184
188
  if (!m)
185
189
  return false;
186
- const dims = stampTileDims(m, tileScaleFactor, viewportTileScale);
190
+ const dims = stampTileDims(m, tileScaleFactor, viewportTileScale, headerTileScaleFactor);
187
191
  const halfW = (dims.width / 2 + HIT_PADDING) / zoom;
188
192
  const halfH = (dims.height / 2 + HIT_PADDING) / zoom;
189
193
  return (Math.abs(world.x - m.anchor.x) <= halfW &&
@@ -193,14 +197,14 @@ const isOnMeasurementTile = (doc, id, world, zoom, viewportTileScale = 1, tileSc
193
197
  // via DragSelectionConfig AND the web pointer handlers — one source of truth) ---
194
198
  // Grabbing the tile of a line annotation slides it along the line; everything
195
199
  // else (bare stamps, grabs on the line body) is a group move.
196
- const classifyGrab = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor) => {
200
+ const classifyGrab = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor, headerTileScaleFactor = doc.headerTileScaleFactor ?? tileScaleFactor) => {
197
201
  const m = doc.placedMeasurements.find((x) => x.id === id);
198
202
  if (!m)
199
203
  return 'move';
200
204
  // Same footprint the tile is drawn at (wide pill for group headers; folds in
201
205
  // the document-wide tile scale + viewport scale so slide-grab matches the
202
206
  // draw).
203
- const dims = stampTileDims(m, tileScaleFactor, viewportTileScale);
207
+ const dims = stampTileDims(m, tileScaleFactor, viewportTileScale, headerTileScaleFactor);
204
208
  const halfW = (dims.width / 2 + HIT_PADDING) / zoom;
205
209
  const halfH = (dims.height / 2 + HIT_PADDING) / zoom;
206
210
  const onTile = Math.abs(world.x - m.anchor.x) <= halfW &&
@@ -600,7 +604,7 @@ export const createSelectTool = (options = {}) => ({
600
604
  // reusing the same hit-test and translate logic the pointer handlers below
601
605
  // use for web — one source of truth.
602
606
  dragSelection: {
603
- hitTest: (doc, world, zoom, viewportTileScale, tileScaleFactor) => findHit(doc, world, zoom, viewportTileScale, tileScaleFactor),
607
+ hitTest: (doc, world, zoom, viewportTileScale, tileScaleFactor, headerTileScaleFactor) => findHit(doc, world, zoom, viewportTileScale, tileScaleFactor, headerTileScaleFactor),
604
608
  buildTranslatePatch: (doc, id, kind, delta) => {
605
609
  const op = translatePatch(kind, id, doc, delta);
606
610
  return op ? { ops: [op] } : null;
@@ -639,7 +643,7 @@ export const createSelectTool = (options = {}) => ({
639
643
  // tile stays draggable (otherwise it can only be moved after deselecting).
640
644
  const selId = ctx.selection?.ids[0];
641
645
  if (selId &&
642
- !isOnMeasurementTile(ctx.document, selId, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor)) {
646
+ !isOnMeasurementTile(ctx.document, selId, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor, ctx.headerTileScaleFactor)) {
643
647
  const handle = findHandleHit(ctx.document, selId, world, zoom);
644
648
  if (handle) {
645
649
  ctx.setSelection({ ids: [selId] });
@@ -708,7 +712,7 @@ export const createSelectTool = (options = {}) => ({
708
712
  };
709
713
  }
710
714
  }
711
- const hit = findHit(ctx.document, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor);
715
+ const hit = findHit(ctx.document, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor, ctx.headerTileScaleFactor);
712
716
  if (!hit) {
713
717
  // Background layers only get a look-in when every annotation element
714
718
  // misses — they can cover the whole document, so elements above always
@@ -753,7 +757,7 @@ export const createSelectTool = (options = {}) => ({
753
757
  : null;
754
758
  ctx.setSelection({ ids: [hit.id] });
755
759
  const mode = hit.kind === 'measurement' &&
756
- classifyGrab(ctx.document, hit.id, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor) === 'slide'
760
+ classifyGrab(ctx.document, hit.id, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor, ctx.headerTileScaleFactor) === 'slide'
757
761
  ? 'slide'
758
762
  : 'move';
759
763
  return {
@@ -24,6 +24,7 @@ export interface AnnotationCanvasHandle {
24
24
  durationMs?: number;
25
25
  }): void;
26
26
  getTileScaleFactor(): number;
27
+ getHeaderTileScaleFactor(): number;
27
28
  getTileDocSize(factor: number): number;
28
29
  ensureMeasurementVisible(id: AnnotationElementId): void;
29
30
  placeMeasurementAtCenter(ref: MeasurementRef): void;
@@ -84,6 +85,7 @@ export interface AnnotationCanvasStateApi {
84
85
  ctx: ToolContext;
85
86
  tileViewportScale: number;
86
87
  tileScaleFactor: number;
88
+ headerTileScaleFactor: number;
87
89
  penDrawingStroke: AnnotationStroke | null;
88
90
  customPreviewState: ToolState;
89
91
  dispatchPointerDown(event: CanvasPointerEvent): void;
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { applyPatch, invertPatch, DEFAULT_LAYER_ID, } from '../../types/annotation.js';
3
- import { resolveTileScaleFactor, stampTileDims, STAMP_TILE_SIZE, tileDocSizeForFactor, tileScaleFromDocSize, } from './stampLayout.js';
3
+ import { resolveHeaderTileScaleFactor, resolveTileScaleFactor, stampTileDims, STAMP_TILE_SIZE, tileDocSizeForFactor, tileScaleFromDocSize, } from './stampLayout.js';
4
4
  import { createViewportApi, fitRectToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
5
5
  import { backgroundLayerDocRect, backgroundLayersOf, backgroundLayersUnionRect, } from './backgroundLayers.js';
6
6
  import { buildRemoveMeasurementOps, recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
@@ -157,13 +157,28 @@ export const useAnnotationCanvasState = (props) => {
157
157
  // commit writes tileScaleFactorMobile and takes over for good.
158
158
  const tileScalePlatformResolved = tileScalePlatform ?? 'web';
159
159
  let tileScaleFactor = resolveTileScaleFactor(effectiveCanvas, tileScalePlatformResolved);
160
+ // Group/form header chips carry their own document-wide knob (Asana
161
+ // 1217570500895543), resolved the same way. With no header knob set it
162
+ // equals the measurement factor (resolveHeaderTileScaleFactor's fallback) —
163
+ // the pre-split one-knob behavior.
164
+ let headerTileScaleFactor = resolveHeaderTileScaleFactor(effectiveCanvas, tileScalePlatformResolved);
160
165
  if (tileScalePlatformResolved === 'mobile' &&
161
166
  effectiveCanvas.tileScaleFactorMobile == null &&
162
167
  effectiveCanvas.tileDocSize != null &&
163
168
  effectiveCanvas.tileDocSize > 0 &&
164
169
  width > 0 &&
165
170
  height > 0) {
166
- tileScaleFactor = tileScaleFromDocSize(effectiveCanvas.tileDocSize, baseFitZoom);
171
+ const calibrated = tileScaleFromDocSize(effectiveCanvas.tileDocSize, baseFitZoom);
172
+ // Scale the header factor by the same calibration ratio so its relation
173
+ // to the measurement factor survives the override: a doc with no header
174
+ // knob keeps headers exactly at the calibrated measurement factor, and a
175
+ // doc whose web user set a header knob keeps the same header-to-tile
176
+ // proportion the web user saw.
177
+ headerTileScaleFactor =
178
+ tileScaleFactor > 0
179
+ ? headerTileScaleFactor * (calibrated / tileScaleFactor)
180
+ : calibrated;
181
+ tileScaleFactor = calibrated;
167
182
  }
168
183
  const ctx = useMemo(() => ({
169
184
  document: canvas,
@@ -171,6 +186,7 @@ export const useAnnotationCanvasState = (props) => {
171
186
  viewport: viewportApi,
172
187
  tileViewportScale,
173
188
  tileScaleFactor,
189
+ headerTileScaleFactor,
174
190
  preview(patch) {
175
191
  setPreviewPatch(patch);
176
192
  },
@@ -222,6 +238,7 @@ export const useAnnotationCanvasState = (props) => {
222
238
  viewportApi,
223
239
  tileViewportScale,
224
240
  tileScaleFactor,
241
+ headerTileScaleFactor,
225
242
  tileScalePlatformResolved,
226
243
  baseFitZoom,
227
244
  onCommit,
@@ -425,6 +442,9 @@ export const useAnnotationCanvasState = (props) => {
425
442
  getTileScaleFactor() {
426
443
  return ctxRef.current.tileScaleFactor;
427
444
  },
445
+ getHeaderTileScaleFactor() {
446
+ return ctxRef.current.headerTileScaleFactor;
447
+ },
428
448
  getTileDocSize(factor) {
429
449
  // Tiles are doc-constant, so the doc size a factor yields derives
430
450
  // from the content-fit reference, not the current zoom.
@@ -437,7 +457,7 @@ export const useAnnotationCanvasState = (props) => {
437
457
  return;
438
458
  // The tile's CURRENT on-screen half-extents: stampTileDims with the
439
459
  // zoom-relative tileViewportScale folded in.
440
- const dims = stampTileDims(m, c.tileScaleFactor, c.tileViewportScale);
460
+ const dims = stampTileDims(m, c.tileScaleFactor, c.tileViewportScale, c.headerTileScaleFactor);
441
461
  const halfW = dims.width / 2;
442
462
  const halfH = dims.height / 2;
443
463
  const s = c.viewport.worldToScreen(m.anchor);
@@ -734,6 +754,7 @@ export const useAnnotationCanvasState = (props) => {
734
754
  ctx,
735
755
  tileViewportScale,
736
756
  tileScaleFactor,
757
+ headerTileScaleFactor,
737
758
  penDrawingStroke,
738
759
  customPreviewState: toolState,
739
760
  dispatchPointerDown,
@@ -194,6 +194,13 @@ export interface CalculatorDefinition {
194
194
  diagramFileId?: string | null;
195
195
  /** Public calculators are readable by everyone; rules/query key. */
196
196
  isPublic: boolean;
197
+ sharedUserIds?: string[];
198
+ /**
199
+ * Orgs this calculator is shared with. Written only by the backend
200
+ * shareCalculatorWithOrg / unshareCalculatorWithOrg callables, which also
201
+ * maintain the organizations/{orgId}/sharedCalculators pointer docs clients
202
+ * use for discovery (rules can't authorize an array-contains query here).
203
+ */
197
204
  sharedOrgIds?: string[];
198
205
  createdBy?: {
199
206
  userId: string;
package/dist/exports.d.ts CHANGED
@@ -29,7 +29,7 @@ export type { GestureConfig, PanTrigger, AnnotationCanvasInnerProps, } from './a
29
29
  export type { CanvasPointerEvent, RequestTextInput, ShapeDrawConfig, Tool, ToolContext, ToolState, } from './annotation/canvas/Tool.js';
30
30
  export type { MeasurementRef, PickMeasurement, } from './annotation/canvas/measurementPicker.js';
31
31
  export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './annotation/canvas/measurementStampOverlay.js';
32
- export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
32
+ export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
33
33
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, type GroupHeaderBadge, type GroupHeaderDescriptor, } from './annotation/canvas/groupHeaderLayout.js';
34
34
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
35
35
  export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
package/dist/exports.js CHANGED
@@ -34,7 +34,7 @@ export { useAnnotationMutations, } from './annotation/data/hooks/useAnnotationMu
34
34
  export { useAnnotationCanvasDoc, } from './annotation/data/hooks/useAnnotationCanvasDoc.js';
35
35
  export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
36
36
  export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
37
- export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
37
+ export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
38
38
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, } from './annotation/canvas/groupHeaderLayout.js';
39
39
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
40
40
  export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
@@ -0,0 +1,3 @@
1
+ export * from './colors.js';
2
+ export * from './semantic.js';
3
+ export * from './preset.js';