@reekon-tools/boldr-utils 1.19.5 → 1.20.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 (88) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.d.ts +2 -0
  2. package/dist/annotation/canvas/AnnotationCanvasInner.js +12 -1
  3. package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +2 -0
  4. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +329 -35
  5. package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +28 -1
  6. package/dist/annotation/canvas/AnnotationCanvasSkia.js +99 -14
  7. package/dist/annotation/canvas/Tool.d.ts +10 -1
  8. package/dist/annotation/canvas/backgroundLayers.d.ts +20 -1
  9. package/dist/annotation/canvas/backgroundLayers.js +94 -3
  10. package/dist/annotation/canvas/elements/BackgroundImageElement.d.ts +2 -1
  11. package/dist/annotation/canvas/elements/BackgroundImageElement.js +10 -2
  12. package/dist/annotation/canvas/fileTileSwap.d.ts +12 -0
  13. package/dist/annotation/canvas/fileTileSwap.js +87 -0
  14. package/dist/annotation/canvas/snapGeometry.d.ts +32 -0
  15. package/dist/annotation/canvas/snapGeometry.js +168 -0
  16. package/dist/annotation/canvas/stampLayout.d.ts +1 -1
  17. package/dist/annotation/canvas/stampLayout.js +10 -8
  18. package/dist/annotation/canvas/tools/measurementTool.js +38 -4
  19. package/dist/annotation/canvas/tools/selectTool.d.ts +18 -0
  20. package/dist/annotation/canvas/tools/selectTool.js +201 -43
  21. package/dist/annotation/canvas/tools/shapeTool.js +27 -5
  22. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +2 -0
  23. package/dist/annotation/canvas/useAnnotationCanvasState.js +19 -2
  24. package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +5 -1
  25. package/dist/exports.d.ts +3 -1
  26. package/dist/exports.js +2 -0
  27. package/dist/types/annotation.d.ts +10 -0
  28. package/package.json +1 -1
  29. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  30. package/dist/canvas/AnnotationCanvas.js +0 -10
  31. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  32. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  33. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  34. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  35. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  36. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  37. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  38. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  39. package/dist/canvas/Tool.d.ts +0 -38
  40. package/dist/canvas/Tool.js +0 -1
  41. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  42. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  43. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  44. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  45. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  46. package/dist/canvas/elements/ShapeElement.js +0 -62
  47. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  48. package/dist/canvas/elements/StrokeElement.js +0 -18
  49. package/dist/canvas/measurementPicker.d.ts +0 -10
  50. package/dist/canvas/measurementPicker.js +0 -1
  51. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  52. package/dist/canvas/measurementStampOverlay.js +0 -1
  53. package/dist/canvas/pointerAdapter.d.ts +0 -3
  54. package/dist/canvas/pointerAdapter.js +0 -19
  55. package/dist/canvas/stampLayout.d.ts +0 -5
  56. package/dist/canvas/stampLayout.js +0 -14
  57. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  58. package/dist/canvas/tools/measurementStampTool.js +0 -37
  59. package/dist/canvas/tools/panTool.d.ts +0 -5
  60. package/dist/canvas/tools/panTool.js +0 -25
  61. package/dist/canvas/tools/penTool.d.ts +0 -13
  62. package/dist/canvas/tools/penTool.js +0 -68
  63. package/dist/canvas/tools/selectTool.d.ts +0 -2
  64. package/dist/canvas/tools/selectTool.js +0 -182
  65. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  66. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  67. package/dist/canvas/viewport.d.ts +0 -16
  68. package/dist/canvas/viewport.js +0 -54
  69. package/dist/data/AnnotationDataContext.d.ts +0 -8
  70. package/dist/data/AnnotationDataContext.js +0 -11
  71. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  72. package/dist/data/AnnotationDataProvider.js +0 -4
  73. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  74. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  75. package/dist/data/canvasPersistence.d.ts +0 -3
  76. package/dist/data/canvasPersistence.js +0 -26
  77. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  78. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  79. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  80. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  81. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  82. package/dist/data/hooks/useAnnotationList.js +0 -26
  83. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  84. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  85. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  86. package/dist/hooks/useParseMeasurement.js +0 -14
  87. package/dist/utils/evaluateFormula.d.ts +0 -20
  88. package/dist/utils/evaluateFormula.js +0 -31
@@ -0,0 +1,168 @@
1
+ // Magnetic snapping for line endpoints (Asana 1217966180270874). While an
2
+ // endpoint of a measurement line (`placement: 'line'`) or a line/arrow shape is
3
+ // being placed or dragged, it snaps to nearby geometry: other lines' endpoints
4
+ // and midpoints, polygon/rect vertices, and measurement-rectangle corners.
5
+ // Skia-free, safe in tools, hit-tests and unit tests.
6
+ //
7
+ // `snapNearest`/`snapDelta` carry 'worklet' directives: the native Inner calls
8
+ // them per frame on the UI thread (the live endpoint derived values and the
9
+ // shape rubber-band gesture) — the same mechanism planeGeometry's grid
10
+ // generators rely on. Keep them free of closures over non-worklet module
11
+ // functions (numeric constants are fine) so the workletizer can lift them.
12
+ //
13
+ // Targets travel to the UI thread as a FLAT number array (`[x, y, kind, …]`,
14
+ // stride SNAP_STRIDE) written once per drag — see `encodeSnapTargets`.
15
+ import { placementOf, normalizeRect } from './measurementGeometry.js';
16
+ // Screen-px capture radius: the grabbed endpoint jumps to a target once it is
17
+ // within this many pixels of it. Converted to doc space via `/ zoom` at drag
18
+ // begin (frozen for the drag, like the slide detent's SLIDE_SNAP_PX).
19
+ export const SNAP_RADIUS_PX = 16;
20
+ // Screen-px radius / ring width of the snap indicator drawn around the target
21
+ // while a snap is active. Larger than the endpoint handle (HANDLE_RADIUS_PX 7)
22
+ // so the ring frames the handle instead of hiding under it.
23
+ export const SNAP_RING_PX = 11;
24
+ export const SNAP_RING_WIDTH_PX = 2;
25
+ // Target kinds, ordered by priority: on an exact distance tie the LOWER kind
26
+ // wins, so a true endpoint/vertex beats a midpoint sitting on the same pixel.
27
+ export const SNAP_KIND_POINT = 0;
28
+ export const SNAP_KIND_MIDPOINT = 1;
29
+ // Flat encoding stride: [x, y, kind].
30
+ export const SNAP_STRIDE = 3;
31
+ // Every snap target in the document, in doc space. `excludeId` drops the
32
+ // element being edited ENTIRELY (its endpoints and midpoint) — snapping a
33
+ // line's endpoint onto its own other end would collapse it, and onto its own
34
+ // midpoint would halve it. Targets:
35
+ // - measurement `placement:'line'`: a, b (point) + midpoint. Plane-edge
36
+ // tiles included — their endpoints are real geometry even though the
37
+ // select tool refuses to drag them.
38
+ // - measurement `placement:'rectangle'`: the four normalized corners.
39
+ // - shape line/arrow: both endpoints + midpoint.
40
+ // - shape rect: the four normalized corners (stored as two opposite corners).
41
+ // - shape polygon (triangles included): every vertex.
42
+ // - ellipse, text, strokes, backgrounds: nothing.
43
+ // Degenerate (zero-length) lines contribute their coincident endpoints but no
44
+ // midpoint.
45
+ export const collectSnapTargets = (doc, excludeId) => {
46
+ const out = [];
47
+ const point = (p, sourceId) => out.push({ x: p.x, y: p.y, kind: SNAP_KIND_POINT, sourceId });
48
+ const segment = (a, b, sourceId) => {
49
+ point(a, sourceId);
50
+ point(b, sourceId);
51
+ if (a.x !== b.x || a.y !== b.y) {
52
+ out.push({
53
+ x: (a.x + b.x) / 2,
54
+ y: (a.y + b.y) / 2,
55
+ kind: SNAP_KIND_MIDPOINT,
56
+ sourceId,
57
+ });
58
+ }
59
+ };
60
+ const corners = (r, sourceId) => {
61
+ const n = normalizeRect(r);
62
+ point({ x: n.minX, y: n.minY }, sourceId);
63
+ point({ x: n.maxX, y: n.minY }, sourceId);
64
+ point({ x: n.maxX, y: n.maxY }, sourceId);
65
+ point({ x: n.minX, y: n.maxY }, sourceId);
66
+ };
67
+ for (const m of doc.placedMeasurements) {
68
+ if (excludeId != null && m.id === excludeId)
69
+ continue;
70
+ const placement = placementOf(m);
71
+ if (placement === 'line' && m.line)
72
+ segment(m.line.a, m.line.b, m.id);
73
+ else if (placement === 'rectangle' && m.rect)
74
+ corners(m.rect, m.id);
75
+ }
76
+ for (const s of doc.shapes) {
77
+ if (excludeId != null && s.id === excludeId)
78
+ continue;
79
+ const pts = s.geometry.points;
80
+ switch (s.kind) {
81
+ case 'line':
82
+ case 'arrow': {
83
+ const [a, b] = pts;
84
+ if (a && b)
85
+ segment(a, b, s.id);
86
+ break;
87
+ }
88
+ case 'rect': {
89
+ const [a, b] = pts;
90
+ if (a && b)
91
+ corners({ a, b }, s.id);
92
+ break;
93
+ }
94
+ case 'polygon':
95
+ for (const p of pts)
96
+ point(p, s.id);
97
+ break;
98
+ default:
99
+ break;
100
+ }
101
+ }
102
+ return out;
103
+ };
104
+ // Flat `[x, y, kind, …]` buffer for UI-thread transfer (a plain number[],
105
+ // like the freehand `livePoints` — TypedArrays aren't proven shareable here).
106
+ export const encodeSnapTargets = (targets) => {
107
+ const flat = new Array(targets.length * SNAP_STRIDE);
108
+ for (let i = 0; i < targets.length; i++) {
109
+ const t = targets[i];
110
+ flat[i * SNAP_STRIDE] = t.x;
111
+ flat[i * SNAP_STRIDE + 1] = t.y;
112
+ flat[i * SNAP_STRIDE + 2] = t.kind;
113
+ }
114
+ return flat;
115
+ };
116
+ export const collectSnapTargetsFlat = (doc, excludeId) => encodeSnapTargets(collectSnapTargets(doc, excludeId));
117
+ // Nearest target to (x, y) within `radiusDoc` (inclusive), or null. Ties on
118
+ // distance go to the lower kind (point beats midpoint); remaining ties to the
119
+ // earlier target. `radiusDoc <= 0` disables snapping. Worklet-callable.
120
+ export const snapNearest = (x, y, flat, radiusDoc) => {
121
+ 'worklet';
122
+ if (radiusDoc <= 0)
123
+ return null;
124
+ const r2 = radiusDoc * radiusDoc;
125
+ let bestD2 = Infinity;
126
+ let bestKind = Infinity;
127
+ let bestIdx = -1;
128
+ for (let i = 0; i + 2 < flat.length; i += 3) {
129
+ const dx = flat[i] - x;
130
+ const dy = flat[i + 1] - y;
131
+ const d2 = dx * dx + dy * dy;
132
+ if (d2 > r2)
133
+ continue;
134
+ const kind = flat[i + 2];
135
+ if (d2 < bestD2 || (d2 === bestD2 && kind < bestKind)) {
136
+ bestD2 = d2;
137
+ bestKind = kind;
138
+ bestIdx = i;
139
+ }
140
+ }
141
+ if (bestIdx < 0)
142
+ return null;
143
+ return {
144
+ x: flat[bestIdx],
145
+ y: flat[bestIdx + 1],
146
+ kind: flat[bestIdx + 2] === 1 ? 1 : 0,
147
+ index: bestIdx / 3,
148
+ };
149
+ };
150
+ // Snap a DRAG: `base` is the grabbed endpoint's committed position and `delta`
151
+ // the raw world-space drag offset. Returns the effective delta that lands the
152
+ // endpoint exactly on the nearest target (or the raw delta when nothing is in
153
+ // range), so the existing delta-based patch builders stay untouched. Worklet-
154
+ // callable — the native select gesture runs it in onEnd so the commit matches
155
+ // the live preview bit-for-bit.
156
+ export const snapDelta = (base, delta, flat, radiusDoc) => {
157
+ 'worklet';
158
+ const hit = snapNearest(base.x + delta.x, base.y + delta.y, flat, radiusDoc);
159
+ if (!hit)
160
+ return { delta, hit: null };
161
+ return { delta: { x: hit.x - base.x, y: hit.y - base.y }, hit };
162
+ };
163
+ // Snap an ABSOLUTE point (drag-to-create tools place raw world points).
164
+ export const snapPoint = (p, flat, radiusDoc) => {
165
+ 'worklet';
166
+ const hit = snapNearest(p.x, p.y, flat, radiusDoc);
167
+ return hit ? { point: { x: hit.x, y: hit.y }, hit } : { point: p, hit: null };
168
+ };
@@ -20,7 +20,7 @@ export declare const TILE_SCALE_MIN = 0.05;
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
+ export declare const resolveFileTileScaleFactor: (canvas: Pick<AnnotationCanvasState, "tileScaleFactor" | "tileScaleFactorMobile">, platform: TileScalePlatform) => number;
24
24
  export declare const clampTileScale: (v: number) => number;
25
25
  export declare const tileDocSizeForFactor: (factor: number, zoom: number) => number;
26
26
  export declare const tileScaleFromDocSize: (docSize: number, zoom: number) => number;
@@ -97,14 +97,16 @@ export const resolveTileScaleFactor = (canvas, platform) => (platform === 'mobil
97
97
  export const resolveHeaderTileScaleFactor = (canvas, platform) => (platform === 'mobile'
98
98
  ? (canvas.headerTileScaleFactorMobile ?? canvas.headerTileScaleFactor)
99
99
  : canvas.headerTileScaleFactor) ?? resolveTileScaleFactor(canvas, platform);
100
- // Resolve the document-wide OBJECT-tile-scale knob for a platform — the third
101
- // knob (Asana 1217570500895545), resolved exactly like the header knob: own
102
- // platform value → web value → the MEASUREMENT factor, so an object tile
103
- // placed before its knob was ever moved matches the measurement tiles beside
104
- // it. The ONE read path for the file fields.
105
- export const resolveFileTileScaleFactor = (canvas, platform) => (platform === 'mobile'
106
- ? (canvas.fileTileScaleFactorMobile ?? canvas.fileTileScaleFactor)
107
- : canvas.fileTileScaleFactor) ?? resolveTileScaleFactor(canvas, platform);
100
+ // Resolve the OBJECT-tile scale for a platform: the MEASUREMENT factor.
101
+ // Object tiles briefly had their own third knob (Asana 1217570500895545), but
102
+ // it was retired — an object thumbnail must size consistently with the
103
+ // measurement tiles beside it, so its slider now drives (and follows) the one
104
+ // shared factor. The `fileTileScaleFactor(Mobile)` fields it wrote persist on
105
+ // old documents and old undo payloads still write them; they are deliberately
106
+ // IGNORED here — resolving them would let a stale divergent value shadow the
107
+ // shared knob forever. Kept as a named resolver (not inlined at call sites)
108
+ // so the read path stays greppable and the retirement is documented once.
109
+ export const resolveFileTileScaleFactor = (canvas, platform) => resolveTileScaleFactor(canvas, platform);
108
110
  // Clamp a tile-scale-factor candidate to the supported range. The single guard
109
111
  // for the value before it lands in the document (slider input, restored docs).
110
112
  export const clampTileScale = (v) => v < TILE_SCALE_MIN ? TILE_SCALE_MIN : v > TILE_SCALE_MAX ? TILE_SCALE_MAX : v;
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_LAYER_ID } from '../../../types/annotation.js';
2
2
  import { DEFAULT_LINE_POS, recomputeAnchor, rectCenter, } from '../measurementGeometry.js';
3
+ import { collectSnapTargetsFlat, snapPoint } from '../snapGeometry.js';
3
4
  // The `line*` style fields for a placed measurement, built from the tool's
4
5
  // style options. Undefined fields are dropped (never emitted) so the render
5
6
  // defaults apply and Firestore never sees an `undefined` value.
@@ -22,6 +23,25 @@ const lineStyleFields = (placement, style) => {
22
23
  }
23
24
  return out;
24
25
  };
26
+ // Snap state for a fresh line press. The new annotation isn't in the doc yet,
27
+ // so nothing needs excluding. Empty when snapping is off.
28
+ const snapStateForPress = (ctx, placement) => placement === 'line' && ctx.snap?.enabled
29
+ ? {
30
+ snap: {
31
+ targets: collectSnapTargetsFlat(ctx.document),
32
+ radiusDoc: ctx.snap.radiusPx / ctx.viewport.state.zoom,
33
+ },
34
+ }
35
+ : {};
36
+ // The world point a drag end should use: snapped when a target is in range.
37
+ // Reports the target (or null) to the indicator.
38
+ const snapEnd = (s, ctx, world) => {
39
+ if (!s.snap)
40
+ return world;
41
+ const r = snapPoint(world, s.snap.targets, s.snap.radiusDoc);
42
+ ctx.snap?.setIndicator(r.hit ? { x: r.hit.x, y: r.hit.y } : null);
43
+ return r.point;
44
+ };
25
45
  let counter = 0;
26
46
  const makeId = () => `annotation-${Date.now().toString(36)}-${(counter++).toString(36)}`;
27
47
  const firstLayerId = (doc) => doc.layers[0]?.id ?? DEFAULT_LAYER_ID;
@@ -130,13 +150,21 @@ export const createMeasurementTool = (options = {}) => {
130
150
  id: options.id ?? defaultId,
131
151
  label: options.label ?? defaultLabel,
132
152
  cursor: 'crosshair',
133
- onPointerDown(event) {
153
+ onPointerDown(event, ctx) {
154
+ const snap = snapStateForPress(ctx, placement);
155
+ // The START of a new line snaps too (a leader that begins exactly on a
156
+ // corner is the point of the feature); the ring flashes there until the
157
+ // first move re-evaluates it against the moving end.
158
+ const startWorld = snap.snap
159
+ ? snapPoint(event.world, snap.snap.targets, snap.snap.radiusDoc).point
160
+ : event.world;
134
161
  return {
135
162
  kind: 'measurement-drawing',
136
163
  id: makeId(),
137
- startWorld: event.world,
164
+ startWorld,
138
165
  startScreen: event.screen,
139
166
  moved: false,
167
+ ...snap,
140
168
  };
141
169
  },
142
170
  onPointerMove(event, ctx, state) {
@@ -149,7 +177,7 @@ export const createMeasurementTool = (options = {}) => {
149
177
  placement,
150
178
  linePos,
151
179
  a: s.startWorld,
152
- b: event.world,
180
+ b: snapEnd(s, ctx, event.world),
153
181
  style,
154
182
  valueSource: valueSourceFor(ctx),
155
183
  });
@@ -160,6 +188,7 @@ export const createMeasurementTool = (options = {}) => {
160
188
  const s = state;
161
189
  if (s?.kind !== 'measurement-drawing')
162
190
  return;
191
+ ctx.snap?.setIndicator(null);
163
192
  const dx = event.screen.x - s.startScreen.x;
164
193
  const dy = event.screen.y - s.startScreen.y;
165
194
  if (!s.moved || dx * dx + dy * dy < minDragPx * minDragPx) {
@@ -167,18 +196,23 @@ export const createMeasurementTool = (options = {}) => {
167
196
  ctx.preview({ ops: [] });
168
197
  return;
169
198
  }
199
+ // Same snap as the last move, so the commit equals the preview.
200
+ const b = s.snap
201
+ ? snapPoint(event.world, s.snap.targets, s.snap.radiusDoc).point
202
+ : event.world;
170
203
  place(ctx, buildMeasurement({
171
204
  id: s.id,
172
205
  layerId: firstLayerId(ctx.document),
173
206
  placement,
174
207
  linePos,
175
208
  a: s.startWorld,
176
- b: event.world,
209
+ b,
177
210
  style,
178
211
  valueSource: valueSourceFor(ctx),
179
212
  }));
180
213
  },
181
214
  onCancel(_state, ctx) {
215
+ ctx.snap?.setIndicator(null);
182
216
  ctx.preview({ ops: [] });
183
217
  },
184
218
  };
@@ -1,5 +1,23 @@
1
+ import type { Vec2 } from '../../../types/annotation.js';
2
+ import { type BackgroundCropEdge } from '../backgroundLayers.js';
1
3
  import type { Tool } from '../Tool.js';
2
4
  export interface SelectToolOptions {
3
5
  panOnEmptyDrag?: boolean;
4
6
  }
7
+ export declare const cropDragVisibleRect: (start: {
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ }, full: {
13
+ x: number;
14
+ y: number;
15
+ width: number;
16
+ height: number;
17
+ }, edge: BackgroundCropEdge, delta: Vec2) => {
18
+ x: number;
19
+ y: number;
20
+ width: number;
21
+ height: number;
22
+ };
5
23
  export declare const createSelectTool: (options?: SelectToolOptions) => Tool;