@reekon-tools/boldr-utils 1.9.0 → 1.9.2
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/calculator/validate.js +33 -8
- package/dist/canvas/AnnotationCanvas.d.ts +11 -0
- package/dist/canvas/AnnotationCanvas.js +10 -0
- package/dist/canvas/AnnotationCanvas.native.d.ts +8 -0
- package/dist/canvas/AnnotationCanvas.native.js +6 -0
- package/dist/canvas/AnnotationCanvasInner.d.ts +39 -0
- package/dist/canvas/AnnotationCanvasInner.js +219 -0
- package/dist/canvas/AnnotationCanvasInner.native.d.ts +35 -0
- package/dist/canvas/AnnotationCanvasInner.native.js +138 -0
- package/dist/canvas/AnnotationCanvasSkia.d.ts +27 -0
- package/dist/canvas/AnnotationCanvasSkia.js +20 -0
- package/dist/canvas/Tool.d.ts +38 -0
- package/dist/canvas/Tool.js +1 -0
- package/dist/canvas/elements/BackgroundImageElement.d.ts +9 -0
- package/dist/canvas/elements/BackgroundImageElement.js +37 -0
- package/dist/canvas/elements/MeasurementStampElement.d.ts +13 -0
- package/dist/canvas/elements/MeasurementStampElement.js +30 -0
- package/dist/canvas/elements/ShapeElement.d.ts +7 -0
- package/dist/canvas/elements/ShapeElement.js +62 -0
- package/dist/canvas/elements/StrokeElement.d.ts +7 -0
- package/dist/canvas/elements/StrokeElement.js +18 -0
- package/dist/canvas/measurementPicker.d.ts +10 -0
- package/dist/canvas/measurementPicker.js +1 -0
- package/dist/canvas/measurementStampOverlay.d.ts +11 -0
- package/dist/canvas/measurementStampOverlay.js +1 -0
- package/dist/canvas/pointerAdapter.d.ts +3 -0
- package/dist/canvas/pointerAdapter.js +19 -0
- package/dist/canvas/stampLayout.d.ts +5 -0
- package/dist/canvas/stampLayout.js +14 -0
- package/dist/canvas/tools/measurementStampTool.d.ts +9 -0
- package/dist/canvas/tools/measurementStampTool.js +37 -0
- package/dist/canvas/tools/panTool.d.ts +5 -0
- package/dist/canvas/tools/panTool.js +25 -0
- package/dist/canvas/tools/penTool.d.ts +13 -0
- package/dist/canvas/tools/penTool.js +68 -0
- package/dist/canvas/tools/selectTool.d.ts +2 -0
- package/dist/canvas/tools/selectTool.js +182 -0
- package/dist/canvas/useAnnotationCanvasState.d.ts +54 -0
- package/dist/canvas/useAnnotationCanvasState.js +210 -0
- package/dist/canvas/viewport.d.ts +16 -0
- package/dist/canvas/viewport.js +54 -0
- package/dist/data/AnnotationDataContext.d.ts +8 -0
- package/dist/data/AnnotationDataContext.js +11 -0
- package/dist/data/AnnotationDataProvider.d.ts +65 -0
- package/dist/data/AnnotationDataProvider.js +4 -0
- package/dist/data/InMemoryAnnotationProvider.d.ts +30 -0
- package/dist/data/InMemoryAnnotationProvider.js +197 -0
- package/dist/data/canvasPersistence.d.ts +3 -0
- package/dist/data/canvasPersistence.js +26 -0
- package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +33 -0
- package/dist/data/hooks/useAnnotationCanvasDoc.js +314 -0
- package/dist/data/hooks/useAnnotationDoc.d.ts +7 -0
- package/dist/data/hooks/useAnnotationDoc.js +33 -0
- package/dist/data/hooks/useAnnotationList.d.ts +7 -0
- package/dist/data/hooks/useAnnotationList.js +26 -0
- package/dist/data/hooks/useAnnotationMutations.d.ts +9 -0
- package/dist/data/hooks/useAnnotationMutations.js +11 -0
- package/dist/hooks/useParseMeasurement.d.ts +4 -0
- package/dist/hooks/useParseMeasurement.js +14 -0
- package/dist/types/firestore.d.ts +1 -0
- package/dist/utils/evaluateFormula.d.ts +20 -0
- package/dist/utils/evaluateFormula.js +31 -0
- package/package.json +1 -1
- package/dist/theme/index.d.ts +0 -3
- package/dist/theme/index.js +0 -3
- package/dist/theme/preset.d.ts +0 -120
- package/dist/theme/preset.js +0 -25
- package/dist/theme/semantic.d.ts +0 -136
- package/dist/theme/semantic.js +0 -116
|
@@ -321,6 +321,10 @@ export const validateCalculatorDefinition = (input) => {
|
|
|
321
321
|
push('variable-unit-dimension-mismatch', unitPath, `Unit "${unit}" is not a ${dim} unit (variable ${variable} maps to "${mapped.name}")`);
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
|
+
// The unit the EXPRESSION produces, which is what `resultUnit` claims to
|
|
325
|
+
// be. Hoisted out of the `target` branch because the number-target check
|
|
326
|
+
// further down needs it too.
|
|
327
|
+
const inferred = inferExpressionUnit(def.fields, eq);
|
|
324
328
|
if (target) {
|
|
325
329
|
const dim = fieldDimension(target);
|
|
326
330
|
if (eq.resultUnit != null) {
|
|
@@ -331,11 +335,16 @@ export const validateCalculatorDefinition = (input) => {
|
|
|
331
335
|
push('result-unit-dimension-mismatch', `${path}.resultUnit`, `Result unit "${eq.resultUnit}" is not a ${dim} unit (target "${target.name}")`);
|
|
332
336
|
}
|
|
333
337
|
}
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
|
|
338
|
+
// Dimension checks against the target field can't see a same-dimension
|
|
339
|
+
// scale error: three inch variables multiplied are in³, so `cu_ft`
|
|
340
|
+
// passes every check above while reporting 1728× the truth.
|
|
341
|
+
//
|
|
342
|
+
// Deliberately NOT applied to a dimensionless target. Literals carry no
|
|
343
|
+
// units here, so `area / 33` infers as an area whether the area arrives
|
|
344
|
+
// in µm² or ft² — the inference cannot tell a broken count from a
|
|
345
|
+
// correct one, and every legitimate count written as
|
|
346
|
+
// `area_in_ft² / 33_ft²` would be flagged. The canonical-leak check
|
|
347
|
+
// below is the one that separates them.
|
|
339
348
|
if (inferred.ok && dim !== 'none') {
|
|
340
349
|
if (inferred.dimension !== dim) {
|
|
341
350
|
push('expression-dimension-mismatch', `${path}.expression`, `Expression produces ${describeDimension(inferred.dimension)} but "${target.name}" is ${describeDimension(dim)} — check the variable units and the expression`);
|
|
@@ -365,14 +374,30 @@ export const validateCalculatorDefinition = (input) => {
|
|
|
365
374
|
}
|
|
366
375
|
}
|
|
367
376
|
// The canonical-leak trap: a number target consuming a unit-carrying
|
|
368
|
-
// field without a variable unit receives raw µm-scale values.
|
|
369
|
-
|
|
377
|
+
// field without a variable unit receives raw µm-scale values. Reported
|
|
378
|
+
// per variable, because the fix is per variable ("pick a unit for A").
|
|
379
|
+
//
|
|
380
|
+
// Gated on the inference: unit-carrying variables are fine as long as
|
|
381
|
+
// their dimensions CANCEL, which is the common shape for a count —
|
|
382
|
+
// `ceil(area / area)` sheets, `(12 * rise) / run` pitch. Flagging those
|
|
383
|
+
// put a permanent warning on two correct calculators, and a check that
|
|
384
|
+
// cries wolf on correct authoring is one authors learn to scroll past.
|
|
385
|
+
//
|
|
386
|
+
// An ERROR, not a warning. It was a warning so that pre-annotation docs
|
|
387
|
+
// kept validating, but "keeps validating" is how the roofing estimator
|
|
388
|
+
// shipped `Bundles = A / 33` over a canonical µm² area and showed
|
|
389
|
+
// 184,990,000,000 — the right answer times µm²-per-ft². There is no
|
|
390
|
+
// authoring where a µm-scale value in a unitless field is intended, and
|
|
391
|
+
// nothing gates on `ok`: mobile only console.warns in __DEV__, and the
|
|
392
|
+
// web editor shows the banner without blocking save.
|
|
393
|
+
if (target?.kind === ColumnType.Number &&
|
|
394
|
+
(!inferred.ok || inferred.dimension !== 'none')) {
|
|
370
395
|
for (const [variable, fieldId] of Object.entries(eq.variableToFieldId)) {
|
|
371
396
|
const mapped = fieldById.get(fieldId);
|
|
372
397
|
if (!mapped || fieldDimension(mapped) === 'none')
|
|
373
398
|
continue;
|
|
374
399
|
if (eq.variableUnits?.[variable] == null) {
|
|
375
|
-
push('canonical-value-into-number-target', `${path}.variableToFieldId.${variable}`, `"${target.name}" is a number field but variable ${variable} ("${mapped.name}") has no display unit
|
|
400
|
+
push('canonical-value-into-number-target', `${path}.variableToFieldId.${variable}`, `"${target.name}" is a number field, so it shows the raw result — but variable ${variable} ("${mapped.name}") has no display unit, so it arrives as a canonical µm-scale value and the result will be off by a large factor. Pick a unit for ${variable}.`);
|
|
376
401
|
}
|
|
377
402
|
}
|
|
378
403
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.js';
|
|
3
|
+
export type { AnnotationCanvasHandle, GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
|
|
4
|
+
export interface CanvasKitOpts {
|
|
5
|
+
locateFile?: (file: string) => string;
|
|
6
|
+
}
|
|
7
|
+
export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
|
|
8
|
+
fallback?: ReactNode;
|
|
9
|
+
canvasKitOpts?: CanvasKitOpts;
|
|
10
|
+
};
|
|
11
|
+
export declare const AnnotationCanvas: ({ fallback, canvasKitOpts, ...rest }: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { WithSkiaWeb } from '@shopify/react-native-skia/lib/module/web/index.js';
|
|
3
|
+
// Web-only entry point: lazy-loads CanvasKit wasm via WithSkiaWeb. Imperative
|
|
4
|
+
// API (undo/redo/zoom) is exposed through the `imperativeRef` prop rather
|
|
5
|
+
// than React refs, since WithSkiaWeb can't forward refs through its lazy
|
|
6
|
+
// component boundary. The native build sits in `AnnotationCanvas.native.tsx`
|
|
7
|
+
// and skips WithSkiaWeb entirely (no wasm to load).
|
|
8
|
+
export const AnnotationCanvas = ({ fallback, canvasKitOpts, ...rest }) => (_jsx(WithSkiaWeb, { getComponent: () => import('./AnnotationCanvasInner.js').then((m) => ({
|
|
9
|
+
default: m.AnnotationCanvasInner,
|
|
10
|
+
})), fallback: fallback ?? null, componentProps: rest, opts: canvasKitOpts }));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.native.js';
|
|
2
|
+
export type { AnnotationCanvasHandle, } from './useAnnotationCanvasState.js';
|
|
3
|
+
export type { GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
|
|
4
|
+
export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
|
|
5
|
+
fallback?: unknown;
|
|
6
|
+
canvasKitOpts?: unknown;
|
|
7
|
+
};
|
|
8
|
+
export declare const AnnotationCanvas: (props: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { AnnotationCanvasInner } from './AnnotationCanvasInner.native.js';
|
|
3
|
+
export const AnnotationCanvas = (props) => {
|
|
4
|
+
const { fallback: _f, canvasKitOpts: _o, ...rest } = props;
|
|
5
|
+
return _jsx(AnnotationCanvasInner, { ...rest });
|
|
6
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type CSSProperties, type MutableRefObject } from 'react';
|
|
2
|
+
import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
|
|
3
|
+
import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
|
|
4
|
+
import type { MeasurementRef } from './measurementPicker.js';
|
|
5
|
+
import type { Tool } from './Tool.js';
|
|
6
|
+
import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
|
|
7
|
+
import { type ViewportState } from './viewport.js';
|
|
8
|
+
import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
|
|
9
|
+
export type { AnnotationCanvasHandle };
|
|
10
|
+
export interface AnnotationCanvasInnerProps {
|
|
11
|
+
canvas: AnnotationCanvasState;
|
|
12
|
+
onCommit(patch: AnnotationDocumentPatch): void;
|
|
13
|
+
tools: Tool[];
|
|
14
|
+
activeToolId: string;
|
|
15
|
+
selection: Selection | null;
|
|
16
|
+
onSelectionChange(selection: Selection | null): void;
|
|
17
|
+
measurements?: Measurement[];
|
|
18
|
+
fallbackUnit?: Units;
|
|
19
|
+
fractionalTolerance?: FractionalTolerance;
|
|
20
|
+
decimalTolerance?: DecimalTolerance;
|
|
21
|
+
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
22
|
+
pickMeasurement?: () => Promise<MeasurementRef | null>;
|
|
23
|
+
renderMeasurementStamp?: RenderMeasurementStamp;
|
|
24
|
+
stampFontSource?: unknown;
|
|
25
|
+
stampValueFontSize?: number;
|
|
26
|
+
stampLabelFontSize?: number;
|
|
27
|
+
gestures?: GestureConfig;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
initialViewport?: ViewportState;
|
|
31
|
+
style?: CSSProperties;
|
|
32
|
+
imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
|
|
33
|
+
}
|
|
34
|
+
export type PanTrigger = 'middleMouse' | 'rightMouse' | 'space';
|
|
35
|
+
export interface GestureConfig {
|
|
36
|
+
wheel?: 'zoom' | 'pan' | 'auto';
|
|
37
|
+
panTriggers?: PanTrigger[];
|
|
38
|
+
}
|
|
39
|
+
export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFont } from '@shopify/react-native-skia';
|
|
3
|
+
import { useCallback, useEffect, useRef, } from 'react';
|
|
4
|
+
import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
|
|
5
|
+
import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
|
|
6
|
+
import { STAMP_TILE_SIZE } from './stampLayout.js';
|
|
7
|
+
const DEFAULT_PAN_TRIGGERS = ['middleMouse', 'space'];
|
|
8
|
+
export const AnnotationCanvasInner = (props) => {
|
|
9
|
+
const { fallbackUnit, fractionalTolerance, decimalTolerance, resolveImageUrl, stampFontSource, stampValueFontSize = 14, stampLabelFontSize = 11, gestures, width, height, style, activeToolId, tools, } = props;
|
|
10
|
+
const wheelMode = gestures?.wheel ?? 'auto';
|
|
11
|
+
const panTriggers = gestures?.panTriggers ?? DEFAULT_PAN_TRIGGERS;
|
|
12
|
+
const allowSpacePan = panTriggers.includes('space');
|
|
13
|
+
const allowMiddlePan = panTriggers.includes('middleMouse');
|
|
14
|
+
const allowRightPan = panTriggers.includes('rightMouse');
|
|
15
|
+
const valueFont = useFont(stampFontSource, stampValueFontSize);
|
|
16
|
+
const labelFont = useFont(stampFontSource, stampLabelFontSize);
|
|
17
|
+
const state = useAnnotationCanvasState(props);
|
|
18
|
+
const containerRef = useRef(null);
|
|
19
|
+
const panGestureRef = useRef(null);
|
|
20
|
+
const spaceDownRef = useRef(false);
|
|
21
|
+
const activeTool = tools.find((t) => t.id === activeToolId) ?? null;
|
|
22
|
+
const toCanvasPointer = useCallback((event) => {
|
|
23
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
24
|
+
const screen = {
|
|
25
|
+
x: event.clientX - (rect?.left ?? 0),
|
|
26
|
+
y: event.clientY - (rect?.top ?? 0),
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
pointerId: event.pointerId,
|
|
30
|
+
screen,
|
|
31
|
+
world: state.ctx.viewport.screenToWorld(screen),
|
|
32
|
+
pressure: event.pressure || undefined,
|
|
33
|
+
shiftKey: event.shiftKey,
|
|
34
|
+
altKey: event.altKey,
|
|
35
|
+
metaKey: event.metaKey,
|
|
36
|
+
ctrlKey: event.ctrlKey,
|
|
37
|
+
};
|
|
38
|
+
}, [state.ctx.viewport]);
|
|
39
|
+
const isPanTriggerDown = useCallback((event) => {
|
|
40
|
+
if (allowMiddlePan && event.button === 1)
|
|
41
|
+
return true;
|
|
42
|
+
if (allowRightPan && event.button === 2)
|
|
43
|
+
return true;
|
|
44
|
+
if (allowSpacePan && event.button === 0 && spaceDownRef.current)
|
|
45
|
+
return true;
|
|
46
|
+
return false;
|
|
47
|
+
}, [allowMiddlePan, allowRightPan, allowSpacePan]);
|
|
48
|
+
const handlePointerDown = useCallback((event) => {
|
|
49
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
50
|
+
const screen = {
|
|
51
|
+
x: event.clientX - (rect?.left ?? 0),
|
|
52
|
+
y: event.clientY - (rect?.top ?? 0),
|
|
53
|
+
};
|
|
54
|
+
if (isPanTriggerDown(event)) {
|
|
55
|
+
panGestureRef.current = { pointerId: event.pointerId, lastScreen: screen };
|
|
56
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
61
|
+
state.dispatchPointerDown(toCanvasPointer(event));
|
|
62
|
+
}, [state, toCanvasPointer, isPanTriggerDown]);
|
|
63
|
+
const handlePointerMove = useCallback((event) => {
|
|
64
|
+
const pan = panGestureRef.current;
|
|
65
|
+
if (pan && event.pointerId === pan.pointerId) {
|
|
66
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
67
|
+
const screen = {
|
|
68
|
+
x: event.clientX - (rect?.left ?? 0),
|
|
69
|
+
y: event.clientY - (rect?.top ?? 0),
|
|
70
|
+
};
|
|
71
|
+
state.pan({
|
|
72
|
+
x: screen.x - pan.lastScreen.x,
|
|
73
|
+
y: screen.y - pan.lastScreen.y,
|
|
74
|
+
});
|
|
75
|
+
panGestureRef.current = { pointerId: pan.pointerId, lastScreen: screen };
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
state.dispatchPointerMove(toCanvasPointer(event));
|
|
79
|
+
}, [state, toCanvasPointer]);
|
|
80
|
+
const handlePointerUp = useCallback((event) => {
|
|
81
|
+
const pan = panGestureRef.current;
|
|
82
|
+
if (pan && event.pointerId === pan.pointerId) {
|
|
83
|
+
panGestureRef.current = null;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
state.dispatchPointerUp(toCanvasPointer(event));
|
|
87
|
+
}, [state, toCanvasPointer]);
|
|
88
|
+
const handlePointerCancel = useCallback(() => {
|
|
89
|
+
panGestureRef.current = null;
|
|
90
|
+
state.dispatchPointerCancel();
|
|
91
|
+
}, [state]);
|
|
92
|
+
const handleWheel = useCallback((event) => {
|
|
93
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
94
|
+
const focal = {
|
|
95
|
+
x: event.clientX - (rect?.left ?? 0),
|
|
96
|
+
y: event.clientY - (rect?.top ?? 0),
|
|
97
|
+
};
|
|
98
|
+
if (event.ctrlKey || event.metaKey) {
|
|
99
|
+
const factor = Math.exp(-event.deltaY / 200);
|
|
100
|
+
state.zoom(focal, state.ctx.viewport.state.zoom * factor);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (wheelMode === 'zoom') {
|
|
104
|
+
const factor = Math.exp(-event.deltaY / 300);
|
|
105
|
+
state.zoom(focal, state.ctx.viewport.state.zoom * factor);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (event.shiftKey) {
|
|
109
|
+
state.pan({ x: event.deltaY, y: event.deltaX });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
state.pan({ x: event.deltaX, y: event.deltaY });
|
|
113
|
+
}, [state, wheelMode]);
|
|
114
|
+
const handleContextMenu = useCallback((event) => {
|
|
115
|
+
if (allowRightPan)
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
}, [allowRightPan]);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const el = containerRef.current;
|
|
120
|
+
if (!el)
|
|
121
|
+
return;
|
|
122
|
+
const stop = (e) => {
|
|
123
|
+
if (e.target === el || el.contains(e.target)) {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
el.addEventListener('wheel', stop, { passive: false });
|
|
128
|
+
return () => el.removeEventListener('wheel', stop);
|
|
129
|
+
}, []);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!allowSpacePan)
|
|
132
|
+
return;
|
|
133
|
+
const onKeyDown = (e) => {
|
|
134
|
+
if (e.code === 'Space' && !e.repeat) {
|
|
135
|
+
spaceDownRef.current = true;
|
|
136
|
+
if (containerRef.current)
|
|
137
|
+
containerRef.current.style.cursor = 'grab';
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const onKeyUp = (e) => {
|
|
141
|
+
if (e.code === 'Space') {
|
|
142
|
+
spaceDownRef.current = false;
|
|
143
|
+
if (containerRef.current)
|
|
144
|
+
containerRef.current.style.cursor = '';
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
window.addEventListener('keydown', onKeyDown);
|
|
148
|
+
window.addEventListener('keyup', onKeyUp);
|
|
149
|
+
return () => {
|
|
150
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
151
|
+
window.removeEventListener('keyup', onKeyUp);
|
|
152
|
+
};
|
|
153
|
+
}, [allowSpacePan]);
|
|
154
|
+
const containerStyle = {
|
|
155
|
+
position: 'relative',
|
|
156
|
+
width,
|
|
157
|
+
height,
|
|
158
|
+
overflow: 'hidden',
|
|
159
|
+
touchAction: 'none',
|
|
160
|
+
userSelect: 'none',
|
|
161
|
+
cursor: activeTool?.cursor ?? 'default',
|
|
162
|
+
...style,
|
|
163
|
+
};
|
|
164
|
+
const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
|
|
165
|
+
const { renderMeasurementStamp, selection } = props;
|
|
166
|
+
return (_jsxs("div", { ref: containerRef, style: containerStyle, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerCancel, onWheel: handleWheel, onContextMenu: handleContextMenu, children: [AnnotationCanvasSkia({
|
|
167
|
+
width,
|
|
168
|
+
height,
|
|
169
|
+
effectiveCanvas: state.effectiveCanvas,
|
|
170
|
+
worldTransform: state.worldTransform,
|
|
171
|
+
measurementsById: state.measurementsById,
|
|
172
|
+
fallbackUnit,
|
|
173
|
+
fractionalTolerance,
|
|
174
|
+
decimalTolerance,
|
|
175
|
+
resolveImageUrl,
|
|
176
|
+
valueFont,
|
|
177
|
+
labelFont,
|
|
178
|
+
hideMeasurementStamps: !!renderMeasurementStamp,
|
|
179
|
+
penDrawingStroke: state.penDrawingStroke,
|
|
180
|
+
customPreview,
|
|
181
|
+
}), renderMeasurementStamp && (_jsx("div", { style: {
|
|
182
|
+
position: 'absolute',
|
|
183
|
+
inset: 0,
|
|
184
|
+
pointerEvents: 'none',
|
|
185
|
+
}, children: state.effectiveCanvas.placedMeasurements.map((placed) => {
|
|
186
|
+
const size = STAMP_TILE_SIZE * (placed.scale ?? 1);
|
|
187
|
+
const cx = (placed.anchor.x - state.viewport.pan.x) * state.viewport.zoom;
|
|
188
|
+
const cy = (placed.anchor.y - state.viewport.pan.y) * state.viewport.zoom;
|
|
189
|
+
const isSelected = selection?.ids.includes(placed.id) ?? false;
|
|
190
|
+
return (_jsxs("div", { style: {
|
|
191
|
+
position: 'absolute',
|
|
192
|
+
left: 0,
|
|
193
|
+
top: 0,
|
|
194
|
+
width: size,
|
|
195
|
+
height: size,
|
|
196
|
+
transform: `translate(${cx - size / 2}px, ${cy - size / 2}px)`,
|
|
197
|
+
}, children: [renderMeasurementStamp({
|
|
198
|
+
placed,
|
|
199
|
+
measurement: state.measurementsById.get(placed.measurementId) ?? null,
|
|
200
|
+
selected: isSelected,
|
|
201
|
+
size,
|
|
202
|
+
zoom: state.viewport.zoom,
|
|
203
|
+
}), isSelected && (_jsx("div", { role: "button", "aria-label": "Remove measurement", onPointerDown: (e) => {
|
|
204
|
+
e.stopPropagation();
|
|
205
|
+
state.ctx.commit({
|
|
206
|
+
ops: [{ op: 'removeMeasurement', id: placed.id }],
|
|
207
|
+
});
|
|
208
|
+
state.ctx.setSelection(null);
|
|
209
|
+
}, style: {
|
|
210
|
+
position: 'absolute',
|
|
211
|
+
top: -10,
|
|
212
|
+
right: -10,
|
|
213
|
+
width: 40,
|
|
214
|
+
height: 40,
|
|
215
|
+
cursor: 'pointer',
|
|
216
|
+
pointerEvents: 'auto',
|
|
217
|
+
} }))] }, placed.id));
|
|
218
|
+
}) }))] }));
|
|
219
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type MutableRefObject } from 'react';
|
|
2
|
+
import { type ViewStyle } from 'react-native';
|
|
3
|
+
import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
|
|
4
|
+
import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
|
|
5
|
+
import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
|
|
6
|
+
import type { MeasurementRef } from './measurementPicker.js';
|
|
7
|
+
import type { Tool } from './Tool.js';
|
|
8
|
+
import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
|
|
9
|
+
import type { ViewportState } from './viewport.js';
|
|
10
|
+
export type { AnnotationCanvasHandle };
|
|
11
|
+
export interface AnnotationCanvasInnerProps {
|
|
12
|
+
canvas: AnnotationCanvasState;
|
|
13
|
+
onCommit(patch: AnnotationDocumentPatch): void;
|
|
14
|
+
tools: Tool[];
|
|
15
|
+
activeToolId: string;
|
|
16
|
+
selection: Selection | null;
|
|
17
|
+
onSelectionChange(selection: Selection | null): void;
|
|
18
|
+
measurements?: Measurement[];
|
|
19
|
+
fallbackUnit?: Units;
|
|
20
|
+
fractionalTolerance?: FractionalTolerance;
|
|
21
|
+
decimalTolerance?: DecimalTolerance;
|
|
22
|
+
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
23
|
+
pickMeasurement?: () => Promise<MeasurementRef | null>;
|
|
24
|
+
renderMeasurementStamp?: RenderMeasurementStamp;
|
|
25
|
+
stampFontSource?: unknown;
|
|
26
|
+
stampValueFontSize?: number;
|
|
27
|
+
stampLabelFontSize?: number;
|
|
28
|
+
gestures?: unknown;
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
initialViewport?: ViewportState;
|
|
32
|
+
style?: ViewStyle;
|
|
33
|
+
imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
|
|
34
|
+
}
|
|
35
|
+
export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFont } from '@shopify/react-native-skia';
|
|
3
|
+
import { useMemo, useRef } from 'react';
|
|
4
|
+
import { StyleSheet, TouchableOpacity, View } from 'react-native';
|
|
5
|
+
import { Gesture, GestureDetector, GestureHandlerRootView, } from 'react-native-gesture-handler';
|
|
6
|
+
import { STAMP_TILE_SIZE } from './stampLayout.js';
|
|
7
|
+
import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
|
|
8
|
+
import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
|
|
9
|
+
// Native fingerprint: one finger drives the active tool, two fingers
|
|
10
|
+
// pan/zoom the viewport. Tap counts as a brief pointer down+up so tools
|
|
11
|
+
// like measurement-stamp (which only listen to onPointerUp) work via tap.
|
|
12
|
+
export const AnnotationCanvasInner = (props) => {
|
|
13
|
+
const { fallbackUnit, fractionalTolerance, decimalTolerance, resolveImageUrl, stampFontSource, stampValueFontSize = 14, stampLabelFontSize = 11, width, height, style, } = props;
|
|
14
|
+
const valueFont = useFont(stampFontSource, stampValueFontSize);
|
|
15
|
+
const labelFont = useFont(stampFontSource, stampLabelFontSize);
|
|
16
|
+
const state = useAnnotationCanvasState(props);
|
|
17
|
+
// Per-gesture refs so we always emit a matching down/move/up sequence.
|
|
18
|
+
const pointerIdRef = useRef(1);
|
|
19
|
+
const inFlightRef = useRef(null);
|
|
20
|
+
const pinchStartZoomRef = useRef(1);
|
|
21
|
+
const buildEvent = (pointerId, screen) => ({
|
|
22
|
+
pointerId,
|
|
23
|
+
screen,
|
|
24
|
+
world: state.ctx.viewport.screenToWorld(screen),
|
|
25
|
+
});
|
|
26
|
+
const gesture = useMemo(() => {
|
|
27
|
+
const toolPan = Gesture.Pan()
|
|
28
|
+
.minPointers(1)
|
|
29
|
+
.maxPointers(1)
|
|
30
|
+
.runOnJS(true)
|
|
31
|
+
.onBegin((e) => {
|
|
32
|
+
const id = pointerIdRef.current++;
|
|
33
|
+
const screen = { x: e.x, y: e.y };
|
|
34
|
+
inFlightRef.current = { id, lastScreen: screen };
|
|
35
|
+
state.dispatchPointerDown(buildEvent(id, screen));
|
|
36
|
+
})
|
|
37
|
+
.onUpdate((e) => {
|
|
38
|
+
const f = inFlightRef.current;
|
|
39
|
+
if (!f)
|
|
40
|
+
return;
|
|
41
|
+
const screen = { x: e.x, y: e.y };
|
|
42
|
+
f.lastScreen = screen;
|
|
43
|
+
state.dispatchPointerMove(buildEvent(f.id, screen));
|
|
44
|
+
})
|
|
45
|
+
.onEnd((e) => {
|
|
46
|
+
const f = inFlightRef.current;
|
|
47
|
+
if (!f)
|
|
48
|
+
return;
|
|
49
|
+
state.dispatchPointerUp(buildEvent(f.id, { x: e.x, y: e.y }));
|
|
50
|
+
inFlightRef.current = null;
|
|
51
|
+
})
|
|
52
|
+
.onFinalize(() => {
|
|
53
|
+
if (inFlightRef.current) {
|
|
54
|
+
state.dispatchPointerCancel();
|
|
55
|
+
inFlightRef.current = null;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const tap = Gesture.Tap()
|
|
59
|
+
.maxDuration(250)
|
|
60
|
+
.runOnJS(true)
|
|
61
|
+
.onEnd((e) => {
|
|
62
|
+
const id = pointerIdRef.current++;
|
|
63
|
+
const screen = { x: e.x, y: e.y };
|
|
64
|
+
// Synthesize a down+up sequence so tools that only listen to
|
|
65
|
+
// onPointerUp (e.g. measurement stamp) still fire.
|
|
66
|
+
state.dispatchPointerDown(buildEvent(id, screen));
|
|
67
|
+
state.dispatchPointerUp(buildEvent(id, screen));
|
|
68
|
+
});
|
|
69
|
+
const viewportPan = Gesture.Pan()
|
|
70
|
+
.minPointers(2)
|
|
71
|
+
.maxPointers(2)
|
|
72
|
+
.runOnJS(true)
|
|
73
|
+
.onChange((e) => {
|
|
74
|
+
state.pan({ x: e.changeX, y: e.changeY });
|
|
75
|
+
});
|
|
76
|
+
const pinch = Gesture.Pinch()
|
|
77
|
+
.runOnJS(true)
|
|
78
|
+
.onBegin(() => {
|
|
79
|
+
pinchStartZoomRef.current = state.ctx.viewport.state.zoom;
|
|
80
|
+
})
|
|
81
|
+
.onUpdate((e) => {
|
|
82
|
+
state.zoom({ x: e.focalX, y: e.focalY }, pinchStartZoomRef.current * e.scale);
|
|
83
|
+
});
|
|
84
|
+
return Gesture.Race(tap, Gesture.Simultaneous(viewportPan, pinch), toolPan);
|
|
85
|
+
}, [state]);
|
|
86
|
+
const activeTool = props.tools.find((t) => t.id === props.activeToolId) ?? null;
|
|
87
|
+
const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
|
|
88
|
+
const { renderMeasurementStamp, selection } = props;
|
|
89
|
+
return (_jsxs(GestureHandlerRootView, { style: [{ width, height }, style], children: [_jsx(GestureDetector, { gesture: gesture, children: _jsx(View, { style: { width, height }, collapsable: false, children: AnnotationCanvasSkia({
|
|
90
|
+
width,
|
|
91
|
+
height,
|
|
92
|
+
effectiveCanvas: state.effectiveCanvas,
|
|
93
|
+
worldTransform: state.worldTransform,
|
|
94
|
+
measurementsById: state.measurementsById,
|
|
95
|
+
fallbackUnit,
|
|
96
|
+
fractionalTolerance,
|
|
97
|
+
decimalTolerance,
|
|
98
|
+
resolveImageUrl,
|
|
99
|
+
valueFont,
|
|
100
|
+
labelFont,
|
|
101
|
+
hideMeasurementStamps: !!renderMeasurementStamp,
|
|
102
|
+
penDrawingStroke: state.penDrawingStroke,
|
|
103
|
+
customPreview,
|
|
104
|
+
}) }) }), renderMeasurementStamp && (_jsx(View, { pointerEvents: "box-none", style: StyleSheet.absoluteFill, children: state.effectiveCanvas.placedMeasurements.map((placed) => {
|
|
105
|
+
const size = STAMP_TILE_SIZE * (placed.scale ?? 1);
|
|
106
|
+
const cx = (placed.anchor.x - state.viewport.pan.x) * state.viewport.zoom;
|
|
107
|
+
const cy = (placed.anchor.y - state.viewport.pan.y) * state.viewport.zoom;
|
|
108
|
+
const isSelected = selection?.ids.includes(placed.id) ?? false;
|
|
109
|
+
return (_jsxs(View, { pointerEvents: "box-none", style: {
|
|
110
|
+
position: 'absolute',
|
|
111
|
+
left: 0,
|
|
112
|
+
top: 0,
|
|
113
|
+
width: size,
|
|
114
|
+
height: size,
|
|
115
|
+
transform: [
|
|
116
|
+
{ translateX: cx - size / 2 },
|
|
117
|
+
{ translateY: cy - size / 2 },
|
|
118
|
+
],
|
|
119
|
+
}, children: [_jsx(View, { pointerEvents: "none", style: StyleSheet.absoluteFill, children: renderMeasurementStamp({
|
|
120
|
+
placed,
|
|
121
|
+
measurement: state.measurementsById.get(placed.measurementId) ?? null,
|
|
122
|
+
selected: isSelected,
|
|
123
|
+
size,
|
|
124
|
+
zoom: state.viewport.zoom,
|
|
125
|
+
}) }), isSelected && (_jsx(TouchableOpacity, { accessibilityRole: "button", accessibilityLabel: "Remove measurement", hitSlop: 10, onPress: () => {
|
|
126
|
+
state.ctx.commit({
|
|
127
|
+
ops: [{ op: 'removeMeasurement', id: placed.id }],
|
|
128
|
+
});
|
|
129
|
+
state.ctx.setSelection(null);
|
|
130
|
+
}, style: {
|
|
131
|
+
position: 'absolute',
|
|
132
|
+
top: -8,
|
|
133
|
+
right: -8,
|
|
134
|
+
width: 36,
|
|
135
|
+
height: 36,
|
|
136
|
+
} }))] }, placed.id));
|
|
137
|
+
}) }))] }));
|
|
138
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type SkFont } from '@shopify/react-native-skia';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type { AnnotationCanvasState, AnnotationStroke } from '../types/annotation.js';
|
|
4
|
+
import { DecimalTolerance, FractionalTolerance, Units, type Measurement } from '../types/firestore.js';
|
|
5
|
+
export interface AnnotationCanvasSkiaProps {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
effectiveCanvas: AnnotationCanvasState;
|
|
9
|
+
worldTransform: Array<{
|
|
10
|
+
scale: number;
|
|
11
|
+
} | {
|
|
12
|
+
translateX: number;
|
|
13
|
+
} | {
|
|
14
|
+
translateY: number;
|
|
15
|
+
}>;
|
|
16
|
+
measurementsById: Map<string, Measurement>;
|
|
17
|
+
fallbackUnit?: Units;
|
|
18
|
+
fractionalTolerance?: FractionalTolerance;
|
|
19
|
+
decimalTolerance?: DecimalTolerance;
|
|
20
|
+
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
21
|
+
valueFont: SkFont | null;
|
|
22
|
+
labelFont: SkFont | null;
|
|
23
|
+
hideMeasurementStamps?: boolean;
|
|
24
|
+
penDrawingStroke: AnnotationStroke | null;
|
|
25
|
+
customPreview?: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, measurementsById, fallbackUnit, fractionalTolerance, decimalTolerance, resolveImageUrl, valueFont, labelFont, hideMeasurementStamps, penDrawingStroke, customPreview, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Canvas, Group } from '@shopify/react-native-skia';
|
|
3
|
+
import { Units, } from '../types/firestore.js';
|
|
4
|
+
import { BackgroundImageElement } from './elements/BackgroundImageElement.js';
|
|
5
|
+
import { MeasurementStampElement } from './elements/MeasurementStampElement.js';
|
|
6
|
+
import { ShapeElement } from './elements/ShapeElement.js';
|
|
7
|
+
import { StrokeElement } from './elements/StrokeElement.js';
|
|
8
|
+
// Platform-agnostic Skia subtree shared by web and native Inners.
|
|
9
|
+
//
|
|
10
|
+
// Call this as a FUNCTION (`AnnotationCanvasSkia({ ... })`), not as a JSX
|
|
11
|
+
// component (`<AnnotationCanvasSkia ... />`). Used as a component, it adds
|
|
12
|
+
// a React component boundary between the parent and Skia's `<Canvas>` that
|
|
13
|
+
// breaks Skia's reconciler when the page first calls `useFont` — the JS
|
|
14
|
+
// thread hangs in `MakeFreeTypeFaceFromData`. Symptoms only appeared on
|
|
15
|
+
// Vite's dev server with a symlinked boldr-utils + React Refresh, but
|
|
16
|
+
// since the function-call pattern works identically on native we use it
|
|
17
|
+
// in both Inners for consistency. Don't add hooks here; this is a plain
|
|
18
|
+
// JSX-returning helper, not a component.
|
|
19
|
+
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, measurementsById, fallbackUnit, fractionalTolerance, decimalTolerance, resolveImageUrl, valueFont, labelFont, hideMeasurementStamps, penDrawingStroke, customPreview, }) => (_jsx(Canvas, { style: { width, height }, children: _jsxs(Group, { transform: worldTransform, children: [effectiveCanvas.viewport.backgroundImage && (_jsx(BackgroundImageElement, { image: effectiveCanvas.viewport.backgroundImage, docWidth: effectiveCanvas.viewport.width, docHeight: effectiveCanvas.viewport.height, fit: effectiveCanvas.viewport.backgroundFit ?? 'contain', resolveUrl: resolveImageUrl })), effectiveCanvas.strokes.map((stroke) => (_jsx(StrokeElement, { stroke: stroke }, stroke.id))), effectiveCanvas.shapes.map((shape) => (_jsx(ShapeElement, { shape: shape, font: valueFont }, shape.id))), !hideMeasurementStamps &&
|
|
20
|
+
effectiveCanvas.placedMeasurements.map((placed) => (_jsx(MeasurementStampElement, { placed: placed, measurement: measurementsById.get(placed.measurementId) ?? null, fallbackUnit: fallbackUnit ?? Units.Millimeters, fractionalTolerance: fractionalTolerance, decimalTolerance: decimalTolerance, valueFont: valueFont, labelFont: labelFont }, placed.id))), penDrawingStroke && _jsx(StrokeElement, { stroke: penDrawingStroke }), customPreview] }) }));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactNode, ComponentType } from 'react';
|
|
2
|
+
import type { AnnotationCanvasState, AnnotationDocumentPatch, AnnotationElement, Selection, Vec2 } from '../types/annotation.js';
|
|
3
|
+
import type { MeasurementRef } from './measurementPicker.js';
|
|
4
|
+
import type { ViewportApi } from './viewport.js';
|
|
5
|
+
export interface CanvasPointerEvent {
|
|
6
|
+
pointerId: number;
|
|
7
|
+
world: Vec2;
|
|
8
|
+
screen: Vec2;
|
|
9
|
+
pressure?: number;
|
|
10
|
+
shiftKey?: boolean;
|
|
11
|
+
altKey?: boolean;
|
|
12
|
+
metaKey?: boolean;
|
|
13
|
+
ctrlKey?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ToolContext {
|
|
16
|
+
document: AnnotationCanvasState;
|
|
17
|
+
selection: Selection | null;
|
|
18
|
+
viewport: ViewportApi;
|
|
19
|
+
preview(patch: AnnotationDocumentPatch): void;
|
|
20
|
+
commit(patch: AnnotationDocumentPatch): void;
|
|
21
|
+
setSelection(selection: Selection | null): void;
|
|
22
|
+
requestPickMeasurement(): Promise<MeasurementRef | null>;
|
|
23
|
+
applyPan(deltaScreen: Vec2): void;
|
|
24
|
+
applyZoom(focalScreen: Vec2, nextZoom: number): void;
|
|
25
|
+
}
|
|
26
|
+
export type ToolState = unknown;
|
|
27
|
+
export interface Tool {
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
icon?: ComponentType;
|
|
31
|
+
cursor?: string;
|
|
32
|
+
onPointerDown?(event: CanvasPointerEvent, ctx: ToolContext, state: ToolState): ToolState | void;
|
|
33
|
+
onPointerMove?(event: CanvasPointerEvent, ctx: ToolContext, state: ToolState): ToolState | void;
|
|
34
|
+
onPointerUp?(event: CanvasPointerEvent, ctx: ToolContext, state: ToolState): ToolState | void;
|
|
35
|
+
onCancel?(state: ToolState, ctx: ToolContext): void;
|
|
36
|
+
renderPreview?(state: ToolState, ctx: ToolContext): ReactNode;
|
|
37
|
+
hitTest?(element: AnnotationElement, worldPoint: Vec2): boolean;
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnnotationBackgroundImage, BackgroundFit } from '../../types/annotation.js';
|
|
2
|
+
export interface BackgroundImageElementProps {
|
|
3
|
+
image: AnnotationBackgroundImage;
|
|
4
|
+
docWidth: number;
|
|
5
|
+
docHeight: number;
|
|
6
|
+
fit?: BackgroundFit;
|
|
7
|
+
resolveUrl?: (path: string) => Promise<string>;
|
|
8
|
+
}
|
|
9
|
+
export declare const BackgroundImageElement: ({ image, docWidth, docHeight, fit, resolveUrl, }: BackgroundImageElementProps) => import("react/jsx-runtime").JSX.Element | null;
|