@reekon-tools/boldr-utils 1.19.5 → 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 +2 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.js +12 -1
- package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +2 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +329 -35
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +28 -1
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +99 -14
- package/dist/annotation/canvas/Tool.d.ts +10 -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 +2 -1
- package/dist/annotation/canvas/elements/BackgroundImageElement.js +10 -2
- package/dist/annotation/canvas/fileTileSwap.d.ts +12 -0
- package/dist/annotation/canvas/fileTileSwap.js +87 -0
- package/dist/annotation/canvas/snapGeometry.d.ts +32 -0
- package/dist/annotation/canvas/snapGeometry.js +168 -0
- package/dist/annotation/canvas/stampLayout.d.ts +1 -1
- package/dist/annotation/canvas/stampLayout.js +10 -8
- package/dist/annotation/canvas/tools/measurementTool.js +38 -4
- package/dist/annotation/canvas/tools/selectTool.d.ts +18 -0
- package/dist/annotation/canvas/tools/selectTool.js +201 -43
- package/dist/annotation/canvas/tools/shapeTool.js +27 -5
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +2 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +19 -2
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +5 -1
- package/dist/exports.d.ts +3 -1
- package/dist/exports.js +2 -0
- package/dist/types/annotation.d.ts +10 -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
|
@@ -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,12 +30,45 @@ 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
|
+
// 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';
|
|
37
|
+
// Outline of a selected LOCKED background layer: the lock accent (the same
|
|
38
|
+
// yellow the selected group-header border uses), distinct from the blue
|
|
39
|
+
// manipulable-selection chrome — locked means "selected, but pinned".
|
|
40
|
+
const BACKGROUND_LOCKED_COLOR = YellowColors.mainYellow;
|
|
41
|
+
// Link badge on a LINKED background layer (layer.fileId — an expanded file
|
|
42
|
+
// tile, see fileTileSwap.ts): a chain glyph in a white disc at the layer's
|
|
43
|
+
// top-right, saying "this is a group file's image, not a plain background".
|
|
44
|
+
// Always on (selected or not) so the link is legible at a glance — which
|
|
45
|
+
// makes it the one piece of chrome selection-clearing does NOT strip before a
|
|
46
|
+
// capture: hosts must pass `hideLinkBadges` while snapshotting for
|
|
47
|
+
// thumbnails/share/export, or the badge bakes into the output image.
|
|
48
|
+
// Doc-space sized off the visible rect, like the selection stroke and bars.
|
|
49
|
+
const LINKED_BADGE_BG = '#FFFFFF';
|
|
50
|
+
const LINKED_BADGE_INK = '#181A1C';
|
|
51
|
+
const LINKED_BADGE_RADIUS_RATIO = 0.05;
|
|
52
|
+
// Lucide `link` (24-unit viewBox, stroke-width 2) — drawn stroked inside a
|
|
53
|
+
// scale-to-badge Group, so the stroke width scales with the glyph.
|
|
54
|
+
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' +
|
|
55
|
+
'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71';
|
|
56
|
+
// Lazy + cached: on web this module is only evaluated after CanvasKit loads
|
|
57
|
+
// (WithSkiaWeb gates the import), but deferring to first draw costs nothing
|
|
58
|
+
// and can't regress if that ever changes.
|
|
59
|
+
let linkedGlyphPath = null;
|
|
60
|
+
const getLinkedGlyphPath = () => (linkedGlyphPath ?? (linkedGlyphPath = Skia.Path.MakeFromSVGString(LINKED_GLYPH_SVG)));
|
|
32
61
|
// A draggable selection handle (line endpoints, rect corners, text resize):
|
|
33
62
|
// a white disc with a colored ring, so it reads as a distinct, grabbable knob
|
|
34
63
|
// over a line of ANY color — including the default measurement-line blue, which
|
|
35
64
|
// a solid-colored handle would camouflage into. `c`/`r`/`ringWidth` may be
|
|
36
65
|
// Reanimated values (native live-drag) or plain numbers (web).
|
|
37
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 }));
|
|
38
72
|
// Bounds of a flat [x,y,x,y,…] stroke point array, or null if empty.
|
|
39
73
|
const strokeBounds = (points) => {
|
|
40
74
|
if (points.length < 2)
|
|
@@ -80,7 +114,7 @@ const SelectionBox = ({ bounds, isDragging, transform, }) => (_jsx(DraggableElem
|
|
|
80
114
|
// since the function-call pattern works identically on native we use it
|
|
81
115
|
// in both Inners for consistency. Don't add hooks here; this is a plain
|
|
82
116
|
// JSX-returning helper, not a component.
|
|
83
|
-
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, deferBackgroundToResolver, 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, }) => {
|
|
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, }) => {
|
|
84
118
|
// Doc rect a SCALE-mode plane grid covers: the first background layer (the
|
|
85
119
|
// photo being calibrated), else the document rect. Pinned to layer 0 like
|
|
86
120
|
// the tile-sizing reference — adding a second image must not re-span the
|
|
@@ -94,7 +128,39 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
|
|
|
94
128
|
width: effectiveCanvas.viewport.width,
|
|
95
129
|
height: effectiveCanvas.viewport.height,
|
|
96
130
|
};
|
|
97
|
-
const world = (_jsxs(Group, { transform: worldTransform, children: [backgroundContent, backgroundLayersOf(effectiveCanvas.viewport).map((layer) =>
|
|
131
|
+
const world = (_jsxs(Group, { transform: worldTransform, children: [backgroundContent, backgroundLayersOf(effectiveCanvas.viewport).map((layer) => {
|
|
132
|
+
// Native crop-bar drag: clip to the LIVE visible rect (doc space)
|
|
133
|
+
// instead of the committed crop window, which is suppressed so a bar
|
|
134
|
+
// dragged outward reveals what the window hid.
|
|
135
|
+
const isCropDrag = layer.id === bgCropId && liveBgCropClip != null;
|
|
136
|
+
const element = (_jsx(BackgroundLayerElement, { layer: layer, resolveUrl: resolveImageUrl, deferToResolver: deferBackgroundToResolver, skImage: backgroundSkImages?.[layer.id], viewport: viewport, screenWidth: width, screenHeight: height, suppressCrop: isCropDrag }));
|
|
137
|
+
// Link badge on a LINKED layer (an expanded file tile) — always on,
|
|
138
|
+
// selected or not, locked or not: the link is a fact about the layer,
|
|
139
|
+
// not an affordance. A sibling of the (possibly live-clipped) image
|
|
140
|
+
// inside the same DraggableElement, so it rides drags and resizes;
|
|
141
|
+
// hidden during a live crop drag (it would sit on stale committed
|
|
142
|
+
// edges) and under `hideLinkBadges` (capture/export renders).
|
|
143
|
+
const badge = layer.fileId && !isCropDrag && !hideLinkBadges
|
|
144
|
+
? (() => {
|
|
145
|
+
const glyph = getLinkedGlyphPath();
|
|
146
|
+
if (!glyph)
|
|
147
|
+
return null;
|
|
148
|
+
const r = backgroundLayerDocRect(layer);
|
|
149
|
+
const badgeR = Math.min(r.width, r.height) * LINKED_BADGE_RADIUS_RATIO;
|
|
150
|
+
const bc = {
|
|
151
|
+
x: r.x + r.width - 2 * badgeR,
|
|
152
|
+
y: r.y + 2 * badgeR,
|
|
153
|
+
};
|
|
154
|
+
const glyphSize = badgeR * 1.2;
|
|
155
|
+
return (_jsxs(_Fragment, { children: [_jsx(Circle, { c: bc, r: badgeR, color: LINKED_BADGE_BG }), _jsx(Group, { transform: [
|
|
156
|
+
{ translateX: bc.x - glyphSize / 2 },
|
|
157
|
+
{ translateY: bc.y - glyphSize / 2 },
|
|
158
|
+
{ scale: glyphSize / 24 },
|
|
159
|
+
], children: _jsx(Path, { path: glyph, color: LINKED_BADGE_INK, style: "stroke", strokeWidth: 2, strokeCap: "round", strokeJoin: "round" }) })] }));
|
|
160
|
+
})()
|
|
161
|
+
: null;
|
|
162
|
+
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));
|
|
163
|
+
}), (effectiveCanvas.planes ?? []).map((plane) => {
|
|
98
164
|
if (plane.id === planeDragId && planeDragPath) {
|
|
99
165
|
const statics = plane.mode === 'perspective'
|
|
100
166
|
? plane.corners
|
|
@@ -247,23 +313,42 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
|
|
|
247
313
|
shape.kind === 'polygon';
|
|
248
314
|
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
315
|
}
|
|
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
|
-
//
|
|
316
|
+
// A selected background layer: outline its VISIBLE (cropped) doc rect
|
|
317
|
+
// and, when unlocked, offer FOUR corner handles (each scales about
|
|
318
|
+
// its opposite corner) plus four edge crop bars (raster layers only —
|
|
319
|
+
// dragging a bar windows the image non-destructively). Chrome tracks
|
|
320
|
+
// the live drag/resize transform the same way the layer itself does
|
|
321
|
+
// above. A LOCKED layer (backgroundLayerIsLocked) keeps the outline —
|
|
322
|
+
// it is selected, and the host's chrome offers the lock toggle — but
|
|
323
|
+
// recolored to the lock yellow and with no handles: it cannot scale
|
|
324
|
+
// or crop, so a grabbable gizmo would lie. During a native crop-bar
|
|
325
|
+
// drag the chrome is just the LIVE outline — handles and bars would
|
|
326
|
+
// sit on stale committed edges.
|
|
258
327
|
const layer = backgroundLayersOf(effectiveCanvas.viewport).find((l) => l.id === selectedId);
|
|
259
328
|
if (layer) {
|
|
329
|
+
if (layer.id === bgCropId && liveBgCrop) {
|
|
330
|
+
return (_jsx(Rect, { x: liveBgCrop.x, y: liveBgCrop.y, width: liveBgCrop.width, height: liveBgCrop.height, color: SELECTION_COLOR, style: "stroke", strokeWidth: SELECTION_STROKE }));
|
|
331
|
+
}
|
|
260
332
|
const locked = isBackgroundLayerLocked(effectiveCanvas.viewport, layer.id);
|
|
261
333
|
const r = backgroundLayerDocRect(layer);
|
|
262
334
|
const isResizing = layer.id === resizingId;
|
|
263
335
|
const liveTransform = isResizing ? resizeTransform : dragTransform;
|
|
264
|
-
|
|
336
|
+
const corners = [
|
|
337
|
+
{ x: r.x, y: r.y },
|
|
338
|
+
{ x: r.x + r.width, y: r.y },
|
|
339
|
+
{ x: r.x, y: r.y + r.height },
|
|
340
|
+
{ x: r.x + r.width, y: r.y + r.height },
|
|
341
|
+
];
|
|
342
|
+
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 &&
|
|
343
|
+
layer.format !== 'svg' &&
|
|
344
|
+
backgroundCropBars(layer).map((bar) => {
|
|
345
|
+
const inset = Math.min(bar.width, bar.height) * 0.18;
|
|
346
|
+
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));
|
|
347
|
+
}), handleRadius != null &&
|
|
348
|
+
!locked &&
|
|
349
|
+
corners.map((c, i) => (_jsx(Handle, { c: c, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR }, i)))] }));
|
|
265
350
|
}
|
|
266
351
|
return null;
|
|
267
|
-
})(), 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] }));
|
|
268
353
|
return (_jsx(Canvas, { ref: canvasRef, style: { width, height }, children: wrapContent ? wrapContent(world) : world }));
|
|
269
354
|
};
|
|
@@ -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';
|
|
@@ -18,6 +19,11 @@ export interface CanvasPointerEvent {
|
|
|
18
19
|
metaKey?: boolean;
|
|
19
20
|
ctrlKey?: boolean;
|
|
20
21
|
}
|
|
22
|
+
export interface SnapContext {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
radiusPx: number;
|
|
25
|
+
setIndicator(point: Vec2 | null): void;
|
|
26
|
+
}
|
|
21
27
|
export interface ToolContext {
|
|
22
28
|
document: AnnotationCanvasState;
|
|
23
29
|
selection: Selection | null;
|
|
@@ -26,6 +32,7 @@ export interface ToolContext {
|
|
|
26
32
|
tileScaleFactor: number;
|
|
27
33
|
headerTileScaleFactor: number;
|
|
28
34
|
fileTileScaleFactor: number;
|
|
35
|
+
snap?: SnapContext | null;
|
|
29
36
|
preview(patch: AnnotationDocumentPatch): void;
|
|
30
37
|
commit(patch: AnnotationDocumentPatch): void;
|
|
31
38
|
setSelection(selection: Selection | null): void;
|
|
@@ -90,7 +97,9 @@ export interface DragSelectionConfig {
|
|
|
90
97
|
locked: boolean;
|
|
91
98
|
} | null;
|
|
92
99
|
hitTestResizeHandle?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): ResizeGeometry | null;
|
|
93
|
-
buildResizePatch?(doc: AnnotationCanvasState, id: AnnotationElementId, delta: Vec2): AnnotationDocumentPatch | null;
|
|
100
|
+
buildResizePatch?(doc: AnnotationCanvasState, id: AnnotationElementId, delta: Vec2, geom?: ResizeGeometry): AnnotationDocumentPatch | null;
|
|
101
|
+
hitTestBackgroundCropBar?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): BackgroundCropEdge | null;
|
|
102
|
+
buildBackgroundCropPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, edge: BackgroundCropEdge, delta: Vec2): AnnotationDocumentPatch | null;
|
|
94
103
|
}
|
|
95
104
|
export interface PlaneEditConfig {
|
|
96
105
|
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) => {
|
|
@@ -17,5 +17,6 @@ export interface BackgroundLayerElementProps {
|
|
|
17
17
|
viewport?: ViewportState;
|
|
18
18
|
screenWidth?: number;
|
|
19
19
|
screenHeight?: number;
|
|
20
|
+
suppressCrop?: boolean;
|
|
20
21
|
}
|
|
21
|
-
export declare const BackgroundLayerElement: import("react").MemoExoticComponent<({ layer, resolveUrl, deferToResolver, 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>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Group, Image, useImage, } from '@shopify/react-native-skia';
|
|
2
|
+
import { Group, Image, rect, useImage, } from '@shopify/react-native-skia';
|
|
3
3
|
import { memo, useEffect, useRef } from 'react';
|
|
4
4
|
import { backgroundLayerDocRect } from '../backgroundLayers.js';
|
|
5
5
|
import { useBackgroundUrl } from './backgroundUrl.js';
|
|
@@ -38,7 +38,7 @@ export const BackgroundSkImageLoader = ({ layer, resolveUrl, deferToResolver, on
|
|
|
38
38
|
// A component (not inlined into the scene helper) because each layer owns
|
|
39
39
|
// per-image hooks — URL resolution and raster decode — and AnnotationCanvasSkia
|
|
40
40
|
// is a hook-free plain function.
|
|
41
|
-
export const BackgroundLayerElement = memo(({ layer, resolveUrl, deferToResolver, skImage: skImageProp, viewport, screenWidth, screenHeight, }) => {
|
|
41
|
+
export const BackgroundLayerElement = memo(({ layer, resolveUrl, deferToResolver, skImage: skImageProp, viewport, screenWidth, screenHeight, suppressCrop, }) => {
|
|
42
42
|
const isSvg = layer.format === 'svg';
|
|
43
43
|
// Self-load only when no pre-decoded image is provided; the hook still runs
|
|
44
44
|
// unconditionally (with a null source, so it no-ops) to satisfy hook rules.
|
|
@@ -60,6 +60,14 @@ export const BackgroundLayerElement = memo(({ layer, resolveUrl, deferToResolver
|
|
|
60
60
|
}
|
|
61
61
|
if (!content)
|
|
62
62
|
return null;
|
|
63
|
+
// Committed crop window: clip in the layer's intrinsic pixel space (inside
|
|
64
|
+
// the transform, so it scales with the image). Non-destructive — the full
|
|
65
|
+
// image is drawn, the clip just windows it. See suppressCrop for the one
|
|
66
|
+
// case that skips it.
|
|
67
|
+
const crop = !suppressCrop && layer.crop ? layer.crop : null;
|
|
68
|
+
if (crop) {
|
|
69
|
+
content = (_jsx(Group, { clip: rect(crop.x, crop.y, crop.width, crop.height), children: content }));
|
|
70
|
+
}
|
|
63
71
|
return (_jsx(Group, { transform: [
|
|
64
72
|
{ translateX: t.x },
|
|
65
73
|
{ translateY: t.y },
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationElementId } from '../../types/annotation.js';
|
|
2
|
+
import type { FileUpload } from '../../types/firestore.js';
|
|
3
|
+
export interface FileTileImageSource {
|
|
4
|
+
storagePath: string;
|
|
5
|
+
downloadUrl: string;
|
|
6
|
+
widthPx: number;
|
|
7
|
+
heightPx: number;
|
|
8
|
+
format?: 'raster' | 'svg';
|
|
9
|
+
}
|
|
10
|
+
export declare const fileTileImageSource: (file: Pick<FileUpload, "type" | "fileData"> | null | undefined) => FileTileImageSource | null;
|
|
11
|
+
export declare const expandFileTileToBackground: (doc: AnnotationCanvasState, placedId: AnnotationElementId, layerId: AnnotationElementId, image: FileTileImageSource) => AnnotationDocumentPatch | null;
|
|
12
|
+
export declare const collapseBackgroundToFileTile: (doc: AnnotationCanvasState, layerId: AnnotationElementId, tileId: AnnotationElementId, now?: number) => AnnotationDocumentPatch | null;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DEFAULT_LAYER_ID, } from '../../types/annotation.js';
|
|
2
|
+
import { backgroundLayerDocRect, backgroundLayersOf, } from './backgroundLayers.js';
|
|
3
|
+
import { imageDocRect } from './viewport.js';
|
|
4
|
+
export const fileTileImageSource = (file) => {
|
|
5
|
+
if (!file)
|
|
6
|
+
return null;
|
|
7
|
+
// Structural, not type-switched: Canvas, Annotation and Template docs all
|
|
8
|
+
// carry AnnotationFileData; anything else (Label, Note, Calculator, …) has
|
|
9
|
+
// no `canvas` key and falls out here.
|
|
10
|
+
const data = file.fileData;
|
|
11
|
+
if (!data?.canvas || data.isLabel)
|
|
12
|
+
return null;
|
|
13
|
+
if (data.canvas.viewport.pdfDocument)
|
|
14
|
+
return null;
|
|
15
|
+
const raster = backgroundLayersOf(data.canvas.viewport).find((l) => l.format !== 'svg' && l.storagePath && l.widthPx > 0);
|
|
16
|
+
if (!raster)
|
|
17
|
+
return null;
|
|
18
|
+
return {
|
|
19
|
+
storagePath: raster.storagePath,
|
|
20
|
+
downloadUrl: raster.downloadUrl,
|
|
21
|
+
widthPx: raster.widthPx,
|
|
22
|
+
heightPx: raster.heightPx,
|
|
23
|
+
...(raster.format ? { format: raster.format } : {}),
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
// Patch that replaces the object tile `placedId` with a background layer
|
|
27
|
+
// showing `image`: the image contain-fits the document rect (the size a
|
|
28
|
+
// first-placed background gets) and centers on the tile's anchor, so it
|
|
29
|
+
// appears where the tile was. The layer starts FREE — the point of expanding
|
|
30
|
+
// is to arrange the photo, and the pill's lock is one tap away. Null when the
|
|
31
|
+
// tile is missing or isn't an object tile.
|
|
32
|
+
export const expandFileTileToBackground = (doc, placedId, layerId, image) => {
|
|
33
|
+
const placed = doc.placedMeasurements.find((m) => m.id === placedId);
|
|
34
|
+
if (!placed?.fileId)
|
|
35
|
+
return null;
|
|
36
|
+
const fitted = imageDocRect(image.widthPx, image.heightPx, doc.viewport.width, doc.viewport.height, 'contain');
|
|
37
|
+
const layer = {
|
|
38
|
+
id: layerId,
|
|
39
|
+
storagePath: image.storagePath,
|
|
40
|
+
downloadUrl: image.downloadUrl,
|
|
41
|
+
widthPx: image.widthPx,
|
|
42
|
+
heightPx: image.heightPx,
|
|
43
|
+
...(image.format ? { format: image.format } : {}),
|
|
44
|
+
transform: {
|
|
45
|
+
x: placed.anchor.x - fitted.width / 2,
|
|
46
|
+
y: placed.anchor.y - fitted.height / 2,
|
|
47
|
+
scaleX: image.widthPx > 0 ? fitted.width / image.widthPx : 1,
|
|
48
|
+
scaleY: image.heightPx > 0 ? fitted.height / image.heightPx : 1,
|
|
49
|
+
},
|
|
50
|
+
locked: false,
|
|
51
|
+
fileId: placed.fileId,
|
|
52
|
+
...(placed.filePath ? { filePath: placed.filePath } : {}),
|
|
53
|
+
...(placed.groupId ? { fileGroupId: placed.groupId } : {}),
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
ops: [
|
|
57
|
+
{ op: 'removeMeasurement', id: placedId },
|
|
58
|
+
{ op: 'addBackgroundImage', image: layer },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
// Patch that replaces the linked background layer `layerId` with an object
|
|
63
|
+
// tile at the layer's visible center — the inverse switch. The tile's fields
|
|
64
|
+
// come back off the layer (see the fileId trio on AnnotationBackgroundLayer),
|
|
65
|
+
// at the default tile scale; the crop window, if any, is simply dropped (a
|
|
66
|
+
// tile is a thumbnail, not a window). Null for a layer that isn't linked.
|
|
67
|
+
export const collapseBackgroundToFileTile = (doc, layerId, tileId, now = Date.now()) => {
|
|
68
|
+
const layer = backgroundLayersOf(doc.viewport).find((l) => l.id === layerId);
|
|
69
|
+
if (!layer?.fileId)
|
|
70
|
+
return null;
|
|
71
|
+
const r = backgroundLayerDocRect(layer);
|
|
72
|
+
const tile = {
|
|
73
|
+
id: tileId,
|
|
74
|
+
layerId: doc.layers[0]?.id ?? DEFAULT_LAYER_ID,
|
|
75
|
+
fileId: layer.fileId,
|
|
76
|
+
...(layer.filePath ? { filePath: layer.filePath } : {}),
|
|
77
|
+
...(layer.fileGroupId ? { groupId: layer.fileGroupId } : {}),
|
|
78
|
+
anchor: { x: r.x + r.width / 2, y: r.y + r.height / 2 },
|
|
79
|
+
createdAt: now,
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
ops: [
|
|
83
|
+
{ op: 'removeBackgroundImage', id: layerId },
|
|
84
|
+
{ op: 'addMeasurement', measurement: tile },
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
};
|
|
@@ -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
|
+
};
|