@reekon-tools/boldr-utils 1.14.2 → 1.15.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 (84) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.js +2 -2
  2. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +40 -17
  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 +4 -4
  6. package/dist/annotation/canvas/measurementPicker.d.ts +5 -0
  7. package/dist/annotation/canvas/stampLayout.d.ts +4 -2
  8. package/dist/annotation/canvas/stampLayout.js +29 -8
  9. package/dist/annotation/canvas/tools/panTool.js +1 -1
  10. package/dist/annotation/canvas/tools/selectTool.js +10 -10
  11. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +4 -1
  12. package/dist/annotation/canvas/useAnnotationCanvasState.js +73 -2
  13. package/dist/exports.d.ts +2 -2
  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 +5 -1
  22. package/dist/types/annotation.js +11 -1
  23. package/dist/types/firestore.d.ts +0 -2
  24. package/package.json +1 -1
  25. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  26. package/dist/canvas/AnnotationCanvas.js +0 -10
  27. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  28. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  29. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  30. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  31. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  32. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  33. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  34. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  35. package/dist/canvas/Tool.d.ts +0 -38
  36. package/dist/canvas/Tool.js +0 -1
  37. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  38. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  39. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  40. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  41. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  42. package/dist/canvas/elements/ShapeElement.js +0 -62
  43. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  44. package/dist/canvas/elements/StrokeElement.js +0 -18
  45. package/dist/canvas/measurementPicker.d.ts +0 -10
  46. package/dist/canvas/measurementPicker.js +0 -1
  47. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  48. package/dist/canvas/measurementStampOverlay.js +0 -1
  49. package/dist/canvas/pointerAdapter.d.ts +0 -3
  50. package/dist/canvas/pointerAdapter.js +0 -19
  51. package/dist/canvas/stampLayout.d.ts +0 -5
  52. package/dist/canvas/stampLayout.js +0 -14
  53. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  54. package/dist/canvas/tools/measurementStampTool.js +0 -37
  55. package/dist/canvas/tools/panTool.d.ts +0 -5
  56. package/dist/canvas/tools/panTool.js +0 -25
  57. package/dist/canvas/tools/penTool.d.ts +0 -13
  58. package/dist/canvas/tools/penTool.js +0 -68
  59. package/dist/canvas/tools/selectTool.d.ts +0 -2
  60. package/dist/canvas/tools/selectTool.js +0 -182
  61. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  62. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  63. package/dist/canvas/viewport.d.ts +0 -16
  64. package/dist/canvas/viewport.js +0 -54
  65. package/dist/data/AnnotationDataContext.d.ts +0 -8
  66. package/dist/data/AnnotationDataContext.js +0 -11
  67. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  68. package/dist/data/AnnotationDataProvider.js +0 -4
  69. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  70. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  71. package/dist/data/canvasPersistence.d.ts +0 -3
  72. package/dist/data/canvasPersistence.js +0 -26
  73. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  74. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  75. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  76. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  77. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  78. package/dist/data/hooks/useAnnotationList.js +0 -26
  79. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  80. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  81. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  82. package/dist/hooks/useParseMeasurement.js +0 -14
  83. package/dist/utils/evaluateFormula.d.ts +0 -20
  84. 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, state.headerTileScaleFactor));
287
+ .find((m) => hitPlacedMeasurement(m, world, state.viewport.zoom, state.tileScaleFactor, state.tileViewportScale, state.headerTileScaleFactor, state.fileTileScaleFactor));
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, state.headerTileScaleFactor);
323
+ const { width, height } = stampTileDims(placed, state.tileScaleFactor, state.tileViewportScale, state.headerTileScaleFactor, state.fileTileScaleFactor);
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;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Group, Line, Rect, RoundedRect, Skia, rect, rrect, useFont, } from '@shopify/react-native-skia';
3
- import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
3
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
4
4
  import { StyleSheet, TouchableOpacity, View, } from 'react-native';
5
5
  import { Gesture, GestureDetector, GestureHandlerRootView, } from 'react-native-gesture-handler';
6
6
  import Animated, { runOnJS, runOnUI, useAnimatedStyle, useDerivedValue, useSharedValue, } from 'react-native-reanimated';
@@ -875,7 +875,7 @@ export const AnnotationCanvasInner = (props) => {
875
875
  return;
876
876
  const world = st.ctx.viewport.screenToWorld(screen);
877
877
  const zoomNow = st.ctx.viewport.state.zoom;
878
- const hit = dragSelection.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor);
878
+ const hit = dragSelection.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor, st.ctx.fileTileScaleFactor);
879
879
  if (!hit || hit.kind !== 'measurement' || hit.id !== prevSelectedId) {
880
880
  return;
881
881
  }
@@ -1193,7 +1193,7 @@ export const AnnotationCanvasInner = (props) => {
1193
1193
  // so a selected tile isn't stuck until you deselect it).
1194
1194
  const selId = st.ctx.selection?.ids[0];
1195
1195
  if (selId &&
1196
- !cfg.isSelectedTileGrab?.(st.ctx.document, selId, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor)) {
1196
+ !cfg.isSelectedTileGrab?.(st.ctx.document, selId, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor, st.ctx.fileTileScaleFactor)) {
1197
1197
  const handle = cfg.hitTestHandle?.(st.ctx.document, selId, world, zoomNow);
1198
1198
  if (handle) {
1199
1199
  const m = st.ctx.document.placedMeasurements.find((x) => x.id === selId);
@@ -1293,7 +1293,7 @@ export const AnnotationCanvasInner = (props) => {
1293
1293
  return;
1294
1294
  }
1295
1295
  }
1296
- const hit = cfg.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor);
1296
+ const hit = cfg.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor, st.ctx.fileTileScaleFactor);
1297
1297
  if (!hit) {
1298
1298
  // Backgrounds can cover the whole document, so they only get the
1299
1299
  // drag once every annotation element misses, and only when ALREADY
@@ -1313,7 +1313,7 @@ export const AnnotationCanvasInner = (props) => {
1313
1313
  st.ctx.setSelection({ ids: [hit.id] });
1314
1314
  // Grabbing a line annotation's tile slides it; otherwise group-move.
1315
1315
  const grab = hit.kind === 'measurement'
1316
- ? cfg.classifyMeasurementGrab?.(st.ctx.document, hit.id, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor)
1316
+ ? cfg.classifyMeasurementGrab?.(st.ctx.document, hit.id, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor, st.ctx.fileTileScaleFactor)
1317
1317
  : 'move';
1318
1318
  if (grab === 'slide') {
1319
1319
  const m = st.ctx.document.placedMeasurements.find((x) => x.id === hit.id);
@@ -1621,7 +1621,7 @@ export const AnnotationCanvasInner = (props) => {
1621
1621
  // non-fullscreen canvas (e.g. a diagram strip) escape into surrounding UI.
1622
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
1623
1623
  ? (state.measurementsById.get(placed.measurementId) ?? null)
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
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, fileTileScaleFactor: state.fileTileScaleFactor, tileViewportScale: state.tileViewportScale, onStampPress: onMeasurementStampPress, onStampLongPress: onMeasurementStampLongPress, onRemove: onMeasurementStampRemove
1625
1625
  ? () => {
1626
1626
  const defaultRemove = () => {
1627
1627
  const { ops, keepSelection } = buildRemoveMeasurementOps(placed);
@@ -1637,10 +1637,10 @@ export const AnnotationCanvasInner = (props) => {
1637
1637
  wrapContent: loupeWrap,
1638
1638
  }) }))] }));
1639
1639
  };
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, }) => {
1640
+ const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, rectCtx, renderMeasurementStamp, tileScaleFactor, headerTileScaleFactor, fileTileScaleFactor, tileViewportScale, onStampPress, onStampLongPress, onRemove, }) => {
1641
1641
  // Square tile or wide group-header pill — one footprint source of truth
1642
1642
  // shared with the hit-test (stampTileDims).
1643
- const { width, height } = stampTileDims(placed, tileScaleFactor, tileViewportScale, headerTileScaleFactor ?? tileScaleFactor);
1643
+ const { width, height } = stampTileDims(placed, tileScaleFactor, tileViewportScale, headerTileScaleFactor ?? tileScaleFactor, fileTileScaleFactor ?? tileScaleFactor);
1644
1644
  const halfW = width / 2;
1645
1645
  const halfH = height / 2;
1646
1646
  // Remove-"X" touch target, sized as a fraction of the tile and corner-pinned
@@ -1659,13 +1659,36 @@ const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging,
1659
1659
  // in the same render the new anchor arrives — no flicker.
1660
1660
  //
1661
1661
  // Size: the LAID-OUT box is the tile's screen size at `zoomSnapshot` (the
1662
- // JS viewport this render was built from). Tiles scale with zoom, so while
1663
- // a pinch/glide has the live zoom ahead of that snapshot the worklet
1664
- // bridges the gap with a scale transform (about the centered anchor);
1665
- // when the gesture ends and the JS snapshot syncs, the item re-renders at
1666
- // the final size and the scale settles back to exactly 1 the same
1667
- // captured-prop handoff the drag offsets use, so there is no jump.
1662
+ // JS viewport this render was built from) a real re-layout, which is what
1663
+ // keeps the tile crisp: iOS rasterizes a view at its laid-out size, so a
1664
+ // box magnified by a transform alone blurs at 3× (tried; illegible). Tiles
1665
+ // scale with zoom, so while a pinch/glide has the live zoom ahead of that
1666
+ // snapshot the worklet bridges the gap with a scale transform (about the
1667
+ // centered anchor); when the gesture ends and the JS snapshot syncs, the
1668
+ // item re-renders at the final size and the scale settles back to 1.
1669
+ //
1670
+ // That settle MUST land in the same frame as the re-layout, and captured
1671
+ // props can't do it: reanimated re-evaluates a useAnimatedStyle whose
1672
+ // captured values changed from a PASSIVE effect — a later task, after
1673
+ // React's commit has already mounted the new layout — so every pinch
1674
+ // release drew one frame of the new, bigger box under the old, still-
1675
+ // magnifying transform and the whole canvas flashed (Asana
1676
+ // 1217570500895545 QA). The layout-dependent inputs therefore travel as
1677
+ // SHARED VALUES written from useLayoutEffect: that runs inside the commit
1678
+ // task, right behind the mount dispatch, and mapper runs on the UI runtime
1679
+ // are microtasks, so the transform update lands in the same main-queue
1680
+ // drain as the new layout — before Core Animation commits the frame.
1681
+ // (`dragging`/`sliding` stay captured: the anchor they gate changes in the
1682
+ // same render and doesn't touch layout, so both lag together, harmlessly.)
1668
1683
  const zoomBase = zoomSnapshot > 0 ? zoomSnapshot : 1;
1684
+ const zoomBaseSv = useSharedValue(zoomBase);
1685
+ const halfWSv = useSharedValue(halfW);
1686
+ const halfHSv = useSharedValue(halfH);
1687
+ useLayoutEffect(() => {
1688
+ zoomBaseSv.value = zoomBase;
1689
+ halfWSv.value = halfW;
1690
+ halfHSv.value = halfH;
1691
+ }, [zoomBase, halfW, halfH, zoomBaseSv, halfWSv, halfHSv]);
1669
1692
  const animatedStyle = useAnimatedStyle(() => {
1670
1693
  'worklet';
1671
1694
  let worldX = anchorX;
@@ -1714,11 +1737,11 @@ const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging,
1714
1737
  const cy = (worldY - panY.value) * zoom.value;
1715
1738
  return {
1716
1739
  transform: [
1717
- { translateX: cx - halfW },
1718
- { translateY: cy - halfH },
1740
+ { translateX: cx - halfWSv.value },
1741
+ { translateY: cy - halfHSv.value },
1719
1742
  // Scales about the box center, which the translate just pinned to
1720
1743
  // the anchor — so the tile grows/shrinks in place.
1721
- { scale: zoom.value / zoomBase },
1744
+ { scale: zoom.value / zoomBaseSv.value },
1722
1745
  ],
1723
1746
  };
1724
1747
  });
@@ -25,6 +25,7 @@ export interface ToolContext {
25
25
  tileViewportScale: number;
26
26
  tileScaleFactor: number;
27
27
  headerTileScaleFactor: number;
28
+ fileTileScaleFactor: number;
28
29
  preview(patch: AnnotationDocumentPatch): void;
29
30
  commit(patch: AnnotationDocumentPatch): void;
30
31
  setSelection(selection: Selection | null): void;
@@ -55,12 +56,12 @@ export interface ShapeDrawConfig {
55
56
  }
56
57
  export type DragElementKind = 'stroke' | 'shape' | 'measurement' | 'background';
57
58
  export interface DragSelectionConfig {
58
- hitTest(doc: AnnotationCanvasState, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number, headerTileScaleFactor?: number): {
59
+ hitTest(doc: AnnotationCanvasState, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number, headerTileScaleFactor?: number, fileTileScaleFactor?: number): {
59
60
  id: AnnotationElementId;
60
61
  kind: DragElementKind;
61
62
  } | null;
62
63
  buildTranslatePatch(doc: AnnotationCanvasState, id: AnnotationElementId, kind: DragElementKind, delta: Vec2): AnnotationDocumentPatch | null;
63
- classifyMeasurementGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number, headerTileScaleFactor?: number): 'slide' | 'move';
64
+ classifyMeasurementGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number, headerTileScaleFactor?: number, fileTileScaleFactor?: number): 'slide' | 'move';
64
65
  buildSlidePatch?(doc: AnnotationCanvasState, id: AnnotationElementId, delta: Vec2, zoom: number): AnnotationDocumentPatch | null;
65
66
  hitTestHandle?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): 'a' | 'b' | null;
66
67
  buildEndpointPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, handle: 'a' | 'b', delta: Vec2): AnnotationDocumentPatch | null;
@@ -78,7 +79,7 @@ export interface DragSelectionConfig {
78
79
  fixed: Vec2;
79
80
  } | null;
80
81
  buildShapeCornerPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, corner: RectCorner, delta: Vec2): AnnotationDocumentPatch | null;
81
- isSelectedTileGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number, headerTileScaleFactor?: number): boolean;
82
+ isSelectedTileGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number, headerTileScaleFactor?: number, fileTileScaleFactor?: number): boolean;
82
83
  hitTestBackground?(doc: AnnotationCanvasState, world: Vec2): {
83
84
  id: AnnotationElementId;
84
85
  } | 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, headerTileScaleFactor?: number) => boolean;
39
+ export declare const hitPlacedMeasurement: (m: PlacedMeasurementRef, p: Vec2, zoom?: number, tileScaleFactor?: number, viewportScale?: number, headerTileScaleFactor?: number, fileTileScaleFactor?: number) => boolean;
40
40
  export interface RemoveMeasurementResult {
41
41
  ops: AnnotationPatchOp[];
42
42
  keepSelection: boolean;
@@ -110,15 +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, headerTileScaleFactor = tileScaleFactor) => {
113
+ export const hitPlacedMeasurement = (m, p, zoom = 1, tileScaleFactor = 1, viewportScale = 1, headerTileScaleFactor = tileScaleFactor, fileTileScaleFactor = 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
- // sized by their OWN document-wide knob, `headerTileScaleFactor` — see
118
+ // stamps share the square footprint; group-header chips are a wide pill and
119
+ // object tiles a square, each sized by its OWN document-wide knob — see
120
120
  // stampTileDims in stampLayout).
121
- const dims = stampTileDims(m, tileScaleFactor, viewportScale, headerTileScaleFactor);
121
+ const dims = stampTileDims(m, tileScaleFactor, viewportScale, headerTileScaleFactor, fileTileScaleFactor);
122
122
  const halfW = (dims.width / 2 + STAMP_HIT_PADDING) / zoom;
123
123
  const halfH = (dims.height / 2 + STAMP_HIT_PADDING) / zoom;
124
124
  const dx = Math.abs(p.x - m.anchor.x);
@@ -8,3 +8,8 @@ export interface MeasurementRef {
8
8
  unit?: Units;
9
9
  }
10
10
  export type PickMeasurement = () => Promise<MeasurementRef | null>;
11
+ export interface FileRef {
12
+ fileId: string;
13
+ filePath: string;
14
+ groupId: string;
15
+ }
@@ -20,13 +20,15 @@ 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
22
  export declare const resolveHeaderTileScaleFactor: (canvas: Pick<AnnotationCanvasState, "tileScaleFactor" | "tileScaleFactorMobile" | "headerTileScaleFactor" | "headerTileScaleFactorMobile">, platform: TileScalePlatform) => number;
23
+ export declare const resolveFileTileScaleFactor: (canvas: Pick<AnnotationCanvasState, "tileScaleFactor" | "tileScaleFactorMobile" | "fileTileScaleFactor" | "fileTileScaleFactorMobile">, platform: TileScalePlatform) => number;
23
24
  export declare const clampTileScale: (v: number) => number;
24
25
  export declare const tileDocSizeForFactor: (factor: number, zoom: number) => number;
25
26
  export declare const tileScaleFromDocSize: (docSize: number, zoom: number) => number;
26
- export declare const isUnassociatedStamp: (m: Pick<PlacedMeasurementRef, "measurementId" | "measurementPath" | "columnId" | "isGroupHeader">) => boolean;
27
+ export declare const isFileStamp: (m: Pick<PlacedMeasurementRef, "fileId">) => boolean;
28
+ export declare const isUnassociatedStamp: (m: Pick<PlacedMeasurementRef, "measurementId" | "measurementPath" | "columnId" | "isGroupHeader" | "fileId">) => boolean;
27
29
  export declare const stampTileSize: (m: Pick<PlacedMeasurementRef, "scale">, tileScaleFactor?: number, viewportScale?: number) => number;
28
30
  export interface StampTileDims {
29
31
  width: number;
30
32
  height: number;
31
33
  }
32
- export declare const stampTileDims: (m: Pick<PlacedMeasurementRef, "scale" | "isGroupHeader" | "collapsed">, tileScaleFactor?: number, viewportScale?: number, headerTileScaleFactor?: number) => StampTileDims;
34
+ export declare const stampTileDims: (m: Pick<PlacedMeasurementRef, "scale" | "isGroupHeader" | "collapsed" | "fileId">, tileScaleFactor?: number, viewportScale?: number, headerTileScaleFactor?: number, fileTileScaleFactor?: number) => StampTileDims;
@@ -90,8 +90,15 @@ export const resolveTileScaleFactor = (canvas, platform) => (platform === 'mobil
90
90
  // resolve through here so they agree.
91
91
  export const resolveHeaderTileScaleFactor = (canvas, platform) => (platform === 'mobile'
92
92
  ? (canvas.headerTileScaleFactorMobile ?? canvas.headerTileScaleFactor)
93
- : canvas.headerTileScaleFactor) ??
94
- resolveTileScaleFactor(canvas, platform);
93
+ : canvas.headerTileScaleFactor) ?? resolveTileScaleFactor(canvas, platform);
94
+ // Resolve the document-wide OBJECT-tile-scale knob for a platform — the third
95
+ // knob (Asana 1217570500895545), resolved exactly like the header knob: own
96
+ // platform value → web value → the MEASUREMENT factor, so an object tile
97
+ // placed before its knob was ever moved matches the measurement tiles beside
98
+ // it. The ONE read path for the file fields.
99
+ export const resolveFileTileScaleFactor = (canvas, platform) => (platform === 'mobile'
100
+ ? (canvas.fileTileScaleFactorMobile ?? canvas.fileTileScaleFactor)
101
+ : canvas.fileTileScaleFactor) ?? resolveTileScaleFactor(canvas, platform);
95
102
  // Clamp a tile-scale-factor candidate to the supported range. The single guard
96
103
  // for the value before it lands in the document (slider input, restored docs).
97
104
  export const clampTileScale = (v) => v < TILE_SCALE_MIN ? TILE_SCALE_MIN : v > TILE_SCALE_MAX ? TILE_SCALE_MAX : v;
@@ -130,12 +137,21 @@ export const tileScaleFromDocSize = (docSize, zoom) => (docSize * zoom) / STAMP_
130
137
  // (An even earlier per-canvas auto-multiplier — tile size derived from the
131
138
  // canvas's pixel size — was retired for the user-driven "Tile size" slider,
132
139
  // which remains: the slider and the zoom multiplier compose.)
140
+ // A placed element is an OBJECT tile (a group file placed from the picker's
141
+ // object grid) when it carries a `fileId` — the one field measurement, column
142
+ // and header tiles never set. The single discriminant every host and engine
143
+ // path should branch on, so the check can't drift between them.
144
+ export const isFileStamp = (m) => !!m.fileId;
133
145
  // A placed measurement is an unassociated input until a measurement reference
134
146
  // is attached (id or path) OR it is bound to a form column (`columnId`). Such
135
147
  // stamps render as the blank "+" placeholder (same footprint as a populated
136
- // tile — see STAMP_TILE_SIZE). Group-header chips carry neither ref but are
137
- // NOT inputs — never treat them as blank "+" placeholders.
138
- export const isUnassociatedStamp = (m) => !m.isGroupHeader && !m.measurementId && !m.measurementPath && !m.columnId;
148
+ // tile — see STAMP_TILE_SIZE). Group-header chips and object tiles carry
149
+ // neither ref but are NOT inputs — never treat them as blank "+" placeholders.
150
+ export const isUnassociatedStamp = (m) => !m.isGroupHeader &&
151
+ !isFileStamp(m) &&
152
+ !m.measurementId &&
153
+ !m.measurementPath &&
154
+ !m.columnId;
139
155
  // Screen-space edge length for a placed stamp: the base size scaled by the
140
156
  // per-stamp `scale`, the document-wide `tileScaleFactor`, and the
141
157
  // zoom-relative `viewportScale`. Blank and populated stamps share the
@@ -149,9 +165,14 @@ export const isUnassociatedStamp = (m) => !m.isGroupHeader && !m.measurementId &
149
165
  // with the drawing as the user zooms.
150
166
  export const stampTileSize = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1) => STAMP_TILE_SIZE * (m.scale ?? 1) * tileScaleFactor * viewportScale;
151
167
  // `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) => {
168
+ // (resolveHeaderTileScaleFactor) and `fileTileScaleFactor` the object tiles'
169
+ // (resolveFileTileScaleFactor); both default to `tileScaleFactor` so callers
170
+ // that predate the per-kind split keep their one-knob behavior.
171
+ export const stampTileDims = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1, headerTileScaleFactor = tileScaleFactor, fileTileScaleFactor = tileScaleFactor) => {
172
+ if (isFileStamp(m)) {
173
+ const k = (m.scale ?? 1) * fileTileScaleFactor * viewportScale;
174
+ return { width: STAMP_TILE_SIZE * k, height: STAMP_TILE_SIZE * k };
175
+ }
155
176
  if (!m.isGroupHeader) {
156
177
  const k = (m.scale ?? 1) * tileScaleFactor * viewportScale;
157
178
  return { width: STAMP_TILE_SIZE * k, height: STAMP_TILE_SIZE * k };
@@ -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, ctx.headerTileScaleFactor)) {
54
+ if (hitPlacedMeasurement(m, event.world, zoom, ctx.tileScaleFactor, ctx.tileViewportScale, ctx.headerTileScaleFactor, ctx.fileTileScaleFactor)) {
55
55
  ctx.setSelection({ ids: [m.id] });
56
56
  return;
57
57
  }
@@ -70,11 +70,11 @@ tileScaleFactor = doc.tileScaleFactor,
70
70
  // Platform-resolved header-scale knob (ctx.headerTileScaleFactor). Defaults
71
71
  // to the doc's web header field, falling back to the measurement knob the
72
72
  // way resolveHeaderTileScaleFactor does.
73
- headerTileScaleFactor = doc.headerTileScaleFactor ?? tileScaleFactor) => {
73
+ headerTileScaleFactor = doc.headerTileScaleFactor ?? tileScaleFactor, fileTileScaleFactor = doc.fileTileScaleFactor ?? tileScaleFactor) => {
74
74
  // Hit-test in z-order (top first): measurements > shapes > strokes.
75
75
  for (let i = doc.placedMeasurements.length - 1; i >= 0; i--) {
76
76
  const m = doc.placedMeasurements[i];
77
- if (hitPlacedMeasurement(m, world, zoom, tileScaleFactor, viewportTileScale, headerTileScaleFactor)) {
77
+ if (hitPlacedMeasurement(m, world, zoom, tileScaleFactor, viewportTileScale, headerTileScaleFactor, fileTileScaleFactor)) {
78
78
  return { id: m.id, kind: 'measurement' };
79
79
  }
80
80
  }
@@ -183,11 +183,11 @@ const translatePatch = (elementKind, id, doc, delta) => {
183
183
  // is selected; without this, a handle sitting under the tile (a rectangle
184
184
  // tile at the rect center, a line tile slid onto an endpoint) hijacks the grab
185
185
  // and the tile becomes unmovable until deselected.
186
- const isOnMeasurementTile = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor, headerTileScaleFactor = doc.headerTileScaleFactor ?? tileScaleFactor) => {
186
+ const isOnMeasurementTile = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor, headerTileScaleFactor = doc.headerTileScaleFactor ?? tileScaleFactor, fileTileScaleFactor = doc.fileTileScaleFactor ?? tileScaleFactor) => {
187
187
  const m = doc.placedMeasurements.find((x) => x.id === id);
188
188
  if (!m)
189
189
  return false;
190
- const dims = stampTileDims(m, tileScaleFactor, viewportTileScale, headerTileScaleFactor);
190
+ const dims = stampTileDims(m, tileScaleFactor, viewportTileScale, headerTileScaleFactor, fileTileScaleFactor);
191
191
  const halfW = (dims.width / 2 + HIT_PADDING) / zoom;
192
192
  const halfH = (dims.height / 2 + HIT_PADDING) / zoom;
193
193
  return (Math.abs(world.x - m.anchor.x) <= halfW &&
@@ -197,14 +197,14 @@ const isOnMeasurementTile = (doc, id, world, zoom, viewportTileScale = 1, tileSc
197
197
  // via DragSelectionConfig AND the web pointer handlers — one source of truth) ---
198
198
  // Grabbing the tile of a line annotation slides it along the line; everything
199
199
  // else (bare stamps, grabs on the line body) is a group move.
200
- const classifyGrab = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor, headerTileScaleFactor = doc.headerTileScaleFactor ?? tileScaleFactor) => {
200
+ const classifyGrab = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor, headerTileScaleFactor = doc.headerTileScaleFactor ?? tileScaleFactor, fileTileScaleFactor = doc.fileTileScaleFactor ?? tileScaleFactor) => {
201
201
  const m = doc.placedMeasurements.find((x) => x.id === id);
202
202
  if (!m)
203
203
  return 'move';
204
204
  // Same footprint the tile is drawn at (wide pill for group headers; folds in
205
205
  // the document-wide tile scale + viewport scale so slide-grab matches the
206
206
  // draw).
207
- const dims = stampTileDims(m, tileScaleFactor, viewportTileScale, headerTileScaleFactor);
207
+ const dims = stampTileDims(m, tileScaleFactor, viewportTileScale, headerTileScaleFactor, fileTileScaleFactor);
208
208
  const halfW = (dims.width / 2 + HIT_PADDING) / zoom;
209
209
  const halfH = (dims.height / 2 + HIT_PADDING) / zoom;
210
210
  const onTile = Math.abs(world.x - m.anchor.x) <= halfW &&
@@ -604,7 +604,7 @@ export const createSelectTool = (options = {}) => ({
604
604
  // reusing the same hit-test and translate logic the pointer handlers below
605
605
  // use for web — one source of truth.
606
606
  dragSelection: {
607
- hitTest: (doc, world, zoom, viewportTileScale, tileScaleFactor, headerTileScaleFactor) => findHit(doc, world, zoom, viewportTileScale, tileScaleFactor, headerTileScaleFactor),
607
+ hitTest: (doc, world, zoom, viewportTileScale, tileScaleFactor, headerTileScaleFactor, fileTileScaleFactor) => findHit(doc, world, zoom, viewportTileScale, tileScaleFactor, headerTileScaleFactor, fileTileScaleFactor),
608
608
  buildTranslatePatch: (doc, id, kind, delta) => {
609
609
  const op = translatePatch(kind, id, doc, delta);
610
610
  return op ? { ops: [op] } : null;
@@ -643,7 +643,7 @@ export const createSelectTool = (options = {}) => ({
643
643
  // tile stays draggable (otherwise it can only be moved after deselecting).
644
644
  const selId = ctx.selection?.ids[0];
645
645
  if (selId &&
646
- !isOnMeasurementTile(ctx.document, selId, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor, ctx.headerTileScaleFactor)) {
646
+ !isOnMeasurementTile(ctx.document, selId, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor, ctx.headerTileScaleFactor, ctx.fileTileScaleFactor)) {
647
647
  const handle = findHandleHit(ctx.document, selId, world, zoom);
648
648
  if (handle) {
649
649
  ctx.setSelection({ ids: [selId] });
@@ -712,7 +712,7 @@ export const createSelectTool = (options = {}) => ({
712
712
  };
713
713
  }
714
714
  }
715
- const hit = findHit(ctx.document, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor, ctx.headerTileScaleFactor);
715
+ const hit = findHit(ctx.document, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor, ctx.headerTileScaleFactor, ctx.fileTileScaleFactor);
716
716
  if (!hit) {
717
717
  // Background layers only get a look-in when every annotation element
718
718
  // misses — they can cover the whole document, so elements above always
@@ -757,7 +757,7 @@ export const createSelectTool = (options = {}) => ({
757
757
  : null;
758
758
  ctx.setSelection({ ids: [hit.id] });
759
759
  const mode = hit.kind === 'measurement' &&
760
- classifyGrab(ctx.document, hit.id, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor, ctx.headerTileScaleFactor) === 'slide'
760
+ classifyGrab(ctx.document, hit.id, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor, ctx.headerTileScaleFactor, ctx.fileTileScaleFactor) === 'slide'
761
761
  ? 'slide'
762
762
  : 'move';
763
763
  return {
@@ -1,6 +1,6 @@
1
1
  import type { Measurement } from '../../types/firestore.js';
2
2
  import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationElementId, type AnnotationStroke, type MeasurementPlacement, type Selection, type TileScalePlatform, type Vec2 } from '../../types/annotation.js';
3
- import type { MeasurementRef } from './measurementPicker.js';
3
+ import type { FileRef, MeasurementRef } from './measurementPicker.js';
4
4
  import type { CanvasPointerEvent, RequestTextInput, Tool, ToolContext, ToolState } from './Tool.js';
5
5
  import { type ViewportState } from './viewport.js';
6
6
  export interface CanvasScreenRect {
@@ -25,6 +25,7 @@ export interface AnnotationCanvasHandle {
25
25
  }): void;
26
26
  getTileScaleFactor(): number;
27
27
  getHeaderTileScaleFactor(): number;
28
+ getFileTileScaleFactor(): number;
28
29
  getTileDocSize(factor: number): number;
29
30
  ensureMeasurementVisible(id: AnnotationElementId): void;
30
31
  placeMeasurementAtCenter(ref: MeasurementRef): void;
@@ -47,6 +48,7 @@ export interface AnnotationCanvasHandle {
47
48
  placeGroupHeaderAtCenter(ref: {
48
49
  groupId: string;
49
50
  }): AnnotationElementId;
51
+ placeFileAtCenter(ref: FileRef): AnnotationElementId;
50
52
  setGroupHeaderCollapsed(id: AnnotationElementId, collapsed: boolean): void;
51
53
  deleteSelected(): void;
52
54
  }
@@ -86,6 +88,7 @@ export interface AnnotationCanvasStateApi {
86
88
  tileViewportScale: number;
87
89
  tileScaleFactor: number;
88
90
  headerTileScaleFactor: number;
91
+ fileTileScaleFactor: number;
89
92
  penDrawingStroke: AnnotationStroke | null;
90
93
  customPreviewState: ToolState;
91
94
  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 { resolveHeaderTileScaleFactor, resolveTileScaleFactor, stampTileDims, STAMP_TILE_SIZE, tileDocSizeForFactor, tileScaleFromDocSize, } from './stampLayout.js';
3
+ import { resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, 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';
@@ -162,6 +162,8 @@ export const useAnnotationCanvasState = (props) => {
162
162
  // equals the measurement factor (resolveHeaderTileScaleFactor's fallback) —
163
163
  // the pre-split one-knob behavior.
164
164
  let headerTileScaleFactor = resolveHeaderTileScaleFactor(effectiveCanvas, tileScalePlatformResolved);
165
+ // Object tiles: the third knob, same fallback (Asana 1217570500895545).
166
+ let fileTileScaleFactor = resolveFileTileScaleFactor(effectiveCanvas, tileScalePlatformResolved);
165
167
  if (tileScalePlatformResolved === 'mobile' &&
166
168
  effectiveCanvas.tileScaleFactorMobile == null &&
167
169
  effectiveCanvas.tileDocSize != null &&
@@ -178,6 +180,10 @@ export const useAnnotationCanvasState = (props) => {
178
180
  tileScaleFactor > 0
179
181
  ? headerTileScaleFactor * (calibrated / tileScaleFactor)
180
182
  : calibrated;
183
+ fileTileScaleFactor =
184
+ tileScaleFactor > 0
185
+ ? fileTileScaleFactor * (calibrated / tileScaleFactor)
186
+ : calibrated;
181
187
  tileScaleFactor = calibrated;
182
188
  }
183
189
  const ctx = useMemo(() => ({
@@ -187,6 +193,7 @@ export const useAnnotationCanvasState = (props) => {
187
193
  tileViewportScale,
188
194
  tileScaleFactor,
189
195
  headerTileScaleFactor,
196
+ fileTileScaleFactor,
190
197
  preview(patch) {
191
198
  setPreviewPatch(patch);
192
199
  },
@@ -209,6 +216,47 @@ export const useAnnotationCanvasState = (props) => {
209
216
  };
210
217
  }
211
218
  }
219
+ // The three tile-kind knobs are independent (Asana 1217570500895545):
220
+ // moving the MEASUREMENT slider must not move header chips or object
221
+ // tiles. Their knobs fall back to the measurement factor while unset
222
+ // (back-compat for documents from the one-knob era), so the first
223
+ // measurement-slider commit on this platform latches each unset knob
224
+ // at the value it is drawn at RIGHT NOW — the resolved (and, on
225
+ // mobile, calibration-corrected) factor — and it stops following. Same
226
+ // undo step as the slider commit.
227
+ const measurementKnobOp = effective.ops.find((o) => o.op === 'setTileScaleFactor' &&
228
+ (o.target ?? 'measurement') === 'measurement' &&
229
+ (o.platform ?? 'web') === tileScalePlatformResolved);
230
+ if (measurementKnobOp) {
231
+ const isMobile = tileScalePlatformResolved === 'mobile';
232
+ const headerUnset = isMobile
233
+ ? canvas.headerTileScaleFactorMobile == null
234
+ : canvas.headerTileScaleFactor == null;
235
+ const fileUnset = isMobile
236
+ ? canvas.fileTileScaleFactorMobile == null
237
+ : canvas.fileTileScaleFactor == null;
238
+ const latch = [];
239
+ if (headerUnset) {
240
+ latch.push({
241
+ op: 'setTileScaleFactor',
242
+ value: headerTileScaleFactor,
243
+ platform: tileScalePlatformResolved,
244
+ target: 'header',
245
+ });
246
+ }
247
+ if (fileUnset) {
248
+ latch.push({
249
+ op: 'setTileScaleFactor',
250
+ value: fileTileScaleFactor,
251
+ platform: tileScalePlatformResolved,
252
+ target: 'file',
253
+ });
254
+ }
255
+ // Latches go FIRST so they read the pre-move state; the slider op
256
+ // then only moves the measurement knob.
257
+ if (latch.length)
258
+ effective = { ops: [...latch, ...effective.ops] };
259
+ }
212
260
  const inverse = invertPatch(canvas, effective);
213
261
  undoStackRef.current.push({ forward: effective, inverse });
214
262
  redoStackRef.current = [];
@@ -239,6 +287,7 @@ export const useAnnotationCanvasState = (props) => {
239
287
  tileViewportScale,
240
288
  tileScaleFactor,
241
289
  headerTileScaleFactor,
290
+ fileTileScaleFactor,
242
291
  tileScalePlatformResolved,
243
292
  baseFitZoom,
244
293
  onCommit,
@@ -445,6 +494,9 @@ export const useAnnotationCanvasState = (props) => {
445
494
  getHeaderTileScaleFactor() {
446
495
  return ctxRef.current.headerTileScaleFactor;
447
496
  },
497
+ getFileTileScaleFactor() {
498
+ return ctxRef.current.fileTileScaleFactor;
499
+ },
448
500
  getTileDocSize(factor) {
449
501
  // Tiles are doc-constant, so the doc size a factor yields derives
450
502
  // from the content-fit reference, not the current zoom.
@@ -457,7 +509,7 @@ export const useAnnotationCanvasState = (props) => {
457
509
  return;
458
510
  // The tile's CURRENT on-screen half-extents: stampTileDims with the
459
511
  // zoom-relative tileViewportScale folded in.
460
- const dims = stampTileDims(m, c.tileScaleFactor, c.tileViewportScale, c.headerTileScaleFactor);
512
+ const dims = stampTileDims(m, c.tileScaleFactor, c.tileViewportScale, c.headerTileScaleFactor, c.fileTileScaleFactor);
461
513
  const halfW = dims.width / 2;
462
514
  const halfH = dims.height / 2;
463
515
  const s = c.viewport.worldToScreen(m.anchor);
@@ -681,6 +733,24 @@ export const useAnnotationCanvasState = (props) => {
681
733
  c.setSelection({ ids: [placed.id] });
682
734
  return placed.id;
683
735
  },
736
+ placeFileAtCenter(ref) {
737
+ const c = ctxRef.current;
738
+ const anchor = cascadeCenterWorld();
739
+ // No showLabel/showValue/placement — an object tile is not an input;
740
+ // it renders the file's preview, resolved live by the consumer.
741
+ const placed = {
742
+ id: `file-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`,
743
+ layerId: c.document.layers[0]?.id ?? DEFAULT_LAYER_ID,
744
+ fileId: ref.fileId,
745
+ filePath: ref.filePath,
746
+ groupId: ref.groupId,
747
+ anchor,
748
+ createdAt: Date.now(),
749
+ };
750
+ c.commit({ ops: [{ op: 'addMeasurement', measurement: placed }] });
751
+ c.setSelection({ ids: [placed.id] });
752
+ return placed.id;
753
+ },
684
754
  setGroupHeaderCollapsed(id, collapsed) {
685
755
  const c = ctxRef.current;
686
756
  const target = c.document.placedMeasurements.find((m) => m.id === id);
@@ -755,6 +825,7 @@ export const useAnnotationCanvasState = (props) => {
755
825
  tileViewportScale,
756
826
  tileScaleFactor,
757
827
  headerTileScaleFactor,
828
+ fileTileScaleFactor,
758
829
  penDrawingStroke,
759
830
  customPreviewState: toolState,
760
831
  dispatchPointerDown,
package/dist/exports.d.ts CHANGED
@@ -27,9 +27,9 @@ export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotation
27
27
  export type { AnnotationCanvasHandle } from './annotation/canvas/useAnnotationCanvasState.js';
28
28
  export type { GestureConfig, PanTrigger, AnnotationCanvasInnerProps, } from './annotation/canvas/AnnotationCanvasInner.js';
29
29
  export type { CanvasPointerEvent, RequestTextInput, ShapeDrawConfig, Tool, ToolContext, ToolState, } from './annotation/canvas/Tool.js';
30
- export type { MeasurementRef, PickMeasurement, } from './annotation/canvas/measurementPicker.js';
30
+ export type { FileRef, 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, resolveHeaderTileScaleFactor, 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, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, 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, resolveHeaderTileScaleFactor, 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, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, } 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';
@@ -0,0 +1,3 @@
1
+ export * from './colors.js';
2
+ export * from './semantic.js';
3
+ export * from './preset.js';