@reekon-tools/boldr-utils 1.19.6 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/annotation/canvas/AnnotationCanvasInner.d.ts +1 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.js +10 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +1 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +214 -29
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +7 -1
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +11 -2
- package/dist/annotation/canvas/Tool.d.ts +6 -0
- package/dist/annotation/canvas/snapGeometry.d.ts +32 -0
- package/dist/annotation/canvas/snapGeometry.js +168 -0
- package/dist/annotation/canvas/tools/measurementTool.js +38 -4
- package/dist/annotation/canvas/tools/selectTool.js +43 -1
- package/dist/annotation/canvas/tools/shapeTool.js +27 -5
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +2 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +16 -1
- package/dist/exports.d.ts +2 -1
- package/dist/exports.js +1 -0
- package/package.json +1 -1
- package/dist/canvas/AnnotationCanvas.d.ts +0 -11
- package/dist/canvas/AnnotationCanvas.js +0 -10
- package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
- package/dist/canvas/AnnotationCanvas.native.js +0 -6
- package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
- package/dist/canvas/AnnotationCanvasInner.js +0 -219
- package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
- package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
- package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
- package/dist/canvas/AnnotationCanvasSkia.js +0 -20
- package/dist/canvas/Tool.d.ts +0 -38
- package/dist/canvas/Tool.js +0 -1
- package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
- package/dist/canvas/elements/BackgroundImageElement.js +0 -37
- package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
- package/dist/canvas/elements/MeasurementStampElement.js +0 -30
- package/dist/canvas/elements/ShapeElement.d.ts +0 -7
- package/dist/canvas/elements/ShapeElement.js +0 -62
- package/dist/canvas/elements/StrokeElement.d.ts +0 -7
- package/dist/canvas/elements/StrokeElement.js +0 -18
- package/dist/canvas/measurementPicker.d.ts +0 -10
- package/dist/canvas/measurementPicker.js +0 -1
- package/dist/canvas/measurementStampOverlay.d.ts +0 -11
- package/dist/canvas/measurementStampOverlay.js +0 -1
- package/dist/canvas/pointerAdapter.d.ts +0 -3
- package/dist/canvas/pointerAdapter.js +0 -19
- package/dist/canvas/stampLayout.d.ts +0 -5
- package/dist/canvas/stampLayout.js +0 -14
- package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
- package/dist/canvas/tools/measurementStampTool.js +0 -37
- package/dist/canvas/tools/panTool.d.ts +0 -5
- package/dist/canvas/tools/panTool.js +0 -25
- package/dist/canvas/tools/penTool.d.ts +0 -13
- package/dist/canvas/tools/penTool.js +0 -68
- package/dist/canvas/tools/selectTool.d.ts +0 -2
- package/dist/canvas/tools/selectTool.js +0 -182
- package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
- package/dist/canvas/useAnnotationCanvasState.js +0 -210
- package/dist/canvas/viewport.d.ts +0 -16
- package/dist/canvas/viewport.js +0 -54
- package/dist/data/AnnotationDataContext.d.ts +0 -8
- package/dist/data/AnnotationDataContext.js +0 -11
- package/dist/data/AnnotationDataProvider.d.ts +0 -65
- package/dist/data/AnnotationDataProvider.js +0 -4
- package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
- package/dist/data/InMemoryAnnotationProvider.js +0 -197
- package/dist/data/canvasPersistence.d.ts +0 -3
- package/dist/data/canvasPersistence.js +0 -26
- package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
- package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
- package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
- package/dist/data/hooks/useAnnotationDoc.js +0 -33
- package/dist/data/hooks/useAnnotationList.d.ts +0 -7
- package/dist/data/hooks/useAnnotationList.js +0 -26
- package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
- package/dist/data/hooks/useAnnotationMutations.js +0 -11
- package/dist/hooks/useParseMeasurement.d.ts +0 -4
- package/dist/hooks/useParseMeasurement.js +0 -14
- package/dist/utils/evaluateFormula.d.ts +0 -20
- package/dist/utils/evaluateFormula.js +0 -31
|
@@ -22,6 +22,7 @@ export interface AnnotationCanvasInnerProps {
|
|
|
22
22
|
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
23
23
|
deferBackgroundToResolver?: boolean;
|
|
24
24
|
hideLinkBadges?: boolean;
|
|
25
|
+
snapToGeometry?: boolean;
|
|
25
26
|
pickMeasurement?: () => Promise<MeasurementRef | null>;
|
|
26
27
|
requestTextInput?: RequestTextInput;
|
|
27
28
|
renderBackground?: RenderCanvasBackground;
|
|
@@ -6,6 +6,7 @@ import { TEXT_FONT_FAMILY } from './elements/ShapeElement.js';
|
|
|
6
6
|
import { buildRemoveMeasurementOps, hitPlacedMeasurement, } from './measurementGeometry.js';
|
|
7
7
|
import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
|
|
8
8
|
import { stampTileDims } from './stampLayout.js';
|
|
9
|
+
import { SNAP_RING_PX, SNAP_RING_WIDTH_PX } from './snapGeometry.js';
|
|
9
10
|
// Screen-px radius of a measurement-annotation endpoint handle (matches the
|
|
10
11
|
// native HANDLE_RADIUS_PX). Divided by zoom for a constant on-screen size.
|
|
11
12
|
const HANDLE_PX = 7;
|
|
@@ -338,6 +339,15 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
338
339
|
? HANDLE_RING_PX / state.viewport.zoom
|
|
339
340
|
: undefined,
|
|
340
341
|
showPlaneHandles: activeTool?.planeEditing === true,
|
|
342
|
+
// Magnetic-snap ring for the JS-path drags (select / measurement /
|
|
343
|
+
// shape tools report the target through ctx.snap.setIndicator).
|
|
344
|
+
snapIndicator: state.snapIndicator
|
|
345
|
+
? {
|
|
346
|
+
c: state.snapIndicator,
|
|
347
|
+
r: SNAP_RING_PX / state.viewport.zoom,
|
|
348
|
+
ringWidth: SNAP_RING_WIDTH_PX / state.viewport.zoom,
|
|
349
|
+
}
|
|
350
|
+
: null,
|
|
341
351
|
customPreview,
|
|
342
352
|
}), renderMeasurementStamp && (_jsx("div", { style: {
|
|
343
353
|
position: 'absolute',
|
|
@@ -23,6 +23,7 @@ export interface AnnotationCanvasInnerProps {
|
|
|
23
23
|
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
24
24
|
deferBackgroundToResolver?: boolean;
|
|
25
25
|
hideLinkBadges?: boolean;
|
|
26
|
+
snapToGeometry?: boolean;
|
|
26
27
|
pickMeasurement?: () => Promise<MeasurementRef | null>;
|
|
27
28
|
requestTextInput?: RequestTextInput;
|
|
28
29
|
renderBackground?: RenderCanvasBackground;
|
|
@@ -11,6 +11,7 @@ import { MIN_BACKGROUND_DOC_EXTENT, backgroundLayersOf, backgroundLayerDocRect,
|
|
|
11
11
|
import { BackgroundSkImageLoader } from './elements/BackgroundImageElement.js';
|
|
12
12
|
import { buildRemoveMeasurementOps, linePosOf, } from './measurementGeometry.js';
|
|
13
13
|
import { planeEdgeLine, planeGridSegments, planeGridSegmentsForScale, resolvePlaneGridSpacingUm, } from './planeGeometry.js';
|
|
14
|
+
import { SNAP_RADIUS_PX, SNAP_RING_PX, SNAP_RING_WIDTH_PX, collectSnapTargetsFlat, snapDelta, snapNearest, } from './snapGeometry.js';
|
|
14
15
|
import { buildShapeFromDrag } from './tools/shapeTool.js';
|
|
15
16
|
import { SELECTION_PAD } from './textGeometry.js';
|
|
16
17
|
import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
|
|
@@ -64,7 +65,7 @@ const LOUPE_CROSS_WIDTH_PX = 1.5;
|
|
|
64
65
|
// is only re-synced once each gesture ends. The tool/tap path still hops to
|
|
65
66
|
// JS (`runOnJS(true)`) since drawing/selection are React-state operations.
|
|
66
67
|
export const AnnotationCanvasInner = (props) => {
|
|
67
|
-
const { resolveImageUrl, deferBackgroundToResolver, hideLinkBadges, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, renderBackground, } = props;
|
|
68
|
+
const { resolveImageUrl, deferBackgroundToResolver, hideLinkBadges, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, snapToGeometry = false, renderBackground, } = props;
|
|
68
69
|
const valueFont = useFont(stampFontSource, stampValueFontSize);
|
|
69
70
|
// The animated-viewport implementation needs the Reanimated shared values,
|
|
70
71
|
// which are created below AFTER the state hook — so the hook gets a stable
|
|
@@ -293,7 +294,10 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
293
294
|
// owned by the UI thread — the shape twin of `livePoints`. The drag worklet
|
|
294
295
|
// tracks the start/current world points; derived paths below render the
|
|
295
296
|
// live shape. React is untouched until the single commit on release.
|
|
296
|
-
|
|
297
|
+
// `snapR` is the doc-space magnetic-snap radius frozen at press (0 = off)
|
|
298
|
+
// and `snapped` whether the live end is currently locked onto a target
|
|
299
|
+
// (drives the snap ring) — see snapGeometry.ts.
|
|
300
|
+
const liveShape = useSharedValue({ active: false, ax: 0, ay: 0, bx: 0, by: 0, snapR: 0, snapped: false });
|
|
297
301
|
// Released shapes awaiting their committed paint — the shape twin of
|
|
298
302
|
// `handoffStrokes`, with the same lifecycle: appended in the gesture's
|
|
299
303
|
// onEnd worklet tick, released by the paint-watch effect one rAF after the
|
|
@@ -317,6 +321,28 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
317
321
|
width: shapeDraw.width,
|
|
318
322
|
};
|
|
319
323
|
}, [shapeDraw, shapeCfg]);
|
|
324
|
+
// Magnetic snapping for line endpoints (snapGeometry.ts). `snapCfg` mirrors
|
|
325
|
+
// the `snapToGeometry` prop for the worklets (derived values and gesture
|
|
326
|
+
// callbacks are created once and can't close over a changing prop).
|
|
327
|
+
// `snapTargets` is the flat `[x, y, kind, …]` target buffer, written ONCE
|
|
328
|
+
// per drag — from JS in beginSelectDrag for endpoint drags (excluding the
|
|
329
|
+
// grabbed element), and by the effect below for the shape rubber-band,
|
|
330
|
+
// whose onBegin is a worklet with no doc access. The effect only runs while
|
|
331
|
+
// a line-kind shape tool is active, so it can never clobber a select drag's
|
|
332
|
+
// harvest; keyed on the COMMITTED doc so a just-drawn line becomes a target.
|
|
333
|
+
const snapCfg = useSharedValue({
|
|
334
|
+
enabled: false,
|
|
335
|
+
radiusPx: SNAP_RADIUS_PX,
|
|
336
|
+
});
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
snapCfg.value = { enabled: snapToGeometry, radiusPx: SNAP_RADIUS_PX };
|
|
339
|
+
}, [snapToGeometry, snapCfg]);
|
|
340
|
+
const snapTargets = useSharedValue([]);
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
if (!snapToGeometry || !shapeDraw || shapeDraw.kind !== 'line')
|
|
343
|
+
return;
|
|
344
|
+
snapTargets.value = collectSnapTargetsFlat(state.ctx.document);
|
|
345
|
+
}, [snapToGeometry, shapeDraw, state.ctx.document, snapTargets]);
|
|
320
346
|
// Outline of the live rubber-band + every pending handoff shape, as one
|
|
321
347
|
// stroked path. WORKLET TWIN of shapeGeometry.shapePointsFromDrag +
|
|
322
348
|
// ShapeElement's per-kind rendering — keep in sync. Merging into one path
|
|
@@ -456,7 +482,9 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
456
482
|
// (0 = a, 1 = b) is moving, so the Skia line + overlay tile can be driven on
|
|
457
483
|
// the UI thread from `dragX`/`dragY`.
|
|
458
484
|
const [epDragId, setEpDragId] = useState(null);
|
|
459
|
-
|
|
485
|
+
// `snapR`: doc-space magnetic-snap radius frozen at grab (0 = off) — see
|
|
486
|
+
// epLive below and snapGeometry.ts.
|
|
487
|
+
const epCtx = useSharedValue({ ax: 0, ay: 0, bx: 0, by: 0, t0: 0.5, handle: 0, snapR: 0 });
|
|
460
488
|
const epTargetRef = useRef(null);
|
|
461
489
|
// Endpoint drag of a line/arrow SHAPE (vs. a measurement line above).
|
|
462
490
|
// `shapeEpDragId` (React state) tells the Skia layer to render that shape's
|
|
@@ -468,19 +496,32 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
468
496
|
// Live line endpoints during an endpoint drag: the grabbed handle follows the
|
|
469
497
|
// finger (dragX/dragY); the other stays put. Fed to the Skia <Line> for the
|
|
470
498
|
// dragged annotation.
|
|
499
|
+
// The grabbed endpoint's live position: base + drag, then magnetically
|
|
500
|
+
// snapped to the nearest target within epCtx.snapR (0 = off). The SINGLE
|
|
501
|
+
// source for the live line, the tile overlay's follow math, the loupe
|
|
502
|
+
// crosshair and the snap ring — and the select gesture's onEnd recomputes
|
|
503
|
+
// the same snap (snapDelta, same inputs) so the commit matches it exactly.
|
|
504
|
+
const epLive = useDerivedValue(() => {
|
|
505
|
+
'worklet';
|
|
506
|
+
const c = epCtx.value;
|
|
507
|
+
const mx = (c.handle === 0 ? c.ax : c.bx) + dragX.value;
|
|
508
|
+
const my = (c.handle === 0 ? c.ay : c.by) + dragY.value;
|
|
509
|
+
const hit = c.snapR > 0 ? snapNearest(mx, my, snapTargets.value, c.snapR) : null;
|
|
510
|
+
return hit
|
|
511
|
+
? { x: hit.x, y: hit.y, snapped: true }
|
|
512
|
+
: { x: mx, y: my, snapped: false };
|
|
513
|
+
});
|
|
471
514
|
const liveLineP1 = useDerivedValue(() => {
|
|
472
515
|
'worklet';
|
|
473
516
|
const c = epCtx.value;
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
: { x: c.ax, y: c.ay };
|
|
517
|
+
const e = epLive.value;
|
|
518
|
+
return c.handle === 0 ? { x: e.x, y: e.y } : { x: c.ax, y: c.ay };
|
|
477
519
|
});
|
|
478
520
|
const liveLineP2 = useDerivedValue(() => {
|
|
479
521
|
'worklet';
|
|
480
522
|
const c = epCtx.value;
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
: { x: c.bx, y: c.by };
|
|
523
|
+
const e = epLive.value;
|
|
524
|
+
return c.handle === 1 ? { x: e.x, y: e.y } : { x: c.bx, y: c.by };
|
|
484
525
|
});
|
|
485
526
|
// Screen-constant endpoint handle radius: a fixed pixel size divided by the
|
|
486
527
|
// live zoom (the handles are drawn inside the zoom-scaled world group).
|
|
@@ -488,6 +529,45 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
488
529
|
'worklet';
|
|
489
530
|
return HANDLE_RADIUS_PX / zoom.value;
|
|
490
531
|
});
|
|
532
|
+
// Snap ring (magnetic endpoint snapping). Three sources, in priority order:
|
|
533
|
+
// the UI-thread shape rubber-band, the UI-thread endpoint drag (gated off
|
|
534
|
+
// once the gesture ends or cancels), and the JS-path tools (measurement /
|
|
535
|
+
// pointer-path drags), whose ctx.snap.setIndicator lands in
|
|
536
|
+
// state.snapIndicator and is mirrored into a shared value here. Visibility
|
|
537
|
+
// is an opacity so nothing remounts per frame.
|
|
538
|
+
const jsSnapIndicator = useSharedValue(null);
|
|
539
|
+
useEffect(() => {
|
|
540
|
+
jsSnapIndicator.value = state.snapIndicator;
|
|
541
|
+
}, [state.snapIndicator, jsSnapIndicator]);
|
|
542
|
+
const snapIndicatorC = useDerivedValue(() => {
|
|
543
|
+
'worklet';
|
|
544
|
+
const sh = liveShape.value;
|
|
545
|
+
if (sh.active && sh.snapped)
|
|
546
|
+
return { x: sh.bx, y: sh.by };
|
|
547
|
+
const e = epLive.value;
|
|
548
|
+
if (e.snapped && !dragEnded.value)
|
|
549
|
+
return { x: e.x, y: e.y };
|
|
550
|
+
const j = jsSnapIndicator.value;
|
|
551
|
+
return j ? { x: j.x, y: j.y } : { x: 0, y: 0 };
|
|
552
|
+
});
|
|
553
|
+
const snapIndicatorOpacity = useDerivedValue(() => {
|
|
554
|
+
'worklet';
|
|
555
|
+
const sh = liveShape.value;
|
|
556
|
+
if (sh.active && sh.snapped)
|
|
557
|
+
return 1;
|
|
558
|
+
const e = epLive.value;
|
|
559
|
+
if (e.snapped && !dragEnded.value)
|
|
560
|
+
return 1;
|
|
561
|
+
return jsSnapIndicator.value ? 1 : 0;
|
|
562
|
+
});
|
|
563
|
+
const snapRingRadius = useDerivedValue(() => {
|
|
564
|
+
'worklet';
|
|
565
|
+
return SNAP_RING_PX / zoom.value;
|
|
566
|
+
});
|
|
567
|
+
const snapRingWidth = useDerivedValue(() => {
|
|
568
|
+
'worklet';
|
|
569
|
+
return SNAP_RING_WIDTH_PX / zoom.value;
|
|
570
|
+
});
|
|
491
571
|
const handleRingWidth = useDerivedValue(() => {
|
|
492
572
|
'worklet';
|
|
493
573
|
return HANDLE_RING_PX / zoom.value;
|
|
@@ -899,9 +979,9 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
899
979
|
else if (loupeCrossMode === 'endpoint') {
|
|
900
980
|
// Measurement-line and shape line/arrow endpoint drags both stage
|
|
901
981
|
// their line in epCtx; the grabbed end follows the finger.
|
|
902
|
-
const
|
|
903
|
-
wx =
|
|
904
|
-
wy =
|
|
982
|
+
const e = epLive.value;
|
|
983
|
+
wx = e.x;
|
|
984
|
+
wy = e.y;
|
|
905
985
|
}
|
|
906
986
|
else if (loupeCrossMode === 'rect-corner') {
|
|
907
987
|
const c = rectCtx.value;
|
|
@@ -1402,9 +1482,29 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1402
1482
|
.maxPointers(1)
|
|
1403
1483
|
.onBegin((e) => {
|
|
1404
1484
|
'worklet';
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1485
|
+
let wx = e.x / zoom.value + panX.value;
|
|
1486
|
+
let wy = e.y / zoom.value + panY.value;
|
|
1487
|
+
// Magnetic snap (line tool only): freeze the doc-space radius for
|
|
1488
|
+
// this drag and snap the START point too — the WORKLET counterpart
|
|
1489
|
+
// of shapeTool's snapStateForPress/snapWorld (targets come from the
|
|
1490
|
+
// doc-synced `snapTargets` effect).
|
|
1491
|
+
const snapR = cfg.kind === 'line' && snapCfg.value.enabled
|
|
1492
|
+
? snapCfg.value.radiusPx / zoom.value
|
|
1493
|
+
: 0;
|
|
1494
|
+
const hit = snapR > 0 ? snapNearest(wx, wy, snapTargets.value, snapR) : null;
|
|
1495
|
+
if (hit) {
|
|
1496
|
+
wx = hit.x;
|
|
1497
|
+
wy = hit.y;
|
|
1498
|
+
}
|
|
1499
|
+
liveShape.value = {
|
|
1500
|
+
active: true,
|
|
1501
|
+
ax: wx,
|
|
1502
|
+
ay: wy,
|
|
1503
|
+
bx: wx,
|
|
1504
|
+
by: wy,
|
|
1505
|
+
snapR,
|
|
1506
|
+
snapped: hit !== null,
|
|
1507
|
+
};
|
|
1408
1508
|
magTouchX.value = e.x;
|
|
1409
1509
|
magTouchY.value = e.y;
|
|
1410
1510
|
runOnJS(setMagnifying)(true);
|
|
@@ -1416,12 +1516,23 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1416
1516
|
return;
|
|
1417
1517
|
magTouchX.value = e.x;
|
|
1418
1518
|
magTouchY.value = e.y;
|
|
1519
|
+
let bx = e.x / zoom.value + panX.value;
|
|
1520
|
+
let by = e.y / zoom.value + panY.value;
|
|
1521
|
+
const hit = s.snapR > 0
|
|
1522
|
+
? snapNearest(bx, by, snapTargets.value, s.snapR)
|
|
1523
|
+
: null;
|
|
1524
|
+
if (hit) {
|
|
1525
|
+
bx = hit.x;
|
|
1526
|
+
by = hit.y;
|
|
1527
|
+
}
|
|
1419
1528
|
liveShape.value = {
|
|
1420
1529
|
active: true,
|
|
1421
1530
|
ax: s.ax,
|
|
1422
1531
|
ay: s.ay,
|
|
1423
|
-
bx
|
|
1424
|
-
by
|
|
1532
|
+
bx,
|
|
1533
|
+
by,
|
|
1534
|
+
snapR: s.snapR,
|
|
1535
|
+
snapped: hit !== null,
|
|
1425
1536
|
};
|
|
1426
1537
|
})
|
|
1427
1538
|
.onEnd(() => {
|
|
@@ -1434,7 +1545,15 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1434
1545
|
const dxPx = (s.bx - s.ax) * zoom.value;
|
|
1435
1546
|
const dyPx = (s.by - s.ay) * zoom.value;
|
|
1436
1547
|
if (dxPx * dxPx + dyPx * dyPx < minDragSq) {
|
|
1437
|
-
liveShape.value = {
|
|
1548
|
+
liveShape.value = {
|
|
1549
|
+
active: false,
|
|
1550
|
+
ax: 0,
|
|
1551
|
+
ay: 0,
|
|
1552
|
+
bx: 0,
|
|
1553
|
+
by: 0,
|
|
1554
|
+
snapR: 0,
|
|
1555
|
+
snapped: false,
|
|
1556
|
+
};
|
|
1438
1557
|
return;
|
|
1439
1558
|
}
|
|
1440
1559
|
// Hand off in THIS worklet tick (see the stroke onEnd): the live
|
|
@@ -1454,14 +1573,30 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1454
1573
|
by: s.by,
|
|
1455
1574
|
},
|
|
1456
1575
|
];
|
|
1457
|
-
liveShape.value = {
|
|
1576
|
+
liveShape.value = {
|
|
1577
|
+
active: false,
|
|
1578
|
+
ax: 0,
|
|
1579
|
+
ay: 0,
|
|
1580
|
+
bx: 0,
|
|
1581
|
+
by: 0,
|
|
1582
|
+
snapR: 0,
|
|
1583
|
+
snapped: false,
|
|
1584
|
+
};
|
|
1458
1585
|
runOnJS(commitShape)(id, { x: s.ax, y: s.ay }, { x: s.bx, y: s.by });
|
|
1459
1586
|
})
|
|
1460
1587
|
.onFinalize(() => {
|
|
1461
1588
|
'worklet';
|
|
1462
1589
|
// Cancel path (no onEnd): drop the in-flight rubber-band. After a
|
|
1463
1590
|
// normal end the slot is already inactive, so this is a no-op.
|
|
1464
|
-
liveShape.value = {
|
|
1591
|
+
liveShape.value = {
|
|
1592
|
+
active: false,
|
|
1593
|
+
ax: 0,
|
|
1594
|
+
ay: 0,
|
|
1595
|
+
bx: 0,
|
|
1596
|
+
by: 0,
|
|
1597
|
+
snapR: 0,
|
|
1598
|
+
snapped: false,
|
|
1599
|
+
};
|
|
1465
1600
|
runOnJS(setMagnifying)(false);
|
|
1466
1601
|
});
|
|
1467
1602
|
};
|
|
@@ -1487,6 +1622,16 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1487
1622
|
if (handle) {
|
|
1488
1623
|
const m = st.ctx.document.placedMeasurements.find((x) => x.id === selId);
|
|
1489
1624
|
if (m?.line) {
|
|
1625
|
+
// Magnetic snap: harvest every OTHER element's targets once and
|
|
1626
|
+
// freeze the capture radius in doc space at the grab zoom (the
|
|
1627
|
+
// JS twin is selectTool's snapStateForGrab). Same JS tick as the
|
|
1628
|
+
// epCtx write, so both land in the same UI batch.
|
|
1629
|
+
const snapR = st.ctx.snap?.enabled
|
|
1630
|
+
? st.ctx.snap.radiusPx / zoomNow
|
|
1631
|
+
: 0;
|
|
1632
|
+
if (snapR > 0) {
|
|
1633
|
+
snapTargets.value = collectSnapTargetsFlat(st.ctx.document, selId);
|
|
1634
|
+
}
|
|
1490
1635
|
epCtx.value = {
|
|
1491
1636
|
ax: m.line.a.x,
|
|
1492
1637
|
ay: m.line.a.y,
|
|
@@ -1494,6 +1639,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1494
1639
|
by: m.line.b.y,
|
|
1495
1640
|
t0: m.linePos ?? 0.5,
|
|
1496
1641
|
handle: handle === 'a' ? 0 : 1,
|
|
1642
|
+
snapR,
|
|
1497
1643
|
};
|
|
1498
1644
|
epTargetRef.current = { id: selId, handle };
|
|
1499
1645
|
setEpDragId(selId);
|
|
@@ -1509,6 +1655,12 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1509
1655
|
const a = s?.geometry.points[0];
|
|
1510
1656
|
const b = s?.geometry.points[1];
|
|
1511
1657
|
if (a && b) {
|
|
1658
|
+
const snapR = st.ctx.snap?.enabled
|
|
1659
|
+
? st.ctx.snap.radiusPx / zoomNow
|
|
1660
|
+
: 0;
|
|
1661
|
+
if (snapR > 0) {
|
|
1662
|
+
snapTargets.value = collectSnapTargetsFlat(st.ctx.document, selId);
|
|
1663
|
+
}
|
|
1512
1664
|
epCtx.value = {
|
|
1513
1665
|
ax: a.x,
|
|
1514
1666
|
ay: a.y,
|
|
@@ -1516,6 +1668,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1516
1668
|
by: b.y,
|
|
1517
1669
|
t0: 0.5,
|
|
1518
1670
|
handle: shapeHandle === 'a' ? 0 : 1,
|
|
1671
|
+
snapR,
|
|
1519
1672
|
};
|
|
1520
1673
|
shapeEpTargetRef.current = { id: selId, handle: shapeHandle };
|
|
1521
1674
|
setShapeEpDragId(selId);
|
|
@@ -1794,6 +1947,10 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1794
1947
|
dragEnded.value = false;
|
|
1795
1948
|
dragX.value = 0;
|
|
1796
1949
|
dragY.value = 0;
|
|
1950
|
+
// A stale snap radius from a previous endpoint drag must not bend
|
|
1951
|
+
// a move/slide's delta in onEnd; beginSelectDrag re-arms it below
|
|
1952
|
+
// (queued after this write) when this grab IS an endpoint.
|
|
1953
|
+
epCtx.value = { ...epCtx.value, snapR: 0 };
|
|
1797
1954
|
// The loupe (turned on by beginSelectDrag only for precision sub-
|
|
1798
1955
|
// drags) follows the live finger, not the hit-test origin.
|
|
1799
1956
|
magTouchX.value = e.x;
|
|
@@ -1817,12 +1974,27 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1817
1974
|
.onEnd(() => {
|
|
1818
1975
|
'worklet';
|
|
1819
1976
|
dragEnded.value = true;
|
|
1820
|
-
|
|
1977
|
+
let dx = dragX.value;
|
|
1978
|
+
let dy = dragY.value;
|
|
1979
|
+
// Endpoint drag with snapping armed: hand JS the EFFECTIVE delta
|
|
1980
|
+
// (the one landing on the snapped target) — same inputs as epLive,
|
|
1981
|
+
// so the commit matches the live preview and the delta-based patch
|
|
1982
|
+
// builders stay untouched.
|
|
1983
|
+
const c = epCtx.value;
|
|
1984
|
+
if (c.snapR > 0) {
|
|
1985
|
+
const base = c.handle === 0 ? { x: c.ax, y: c.ay } : { x: c.bx, y: c.by };
|
|
1986
|
+
const r = snapDelta(base, { x: dx, y: dy }, snapTargets.value, c.snapR);
|
|
1987
|
+
dx = r.delta.x;
|
|
1988
|
+
dy = r.delta.y;
|
|
1989
|
+
}
|
|
1990
|
+
runOnJS(endSelectDrag)(dx, dy);
|
|
1821
1991
|
})
|
|
1822
1992
|
.onFinalize(() => {
|
|
1823
1993
|
'worklet';
|
|
1824
1994
|
if (!dragEnded.value)
|
|
1825
1995
|
runOnJS(cancelSelectDrag)();
|
|
1996
|
+
// Also closes the snap-ring gate on the cancel path.
|
|
1997
|
+
dragEnded.value = true;
|
|
1826
1998
|
});
|
|
1827
1999
|
};
|
|
1828
2000
|
// Plane tool: a hybrid pan. Handle drags run on the UI thread (shared
|
|
@@ -2022,6 +2194,8 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
2022
2194
|
epCtx,
|
|
2023
2195
|
resizeCtx,
|
|
2024
2196
|
rectCtx,
|
|
2197
|
+
snapCfg,
|
|
2198
|
+
snapTargets,
|
|
2025
2199
|
]);
|
|
2026
2200
|
const activeTool = props.tools.find((t) => t.id === props.activeToolId) ?? null;
|
|
2027
2201
|
const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
|
|
@@ -2099,6 +2273,15 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
2099
2273
|
shapeEndpointDragId: shapeEpDragId,
|
|
2100
2274
|
liveLineP1,
|
|
2101
2275
|
liveLineP2,
|
|
2276
|
+
// Magnetic-snap ring; rides into the loupe pass via the spread below.
|
|
2277
|
+
snapIndicator: snapToGeometry
|
|
2278
|
+
? {
|
|
2279
|
+
c: snapIndicatorC,
|
|
2280
|
+
r: snapRingRadius,
|
|
2281
|
+
ringWidth: snapRingWidth,
|
|
2282
|
+
opacity: snapIndicatorOpacity,
|
|
2283
|
+
}
|
|
2284
|
+
: null,
|
|
2102
2285
|
bgCropId,
|
|
2103
2286
|
liveBgCrop: {
|
|
2104
2287
|
x: liveBgCropX,
|
|
@@ -2157,7 +2340,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
2157
2340
|
// non-fullscreen canvas (e.g. a diagram strip) escape into surrounding UI.
|
|
2158
2341
|
_jsxs(GestureHandlerRootView, { style: [{ width, height, overflow: 'hidden' }, style], children: [backgroundLayers.map((layer) => (_jsx(BackgroundSkImageLoader, { layer: layer, resolveUrl: resolveImageUrl, deferToResolver: deferBackgroundToResolver, onImage: onBackgroundSkImage }, layer.id))), _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
|
|
2159
2342
|
? (state.measurementsById.get(placed.measurementId) ?? null)
|
|
2160
|
-
: (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, planeEdgeDragging: planeDrag != null && placed.planeId === planeDrag.planeId, zoomSnapshot: state.viewport.zoom, zoom: zoom, panX: panX, panY: panY, dragX: dragX, dragY: dragY, slideCtx: slideCtx, epCtx: epCtx, rectCtx: rectCtx, planeTileCtx: planeTileCtx, planeDragTX: planeDragTX, planeDragTY: planeDragTY, renderMeasurementStamp: renderMeasurementStamp, tileScaleFactor: state.tileScaleFactor, headerTileScaleFactor: state.headerTileScaleFactor, fileTileScaleFactor: state.fileTileScaleFactor, tileViewportScale: state.tileViewportScale, onStampPress: onMeasurementStampPress, onStampLongPress: onMeasurementStampLongPress, onRemove: onMeasurementStampRemove
|
|
2343
|
+
: (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, planeEdgeDragging: planeDrag != null && placed.planeId === planeDrag.planeId, zoomSnapshot: state.viewport.zoom, zoom: zoom, panX: panX, panY: panY, dragX: dragX, dragY: dragY, slideCtx: slideCtx, epCtx: epCtx, epLive: epLive, rectCtx: rectCtx, planeTileCtx: planeTileCtx, planeDragTX: planeDragTX, planeDragTY: planeDragTY, renderMeasurementStamp: renderMeasurementStamp, tileScaleFactor: state.tileScaleFactor, headerTileScaleFactor: state.headerTileScaleFactor, fileTileScaleFactor: state.fileTileScaleFactor, tileViewportScale: state.tileViewportScale, onStampPress: onMeasurementStampPress, onStampLongPress: onMeasurementStampLongPress, onRemove: onMeasurementStampRemove
|
|
2161
2344
|
? () => {
|
|
2162
2345
|
const defaultRemove = () => {
|
|
2163
2346
|
const { ops, keepSelection } = buildRemoveMeasurementOps(placed);
|
|
@@ -2184,7 +2367,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
2184
2367
|
measurementLineScale: 1 / LOUPE_MAGNIFICATION,
|
|
2185
2368
|
}) }))] }));
|
|
2186
2369
|
};
|
|
2187
|
-
const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, rectCtx, planeEdgeDragging, planeTileCtx, planeDragTX, planeDragTY, renderMeasurementStamp, tileScaleFactor, headerTileScaleFactor, fileTileScaleFactor, tileViewportScale, onStampPress, onStampLongPress, onRemove, }) => {
|
|
2370
|
+
const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, epLive, rectCtx, planeEdgeDragging, planeTileCtx, planeDragTX, planeDragTY, renderMeasurementStamp, tileScaleFactor, headerTileScaleFactor, fileTileScaleFactor, tileViewportScale, onStampPress, onStampLongPress, onRemove, }) => {
|
|
2188
2371
|
// Square tile or wide group-header pill — one footprint source of truth
|
|
2189
2372
|
// shared with the hit-test (stampTileDims).
|
|
2190
2373
|
const { width, height } = stampTileDims(placed, tileScaleFactor, tileViewportScale, headerTileScaleFactor ?? tileScaleFactor, fileTileScaleFactor ?? tileScaleFactor);
|
|
@@ -2260,13 +2443,15 @@ const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging,
|
|
|
2260
2443
|
worldY = c.ay + aby * t;
|
|
2261
2444
|
}
|
|
2262
2445
|
else if (endpointDragging) {
|
|
2263
|
-
// Tile stays at its linePos along the LIVE line:
|
|
2264
|
-
//
|
|
2446
|
+
// Tile stays at its linePos along the LIVE line: the moving endpoint
|
|
2447
|
+
// (handle 0 = a, 1 = b) comes from epLive — already drag-offset and
|
|
2448
|
+
// magnetically snapped — then lerp at t0.
|
|
2265
2449
|
const c = epCtx.value;
|
|
2266
|
-
const
|
|
2267
|
-
const
|
|
2268
|
-
const
|
|
2269
|
-
const
|
|
2450
|
+
const e = epLive.value;
|
|
2451
|
+
const ax = c.handle === 0 ? e.x : c.ax;
|
|
2452
|
+
const ay = c.handle === 0 ? e.y : c.ay;
|
|
2453
|
+
const bx = c.handle === 1 ? e.x : c.bx;
|
|
2454
|
+
const by = c.handle === 1 ? e.y : c.by;
|
|
2270
2455
|
worldX = ax + (bx - ax) * c.t0;
|
|
2271
2456
|
worldY = ay + (by - ay) * c.t0;
|
|
2272
2457
|
}
|
|
@@ -111,6 +111,12 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
111
111
|
handleRingWidth?: number | {
|
|
112
112
|
value: number;
|
|
113
113
|
};
|
|
114
|
+
snapIndicator?: {
|
|
115
|
+
c: AnimatedPoint;
|
|
116
|
+
r: AnimatedNumber;
|
|
117
|
+
ringWidth: AnimatedNumber;
|
|
118
|
+
opacity?: AnimatedNumber;
|
|
119
|
+
} | null;
|
|
114
120
|
showPlaneHandles?: boolean;
|
|
115
121
|
planeDragId?: string | null;
|
|
116
122
|
planeDragIndex?: number | null;
|
|
@@ -127,5 +133,5 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
127
133
|
wrapContent?: (content: ReactNode) => ReactNode;
|
|
128
134
|
canvasRef?: RefObject<CanvasRef | null>;
|
|
129
135
|
}
|
|
130
|
-
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, deferBackgroundToResolver, hideLinkBadges, backgroundSkImages, backgroundContent, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, bgCropId, liveBgCrop, liveBgCropClip, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale, measurementLineScale, customPreview, wrapContent, canvasRef, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
136
|
+
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, deferBackgroundToResolver, hideLinkBadges, backgroundSkImages, backgroundContent, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, bgCropId, liveBgCrop, liveBgCropClip, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, snapIndicator, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale, measurementLineScale, customPreview, wrapContent, canvasRef, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
131
137
|
export {};
|
|
@@ -30,6 +30,10 @@ const MEASUREMENT_HANDLE_COLOR = '#0066FF';
|
|
|
30
30
|
// hit-test the text resize handle, which sits on the padded box corner.
|
|
31
31
|
const SELECTION_COLOR = '#0066FF';
|
|
32
32
|
const SELECTION_STROKE = 1.5;
|
|
33
|
+
// Snap ring (magnetic endpoint snapping, see snapGeometry.ts): drawn around
|
|
34
|
+
// the target the dragged endpoint is currently locked onto. Orange so it reads
|
|
35
|
+
// against both the blue selection chrome and the yellow lock accent.
|
|
36
|
+
const SNAP_INDICATOR_COLOR = '#FF7A00';
|
|
33
37
|
// Outline of a selected LOCKED background layer: the lock accent (the same
|
|
34
38
|
// yellow the selected group-header border uses), distinct from the blue
|
|
35
39
|
// manipulable-selection chrome — locked means "selected, but pinned".
|
|
@@ -60,6 +64,11 @@ const getLinkedGlyphPath = () => (linkedGlyphPath ?? (linkedGlyphPath = Skia.Pat
|
|
|
60
64
|
// a solid-colored handle would camouflage into. `c`/`r`/`ringWidth` may be
|
|
61
65
|
// Reanimated values (native live-drag) or plain numbers (web).
|
|
62
66
|
const Handle = ({ c, r, ringWidth, color = MEASUREMENT_HANDLE_COLOR, }) => (_jsxs(_Fragment, { children: [_jsx(Circle, { c: c, r: r, color: "#FFFFFF" }), _jsx(Circle, { c: c, r: r, color: color, style: "stroke", strokeWidth: ringWidth })] }));
|
|
67
|
+
// The snap indicator: a stroke-only ring (NOT a Handle — its white disc would
|
|
68
|
+
// hide the very pixel being snapped to). Screen-constant like the handles;
|
|
69
|
+
// `opacity` lets the native UI-thread path show/hide it without remounting
|
|
70
|
+
// (a zero radius still paints a strokeWidth-sized dot, so radius can't gate it).
|
|
71
|
+
const SnapRing = ({ c, r, ringWidth, opacity = 1, }) => (_jsx(Circle, { c: c, r: r, color: SNAP_INDICATOR_COLOR, style: "stroke", strokeWidth: ringWidth, opacity: opacity }));
|
|
63
72
|
// Bounds of a flat [x,y,x,y,…] stroke point array, or null if empty.
|
|
64
73
|
const strokeBounds = (points) => {
|
|
65
74
|
if (points.length < 2)
|
|
@@ -105,7 +114,7 @@ const SelectionBox = ({ bounds, isDragging, transform, }) => (_jsx(DraggableElem
|
|
|
105
114
|
// since the function-call pattern works identically on native we use it
|
|
106
115
|
// in both Inners for consistency. Don't add hooks here; this is a plain
|
|
107
116
|
// JSX-returning helper, not a component.
|
|
108
|
-
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, deferBackgroundToResolver, hideLinkBadges, backgroundSkImages, backgroundContent, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, bgCropId, liveBgCrop, liveBgCropClip, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale = 1, measurementLineScale = 1, customPreview, wrapContent, canvasRef, }) => {
|
|
117
|
+
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, deferBackgroundToResolver, hideLinkBadges, backgroundSkImages, backgroundContent, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, bgCropId, liveBgCrop, liveBgCropClip, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, snapIndicator, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale = 1, measurementLineScale = 1, customPreview, wrapContent, canvasRef, }) => {
|
|
109
118
|
// Doc rect a SCALE-mode plane grid covers: the first background layer (the
|
|
110
119
|
// photo being calibrated), else the document rect. Pinned to layer 0 like
|
|
111
120
|
// the tile-sizing reference — adding a second image must not re-span the
|
|
@@ -340,6 +349,6 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
|
|
|
340
349
|
corners.map((c, i) => (_jsx(Handle, { c: c, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR }, i)))] }));
|
|
341
350
|
}
|
|
342
351
|
return null;
|
|
343
|
-
})(), penDrawingStroke && _jsx(StrokeElement, { stroke: penDrawingStroke }), shapePreview && (_jsxs(_Fragment, { children: [_jsx(Path, { path: shapePreview.path, color: shapePreview.color, style: "stroke", strokeWidth: shapePreview.width, strokeCap: toSkiaStrokeCap(shapePreview.cap), strokeJoin: "round", children: shapePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(shapePreview.width) })) }), _jsx(Path, { path: shapePreview.headPath, color: shapePreview.color, style: "fill" })] })), livePreview?.handoffPaths?.map((p, i) => (_jsx(Path, { path: p, color: livePreview.color, style: "stroke", strokeWidth: livePreview.width, strokeCap: toSkiaStrokeCap(livePreview.cap), strokeJoin: "round", opacity: livePreview.opacity, children: livePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(livePreview.width) })) }, i))), livePreview && (_jsx(Path, { path: livePreview.path, color: livePreview.color, style: "stroke", strokeWidth: livePreview.width, strokeCap: toSkiaStrokeCap(livePreview.cap), strokeJoin: "round", opacity: livePreview.opacity, children: livePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(livePreview.width) })) })), customPreview] }));
|
|
352
|
+
})(), penDrawingStroke && _jsx(StrokeElement, { stroke: penDrawingStroke }), shapePreview && (_jsxs(_Fragment, { children: [_jsx(Path, { path: shapePreview.path, color: shapePreview.color, style: "stroke", strokeWidth: shapePreview.width, strokeCap: toSkiaStrokeCap(shapePreview.cap), strokeJoin: "round", children: shapePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(shapePreview.width) })) }), _jsx(Path, { path: shapePreview.headPath, color: shapePreview.color, style: "fill" })] })), livePreview?.handoffPaths?.map((p, i) => (_jsx(Path, { path: p, color: livePreview.color, style: "stroke", strokeWidth: livePreview.width, strokeCap: toSkiaStrokeCap(livePreview.cap), strokeJoin: "round", opacity: livePreview.opacity, children: livePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(livePreview.width) })) }, i))), livePreview && (_jsx(Path, { path: livePreview.path, color: livePreview.color, style: "stroke", strokeWidth: livePreview.width, strokeCap: toSkiaStrokeCap(livePreview.cap), strokeJoin: "round", opacity: livePreview.opacity, children: livePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(livePreview.width) })) })), snapIndicator && (_jsx(SnapRing, { c: snapIndicator.c, r: snapIndicator.r, ringWidth: snapIndicator.ringWidth, opacity: snapIndicator.opacity })), customPreview] }));
|
|
344
353
|
return (_jsx(Canvas, { ref: canvasRef, style: { width, height }, children: wrapContent ? wrapContent(world) : world }));
|
|
345
354
|
};
|
|
@@ -19,6 +19,11 @@ export interface CanvasPointerEvent {
|
|
|
19
19
|
metaKey?: boolean;
|
|
20
20
|
ctrlKey?: boolean;
|
|
21
21
|
}
|
|
22
|
+
export interface SnapContext {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
radiusPx: number;
|
|
25
|
+
setIndicator(point: Vec2 | null): void;
|
|
26
|
+
}
|
|
22
27
|
export interface ToolContext {
|
|
23
28
|
document: AnnotationCanvasState;
|
|
24
29
|
selection: Selection | null;
|
|
@@ -27,6 +32,7 @@ export interface ToolContext {
|
|
|
27
32
|
tileScaleFactor: number;
|
|
28
33
|
headerTileScaleFactor: number;
|
|
29
34
|
fileTileScaleFactor: number;
|
|
35
|
+
snap?: SnapContext | null;
|
|
30
36
|
preview(patch: AnnotationDocumentPatch): void;
|
|
31
37
|
commit(patch: AnnotationDocumentPatch): void;
|
|
32
38
|
setSelection(selection: Selection | null): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AnnotationCanvasState, AnnotationElementId, Vec2 } from '../../types/annotation.js';
|
|
2
|
+
export declare const SNAP_RADIUS_PX = 16;
|
|
3
|
+
export declare const SNAP_RING_PX = 11;
|
|
4
|
+
export declare const SNAP_RING_WIDTH_PX = 2;
|
|
5
|
+
export declare const SNAP_KIND_POINT = 0;
|
|
6
|
+
export declare const SNAP_KIND_MIDPOINT = 1;
|
|
7
|
+
export type SnapKind = 0 | 1;
|
|
8
|
+
export declare const SNAP_STRIDE = 3;
|
|
9
|
+
export interface SnapTarget {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
kind: SnapKind;
|
|
13
|
+
sourceId: AnnotationElementId;
|
|
14
|
+
}
|
|
15
|
+
export interface SnapHit {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
kind: SnapKind;
|
|
19
|
+
index: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const collectSnapTargets: (doc: AnnotationCanvasState, excludeId?: AnnotationElementId | null) => SnapTarget[];
|
|
22
|
+
export declare const encodeSnapTargets: (targets: readonly SnapTarget[]) => number[];
|
|
23
|
+
export declare const collectSnapTargetsFlat: (doc: AnnotationCanvasState, excludeId?: AnnotationElementId | null) => number[];
|
|
24
|
+
export declare const snapNearest: (x: number, y: number, flat: readonly number[], radiusDoc: number) => SnapHit | null;
|
|
25
|
+
export declare const snapDelta: (base: Vec2, delta: Vec2, flat: readonly number[], radiusDoc: number) => {
|
|
26
|
+
delta: Vec2;
|
|
27
|
+
hit: SnapHit | null;
|
|
28
|
+
};
|
|
29
|
+
export declare const snapPoint: (p: Vec2, flat: readonly number[], radiusDoc: number) => {
|
|
30
|
+
point: Vec2;
|
|
31
|
+
hit: SnapHit | null;
|
|
32
|
+
};
|