@reekon-tools/boldr-utils 1.20.1 → 1.20.3

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.
@@ -41,6 +41,7 @@ export interface AnnotationCanvasInnerProps {
41
41
  initialViewport?: ViewportState;
42
42
  style?: CSSProperties;
43
43
  magnifierTopOffset?: number;
44
+ onInteractingChange?: (interacting: boolean) => void;
44
45
  imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
45
46
  snapshotRef?: MutableRefObject<(() => Uint8Array | null) | null>;
46
47
  }
@@ -42,6 +42,7 @@ export interface AnnotationCanvasInnerProps {
42
42
  initialViewport?: ViewportState;
43
43
  style?: ViewStyle;
44
44
  magnifierTopOffset?: number;
45
+ onInteractingChange?: (interacting: boolean) => void;
45
46
  imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
46
47
  snapshotRef?: MutableRefObject<(() => Uint8Array | null) | null>;
47
48
  }
@@ -65,7 +65,7 @@ const LOUPE_CROSS_WIDTH_PX = 1.5;
65
65
  // is only re-synced once each gesture ends. The tool/tap path still hops to
66
66
  // JS (`runOnJS(true)`) since drawing/selection are React-state operations.
67
67
  export const AnnotationCanvasInner = (props) => {
68
- const { resolveImageUrl, deferBackgroundToResolver, hideLinkBadges, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, snapToGeometry = false, renderBackground, } = props;
68
+ const { resolveImageUrl, deferBackgroundToResolver, hideLinkBadges, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, onInteractingChange, snapToGeometry = false, renderBackground, } = props;
69
69
  const valueFont = useFont(stampFontSource, stampValueFontSize);
70
70
  // The animated-viewport implementation needs the Reanimated shared values,
71
71
  // which are created below AFTER the state hook — so the hook gets a stable
@@ -159,6 +159,13 @@ export const AnnotationCanvasInner = (props) => {
159
159
  const magTouchX = useSharedValue(0);
160
160
  const magTouchY = useSharedValue(0);
161
161
  const [magnifying, setMagnifying] = useState(false);
162
+ // Viewport pan/zoom in motion. Set from the viewport pan/pinch onChange
163
+ // worklets on the first frame that actually moves the viewport (never their
164
+ // onBegin, which fires for plain taps too) and cleared when the LAST
165
+ // viewport gesture finalizes — the two run simultaneously during a
166
+ // two-finger gesture, hence the endViewportGesture ref-count check rather
167
+ // than clearing per-gesture.
168
+ const [viewportMoving, setViewportMoving] = useState(false);
162
169
  // Lens geometry (screen points): a fixed square in the top-left, pushed below
163
170
  // any overlaid toolbar by magnifierTopOffset. Its CENTER maps to the live
164
171
  // touch point, so the magnified slice is the canvas around the finger.
@@ -474,6 +481,22 @@ export const AnnotationCanvasInner = (props) => {
474
481
  // per-frame JS. The Skia line stays static (draggingId is NOT set), only the
475
482
  // tile moves.
476
483
  const [slidingId, setSlidingId] = useState(null);
484
+ // Consumer notification (onInteractingChange): `magnifying` covers every
485
+ // precision gesture (set on actual movement, cleared on every finalize
486
+ // path); whole-element moves and tile slides deliberately DON'T show the
487
+ // loupe, so their gating ids are OR'd in here — as is viewport motion.
488
+ // (The other sub-drag ids — endpoint/corner/crop — all set `magnifying`
489
+ // alongside their own id, so they're already covered.) Ref-guarded so a
490
+ // consumer that re-renders us doesn't get duplicate notifications, and so
491
+ // the mount render doesn't fire a spurious `false`.
492
+ const interacting = magnifying || viewportMoving || draggingId != null || slidingId != null;
493
+ const notifiedInteractingRef = useRef(false);
494
+ useEffect(() => {
495
+ if (notifiedInteractingRef.current === interacting)
496
+ return;
497
+ notifiedInteractingRef.current = interacting;
498
+ onInteractingChange?.(interacting);
499
+ }, [interacting, onInteractingChange]);
477
500
  const slideCtx = useSharedValue({ ax: 0, ay: 0, bx: 0, by: 0, t0: 0.5 });
478
501
  const slideTargetRef = useRef(null);
479
502
  // Endpoint drag (resize/rotate a measurement annotation's line). `epDragId`
@@ -1139,6 +1162,10 @@ export const AnnotationCanvasInner = (props) => {
1139
1162
  };
1140
1163
  const endViewportGesture = () => {
1141
1164
  activeViewportGestures.current = Math.max(0, activeViewportGestures.current - 1);
1165
+ // Simultaneous pan+pinch: only the LAST finalize ends "viewport moving"
1166
+ // (a no-op setState when it never started — onChange never fired).
1167
+ if (activeViewportGestures.current === 0)
1168
+ setViewportMoving(false);
1142
1169
  };
1143
1170
  const toolPan = Gesture.Pan()
1144
1171
  .minPointers(1)
@@ -1274,6 +1301,10 @@ export const AnnotationCanvasInner = (props) => {
1274
1301
  'worklet';
1275
1302
  panX.value -= e.changeX / zoom.value;
1276
1303
  panY.value -= e.changeY / zoom.value;
1304
+ // First frame of actual movement flips "viewport moving" (gated on
1305
+ // the dirty bit so it's one JS hop per gesture, not per frame).
1306
+ if (!viewportDirty.value)
1307
+ runOnJS(setViewportMoving)(true);
1277
1308
  viewportDirty.value = true;
1278
1309
  })
1279
1310
  // onFinalize, not onEnd: on Android this pan normally exits through
@@ -1344,6 +1375,10 @@ export const AnnotationCanvasInner = (props) => {
1344
1375
  panX.value = focalWorldX - e.focalX / next;
1345
1376
  panY.value = focalWorldY - e.focalY / next;
1346
1377
  zoom.value = next;
1378
+ // Same first-movement flip as the pan (see there) — the early returns
1379
+ // above keep a no-op pinch from ever firing it.
1380
+ if (!viewportDirty.value)
1381
+ runOnJS(setViewportMoving)(true);
1347
1382
  viewportDirty.value = true;
1348
1383
  })
1349
1384
  // onFinalize, not onEnd, for the same reason as the pan: an Android
@@ -1,7 +1,7 @@
1
1
  export type ExportCategory = 'defaultGroup' | 'groups' | 'forms';
2
2
  export type ExportObjectType = 'calculators' | 'files' | 'photos' | 'cad' | 'labels' | 'notes';
3
3
  export type ExportMeasurementFilter = 'all' | 'complete' | 'incomplete';
4
- export type ExportImageFormat = 'hyperlink' | 'embedded' | 'none';
4
+ export type ExportImageFormat = 'hyperlink' | 'embedded' | 'zip' | 'none';
5
5
  export type ExportMeasurementDataField = 'measurement' | 'label' | 'notes' | 'isComplete' | 'completedBy' | 'completedAt' | 'createdAt' | 'createdBy';
6
6
  export type ExportConfig = {
7
7
  categories: ExportCategory[];
@@ -46,7 +46,7 @@ export const EXPORT_EVERYTHING_CONFIG = {
46
46
  const isCategory = (v) => typeof v === 'string' && ALL_EXPORT_CATEGORIES.includes(v);
47
47
  const isObjectType = (v) => typeof v === 'string' && ALL_EXPORT_OBJECT_TYPES.includes(v);
48
48
  const isMeasurementFilter = (v) => v === 'all' || v === 'complete' || v === 'incomplete';
49
- const isImageFormat = (v) => v === 'hyperlink' || v === 'embedded' || v === 'none';
49
+ const isImageFormat = (v) => v === 'hyperlink' || v === 'embedded' || v === 'zip' || v === 'none';
50
50
  const isMeasurementDataField = (v) => typeof v === 'string' &&
51
51
  ALL_EXPORT_MEASUREMENT_DATA_FIELDS.includes(v);
52
52
  // Tolerant reader for the raw user-doc field. Missing/invalid fields fall
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.20.1",
3
+ "version": "1.20.3",
4
4
  "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
5
  "author": "REEKON Tools",
6
6
  "license": "MIT",