@reekon-tools/boldr-utils 1.10.3 → 1.12.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.native.js +48 -7
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +2 -2
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +16 -3
- package/dist/annotation/canvas/Tool.d.ts +8 -1
- package/dist/annotation/canvas/backgroundLayers.d.ts +20 -0
- package/dist/annotation/canvas/backgroundLayers.js +98 -0
- package/dist/annotation/canvas/elements/BackgroundImageElement.d.ts +10 -7
- package/dist/annotation/canvas/elements/BackgroundImageElement.js +45 -11
- package/dist/annotation/canvas/elements/BackgroundSvg.d.ts +3 -4
- package/dist/annotation/canvas/elements/BackgroundSvg.js +23 -29
- package/dist/annotation/canvas/elements/backgroundUrl.js +2 -2
- package/dist/annotation/canvas/tools/selectTool.js +183 -19
- package/dist/annotation/canvas/useAnnotationCanvasState.js +29 -20
- package/dist/annotation/canvas/viewport.js +7 -6
- package/dist/annotation/data/AnnotationDataProvider.d.ts +1 -1
- package/dist/annotation/data/InMemoryAnnotationProvider.d.ts +1 -1
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.d.ts +7 -1
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +124 -22
- package/dist/annotation/data/hooks/useAnnotationMutations.d.ts +1 -1
- package/dist/calculator/chips.d.ts +32 -0
- package/dist/calculator/chips.js +79 -0
- package/dist/calculator/display.d.ts +78 -0
- package/dist/calculator/display.js +223 -0
- package/dist/calculator/index.d.ts +3 -0
- package/dist/calculator/index.js +8 -3
- package/dist/calculator/runtime.d.ts +61 -0
- package/dist/calculator/runtime.js +430 -0
- package/dist/exports.d.ts +1 -0
- package/dist/exports.js +1 -0
- package/dist/theme/index.d.ts +3 -0
- package/dist/theme/index.js +3 -0
- package/dist/theme/preset.d.ts +120 -0
- package/dist/theme/preset.js +25 -0
- package/dist/theme/semantic.d.ts +136 -0
- package/dist/theme/semantic.js +116 -0
- package/dist/types/annotation.d.ts +27 -0
- package/dist/types/annotation.js +68 -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
|
@@ -7,7 +7,8 @@ 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 {
|
|
10
|
+
import { backgroundLayersOf } from './backgroundLayers.js';
|
|
11
|
+
import { BackgroundSkImageLoader } from './elements/BackgroundImageElement.js';
|
|
11
12
|
import { buildRemoveMeasurementOps, } from './measurementGeometry.js';
|
|
12
13
|
import { buildShapeFromDrag } from './tools/shapeTool.js';
|
|
13
14
|
import { SELECTION_PAD } from './textGeometry.js';
|
|
@@ -73,10 +74,39 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
73
74
|
tileScalePlatform: 'mobile',
|
|
74
75
|
animateViewport,
|
|
75
76
|
});
|
|
76
|
-
// Decode
|
|
77
|
-
// canvas and the magnifier loupe. If each canvas loaded its own, the
|
|
78
|
-
// would flash its blank base every gesture while it re-resolved +
|
|
79
|
-
|
|
77
|
+
// Decode each background layer ONCE here and share the images with both the
|
|
78
|
+
// main canvas and the magnifier loupe. If each canvas loaded its own, the
|
|
79
|
+
// loupe would flash its blank base every gesture while it re-resolved +
|
|
80
|
+
// re-decoded. The stack is variable-length, so the decodes can't be a fixed
|
|
81
|
+
// hook call — a null-rendering BackgroundSkImageLoader per layer owns each
|
|
82
|
+
// one and reports it into `loadedBackgroundImages` (see the loaders in the
|
|
83
|
+
// render tree below).
|
|
84
|
+
const backgroundLayers = useMemo(() => backgroundLayersOf(state.effectiveCanvas.viewport), [state.effectiveCanvas.viewport]);
|
|
85
|
+
const [loadedBackgroundImages, setLoadedBackgroundImages] = useState({});
|
|
86
|
+
const onBackgroundSkImage = useCallback((id, image) => {
|
|
87
|
+
setLoadedBackgroundImages((prev) => prev[id] === image ? prev : { ...prev, [id]: image });
|
|
88
|
+
}, []);
|
|
89
|
+
// Removed layers must not pin their decoded bitmaps in the record forever.
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
setLoadedBackgroundImages((prev) => {
|
|
92
|
+
const stale = Object.keys(prev).filter((id) => !backgroundLayers.some((l) => l.id === id));
|
|
93
|
+
if (stale.length === 0)
|
|
94
|
+
return prev;
|
|
95
|
+
const next = { ...prev };
|
|
96
|
+
for (const id of stale)
|
|
97
|
+
delete next[id];
|
|
98
|
+
return next;
|
|
99
|
+
});
|
|
100
|
+
}, [backgroundLayers]);
|
|
101
|
+
// Every current layer id is present (null until its decode lands), so the
|
|
102
|
+
// canvas elements never self-load — the loupe reuses the same decode.
|
|
103
|
+
const backgroundSkImages = useMemo(() => {
|
|
104
|
+
const out = {};
|
|
105
|
+
for (const layer of backgroundLayers) {
|
|
106
|
+
out[layer.id] = loadedBackgroundImages[layer.id] ?? null;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}, [backgroundLayers, loadedBackgroundImages]);
|
|
80
110
|
// Latest state behind a ref so the gesture object can be built once and
|
|
81
111
|
// never rebuilt mid-gesture — its JS callbacks read `stateRef.current`.
|
|
82
112
|
const stateRef = useRef(state);
|
|
@@ -1200,6 +1230,17 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1200
1230
|
}
|
|
1201
1231
|
const hit = cfg.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor);
|
|
1202
1232
|
if (!hit) {
|
|
1233
|
+
// Backgrounds can cover the whole document, so they only get the
|
|
1234
|
+
// drag once every annotation element misses, and only when ALREADY
|
|
1235
|
+
// selected (tap-to-select flows through the tool's pointer
|
|
1236
|
+
// handlers). The hit rides the exact same dragX/dragY transform and
|
|
1237
|
+
// buildTranslatePatch commit as any other element.
|
|
1238
|
+
const bg = cfg.hitTestSelectedBackground?.(st.ctx.document, world, st.ctx.selection?.ids ?? []);
|
|
1239
|
+
if (bg) {
|
|
1240
|
+
dragTargetRef.current = bg;
|
|
1241
|
+
setDraggingId(bg.id);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1203
1244
|
st.ctx.setSelection(null);
|
|
1204
1245
|
dragTargetRef.current = null;
|
|
1205
1246
|
return;
|
|
@@ -1435,7 +1476,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1435
1476
|
effectiveCanvas: state.effectiveCanvas,
|
|
1436
1477
|
worldTransform,
|
|
1437
1478
|
resolveImageUrl,
|
|
1438
|
-
|
|
1479
|
+
backgroundSkImages,
|
|
1439
1480
|
valueFont,
|
|
1440
1481
|
// Freehand drawing runs on the UI thread via `livePreview`, so the
|
|
1441
1482
|
// JS-state pen preview is unused on native.
|
|
@@ -1512,7 +1553,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1512
1553
|
// drawing to the canvas box by construction, but the stamp overlay is
|
|
1513
1554
|
// plain RN views — without clipping, tiles panned past the edge of a
|
|
1514
1555
|
// non-fullscreen canvas (e.g. a diagram strip) escape into surrounding UI.
|
|
1515
|
-
_jsxs(GestureHandlerRootView, { style: [{ width, height, overflow: 'hidden' }, style], children: [_jsx(GestureDetector, { gesture: gesture, children: _jsx(View, { style: { width, height }, collapsable: false, children: AnnotationCanvasSkia(skiaProps) }) }), renderMeasurementStamp && (_jsx(View, { pointerEvents: "box-none", style: StyleSheet.absoluteFill, children: state.effectiveCanvas.placedMeasurements.map((placed) => (_jsx(MeasurementStampOverlayItem, { placed: placed, measurement: placed.measurementId
|
|
1556
|
+
_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
|
|
1516
1557
|
? (state.measurementsById.get(placed.measurementId) ?? null)
|
|
1517
1558
|
: (resolveStampMeasurement?.(placed) ?? null), selected: selection?.ids.includes(placed.id) ?? false, dragging: draggingId === placed.id, sliding: slidingId === placed.id, endpointDragging: epDragId === placed.id, rectResizing: rectDragId === placed.id, zoomSnapshot: state.viewport.zoom, zoom: zoom, panX: panX, panY: panY, dragX: dragX, dragY: dragY, slideCtx: slideCtx, epCtx: epCtx, rectCtx: rectCtx, renderMeasurementStamp: renderMeasurementStamp, tileScaleFactor: state.tileScaleFactor, tileViewportScale: state.tileViewportScale, onStampPress: onMeasurementStampPress, onStampLongPress: onMeasurementStampLongPress, onRemove: onMeasurementStampRemove
|
|
1518
1559
|
? () => {
|
|
@@ -23,7 +23,7 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
23
23
|
};
|
|
24
24
|
viewport?: ViewportState;
|
|
25
25
|
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
26
|
-
|
|
26
|
+
backgroundSkImages?: Record<string, SkImage | null>;
|
|
27
27
|
valueFont: SkFont | null;
|
|
28
28
|
textFontMgr?: SkTypefaceFontProvider | null;
|
|
29
29
|
penDrawingStroke: AnnotationStroke | null;
|
|
@@ -92,5 +92,5 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
92
92
|
wrapContent?: (content: ReactNode) => ReactNode;
|
|
93
93
|
canvasRef?: RefObject<CanvasRef | null>;
|
|
94
94
|
}
|
|
95
|
-
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl,
|
|
95
|
+
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, customPreview, wrapContent, canvasRef, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
96
96
|
export {};
|
|
@@ -4,7 +4,8 @@ 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 {
|
|
7
|
+
import { backgroundLayerDocRect, backgroundLayersOf, } from './backgroundLayers.js';
|
|
8
|
+
import { BackgroundLayerElement } from './elements/BackgroundImageElement.js';
|
|
8
9
|
import { ShapeElement } from './elements/ShapeElement.js';
|
|
9
10
|
import { StrokeElement } from './elements/StrokeElement.js';
|
|
10
11
|
// Default visual constants for the measurement-annotation line (the tile itself
|
|
@@ -78,8 +79,8 @@ const SelectionBox = ({ bounds, isDragging, transform, }) => (_jsx(DraggableElem
|
|
|
78
79
|
// since the function-call pattern works identically on native we use it
|
|
79
80
|
// in both Inners for consistency. Don't add hooks here; this is a plain
|
|
80
81
|
// JSX-returning helper, not a component.
|
|
81
|
-
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl,
|
|
82
|
-
const world = (_jsxs(Group, { transform: worldTransform, children: [effectiveCanvas.viewport.
|
|
82
|
+
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, customPreview, wrapContent, canvasRef, }) => {
|
|
83
|
+
const world = (_jsxs(Group, { transform: worldTransform, children: [backgroundLayersOf(effectiveCanvas.viewport).map((layer) => (_jsx(DraggableElement, { isDragging: layer.id === draggingId || layer.id === resizingId, transform: layer.id === resizingId ? resizeTransform : dragTransform, children: _jsx(BackgroundLayerElement, { layer: layer, resolveUrl: resolveImageUrl, skImage: backgroundSkImages?.[layer.id], viewport: viewport, screenWidth: width, screenHeight: height }) }, layer.id))), effectiveCanvas.strokes.map((stroke) => (_jsx(DraggableElement, { isDragging: stroke.id === draggingId, transform: dragTransform, children: _jsx(StrokeElement, { stroke: stroke }) }, stroke.id))), effectiveCanvas.shapes.map((shape) => {
|
|
83
84
|
// Line/arrow shapes support endpoint editing. When selected they show
|
|
84
85
|
// grab handles at both ends; during an endpoint drag the line renders
|
|
85
86
|
// from the live endpoints (one following the finger) — the shape twin
|
|
@@ -218,6 +219,18 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
|
|
|
218
219
|
shape.kind === 'polygon';
|
|
219
220
|
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 })] }))] }));
|
|
220
221
|
}
|
|
222
|
+
// A selected background layer: outline its doc rect and offer ONE
|
|
223
|
+
// bottom-right handle — backgrounds scale uniformly about their
|
|
224
|
+
// top-left corner (the transform origin), so a single opposite-corner
|
|
225
|
+
// grab is the whole gizmo. Chrome tracks the live drag/resize
|
|
226
|
+
// transform the same way the layer itself does above.
|
|
227
|
+
const layer = backgroundLayersOf(effectiveCanvas.viewport).find((l) => l.id === selectedId);
|
|
228
|
+
if (layer) {
|
|
229
|
+
const r = backgroundLayerDocRect(layer);
|
|
230
|
+
const isResizing = layer.id === resizingId;
|
|
231
|
+
const liveTransform = isResizing ? resizeTransform : dragTransform;
|
|
232
|
+
return (_jsxs(DraggableElement, { isDragging: isDragging || isResizing, transform: liveTransform, children: [_jsx(Rect, { x: r.x, y: r.y, width: r.width, height: r.height, color: SELECTION_COLOR, style: "stroke", strokeWidth: SELECTION_STROKE }), handleRadius != null && (_jsx(Handle, { c: { x: r.x + r.width, y: r.y + r.height }, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR }))] }));
|
|
233
|
+
}
|
|
221
234
|
return null;
|
|
222
235
|
})(), 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] }));
|
|
223
236
|
return (_jsx(Canvas, { ref: canvasRef, style: { width, height }, children: wrapContent ? wrapContent(world) : world }));
|
|
@@ -52,7 +52,7 @@ export interface ShapeDrawConfig {
|
|
|
52
52
|
startCap?: StrokeCap;
|
|
53
53
|
dash?: boolean;
|
|
54
54
|
}
|
|
55
|
-
export type DragElementKind = 'stroke' | 'shape' | 'measurement';
|
|
55
|
+
export type DragElementKind = 'stroke' | 'shape' | 'measurement' | 'background';
|
|
56
56
|
export interface DragSelectionConfig {
|
|
57
57
|
hitTest(doc: AnnotationCanvasState, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number): {
|
|
58
58
|
id: AnnotationElementId;
|
|
@@ -78,6 +78,13 @@ export interface DragSelectionConfig {
|
|
|
78
78
|
} | null;
|
|
79
79
|
buildShapeCornerPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, corner: RectCorner, delta: Vec2): AnnotationDocumentPatch | null;
|
|
80
80
|
isSelectedTileGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number): boolean;
|
|
81
|
+
hitTestBackground?(doc: AnnotationCanvasState, world: Vec2): {
|
|
82
|
+
id: AnnotationElementId;
|
|
83
|
+
} | null;
|
|
84
|
+
hitTestSelectedBackground?(doc: AnnotationCanvasState, world: Vec2, selectedIds: AnnotationElementId[]): {
|
|
85
|
+
id: AnnotationElementId;
|
|
86
|
+
kind: 'background';
|
|
87
|
+
} | null;
|
|
81
88
|
hitTestResizeHandle?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): ResizeGeometry | null;
|
|
82
89
|
buildResizePatch?(doc: AnnotationCanvasState, id: AnnotationElementId, delta: Vec2): AnnotationDocumentPatch | null;
|
|
83
90
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AnnotationBackgroundLayer, AnnotationViewport, BackgroundLayerTransform } from '../../types/annotation.js';
|
|
2
|
+
export declare const LEGACY_BACKGROUND_LAYER_ID = "background-legacy";
|
|
3
|
+
export declare const BACKGROUND_PLACEMENT_GAP_RATIO = 0.02;
|
|
4
|
+
export declare const backgroundLayersOf: (viewport: AnnotationViewport) => AnnotationBackgroundLayer[];
|
|
5
|
+
export declare const backgroundLayerDocRect: (layer: AnnotationBackgroundLayer) => {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
export declare const backgroundLayersUnionRect: (layers: AnnotationBackgroundLayer[]) => {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
} | null;
|
|
17
|
+
export declare const placeNewBackgroundLayer: (existing: AnnotationBackgroundLayer[], dims: {
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
}, viewport: AnnotationViewport) => BackgroundLayerTransform;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { imageDocRect } from './viewport.js';
|
|
2
|
+
// Id of the layer synthesized from the legacy single
|
|
3
|
+
// `viewport.backgroundImage` slot. Deterministic (never random): two clients
|
|
4
|
+
// that materialize the array independently must converge on the SAME layer,
|
|
5
|
+
// since id equality is what lets their update/remove ops target it.
|
|
6
|
+
export const LEGACY_BACKGROUND_LAYER_ID = 'background-legacy';
|
|
7
|
+
// Horizontal gap between the existing background union and a newly placed
|
|
8
|
+
// image, as a fraction of the union's width (see placeNewBackgroundLayer).
|
|
9
|
+
export const BACKGROUND_PLACEMENT_GAP_RATIO = 0.02;
|
|
10
|
+
// Normalized view of a viewport's background stack. When only the legacy
|
|
11
|
+
// single field exists, it is presented as one layer whose transform
|
|
12
|
+
// reproduces the legacy fit placement exactly (via imageDocRect) under the
|
|
13
|
+
// deterministic legacy id. The axes scale independently so a legacy
|
|
14
|
+
// 'stretch' fit converts losslessly.
|
|
15
|
+
export const backgroundLayersOf = (viewport) => {
|
|
16
|
+
if (viewport.backgroundImages)
|
|
17
|
+
return viewport.backgroundImages;
|
|
18
|
+
const legacy = viewport.backgroundImage;
|
|
19
|
+
if (!legacy)
|
|
20
|
+
return [];
|
|
21
|
+
const dims = imageDocRect(legacy.widthPx, legacy.heightPx, viewport.width, viewport.height, viewport.backgroundFit ?? 'contain');
|
|
22
|
+
// Zero/unknown intrinsic dimensions: imageDocRect's guard makes such an
|
|
23
|
+
// image fill the whole doc rect, and the synthesized layer must occupy the
|
|
24
|
+
// same rect — both for rendering and for the fit/tile math that reads
|
|
25
|
+
// backgroundLayerDocRect. Adopting the doc rect as the intrinsic size (at
|
|
26
|
+
// scale 1) reproduces that placement with a finite, JSON-safe transform;
|
|
27
|
+
// a 0-intrinsic layer at any scale would collapse to a 0x0 rect instead.
|
|
28
|
+
const degenerate = !(legacy.widthPx > 0) || !(legacy.heightPx > 0);
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
id: LEGACY_BACKGROUND_LAYER_ID,
|
|
32
|
+
storagePath: legacy.storagePath,
|
|
33
|
+
downloadUrl: legacy.downloadUrl,
|
|
34
|
+
widthPx: degenerate ? dims.width : legacy.widthPx,
|
|
35
|
+
heightPx: degenerate ? dims.height : legacy.heightPx,
|
|
36
|
+
// Only include `format` when defined — the layer must survive a
|
|
37
|
+
// JSON round-trip without growing/losing keys (Firestore writes strip
|
|
38
|
+
// undefined values), so absence stays absence.
|
|
39
|
+
...(legacy.format ? { format: legacy.format } : {}),
|
|
40
|
+
transform: {
|
|
41
|
+
x: dims.x,
|
|
42
|
+
y: dims.y,
|
|
43
|
+
scaleX: degenerate ? 1 : dims.width / legacy.widthPx,
|
|
44
|
+
scaleY: degenerate ? 1 : dims.height / legacy.heightPx,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
};
|
|
49
|
+
// The doc-space rectangle a layer occupies: the intrinsic pixel grid under
|
|
50
|
+
// the layer's translate+scale.
|
|
51
|
+
export const backgroundLayerDocRect = (layer) => ({
|
|
52
|
+
x: layer.transform.x,
|
|
53
|
+
y: layer.transform.y,
|
|
54
|
+
width: layer.widthPx * layer.transform.scaleX,
|
|
55
|
+
height: layer.heightPx * layer.transform.scaleY,
|
|
56
|
+
});
|
|
57
|
+
// Bounding box of every layer's doc rect. Null when there are no layers —
|
|
58
|
+
// callers fall back to the document rect.
|
|
59
|
+
export const backgroundLayersUnionRect = (layers) => {
|
|
60
|
+
if (layers.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
let minX = Infinity;
|
|
63
|
+
let minY = Infinity;
|
|
64
|
+
let maxX = -Infinity;
|
|
65
|
+
let maxY = -Infinity;
|
|
66
|
+
for (const layer of layers) {
|
|
67
|
+
const rect = backgroundLayerDocRect(layer);
|
|
68
|
+
minX = Math.min(minX, rect.x);
|
|
69
|
+
minY = Math.min(minY, rect.y);
|
|
70
|
+
maxX = Math.max(maxX, rect.x + rect.width);
|
|
71
|
+
maxY = Math.max(maxY, rect.y + rect.height);
|
|
72
|
+
}
|
|
73
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
74
|
+
};
|
|
75
|
+
// Placement for a newly added image. The first image contain-fits into the
|
|
76
|
+
// doc rect — the same place the legacy single image lands, so a one-image doc
|
|
77
|
+
// is indistinguishable from a legacy one. Later images sit beside the
|
|
78
|
+
// existing content: scaled uniformly so their doc height matches the union
|
|
79
|
+
// rect's height, offset right of the union by a width-relative gap.
|
|
80
|
+
export const placeNewBackgroundLayer = (existing, dims, viewport) => {
|
|
81
|
+
const union = backgroundLayersUnionRect(existing);
|
|
82
|
+
if (!union) {
|
|
83
|
+
const rect = imageDocRect(dims.width, dims.height, viewport.width, viewport.height, 'contain');
|
|
84
|
+
return {
|
|
85
|
+
x: rect.x,
|
|
86
|
+
y: rect.y,
|
|
87
|
+
scaleX: dims.width > 0 ? rect.width / dims.width : 1,
|
|
88
|
+
scaleY: dims.height > 0 ? rect.height / dims.height : 1,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const scale = dims.height > 0 ? union.height / dims.height : 1;
|
|
92
|
+
return {
|
|
93
|
+
x: union.x + union.width + BACKGROUND_PLACEMENT_GAP_RATIO * union.width,
|
|
94
|
+
y: union.y,
|
|
95
|
+
scaleX: scale,
|
|
96
|
+
scaleY: scale,
|
|
97
|
+
};
|
|
98
|
+
};
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { type SkImage } from '@shopify/react-native-skia';
|
|
2
|
-
import type { AnnotationBackgroundImage,
|
|
2
|
+
import type { AnnotationBackgroundImage, AnnotationBackgroundLayer } from '../../../types/annotation.js';
|
|
3
3
|
import type { ViewportState } from '../viewport.js';
|
|
4
4
|
export declare const useBackgroundSkImage: (image: AnnotationBackgroundImage | null | undefined, resolveUrl?: (path: string) => Promise<string>) => SkImage | null;
|
|
5
|
-
export interface
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
export interface BackgroundSkImageLoaderProps {
|
|
6
|
+
layer: AnnotationBackgroundLayer;
|
|
7
|
+
resolveUrl?: (path: string) => Promise<string>;
|
|
8
|
+
onImage: (id: string, image: SkImage | null) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare const BackgroundSkImageLoader: ({ layer, resolveUrl, onImage, }: BackgroundSkImageLoaderProps) => null;
|
|
11
|
+
export interface BackgroundLayerElementProps {
|
|
12
|
+
layer: AnnotationBackgroundLayer;
|
|
10
13
|
resolveUrl?: (path: string) => Promise<string>;
|
|
11
14
|
skImage?: SkImage | null;
|
|
12
15
|
viewport?: ViewportState;
|
|
13
16
|
screenWidth?: number;
|
|
14
17
|
screenHeight?: number;
|
|
15
18
|
}
|
|
16
|
-
export declare const
|
|
19
|
+
export declare const BackgroundLayerElement: import("react").MemoExoticComponent<({ layer, resolveUrl, skImage: skImageProp, viewport, screenWidth, screenHeight, }: BackgroundLayerElementProps) => import("react/jsx-runtime").JSX.Element | null>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Image, useImage } from '@shopify/react-native-skia';
|
|
3
|
-
import { memo } from 'react';
|
|
4
|
-
import {
|
|
2
|
+
import { Group, Image, useImage, } from '@shopify/react-native-skia';
|
|
3
|
+
import { memo, useEffect, useRef } from 'react';
|
|
4
|
+
import { backgroundLayerDocRect } from '../backgroundLayers.js';
|
|
5
5
|
import { useBackgroundUrl } from './backgroundUrl.js';
|
|
6
6
|
import { BackgroundSvgElement } from './BackgroundSvg.js';
|
|
7
7
|
// Resolve a background image's URL and decode it to an SkImage. Exported so a
|
|
@@ -15,21 +15,55 @@ export const useBackgroundSkImage = (image, resolveUrl) => {
|
|
|
15
15
|
const url = useBackgroundUrl(image, resolveUrl);
|
|
16
16
|
return useImage(url || null);
|
|
17
17
|
};
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// Null-rendering decode host: one mounted instance per background layer, so a
|
|
19
|
+
// variable-length layer stack gets per-image hooks without any component
|
|
20
|
+
// mapping over hooks. The consumer (the native Inner) mounts one per layer,
|
|
21
|
+
// accumulates the reported images into a Record keyed by layer id, and feeds
|
|
22
|
+
// that to every canvas sharing the decode (main + magnifier loupe).
|
|
23
|
+
export const BackgroundSkImageLoader = ({ layer, resolveUrl, onImage, }) => {
|
|
24
|
+
// SVG layers never draw through an SkImage (BackgroundSvgElement owns its
|
|
25
|
+
// own vector pipeline), so skip the decode and report null.
|
|
26
|
+
const image = useBackgroundSkImage(layer.format === 'svg' ? null : layer, resolveUrl);
|
|
27
|
+
// The callback rides a ref so an inline consumer closure doesn't re-report
|
|
28
|
+
// (and re-render the collector) every render.
|
|
29
|
+
const onImageRef = useRef(onImage);
|
|
30
|
+
onImageRef.current = onImage;
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
onImageRef.current(layer.id, image);
|
|
33
|
+
}, [layer.id, image]);
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
// One background layer: the image's intrinsic pixel grid drawn under the
|
|
37
|
+
// layer's translate+scale, so its doc rect is backgroundLayerDocRect(layer).
|
|
38
|
+
// A component (not inlined into the scene helper) because each layer owns
|
|
39
|
+
// per-image hooks — URL resolution and raster decode — and AnnotationCanvasSkia
|
|
40
|
+
// is a hook-free plain function.
|
|
41
|
+
export const BackgroundLayerElement = memo(({ layer, resolveUrl, skImage: skImageProp, viewport, screenWidth, screenHeight, }) => {
|
|
42
|
+
const isSvg = layer.format === 'svg';
|
|
20
43
|
// Self-load only when no pre-decoded image is provided; the hook still runs
|
|
21
44
|
// unconditionally (with a null source, so it no-ops) to satisfy hook rules.
|
|
22
45
|
const provided = skImageProp !== undefined;
|
|
23
|
-
const loaded = useBackgroundSkImage(provided || isSvg ? null :
|
|
46
|
+
const loaded = useBackgroundSkImage(provided || isSvg ? null : layer, resolveUrl);
|
|
24
47
|
const skImage = provided ? skImageProp : loaded;
|
|
48
|
+
const t = layer.transform;
|
|
49
|
+
let content = null;
|
|
25
50
|
if (isSvg) {
|
|
26
51
|
// Environment-forked renderer (one shared module — see BackgroundSvg):
|
|
27
52
|
// native draws true vectors through Skia's SVG module; web rasterizes
|
|
28
|
-
// through the browser at viewport resolution.
|
|
29
|
-
|
|
53
|
+
// through the browser at viewport resolution. Both draw at intrinsic
|
|
54
|
+
// size — the Group below owns the placement — but the web rasterizer
|
|
55
|
+
// still needs the doc rect to crop against the visible region.
|
|
56
|
+
content = (_jsx(BackgroundSvgElement, { image: layer, dims: backgroundLayerDocRect(layer), resolveUrl: resolveUrl, viewport: viewport, screenWidth: screenWidth, screenHeight: screenHeight }));
|
|
57
|
+
}
|
|
58
|
+
else if (skImage) {
|
|
59
|
+
content = (_jsx(Image, { image: skImage, x: 0, y: 0, width: layer.widthPx, height: layer.heightPx, fit: "fill" }));
|
|
30
60
|
}
|
|
31
|
-
if (!
|
|
61
|
+
if (!content)
|
|
32
62
|
return null;
|
|
33
|
-
|
|
34
|
-
|
|
63
|
+
return (_jsx(Group, { transform: [
|
|
64
|
+
{ translateX: t.x },
|
|
65
|
+
{ translateY: t.y },
|
|
66
|
+
{ scaleX: t.scaleX },
|
|
67
|
+
{ scaleY: t.scaleY },
|
|
68
|
+
], children: content }));
|
|
35
69
|
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import type { AnnotationBackgroundImage
|
|
1
|
+
import type { AnnotationBackgroundImage } from '../../../types/annotation.js';
|
|
2
2
|
import { type ViewportState } from '../viewport.js';
|
|
3
|
+
import { type RectPx } from '../svgRasterPlan.js';
|
|
3
4
|
export interface BackgroundSvgProps {
|
|
4
5
|
image: AnnotationBackgroundImage;
|
|
5
|
-
|
|
6
|
-
docHeight: number;
|
|
7
|
-
fit?: BackgroundFit;
|
|
6
|
+
dims: RectPx;
|
|
8
7
|
resolveUrl?: (path: string) => Promise<string>;
|
|
9
8
|
viewport?: ViewportState;
|
|
10
9
|
screenWidth?: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { Image as SkiaImage, ImageSVG, Skia, } from '@shopify/react-native-skia';
|
|
3
3
|
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { DEFAULT_VIEWPORT
|
|
4
|
+
import { DEFAULT_VIEWPORT } from '../viewport.js';
|
|
5
5
|
import { ensureRootFontSize, planSvgRaster, shouldRegenerateRaster, svgIntrinsicSize, wrapSvgForRaster, } from '../svgRasterPlan.js';
|
|
6
6
|
import { useBackgroundUrl } from './backgroundUrl.js';
|
|
7
7
|
// SVG background renderer. ONE shared module for both platforms — this file
|
|
@@ -91,22 +91,17 @@ const useBackgroundSkSvg = (url) => {
|
|
|
91
91
|
}, [url]);
|
|
92
92
|
return svg;
|
|
93
93
|
};
|
|
94
|
-
const SkSvgBackground = ({ image,
|
|
94
|
+
const SkSvgBackground = ({ image, resolveUrl }) => {
|
|
95
95
|
const url = useBackgroundUrl(image, resolveUrl);
|
|
96
96
|
const svg = useBackgroundSkSvg(url);
|
|
97
|
-
const dims = imageDocRect(image.widthPx, image.heightPx, docWidth, docHeight, fit);
|
|
98
97
|
if (!svg)
|
|
99
98
|
return null;
|
|
100
|
-
// Draw the SVG at its stamped intrinsic size
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
// transform, so it stays crisp at any
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{ translateY: dims.y },
|
|
107
|
-
{ scaleX: dims.width / image.widthPx },
|
|
108
|
-
{ scaleY: dims.height / image.heightPx },
|
|
109
|
-
], children: _jsx(ImageSVG, { svg: svg, x: 0, y: 0, width: image.widthPx, height: image.heightPx }) }));
|
|
99
|
+
// Draw the SVG at its stamped intrinsic size (rather than relying on
|
|
100
|
+
// ImageSVG's own fitting, which only applies to sources with a viewBox) —
|
|
101
|
+
// the caller's Group maps intrinsic px to the layer's doc rect. The vector
|
|
102
|
+
// source renders through the canvas transform, so it stays crisp at any
|
|
103
|
+
// canvas zoom.
|
|
104
|
+
return (_jsx(ImageSVG, { svg: svg, x: 0, y: 0, width: image.widthPx, height: image.heightPx }));
|
|
110
105
|
};
|
|
111
106
|
// --- Browser path (web) ------------------------------------------------------
|
|
112
107
|
const REGEN_DEBOUNCE_MS = 150;
|
|
@@ -149,7 +144,7 @@ const loadSvgImage = (xml) => new Promise((resolve, reject) => {
|
|
|
149
144
|
};
|
|
150
145
|
img.src = objectUrl;
|
|
151
146
|
});
|
|
152
|
-
const BrowserSvgBackground = ({ image,
|
|
147
|
+
const BrowserSvgBackground = ({ image, dims, resolveUrl, viewport, screenWidth, screenHeight, }) => {
|
|
153
148
|
const url = useBackgroundUrl(image, resolveUrl);
|
|
154
149
|
const [svgText, setSvgText] = useState(null);
|
|
155
150
|
useEffect(() => {
|
|
@@ -183,7 +178,6 @@ const BrowserSvgBackground = ({ image, docWidth, docHeight, fit = 'contain', res
|
|
|
183
178
|
// changed, unmount).
|
|
184
179
|
const genRef = useRef(0);
|
|
185
180
|
const timerRef = useRef(null);
|
|
186
|
-
const dims = imageDocRect(image.widthPx, image.heightPx, docWidth, docHeight, fit);
|
|
187
181
|
// Latest plan inputs live in a ref so the async raster loop always plans
|
|
188
182
|
// against current values without effect churn on every pan frame.
|
|
189
183
|
inputsRef.current = {
|
|
@@ -191,8 +185,8 @@ const BrowserSvgBackground = ({ image, docWidth, docHeight, fit = 'contain', res
|
|
|
191
185
|
heightPx: image.heightPx,
|
|
192
186
|
dims,
|
|
193
187
|
viewport: viewport ?? DEFAULT_VIEWPORT,
|
|
194
|
-
screenWidth: screenWidth ??
|
|
195
|
-
screenHeight: screenHeight ??
|
|
188
|
+
screenWidth: screenWidth ?? dims.width,
|
|
189
|
+
screenHeight: screenHeight ?? dims.height,
|
|
196
190
|
dpr: typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,
|
|
197
191
|
};
|
|
198
192
|
const run = useCallback(async () => {
|
|
@@ -287,8 +281,9 @@ const BrowserSvgBackground = ({ image, docWidth, docHeight, fit = 'contain', res
|
|
|
287
281
|
preparedRef.current = null;
|
|
288
282
|
};
|
|
289
283
|
}, [svgText, image.widthPx, image.heightPx, schedule]);
|
|
290
|
-
// Follow the viewport: debounce-regenerate when zoom/pan/layout
|
|
291
|
-
//
|
|
284
|
+
// Follow the viewport: debounce-regenerate when zoom/pan/layout — or the
|
|
285
|
+
// layer's own doc rect (a background drag/scale changes how much screen it
|
|
286
|
+
// covers) — outgrow the applied bitmap (run() no-ops when they haven't).
|
|
292
287
|
const vp = viewport ?? DEFAULT_VIEWPORT;
|
|
293
288
|
useEffect(() => {
|
|
294
289
|
if (!preparedRef.current)
|
|
@@ -300,9 +295,10 @@ const BrowserSvgBackground = ({ image, docWidth, docHeight, fit = 'contain', res
|
|
|
300
295
|
vp.pan.y,
|
|
301
296
|
screenWidth,
|
|
302
297
|
screenHeight,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
298
|
+
dims.x,
|
|
299
|
+
dims.y,
|
|
300
|
+
dims.width,
|
|
301
|
+
dims.height,
|
|
306
302
|
image.widthPx,
|
|
307
303
|
image.heightPx,
|
|
308
304
|
schedule,
|
|
@@ -325,12 +321,10 @@ const BrowserSvgBackground = ({ image, docWidth, docHeight, fit = 'contain', res
|
|
|
325
321
|
}, []);
|
|
326
322
|
if (!applied || !(image.widthPx > 0) || !(image.heightPx > 0))
|
|
327
323
|
return null;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
{ scaleY: dims.height / image.heightPx },
|
|
333
|
-
], children: _jsx(SkiaImage, { image: applied.image, x: applied.rect.x, y: applied.rect.y, width: applied.rect.width, height: applied.rect.height, fit: "fill" }) }));
|
|
324
|
+
// `applied.rect` is in svg-local px — the same space the caller's Group
|
|
325
|
+
// maps to the layer's doc rect — so the crop lands exactly where the
|
|
326
|
+
// vector content it rasterized would.
|
|
327
|
+
return (_jsx(SkiaImage, { image: applied.image, x: applied.rect.x, y: applied.rect.y, width: applied.rect.width, height: applied.rect.height, fit: "fill" }));
|
|
334
328
|
};
|
|
335
329
|
// HAS_DOM is fixed for the life of the process, so each mount renders one
|
|
336
330
|
// branch consistently (no hook-order hazard).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
// Resolve a background image's (possibly expired) storage URL. Shared by the
|
|
3
|
-
// raster loader (
|
|
4
|
-
// (BackgroundSvg).
|
|
3
|
+
// raster loader (BackgroundLayerElement / BackgroundSkImageLoader) and both
|
|
4
|
+
// SVG render paths (BackgroundSvg).
|
|
5
5
|
export const useBackgroundUrl = (image, resolveUrl) => {
|
|
6
6
|
const [url, setUrl] = useState(image?.downloadUrl ?? '');
|
|
7
7
|
// Hold resolveUrl in a ref so re-resolution is keyed only on the image
|