@reekon-tools/boldr-utils 1.6.35 → 1.6.37

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 (75) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.js +11 -7
  2. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +12 -7
  3. package/dist/annotation/canvas/measurementGeometry.js +9 -7
  4. package/dist/annotation/canvas/measurementStampOverlay.d.ts +2 -0
  5. package/dist/annotation/canvas/stampLayout.d.ts +8 -1
  6. package/dist/annotation/canvas/stampLayout.js +18 -2
  7. package/dist/annotation/canvas/tools/measurementLineTool.d.ts +12 -0
  8. package/dist/annotation/canvas/tools/measurementLineTool.js +95 -0
  9. package/dist/annotation/canvas/tools/selectTool.js +14 -12
  10. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +4 -0
  11. package/dist/annotation/canvas/useAnnotationCanvasState.js +48 -1
  12. package/dist/exports.d.ts +1 -1
  13. package/dist/exports.js +1 -1
  14. package/dist/types/annotation.d.ts +1 -0
  15. package/package.json +1 -1
  16. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  17. package/dist/canvas/AnnotationCanvas.js +0 -10
  18. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  19. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  20. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  21. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  22. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  23. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  24. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  25. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  26. package/dist/canvas/Tool.d.ts +0 -38
  27. package/dist/canvas/Tool.js +0 -1
  28. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  29. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  30. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  31. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  32. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  33. package/dist/canvas/elements/ShapeElement.js +0 -62
  34. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  35. package/dist/canvas/elements/StrokeElement.js +0 -18
  36. package/dist/canvas/measurementPicker.d.ts +0 -10
  37. package/dist/canvas/measurementPicker.js +0 -1
  38. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  39. package/dist/canvas/measurementStampOverlay.js +0 -1
  40. package/dist/canvas/pointerAdapter.d.ts +0 -3
  41. package/dist/canvas/pointerAdapter.js +0 -19
  42. package/dist/canvas/stampLayout.d.ts +0 -5
  43. package/dist/canvas/stampLayout.js +0 -14
  44. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  45. package/dist/canvas/tools/measurementStampTool.js +0 -37
  46. package/dist/canvas/tools/panTool.d.ts +0 -5
  47. package/dist/canvas/tools/panTool.js +0 -25
  48. package/dist/canvas/tools/penTool.d.ts +0 -13
  49. package/dist/canvas/tools/penTool.js +0 -68
  50. package/dist/canvas/tools/selectTool.d.ts +0 -2
  51. package/dist/canvas/tools/selectTool.js +0 -182
  52. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  53. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  54. package/dist/canvas/viewport.d.ts +0 -16
  55. package/dist/canvas/viewport.js +0 -54
  56. package/dist/data/AnnotationDataContext.d.ts +0 -8
  57. package/dist/data/AnnotationDataContext.js +0 -11
  58. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  59. package/dist/data/AnnotationDataProvider.js +0 -4
  60. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  61. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  62. package/dist/data/canvasPersistence.d.ts +0 -3
  63. package/dist/data/canvasPersistence.js +0 -26
  64. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  65. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  66. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  67. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  68. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  69. package/dist/data/hooks/useAnnotationList.js +0 -26
  70. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  71. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  72. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  73. package/dist/hooks/useParseMeasurement.js +0 -14
  74. package/dist/utils/evaluateFormula.d.ts +0 -20
  75. package/dist/utils/evaluateFormula.js +0 -31
@@ -5,7 +5,7 @@ import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
5
5
  import { TEXT_FONT_FAMILY } from './elements/ShapeElement.js';
6
6
  import { buildRemoveMeasurementOps, hitPlacedMeasurement, } from './measurementGeometry.js';
7
7
  import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
8
- import { stampTileSize } from './stampLayout.js';
8
+ import { stampTileDims } from './stampLayout.js';
9
9
  // Screen-px radius of a measurement-annotation endpoint handle (matches the
10
10
  // native HANDLE_RADIUS_PX). Divided by zoom for a constant on-screen size.
11
11
  const HANDLE_PX = 7;
@@ -317,7 +317,9 @@ export const AnnotationCanvasInner = (props) => {
317
317
  inset: 0,
318
318
  pointerEvents: 'none',
319
319
  }, children: state.effectiveCanvas.placedMeasurements.map((placed) => {
320
- const size = stampTileSize(placed, state.tileScaleFactor, state.tileViewportScale);
320
+ // Square tile or wide group-header pill — one footprint source of
321
+ // truth shared with the hit-test (stampTileDims).
322
+ const { width, height } = stampTileDims(placed, state.tileScaleFactor, state.tileViewportScale);
321
323
  const cx = (placed.anchor.x - state.viewport.pan.x) * state.viewport.zoom;
322
324
  const cy = (placed.anchor.y - state.viewport.pan.y) * state.viewport.zoom;
323
325
  const isSelected = selection?.ids.includes(placed.id) ?? false;
@@ -326,19 +328,21 @@ export const AnnotationCanvasInner = (props) => {
326
328
  : (resolveStampMeasurement?.(placed) ?? null);
327
329
  // Corner-pinned, tile-proportional remove target (see the style
328
330
  // comment below) so it can't blanket a small tile and eat its grab.
329
- const removeSize = Math.min(40, size * 0.4);
331
+ const removeSize = Math.min(40, Math.min(width, height) * 0.4);
330
332
  return (_jsxs("div", { style: {
331
333
  position: 'absolute',
332
334
  left: 0,
333
335
  top: 0,
334
- width: size,
335
- height: size,
336
- transform: `translate(${cx - size / 2}px, ${cy - size / 2}px)`,
336
+ width,
337
+ height,
338
+ transform: `translate(${cx - width / 2}px, ${cy - height / 2}px)`,
337
339
  }, children: [renderMeasurementStamp({
338
340
  placed,
339
341
  measurement,
340
342
  selected: isSelected,
341
- size,
343
+ size: width,
344
+ width,
345
+ height,
342
346
  zoom: state.viewport.zoom,
343
347
  }), onMeasurementStampPress && (_jsx(StampPressTarget, { onPress: () => onMeasurementStampPress(placed), onLongPress: onMeasurementStampLongPress
344
348
  ? () => onMeasurementStampLongPress(placed)
@@ -4,7 +4,7 @@ import { useCallback, useEffect, 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';
7
- import { stampTileSize } from './stampLayout.js';
7
+ import { stampTileDims } from './stampLayout.js';
8
8
  import { DEFAULT_LAYER_ID, } from '../../types/annotation.js';
9
9
  import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
10
10
  import { useBackgroundSkImage } from './elements/BackgroundImageElement.js';
@@ -1446,14 +1446,17 @@ export const AnnotationCanvasInner = (props) => {
1446
1446
  }) }))] }));
1447
1447
  };
1448
1448
  const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, rectCtx, renderMeasurementStamp, tileScaleFactor, tileViewportScale, onStampPress, onStampLongPress, onRemove, }) => {
1449
- const size = stampTileSize(placed, tileScaleFactor, tileViewportScale);
1450
- const half = size / 2;
1449
+ // Square tile or wide group-header pill — one footprint source of truth
1450
+ // shared with the hit-test (stampTileDims).
1451
+ const { width, height } = stampTileDims(placed, tileScaleFactor, tileViewportScale);
1452
+ const halfW = width / 2;
1453
+ const halfH = height / 2;
1451
1454
  // Remove-"X" touch target, sized as a fraction of the tile and corner-pinned
1452
1455
  // so it never reaches the center. A FIXED 36px+hitSlop target blanketed small
1453
1456
  // tiles (the tile-scale slider can shrink them to ~38px), swallowing the
1454
1457
  // center grab and making a selected tile impossible to drag (it had to be
1455
1458
  // deselected first). Capped so it doesn't grow past the old size on big tiles.
1456
- const removeSize = Math.min(36, size * 0.4);
1459
+ const removeSize = Math.min(36, Math.min(width, height) * 0.4);
1457
1460
  const anchorX = placed.anchor.x;
1458
1461
  const anchorY = placed.anchor.y;
1459
1462
  // doc → screen each frame, on the UI thread. Position is a translate
@@ -1509,17 +1512,19 @@ const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging,
1509
1512
  const cx = (worldX - panX.value) * zoom.value;
1510
1513
  const cy = (worldY - panY.value) * zoom.value;
1511
1514
  return {
1512
- transform: [{ translateX: cx - half }, { translateY: cy - half }],
1515
+ transform: [{ translateX: cx - halfW }, { translateY: cy - halfH }],
1513
1516
  };
1514
1517
  });
1515
1518
  return (_jsxs(Animated.View, { pointerEvents: "box-none", style: [
1516
- { position: 'absolute', left: 0, top: 0, width: size, height: size },
1519
+ { position: 'absolute', left: 0, top: 0, width, height },
1517
1520
  animatedStyle,
1518
1521
  ], children: [_jsx(View, { pointerEvents: "none", style: StyleSheet.absoluteFill, children: renderMeasurementStamp({
1519
1522
  placed,
1520
1523
  measurement,
1521
1524
  selected,
1522
- size,
1525
+ size: width,
1526
+ width,
1527
+ height,
1523
1528
  zoom: zoomSnapshot,
1524
1529
  }) }), onStampPress && (_jsx(TouchableOpacity, { accessibilityRole: "button", accessibilityLabel: "Open measurement", onPress: () => onStampPress(placed), onLongPress: onStampLongPress ? () => onStampLongPress(placed) : undefined, style: StyleSheet.absoluteFill })), selected && measurement && onRemove && (_jsx(TouchableOpacity, { accessibilityRole: "button", accessibilityLabel: "Remove measurement", onPress: onRemove, style: {
1525
1530
  position: 'absolute',
@@ -7,7 +7,7 @@
7
7
  // boundary, so they inline the same math. Those inlined copies are the "worklet
8
8
  // twins" of these helpers — if you change the math here, change them too (they
9
9
  // are commented as such in AnnotationCanvasInner.native.tsx).
10
- import { stampTileSize } from './stampLayout.js';
10
+ import { stampTileDims } from './stampLayout.js';
11
11
  // Default position of the tile along its line when `linePos` is absent: center.
12
12
  export const DEFAULT_LINE_POS = 0.5;
13
13
  export const lerp = (a, b, t) => ({
@@ -111,16 +111,18 @@ const segmentDistSq = (p, a, b) => {
111
111
  // border ring of a rectangle annotation (interiors stay transparent to hits so
112
112
  // elements inside remain reachable).
113
113
  export const hitPlacedMeasurement = (m, p, zoom = 1, tileScaleFactor = 1, viewportScale = 1) => {
114
- // The stamp renders as a constant *screen*-size square centered on the
114
+ // The stamp renders as a constant *screen*-size box centered on the
115
115
  // anchor, so its doc-space footprint shrinks as you zoom in. Convert the
116
- // screen-space half-extent (+ padding) back to doc space via the zoom so
116
+ // screen-space half-extents (+ padding) back to doc space via the zoom so
117
117
  // the hit box always matches what's drawn (blank and populated stamps share
118
- // the footprint — see stampLayout).
119
- const half = (stampTileSize(m, tileScaleFactor, viewportScale) / 2 + STAMP_HIT_PADDING) /
120
- zoom;
118
+ // the square footprint; group-header chips are a wide pill — see
119
+ // stampTileDims in stampLayout).
120
+ const dims = stampTileDims(m, tileScaleFactor, viewportScale);
121
+ const halfW = (dims.width / 2 + STAMP_HIT_PADDING) / zoom;
122
+ const halfH = (dims.height / 2 + STAMP_HIT_PADDING) / zoom;
121
123
  const dx = Math.abs(p.x - m.anchor.x);
122
124
  const dy = Math.abs(p.y - m.anchor.y);
123
- if (dx <= half && dy <= half)
125
+ if (dx <= halfW && dy <= halfH)
124
126
  return true;
125
127
  // Measurement annotation: also grab anywhere along the line body.
126
128
  if (m.line) {
@@ -6,6 +6,8 @@ export interface MeasurementStampRenderArgs {
6
6
  measurement: Measurement | null;
7
7
  selected: boolean;
8
8
  size: number;
9
+ width: number;
10
+ height: number;
9
11
  zoom: number;
10
12
  }
11
13
  export type RenderMeasurementStamp = (args: MeasurementStampRenderArgs) => ReactNode;
@@ -1,5 +1,7 @@
1
1
  import type { AnnotationCanvasState, PlacedMeasurementRef, TileScalePlatform } from '../../types/annotation.js';
2
2
  export declare const STAMP_TILE_SIZE = 96;
3
+ export declare const GROUP_HEADER_TILE_WIDTH = 240;
4
+ export declare const GROUP_HEADER_TILE_HEIGHT = 64;
3
5
  export declare const DEFAULT_TILE_SCALE = 1;
4
6
  export declare const TILE_SCALE_MIN = 0.4;
5
7
  export declare const TILE_SCALE_MAX = 2;
@@ -7,5 +9,10 @@ export declare const resolveTileScaleFactor: (canvas: Pick<AnnotationCanvasState
7
9
  export declare const clampTileScale: (v: number) => number;
8
10
  export declare const tileDocSizeForFactor: (factor: number, zoom: number) => number;
9
11
  export declare const tileScaleFromDocSize: (docSize: number, zoom: number) => number;
10
- export declare const isUnassociatedStamp: (m: Pick<PlacedMeasurementRef, "measurementId" | "measurementPath" | "columnId">) => boolean;
12
+ export declare const isUnassociatedStamp: (m: Pick<PlacedMeasurementRef, "measurementId" | "measurementPath" | "columnId" | "isGroupHeader">) => boolean;
11
13
  export declare const stampTileSize: (m: Pick<PlacedMeasurementRef, "scale">, tileScaleFactor?: number, viewportScale?: number) => number;
14
+ export interface StampTileDims {
15
+ width: number;
16
+ height: number;
17
+ }
18
+ export declare const stampTileDims: (m: Pick<PlacedMeasurementRef, "scale" | "isGroupHeader">, tileScaleFactor?: number, viewportScale?: number) => StampTileDims;
@@ -12,6 +12,12 @@
12
12
  // user who set the tile-scale slider against blank tiles saw their tiles
13
13
  // balloon once populated (Asana 1216233394800417).
14
14
  export const STAMP_TILE_SIZE = 96;
15
+ // Screen-space footprint of a group-header chip (`isGroupHeader`) — a wide
16
+ // pill rather than the square tile, same 3.75:1 ratio as the legacy Nutrient
17
+ // header stamp (150×40). Like STAMP_TILE_SIZE it is a fixed-size pin scaled by
18
+ // `placed.scale` × the document-wide tile-scale factor, never by zoom.
19
+ export const GROUP_HEADER_TILE_WIDTH = 240;
20
+ export const GROUP_HEADER_TILE_HEIGHT = 64;
15
21
  // Document-wide tile scale factor: one knob that shrinks/grows EVERY
16
22
  // measurement tile on the canvas at once, on top of each tile's own `scale`.
17
23
  // Lets a user pull tiles down on a dense drawing where lines crowd together,
@@ -76,8 +82,9 @@ export const tileScaleFromDocSize = (docSize, zoom) => (docSize * zoom) / STAMP_
76
82
  // A placed measurement is an unassociated input until a measurement reference
77
83
  // is attached (id or path) OR it is bound to a form column (`columnId`). Such
78
84
  // stamps render as the blank "+" placeholder (same footprint as a populated
79
- // tile — see STAMP_TILE_SIZE).
80
- export const isUnassociatedStamp = (m) => !m.measurementId && !m.measurementPath && !m.columnId;
85
+ // tile — see STAMP_TILE_SIZE). Group-header chips carry neither ref but are
86
+ // NOT inputs never treat them as blank "+" placeholders.
87
+ export const isUnassociatedStamp = (m) => !m.isGroupHeader && !m.measurementId && !m.measurementPath && !m.columnId;
81
88
  // Screen-space edge length for a placed stamp: the base size scaled by the
82
89
  // per-stamp `scale` and the document-wide `tileScaleFactor`. Blank and
83
90
  // populated stamps share the footprint so a tile never resizes when it gains a
@@ -89,3 +96,12 @@ export const isUnassociatedStamp = (m) => !m.measurementId && !m.measurementPath
89
96
  // retained for call compatibility and is always 1 — the old per-canvas
90
97
  // multiplier was retired (see the note above). Independent of zoom.
91
98
  export const stampTileSize = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1) => STAMP_TILE_SIZE * (m.scale ?? 1) * tileScaleFactor * viewportScale;
99
+ export const stampTileDims = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1) => {
100
+ const k = (m.scale ?? 1) * tileScaleFactor * viewportScale;
101
+ return m.isGroupHeader
102
+ ? {
103
+ width: GROUP_HEADER_TILE_WIDTH * k,
104
+ height: GROUP_HEADER_TILE_HEIGHT * k,
105
+ }
106
+ : { width: STAMP_TILE_SIZE * k, height: STAMP_TILE_SIZE * k };
107
+ };
@@ -0,0 +1,12 @@
1
+ import type { PlacedMeasurementRef } from '../../../types/annotation.js';
2
+ import type { Tool } from '../Tool.js';
3
+ export interface MeasurementLineToolOptions {
4
+ id?: string;
5
+ label?: string;
6
+ minDragPx?: number;
7
+ autoSwitchToSelect?: boolean;
8
+ selectToolId?: string;
9
+ onAutoSwitch?: (toToolId: string) => void;
10
+ onPlaced?: (measurement: PlacedMeasurementRef) => void;
11
+ }
12
+ export declare const createMeasurementLineTool: (options?: MeasurementLineToolOptions) => Tool;
@@ -0,0 +1,95 @@
1
+ import { DEFAULT_LAYER_ID } from '../../../types/annotation.js';
2
+ import { DEFAULT_LINE_POS, recomputeAnchor } from '../measurementGeometry.js';
3
+ let counter = 0;
4
+ const makeId = () => `annotation-${Date.now().toString(36)}-${(counter++).toString(36)}`;
5
+ const firstLayerId = (doc) => doc.layers[0]?.id ?? DEFAULT_LAYER_ID;
6
+ // Build the blank line-measurement annotation for a drag from `a` to `b`: a
7
+ // 2-point line with a value tile magnetized to its center (linePos 0.5). The
8
+ // payload mirrors useAnnotationCanvasState's placeAnnotationAtCenter so a drawn
9
+ // line and a (legacy) center-placed one are byte-identical once committed.
10
+ const buildLineMeasurement = (opts) => {
11
+ const line = { a: opts.a, b: opts.b };
12
+ return {
13
+ id: opts.id,
14
+ layerId: opts.layerId,
15
+ placement: 'line',
16
+ line,
17
+ linePos: DEFAULT_LINE_POS,
18
+ // Center of the line; recomputeAnchor keeps this in sync on edits.
19
+ anchor: recomputeAnchor(line, 'line', DEFAULT_LINE_POS, {
20
+ x: (opts.a.x + opts.b.x) / 2,
21
+ y: (opts.a.y + opts.b.y) / 2,
22
+ }),
23
+ showLabel: true,
24
+ showValue: true,
25
+ createdAt: Date.now(),
26
+ };
27
+ };
28
+ // Drag-to-draw measurement-line tool. Press-drag rubber-bands a measurement
29
+ // annotation (a line with a blank value tile at its center); release commits
30
+ // it. Mirrors createShapeTool's interaction so "input lines" are placed the
31
+ // same way as shapes — draw to add, not tap-the-icon-to-add — replacing the
32
+ // old center-place affordance. The annotation stays blank (no measurement
33
+ // associated) until the user fills it via the tile / picker. Skia-free, like
34
+ // every tool factory; it renders its live preview through ctx.preview, so it
35
+ // works on web and (via the generic tool-pan dispatch) on native.
36
+ export const createMeasurementLineTool = (options = {}) => {
37
+ const minDragPx = options.minDragPx ?? 4;
38
+ const autoSwitchToSelect = options.autoSwitchToSelect ?? true;
39
+ const selectToolId = options.selectToolId ?? 'select';
40
+ return {
41
+ id: options.id ?? 'measure-line',
42
+ label: options.label ?? 'Measurement line',
43
+ cursor: 'crosshair',
44
+ onPointerDown(event) {
45
+ return {
46
+ kind: 'measurement-line-drawing',
47
+ id: makeId(),
48
+ startWorld: event.world,
49
+ startScreen: event.screen,
50
+ moved: false,
51
+ };
52
+ },
53
+ onPointerMove(event, ctx, state) {
54
+ const s = state;
55
+ if (s?.kind !== 'measurement-line-drawing')
56
+ return s;
57
+ const measurement = buildLineMeasurement({
58
+ id: s.id,
59
+ layerId: firstLayerId(ctx.document),
60
+ a: s.startWorld,
61
+ b: event.world,
62
+ });
63
+ ctx.preview({ ops: [{ op: 'addMeasurement', measurement }] });
64
+ return { ...s, moved: true };
65
+ },
66
+ onPointerUp(event, ctx, state) {
67
+ const s = state;
68
+ if (s?.kind !== 'measurement-line-drawing')
69
+ return;
70
+ const dx = event.screen.x - s.startScreen.x;
71
+ const dy = event.screen.y - s.startScreen.y;
72
+ if (!s.moved || dx * dx + dy * dy < minDragPx * minDragPx) {
73
+ // Accidental tap — discard the rubber-band, commit nothing.
74
+ ctx.preview({ ops: [] });
75
+ return;
76
+ }
77
+ const measurement = buildLineMeasurement({
78
+ id: s.id,
79
+ layerId: firstLayerId(ctx.document),
80
+ a: s.startWorld,
81
+ b: event.world,
82
+ });
83
+ ctx.commit({ ops: [{ op: 'addMeasurement', measurement }] });
84
+ // Leave it selected (and hand back to select) so the blank line can be
85
+ // filled / moved straight away — the same flow as the text tool.
86
+ ctx.setSelection({ ids: [measurement.id] });
87
+ options.onPlaced?.(measurement);
88
+ if (autoSwitchToSelect)
89
+ options.onAutoSwitch?.(selectToolId);
90
+ },
91
+ onCancel(_state, ctx) {
92
+ ctx.preview({ ops: [] });
93
+ },
94
+ };
95
+ };
@@ -1,4 +1,4 @@
1
- import { stampTileSize } from '../stampLayout.js';
1
+ import { stampTileDims } from '../stampLayout.js';
2
2
  import { placementOf, linePosOf, snapLinePos, lerp, recomputeAnchor, rectCenter, rectCornerPoint, oppositeRectCorner, hitPlacedMeasurement, } from '../measurementGeometry.js';
3
3
  import { hitShapeOutline, visualShapeBounds } from '../shapeGeometry.js';
4
4
  import { DEFAULT_TEXT_FONT_SIZE, resizeScaleFromDrag, textResizeGeometry, textShapeBounds, } from '../textGeometry.js';
@@ -157,10 +157,11 @@ const isOnMeasurementTile = (doc, id, world, zoom, viewportTileScale = 1, tileSc
157
157
  const m = doc.placedMeasurements.find((x) => x.id === id);
158
158
  if (!m)
159
159
  return false;
160
- const half = (stampTileSize(m, tileScaleFactor, viewportTileScale) / 2 + HIT_PADDING) /
161
- zoom;
162
- return (Math.abs(world.x - m.anchor.x) <= half &&
163
- Math.abs(world.y - m.anchor.y) <= half);
160
+ const dims = stampTileDims(m, tileScaleFactor, viewportTileScale);
161
+ const halfW = (dims.width / 2 + HIT_PADDING) / zoom;
162
+ const halfH = (dims.height / 2 + HIT_PADDING) / zoom;
163
+ return (Math.abs(world.x - m.anchor.x) <= halfW &&
164
+ Math.abs(world.y - m.anchor.y) <= halfH);
164
165
  };
165
166
  // --- Measurement-annotation grab logic (shared by the native UI-thread drag
166
167
  // via DragSelectionConfig AND the web pointer handlers — one source of truth) ---
@@ -170,13 +171,14 @@ const classifyGrab = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFact
170
171
  const m = doc.placedMeasurements.find((x) => x.id === id);
171
172
  if (!m)
172
173
  return 'move';
173
- // Same footprint the tile is drawn at (smaller for unassociated inputs, #6;
174
- // folds in the document-wide tile scale + viewport scale so slide-grab
175
- // matches the draw).
176
- const half = (stampTileSize(m, tileScaleFactor, viewportTileScale) / 2 + HIT_PADDING) /
177
- zoom;
178
- const onTile = Math.abs(world.x - m.anchor.x) <= half &&
179
- Math.abs(world.y - m.anchor.y) <= half;
174
+ // Same footprint the tile is drawn at (wide pill for group headers; folds in
175
+ // the document-wide tile scale + viewport scale so slide-grab matches the
176
+ // draw).
177
+ const dims = stampTileDims(m, tileScaleFactor, viewportTileScale);
178
+ const halfW = (dims.width / 2 + HIT_PADDING) / zoom;
179
+ const halfH = (dims.height / 2 + HIT_PADDING) / zoom;
180
+ const onTile = Math.abs(world.x - m.anchor.x) <= halfW &&
181
+ Math.abs(world.y - m.anchor.y) <= halfH;
180
182
  return onTile && placementOf(m) === 'line' && m.line ? 'slide' : 'move';
181
183
  };
182
184
  // Slide the tile along its line by a world-space drag delta (relative to
@@ -12,6 +12,7 @@ export interface AnnotationCanvasHandle {
12
12
  resetView(): void;
13
13
  getTileScaleFactor(): number;
14
14
  getTileDocSize(factor: number): number;
15
+ ensureMeasurementVisible(id: AnnotationElementId): void;
15
16
  placeMeasurementAtCenter(ref: MeasurementRef): void;
16
17
  placeAnnotationAtCenter(opts?: {
17
18
  defaultLengthDoc?: number;
@@ -29,6 +30,9 @@ export interface AnnotationCanvasHandle {
29
30
  groupId: string;
30
31
  columnId: string;
31
32
  }): AnnotationElementId;
33
+ placeGroupHeaderAtCenter(ref: {
34
+ groupId: string;
35
+ }): AnnotationElementId;
32
36
  deleteSelected(): void;
33
37
  }
34
38
  export interface UseAnnotationCanvasStateProps {
@@ -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, tileDocSizeForFactor, tileScaleFromDocSize, } from './stampLayout.js';
3
+ import { resolveTileScaleFactor, stampTileDims, tileDocSizeForFactor, tileScaleFromDocSize, } from './stampLayout.js';
4
4
  import { createViewportApi, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
5
5
  import { buildRemoveMeasurementOps, recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
6
6
  // The viewport that frames the document's content on screen. When a background
@@ -301,6 +301,31 @@ export const useAnnotationCanvasState = (props) => {
301
301
  getTileDocSize(factor) {
302
302
  return tileDocSizeForFactor(factor, ctxRef.current.viewport.state.zoom);
303
303
  },
304
+ ensureMeasurementVisible(id) {
305
+ const c = ctxRef.current;
306
+ const m = c.document.placedMeasurements.find((x) => x.id === id);
307
+ if (!m)
308
+ return;
309
+ // The tile is a screen-constant box centered on its anchor, so its
310
+ // half-extents are screen px regardless of zoom (stampTileDims).
311
+ const dims = stampTileDims(m, c.tileScaleFactor, c.tileViewportScale);
312
+ const halfW = dims.width / 2;
313
+ const halfH = dims.height / 2;
314
+ const s = c.viewport.worldToScreen(m.anchor);
315
+ if (s.x - halfW >= 0 &&
316
+ s.x + halfW <= width &&
317
+ s.y - halfH >= 0 &&
318
+ s.y + halfH <= height) {
319
+ return;
320
+ }
321
+ setViewport((v) => ({
322
+ zoom: v.zoom,
323
+ pan: {
324
+ x: m.anchor.x - width / (2 * v.zoom),
325
+ y: m.anchor.y - height / (2 * v.zoom),
326
+ },
327
+ }));
328
+ },
304
329
  placeMeasurementAtCenter(ref) {
305
330
  const c = ctxRef.current;
306
331
  const anchor = c.viewport.screenToWorld({
@@ -486,6 +511,28 @@ export const useAnnotationCanvasState = (props) => {
486
511
  c.setSelection({ ids: [placed.id] });
487
512
  return placed.id;
488
513
  },
514
+ placeGroupHeaderAtCenter(ref) {
515
+ const c = ctxRef.current;
516
+ const anchor = c.viewport.screenToWorld({
517
+ x: width / 2,
518
+ y: height / 2,
519
+ });
520
+ // No showLabel/showValue/placement — a header is not an input; it
521
+ // renders the group's name/index, resolved live by the consumer.
522
+ const placed = {
523
+ id: `header-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`,
524
+ layerId: c.document.layers[0]?.id ?? DEFAULT_LAYER_ID,
525
+ isGroupHeader: true,
526
+ groupId: ref.groupId,
527
+ anchor,
528
+ createdAt: Date.now(),
529
+ };
530
+ c.commit({ ops: [{ op: 'addMeasurement', measurement: placed }] });
531
+ // Select it so the consumer can immediately move it (the consumer is
532
+ // responsible for switching to its move/select tool).
533
+ c.setSelection({ ids: [placed.id] });
534
+ return placed.id;
535
+ },
489
536
  deleteSelected() {
490
537
  const c = ctxRef.current;
491
538
  const ids = c.selection?.ids;
package/dist/exports.d.ts CHANGED
@@ -23,7 +23,7 @@ export type { GestureConfig, PanTrigger, AnnotationCanvasInnerProps, } from './a
23
23
  export type { CanvasPointerEvent, RequestTextInput, ShapeDrawConfig, Tool, ToolContext, ToolState, } from './annotation/canvas/Tool.js';
24
24
  export type { MeasurementRef, PickMeasurement, } from './annotation/canvas/measurementPicker.js';
25
25
  export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './annotation/canvas/measurementStampOverlay.js';
26
- export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
26
+ export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
27
27
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
28
28
  export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
29
29
  export { createSelectTool, type SelectToolOptions, } from './annotation/canvas/tools/selectTool.js';
package/dist/exports.js CHANGED
@@ -23,7 +23,7 @@ export { useAnnotationMutations, } from './annotation/data/hooks/useAnnotationMu
23
23
  export { useAnnotationCanvasDoc, } from './annotation/data/hooks/useAnnotationCanvasDoc.js';
24
24
  export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
25
25
  export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
26
- export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
26
+ export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
27
27
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
28
28
  export { createPenTool, } from './annotation/canvas/tools/penTool.js';
29
29
  export { createSelectTool, } from './annotation/canvas/tools/selectTool.js';
@@ -53,6 +53,7 @@ export interface PlacedMeasurementRef {
53
53
  measurementId?: string;
54
54
  groupId?: string;
55
55
  columnId?: string;
56
+ isGroupHeader?: boolean;
56
57
  anchor: Vec2;
57
58
  placement?: MeasurementPlacement;
58
59
  line?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.6.35",
3
+ "version": "1.6.37",
4
4
  "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
5
  "author": "REEKON Tools",
6
6
  "license": "MIT",
@@ -1,11 +0,0 @@
1
- import { type ReactNode } from 'react';
2
- import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.js';
3
- export type { AnnotationCanvasHandle, GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
4
- export interface CanvasKitOpts {
5
- locateFile?: (file: string) => string;
6
- }
7
- export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
8
- fallback?: ReactNode;
9
- canvasKitOpts?: CanvasKitOpts;
10
- };
11
- export declare const AnnotationCanvas: ({ fallback, canvasKitOpts, ...rest }: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,10 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { WithSkiaWeb } from '@shopify/react-native-skia/lib/module/web/index.js';
3
- // Web-only entry point: lazy-loads CanvasKit wasm via WithSkiaWeb. Imperative
4
- // API (undo/redo/zoom) is exposed through the `imperativeRef` prop rather
5
- // than React refs, since WithSkiaWeb can't forward refs through its lazy
6
- // component boundary. The native build sits in `AnnotationCanvas.native.tsx`
7
- // and skips WithSkiaWeb entirely (no wasm to load).
8
- export const AnnotationCanvas = ({ fallback, canvasKitOpts, ...rest }) => (_jsx(WithSkiaWeb, { getComponent: () => import('./AnnotationCanvasInner.js').then((m) => ({
9
- default: m.AnnotationCanvasInner,
10
- })), fallback: fallback ?? null, componentProps: rest, opts: canvasKitOpts }));
@@ -1,8 +0,0 @@
1
- import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.native.js';
2
- export type { AnnotationCanvasHandle, } from './useAnnotationCanvasState.js';
3
- export type { GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
4
- export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
5
- fallback?: unknown;
6
- canvasKitOpts?: unknown;
7
- };
8
- export declare const AnnotationCanvas: (props: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { AnnotationCanvasInner } from './AnnotationCanvasInner.native.js';
3
- export const AnnotationCanvas = (props) => {
4
- const { fallback: _f, canvasKitOpts: _o, ...rest } = props;
5
- return _jsx(AnnotationCanvasInner, { ...rest });
6
- };
@@ -1,39 +0,0 @@
1
- import { type CSSProperties, type MutableRefObject } from 'react';
2
- import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
3
- import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
4
- import type { MeasurementRef } from './measurementPicker.js';
5
- import type { Tool } from './Tool.js';
6
- import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
7
- import { type ViewportState } from './viewport.js';
8
- import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
9
- export type { AnnotationCanvasHandle };
10
- export interface AnnotationCanvasInnerProps {
11
- canvas: AnnotationCanvasState;
12
- onCommit(patch: AnnotationDocumentPatch): void;
13
- tools: Tool[];
14
- activeToolId: string;
15
- selection: Selection | null;
16
- onSelectionChange(selection: Selection | null): void;
17
- measurements?: Measurement[];
18
- fallbackUnit?: Units;
19
- fractionalTolerance?: FractionalTolerance;
20
- decimalTolerance?: DecimalTolerance;
21
- resolveImageUrl?: (storagePath: string) => Promise<string>;
22
- pickMeasurement?: () => Promise<MeasurementRef | null>;
23
- renderMeasurementStamp?: RenderMeasurementStamp;
24
- stampFontSource?: unknown;
25
- stampValueFontSize?: number;
26
- stampLabelFontSize?: number;
27
- gestures?: GestureConfig;
28
- width: number;
29
- height: number;
30
- initialViewport?: ViewportState;
31
- style?: CSSProperties;
32
- imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
33
- }
34
- export type PanTrigger = 'middleMouse' | 'rightMouse' | 'space';
35
- export interface GestureConfig {
36
- wheel?: 'zoom' | 'pan' | 'auto';
37
- panTriggers?: PanTrigger[];
38
- }
39
- export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;