@reekon-tools/boldr-utils 1.6.23 → 1.6.25
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.
- package/dist/annotation/canvas/AnnotationCanvasInner.d.ts +1 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +1 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +387 -77
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +14 -2
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +55 -42
- package/dist/annotation/canvas/Tool.d.ts +8 -0
- package/dist/annotation/canvas/elements/BackgroundImageElement.d.ts +4 -1
- package/dist/annotation/canvas/elements/BackgroundImageElement.js +21 -19
- package/dist/annotation/canvas/elements/ShapeElement.js +45 -25
- package/dist/annotation/canvas/elements/StrokeElement.js +26 -11
- package/dist/annotation/canvas/shapeGeometry.d.ts +7 -0
- package/dist/annotation/canvas/shapeGeometry.js +36 -0
- package/dist/annotation/canvas/tools/penTool.d.ts +1 -0
- package/dist/annotation/canvas/tools/penTool.js +11 -1
- package/dist/annotation/canvas/tools/selectTool.js +118 -1
- package/dist/annotation/canvas/tools/shapeTool.d.ts +2 -0
- package/dist/annotation/canvas/tools/shapeTool.js +13 -2
- package/dist/annotation/canvas/useAnnotationCanvasState.js +41 -10
- package/dist/annotation/canvas/viewport.d.ts +13 -1
- package/dist/annotation/canvas/viewport.js +32 -0
- package/dist/exports.d.ts +1 -1
- package/dist/exports.js +1 -1
- package/dist/types/annotation.d.ts +3 -0
- package/dist/types/annotation.js +8 -2
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Skia, useFont } from '@shopify/react-native-skia';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Group, Line, Rect, RoundedRect, Skia, rect, rrect, useFont, } from '@shopify/react-native-skia';
|
|
3
3
|
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';
|
|
@@ -7,8 +7,10 @@ import Animated, { runOnJS, runOnUI, useAnimatedStyle, useDerivedValue, useShare
|
|
|
7
7
|
import { stampTileSize } from './stampLayout.js';
|
|
8
8
|
import { DEFAULT_LAYER_ID, } from '../../types/annotation.js';
|
|
9
9
|
import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
|
|
10
|
+
import { useBackgroundSkImage } from './elements/BackgroundImageElement.js';
|
|
10
11
|
import { buildRemoveMeasurementOps, } from './measurementGeometry.js';
|
|
11
12
|
import { buildShapeFromDrag } from './tools/shapeTool.js';
|
|
13
|
+
import { SELECTION_PAD } from './textGeometry.js';
|
|
12
14
|
import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
|
|
13
15
|
let strokeCounter = 0;
|
|
14
16
|
const makeStrokeId = () => `stroke-${Date.now().toString(36)}-${(strokeCounter++).toString(36)}`;
|
|
@@ -24,6 +26,24 @@ const HANDLE_RADIUS_PX = 7;
|
|
|
24
26
|
// Screen-px stroke width of the handle's colored ring (white-disc + ring, so the
|
|
25
27
|
// knob stays legible over a line of any color). Also zoom-divided.
|
|
26
28
|
const HANDLE_RING_PX = 2;
|
|
29
|
+
// Doc-space floor on a shape-corner resize's width/height — the worklet twin of
|
|
30
|
+
// selectTool's MIN_SHAPE_EXTENT (the live preview must clamp identically to the
|
|
31
|
+
// buildShapeCornerPatch commit). Keep the two in sync.
|
|
32
|
+
const MIN_SHAPE_EXTENT = 1;
|
|
33
|
+
// Magnifying-glass loupe. A separate, top-most overlay canvas re-draws the Skia
|
|
34
|
+
// scene zoomed around the finger and clipped to a fixed lens window, so it sits
|
|
35
|
+
// ABOVE the RN measurement/input overlay (which the main canvas sits below) and
|
|
36
|
+
// never touches the main canvas (no flicker). It mounts only during a precision
|
|
37
|
+
// gesture, so there's no cost at rest.
|
|
38
|
+
const LOUPE_SIZE_FRACTION = 0.3; // of the smaller canvas dimension
|
|
39
|
+
const LOUPE_MARGIN = 12; // points from the top-left corner
|
|
40
|
+
const LOUPE_MAGNIFICATION = 2; // extra zoom inside the lens, on top of viewport
|
|
41
|
+
const LOUPE_RADIUS = 16; // lens corner radius
|
|
42
|
+
const LOUPE_BORDER_WIDTH = 2;
|
|
43
|
+
const LOUPE_BORDER_COLOR = '#1F2937';
|
|
44
|
+
const LOUPE_BG_COLOR = '#FFFFFF';
|
|
45
|
+
const LOUPE_CROSSHAIR_COLOR = '#1F293799'; // ~60% dark — marks the exact point
|
|
46
|
+
const LOUPE_CROSSHAIR_ARM = 9; // half-length of each crosshair arm, points
|
|
27
47
|
// Native fingerprint: one finger drives the active tool, two fingers
|
|
28
48
|
// pan/zoom the viewport. Tap counts as a brief pointer down+up so tools
|
|
29
49
|
// like measurement-stamp (which only listen to onPointerUp) work via tap.
|
|
@@ -36,13 +56,22 @@ const HANDLE_RING_PX = 2;
|
|
|
36
56
|
// is only re-synced once each gesture ends. The tool/tap path still hops to
|
|
37
57
|
// JS (`runOnJS(true)`) since drawing/selection are React-state operations.
|
|
38
58
|
export const AnnotationCanvasInner = (props) => {
|
|
39
|
-
const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, width, height, style, } = props;
|
|
59
|
+
const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, } = props;
|
|
40
60
|
const valueFont = useFont(stampFontSource, stampValueFontSize);
|
|
41
61
|
const state = useAnnotationCanvasState(props);
|
|
62
|
+
// Decode the background image ONCE here and share it with both the main
|
|
63
|
+
// canvas and the magnifier loupe. If each canvas loaded its own, the loupe
|
|
64
|
+
// would flash its blank base every gesture while it re-resolved + re-decoded.
|
|
65
|
+
const backgroundSkImage = useBackgroundSkImage(state.effectiveCanvas.viewport.backgroundImage, resolveImageUrl);
|
|
42
66
|
// Latest state behind a ref so the gesture object can be built once and
|
|
43
67
|
// never rebuilt mid-gesture — its JS callbacks read `stateRef.current`.
|
|
44
68
|
const stateRef = useRef(state);
|
|
45
69
|
stateRef.current = state;
|
|
70
|
+
// Open-value-entry callback (edit mode). Rides a ref so the gesture needn't
|
|
71
|
+
// rebuild when it changes; the second-tap-on-selected detection lives in the
|
|
72
|
+
// tap gesture below (it compares against the selection captured pre-tap).
|
|
73
|
+
const onStampDoubleTapRef = useRef(props.onMeasurementStampDoubleTap);
|
|
74
|
+
onStampDoubleTapRef.current = props.onMeasurementStampDoubleTap;
|
|
46
75
|
// Live viewport on the UI thread. Initialised from the JS snapshot; kept in
|
|
47
76
|
// sync from JS only when not actively gesturing (see the effect below).
|
|
48
77
|
const zoom = useSharedValue(state.viewport.zoom);
|
|
@@ -58,6 +87,40 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
58
87
|
{ translateY: -panY.value },
|
|
59
88
|
];
|
|
60
89
|
});
|
|
90
|
+
// Magnifying-glass loupe. The live touch (screen points) rides shared values
|
|
91
|
+
// the gesture worklets update; `magnifying` (React state) gates whether the
|
|
92
|
+
// loupe shader wraps the canvas at all — so the extra fullscreen pass exists
|
|
93
|
+
// only while a precision gesture is in flight (drawing, shape rubber-band,
|
|
94
|
+
// endpoint/corner drags), never during idle renders or viewport pan/zoom.
|
|
95
|
+
const magTouchX = useSharedValue(0);
|
|
96
|
+
const magTouchY = useSharedValue(0);
|
|
97
|
+
const [magnifying, setMagnifying] = useState(false);
|
|
98
|
+
// Lens geometry (screen points): a fixed square in the top-left, pushed below
|
|
99
|
+
// any overlaid toolbar by magnifierTopOffset. Its CENTER maps to the live
|
|
100
|
+
// touch point, so the magnified slice is the canvas around the finger.
|
|
101
|
+
const loupeSize = Math.round(Math.min(width, height) * LOUPE_SIZE_FRACTION);
|
|
102
|
+
const loupeX = LOUPE_MARGIN;
|
|
103
|
+
const loupeY = magnifierTopOffset + LOUPE_MARGIN;
|
|
104
|
+
const loupeCx = loupeX + loupeSize / 2;
|
|
105
|
+
const loupeCy = loupeY + loupeSize / 2;
|
|
106
|
+
// World→lens transform: take the live screen point under the finger to the
|
|
107
|
+
// lens center, scaled by LOUPE_MAGNIFICATION on top of the viewport zoom.
|
|
108
|
+
// screen' = lensCenter + m * (worldTransform(world) - touch). Composed as a
|
|
109
|
+
// transform list (last entry applied to the point first), prepended onto the
|
|
110
|
+
// viewport's own [scale, -pan] ops.
|
|
111
|
+
const loupeTransform = useDerivedValue(() => {
|
|
112
|
+
'worklet';
|
|
113
|
+
return [
|
|
114
|
+
{ translateX: loupeCx },
|
|
115
|
+
{ translateY: loupeCy },
|
|
116
|
+
{ scale: LOUPE_MAGNIFICATION },
|
|
117
|
+
{ translateX: -magTouchX.value },
|
|
118
|
+
{ translateY: -magTouchY.value },
|
|
119
|
+
{ scale: zoom.value },
|
|
120
|
+
{ translateX: -panX.value },
|
|
121
|
+
{ translateY: -panY.value },
|
|
122
|
+
];
|
|
123
|
+
}, [loupeCx, loupeCy]);
|
|
61
124
|
// In-flight freehand stroke, owned by the UI thread. The drawing gesture
|
|
62
125
|
// appends world-space points to `livePoints`; `livePath` rebuilds the Skia
|
|
63
126
|
// path off it. React state is untouched until the stroke commits on
|
|
@@ -179,13 +242,14 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
179
242
|
// Live mirror of the active shape tool's config for the worklet path
|
|
180
243
|
// builders (the derived values are created once, so they can't close over
|
|
181
244
|
// the changing `shapeDraw` prop).
|
|
182
|
-
const shapeCfg = useSharedValue({ kind: 'line', cap: 'round', width: 2 });
|
|
245
|
+
const shapeCfg = useSharedValue({ kind: 'line', cap: 'round', startCap: 'round', width: 2 });
|
|
183
246
|
useEffect(() => {
|
|
184
247
|
if (!shapeDraw)
|
|
185
248
|
return;
|
|
186
249
|
shapeCfg.value = {
|
|
187
250
|
kind: shapeDraw.kind,
|
|
188
251
|
cap: shapeDraw.cap ?? 'round',
|
|
252
|
+
startCap: shapeDraw.startCap ?? 'round',
|
|
189
253
|
width: shapeDraw.width,
|
|
190
254
|
};
|
|
191
255
|
}, [shapeDraw, shapeCfg]);
|
|
@@ -256,15 +320,20 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
256
320
|
path.close();
|
|
257
321
|
};
|
|
258
322
|
for (const e of handoffShapes.value) {
|
|
259
|
-
if (e.kind
|
|
323
|
+
if (e.kind !== 'line')
|
|
324
|
+
continue;
|
|
325
|
+
// End head at b (points a→b); start head at a (points b→a, args swapped).
|
|
326
|
+
if (e.cap === 'arrow')
|
|
260
327
|
addHead(e.ax, e.ay, e.bx, e.by);
|
|
261
|
-
|
|
328
|
+
if (e.startCap === 'arrow')
|
|
329
|
+
addHead(e.bx, e.by, e.ax, e.ay);
|
|
262
330
|
}
|
|
263
331
|
const s = liveShape.value;
|
|
264
|
-
if (s.active &&
|
|
265
|
-
shapeCfg.value.
|
|
266
|
-
|
|
267
|
-
|
|
332
|
+
if (s.active && shapeCfg.value.kind === 'line') {
|
|
333
|
+
if (shapeCfg.value.cap === 'arrow')
|
|
334
|
+
addHead(s.ax, s.ay, s.bx, s.by);
|
|
335
|
+
if (shapeCfg.value.startCap === 'arrow')
|
|
336
|
+
addHead(s.bx, s.by, s.ax, s.ay);
|
|
268
337
|
}
|
|
269
338
|
return path;
|
|
270
339
|
});
|
|
@@ -420,6 +489,124 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
420
489
|
{ translateY: -c.py },
|
|
421
490
|
];
|
|
422
491
|
});
|
|
492
|
+
// Geometric-shape corner resize (rect/ellipse/polygon). `shapeResizeId`
|
|
493
|
+
// (React state) gates which shape renders live; `shapeResizeCtx` carries the
|
|
494
|
+
// fixed (opposite) corner and the grabbed corner's start position (from
|
|
495
|
+
// DragSelectionConfig.hitTestShapeCorner), and `shapeResizePts` the shape's
|
|
496
|
+
// start geometry. The preview re-renders the shape from live GEOMETRY (a
|
|
497
|
+
// derived path) rather than a scale transform — a non-uniform scale transform
|
|
498
|
+
// would warp the stroke width (top/bottom edges thicken with scaleY); drawing
|
|
499
|
+
// the scaled outline at a constant stroke keeps it crisp. The commit on
|
|
500
|
+
// release goes through buildShapeCornerPatch, which scales identically.
|
|
501
|
+
const [shapeResizeId, setShapeResizeId] = useState(null);
|
|
502
|
+
const shapeResizeCtx = useSharedValue({ fx: 0, fy: 0, mx: 0, my: 0, uni: 0 });
|
|
503
|
+
// The resized shape's start geometry: kind + flat [x,y,x,y,…] points.
|
|
504
|
+
const shapeResizePts = useSharedValue({
|
|
505
|
+
kind: '',
|
|
506
|
+
pts: [],
|
|
507
|
+
});
|
|
508
|
+
const shapeResizeTargetRef = useRef(null);
|
|
509
|
+
// Live scale factors about the fixed corner. WORKLET TWIN of
|
|
510
|
+
// selectTool.shapeCornerPatch — keep the clamp + the ellipse uniform-scale
|
|
511
|
+
// branch in sync. Read by the path + box derived values below.
|
|
512
|
+
const shapeResizeScale = useDerivedValue(() => {
|
|
513
|
+
'worklet';
|
|
514
|
+
const c = shapeResizeCtx.value;
|
|
515
|
+
const denomX = c.mx - c.fx;
|
|
516
|
+
const denomY = c.my - c.fy;
|
|
517
|
+
let offX = c.mx + dragX.value - c.fx;
|
|
518
|
+
let offY = c.my + dragY.value - c.fy;
|
|
519
|
+
offX =
|
|
520
|
+
denomX >= 0
|
|
521
|
+
? Math.max(MIN_SHAPE_EXTENT, offX)
|
|
522
|
+
: Math.min(-MIN_SHAPE_EXTENT, offX);
|
|
523
|
+
offY =
|
|
524
|
+
denomY >= 0
|
|
525
|
+
? Math.max(MIN_SHAPE_EXTENT, offY)
|
|
526
|
+
: Math.min(-MIN_SHAPE_EXTENT, offY);
|
|
527
|
+
let sx = denomX !== 0 ? offX / denomX : 1;
|
|
528
|
+
let sy = denomY !== 0 ? offY / denomY : 1;
|
|
529
|
+
if (c.uni === 1) {
|
|
530
|
+
const oldDiag = Math.sqrt(denomX * denomX + denomY * denomY);
|
|
531
|
+
const s0 = oldDiag !== 0 ? Math.sqrt(offX * offX + offY * offY) / oldDiag : 1;
|
|
532
|
+
sx = s0;
|
|
533
|
+
sy = s0;
|
|
534
|
+
}
|
|
535
|
+
return { sx, sy };
|
|
536
|
+
});
|
|
537
|
+
// The shape's outline scaled about the fixed corner, rebuilt per frame.
|
|
538
|
+
// WORKLET TWIN of ShapeElement's per-kind rendering (rect/ellipse/polygon) —
|
|
539
|
+
// keep in sync.
|
|
540
|
+
const shapeResizePath = useDerivedValue(() => {
|
|
541
|
+
'worklet';
|
|
542
|
+
const path = Skia.Path.Make();
|
|
543
|
+
const g = shapeResizePts.value;
|
|
544
|
+
const c = shapeResizeCtx.value;
|
|
545
|
+
const { sx, sy } = shapeResizeScale.value;
|
|
546
|
+
const n = g.pts.length;
|
|
547
|
+
if (n < 4)
|
|
548
|
+
return path;
|
|
549
|
+
const px = (i) => c.fx + (g.pts[i] - c.fx) * sx;
|
|
550
|
+
const py = (i) => c.fy + (g.pts[i + 1] - c.fy) * sy;
|
|
551
|
+
if (g.kind === 'rect' || g.kind === 'ellipse') {
|
|
552
|
+
const ax = px(0);
|
|
553
|
+
const ay = py(0);
|
|
554
|
+
const bx = px(2);
|
|
555
|
+
const by = py(2);
|
|
556
|
+
if (g.kind === 'ellipse') {
|
|
557
|
+
const r = Math.max(Math.abs(bx - ax), Math.abs(by - ay)) / 2;
|
|
558
|
+
path.addCircle((ax + bx) / 2, (ay + by) / 2, r);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
const minX = Math.min(ax, bx);
|
|
562
|
+
const maxX = Math.max(ax, bx);
|
|
563
|
+
const minY = Math.min(ay, by);
|
|
564
|
+
const maxY = Math.max(ay, by);
|
|
565
|
+
path.moveTo(minX, minY);
|
|
566
|
+
path.lineTo(maxX, minY);
|
|
567
|
+
path.lineTo(maxX, maxY);
|
|
568
|
+
path.lineTo(minX, maxY);
|
|
569
|
+
path.close();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
// polygon (incl. triangle): the scaled outline through every point.
|
|
574
|
+
path.moveTo(px(0), py(0));
|
|
575
|
+
for (let i = 2; i < n; i += 2)
|
|
576
|
+
path.lineTo(px(i), py(i));
|
|
577
|
+
path.close();
|
|
578
|
+
}
|
|
579
|
+
return path;
|
|
580
|
+
});
|
|
581
|
+
// Live selection box: the scaled visual bounds (fixed corner ↔ scaled moving
|
|
582
|
+
// corner) padded by SELECTION_PAD, so the box tracks the resize at a constant
|
|
583
|
+
// stroke too (the transform would have warped it like the shape). Four
|
|
584
|
+
// separate derived values so each feeds its own animated <Rect> prop (Skia
|
|
585
|
+
// animates props individually — see `liveRect`).
|
|
586
|
+
const shapeResizeBoxX = useDerivedValue(() => {
|
|
587
|
+
'worklet';
|
|
588
|
+
const c = shapeResizeCtx.value;
|
|
589
|
+
const nmx = c.fx + (c.mx - c.fx) * shapeResizeScale.value.sx;
|
|
590
|
+
return Math.min(c.fx, nmx) - SELECTION_PAD;
|
|
591
|
+
});
|
|
592
|
+
const shapeResizeBoxY = useDerivedValue(() => {
|
|
593
|
+
'worklet';
|
|
594
|
+
const c = shapeResizeCtx.value;
|
|
595
|
+
const nmy = c.fy + (c.my - c.fy) * shapeResizeScale.value.sy;
|
|
596
|
+
return Math.min(c.fy, nmy) - SELECTION_PAD;
|
|
597
|
+
});
|
|
598
|
+
const shapeResizeBoxW = useDerivedValue(() => {
|
|
599
|
+
'worklet';
|
|
600
|
+
const c = shapeResizeCtx.value;
|
|
601
|
+
const nmx = c.fx + (c.mx - c.fx) * shapeResizeScale.value.sx;
|
|
602
|
+
return Math.abs(nmx - c.fx) + SELECTION_PAD * 2;
|
|
603
|
+
});
|
|
604
|
+
const shapeResizeBoxH = useDerivedValue(() => {
|
|
605
|
+
'worklet';
|
|
606
|
+
const c = shapeResizeCtx.value;
|
|
607
|
+
const nmy = c.fy + (c.my - c.fy) * shapeResizeScale.value.sy;
|
|
608
|
+
return Math.abs(nmy - c.fy) + SELECTION_PAD * 2;
|
|
609
|
+
});
|
|
423
610
|
// Per-gesture refs so we always emit a matching down/move/up sequence.
|
|
424
611
|
const pointerIdRef = useRef(1);
|
|
425
612
|
const inFlightRef = useRef(null);
|
|
@@ -463,6 +650,8 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
463
650
|
const id = pointerIdRef.current++;
|
|
464
651
|
const screen = { x: e.x, y: e.y };
|
|
465
652
|
inFlightRef.current = { id, lastScreen: screen };
|
|
653
|
+
magTouchX.value = e.x;
|
|
654
|
+
magTouchY.value = e.y;
|
|
466
655
|
stateRef.current.dispatchPointerDown(buildEvent(id, screen));
|
|
467
656
|
})
|
|
468
657
|
.onUpdate((e) => {
|
|
@@ -471,6 +660,12 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
471
660
|
return;
|
|
472
661
|
const screen = { x: e.x, y: e.y };
|
|
473
662
|
f.lastScreen = screen;
|
|
663
|
+
// Drag-to-draw tools (measurement line / rectangle) run through this
|
|
664
|
+
// JS-thread pointer path; show the loupe once the touch actually moves
|
|
665
|
+
// so a plain tap-to-place tool never flashes it.
|
|
666
|
+
magTouchX.value = e.x;
|
|
667
|
+
magTouchY.value = e.y;
|
|
668
|
+
setMagnifying(true);
|
|
474
669
|
stateRef.current.dispatchPointerMove(buildEvent(f.id, screen));
|
|
475
670
|
})
|
|
476
671
|
.onEnd((e) => {
|
|
@@ -481,6 +676,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
481
676
|
inFlightRef.current = null;
|
|
482
677
|
})
|
|
483
678
|
.onFinalize(() => {
|
|
679
|
+
setMagnifying(false);
|
|
484
680
|
if (inFlightRef.current) {
|
|
485
681
|
stateRef.current.dispatchPointerCancel();
|
|
486
682
|
inFlightRef.current = null;
|
|
@@ -530,11 +726,33 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
530
726
|
st.ctx.commit({ ops: [{ op: 'addStroke', stroke }] });
|
|
531
727
|
return;
|
|
532
728
|
}
|
|
729
|
+
// Capture the selection BEFORE this tap re-selects, so we can tell a
|
|
730
|
+
// first tap (select) from a second tap on the already-selected tile.
|
|
731
|
+
const prevSelectedId = st.ctx.selection?.ids[0] ?? null;
|
|
533
732
|
// Otherwise synthesize a down+up sequence so tools that only listen to
|
|
534
733
|
// onPointerUp (e.g. measurement stamp) still fire.
|
|
535
734
|
const id = pointerIdRef.current++;
|
|
536
735
|
st.dispatchPointerDown(buildEvent(id, screen));
|
|
537
736
|
st.dispatchPointerUp(buildEvent(id, screen));
|
|
737
|
+
// Edit-mode "tap the selected tile to open value entry": when the select
|
|
738
|
+
// tool is active the stamp overlay is non-interactive (so drag/select
|
|
739
|
+
// keep working), which means the view-mode TouchableOpacity can't catch
|
|
740
|
+
// the tap. Mirror handleViewStampPress here — a first tap only selects, a
|
|
741
|
+
// second tap on the same (already-selected) tile fires the consumer
|
|
742
|
+
// callback. Not a timed double-tap: the tile must already have been
|
|
743
|
+
// selected by a previous tap.
|
|
744
|
+
const onActivate = onStampDoubleTapRef.current;
|
|
745
|
+
if (!onActivate || !dragSelection || !prevSelectedId)
|
|
746
|
+
return;
|
|
747
|
+
const world = st.ctx.viewport.screenToWorld(screen);
|
|
748
|
+
const zoomNow = st.ctx.viewport.state.zoom;
|
|
749
|
+
const hit = dragSelection.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale);
|
|
750
|
+
if (!hit || hit.kind !== 'measurement' || hit.id !== prevSelectedId) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const placed = st.ctx.document.placedMeasurements.find((p) => p.id === hit.id);
|
|
754
|
+
if (placed)
|
|
755
|
+
onActivate(placed);
|
|
538
756
|
});
|
|
539
757
|
// Viewport pan — runs on the UI thread (no runOnJS), mutating the shared
|
|
540
758
|
// viewport directly. Mirrors viewport.ts `panBy`. Used for both the
|
|
@@ -610,6 +828,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
610
828
|
color: fh.color,
|
|
611
829
|
width: fh.width,
|
|
612
830
|
cap: fh.cap ?? 'round',
|
|
831
|
+
...(fh.startCap === 'arrow' && { startCap: 'arrow' }),
|
|
613
832
|
...(fh.dash && { dash: true }),
|
|
614
833
|
points: worldPoints,
|
|
615
834
|
createdAt: Date.now(),
|
|
@@ -637,9 +856,14 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
637
856
|
e.x / zoom.value + panX.value,
|
|
638
857
|
e.y / zoom.value + panY.value,
|
|
639
858
|
];
|
|
859
|
+
magTouchX.value = e.x;
|
|
860
|
+
magTouchY.value = e.y;
|
|
861
|
+
runOnJS(setMagnifying)(true);
|
|
640
862
|
})
|
|
641
863
|
.onChange((e) => {
|
|
642
864
|
'worklet';
|
|
865
|
+
magTouchX.value = e.x;
|
|
866
|
+
magTouchY.value = e.y;
|
|
643
867
|
const wx = e.x / zoom.value + panX.value;
|
|
644
868
|
const wy = e.y / zoom.value + panY.value;
|
|
645
869
|
const pts = livePoints.value;
|
|
@@ -675,6 +899,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
675
899
|
// Cancel path (no onEnd): drop the in-flight preview. After a normal
|
|
676
900
|
// end the buffer is already empty, so this is a no-op.
|
|
677
901
|
livePoints.value = [];
|
|
902
|
+
runOnJS(setMagnifying)(false);
|
|
678
903
|
});
|
|
679
904
|
};
|
|
680
905
|
// Shape rubber-band (line/arrow/rect/triangle/circle tools) — one finger,
|
|
@@ -694,6 +919,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
694
919
|
color: cfg.color,
|
|
695
920
|
width: cfg.width,
|
|
696
921
|
cap: cfg.cap,
|
|
922
|
+
startCap: cfg.startCap,
|
|
697
923
|
dash: cfg.dash,
|
|
698
924
|
layerId: st.ctx.document.layers[0]?.id ?? DEFAULT_LAYER_ID,
|
|
699
925
|
id,
|
|
@@ -709,12 +935,17 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
709
935
|
const wx = e.x / zoom.value + panX.value;
|
|
710
936
|
const wy = e.y / zoom.value + panY.value;
|
|
711
937
|
liveShape.value = { active: true, ax: wx, ay: wy, bx: wx, by: wy };
|
|
938
|
+
magTouchX.value = e.x;
|
|
939
|
+
magTouchY.value = e.y;
|
|
940
|
+
runOnJS(setMagnifying)(true);
|
|
712
941
|
})
|
|
713
942
|
.onChange((e) => {
|
|
714
943
|
'worklet';
|
|
715
944
|
const s = liveShape.value;
|
|
716
945
|
if (!s.active)
|
|
717
946
|
return;
|
|
947
|
+
magTouchX.value = e.x;
|
|
948
|
+
magTouchY.value = e.y;
|
|
718
949
|
liveShape.value = {
|
|
719
950
|
active: true,
|
|
720
951
|
ax: s.ax,
|
|
@@ -746,6 +977,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
746
977
|
id,
|
|
747
978
|
kind: cfg.kind,
|
|
748
979
|
cap: cfg.cap ?? 'round',
|
|
980
|
+
startCap: cfg.startCap ?? 'round',
|
|
749
981
|
ax: s.ax,
|
|
750
982
|
ay: s.ay,
|
|
751
983
|
bx: s.bx,
|
|
@@ -760,6 +992,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
760
992
|
// Cancel path (no onEnd): drop the in-flight rubber-band. After a
|
|
761
993
|
// normal end the slot is already inactive, so this is a no-op.
|
|
762
994
|
liveShape.value = { active: false, ax: 0, ay: 0, bx: 0, by: 0 };
|
|
995
|
+
runOnJS(setMagnifying)(false);
|
|
763
996
|
});
|
|
764
997
|
};
|
|
765
998
|
// Element drag (select tool) — one finger, UI thread. Hit-tests on the JS
|
|
@@ -794,6 +1027,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
794
1027
|
};
|
|
795
1028
|
epTargetRef.current = { id: selId, handle };
|
|
796
1029
|
setEpDragId(selId);
|
|
1030
|
+
setMagnifying(true);
|
|
797
1031
|
return;
|
|
798
1032
|
}
|
|
799
1033
|
}
|
|
@@ -815,6 +1049,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
815
1049
|
};
|
|
816
1050
|
shapeEpTargetRef.current = { id: selId, handle: shapeHandle };
|
|
817
1051
|
setShapeEpDragId(selId);
|
|
1052
|
+
setMagnifying(true);
|
|
818
1053
|
return;
|
|
819
1054
|
}
|
|
820
1055
|
}
|
|
@@ -831,6 +1066,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
831
1066
|
};
|
|
832
1067
|
resizeTargetRef.current = { id: selId };
|
|
833
1068
|
setResizingId(selId);
|
|
1069
|
+
setMagnifying(true);
|
|
834
1070
|
return;
|
|
835
1071
|
}
|
|
836
1072
|
// Rectangle-annotation corner handle — also selected-element-only.
|
|
@@ -844,6 +1080,35 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
844
1080
|
};
|
|
845
1081
|
rectTargetRef.current = { id: selId, corner: rectCornerHit.corner };
|
|
846
1082
|
setRectDragId(selId);
|
|
1083
|
+
setMagnifying(true);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
// Shape (rect/ellipse/polygon) bounding-box corner handle — the shape
|
|
1087
|
+
// twin of the rect-annotation corner above; scales length and width.
|
|
1088
|
+
const shapeCornerHit = cfg.hitTestShapeCorner?.(st.ctx.document, selId, world, zoomNow);
|
|
1089
|
+
if (shapeCornerHit) {
|
|
1090
|
+
const cornerShape = st.ctx.document.shapes.find((x) => x.id === selId);
|
|
1091
|
+
shapeResizeCtx.value = {
|
|
1092
|
+
fx: shapeCornerHit.fixed.x,
|
|
1093
|
+
fy: shapeCornerHit.fixed.y,
|
|
1094
|
+
mx: shapeCornerHit.moving.x,
|
|
1095
|
+
my: shapeCornerHit.moving.y,
|
|
1096
|
+
uni: cornerShape?.kind === 'ellipse' ? 1 : 0,
|
|
1097
|
+
};
|
|
1098
|
+
const flat = [];
|
|
1099
|
+
for (const p of cornerShape?.geometry.points ?? []) {
|
|
1100
|
+
flat.push(p.x, p.y);
|
|
1101
|
+
}
|
|
1102
|
+
shapeResizePts.value = {
|
|
1103
|
+
kind: cornerShape?.kind ?? '',
|
|
1104
|
+
pts: flat,
|
|
1105
|
+
};
|
|
1106
|
+
shapeResizeTargetRef.current = {
|
|
1107
|
+
id: selId,
|
|
1108
|
+
corner: shapeCornerHit.corner,
|
|
1109
|
+
};
|
|
1110
|
+
setShapeResizeId(selId);
|
|
1111
|
+
setMagnifying(true);
|
|
847
1112
|
return;
|
|
848
1113
|
}
|
|
849
1114
|
}
|
|
@@ -878,6 +1143,8 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
878
1143
|
};
|
|
879
1144
|
const endSelectDrag = (dx, dy) => {
|
|
880
1145
|
const st = stateRef.current;
|
|
1146
|
+
// Any precision sub-drag that turned the loupe on is ending now.
|
|
1147
|
+
setMagnifying(false);
|
|
881
1148
|
// Resize commit: scale the shape by the final drag (clamped in
|
|
882
1149
|
// buildResizePatch exactly like the live preview).
|
|
883
1150
|
const rT = resizeTargetRef.current;
|
|
@@ -907,6 +1174,19 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
907
1174
|
setRectDragId(null);
|
|
908
1175
|
return;
|
|
909
1176
|
}
|
|
1177
|
+
// Shape-corner commit: scale the shape by dragging the grabbed corner
|
|
1178
|
+
// (opposite corner fixed); buildShapeCornerPatch clamps as the preview.
|
|
1179
|
+
const shapeRT = shapeResizeTargetRef.current;
|
|
1180
|
+
if (shapeRT) {
|
|
1181
|
+
if (dx !== 0 || dy !== 0) {
|
|
1182
|
+
const patch = cfg.buildShapeCornerPatch?.(st.ctx.document, shapeRT.id, shapeRT.corner, { x: dx, y: dy });
|
|
1183
|
+
if (patch)
|
|
1184
|
+
st.ctx.commit(patch);
|
|
1185
|
+
}
|
|
1186
|
+
shapeResizeTargetRef.current = null;
|
|
1187
|
+
setShapeResizeId(null);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
910
1190
|
// Endpoint commit: move the grabbed endpoint by the world delta.
|
|
911
1191
|
const epT = epTargetRef.current;
|
|
912
1192
|
if (epT) {
|
|
@@ -959,6 +1239,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
959
1239
|
setDraggingId(null);
|
|
960
1240
|
};
|
|
961
1241
|
const cancelSelectDrag = () => {
|
|
1242
|
+
setMagnifying(false);
|
|
962
1243
|
// No commit — dropping the gating ids snaps everything back.
|
|
963
1244
|
dragTargetRef.current = null;
|
|
964
1245
|
slideTargetRef.current = null;
|
|
@@ -966,12 +1247,14 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
966
1247
|
shapeEpTargetRef.current = null;
|
|
967
1248
|
resizeTargetRef.current = null;
|
|
968
1249
|
rectTargetRef.current = null;
|
|
1250
|
+
shapeResizeTargetRef.current = null;
|
|
969
1251
|
setDraggingId(null);
|
|
970
1252
|
setSlidingId(null);
|
|
971
1253
|
setEpDragId(null);
|
|
972
1254
|
setShapeEpDragId(null);
|
|
973
1255
|
setResizingId(null);
|
|
974
1256
|
setRectDragId(null);
|
|
1257
|
+
setShapeResizeId(null);
|
|
975
1258
|
};
|
|
976
1259
|
return Gesture.Pan()
|
|
977
1260
|
.minPointers(1)
|
|
@@ -981,6 +1264,10 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
981
1264
|
dragEnded.value = false;
|
|
982
1265
|
dragX.value = 0;
|
|
983
1266
|
dragY.value = 0;
|
|
1267
|
+
// The loupe (turned on by beginSelectDrag only for precision sub-
|
|
1268
|
+
// drags) follows the live finger, not the hit-test origin.
|
|
1269
|
+
magTouchX.value = e.x;
|
|
1270
|
+
magTouchY.value = e.y;
|
|
984
1271
|
// Hit-test at the touch-down point — onStart fires only after the
|
|
985
1272
|
// pan threshold, so back out the accumulated translation.
|
|
986
1273
|
runOnJS(beginSelectDrag)({
|
|
@@ -990,6 +1277,8 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
990
1277
|
})
|
|
991
1278
|
.onChange((e) => {
|
|
992
1279
|
'worklet';
|
|
1280
|
+
magTouchX.value = e.x;
|
|
1281
|
+
magTouchY.value = e.y;
|
|
993
1282
|
// World-space delta from the drag origin. Applied to the element
|
|
994
1283
|
// only once draggingId is set; otherwise it affects nothing.
|
|
995
1284
|
dragX.value = e.translationX / zoom.value;
|
|
@@ -1050,72 +1339,89 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1050
1339
|
const activeTool = props.tools.find((t) => t.id === props.activeToolId) ?? null;
|
|
1051
1340
|
const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
|
|
1052
1341
|
const { renderMeasurementStamp, onMeasurementStampPress, onMeasurementStampLongPress, resolveStampMeasurement, onMeasurementStampRemove, selection, } = props;
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1342
|
+
// The Skia scene props, shared by the main canvas and the loupe overlay. The
|
|
1343
|
+
// loupe re-runs the SAME scene with a magnifying world transform + a lens
|
|
1344
|
+
// clip (see loupeWrap) in a separate top-most canvas, so it shows live vector
|
|
1345
|
+
// content (the dragged line, handles, strokes) magnified above everything.
|
|
1346
|
+
const skiaProps = {
|
|
1347
|
+
width,
|
|
1348
|
+
height,
|
|
1349
|
+
effectiveCanvas: state.effectiveCanvas,
|
|
1350
|
+
worldTransform,
|
|
1351
|
+
resolveImageUrl,
|
|
1352
|
+
backgroundSkImage,
|
|
1353
|
+
valueFont,
|
|
1354
|
+
// Freehand drawing runs on the UI thread via `livePreview`, so the
|
|
1355
|
+
// JS-state pen preview is unused on native.
|
|
1356
|
+
penDrawingStroke: null,
|
|
1357
|
+
livePreview: freehand
|
|
1358
|
+
? {
|
|
1359
|
+
path: livePath,
|
|
1360
|
+
handoffPaths: [
|
|
1361
|
+
handoffPath0,
|
|
1362
|
+
handoffPath1,
|
|
1363
|
+
handoffPath2,
|
|
1364
|
+
handoffPath3,
|
|
1365
|
+
],
|
|
1366
|
+
color: freehand.color,
|
|
1367
|
+
width: freehand.width,
|
|
1368
|
+
cap: freehand.cap ?? 'round',
|
|
1369
|
+
dash: freehand.dash ?? false,
|
|
1370
|
+
opacity: freehand.variant === 'highlighter' ? 0.3 : 1,
|
|
1371
|
+
}
|
|
1372
|
+
: null,
|
|
1373
|
+
// UI-thread shape rubber-band (live + pending handoffs) — only while a
|
|
1374
|
+
// shape tool is active (handoffs always release within a frame or two of
|
|
1375
|
+
// the commit, before the tool can change).
|
|
1376
|
+
shapePreview: shapeDraw
|
|
1377
|
+
? {
|
|
1378
|
+
path: shapeBodyPath,
|
|
1379
|
+
headPath: shapeHeadPath,
|
|
1380
|
+
color: shapeDraw.color,
|
|
1381
|
+
width: shapeDraw.width,
|
|
1382
|
+
cap: shapeDraw.cap ?? 'round',
|
|
1383
|
+
dash: shapeDraw.dash ?? false,
|
|
1384
|
+
}
|
|
1385
|
+
: null,
|
|
1386
|
+
draggingId,
|
|
1387
|
+
dragTransform,
|
|
1388
|
+
resizingId,
|
|
1389
|
+
resizeTransform,
|
|
1390
|
+
selectedId: selection?.ids[0] ?? null,
|
|
1391
|
+
endpointDragId: epDragId,
|
|
1392
|
+
shapeEndpointDragId: shapeEpDragId,
|
|
1393
|
+
liveLineP1,
|
|
1394
|
+
liveLineP2,
|
|
1395
|
+
rectDragId,
|
|
1396
|
+
liveRect: {
|
|
1397
|
+
x: liveRectX,
|
|
1398
|
+
y: liveRectY,
|
|
1399
|
+
width: liveRectW,
|
|
1400
|
+
height: liveRectH,
|
|
1401
|
+
},
|
|
1402
|
+
shapeResizeId,
|
|
1403
|
+
shapeResizePath,
|
|
1404
|
+
shapeResizeBox: {
|
|
1405
|
+
x: shapeResizeBoxX,
|
|
1406
|
+
y: shapeResizeBoxY,
|
|
1407
|
+
width: shapeResizeBoxW,
|
|
1408
|
+
height: shapeResizeBoxH,
|
|
1409
|
+
},
|
|
1410
|
+
// Endpoint/corner handles are drag affordances — only the select tool can
|
|
1411
|
+
// act on them, so suppress them when the active tool has no drag support
|
|
1412
|
+
// (e.g. view mode's pan tool, where a selected measurement still shows its
|
|
1413
|
+
// tile chrome but must not advertise draggability).
|
|
1414
|
+
handleRadius: activeTool?.dragSelection ? handleRadius : undefined,
|
|
1415
|
+
handleRingWidth: activeTool?.dragSelection ? handleRingWidth : undefined,
|
|
1416
|
+
customPreview,
|
|
1417
|
+
};
|
|
1418
|
+
// Lens window for the loupe: an opaque white base + the magnified scene
|
|
1419
|
+
// clipped to a rounded square, a border, and a crosshair marking the exact
|
|
1420
|
+
// point under the finger. Drawn in the overlay canvas's screen space (the
|
|
1421
|
+
// magnification lives in loupeTransform, applied to `content`).
|
|
1422
|
+
const loupeClip = rrect(rect(loupeX, loupeY, loupeSize, loupeSize), LOUPE_RADIUS, LOUPE_RADIUS);
|
|
1423
|
+
const loupeWrap = (content) => (_jsxs(_Fragment, { children: [_jsxs(Group, { clip: loupeClip, children: [_jsx(Rect, { x: loupeX, y: loupeY, width: loupeSize, height: loupeSize, color: LOUPE_BG_COLOR }), content] }), _jsx(RoundedRect, { x: loupeX, y: loupeY, width: loupeSize, height: loupeSize, r: LOUPE_RADIUS, color: LOUPE_BORDER_COLOR, style: "stroke", strokeWidth: LOUPE_BORDER_WIDTH }), _jsx(Line, { p1: { x: loupeCx - LOUPE_CROSSHAIR_ARM, y: loupeCy }, p2: { x: loupeCx + LOUPE_CROSSHAIR_ARM, y: loupeCy }, color: LOUPE_CROSSHAIR_COLOR, strokeWidth: 1.5 }), _jsx(Line, { p1: { x: loupeCx, y: loupeCy - LOUPE_CROSSHAIR_ARM }, p2: { x: loupeCx, y: loupeCy + LOUPE_CROSSHAIR_ARM }, color: LOUPE_CROSSHAIR_COLOR, strokeWidth: 1.5 })] }));
|
|
1424
|
+
return (_jsxs(GestureHandlerRootView, { style: [{ width, height }, style], children: [_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
|
|
1119
1425
|
? (state.measurementsById.get(placed.measurementId) ?? null)
|
|
1120
1426
|
: (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.effectiveCanvas.tileScaleFactor, tileViewportScale: state.tileViewportScale, onStampPress: onMeasurementStampPress, onStampLongPress: onMeasurementStampLongPress, onRemove: () => {
|
|
1121
1427
|
const defaultRemove = () => {
|
|
@@ -1129,7 +1435,11 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1129
1435
|
return;
|
|
1130
1436
|
}
|
|
1131
1437
|
defaultRemove();
|
|
1132
|
-
} }, placed.id))) }))
|
|
1438
|
+
} }, placed.id))) })), magnifying && (_jsx(View, { pointerEvents: "none", style: StyleSheet.absoluteFill, children: AnnotationCanvasSkia({
|
|
1439
|
+
...skiaProps,
|
|
1440
|
+
worldTransform: loupeTransform,
|
|
1441
|
+
wrapContent: loupeWrap,
|
|
1442
|
+
}) }))] }));
|
|
1133
1443
|
};
|
|
1134
1444
|
const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, rectCtx, renderMeasurementStamp, tileScaleFactor, tileViewportScale, onStampPress, onStampLongPress, onRemove, }) => {
|
|
1135
1445
|
const size = stampTileSize(placed, tileScaleFactor, tileViewportScale);
|