@reekon-tools/boldr-utils 1.19.4 → 1.19.6
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 +2 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.js +3 -1
- package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +2 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +118 -8
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +23 -1
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +89 -13
- package/dist/annotation/canvas/Tool.d.ts +4 -1
- package/dist/annotation/canvas/backgroundLayers.d.ts +20 -1
- package/dist/annotation/canvas/backgroundLayers.js +94 -3
- package/dist/annotation/canvas/elements/BackgroundImageElement.d.ts +6 -3
- package/dist/annotation/canvas/elements/BackgroundImageElement.js +16 -8
- package/dist/annotation/canvas/elements/BackgroundSvg.d.ts +1 -0
- package/dist/annotation/canvas/elements/BackgroundSvg.js +4 -4
- package/dist/annotation/canvas/elements/backgroundUrl.d.ts +2 -1
- package/dist/annotation/canvas/elements/backgroundUrl.js +15 -2
- package/dist/annotation/canvas/fileTileSwap.d.ts +12 -0
- package/dist/annotation/canvas/fileTileSwap.js +87 -0
- package/dist/annotation/canvas/stampLayout.d.ts +1 -1
- package/dist/annotation/canvas/stampLayout.js +10 -8
- package/dist/annotation/canvas/tools/selectTool.d.ts +18 -0
- package/dist/annotation/canvas/tools/selectTool.js +158 -42
- package/dist/annotation/canvas/useAnnotationCanvasState.js +3 -1
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +5 -1
- package/dist/exports.d.ts +1 -0
- package/dist/exports.js +1 -0
- package/dist/types/annotation.d.ts +10 -0
- package/package.json +1 -1
|
@@ -20,6 +20,8 @@ export interface AnnotationCanvasInnerProps {
|
|
|
20
20
|
fractionalTolerance?: FractionalTolerance;
|
|
21
21
|
decimalTolerance?: DecimalTolerance;
|
|
22
22
|
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
23
|
+
deferBackgroundToResolver?: boolean;
|
|
24
|
+
hideLinkBadges?: boolean;
|
|
23
25
|
pickMeasurement?: () => Promise<MeasurementRef | null>;
|
|
24
26
|
requestTextInput?: RequestTextInput;
|
|
25
27
|
renderBackground?: RenderCanvasBackground;
|
|
@@ -27,7 +27,7 @@ const DEFAULT_PAN_TRIGGERS = ['middleMouse', 'space'];
|
|
|
27
27
|
// (SSR / the vitest environment), where 1 is the right answer.
|
|
28
28
|
const devicePixelRatioOf = () => typeof window === 'undefined' ? 1 : window.devicePixelRatio || 1;
|
|
29
29
|
export const AnnotationCanvasInner = (props) => {
|
|
30
|
-
const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, gestures, width, height, style, activeToolId, tools, snapshotRef, renderBackground, } = props;
|
|
30
|
+
const { resolveImageUrl, deferBackgroundToResolver, hideLinkBadges, stampFontSource, stampValueFontSize = 14, gestures, width, height, style, activeToolId, tools, snapshotRef, renderBackground, } = props;
|
|
31
31
|
// Snapshot plumbing for thumbnail capture: hold the <Canvas> ref and publish
|
|
32
32
|
// a capture fn on the consumer's snapshotRef. encodeToBytes() yields PNG.
|
|
33
33
|
const canvasRef = useCanvasRef();
|
|
@@ -315,6 +315,8 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
315
315
|
worldTransform: state.worldTransform,
|
|
316
316
|
viewport: state.viewport,
|
|
317
317
|
resolveImageUrl,
|
|
318
|
+
deferBackgroundToResolver,
|
|
319
|
+
hideLinkBadges,
|
|
318
320
|
backgroundContent,
|
|
319
321
|
canvasRef,
|
|
320
322
|
valueFont,
|
|
@@ -21,6 +21,8 @@ export interface AnnotationCanvasInnerProps {
|
|
|
21
21
|
fractionalTolerance?: FractionalTolerance;
|
|
22
22
|
decimalTolerance?: DecimalTolerance;
|
|
23
23
|
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
24
|
+
deferBackgroundToResolver?: boolean;
|
|
25
|
+
hideLinkBadges?: boolean;
|
|
24
26
|
pickMeasurement?: () => Promise<MeasurementRef | null>;
|
|
25
27
|
requestTextInput?: RequestTextInput;
|
|
26
28
|
renderBackground?: RenderCanvasBackground;
|
|
@@ -7,7 +7,7 @@ import Animated, { runOnJS, runOnUI, useAnimatedStyle, useDerivedValue, useShare
|
|
|
7
7
|
import { stampTileDims } from './stampLayout.js';
|
|
8
8
|
import { DEFAULT_LAYER_ID, } from '../../types/annotation.js';
|
|
9
9
|
import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
|
|
10
|
-
import { backgroundLayersOf, backgroundLayerDocRect, } from './backgroundLayers.js';
|
|
10
|
+
import { MIN_BACKGROUND_DOC_EXTENT, backgroundLayersOf, backgroundLayerDocRect, backgroundLayerFullDocRect, } from './backgroundLayers.js';
|
|
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';
|
|
@@ -64,7 +64,7 @@ const LOUPE_CROSS_WIDTH_PX = 1.5;
|
|
|
64
64
|
// is only re-synced once each gesture ends. The tool/tap path still hops to
|
|
65
65
|
// JS (`runOnJS(true)`) since drawing/selection are React-state operations.
|
|
66
66
|
export const AnnotationCanvasInner = (props) => {
|
|
67
|
-
const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, renderBackground, } = props;
|
|
67
|
+
const { resolveImageUrl, deferBackgroundToResolver, hideLinkBadges, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, renderBackground, } = props;
|
|
68
68
|
const valueFont = useFont(stampFontSource, stampValueFontSize);
|
|
69
69
|
// The animated-viewport implementation needs the Reanimated shared values,
|
|
70
70
|
// which are created below AFTER the state hook — so the hook gets a stable
|
|
@@ -661,6 +661,9 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
661
661
|
// goes through buildResizePatch, which clamps identically.
|
|
662
662
|
const [resizingId, setResizingId] = useState(null);
|
|
663
663
|
const resizeCtx = useSharedValue({ px: 0, py: 0, hx: 0, hy: 0, minS: 1, maxS: 1 });
|
|
664
|
+
// The grab-start geometry rides along to the commit: a background offers
|
|
665
|
+
// four corner handles, each scaling about its opposite corner, so the patch
|
|
666
|
+
// must reuse whichever geometry the grab's hit-test chose.
|
|
664
667
|
const resizeTargetRef = useRef(null);
|
|
665
668
|
const resizeTransform = useDerivedValue(() => {
|
|
666
669
|
'worklet';
|
|
@@ -684,6 +687,60 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
684
687
|
{ translateY: -c.py },
|
|
685
688
|
];
|
|
686
689
|
});
|
|
690
|
+
// Background crop-bar drag. `bgCropId` (React state) gates which layer
|
|
691
|
+
// clips to the live rect; `bgCropCtx` carries the grabbed edge (0 top ·
|
|
692
|
+
// 1 bottom · 2 left · 3 right), the visible doc rect at grab-start and the
|
|
693
|
+
// full (uncropped) image's doc rect it may expand back out to — so the
|
|
694
|
+
// preview is a pure UI-thread rect rebuild driven by dragX/dragY. The
|
|
695
|
+
// commit on release goes through buildBackgroundCropPatch, which clamps
|
|
696
|
+
// identically.
|
|
697
|
+
const [bgCropId, setBgCropId] = useState(null);
|
|
698
|
+
const bgCropTargetRef = useRef(null);
|
|
699
|
+
const bgCropCtx = useSharedValue({ edge: 0, x: 0, y: 0, w: 0, h: 0, bx: 0, by: 0, bw: 0, bh: 0 });
|
|
700
|
+
const liveBgCropRect = useDerivedValue(() => {
|
|
701
|
+
'worklet';
|
|
702
|
+
// WORKLET TWIN of selectTool's cropDragVisibleRect — keep in sync.
|
|
703
|
+
const c = bgCropCtx.value;
|
|
704
|
+
let x = c.x;
|
|
705
|
+
let y = c.y;
|
|
706
|
+
let w = c.w;
|
|
707
|
+
let h = c.h;
|
|
708
|
+
if (c.edge === 0) {
|
|
709
|
+
const newY = Math.min(Math.max(c.y + dragY.value, c.by), c.y + c.h - MIN_BACKGROUND_DOC_EXTENT);
|
|
710
|
+
h = c.y + c.h - newY;
|
|
711
|
+
y = newY;
|
|
712
|
+
}
|
|
713
|
+
else if (c.edge === 1) {
|
|
714
|
+
h = Math.min(Math.max(c.h + dragY.value, MIN_BACKGROUND_DOC_EXTENT), c.by + c.bh - c.y);
|
|
715
|
+
}
|
|
716
|
+
else if (c.edge === 2) {
|
|
717
|
+
const newX = Math.min(Math.max(c.x + dragX.value, c.bx), c.x + c.w - MIN_BACKGROUND_DOC_EXTENT);
|
|
718
|
+
w = c.x + c.w - newX;
|
|
719
|
+
x = newX;
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
w = Math.min(Math.max(c.w + dragX.value, MIN_BACKGROUND_DOC_EXTENT), c.bx + c.bw - c.x);
|
|
723
|
+
}
|
|
724
|
+
return { x, y, width: w, height: h };
|
|
725
|
+
});
|
|
726
|
+
// Per-component slices for the live outline (Skia props take one animated
|
|
727
|
+
// value each; the clip takes the whole rect).
|
|
728
|
+
const liveBgCropX = useDerivedValue(() => {
|
|
729
|
+
'worklet';
|
|
730
|
+
return liveBgCropRect.value.x;
|
|
731
|
+
});
|
|
732
|
+
const liveBgCropY = useDerivedValue(() => {
|
|
733
|
+
'worklet';
|
|
734
|
+
return liveBgCropRect.value.y;
|
|
735
|
+
});
|
|
736
|
+
const liveBgCropW = useDerivedValue(() => {
|
|
737
|
+
'worklet';
|
|
738
|
+
return liveBgCropRect.value.width;
|
|
739
|
+
});
|
|
740
|
+
const liveBgCropH = useDerivedValue(() => {
|
|
741
|
+
'worklet';
|
|
742
|
+
return liveBgCropRect.value.height;
|
|
743
|
+
});
|
|
687
744
|
// Geometric-shape corner resize (rect/ellipse/polygon). `shapeResizeId`
|
|
688
745
|
// (React state) gates which shape renders live; `shapeResizeCtx` carries the
|
|
689
746
|
// fixed (opposite) corner and the grabbed corner's start position (from
|
|
@@ -1477,11 +1534,42 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1477
1534
|
minS: resizeGeom.minScale,
|
|
1478
1535
|
maxS: resizeGeom.maxScale,
|
|
1479
1536
|
};
|
|
1480
|
-
resizeTargetRef.current = { id: selId };
|
|
1537
|
+
resizeTargetRef.current = { id: selId, geom: resizeGeom };
|
|
1481
1538
|
setResizingId(selId);
|
|
1482
1539
|
setMagnifying(true);
|
|
1483
1540
|
return;
|
|
1484
1541
|
}
|
|
1542
|
+
// Crop bar on a selected, unlocked background layer — the edge
|
|
1543
|
+
// windows the image (non-destructively) instead of scaling it.
|
|
1544
|
+
const cropEdge = cfg.hitTestBackgroundCropBar?.(st.ctx.document, selId, world, zoomNow);
|
|
1545
|
+
if (cropEdge) {
|
|
1546
|
+
const cropLayer = backgroundLayersOf(st.ctx.document.viewport).find((l) => l.id === selId);
|
|
1547
|
+
if (cropLayer) {
|
|
1548
|
+
const visible = backgroundLayerDocRect(cropLayer);
|
|
1549
|
+
const full = backgroundLayerFullDocRect(cropLayer);
|
|
1550
|
+
bgCropCtx.value = {
|
|
1551
|
+
edge: cropEdge === 'top'
|
|
1552
|
+
? 0
|
|
1553
|
+
: cropEdge === 'bottom'
|
|
1554
|
+
? 1
|
|
1555
|
+
: cropEdge === 'left'
|
|
1556
|
+
? 2
|
|
1557
|
+
: 3,
|
|
1558
|
+
x: visible.x,
|
|
1559
|
+
y: visible.y,
|
|
1560
|
+
w: visible.width,
|
|
1561
|
+
h: visible.height,
|
|
1562
|
+
bx: full.x,
|
|
1563
|
+
by: full.y,
|
|
1564
|
+
bw: full.width,
|
|
1565
|
+
bh: full.height,
|
|
1566
|
+
};
|
|
1567
|
+
bgCropTargetRef.current = { id: selId, edge: cropEdge };
|
|
1568
|
+
setBgCropId(selId);
|
|
1569
|
+
setMagnifying(true);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1485
1573
|
// Rectangle-annotation corner handle — also selected-element-only.
|
|
1486
1574
|
const rectCornerHit = cfg.hitTestRectCorner?.(st.ctx.document, selId, world, zoomNow);
|
|
1487
1575
|
if (rectCornerHit) {
|
|
@@ -1580,10 +1668,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1580
1668
|
const rT = resizeTargetRef.current;
|
|
1581
1669
|
if (rT) {
|
|
1582
1670
|
if (dx !== 0 || dy !== 0) {
|
|
1583
|
-
const patch = cfg.buildResizePatch?.(st.ctx.document, rT.id, {
|
|
1584
|
-
x: dx,
|
|
1585
|
-
y: dy,
|
|
1586
|
-
});
|
|
1671
|
+
const patch = cfg.buildResizePatch?.(st.ctx.document, rT.id, { x: dx, y: dy }, rT.geom);
|
|
1587
1672
|
if (patch)
|
|
1588
1673
|
st.ctx.commit(patch);
|
|
1589
1674
|
}
|
|
@@ -1591,6 +1676,19 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1591
1676
|
setResizingId(null);
|
|
1592
1677
|
return;
|
|
1593
1678
|
}
|
|
1679
|
+
// Crop commit: window the layer to the live rect's final edges
|
|
1680
|
+
// (clamped in buildBackgroundCropPatch exactly like the live preview).
|
|
1681
|
+
const cropT = bgCropTargetRef.current;
|
|
1682
|
+
if (cropT) {
|
|
1683
|
+
if (dx !== 0 || dy !== 0) {
|
|
1684
|
+
const patch = cfg.buildBackgroundCropPatch?.(st.ctx.document, cropT.id, cropT.edge, { x: dx, y: dy });
|
|
1685
|
+
if (patch)
|
|
1686
|
+
st.ctx.commit(patch);
|
|
1687
|
+
}
|
|
1688
|
+
bgCropTargetRef.current = null;
|
|
1689
|
+
setBgCropId(null);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1594
1692
|
// Rect-corner commit: drag the grabbed corner by the world delta
|
|
1595
1693
|
// (opposite corner fixed); anchor re-centers in the same patch.
|
|
1596
1694
|
const rectT = rectTargetRef.current;
|
|
@@ -1678,6 +1776,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1678
1776
|
resizeTargetRef.current = null;
|
|
1679
1777
|
rectTargetRef.current = null;
|
|
1680
1778
|
shapeResizeTargetRef.current = null;
|
|
1779
|
+
bgCropTargetRef.current = null;
|
|
1681
1780
|
setDraggingId(null);
|
|
1682
1781
|
setSlidingId(null);
|
|
1683
1782
|
setEpDragId(null);
|
|
@@ -1685,6 +1784,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1685
1784
|
setResizingId(null);
|
|
1686
1785
|
setRectDragId(null);
|
|
1687
1786
|
setShapeResizeId(null);
|
|
1787
|
+
setBgCropId(null);
|
|
1688
1788
|
};
|
|
1689
1789
|
return Gesture.Pan()
|
|
1690
1790
|
.minPointers(1)
|
|
@@ -1949,6 +2049,8 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1949
2049
|
effectiveCanvas: state.effectiveCanvas,
|
|
1950
2050
|
worldTransform,
|
|
1951
2051
|
resolveImageUrl,
|
|
2052
|
+
deferBackgroundToResolver,
|
|
2053
|
+
hideLinkBadges,
|
|
1952
2054
|
backgroundSkImages,
|
|
1953
2055
|
// Shared with the loupe overlay below (it re-runs this same scene through a
|
|
1954
2056
|
// magnifying transform), so the lens shows the substrate magnified instead
|
|
@@ -1997,6 +2099,14 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1997
2099
|
shapeEndpointDragId: shapeEpDragId,
|
|
1998
2100
|
liveLineP1,
|
|
1999
2101
|
liveLineP2,
|
|
2102
|
+
bgCropId,
|
|
2103
|
+
liveBgCrop: {
|
|
2104
|
+
x: liveBgCropX,
|
|
2105
|
+
y: liveBgCropY,
|
|
2106
|
+
width: liveBgCropW,
|
|
2107
|
+
height: liveBgCropH,
|
|
2108
|
+
},
|
|
2109
|
+
liveBgCropClip: liveBgCropRect,
|
|
2000
2110
|
rectDragId,
|
|
2001
2111
|
liveRect: {
|
|
2002
2112
|
x: liveRectX,
|
|
@@ -2045,7 +2155,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
2045
2155
|
// drawing to the canvas box by construction, but the stamp overlay is
|
|
2046
2156
|
// plain RN views — without clipping, tiles panned past the edge of a
|
|
2047
2157
|
// non-fullscreen canvas (e.g. a diagram strip) escape into surrounding UI.
|
|
2048
|
-
_jsxs(GestureHandlerRootView, { style: [{ width, height, overflow: 'hidden' }, style], children: [backgroundLayers.map((layer) => (_jsx(BackgroundSkImageLoader, { layer: layer, resolveUrl: resolveImageUrl, 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
|
|
2158
|
+
_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
|
|
2049
2159
|
? (state.measurementsById.get(placed.measurementId) ?? null)
|
|
2050
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
|
|
2051
2161
|
? () => {
|
|
@@ -23,6 +23,8 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
23
23
|
};
|
|
24
24
|
viewport?: ViewportState;
|
|
25
25
|
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
26
|
+
deferBackgroundToResolver?: boolean;
|
|
27
|
+
hideLinkBadges?: boolean;
|
|
26
28
|
backgroundSkImages?: Record<string, SkImage | null>;
|
|
27
29
|
backgroundContent?: ReactNode;
|
|
28
30
|
valueFont: SkFont | null;
|
|
@@ -73,6 +75,26 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
73
75
|
width: AnimatedNumber;
|
|
74
76
|
height: AnimatedNumber;
|
|
75
77
|
};
|
|
78
|
+
bgCropId?: string | null;
|
|
79
|
+
liveBgCrop?: {
|
|
80
|
+
x: AnimatedNumber;
|
|
81
|
+
y: AnimatedNumber;
|
|
82
|
+
width: AnimatedNumber;
|
|
83
|
+
height: AnimatedNumber;
|
|
84
|
+
};
|
|
85
|
+
liveBgCropClip?: {
|
|
86
|
+
x: number;
|
|
87
|
+
y: number;
|
|
88
|
+
width: number;
|
|
89
|
+
height: number;
|
|
90
|
+
} | {
|
|
91
|
+
value: {
|
|
92
|
+
x: number;
|
|
93
|
+
y: number;
|
|
94
|
+
width: number;
|
|
95
|
+
height: number;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
76
98
|
shapeResizeId?: string | null;
|
|
77
99
|
shapeResizePath?: SkPath | {
|
|
78
100
|
value: SkPath;
|
|
@@ -105,5 +127,5 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
105
127
|
wrapContent?: (content: ReactNode) => ReactNode;
|
|
106
128
|
canvasRef?: RefObject<CanvasRef | null>;
|
|
107
129
|
}
|
|
108
|
-
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, backgroundContent, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale, measurementLineScale, customPreview, wrapContent, canvasRef, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
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;
|
|
109
131
|
export {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Canvas, Circle, DashPathEffect, Group, Line, Path, Rect, Skia, } from '@shopify/react-native-skia';
|
|
2
|
+
import { Canvas, Circle, DashPathEffect, Group, Line, Path, Rect, RoundedRect, Skia, } from '@shopify/react-native-skia';
|
|
3
3
|
import { normalizeRect, placementOf } from './measurementGeometry.js';
|
|
4
4
|
import { arrowheadTriangle, dashIntervals, toSkiaStrokeCap, } from './strokeGeometry.js';
|
|
5
5
|
import { SELECTION_PAD, textResizeGeometry, textShapeBounds, } from './textGeometry.js';
|
|
6
6
|
import { visualShapeBounds } from './shapeGeometry.js';
|
|
7
|
-
import { backgroundLayerDocRect, backgroundLayersOf, isBackgroundLayerLocked, } from './backgroundLayers.js';
|
|
7
|
+
import { backgroundCropBars, backgroundLayerDocRect, backgroundLayersOf, isBackgroundLayerLocked, } from './backgroundLayers.js';
|
|
8
|
+
import { YellowColors } from '../../theme/colors.js';
|
|
8
9
|
import { BackgroundLayerElement } from './elements/BackgroundImageElement.js';
|
|
9
10
|
import { PlaneElement, PLANE_CHROME_COLOR, PLANE_GRID_COLOR, PLANE_GRID_OPACITY, PLANE_GRID_WIDTH, PLANE_OUTLINE_WIDTH, } from './elements/PlaneElement.js';
|
|
10
11
|
import { ShapeElement } from './elements/ShapeElement.js';
|
|
@@ -29,6 +30,30 @@ const MEASUREMENT_HANDLE_COLOR = '#0066FF';
|
|
|
29
30
|
// hit-test the text resize handle, which sits on the padded box corner.
|
|
30
31
|
const SELECTION_COLOR = '#0066FF';
|
|
31
32
|
const SELECTION_STROKE = 1.5;
|
|
33
|
+
// Outline of a selected LOCKED background layer: the lock accent (the same
|
|
34
|
+
// yellow the selected group-header border uses), distinct from the blue
|
|
35
|
+
// manipulable-selection chrome — locked means "selected, but pinned".
|
|
36
|
+
const BACKGROUND_LOCKED_COLOR = YellowColors.mainYellow;
|
|
37
|
+
// Link badge on a LINKED background layer (layer.fileId — an expanded file
|
|
38
|
+
// tile, see fileTileSwap.ts): a chain glyph in a white disc at the layer's
|
|
39
|
+
// top-right, saying "this is a group file's image, not a plain background".
|
|
40
|
+
// Always on (selected or not) so the link is legible at a glance — which
|
|
41
|
+
// makes it the one piece of chrome selection-clearing does NOT strip before a
|
|
42
|
+
// capture: hosts must pass `hideLinkBadges` while snapshotting for
|
|
43
|
+
// thumbnails/share/export, or the badge bakes into the output image.
|
|
44
|
+
// Doc-space sized off the visible rect, like the selection stroke and bars.
|
|
45
|
+
const LINKED_BADGE_BG = '#FFFFFF';
|
|
46
|
+
const LINKED_BADGE_INK = '#181A1C';
|
|
47
|
+
const LINKED_BADGE_RADIUS_RATIO = 0.05;
|
|
48
|
+
// Lucide `link` (24-unit viewBox, stroke-width 2) — drawn stroked inside a
|
|
49
|
+
// scale-to-badge Group, so the stroke width scales with the glyph.
|
|
50
|
+
const LINKED_GLYPH_SVG = 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71' +
|
|
51
|
+
'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71';
|
|
52
|
+
// Lazy + cached: on web this module is only evaluated after CanvasKit loads
|
|
53
|
+
// (WithSkiaWeb gates the import), but deferring to first draw costs nothing
|
|
54
|
+
// and can't regress if that ever changes.
|
|
55
|
+
let linkedGlyphPath = null;
|
|
56
|
+
const getLinkedGlyphPath = () => (linkedGlyphPath ?? (linkedGlyphPath = Skia.Path.MakeFromSVGString(LINKED_GLYPH_SVG)));
|
|
32
57
|
// A draggable selection handle (line endpoints, rect corners, text resize):
|
|
33
58
|
// a white disc with a colored ring, so it reads as a distinct, grabbable knob
|
|
34
59
|
// over a line of ANY color — including the default measurement-line blue, which
|
|
@@ -80,7 +105,7 @@ const SelectionBox = ({ bounds, isDragging, transform, }) => (_jsx(DraggableElem
|
|
|
80
105
|
// since the function-call pattern works identically on native we use it
|
|
81
106
|
// in both Inners for consistency. Don't add hooks here; this is a plain
|
|
82
107
|
// JSX-returning helper, not a component.
|
|
83
|
-
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, backgroundContent, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale = 1, measurementLineScale = 1, customPreview, wrapContent, canvasRef, }) => {
|
|
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, }) => {
|
|
84
109
|
// Doc rect a SCALE-mode plane grid covers: the first background layer (the
|
|
85
110
|
// photo being calibrated), else the document rect. Pinned to layer 0 like
|
|
86
111
|
// the tile-sizing reference — adding a second image must not re-span the
|
|
@@ -94,7 +119,39 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
|
|
|
94
119
|
width: effectiveCanvas.viewport.width,
|
|
95
120
|
height: effectiveCanvas.viewport.height,
|
|
96
121
|
};
|
|
97
|
-
const world = (_jsxs(Group, { transform: worldTransform, children: [backgroundContent, backgroundLayersOf(effectiveCanvas.viewport).map((layer) =>
|
|
122
|
+
const world = (_jsxs(Group, { transform: worldTransform, children: [backgroundContent, backgroundLayersOf(effectiveCanvas.viewport).map((layer) => {
|
|
123
|
+
// Native crop-bar drag: clip to the LIVE visible rect (doc space)
|
|
124
|
+
// instead of the committed crop window, which is suppressed so a bar
|
|
125
|
+
// dragged outward reveals what the window hid.
|
|
126
|
+
const isCropDrag = layer.id === bgCropId && liveBgCropClip != null;
|
|
127
|
+
const element = (_jsx(BackgroundLayerElement, { layer: layer, resolveUrl: resolveImageUrl, deferToResolver: deferBackgroundToResolver, skImage: backgroundSkImages?.[layer.id], viewport: viewport, screenWidth: width, screenHeight: height, suppressCrop: isCropDrag }));
|
|
128
|
+
// Link badge on a LINKED layer (an expanded file tile) — always on,
|
|
129
|
+
// selected or not, locked or not: the link is a fact about the layer,
|
|
130
|
+
// not an affordance. A sibling of the (possibly live-clipped) image
|
|
131
|
+
// inside the same DraggableElement, so it rides drags and resizes;
|
|
132
|
+
// hidden during a live crop drag (it would sit on stale committed
|
|
133
|
+
// edges) and under `hideLinkBadges` (capture/export renders).
|
|
134
|
+
const badge = layer.fileId && !isCropDrag && !hideLinkBadges
|
|
135
|
+
? (() => {
|
|
136
|
+
const glyph = getLinkedGlyphPath();
|
|
137
|
+
if (!glyph)
|
|
138
|
+
return null;
|
|
139
|
+
const r = backgroundLayerDocRect(layer);
|
|
140
|
+
const badgeR = Math.min(r.width, r.height) * LINKED_BADGE_RADIUS_RATIO;
|
|
141
|
+
const bc = {
|
|
142
|
+
x: r.x + r.width - 2 * badgeR,
|
|
143
|
+
y: r.y + 2 * badgeR,
|
|
144
|
+
};
|
|
145
|
+
const glyphSize = badgeR * 1.2;
|
|
146
|
+
return (_jsxs(_Fragment, { children: [_jsx(Circle, { c: bc, r: badgeR, color: LINKED_BADGE_BG }), _jsx(Group, { transform: [
|
|
147
|
+
{ translateX: bc.x - glyphSize / 2 },
|
|
148
|
+
{ translateY: bc.y - glyphSize / 2 },
|
|
149
|
+
{ scale: glyphSize / 24 },
|
|
150
|
+
], children: _jsx(Path, { path: glyph, color: LINKED_BADGE_INK, style: "stroke", strokeWidth: 2, strokeCap: "round", strokeJoin: "round" }) })] }));
|
|
151
|
+
})()
|
|
152
|
+
: null;
|
|
153
|
+
return (_jsxs(DraggableElement, { isDragging: layer.id === draggingId || layer.id === resizingId, transform: layer.id === resizingId ? resizeTransform : dragTransform, children: [isCropDrag ? (_jsx(Group, { clip: liveBgCropClip, children: element })) : (element), badge] }, layer.id));
|
|
154
|
+
}), (effectiveCanvas.planes ?? []).map((plane) => {
|
|
98
155
|
if (plane.id === planeDragId && planeDragPath) {
|
|
99
156
|
const statics = plane.mode === 'perspective'
|
|
100
157
|
? plane.corners
|
|
@@ -247,21 +304,40 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
|
|
|
247
304
|
shape.kind === 'polygon';
|
|
248
305
|
return (_jsxs(_Fragment, { children: [_jsx(SelectionBox, { bounds: b, isDragging: isLive, transform: liveTransform }), resizeGeom && handleRadius != null && (_jsx(DraggableElement, { isDragging: isLive, transform: liveTransform, children: _jsx(Handle, { c: resizeGeom.handle, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR }) })), cornerResizable && handleRadius != null && (_jsxs(DraggableElement, { isDragging: isDragging, transform: dragTransform, children: [_jsx(Handle, { c: { x: b.minX, y: b.minY }, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR }), _jsx(Handle, { c: { x: b.maxX, y: b.minY }, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR }), _jsx(Handle, { c: { x: b.minX, y: b.maxY }, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR }), _jsx(Handle, { c: { x: b.maxX, y: b.maxY }, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR })] }))] }));
|
|
249
306
|
}
|
|
250
|
-
// A selected background layer: outline its doc rect
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
// transform the same way the layer itself does
|
|
255
|
-
// (backgroundLayerIsLocked) keeps the outline —
|
|
256
|
-
// the host's chrome offers the lock toggle — but
|
|
257
|
-
//
|
|
307
|
+
// A selected background layer: outline its VISIBLE (cropped) doc rect
|
|
308
|
+
// and, when unlocked, offer FOUR corner handles (each scales about
|
|
309
|
+
// its opposite corner) plus four edge crop bars (raster layers only —
|
|
310
|
+
// dragging a bar windows the image non-destructively). Chrome tracks
|
|
311
|
+
// the live drag/resize transform the same way the layer itself does
|
|
312
|
+
// above. A LOCKED layer (backgroundLayerIsLocked) keeps the outline —
|
|
313
|
+
// it is selected, and the host's chrome offers the lock toggle — but
|
|
314
|
+
// recolored to the lock yellow and with no handles: it cannot scale
|
|
315
|
+
// or crop, so a grabbable gizmo would lie. During a native crop-bar
|
|
316
|
+
// drag the chrome is just the LIVE outline — handles and bars would
|
|
317
|
+
// sit on stale committed edges.
|
|
258
318
|
const layer = backgroundLayersOf(effectiveCanvas.viewport).find((l) => l.id === selectedId);
|
|
259
319
|
if (layer) {
|
|
320
|
+
if (layer.id === bgCropId && liveBgCrop) {
|
|
321
|
+
return (_jsx(Rect, { x: liveBgCrop.x, y: liveBgCrop.y, width: liveBgCrop.width, height: liveBgCrop.height, color: SELECTION_COLOR, style: "stroke", strokeWidth: SELECTION_STROKE }));
|
|
322
|
+
}
|
|
260
323
|
const locked = isBackgroundLayerLocked(effectiveCanvas.viewport, layer.id);
|
|
261
324
|
const r = backgroundLayerDocRect(layer);
|
|
262
325
|
const isResizing = layer.id === resizingId;
|
|
263
326
|
const liveTransform = isResizing ? resizeTransform : dragTransform;
|
|
264
|
-
|
|
327
|
+
const corners = [
|
|
328
|
+
{ x: r.x, y: r.y },
|
|
329
|
+
{ x: r.x + r.width, y: r.y },
|
|
330
|
+
{ x: r.x, y: r.y + r.height },
|
|
331
|
+
{ x: r.x + r.width, y: r.y + r.height },
|
|
332
|
+
];
|
|
333
|
+
return (_jsxs(DraggableElement, { isDragging: isDragging || isResizing, transform: liveTransform, children: [_jsx(Rect, { x: r.x, y: r.y, width: r.width, height: r.height, color: locked ? BACKGROUND_LOCKED_COLOR : SELECTION_COLOR, style: "stroke", strokeWidth: SELECTION_STROKE }), !locked &&
|
|
334
|
+
layer.format !== 'svg' &&
|
|
335
|
+
backgroundCropBars(layer).map((bar) => {
|
|
336
|
+
const inset = Math.min(bar.width, bar.height) * 0.18;
|
|
337
|
+
return (_jsxs(Group, { children: [_jsx(RoundedRect, { x: bar.x, y: bar.y, width: bar.width, height: bar.height, r: Math.min(bar.width, bar.height) / 2, color: "#FFFFFF" }), _jsx(RoundedRect, { x: bar.x + inset, y: bar.y + inset, width: bar.width - 2 * inset, height: bar.height - 2 * inset, r: (Math.min(bar.width, bar.height) - 2 * inset) / 2, color: SELECTION_COLOR })] }, bar.edge));
|
|
338
|
+
}), handleRadius != null &&
|
|
339
|
+
!locked &&
|
|
340
|
+
corners.map((c, i) => (_jsx(Handle, { c: c, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR }, i)))] }));
|
|
265
341
|
}
|
|
266
342
|
return null;
|
|
267
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] }));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode, ComponentType } from 'react';
|
|
2
2
|
import type { AnnotationCanvasState, AnnotationDocumentPatch, AnnotationElement, AnnotationElementId, Selection, StrokeCap, Vec2 } from '../../types/annotation.js';
|
|
3
|
+
import type { BackgroundCropEdge } from './backgroundLayers.js';
|
|
3
4
|
import type { MeasurementRef } from './measurementPicker.js';
|
|
4
5
|
import type { RectCorner } from './measurementGeometry.js';
|
|
5
6
|
import type { ShapeToolKind } from './shapeGeometry.js';
|
|
@@ -90,7 +91,9 @@ export interface DragSelectionConfig {
|
|
|
90
91
|
locked: boolean;
|
|
91
92
|
} | null;
|
|
92
93
|
hitTestResizeHandle?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): ResizeGeometry | null;
|
|
93
|
-
buildResizePatch?(doc: AnnotationCanvasState, id: AnnotationElementId, delta: Vec2): AnnotationDocumentPatch | null;
|
|
94
|
+
buildResizePatch?(doc: AnnotationCanvasState, id: AnnotationElementId, delta: Vec2, geom?: ResizeGeometry): AnnotationDocumentPatch | null;
|
|
95
|
+
hitTestBackgroundCropBar?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): BackgroundCropEdge | null;
|
|
96
|
+
buildBackgroundCropPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, edge: BackgroundCropEdge, delta: Vec2): AnnotationDocumentPatch | null;
|
|
94
97
|
}
|
|
95
98
|
export interface PlaneEditConfig {
|
|
96
99
|
hitHandle(doc: AnnotationCanvasState, world: Vec2, zoom: number): number | null;
|
|
@@ -1,16 +1,35 @@
|
|
|
1
|
-
import type { AnnotationBackgroundLayer, AnnotationViewport, BackgroundLayerTransform } from '../../types/annotation.js';
|
|
1
|
+
import type { AnnotationBackgroundLayer, AnnotationViewport, BackgroundLayerCrop, BackgroundLayerTransform } from '../../types/annotation.js';
|
|
2
2
|
export declare const LEGACY_BACKGROUND_LAYER_ID = "background-legacy";
|
|
3
3
|
export declare const BACKGROUND_PLACEMENT_GAP_RATIO = 0.02;
|
|
4
4
|
export declare const backgroundLayersOf: (viewport: AnnotationViewport) => AnnotationBackgroundLayer[];
|
|
5
5
|
export declare const backgroundLayerIsLocked: (layer: AnnotationBackgroundLayer, index: number) => boolean;
|
|
6
6
|
export declare const isBackgroundLayerLocked: (viewport: AnnotationViewport, id: string) => boolean;
|
|
7
7
|
export declare const defaultBackgroundLayerLocked: (existing: AnnotationBackgroundLayer[]) => boolean;
|
|
8
|
+
export declare const backgroundLayerCropRect: (layer: AnnotationBackgroundLayer) => BackgroundLayerCrop;
|
|
8
9
|
export declare const backgroundLayerDocRect: (layer: AnnotationBackgroundLayer) => {
|
|
9
10
|
x: number;
|
|
10
11
|
y: number;
|
|
11
12
|
width: number;
|
|
12
13
|
height: number;
|
|
13
14
|
};
|
|
15
|
+
export declare const backgroundLayerFullDocRect: (layer: AnnotationBackgroundLayer) => {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
};
|
|
21
|
+
export declare const MIN_BACKGROUND_DOC_EXTENT = 16;
|
|
22
|
+
export declare const MAX_BACKGROUND_DOC_EXTENT = 65536;
|
|
23
|
+
export type BackgroundCropEdge = 'top' | 'bottom' | 'left' | 'right';
|
|
24
|
+
export declare const BACKGROUND_CROP_EDGES: readonly BackgroundCropEdge[];
|
|
25
|
+
export interface BackgroundCropBar {
|
|
26
|
+
edge: BackgroundCropEdge;
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
}
|
|
32
|
+
export declare const backgroundCropBars: (layer: AnnotationBackgroundLayer) => BackgroundCropBar[];
|
|
14
33
|
export declare const backgroundLayersUnionRect: (layers: AnnotationBackgroundLayer[]) => {
|
|
15
34
|
x: number;
|
|
16
35
|
y: number;
|
|
@@ -69,14 +69,105 @@ export const isBackgroundLayerLocked = (viewport, id) => {
|
|
|
69
69
|
// against it. Written explicitly onto the new layer so the state survives
|
|
70
70
|
// later removals — a second image stays free even once it is the only one.
|
|
71
71
|
export const defaultBackgroundLayerLocked = (existing) => existing.length === 0;
|
|
72
|
-
// The
|
|
73
|
-
// the
|
|
74
|
-
|
|
72
|
+
// The layer's crop window in intrinsic pixels, clamped defensively into the
|
|
73
|
+
// image bounds (a hand-edited or future-client doc must not push the visible
|
|
74
|
+
// rect outside the bytes). Absent crop = the full image.
|
|
75
|
+
export const backgroundLayerCropRect = (layer) => {
|
|
76
|
+
const c = layer.crop;
|
|
77
|
+
if (!c)
|
|
78
|
+
return { x: 0, y: 0, width: layer.widthPx, height: layer.heightPx };
|
|
79
|
+
const x = Math.min(Math.max(c.x, 0), layer.widthPx);
|
|
80
|
+
const y = Math.min(Math.max(c.y, 0), layer.heightPx);
|
|
81
|
+
return {
|
|
82
|
+
x,
|
|
83
|
+
y,
|
|
84
|
+
width: Math.min(Math.max(c.width, 0), layer.widthPx - x),
|
|
85
|
+
height: Math.min(Math.max(c.height, 0), layer.heightPx - y),
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
// The doc-space rectangle a layer occupies: its VISIBLE (crop-windowed)
|
|
89
|
+
// pixel grid under the layer's translate+scale. Everything that treats "the
|
|
90
|
+
// layer" as a rect — selection chrome, hit-tests, fit/union math, new-layer
|
|
91
|
+
// placement — reads this, so a crop moves all of them together; only the
|
|
92
|
+
// renderer additionally knows the full image extends past it (clipped, see
|
|
93
|
+
// BackgroundLayerElement).
|
|
94
|
+
export const backgroundLayerDocRect = (layer) => {
|
|
95
|
+
const c = backgroundLayerCropRect(layer);
|
|
96
|
+
return {
|
|
97
|
+
x: layer.transform.x + c.x * layer.transform.scaleX,
|
|
98
|
+
y: layer.transform.y + c.y * layer.transform.scaleY,
|
|
99
|
+
width: c.width * layer.transform.scaleX,
|
|
100
|
+
height: c.height * layer.transform.scaleY,
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
// The full (uncropped) image's doc rect — the bounds a crop edge can expand
|
|
104
|
+
// back out to.
|
|
105
|
+
export const backgroundLayerFullDocRect = (layer) => ({
|
|
75
106
|
x: layer.transform.x,
|
|
76
107
|
y: layer.transform.y,
|
|
77
108
|
width: layer.widthPx * layer.transform.scaleX,
|
|
78
109
|
height: layer.heightPx * layer.transform.scaleY,
|
|
79
110
|
});
|
|
111
|
+
// Doc-space clamp on a background layer's larger doc dimension (corner
|
|
112
|
+
// resize) and on its visible extent (crop). The floor keeps a drag from
|
|
113
|
+
// collapsing a layer below grabbable size; the ceiling keeps the doc rect
|
|
114
|
+
// (which feeds fit math and the web SVG raster planner) from exploding to
|
|
115
|
+
// numerically silly sizes. Here rather than in selectTool because the native
|
|
116
|
+
// crop preview worklet needs the floor too.
|
|
117
|
+
export const MIN_BACKGROUND_DOC_EXTENT = 16;
|
|
118
|
+
export const MAX_BACKGROUND_DOC_EXTENT = 65536;
|
|
119
|
+
export const BACKGROUND_CROP_EDGES = [
|
|
120
|
+
'top',
|
|
121
|
+
'bottom',
|
|
122
|
+
'left',
|
|
123
|
+
'right',
|
|
124
|
+
];
|
|
125
|
+
// Bar proportions relative to the visible rect. Doc-space sizing, like the
|
|
126
|
+
// selection box stroke — the bars belong to the rect they crop, so they zoom
|
|
127
|
+
// with it; grab tolerance stays screen-constant in the hit-test instead.
|
|
128
|
+
const CROP_BAR_LENGTH_RATIO = 0.15;
|
|
129
|
+
const CROP_BAR_THICKNESS_RATIO = 0.22;
|
|
130
|
+
// The four crop bars for a layer's visible rect: pill-shaped grabs centered on
|
|
131
|
+
// each edge midpoint, long side along the edge.
|
|
132
|
+
export const backgroundCropBars = (layer) => {
|
|
133
|
+
const r = backgroundLayerDocRect(layer);
|
|
134
|
+
const midX = r.x + r.width / 2;
|
|
135
|
+
const midY = r.y + r.height / 2;
|
|
136
|
+
const hLen = r.width * CROP_BAR_LENGTH_RATIO;
|
|
137
|
+
const hThick = hLen * CROP_BAR_THICKNESS_RATIO;
|
|
138
|
+
const vLen = r.height * CROP_BAR_LENGTH_RATIO;
|
|
139
|
+
const vThick = vLen * CROP_BAR_THICKNESS_RATIO;
|
|
140
|
+
return [
|
|
141
|
+
{
|
|
142
|
+
edge: 'top',
|
|
143
|
+
x: midX - hLen / 2,
|
|
144
|
+
y: r.y - hThick / 2,
|
|
145
|
+
width: hLen,
|
|
146
|
+
height: hThick,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
edge: 'bottom',
|
|
150
|
+
x: midX - hLen / 2,
|
|
151
|
+
y: r.y + r.height - hThick / 2,
|
|
152
|
+
width: hLen,
|
|
153
|
+
height: hThick,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
edge: 'left',
|
|
157
|
+
x: r.x - vThick / 2,
|
|
158
|
+
y: midY - vLen / 2,
|
|
159
|
+
width: vThick,
|
|
160
|
+
height: vLen,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
edge: 'right',
|
|
164
|
+
x: r.x + r.width - vThick / 2,
|
|
165
|
+
y: midY - vLen / 2,
|
|
166
|
+
width: vThick,
|
|
167
|
+
height: vLen,
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
};
|
|
80
171
|
// Bounding box of every layer's doc rect. Null when there are no layers —
|
|
81
172
|
// callers fall back to the document rect.
|
|
82
173
|
export const backgroundLayersUnionRect = (layers) => {
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { type SkImage } from '@shopify/react-native-skia';
|
|
2
2
|
import type { AnnotationBackgroundImage, AnnotationBackgroundLayer } from '../../../types/annotation.js';
|
|
3
3
|
import type { ViewportState } from '../viewport.js';
|
|
4
|
-
export declare const useBackgroundSkImage: (image: AnnotationBackgroundImage | null | undefined, resolveUrl?: (path: string) => Promise<string
|
|
4
|
+
export declare const useBackgroundSkImage: (image: AnnotationBackgroundImage | null | undefined, resolveUrl?: (path: string) => Promise<string>, deferToResolver?: boolean) => SkImage | null;
|
|
5
5
|
export interface BackgroundSkImageLoaderProps {
|
|
6
6
|
layer: AnnotationBackgroundLayer;
|
|
7
7
|
resolveUrl?: (path: string) => Promise<string>;
|
|
8
|
+
deferToResolver?: boolean;
|
|
8
9
|
onImage: (id: string, image: SkImage | null) => void;
|
|
9
10
|
}
|
|
10
|
-
export declare const BackgroundSkImageLoader: ({ layer, resolveUrl, onImage, }: BackgroundSkImageLoaderProps) => null;
|
|
11
|
+
export declare const BackgroundSkImageLoader: ({ layer, resolveUrl, deferToResolver, onImage, }: BackgroundSkImageLoaderProps) => null;
|
|
11
12
|
export interface BackgroundLayerElementProps {
|
|
12
13
|
layer: AnnotationBackgroundLayer;
|
|
13
14
|
resolveUrl?: (path: string) => Promise<string>;
|
|
15
|
+
deferToResolver?: boolean;
|
|
14
16
|
skImage?: SkImage | null;
|
|
15
17
|
viewport?: ViewportState;
|
|
16
18
|
screenWidth?: number;
|
|
17
19
|
screenHeight?: number;
|
|
20
|
+
suppressCrop?: boolean;
|
|
18
21
|
}
|
|
19
|
-
export declare const BackgroundLayerElement: import("react").MemoExoticComponent<({ layer, resolveUrl, skImage: skImageProp, viewport, screenWidth, screenHeight, }: BackgroundLayerElementProps) => import("react/jsx-runtime").JSX.Element | null>;
|
|
22
|
+
export declare const BackgroundLayerElement: import("react").MemoExoticComponent<({ layer, resolveUrl, deferToResolver, skImage: skImageProp, viewport, screenWidth, screenHeight, suppressCrop, }: BackgroundLayerElementProps) => import("react/jsx-runtime").JSX.Element | null>;
|