@reekon-tools/boldr-utils 1.18.2 → 1.19.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 +26 -5
- package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +2 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +29 -3
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +2 -1
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +2 -2
- package/dist/annotation/canvas/backgroundSlot.d.ts +11 -0
- package/dist/annotation/canvas/pdfDocument.d.ts +25 -0
- package/dist/annotation/canvas/pdfDocument.js +83 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +26 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +35 -5
- package/dist/annotation/canvas/viewport.d.ts +9 -1
- package/dist/annotation/canvas/viewport.js +7 -2
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.d.ts +5 -1
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +26 -0
- package/dist/exports.d.ts +3 -1
- package/dist/exports.js +2 -1
- package/dist/types/annotation.d.ts +18 -0
- package/dist/types/annotation.js +18 -0
- package/package.json +2 -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/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
- /package/dist/{canvas/Tool.js → annotation/canvas/backgroundSlot.js} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type CSSProperties, type MutableRefObject } from 'react';
|
|
2
2
|
import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../../types/firestore.js';
|
|
3
3
|
import type { AnnotationCanvasState, AnnotationDocumentPatch, PlacedMeasurementRef, Selection } from '../../types/annotation.js';
|
|
4
|
+
import type { RenderCanvasBackground } from './backgroundSlot.js';
|
|
4
5
|
import type { MeasurementRef } from './measurementPicker.js';
|
|
5
6
|
import type { RequestTextInput, Tool } from './Tool.js';
|
|
6
7
|
import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
|
|
@@ -21,6 +22,7 @@ export interface AnnotationCanvasInnerProps {
|
|
|
21
22
|
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
22
23
|
pickMeasurement?: () => Promise<MeasurementRef | null>;
|
|
23
24
|
requestTextInput?: RequestTextInput;
|
|
25
|
+
renderBackground?: RenderCanvasBackground;
|
|
24
26
|
renderMeasurementStamp?: RenderMeasurementStamp;
|
|
25
27
|
onMeasurementStampPress?: (placed: PlacedMeasurementRef) => void;
|
|
26
28
|
onMeasurementStampLongPress?: (placed: PlacedMeasurementRef) => void;
|
|
@@ -20,8 +20,14 @@ const HANDLE_RING_PX = 2;
|
|
|
20
20
|
const LONG_PRESS_MS = 500;
|
|
21
21
|
const LONG_PRESS_SLOP_PX = 10;
|
|
22
22
|
const DEFAULT_PAN_TRIGGERS = ['middleMouse', 'space'];
|
|
23
|
+
// Device pixel ratio handed to the background slot (a host rasterizing its own
|
|
24
|
+
// doc content needs device px per doc point = zoom × pixelRatio). Read per
|
|
25
|
+
// render rather than tracked: it only changes with browser zoom or a monitor
|
|
26
|
+
// switch, both of which also resize the canvas. Guarded for non-DOM renders
|
|
27
|
+
// (SSR / the vitest environment), where 1 is the right answer.
|
|
28
|
+
const devicePixelRatioOf = () => typeof window === 'undefined' ? 1 : window.devicePixelRatio || 1;
|
|
23
29
|
export const AnnotationCanvasInner = (props) => {
|
|
24
|
-
const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, gestures, width, height, style, activeToolId, tools, snapshotRef, } = props;
|
|
30
|
+
const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, gestures, width, height, style, activeToolId, tools, snapshotRef, renderBackground, } = props;
|
|
25
31
|
// Snapshot plumbing for thumbnail capture: hold the <Canvas> ref and publish
|
|
26
32
|
// a capture fn on the consumer's snapshotRef. encodeToBytes() yields PNG.
|
|
27
33
|
const canvasRef = useCanvasRef();
|
|
@@ -200,14 +206,19 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
200
206
|
x: event.clientX - (rect?.left ?? 0),
|
|
201
207
|
y: event.clientY - (rect?.top ?? 0),
|
|
202
208
|
};
|
|
209
|
+
// `zoomBy`, not `zoom`: the factor has to compose against the viewport
|
|
210
|
+
// React is about to commit. Wheel events arrive in bursts and React
|
|
211
|
+
// batches them, so computing an absolute target here from
|
|
212
|
+
// `state.ctx.viewport.state.zoom` — a render-stale read — made every
|
|
213
|
+
// event in a batch aim at the same zoom and `zoomAt` no-op for all but
|
|
214
|
+
// the last. Measured: ten same-task wheel events moved the view x1.49
|
|
215
|
+
// where their deltas asked for x54.6.
|
|
203
216
|
if (event.ctrlKey || event.metaKey) {
|
|
204
|
-
|
|
205
|
-
state.zoom(focal, state.ctx.viewport.state.zoom * factor);
|
|
217
|
+
state.zoomBy(focal, Math.exp(-event.deltaY / 200));
|
|
206
218
|
return;
|
|
207
219
|
}
|
|
208
220
|
if (wheelMode === 'zoom') {
|
|
209
|
-
|
|
210
|
-
state.zoom(focal, state.ctx.viewport.state.zoom * factor);
|
|
221
|
+
state.zoomBy(focal, Math.exp(-event.deltaY / 300));
|
|
211
222
|
return;
|
|
212
223
|
}
|
|
213
224
|
if (event.shiftKey) {
|
|
@@ -267,6 +278,15 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
267
278
|
...style,
|
|
268
279
|
};
|
|
269
280
|
const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
|
|
281
|
+
// Host substrate (see renderBackground). `state.viewport` IS the committed
|
|
282
|
+
// viewport on web — the whole viewport lives in React state here, so every
|
|
283
|
+
// wheel/pan step is already a commit and there is no per-frame channel to
|
|
284
|
+
// accidentally hand over.
|
|
285
|
+
const backgroundContent = useMemo(() => renderBackground?.({
|
|
286
|
+
viewport: state.viewport,
|
|
287
|
+
screen: { width, height },
|
|
288
|
+
pixelRatio: devicePixelRatioOf(),
|
|
289
|
+
}), [renderBackground, state.viewport, width, height]);
|
|
270
290
|
const { renderMeasurementStamp, onMeasurementStampPress, onMeasurementStampLongPress, onMeasurementStampDoubleTap, resolveStampMeasurement, onMeasurementStampRemove, selection, } = props;
|
|
271
291
|
// Double-click a stamp → fire the consumer callback (e.g. open its value
|
|
272
292
|
// editor). Runs the SAME hit-test the select tool uses (top-of-z-order), on
|
|
@@ -295,6 +315,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
295
315
|
worldTransform: state.worldTransform,
|
|
296
316
|
viewport: state.viewport,
|
|
297
317
|
resolveImageUrl,
|
|
318
|
+
backgroundContent,
|
|
298
319
|
canvasRef,
|
|
299
320
|
valueFont,
|
|
300
321
|
textFontMgr,
|
|
@@ -3,6 +3,7 @@ import { type ViewStyle } from 'react-native';
|
|
|
3
3
|
import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
|
|
4
4
|
import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../../types/firestore.js';
|
|
5
5
|
import { type AnnotationCanvasState, type AnnotationDocumentPatch, type PlacedMeasurementRef, type Selection } from '../../types/annotation.js';
|
|
6
|
+
import type { RenderCanvasBackground } from './backgroundSlot.js';
|
|
6
7
|
import type { MeasurementRef } from './measurementPicker.js';
|
|
7
8
|
import type { RequestTextInput, Tool } from './Tool.js';
|
|
8
9
|
import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
|
|
@@ -22,6 +23,7 @@ export interface AnnotationCanvasInnerProps {
|
|
|
22
23
|
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
23
24
|
pickMeasurement?: () => Promise<MeasurementRef | null>;
|
|
24
25
|
requestTextInput?: RequestTextInput;
|
|
26
|
+
renderBackground?: RenderCanvasBackground;
|
|
25
27
|
renderMeasurementStamp?: RenderMeasurementStamp;
|
|
26
28
|
onMeasurementStampPress?: (placed: PlacedMeasurementRef) => void;
|
|
27
29
|
onMeasurementStampLongPress?: (placed: PlacedMeasurementRef) => void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Group, Path, Rect, RoundedRect, Skia, rect, rrect, useFont, } from '@shopify/react-native-skia';
|
|
3
3
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
4
|
-
import { StyleSheet, TouchableOpacity, View, } from 'react-native';
|
|
4
|
+
import { PixelRatio, StyleSheet, TouchableOpacity, View, } from 'react-native';
|
|
5
5
|
import { Gesture, GestureDetector, GestureHandlerRootView, } from 'react-native-gesture-handler';
|
|
6
6
|
import Animated, { runOnJS, runOnUI, useAnimatedStyle, useDerivedValue, useSharedValue, } from 'react-native-reanimated';
|
|
7
7
|
import { stampTileDims } from './stampLayout.js';
|
|
@@ -64,7 +64,7 @@ const LOUPE_CROSS_WIDTH_PX = 1.5;
|
|
|
64
64
|
// is only re-synced once each gesture ends. The tool/tap path still hops to
|
|
65
65
|
// JS (`runOnJS(true)`) since drawing/selection are React-state operations.
|
|
66
66
|
export const AnnotationCanvasInner = (props) => {
|
|
67
|
-
const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, } = props;
|
|
67
|
+
const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, renderBackground, } = props;
|
|
68
68
|
const valueFont = useFont(stampFontSource, stampValueFontSize);
|
|
69
69
|
// The animated-viewport implementation needs the Reanimated shared values,
|
|
70
70
|
// which are created below AFTER the state hook — so the hook gets a stable
|
|
@@ -128,6 +128,14 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
128
128
|
const zoom = useSharedValue(state.viewport.zoom);
|
|
129
129
|
const panX = useSharedValue(state.viewport.pan.x);
|
|
130
130
|
const panY = useSharedValue(state.viewport.pan.y);
|
|
131
|
+
// Document-derived pinch floor (see computeZoomFloor). A shared value rather
|
|
132
|
+
// than a captured number so the gesture memo below needn't rebuild — and
|
|
133
|
+
// can't be caught mid-pinch holding a stale floor — when the document's
|
|
134
|
+
// extent changes.
|
|
135
|
+
const zoomMin = useSharedValue(state.zoomMin);
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
zoomMin.value = state.zoomMin;
|
|
138
|
+
}, [zoomMin, state.zoomMin]);
|
|
131
139
|
// Set by the viewport gestures' onChange, consumed by their onFinalize: the
|
|
132
140
|
// post-gesture sync to React state happens iff a gesture actually moved the
|
|
133
141
|
// viewport, and in onFinalize rather than onEnd because on Android the
|
|
@@ -1135,7 +1143,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1135
1143
|
const ratio = e.scaleChange;
|
|
1136
1144
|
if (!Number.isFinite(ratio) || ratio <= 0)
|
|
1137
1145
|
return;
|
|
1138
|
-
const next = Math.min(50, Math.max(
|
|
1146
|
+
const next = Math.min(50, Math.max(zoomMin.value, zoom.value * ratio));
|
|
1139
1147
|
if (next === zoom.value)
|
|
1140
1148
|
return; // clamped at a bound, or ratio 1
|
|
1141
1149
|
// World point currently under the focal screen point (using the live
|
|
@@ -1863,6 +1871,19 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1863
1871
|
]);
|
|
1864
1872
|
const activeTool = props.tools.find((t) => t.id === props.activeToolId) ?? null;
|
|
1865
1873
|
const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
|
|
1874
|
+
// Host substrate (see renderBackground). It is handed `state.viewport` — the
|
|
1875
|
+
// JS/React snapshot the state hook owns — NOT `zoom`/`panX`/`panY`. Those
|
|
1876
|
+
// shared values move every frame of a pan/pinch and never touch React; the
|
|
1877
|
+
// snapshot is written only by setViewport, i.e. at the end of a viewport
|
|
1878
|
+
// gesture (syncViewport), at the end of an animated glide, on the initial
|
|
1879
|
+
// content fit, and from the imperative view methods. So this memo re-runs
|
|
1880
|
+
// strictly on commit — there is no useAnimatedReaction/runOnJS bridging the
|
|
1881
|
+
// live values into it, by design.
|
|
1882
|
+
const backgroundContent = useMemo(() => renderBackground?.({
|
|
1883
|
+
viewport: state.viewport,
|
|
1884
|
+
screen: { width, height },
|
|
1885
|
+
pixelRatio: PixelRatio.get(),
|
|
1886
|
+
}), [renderBackground, state.viewport, width, height]);
|
|
1866
1887
|
const { renderMeasurementStamp, onMeasurementStampPress, onMeasurementStampLongPress, resolveStampMeasurement, onMeasurementStampRemove, selection, } = props;
|
|
1867
1888
|
// The Skia scene props, shared by the main canvas and the loupe overlay. The
|
|
1868
1889
|
// loupe re-runs the SAME scene with a magnifying world transform + a lens
|
|
@@ -1875,6 +1896,11 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1875
1896
|
worldTransform,
|
|
1876
1897
|
resolveImageUrl,
|
|
1877
1898
|
backgroundSkImages,
|
|
1899
|
+
// Shared with the loupe overlay below (it re-runs this same scene through a
|
|
1900
|
+
// magnifying transform), so the lens shows the substrate magnified instead
|
|
1901
|
+
// of a hole where the page should be. The node is rendered in both trees —
|
|
1902
|
+
// fine for a draw-only slot, which is what the doc-space contract implies.
|
|
1903
|
+
backgroundContent,
|
|
1878
1904
|
valueFont,
|
|
1879
1905
|
// Freehand drawing runs on the UI thread via `livePreview`, so the
|
|
1880
1906
|
// JS-state pen preview is unused on native.
|
|
@@ -24,6 +24,7 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
24
24
|
viewport?: ViewportState;
|
|
25
25
|
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
26
26
|
backgroundSkImages?: Record<string, SkImage | null>;
|
|
27
|
+
backgroundContent?: ReactNode;
|
|
27
28
|
valueFont: SkFont | null;
|
|
28
29
|
textFontMgr?: SkTypefaceFontProvider | null;
|
|
29
30
|
penDrawingStroke: AnnotationStroke | null;
|
|
@@ -104,5 +105,5 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
104
105
|
wrapContent?: (content: ReactNode) => ReactNode;
|
|
105
106
|
canvasRef?: RefObject<CanvasRef | null>;
|
|
106
107
|
}
|
|
107
|
-
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, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale, planeDragHandleHidden, customPreview, wrapContent, canvasRef, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
108
|
+
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, backgroundContent, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale, planeDragHandleHidden, customPreview, wrapContent, canvasRef, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
108
109
|
export {};
|
|
@@ -80,7 +80,7 @@ const SelectionBox = ({ bounds, isDragging, transform, }) => (_jsx(DraggableElem
|
|
|
80
80
|
// since the function-call pattern works identically on native we use it
|
|
81
81
|
// in both Inners for consistency. Don't add hooks here; this is a plain
|
|
82
82
|
// JSX-returning helper, not a component.
|
|
83
|
-
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, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale = 1, planeDragHandleHidden, customPreview, wrapContent, canvasRef, }) => {
|
|
83
|
+
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, backgroundContent, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale = 1, planeDragHandleHidden, customPreview, wrapContent, canvasRef, }) => {
|
|
84
84
|
// Doc rect a SCALE-mode plane grid covers: the first background layer (the
|
|
85
85
|
// photo being calibrated), else the document rect. Pinned to layer 0 like
|
|
86
86
|
// the tile-sizing reference — adding a second image must not re-span the
|
|
@@ -94,7 +94,7 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
|
|
|
94
94
|
width: effectiveCanvas.viewport.width,
|
|
95
95
|
height: effectiveCanvas.viewport.height,
|
|
96
96
|
};
|
|
97
|
-
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.planes ?? []).map((plane) => {
|
|
97
|
+
const world = (_jsxs(Group, { transform: worldTransform, children: [backgroundContent, 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.planes ?? []).map((plane) => {
|
|
98
98
|
if (plane.id === planeDragId && planeDragPath) {
|
|
99
99
|
const statics = plane.mode === 'perspective'
|
|
100
100
|
? plane.corners
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ViewportState } from './viewport.js';
|
|
3
|
+
export interface CanvasBackgroundSlotArgs {
|
|
4
|
+
viewport: ViewportState;
|
|
5
|
+
screen: {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
pixelRatio: number;
|
|
10
|
+
}
|
|
11
|
+
export type RenderCanvasBackground = (args: CanvasBackgroundSlotArgs) => ReactNode;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AnnotationCanvasState, AnnotationPdfDocument, AnnotationPdfPageSize } from '../../types/annotation.js';
|
|
2
|
+
export interface PdfDocRect {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const DEFAULT_PDF_PAGE_GAP = 12;
|
|
9
|
+
export declare const makePdfDocumentId: () => string;
|
|
10
|
+
export declare const pdfPageDocRects: (doc: Pick<AnnotationPdfDocument, "pages" | "layout" | "gap">) => PdfDocRect[];
|
|
11
|
+
export declare const pdfDocumentExtent: (doc: Pick<AnnotationPdfDocument, "pages" | "layout" | "gap">) => {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
};
|
|
15
|
+
export declare const pdfFirstPageDocRect: (doc: Pick<AnnotationPdfDocument, "pages" | "layout" | "gap">) => PdfDocRect | null;
|
|
16
|
+
export declare const createPdfDocument: (args: {
|
|
17
|
+
id: string;
|
|
18
|
+
storagePath: string;
|
|
19
|
+
pages: AnnotationPdfPageSize[];
|
|
20
|
+
fileName?: string;
|
|
21
|
+
gap?: number;
|
|
22
|
+
}) => AnnotationPdfDocument;
|
|
23
|
+
export declare const DEFAULT_MIN_ZOOM = 0.05;
|
|
24
|
+
export declare const DEFAULT_MAX_ZOOM = 50;
|
|
25
|
+
export declare const computeZoomFloor: (canvas: Pick<AnnotationCanvasState, "viewport">, width: number, height: number) => number;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { fitRectToScreen } from './viewport.js';
|
|
2
|
+
// Doc-space geometry of a PDF substrate, derived from the persisted descriptor
|
|
3
|
+
// alone — no rasterizer, no file. The rules here MUST match the layout the
|
|
4
|
+
// host hands its PDF rasterizer for the same `layout` value (the package's
|
|
5
|
+
// `continuousVertical(gap)`): pages stacked top to bottom, horizontally centred
|
|
6
|
+
// on the widest page, `gap` between consecutive pages, no trailing gap. If the
|
|
7
|
+
// two ever disagree the pages draw offset from where the fit, the thumbnail
|
|
8
|
+
// crop and the tile-scale reference think they are.
|
|
9
|
+
// Default doc-space (PDF point) gap between pages. Same as the package's.
|
|
10
|
+
export const DEFAULT_PDF_PAGE_GAP = 12;
|
|
11
|
+
// Unique within a document across the clients that touch it — same contract
|
|
12
|
+
// as background layer ids (timestamp + module counter). The `pdf-` prefix
|
|
13
|
+
// keeps it distinguishable from `background-*` in the shared backgrounds slot.
|
|
14
|
+
let pdfIdCounter = 0;
|
|
15
|
+
export const makePdfDocumentId = () => `pdf-${Date.now().toString(36)}-${(pdfIdCounter++).toString(36)}`;
|
|
16
|
+
export const pdfPageDocRects = (doc) => {
|
|
17
|
+
// Only one layout exists; a switch keeps the exhaustiveness check honest for
|
|
18
|
+
// the day a second one lands.
|
|
19
|
+
switch (doc.layout) {
|
|
20
|
+
case 'continuousVertical':
|
|
21
|
+
default: {
|
|
22
|
+
let maxWidth = 0;
|
|
23
|
+
for (const p of doc.pages)
|
|
24
|
+
maxWidth = Math.max(maxWidth, p.width);
|
|
25
|
+
const rects = [];
|
|
26
|
+
let y = 0;
|
|
27
|
+
for (const p of doc.pages) {
|
|
28
|
+
rects.push({
|
|
29
|
+
x: (maxWidth - p.width) / 2,
|
|
30
|
+
y,
|
|
31
|
+
width: p.width,
|
|
32
|
+
height: p.height,
|
|
33
|
+
});
|
|
34
|
+
y += p.height + doc.gap;
|
|
35
|
+
}
|
|
36
|
+
return rects;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
// The union of the page rects — what `viewport.width/height` is set to when
|
|
41
|
+
// the document is attached. Never degenerate: a zero-page document would
|
|
42
|
+
// otherwise hand fitRectToScreen a divide-by-zero.
|
|
43
|
+
export const pdfDocumentExtent = (doc) => {
|
|
44
|
+
let width = 0;
|
|
45
|
+
let height = 0;
|
|
46
|
+
for (const r of pdfPageDocRects(doc)) {
|
|
47
|
+
width = Math.max(width, r.x + r.width);
|
|
48
|
+
height = Math.max(height, r.y + r.height);
|
|
49
|
+
}
|
|
50
|
+
return { width: Math.max(1, width), height: Math.max(1, height) };
|
|
51
|
+
};
|
|
52
|
+
// The first page's rect: the tile-scale reference for a PDF-backed canvas
|
|
53
|
+
// (see computeBaseFitZoom). Fitting the WHOLE filmstrip instead would make a
|
|
54
|
+
// tile's footprint ~N× smaller on an N-page document at the same visual zoom
|
|
55
|
+
// — and would grow every existing tile the moment a page is appended.
|
|
56
|
+
export const pdfFirstPageDocRect = (doc) => pdfPageDocRects(doc)[0] ?? null;
|
|
57
|
+
// Build the descriptor from what a host learns by opening the file.
|
|
58
|
+
export const createPdfDocument = (args) => ({
|
|
59
|
+
id: args.id,
|
|
60
|
+
storagePath: args.storagePath,
|
|
61
|
+
...(args.fileName ? { fileName: args.fileName } : {}),
|
|
62
|
+
pageCount: args.pages.length,
|
|
63
|
+
pages: args.pages.map((p) => ({ width: p.width, height: p.height })),
|
|
64
|
+
layout: 'continuousVertical',
|
|
65
|
+
gap: args.gap ?? DEFAULT_PDF_PAGE_GAP,
|
|
66
|
+
});
|
|
67
|
+
// The zoom floor for a canvas. The historical floor (0.05) predates multi-page
|
|
68
|
+
// documents: a 40-page letter filmstrip is ~32,000 points tall and fits an
|
|
69
|
+
// 800pt screen at zoom ~0.025, so with a fixed floor the opening fit is legal
|
|
70
|
+
// but the first pinch snaps 2× in. The floor therefore follows the document:
|
|
71
|
+
// half the content-fit zoom of the doc rect, never above the historical value
|
|
72
|
+
// (so single-image canvases behave exactly as before).
|
|
73
|
+
export const DEFAULT_MIN_ZOOM = 0.05;
|
|
74
|
+
export const DEFAULT_MAX_ZOOM = 50;
|
|
75
|
+
export const computeZoomFloor = (canvas, width, height) => {
|
|
76
|
+
if (!(width > 0) || !(height > 0))
|
|
77
|
+
return DEFAULT_MIN_ZOOM;
|
|
78
|
+
const { viewport } = canvas;
|
|
79
|
+
const fit = fitRectToScreen({ x: 0, y: 0, width: viewport.width, height: viewport.height }, width, height);
|
|
80
|
+
return fit.zoom > 0
|
|
81
|
+
? Math.min(DEFAULT_MIN_ZOOM, fit.zoom / 2)
|
|
82
|
+
: DEFAULT_MIN_ZOOM;
|
|
83
|
+
};
|
|
@@ -83,6 +83,7 @@ export interface AnnotationCanvasStateApi {
|
|
|
83
83
|
translateY: number;
|
|
84
84
|
}>;
|
|
85
85
|
viewport: ViewportState;
|
|
86
|
+
zoomMin: number;
|
|
86
87
|
measurementsById: Map<string, Measurement>;
|
|
87
88
|
activeTool: Tool | null;
|
|
88
89
|
toolState: ToolState;
|
|
@@ -100,6 +101,31 @@ export interface AnnotationCanvasStateApi {
|
|
|
100
101
|
dispatchLongPress(event: CanvasPointerEvent): void;
|
|
101
102
|
pan(deltaScreen: Vec2): void;
|
|
102
103
|
zoom(focalScreen: Vec2, nextZoom: number): void;
|
|
104
|
+
/**
|
|
105
|
+
* Zoom by a MULTIPLICATIVE FACTOR about a focal screen point.
|
|
106
|
+
*
|
|
107
|
+
* Same anchoring as `zoom`, but the target zoom is derived INSIDE the
|
|
108
|
+
* functional update, so the factor composes against the viewport React is
|
|
109
|
+
* about to commit rather than against whatever was on screen when the event
|
|
110
|
+
* handler ran.
|
|
111
|
+
*
|
|
112
|
+
* WHY THIS EXISTS. `zoom` takes an absolute target, so a caller has to read
|
|
113
|
+
* the current zoom to compute one — and a read during an event handler is a
|
|
114
|
+
* RENDER-STALE read. Wheel and pinch events arrive in bursts that React
|
|
115
|
+
* batches into a single commit, so every event in the batch computes its
|
|
116
|
+
* target from the SAME committed zoom; `zoomAt` is then handed a zoom it has
|
|
117
|
+
* already reached and no-ops, silently discarding the earlier events' steps.
|
|
118
|
+
* Measured on the web PDF probe: ten wheel events dispatched in one task
|
|
119
|
+
* produced x1.49 where the deltas asked for x54.6 — nine of ten steps lost.
|
|
120
|
+
* (The ANCHOR was never the problem; it is exact to 5e-4 CSS px for whatever
|
|
121
|
+
* pair `zoomAt` is handed.)
|
|
122
|
+
*
|
|
123
|
+
* `zoom` is deliberately left alone: an absolute target is the right shape for
|
|
124
|
+
* a slider, a zoom-to-fit or a native gesture that already tracks its own
|
|
125
|
+
* cumulative scale, and the seven production canvas screens keep their
|
|
126
|
+
* behaviour untouched.
|
|
127
|
+
*/
|
|
128
|
+
zoomBy(focalScreen: Vec2, factor: number): void;
|
|
103
129
|
setViewport(next: ViewportState): void;
|
|
104
130
|
}
|
|
105
131
|
export declare const useAnnotationCanvasState: (props: UseAnnotationCanvasStateProps) => AnnotationCanvasStateApi;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { applyPatch, invertPatch, DEFAULT_LAYER_ID, } from '../../types/annotation.js';
|
|
3
3
|
import { resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, resolveTileScaleFactor, stampTileDims, STAMP_TILE_SIZE, tileDocSizeForFactor, tileScaleFromDocSize, } from './stampLayout.js';
|
|
4
|
-
import { createViewportApi, fitRectToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
|
|
4
|
+
import { createViewportApi, fitRectToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './viewport.js';
|
|
5
|
+
import { computeZoomFloor, pdfFirstPageDocRect } from './pdfDocument.js';
|
|
5
6
|
import { backgroundLayerDocRect, backgroundLayersOf, backgroundLayersUnionRect, } from './backgroundLayers.js';
|
|
6
7
|
import { buildRemoveMeasurementOps, recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
|
|
7
8
|
// The viewport that frames the document's content inside a screen rect of the
|
|
@@ -85,9 +86,19 @@ const computeBaseFitZoom = (canvas, width, height) => {
|
|
|
85
86
|
return 1;
|
|
86
87
|
const { viewport } = canvas;
|
|
87
88
|
const first = backgroundLayersOf(viewport)[0];
|
|
89
|
+
// A PDF-backed canvas with no image layers references its FIRST PAGE, not
|
|
90
|
+
// the whole filmstrip — see pdfFirstPageDocRect for why.
|
|
91
|
+
const pdfFirstPage = viewport.pdfDocument
|
|
92
|
+
? pdfFirstPageDocRect(viewport.pdfDocument)
|
|
93
|
+
: null;
|
|
88
94
|
const baseRect = first
|
|
89
95
|
? backgroundLayerDocRect(first)
|
|
90
|
-
:
|
|
96
|
+
: (pdfFirstPage ?? {
|
|
97
|
+
x: 0,
|
|
98
|
+
y: 0,
|
|
99
|
+
width: viewport.width,
|
|
100
|
+
height: viewport.height,
|
|
101
|
+
});
|
|
91
102
|
return fitRectToScreen(baseRect, width, height).zoom;
|
|
92
103
|
};
|
|
93
104
|
// Default leader length (doc units) for a placed measurement when no explicit
|
|
@@ -129,6 +140,13 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
129
140
|
return map;
|
|
130
141
|
}, [measurements]);
|
|
131
142
|
const viewportApi = useMemo(() => createViewportApi(viewport), [viewport]);
|
|
143
|
+
// Document-derived zoom floor (see computeZoomFloor). Behind a ref so the
|
|
144
|
+
// pan/zoom callbacks below stay referentially stable; the native Inner reads
|
|
145
|
+
// `zoomMin` from the returned state and mirrors it into a shared value for
|
|
146
|
+
// the pinch worklet.
|
|
147
|
+
const zoomMin = useMemo(() => computeZoomFloor(effectiveCanvas, width, height), [effectiveCanvas, width, height]);
|
|
148
|
+
const zoomLimitsRef = useRef({ min: zoomMin, max: DEFAULT_ZOOM_LIMITS.max });
|
|
149
|
+
zoomLimitsRef.current = { min: zoomMin, max: DEFAULT_ZOOM_LIMITS.max };
|
|
132
150
|
// Frame the document the first time it carries a background image. The doc
|
|
133
151
|
// hydrates asynchronously (it opens empty, then the persisted canvas — with
|
|
134
152
|
// its background — arrives), and a high-res image at the 1:1 default viewport
|
|
@@ -141,8 +159,10 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
141
159
|
useEffect(() => {
|
|
142
160
|
if (didInitialFitRef.current)
|
|
143
161
|
return;
|
|
144
|
-
if (backgroundLayersOf(canvas.viewport).length === 0
|
|
162
|
+
if (backgroundLayersOf(canvas.viewport).length === 0 &&
|
|
163
|
+
!canvas.viewport.pdfDocument) {
|
|
145
164
|
return;
|
|
165
|
+
}
|
|
146
166
|
if (!(width > 0) || !(height > 0))
|
|
147
167
|
return;
|
|
148
168
|
didInitialFitRef.current = true;
|
|
@@ -290,7 +310,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
290
310
|
setViewport((v) => panBy(v, deltaScreen));
|
|
291
311
|
},
|
|
292
312
|
applyZoom(focalScreen, nextZoom) {
|
|
293
|
-
setViewport((v) => zoomAt(v, focalScreen, nextZoom));
|
|
313
|
+
setViewport((v) => zoomAt(v, focalScreen, nextZoom, zoomLimitsRef.current));
|
|
294
314
|
},
|
|
295
315
|
}), [
|
|
296
316
|
canvas,
|
|
@@ -396,7 +416,15 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
396
416
|
setViewport((v) => panBy(v, deltaScreen));
|
|
397
417
|
}, []);
|
|
398
418
|
const zoom = useCallback((focalScreen, nextZoom) => {
|
|
399
|
-
setViewport((v) => zoomAt(v, focalScreen, nextZoom));
|
|
419
|
+
setViewport((v) => zoomAt(v, focalScreen, nextZoom, zoomLimitsRef.current));
|
|
420
|
+
}, []);
|
|
421
|
+
// `v.zoom * factor` inside the updater, so a burst of events that React
|
|
422
|
+
// batches into one commit composes instead of each recomputing the same
|
|
423
|
+
// target from the same stale base. See `zoomBy` on the API type.
|
|
424
|
+
const zoomBy = useCallback((focalScreen, factor) => {
|
|
425
|
+
if (!Number.isFinite(factor) || factor <= 0)
|
|
426
|
+
return;
|
|
427
|
+
setViewport((v) => zoomAt(v, focalScreen, v.zoom * factor));
|
|
400
428
|
}, []);
|
|
401
429
|
// Imperative API mirror — set via prop so it survives WithSkiaWeb's lazy
|
|
402
430
|
// boundary on web.
|
|
@@ -862,6 +890,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
862
890
|
effectiveCanvas,
|
|
863
891
|
worldTransform,
|
|
864
892
|
viewport,
|
|
893
|
+
zoomMin,
|
|
865
894
|
measurementsById,
|
|
866
895
|
activeTool,
|
|
867
896
|
toolState,
|
|
@@ -879,6 +908,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
879
908
|
dispatchLongPress,
|
|
880
909
|
pan,
|
|
881
910
|
zoom,
|
|
911
|
+
zoomBy,
|
|
882
912
|
setViewport,
|
|
883
913
|
};
|
|
884
914
|
};
|
|
@@ -24,5 +24,13 @@ export declare const fitRectToScreen: (rect: {
|
|
|
24
24
|
width: number;
|
|
25
25
|
height: number;
|
|
26
26
|
}, screenWidth: number, screenHeight: number, padding?: number) => ViewportState;
|
|
27
|
-
export declare const
|
|
27
|
+
export declare const DEFAULT_ZOOM_LIMITS: {
|
|
28
|
+
readonly min: 0.05;
|
|
29
|
+
readonly max: 50;
|
|
30
|
+
};
|
|
31
|
+
export interface ZoomLimits {
|
|
32
|
+
min: number;
|
|
33
|
+
max: number;
|
|
34
|
+
}
|
|
35
|
+
export declare const zoomAt: (state: ViewportState, focalScreen: Vec2, nextZoom: number, limits?: ZoomLimits) => ViewportState;
|
|
28
36
|
export declare const panBy: (state: ViewportState, deltaScreen: Vec2) => ViewportState;
|
|
@@ -64,8 +64,13 @@ export const fitRectToScreen = (rect, screenWidth, screenHeight, padding = 0) =>
|
|
|
64
64
|
};
|
|
65
65
|
// Zoom toward a focal screen point so the world point under the cursor stays
|
|
66
66
|
// fixed. Used for wheel-zoom on web and pinch on native.
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
//
|
|
68
|
+
// `limits` defaults to the historical fixed bounds. A canvas whose document
|
|
69
|
+
// fits below the historical floor (a multi-page PDF filmstrip) passes its own
|
|
70
|
+
// floor — see computeZoomFloor in pdfDocument.ts.
|
|
71
|
+
export const DEFAULT_ZOOM_LIMITS = { min: 0.05, max: 50 };
|
|
72
|
+
export const zoomAt = (state, focalScreen, nextZoom, limits = DEFAULT_ZOOM_LIMITS) => {
|
|
73
|
+
const clampedZoom = Math.max(limits.min, Math.min(limits.max, nextZoom));
|
|
69
74
|
const focalWorld = {
|
|
70
75
|
x: focalScreen.x / state.zoom + state.pan.x,
|
|
71
76
|
y: focalScreen.y / state.zoom + state.pan.y,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AnnotationBackgroundLayer, type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationViewport, type BackgroundFit } from '../../../types/annotation.js';
|
|
1
|
+
import { type AnnotationBackgroundLayer, type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationPdfPageSize, type AnnotationViewport, type BackgroundFit } from '../../../types/annotation.js';
|
|
2
2
|
import type { AnnotationScope, ImageBlob } from '../AnnotationDataProvider.js';
|
|
3
3
|
export type SaveStatus = 'idle' | 'dirty' | 'saving' | 'saved' | 'error';
|
|
4
4
|
export interface UseAnnotationCanvasDocOptions {
|
|
@@ -30,6 +30,10 @@ export interface UseAnnotationCanvasDocResult {
|
|
|
30
30
|
width: number;
|
|
31
31
|
height: number;
|
|
32
32
|
}) => Promise<string>;
|
|
33
|
+
addPdfDocument: (blob: ImageBlob, meta: {
|
|
34
|
+
pages: AnnotationPdfPageSize[];
|
|
35
|
+
fileName?: string;
|
|
36
|
+
}) => Promise<string>;
|
|
33
37
|
updateBackgroundImage: (id: string, patch: Partial<Omit<AnnotationBackgroundLayer, 'id'>>) => void;
|
|
34
38
|
removeBackgroundImage: (id: string) => Promise<void>;
|
|
35
39
|
setBackgroundImage: (blob: ImageBlob, dims: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { applyPatch, createEmptyCanvasState, } from '../../../types/annotation.js';
|
|
3
|
+
import { createPdfDocument, makePdfDocumentId, pdfDocumentExtent, } from '../../canvas/pdfDocument.js';
|
|
3
4
|
import { backgroundLayersOf, placeNewBackgroundLayer, defaultBackgroundLayerLocked, } from '../../canvas/backgroundLayers.js';
|
|
4
5
|
import { imageDocRect } from '../../canvas/viewport.js';
|
|
5
6
|
import { FileUploadType } from '../../../types/firestore.js';
|
|
@@ -395,6 +396,30 @@ export const useAnnotationCanvasDoc = (options) => {
|
|
|
395
396
|
});
|
|
396
397
|
return layerId;
|
|
397
398
|
}, [ensureFileId, uploadImage, onCommit, fallbackViewport]);
|
|
399
|
+
const addPdfDocument = useCallback(async (blob, meta) => {
|
|
400
|
+
if (meta.pages.length === 0) {
|
|
401
|
+
throw new Error('Cannot attach a PDF with no pages');
|
|
402
|
+
}
|
|
403
|
+
const fileId = await ensureFileId();
|
|
404
|
+
const id = makePdfDocumentId();
|
|
405
|
+
const ref = await uploadImage(fileId, `backgrounds/${id}`, blob);
|
|
406
|
+
const document = createPdfDocument({
|
|
407
|
+
id,
|
|
408
|
+
storagePath: ref.storagePath,
|
|
409
|
+
pages: meta.pages,
|
|
410
|
+
fileName: meta.fileName,
|
|
411
|
+
});
|
|
412
|
+
// One patch, two ops: the extent must land WITH the document so no
|
|
413
|
+
// render sees pages laid out past the doc rect (or a doc rect with no
|
|
414
|
+
// pages behind it). Undo reverts both together.
|
|
415
|
+
onCommit({
|
|
416
|
+
ops: [
|
|
417
|
+
{ op: 'setPdfDocument', document },
|
|
418
|
+
{ op: 'setViewport', patch: pdfDocumentExtent(document) },
|
|
419
|
+
],
|
|
420
|
+
});
|
|
421
|
+
return id;
|
|
422
|
+
}, [ensureFileId, uploadImage, onCommit]);
|
|
398
423
|
const updateBackgroundImage = useCallback((id, patch) => {
|
|
399
424
|
onCommit({ ops: [{ op: 'updateBackgroundImage', id, patch }] });
|
|
400
425
|
}, [onCommit]);
|
|
@@ -521,6 +546,7 @@ export const useAnnotationCanvasDoc = (options) => {
|
|
|
521
546
|
refreshThumbnail,
|
|
522
547
|
ensureFileId,
|
|
523
548
|
addBackgroundImage,
|
|
549
|
+
addPdfDocument,
|
|
524
550
|
updateBackgroundImage,
|
|
525
551
|
removeBackgroundImage,
|
|
526
552
|
setBackgroundImage,
|
package/dist/exports.d.ts
CHANGED
|
@@ -31,9 +31,11 @@ export type { GestureConfig, PanTrigger, AnnotationCanvasInnerProps, } from './a
|
|
|
31
31
|
export type { CanvasPointerEvent, RequestTextInput, ShapeDrawConfig, Tool, ToolContext, ToolState, } from './annotation/canvas/Tool.js';
|
|
32
32
|
export type { FileRef, MeasurementRef, PickMeasurement, } from './annotation/canvas/measurementPicker.js';
|
|
33
33
|
export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './annotation/canvas/measurementStampOverlay.js';
|
|
34
|
+
export type { CanvasBackgroundSlotArgs, RenderCanvasBackground, } from './annotation/canvas/backgroundSlot.js';
|
|
34
35
|
export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
|
|
35
36
|
export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, type GroupHeaderBadge, type GroupHeaderDescriptor, } from './annotation/canvas/groupHeaderLayout.js';
|
|
36
|
-
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
|
|
37
|
+
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, type ViewportApi, type ViewportState, type ZoomLimits, } from './annotation/canvas/viewport.js';
|
|
38
|
+
export { DEFAULT_PDF_PAGE_GAP, DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM, makePdfDocumentId, createPdfDocument, pdfPageDocRects, pdfDocumentExtent, pdfFirstPageDocRect, computeZoomFloor, type PdfDocRect, } from './annotation/canvas/pdfDocument.js';
|
|
37
39
|
export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerIsLocked, isBackgroundLayerLocked, defaultBackgroundLayerLocked, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
|
|
38
40
|
export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
|
|
39
41
|
export { createSelectTool, type SelectToolOptions, } from './annotation/canvas/tools/selectTool.js';
|
package/dist/exports.js
CHANGED
|
@@ -38,7 +38,8 @@ export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
|
|
|
38
38
|
export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
|
|
39
39
|
export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, } from './annotation/canvas/stampLayout.js';
|
|
40
40
|
export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, } from './annotation/canvas/groupHeaderLayout.js';
|
|
41
|
-
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
|
|
41
|
+
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './annotation/canvas/viewport.js';
|
|
42
|
+
export { DEFAULT_PDF_PAGE_GAP, DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM, makePdfDocumentId, createPdfDocument, pdfPageDocRects, pdfDocumentExtent, pdfFirstPageDocRect, computeZoomFloor, } from './annotation/canvas/pdfDocument.js';
|
|
42
43
|
export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerIsLocked, isBackgroundLayerLocked, defaultBackgroundLayerLocked, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
|
|
43
44
|
export { createPenTool, } from './annotation/canvas/tools/penTool.js';
|
|
44
45
|
export { createSelectTool, } from './annotation/canvas/tools/selectTool.js';
|
|
@@ -145,12 +145,27 @@ export interface AnnotationBackgroundLayer {
|
|
|
145
145
|
transform: BackgroundLayerTransform;
|
|
146
146
|
locked?: boolean;
|
|
147
147
|
}
|
|
148
|
+
export interface AnnotationPdfPageSize {
|
|
149
|
+
width: number;
|
|
150
|
+
height: number;
|
|
151
|
+
}
|
|
152
|
+
export type AnnotationPdfLayout = 'continuousVertical';
|
|
153
|
+
export interface AnnotationPdfDocument {
|
|
154
|
+
id: AnnotationElementId;
|
|
155
|
+
storagePath: string;
|
|
156
|
+
fileName?: string;
|
|
157
|
+
pageCount: number;
|
|
158
|
+
pages: AnnotationPdfPageSize[];
|
|
159
|
+
layout: AnnotationPdfLayout;
|
|
160
|
+
gap: number;
|
|
161
|
+
}
|
|
148
162
|
export interface AnnotationViewport {
|
|
149
163
|
width: number;
|
|
150
164
|
height: number;
|
|
151
165
|
backgroundImage?: AnnotationBackgroundImage;
|
|
152
166
|
backgroundFit?: BackgroundFit;
|
|
153
167
|
backgroundImages?: AnnotationBackgroundLayer[];
|
|
168
|
+
pdfDocument?: AnnotationPdfDocument;
|
|
154
169
|
}
|
|
155
170
|
export type TileScalePlatform = 'web' | 'mobile';
|
|
156
171
|
export type TileScaleTarget = 'measurement' | 'header' | 'file';
|
|
@@ -225,6 +240,9 @@ export type AnnotationPatchOp = {
|
|
|
225
240
|
} | {
|
|
226
241
|
op: 'setViewport';
|
|
227
242
|
patch: Partial<AnnotationViewport>;
|
|
243
|
+
} | {
|
|
244
|
+
op: 'setPdfDocument';
|
|
245
|
+
document: AnnotationPdfDocument | null;
|
|
228
246
|
} | {
|
|
229
247
|
op: 'setTileScaleFactor';
|
|
230
248
|
value: number;
|