@reekon-tools/boldr-utils 1.6.20 → 1.6.24
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 +3 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.js +35 -8
- package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +3 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +229 -13
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +11 -1
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +51 -41
- package/dist/annotation/canvas/Tool.d.ts +8 -0
- 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/stampLayout.d.ts +2 -4
- package/dist/annotation/canvas/stampLayout.js +31 -51
- 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 +165 -16
- package/dist/annotation/canvas/tools/shapeTool.d.ts +2 -0
- package/dist/annotation/canvas/tools/shapeTool.js +13 -2
- package/dist/annotation/canvas/tools/textEditing.js +2 -2
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +8 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +40 -6
- package/dist/types/annotation.d.ts +4 -0
- package/dist/types/annotation.js +8 -2
- package/package.json +1 -1
|
@@ -24,6 +24,9 @@ export interface AnnotationCanvasInnerProps {
|
|
|
24
24
|
renderMeasurementStamp?: RenderMeasurementStamp;
|
|
25
25
|
onMeasurementStampPress?: (placed: PlacedMeasurementRef) => void;
|
|
26
26
|
onMeasurementStampLongPress?: (placed: PlacedMeasurementRef) => void;
|
|
27
|
+
onMeasurementStampDoubleTap?: (placed: PlacedMeasurementRef) => void;
|
|
28
|
+
resolveStampMeasurement?: (placed: PlacedMeasurementRef) => Measurement | null;
|
|
29
|
+
onMeasurementStampRemove?: (placed: PlacedMeasurementRef, defaultRemove: () => void) => void;
|
|
27
30
|
stampFontSource?: unknown;
|
|
28
31
|
stampValueFontSize?: number;
|
|
29
32
|
stampLabelFontSize?: number;
|
|
@@ -3,7 +3,7 @@ import { Skia, useFont, useTypeface } from '@shopify/react-native-skia';
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, } from 'react';
|
|
4
4
|
import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
|
|
5
5
|
import { TEXT_FONT_FAMILY } from './elements/ShapeElement.js';
|
|
6
|
-
import { buildRemoveMeasurementOps } from './measurementGeometry.js';
|
|
6
|
+
import { buildRemoveMeasurementOps, hitPlacedMeasurement, } from './measurementGeometry.js';
|
|
7
7
|
import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
|
|
8
8
|
import { stampTileSize } from './stampLayout.js';
|
|
9
9
|
// Screen-px radius of a measurement-annotation endpoint handle (matches the
|
|
@@ -244,8 +244,28 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
244
244
|
...style,
|
|
245
245
|
};
|
|
246
246
|
const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
|
|
247
|
-
const { renderMeasurementStamp, onMeasurementStampPress, onMeasurementStampLongPress, selection, } = props;
|
|
248
|
-
|
|
247
|
+
const { renderMeasurementStamp, onMeasurementStampPress, onMeasurementStampLongPress, onMeasurementStampDoubleTap, resolveStampMeasurement, onMeasurementStampRemove, selection, } = props;
|
|
248
|
+
// Double-click a stamp → fire the consumer callback (e.g. open its value
|
|
249
|
+
// editor). Runs the SAME hit-test the select tool uses (top-of-z-order), on
|
|
250
|
+
// the gesture container, so the stamp overlay stays pointer-events:none and
|
|
251
|
+
// drag-to-move / single-tap select are unaffected. dblclick is a distinct
|
|
252
|
+
// browser event that doesn't suppress the preceding pointerdown/up select.
|
|
253
|
+
const handleDoubleClick = useCallback((event) => {
|
|
254
|
+
if (!onMeasurementStampDoubleTap)
|
|
255
|
+
return;
|
|
256
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
257
|
+
const screen = {
|
|
258
|
+
x: event.clientX - (rect?.left ?? 0),
|
|
259
|
+
y: event.clientY - (rect?.top ?? 0),
|
|
260
|
+
};
|
|
261
|
+
const world = state.ctx.viewport.screenToWorld(screen);
|
|
262
|
+
const placed = [...state.effectiveCanvas.placedMeasurements]
|
|
263
|
+
.reverse()
|
|
264
|
+
.find((m) => hitPlacedMeasurement(m, world, state.viewport.zoom, state.effectiveCanvas.tileScaleFactor, state.tileViewportScale));
|
|
265
|
+
if (placed)
|
|
266
|
+
onMeasurementStampDoubleTap(placed);
|
|
267
|
+
}, [onMeasurementStampDoubleTap, state]);
|
|
268
|
+
return (_jsxs("div", { ref: containerRef, style: containerStyle, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerCancel, onDoubleClick: handleDoubleClick, onWheel: handleWheel, onContextMenu: handleContextMenu, children: [AnnotationCanvasSkia({
|
|
249
269
|
width,
|
|
250
270
|
height,
|
|
251
271
|
effectiveCanvas: state.effectiveCanvas,
|
|
@@ -279,7 +299,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
279
299
|
const isSelected = selection?.ids.includes(placed.id) ?? false;
|
|
280
300
|
const measurement = placed.measurementId
|
|
281
301
|
? (state.measurementsById.get(placed.measurementId) ?? null)
|
|
282
|
-
: null;
|
|
302
|
+
: (resolveStampMeasurement?.(placed) ?? null);
|
|
283
303
|
// Corner-pinned, tile-proportional remove target (see the style
|
|
284
304
|
// comment below) so it can't blanket a small tile and eat its grab.
|
|
285
305
|
const removeSize = Math.min(40, size * 0.4);
|
|
@@ -300,10 +320,17 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
300
320
|
? () => onMeasurementStampLongPress(placed)
|
|
301
321
|
: undefined })), isSelected && measurement && (_jsx("div", { role: "button", "aria-label": "Remove measurement", onPointerDown: (e) => {
|
|
302
322
|
e.stopPropagation();
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
323
|
+
const defaultRemove = () => {
|
|
324
|
+
const { ops, keepSelection } = buildRemoveMeasurementOps(placed);
|
|
325
|
+
state.ctx.commit({ ops });
|
|
326
|
+
if (!keepSelection)
|
|
327
|
+
state.ctx.setSelection(null);
|
|
328
|
+
};
|
|
329
|
+
if (onMeasurementStampRemove) {
|
|
330
|
+
onMeasurementStampRemove(placed, defaultRemove);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
defaultRemove();
|
|
307
334
|
}, style: {
|
|
308
335
|
// Sized as a fraction of the tile and corner-pinned so it
|
|
309
336
|
// never covers the center. A fixed 40px target blanketed
|
|
@@ -25,6 +25,9 @@ export interface AnnotationCanvasInnerProps {
|
|
|
25
25
|
renderMeasurementStamp?: RenderMeasurementStamp;
|
|
26
26
|
onMeasurementStampPress?: (placed: PlacedMeasurementRef) => void;
|
|
27
27
|
onMeasurementStampLongPress?: (placed: PlacedMeasurementRef) => void;
|
|
28
|
+
onMeasurementStampDoubleTap?: (placed: PlacedMeasurementRef) => void;
|
|
29
|
+
resolveStampMeasurement?: (placed: PlacedMeasurementRef) => Measurement | null;
|
|
30
|
+
onMeasurementStampRemove?: (placed: PlacedMeasurementRef, defaultRemove: () => void) => void;
|
|
28
31
|
stampFontSource?: unknown;
|
|
29
32
|
stampValueFontSize?: number;
|
|
30
33
|
stampLabelFontSize?: number;
|
|
@@ -9,6 +9,7 @@ import { DEFAULT_LAYER_ID, } from '../../types/annotation.js';
|
|
|
9
9
|
import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
|
|
10
10
|
import { buildRemoveMeasurementOps, } from './measurementGeometry.js';
|
|
11
11
|
import { buildShapeFromDrag } from './tools/shapeTool.js';
|
|
12
|
+
import { SELECTION_PAD } from './textGeometry.js';
|
|
12
13
|
import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
|
|
13
14
|
let strokeCounter = 0;
|
|
14
15
|
const makeStrokeId = () => `stroke-${Date.now().toString(36)}-${(strokeCounter++).toString(36)}`;
|
|
@@ -24,6 +25,10 @@ const HANDLE_RADIUS_PX = 7;
|
|
|
24
25
|
// Screen-px stroke width of the handle's colored ring (white-disc + ring, so the
|
|
25
26
|
// knob stays legible over a line of any color). Also zoom-divided.
|
|
26
27
|
const HANDLE_RING_PX = 2;
|
|
28
|
+
// Doc-space floor on a shape-corner resize's width/height — the worklet twin of
|
|
29
|
+
// selectTool's MIN_SHAPE_EXTENT (the live preview must clamp identically to the
|
|
30
|
+
// buildShapeCornerPatch commit). Keep the two in sync.
|
|
31
|
+
const MIN_SHAPE_EXTENT = 1;
|
|
27
32
|
// Native fingerprint: one finger drives the active tool, two fingers
|
|
28
33
|
// pan/zoom the viewport. Tap counts as a brief pointer down+up so tools
|
|
29
34
|
// like measurement-stamp (which only listen to onPointerUp) work via tap.
|
|
@@ -43,6 +48,11 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
43
48
|
// never rebuilt mid-gesture — its JS callbacks read `stateRef.current`.
|
|
44
49
|
const stateRef = useRef(state);
|
|
45
50
|
stateRef.current = state;
|
|
51
|
+
// Open-value-entry callback (edit mode). Rides a ref so the gesture needn't
|
|
52
|
+
// rebuild when it changes; the second-tap-on-selected detection lives in the
|
|
53
|
+
// tap gesture below (it compares against the selection captured pre-tap).
|
|
54
|
+
const onStampDoubleTapRef = useRef(props.onMeasurementStampDoubleTap);
|
|
55
|
+
onStampDoubleTapRef.current = props.onMeasurementStampDoubleTap;
|
|
46
56
|
// Live viewport on the UI thread. Initialised from the JS snapshot; kept in
|
|
47
57
|
// sync from JS only when not actively gesturing (see the effect below).
|
|
48
58
|
const zoom = useSharedValue(state.viewport.zoom);
|
|
@@ -179,13 +189,14 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
179
189
|
// Live mirror of the active shape tool's config for the worklet path
|
|
180
190
|
// builders (the derived values are created once, so they can't close over
|
|
181
191
|
// the changing `shapeDraw` prop).
|
|
182
|
-
const shapeCfg = useSharedValue({ kind: 'line', cap: 'round', width: 2 });
|
|
192
|
+
const shapeCfg = useSharedValue({ kind: 'line', cap: 'round', startCap: 'round', width: 2 });
|
|
183
193
|
useEffect(() => {
|
|
184
194
|
if (!shapeDraw)
|
|
185
195
|
return;
|
|
186
196
|
shapeCfg.value = {
|
|
187
197
|
kind: shapeDraw.kind,
|
|
188
198
|
cap: shapeDraw.cap ?? 'round',
|
|
199
|
+
startCap: shapeDraw.startCap ?? 'round',
|
|
189
200
|
width: shapeDraw.width,
|
|
190
201
|
};
|
|
191
202
|
}, [shapeDraw, shapeCfg]);
|
|
@@ -256,15 +267,20 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
256
267
|
path.close();
|
|
257
268
|
};
|
|
258
269
|
for (const e of handoffShapes.value) {
|
|
259
|
-
if (e.kind
|
|
270
|
+
if (e.kind !== 'line')
|
|
271
|
+
continue;
|
|
272
|
+
// End head at b (points a→b); start head at a (points b→a, args swapped).
|
|
273
|
+
if (e.cap === 'arrow')
|
|
260
274
|
addHead(e.ax, e.ay, e.bx, e.by);
|
|
261
|
-
|
|
275
|
+
if (e.startCap === 'arrow')
|
|
276
|
+
addHead(e.bx, e.by, e.ax, e.ay);
|
|
262
277
|
}
|
|
263
278
|
const s = liveShape.value;
|
|
264
|
-
if (s.active &&
|
|
265
|
-
shapeCfg.value.
|
|
266
|
-
|
|
267
|
-
|
|
279
|
+
if (s.active && shapeCfg.value.kind === 'line') {
|
|
280
|
+
if (shapeCfg.value.cap === 'arrow')
|
|
281
|
+
addHead(s.ax, s.ay, s.bx, s.by);
|
|
282
|
+
if (shapeCfg.value.startCap === 'arrow')
|
|
283
|
+
addHead(s.bx, s.by, s.ax, s.ay);
|
|
268
284
|
}
|
|
269
285
|
return path;
|
|
270
286
|
});
|
|
@@ -420,6 +436,124 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
420
436
|
{ translateY: -c.py },
|
|
421
437
|
];
|
|
422
438
|
});
|
|
439
|
+
// Geometric-shape corner resize (rect/ellipse/polygon). `shapeResizeId`
|
|
440
|
+
// (React state) gates which shape renders live; `shapeResizeCtx` carries the
|
|
441
|
+
// fixed (opposite) corner and the grabbed corner's start position (from
|
|
442
|
+
// DragSelectionConfig.hitTestShapeCorner), and `shapeResizePts` the shape's
|
|
443
|
+
// start geometry. The preview re-renders the shape from live GEOMETRY (a
|
|
444
|
+
// derived path) rather than a scale transform — a non-uniform scale transform
|
|
445
|
+
// would warp the stroke width (top/bottom edges thicken with scaleY); drawing
|
|
446
|
+
// the scaled outline at a constant stroke keeps it crisp. The commit on
|
|
447
|
+
// release goes through buildShapeCornerPatch, which scales identically.
|
|
448
|
+
const [shapeResizeId, setShapeResizeId] = useState(null);
|
|
449
|
+
const shapeResizeCtx = useSharedValue({ fx: 0, fy: 0, mx: 0, my: 0, uni: 0 });
|
|
450
|
+
// The resized shape's start geometry: kind + flat [x,y,x,y,…] points.
|
|
451
|
+
const shapeResizePts = useSharedValue({
|
|
452
|
+
kind: '',
|
|
453
|
+
pts: [],
|
|
454
|
+
});
|
|
455
|
+
const shapeResizeTargetRef = useRef(null);
|
|
456
|
+
// Live scale factors about the fixed corner. WORKLET TWIN of
|
|
457
|
+
// selectTool.shapeCornerPatch — keep the clamp + the ellipse uniform-scale
|
|
458
|
+
// branch in sync. Read by the path + box derived values below.
|
|
459
|
+
const shapeResizeScale = useDerivedValue(() => {
|
|
460
|
+
'worklet';
|
|
461
|
+
const c = shapeResizeCtx.value;
|
|
462
|
+
const denomX = c.mx - c.fx;
|
|
463
|
+
const denomY = c.my - c.fy;
|
|
464
|
+
let offX = c.mx + dragX.value - c.fx;
|
|
465
|
+
let offY = c.my + dragY.value - c.fy;
|
|
466
|
+
offX =
|
|
467
|
+
denomX >= 0
|
|
468
|
+
? Math.max(MIN_SHAPE_EXTENT, offX)
|
|
469
|
+
: Math.min(-MIN_SHAPE_EXTENT, offX);
|
|
470
|
+
offY =
|
|
471
|
+
denomY >= 0
|
|
472
|
+
? Math.max(MIN_SHAPE_EXTENT, offY)
|
|
473
|
+
: Math.min(-MIN_SHAPE_EXTENT, offY);
|
|
474
|
+
let sx = denomX !== 0 ? offX / denomX : 1;
|
|
475
|
+
let sy = denomY !== 0 ? offY / denomY : 1;
|
|
476
|
+
if (c.uni === 1) {
|
|
477
|
+
const oldDiag = Math.sqrt(denomX * denomX + denomY * denomY);
|
|
478
|
+
const s0 = oldDiag !== 0 ? Math.sqrt(offX * offX + offY * offY) / oldDiag : 1;
|
|
479
|
+
sx = s0;
|
|
480
|
+
sy = s0;
|
|
481
|
+
}
|
|
482
|
+
return { sx, sy };
|
|
483
|
+
});
|
|
484
|
+
// The shape's outline scaled about the fixed corner, rebuilt per frame.
|
|
485
|
+
// WORKLET TWIN of ShapeElement's per-kind rendering (rect/ellipse/polygon) —
|
|
486
|
+
// keep in sync.
|
|
487
|
+
const shapeResizePath = useDerivedValue(() => {
|
|
488
|
+
'worklet';
|
|
489
|
+
const path = Skia.Path.Make();
|
|
490
|
+
const g = shapeResizePts.value;
|
|
491
|
+
const c = shapeResizeCtx.value;
|
|
492
|
+
const { sx, sy } = shapeResizeScale.value;
|
|
493
|
+
const n = g.pts.length;
|
|
494
|
+
if (n < 4)
|
|
495
|
+
return path;
|
|
496
|
+
const px = (i) => c.fx + (g.pts[i] - c.fx) * sx;
|
|
497
|
+
const py = (i) => c.fy + (g.pts[i + 1] - c.fy) * sy;
|
|
498
|
+
if (g.kind === 'rect' || g.kind === 'ellipse') {
|
|
499
|
+
const ax = px(0);
|
|
500
|
+
const ay = py(0);
|
|
501
|
+
const bx = px(2);
|
|
502
|
+
const by = py(2);
|
|
503
|
+
if (g.kind === 'ellipse') {
|
|
504
|
+
const r = Math.max(Math.abs(bx - ax), Math.abs(by - ay)) / 2;
|
|
505
|
+
path.addCircle((ax + bx) / 2, (ay + by) / 2, r);
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
const minX = Math.min(ax, bx);
|
|
509
|
+
const maxX = Math.max(ax, bx);
|
|
510
|
+
const minY = Math.min(ay, by);
|
|
511
|
+
const maxY = Math.max(ay, by);
|
|
512
|
+
path.moveTo(minX, minY);
|
|
513
|
+
path.lineTo(maxX, minY);
|
|
514
|
+
path.lineTo(maxX, maxY);
|
|
515
|
+
path.lineTo(minX, maxY);
|
|
516
|
+
path.close();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
// polygon (incl. triangle): the scaled outline through every point.
|
|
521
|
+
path.moveTo(px(0), py(0));
|
|
522
|
+
for (let i = 2; i < n; i += 2)
|
|
523
|
+
path.lineTo(px(i), py(i));
|
|
524
|
+
path.close();
|
|
525
|
+
}
|
|
526
|
+
return path;
|
|
527
|
+
});
|
|
528
|
+
// Live selection box: the scaled visual bounds (fixed corner ↔ scaled moving
|
|
529
|
+
// corner) padded by SELECTION_PAD, so the box tracks the resize at a constant
|
|
530
|
+
// stroke too (the transform would have warped it like the shape). Four
|
|
531
|
+
// separate derived values so each feeds its own animated <Rect> prop (Skia
|
|
532
|
+
// animates props individually — see `liveRect`).
|
|
533
|
+
const shapeResizeBoxX = useDerivedValue(() => {
|
|
534
|
+
'worklet';
|
|
535
|
+
const c = shapeResizeCtx.value;
|
|
536
|
+
const nmx = c.fx + (c.mx - c.fx) * shapeResizeScale.value.sx;
|
|
537
|
+
return Math.min(c.fx, nmx) - SELECTION_PAD;
|
|
538
|
+
});
|
|
539
|
+
const shapeResizeBoxY = useDerivedValue(() => {
|
|
540
|
+
'worklet';
|
|
541
|
+
const c = shapeResizeCtx.value;
|
|
542
|
+
const nmy = c.fy + (c.my - c.fy) * shapeResizeScale.value.sy;
|
|
543
|
+
return Math.min(c.fy, nmy) - SELECTION_PAD;
|
|
544
|
+
});
|
|
545
|
+
const shapeResizeBoxW = useDerivedValue(() => {
|
|
546
|
+
'worklet';
|
|
547
|
+
const c = shapeResizeCtx.value;
|
|
548
|
+
const nmx = c.fx + (c.mx - c.fx) * shapeResizeScale.value.sx;
|
|
549
|
+
return Math.abs(nmx - c.fx) + SELECTION_PAD * 2;
|
|
550
|
+
});
|
|
551
|
+
const shapeResizeBoxH = useDerivedValue(() => {
|
|
552
|
+
'worklet';
|
|
553
|
+
const c = shapeResizeCtx.value;
|
|
554
|
+
const nmy = c.fy + (c.my - c.fy) * shapeResizeScale.value.sy;
|
|
555
|
+
return Math.abs(nmy - c.fy) + SELECTION_PAD * 2;
|
|
556
|
+
});
|
|
423
557
|
// Per-gesture refs so we always emit a matching down/move/up sequence.
|
|
424
558
|
const pointerIdRef = useRef(1);
|
|
425
559
|
const inFlightRef = useRef(null);
|
|
@@ -530,11 +664,33 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
530
664
|
st.ctx.commit({ ops: [{ op: 'addStroke', stroke }] });
|
|
531
665
|
return;
|
|
532
666
|
}
|
|
667
|
+
// Capture the selection BEFORE this tap re-selects, so we can tell a
|
|
668
|
+
// first tap (select) from a second tap on the already-selected tile.
|
|
669
|
+
const prevSelectedId = st.ctx.selection?.ids[0] ?? null;
|
|
533
670
|
// Otherwise synthesize a down+up sequence so tools that only listen to
|
|
534
671
|
// onPointerUp (e.g. measurement stamp) still fire.
|
|
535
672
|
const id = pointerIdRef.current++;
|
|
536
673
|
st.dispatchPointerDown(buildEvent(id, screen));
|
|
537
674
|
st.dispatchPointerUp(buildEvent(id, screen));
|
|
675
|
+
// Edit-mode "tap the selected tile to open value entry": when the select
|
|
676
|
+
// tool is active the stamp overlay is non-interactive (so drag/select
|
|
677
|
+
// keep working), which means the view-mode TouchableOpacity can't catch
|
|
678
|
+
// the tap. Mirror handleViewStampPress here — a first tap only selects, a
|
|
679
|
+
// second tap on the same (already-selected) tile fires the consumer
|
|
680
|
+
// callback. Not a timed double-tap: the tile must already have been
|
|
681
|
+
// selected by a previous tap.
|
|
682
|
+
const onActivate = onStampDoubleTapRef.current;
|
|
683
|
+
if (!onActivate || !dragSelection || !prevSelectedId)
|
|
684
|
+
return;
|
|
685
|
+
const world = st.ctx.viewport.screenToWorld(screen);
|
|
686
|
+
const zoomNow = st.ctx.viewport.state.zoom;
|
|
687
|
+
const hit = dragSelection.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale);
|
|
688
|
+
if (!hit || hit.kind !== 'measurement' || hit.id !== prevSelectedId) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const placed = st.ctx.document.placedMeasurements.find((p) => p.id === hit.id);
|
|
692
|
+
if (placed)
|
|
693
|
+
onActivate(placed);
|
|
538
694
|
});
|
|
539
695
|
// Viewport pan — runs on the UI thread (no runOnJS), mutating the shared
|
|
540
696
|
// viewport directly. Mirrors viewport.ts `panBy`. Used for both the
|
|
@@ -610,6 +766,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
610
766
|
color: fh.color,
|
|
611
767
|
width: fh.width,
|
|
612
768
|
cap: fh.cap ?? 'round',
|
|
769
|
+
...(fh.startCap === 'arrow' && { startCap: 'arrow' }),
|
|
613
770
|
...(fh.dash && { dash: true }),
|
|
614
771
|
points: worldPoints,
|
|
615
772
|
createdAt: Date.now(),
|
|
@@ -694,6 +851,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
694
851
|
color: cfg.color,
|
|
695
852
|
width: cfg.width,
|
|
696
853
|
cap: cfg.cap,
|
|
854
|
+
startCap: cfg.startCap,
|
|
697
855
|
dash: cfg.dash,
|
|
698
856
|
layerId: st.ctx.document.layers[0]?.id ?? DEFAULT_LAYER_ID,
|
|
699
857
|
id,
|
|
@@ -746,6 +904,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
746
904
|
id,
|
|
747
905
|
kind: cfg.kind,
|
|
748
906
|
cap: cfg.cap ?? 'round',
|
|
907
|
+
startCap: cfg.startCap ?? 'round',
|
|
749
908
|
ax: s.ax,
|
|
750
909
|
ay: s.ay,
|
|
751
910
|
bx: s.bx,
|
|
@@ -846,6 +1005,33 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
846
1005
|
setRectDragId(selId);
|
|
847
1006
|
return;
|
|
848
1007
|
}
|
|
1008
|
+
// Shape (rect/ellipse/polygon) bounding-box corner handle — the shape
|
|
1009
|
+
// twin of the rect-annotation corner above; scales length and width.
|
|
1010
|
+
const shapeCornerHit = cfg.hitTestShapeCorner?.(st.ctx.document, selId, world, zoomNow);
|
|
1011
|
+
if (shapeCornerHit) {
|
|
1012
|
+
const cornerShape = st.ctx.document.shapes.find((x) => x.id === selId);
|
|
1013
|
+
shapeResizeCtx.value = {
|
|
1014
|
+
fx: shapeCornerHit.fixed.x,
|
|
1015
|
+
fy: shapeCornerHit.fixed.y,
|
|
1016
|
+
mx: shapeCornerHit.moving.x,
|
|
1017
|
+
my: shapeCornerHit.moving.y,
|
|
1018
|
+
uni: cornerShape?.kind === 'ellipse' ? 1 : 0,
|
|
1019
|
+
};
|
|
1020
|
+
const flat = [];
|
|
1021
|
+
for (const p of cornerShape?.geometry.points ?? []) {
|
|
1022
|
+
flat.push(p.x, p.y);
|
|
1023
|
+
}
|
|
1024
|
+
shapeResizePts.value = {
|
|
1025
|
+
kind: cornerShape?.kind ?? '',
|
|
1026
|
+
pts: flat,
|
|
1027
|
+
};
|
|
1028
|
+
shapeResizeTargetRef.current = {
|
|
1029
|
+
id: selId,
|
|
1030
|
+
corner: shapeCornerHit.corner,
|
|
1031
|
+
};
|
|
1032
|
+
setShapeResizeId(selId);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
849
1035
|
}
|
|
850
1036
|
const hit = cfg.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale);
|
|
851
1037
|
if (!hit) {
|
|
@@ -907,6 +1093,19 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
907
1093
|
setRectDragId(null);
|
|
908
1094
|
return;
|
|
909
1095
|
}
|
|
1096
|
+
// Shape-corner commit: scale the shape by dragging the grabbed corner
|
|
1097
|
+
// (opposite corner fixed); buildShapeCornerPatch clamps as the preview.
|
|
1098
|
+
const shapeRT = shapeResizeTargetRef.current;
|
|
1099
|
+
if (shapeRT) {
|
|
1100
|
+
if (dx !== 0 || dy !== 0) {
|
|
1101
|
+
const patch = cfg.buildShapeCornerPatch?.(st.ctx.document, shapeRT.id, shapeRT.corner, { x: dx, y: dy });
|
|
1102
|
+
if (patch)
|
|
1103
|
+
st.ctx.commit(patch);
|
|
1104
|
+
}
|
|
1105
|
+
shapeResizeTargetRef.current = null;
|
|
1106
|
+
setShapeResizeId(null);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
910
1109
|
// Endpoint commit: move the grabbed endpoint by the world delta.
|
|
911
1110
|
const epT = epTargetRef.current;
|
|
912
1111
|
if (epT) {
|
|
@@ -966,12 +1165,14 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
966
1165
|
shapeEpTargetRef.current = null;
|
|
967
1166
|
resizeTargetRef.current = null;
|
|
968
1167
|
rectTargetRef.current = null;
|
|
1168
|
+
shapeResizeTargetRef.current = null;
|
|
969
1169
|
setDraggingId(null);
|
|
970
1170
|
setSlidingId(null);
|
|
971
1171
|
setEpDragId(null);
|
|
972
1172
|
setShapeEpDragId(null);
|
|
973
1173
|
setResizingId(null);
|
|
974
1174
|
setRectDragId(null);
|
|
1175
|
+
setShapeResizeId(null);
|
|
975
1176
|
};
|
|
976
1177
|
return Gesture.Pan()
|
|
977
1178
|
.minPointers(1)
|
|
@@ -1049,7 +1250,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1049
1250
|
]);
|
|
1050
1251
|
const activeTool = props.tools.find((t) => t.id === props.activeToolId) ?? null;
|
|
1051
1252
|
const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
|
|
1052
|
-
const { renderMeasurementStamp, onMeasurementStampPress, onMeasurementStampLongPress, selection, } = props;
|
|
1253
|
+
const { renderMeasurementStamp, onMeasurementStampPress, onMeasurementStampLongPress, resolveStampMeasurement, onMeasurementStampRemove, selection, } = props;
|
|
1053
1254
|
return (_jsxs(GestureHandlerRootView, { style: [{ width, height }, style], children: [_jsx(GestureDetector, { gesture: gesture, children: _jsx(View, { style: { width, height }, collapsable: false, children: AnnotationCanvasSkia({
|
|
1054
1255
|
width,
|
|
1055
1256
|
height,
|
|
@@ -1105,6 +1306,14 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1105
1306
|
width: liveRectW,
|
|
1106
1307
|
height: liveRectH,
|
|
1107
1308
|
},
|
|
1309
|
+
shapeResizeId,
|
|
1310
|
+
shapeResizePath,
|
|
1311
|
+
shapeResizeBox: {
|
|
1312
|
+
x: shapeResizeBoxX,
|
|
1313
|
+
y: shapeResizeBoxY,
|
|
1314
|
+
width: shapeResizeBoxW,
|
|
1315
|
+
height: shapeResizeBoxH,
|
|
1316
|
+
},
|
|
1108
1317
|
// Endpoint/corner handles are drag affordances — only the select
|
|
1109
1318
|
// tool can act on them, so suppress them when the active tool has
|
|
1110
1319
|
// no drag support (e.g. view mode's pan tool, where a selected
|
|
@@ -1117,11 +1326,18 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1117
1326
|
customPreview,
|
|
1118
1327
|
}) }) }), renderMeasurementStamp && (_jsx(View, { pointerEvents: "box-none", style: StyleSheet.absoluteFill, children: state.effectiveCanvas.placedMeasurements.map((placed) => (_jsx(MeasurementStampOverlayItem, { placed: placed, measurement: placed.measurementId
|
|
1119
1328
|
? (state.measurementsById.get(placed.measurementId) ?? null)
|
|
1120
|
-
: 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
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1329
|
+
: (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: () => {
|
|
1330
|
+
const defaultRemove = () => {
|
|
1331
|
+
const { ops, keepSelection } = buildRemoveMeasurementOps(placed);
|
|
1332
|
+
state.ctx.commit({ ops });
|
|
1333
|
+
if (!keepSelection)
|
|
1334
|
+
state.ctx.setSelection(null);
|
|
1335
|
+
};
|
|
1336
|
+
if (onMeasurementStampRemove) {
|
|
1337
|
+
onMeasurementStampRemove(placed, defaultRemove);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
defaultRemove();
|
|
1125
1341
|
} }, placed.id))) }))] }));
|
|
1126
1342
|
};
|
|
1127
1343
|
const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, rectCtx, renderMeasurementStamp, tileScaleFactor, tileViewportScale, onStampPress, onStampLongPress, onRemove, }) => {
|
|
@@ -69,6 +69,16 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
69
69
|
width: AnimatedNumber;
|
|
70
70
|
height: AnimatedNumber;
|
|
71
71
|
};
|
|
72
|
+
shapeResizeId?: string | null;
|
|
73
|
+
shapeResizePath?: SkPath | {
|
|
74
|
+
value: SkPath;
|
|
75
|
+
};
|
|
76
|
+
shapeResizeBox?: {
|
|
77
|
+
x: AnimatedNumber;
|
|
78
|
+
y: AnimatedNumber;
|
|
79
|
+
width: AnimatedNumber;
|
|
80
|
+
height: AnimatedNumber;
|
|
81
|
+
};
|
|
72
82
|
handleRadius?: number | {
|
|
73
83
|
value: number;
|
|
74
84
|
};
|
|
@@ -77,5 +87,5 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
77
87
|
};
|
|
78
88
|
customPreview?: ReactNode;
|
|
79
89
|
}
|
|
80
|
-
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, resolveImageUrl, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, handleRadius, handleRingWidth, customPreview, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
90
|
+
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, resolveImageUrl, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, customPreview, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
81
91
|
export {};
|