@reekon-tools/boldr-utils 1.23.0 → 1.25.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/pdfCanvasPages.d.ts +25 -0
- package/dist/annotation/canvas/pdfCanvasPages.js +255 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +11 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +17 -1
- package/dist/annotation/canvas/viewport.d.ts +10 -0
- package/dist/annotation/canvas/viewport.js +26 -0
- package/dist/annotation/data/AnnotationDataProvider.d.ts +6 -0
- package/dist/annotation/data/InMemoryAnnotationProvider.d.ts +6 -0
- package/dist/annotation/data/InMemoryAnnotationProvider.js +25 -0
- package/dist/annotation/data/hooks/usePdfCanvasDoc.d.ts +29 -0
- package/dist/annotation/data/hooks/usePdfCanvasDoc.js +353 -0
- package/dist/export/pdf/index.d.ts +2 -2
- package/dist/export/pdf/index.js +1 -1
- package/dist/export/pdf/renderCanvasToPdf.d.ts +9 -1
- package/dist/export/pdf/renderCanvasToPdf.js +209 -113
- package/dist/export/pdf/types.d.ts +13 -0
- package/dist/exports.d.ts +4 -1
- package/dist/exports.js +4 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -0
- package/dist/types/firestore.d.ts +6 -0
- package/dist/types/firestore.js +5 -0
- package/dist/types/pdfCanvas.d.ts +38 -0
- package/dist/types/pdfCanvas.js +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type AnnotationCanvasState, type AnnotationPdfDocument, type Vec2 } from '../../types/annotation.js';
|
|
2
|
+
import type { PdfCanvasData, PdfCanvasPage, PdfCanvasPageState, PdfCanvasRev, PdfPageRotation } from '../../types/pdfCanvas.js';
|
|
3
|
+
export interface PdfPageDisplaySize {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const normalizePdfRotation: (deg: number) => PdfPageRotation;
|
|
8
|
+
export declare const effectivePageRotation: (intrinsicDeg: number, user: PdfPageRotation) => PdfPageRotation;
|
|
9
|
+
export declare const pdfPageDisplaySize: (page: Pick<PdfCanvasPage, "width" | "height" | "rotation">) => PdfPageDisplaySize;
|
|
10
|
+
export declare const pdfCanvasPageDocId: (pageIndex: number) => string;
|
|
11
|
+
export declare const emptyPdfCanvasPageState: (pageIndex: number) => PdfCanvasPageState;
|
|
12
|
+
export declare const pdfCanvasPageDocument: (data: PdfCanvasData, pageIndex: number) => AnnotationPdfDocument;
|
|
13
|
+
type PdfCanvasKnobs = Pick<PdfCanvasData, 'tileScaleFactor' | 'tileScaleFactorMobile' | 'headerTileScaleFactor' | 'headerTileScaleFactorMobile' | 'tileDocSize'>;
|
|
14
|
+
export declare const pdfCanvasKnobsOf: (source: Partial<PdfCanvasKnobs>) => PdfCanvasKnobs;
|
|
15
|
+
export declare const pdfCanvasKnobsEqual: (a: Partial<PdfCanvasKnobs>, b: Partial<PdfCanvasKnobs>) => boolean;
|
|
16
|
+
export declare const pageStateToCanvasState: (data: PdfCanvasData, pageState: PdfCanvasPageState | null, pageIndex: number) => AnnotationCanvasState;
|
|
17
|
+
export declare const canvasStateToPageState: (canvas: AnnotationCanvasState, pageIndex: number, stamp: {
|
|
18
|
+
updatedAt: number;
|
|
19
|
+
rev?: PdfCanvasRev;
|
|
20
|
+
}) => PdfCanvasPageState;
|
|
21
|
+
export type PdfRotationDelta = 90 | -90 | 180;
|
|
22
|
+
export declare const rotatePointInPage: (p: Vec2, size: PdfPageDisplaySize, delta: PdfRotationDelta) => Vec2;
|
|
23
|
+
export declare const rotatePageState: <T extends Pick<PdfCanvasPageState, "strokes" | "shapes" | "placedMeasurements" | "planes">>(state: T, currentSize: PdfPageDisplaySize, delta: PdfRotationDelta) => T;
|
|
24
|
+
export declare const rotatePdfCanvasPage: (page: PdfCanvasPage, delta: PdfRotationDelta) => PdfCanvasPage;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// Pure helpers for PDF canvas files (see types/pdfCanvas.ts): page geometry
|
|
2
|
+
// under user rotation, the mapping between a persisted page doc and the
|
|
3
|
+
// AnnotationCanvasState the engine mounts, and the coordinate transform that
|
|
4
|
+
// turns a page's annotations with it when the page is rotated.
|
|
5
|
+
import { createDefaultLayer, createEmptyCanvasState, } from '../../types/annotation.js';
|
|
6
|
+
import { textShapeBounds } from './textGeometry.js';
|
|
7
|
+
// Snap any degree value onto the four legal rotations, modulo 360.
|
|
8
|
+
export const normalizePdfRotation = (deg) => {
|
|
9
|
+
const snapped = (((Math.round(deg / 90) * 90) % 360) + 360) % 360;
|
|
10
|
+
return snapped;
|
|
11
|
+
};
|
|
12
|
+
// The rotation a viewer must apply to the ORIGINAL page (its own /Rotate plus
|
|
13
|
+
// the user's) — what the export writes into /Rotate.
|
|
14
|
+
export const effectivePageRotation = (intrinsicDeg, user) => normalizePdfRotation(intrinsicDeg + user);
|
|
15
|
+
// A page's displayed extents: its stored size with width/height swapped at
|
|
16
|
+
// 90/270. This IS the page's canvas doc space.
|
|
17
|
+
export const pdfPageDisplaySize = (page) => page.rotation === 90 || page.rotation === 270
|
|
18
|
+
? { width: page.height, height: page.width }
|
|
19
|
+
: { width: page.width, height: page.height };
|
|
20
|
+
// Firestore doc id of a page under `files/{fileId}/pages`. Plain index: the
|
|
21
|
+
// set of existing pages comes from `annotatedPages`, never from listing.
|
|
22
|
+
export const pdfCanvasPageDocId = (pageIndex) => String(pageIndex);
|
|
23
|
+
export const emptyPdfCanvasPageState = (pageIndex) => ({
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
pageIndex,
|
|
26
|
+
layers: [createDefaultLayer()],
|
|
27
|
+
strokes: [],
|
|
28
|
+
shapes: [],
|
|
29
|
+
placedMeasurements: [],
|
|
30
|
+
updatedAt: 0,
|
|
31
|
+
});
|
|
32
|
+
const requirePage = (data, pageIndex) => {
|
|
33
|
+
const page = data.document.pages[pageIndex];
|
|
34
|
+
if (!page) {
|
|
35
|
+
throw new RangeError(`PDF canvas has ${data.document.pages.length} pages; asked for ${pageIndex}`);
|
|
36
|
+
}
|
|
37
|
+
return page;
|
|
38
|
+
};
|
|
39
|
+
// The one-page substrate descriptor a page canvas carries. pdfPageDocRects of
|
|
40
|
+
// it is exactly {0, 0, w, h}, so every consumer of `viewport.pdfDocument` (fit,
|
|
41
|
+
// zoom floor, tile-scale reference, export matrix) sees the displayed page.
|
|
42
|
+
export const pdfCanvasPageDocument = (data, pageIndex) => {
|
|
43
|
+
const size = pdfPageDisplaySize(requirePage(data, pageIndex));
|
|
44
|
+
return {
|
|
45
|
+
id: `pdf-page-${pageIndex}`,
|
|
46
|
+
storagePath: data.document.storagePath,
|
|
47
|
+
...(data.document.fileName ? { fileName: data.document.fileName } : {}),
|
|
48
|
+
pageCount: 1,
|
|
49
|
+
pages: [size],
|
|
50
|
+
layout: 'continuousVertical',
|
|
51
|
+
gap: 0,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const KNOB_KEYS = [
|
|
55
|
+
'tileScaleFactor',
|
|
56
|
+
'tileScaleFactorMobile',
|
|
57
|
+
'headerTileScaleFactor',
|
|
58
|
+
'headerTileScaleFactorMobile',
|
|
59
|
+
'tileDocSize',
|
|
60
|
+
];
|
|
61
|
+
// The file-level tile-scale knobs present on a canvas (undefined keys omitted).
|
|
62
|
+
export const pdfCanvasKnobsOf = (source) => {
|
|
63
|
+
const out = {};
|
|
64
|
+
for (const key of KNOB_KEYS) {
|
|
65
|
+
const value = source[key];
|
|
66
|
+
if (value !== undefined)
|
|
67
|
+
out[key] = value;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
};
|
|
71
|
+
export const pdfCanvasKnobsEqual = (a, b) => KNOB_KEYS.every((key) => a[key] === b[key]);
|
|
72
|
+
// The AnnotationCanvasState the editor mounts for one page. A null page state
|
|
73
|
+
// (never annotated) yields an empty canvas over the page.
|
|
74
|
+
export const pageStateToCanvasState = (data, pageState, pageIndex) => {
|
|
75
|
+
const doc = pdfCanvasPageDocument(data, pageIndex);
|
|
76
|
+
const size = doc.pages[0];
|
|
77
|
+
const base = createEmptyCanvasState({
|
|
78
|
+
width: size.width,
|
|
79
|
+
height: size.height,
|
|
80
|
+
pdfDocument: doc,
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
...base,
|
|
84
|
+
...pdfCanvasKnobsOf(data),
|
|
85
|
+
layers: pageState && pageState.layers.length > 0 ? pageState.layers : base.layers,
|
|
86
|
+
strokes: pageState?.strokes ?? [],
|
|
87
|
+
shapes: pageState?.shapes ?? [],
|
|
88
|
+
placedMeasurements: pageState?.placedMeasurements ?? [],
|
|
89
|
+
...(pageState?.planes !== undefined ? { planes: pageState.planes } : {}),
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
// Inverse of pageStateToCanvasState: the elements to persist for one page.
|
|
93
|
+
// Viewport and pdfDocument are derived and never stored on the page doc.
|
|
94
|
+
export const canvasStateToPageState = (canvas, pageIndex, stamp) => ({
|
|
95
|
+
schemaVersion: 1,
|
|
96
|
+
pageIndex,
|
|
97
|
+
layers: canvas.layers,
|
|
98
|
+
strokes: canvas.strokes,
|
|
99
|
+
shapes: canvas.shapes,
|
|
100
|
+
placedMeasurements: canvas.placedMeasurements,
|
|
101
|
+
...(canvas.planes !== undefined ? { planes: canvas.planes } : {}),
|
|
102
|
+
updatedAt: stamp.updatedAt,
|
|
103
|
+
...(stamp.rev ? { rev: stamp.rev } : {}),
|
|
104
|
+
});
|
|
105
|
+
export const rotatePointInPage = (p, size, delta) => {
|
|
106
|
+
switch (normalizePdfRotation(delta)) {
|
|
107
|
+
case 90:
|
|
108
|
+
return { x: size.height - p.y, y: p.x };
|
|
109
|
+
case 270:
|
|
110
|
+
return { x: p.y, y: size.width - p.x };
|
|
111
|
+
case 180:
|
|
112
|
+
return { x: size.width - p.x, y: size.height - p.y };
|
|
113
|
+
default:
|
|
114
|
+
return p;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const rotateFlatPoints = (points, size, delta) => {
|
|
118
|
+
const out = new Array(points.length);
|
|
119
|
+
for (let i = 0; i + 1 < points.length; i += 2) {
|
|
120
|
+
const p = rotatePointInPage({ x: points[i], y: points[i + 1] }, size, delta);
|
|
121
|
+
out[i] = p.x;
|
|
122
|
+
out[i + 1] = p.y;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
};
|
|
126
|
+
const rotateStroke = (stroke, size, delta) => ({
|
|
127
|
+
...stroke,
|
|
128
|
+
points: rotateFlatPoints(stroke.points, size, delta),
|
|
129
|
+
});
|
|
130
|
+
const rotateShape = (shape, size, delta) => {
|
|
131
|
+
if (shape.kind === 'text') {
|
|
132
|
+
const bounds = textShapeBounds(shape);
|
|
133
|
+
const origin = shape.geometry.points[0];
|
|
134
|
+
if (bounds && origin) {
|
|
135
|
+
const width = bounds.maxX - bounds.minX;
|
|
136
|
+
const height = bounds.maxY - bounds.minY;
|
|
137
|
+
const centre = rotatePointInPage({ x: bounds.minX + width / 2, y: bounds.minY + height / 2 }, size, delta);
|
|
138
|
+
return {
|
|
139
|
+
...shape,
|
|
140
|
+
geometry: {
|
|
141
|
+
...shape.geometry,
|
|
142
|
+
points: [
|
|
143
|
+
{ x: centre.x - width / 2, y: centre.y - height / 2 },
|
|
144
|
+
...shape.geometry.points.slice(1),
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
...shape,
|
|
152
|
+
geometry: {
|
|
153
|
+
...shape.geometry,
|
|
154
|
+
points: shape.geometry.points.map((p) => rotatePointInPage(p, size, delta)),
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
// Where each rectangle edge lands after the turn — used for the perspective
|
|
159
|
+
// plane's dimensioned-edge provenance and for the dimension tiles' slots.
|
|
160
|
+
const rotateEdge = (edge, delta) => {
|
|
161
|
+
if (edge === 'ref')
|
|
162
|
+
return edge;
|
|
163
|
+
const order = ['top', 'right', 'bottom', 'left'];
|
|
164
|
+
const steps = normalizePdfRotation(delta) / 90;
|
|
165
|
+
return order[(order.indexOf(edge) + steps) % 4];
|
|
166
|
+
};
|
|
167
|
+
const rotatePlaced = (placed, size, delta) => {
|
|
168
|
+
const r = (p) => rotatePointInPage(p, size, delta);
|
|
169
|
+
return {
|
|
170
|
+
...placed,
|
|
171
|
+
anchor: r(placed.anchor),
|
|
172
|
+
...(placed.line
|
|
173
|
+
? { line: { a: r(placed.line.a), b: r(placed.line.b) } }
|
|
174
|
+
: {}),
|
|
175
|
+
...(placed.rect
|
|
176
|
+
? { rect: { a: r(placed.rect.a), b: r(placed.rect.b) } }
|
|
177
|
+
: {}),
|
|
178
|
+
...(placed.leader
|
|
179
|
+
? { leader: { from: r(placed.leader.from), to: r(placed.leader.to) } }
|
|
180
|
+
: {}),
|
|
181
|
+
...(placed.planeEdge
|
|
182
|
+
? { planeEdge: rotateEdge(placed.planeEdge, delta) }
|
|
183
|
+
: {}),
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
const rotatePlane = (plane, size, delta) => {
|
|
187
|
+
const r = (p) => rotatePointInPage(p, size, delta);
|
|
188
|
+
if (plane.mode === 'scale') {
|
|
189
|
+
return {
|
|
190
|
+
...plane,
|
|
191
|
+
refLine: { a: r(plane.refLine.a), b: r(plane.refLine.b) },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// Corners stay in TL, TR, BR, BL order: after a clockwise quarter turn the
|
|
195
|
+
// old bottom-left corner is the new top-left, so the array rotates by one
|
|
196
|
+
// slot per quarter turn.
|
|
197
|
+
const steps = normalizePdfRotation(delta) / 90;
|
|
198
|
+
const turned = plane.corners.map(r);
|
|
199
|
+
const corners = [0, 1, 2, 3].map((i) => turned[(i - steps + 4) % 4]);
|
|
200
|
+
const quarter = steps % 2 === 1;
|
|
201
|
+
const widthEdge = plane.heightEdge
|
|
202
|
+
? rotateEdge(plane.heightEdge, delta)
|
|
203
|
+
: undefined;
|
|
204
|
+
const heightEdge = plane.widthEdge
|
|
205
|
+
? rotateEdge(plane.widthEdge, delta)
|
|
206
|
+
: undefined;
|
|
207
|
+
return {
|
|
208
|
+
...plane,
|
|
209
|
+
corners,
|
|
210
|
+
// A quarter turn makes the horizontal dimension vertical and vice versa.
|
|
211
|
+
...(quarter
|
|
212
|
+
? {
|
|
213
|
+
...(plane.heightUm !== undefined
|
|
214
|
+
? { widthUm: plane.heightUm }
|
|
215
|
+
: { widthUm: undefined }),
|
|
216
|
+
...(plane.widthUm !== undefined
|
|
217
|
+
? { heightUm: plane.widthUm }
|
|
218
|
+
: { heightUm: undefined }),
|
|
219
|
+
...(widthEdge === 'top' || widthEdge === 'bottom'
|
|
220
|
+
? { widthEdge }
|
|
221
|
+
: { widthEdge: undefined }),
|
|
222
|
+
...(heightEdge === 'left' || heightEdge === 'right'
|
|
223
|
+
? { heightEdge }
|
|
224
|
+
: { heightEdge: undefined }),
|
|
225
|
+
}
|
|
226
|
+
: {
|
|
227
|
+
...(plane.widthEdge
|
|
228
|
+
? {
|
|
229
|
+
widthEdge: rotateEdge(plane.widthEdge, delta),
|
|
230
|
+
}
|
|
231
|
+
: {}),
|
|
232
|
+
...(plane.heightEdge
|
|
233
|
+
? {
|
|
234
|
+
heightEdge: rotateEdge(plane.heightEdge, delta),
|
|
235
|
+
}
|
|
236
|
+
: {}),
|
|
237
|
+
}),
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
// Rewrite every coordinate of a page's state from its current displayed size
|
|
241
|
+
// into the size it will have after turning by `delta`.
|
|
242
|
+
export const rotatePageState = (state, currentSize, delta) => ({
|
|
243
|
+
...state,
|
|
244
|
+
strokes: state.strokes.map((s) => rotateStroke(s, currentSize, delta)),
|
|
245
|
+
shapes: state.shapes.map((s) => rotateShape(s, currentSize, delta)),
|
|
246
|
+
placedMeasurements: state.placedMeasurements.map((p) => rotatePlaced(p, currentSize, delta)),
|
|
247
|
+
...(state.planes !== undefined
|
|
248
|
+
? { planes: state.planes.map((p) => rotatePlane(p, currentSize, delta)) }
|
|
249
|
+
: {}),
|
|
250
|
+
});
|
|
251
|
+
// The page record after turning by `delta`.
|
|
252
|
+
export const rotatePdfCanvasPage = (page, delta) => ({
|
|
253
|
+
...page,
|
|
254
|
+
rotation: normalizePdfRotation(page.rotation + delta),
|
|
255
|
+
});
|
|
@@ -23,6 +23,17 @@ export interface AnnotationCanvasHandle {
|
|
|
23
23
|
animated?: boolean;
|
|
24
24
|
durationMs?: number;
|
|
25
25
|
}): void;
|
|
26
|
+
fitDocRect(rect: {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
}, opts?: {
|
|
32
|
+
animated?: boolean;
|
|
33
|
+
durationMs?: number;
|
|
34
|
+
paddingPx?: number;
|
|
35
|
+
maxZoom?: number;
|
|
36
|
+
}): void;
|
|
26
37
|
getTileScaleFactor(): number;
|
|
27
38
|
getHeaderTileScaleFactor(): number;
|
|
28
39
|
getFileTileScaleFactor(): number;
|
|
@@ -2,7 +2,7 @@ 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
4
|
import { SNAP_RADIUS_PX } from './snapGeometry.js';
|
|
5
|
-
import { createViewportApi, fitRectToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './viewport.js';
|
|
5
|
+
import { createViewportApi, fitDocRectViewport, fitRectToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './viewport.js';
|
|
6
6
|
import { computeZoomFloor, pdfFirstPageDocRect } from './pdfDocument.js';
|
|
7
7
|
import { backgroundLayerDocRect, backgroundLayersOf, backgroundLayersUnionRect, } from './backgroundLayers.js';
|
|
8
8
|
import { buildRemoveMeasurementOps, recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
|
|
@@ -555,6 +555,22 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
555
555
|
setViewport(target);
|
|
556
556
|
}
|
|
557
557
|
},
|
|
558
|
+
fitDocRect(rect, opts) {
|
|
559
|
+
if (!(width > 0) || !(height > 0))
|
|
560
|
+
return;
|
|
561
|
+
const limits = zoomLimitsRef.current;
|
|
562
|
+
const target = fitDocRectViewport(rect, width, height, {
|
|
563
|
+
paddingPx: opts?.paddingPx,
|
|
564
|
+
minZoom: limits.min,
|
|
565
|
+
maxZoom: Math.min(opts?.maxZoom ?? limits.max, limits.max),
|
|
566
|
+
});
|
|
567
|
+
if (opts?.animated && animateViewport) {
|
|
568
|
+
animateViewport(target, opts.durationMs ?? 450);
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
setViewport(target);
|
|
572
|
+
}
|
|
573
|
+
},
|
|
558
574
|
getTileScaleFactor() {
|
|
559
575
|
return ctxRef.current.tileScaleFactor;
|
|
560
576
|
},
|
|
@@ -24,6 +24,16 @@ 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 fitDocRectViewport: (rect: {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
}, screenWidth: number, screenHeight: number, limits?: {
|
|
33
|
+
paddingPx?: number;
|
|
34
|
+
minZoom?: number;
|
|
35
|
+
maxZoom?: number;
|
|
36
|
+
}) => ViewportState;
|
|
27
37
|
export declare const DEFAULT_ZOOM_LIMITS: {
|
|
28
38
|
readonly min: 0.05;
|
|
29
39
|
readonly max: 50;
|
|
@@ -62,6 +62,32 @@ export const fitRectToScreen = (rect, screenWidth, screenHeight, padding = 0) =>
|
|
|
62
62
|
pan: { x: rect.x - offsetX / zoom, y: rect.y - offsetY / zoom },
|
|
63
63
|
};
|
|
64
64
|
};
|
|
65
|
+
// Frame a DOC-space rect (a text-search hit, a tile) centred in the screen:
|
|
66
|
+
// zoom so the rect plus `paddingPx` of screen padding fits, clamped to
|
|
67
|
+
// [minZoom, maxZoom] — so a two-word hit does not zoom to 50× and a whole-page
|
|
68
|
+
// rect cannot go below the document's floor — then centre the rect at that
|
|
69
|
+
// zoom (screen = (world − pan) · zoom, so pan = centre − half-screen / zoom).
|
|
70
|
+
export const fitDocRectViewport = (rect, screenWidth, screenHeight, limits = {}) => {
|
|
71
|
+
const safe = {
|
|
72
|
+
x: rect.x,
|
|
73
|
+
y: rect.y,
|
|
74
|
+
width: Math.max(1, rect.width),
|
|
75
|
+
height: Math.max(1, rect.height),
|
|
76
|
+
};
|
|
77
|
+
const fit = fitRectToScreen(safe, screenWidth, screenHeight, limits.paddingPx ?? 0);
|
|
78
|
+
const upper = limits.maxZoom ?? Number.POSITIVE_INFINITY;
|
|
79
|
+
const lower = limits.minZoom ?? 0;
|
|
80
|
+
const zoom = Math.max(lower, Math.min(fit.zoom, upper));
|
|
81
|
+
const cx = safe.x + safe.width / 2;
|
|
82
|
+
const cy = safe.y + safe.height / 2;
|
|
83
|
+
return {
|
|
84
|
+
zoom,
|
|
85
|
+
pan: {
|
|
86
|
+
x: cx - screenWidth / (2 * zoom),
|
|
87
|
+
y: cy - screenHeight / (2 * zoom),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
};
|
|
65
91
|
// Zoom toward a focal screen point so the world point under the cursor stays
|
|
66
92
|
// fixed. Used for wheel-zoom on web and pinch on native.
|
|
67
93
|
//
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FileUpload, FileUploadType, Measurement } from '../../types/firestore.js';
|
|
2
|
+
import type { PdfCanvasPageState } from '../../types/pdfCanvas.js';
|
|
2
3
|
export interface JobScope {
|
|
3
4
|
orgId: string;
|
|
4
5
|
projectId: string;
|
|
@@ -56,6 +57,8 @@ export interface UploadedImageRef {
|
|
|
56
57
|
}
|
|
57
58
|
export type AnnotationFile = Extract<FileUpload, {
|
|
58
59
|
type: FileUploadType.Canvas;
|
|
60
|
+
} | {
|
|
61
|
+
type: FileUploadType.PdfCanvas;
|
|
59
62
|
}>;
|
|
60
63
|
export interface AnnotationFileSummary {
|
|
61
64
|
id: string;
|
|
@@ -78,4 +81,7 @@ export interface AnnotationDataProvider {
|
|
|
78
81
|
uploadImage(scope: AnnotationScope, fileId: string, role: string, blob: ImageBlob): Promise<UploadedImageRef>;
|
|
79
82
|
getImageUrl(scope: AnnotationScope, fileId: string, storagePath: string): Promise<string>;
|
|
80
83
|
deleteImage(scope: AnnotationScope, fileId: string, storagePath: string): Promise<void>;
|
|
84
|
+
getPage(scope: AnnotationScope, fileId: string, pageIndex: number): Promise<PdfCanvasPageState | null>;
|
|
85
|
+
subscribePage(scope: AnnotationScope, fileId: string, pageIndex: number, onNext: (page: PdfCanvasPageState | null) => void, onError?: (err: Error) => void): Unsubscribe;
|
|
86
|
+
writePage(scope: AnnotationScope, fileId: string, pageIndex: number, state: PdfCanvasPageState): Promise<void>;
|
|
81
87
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Measurement } from '../../types/firestore.js';
|
|
2
|
+
import type { PdfCanvasPageState } from '../../types/pdfCanvas.js';
|
|
2
3
|
import { type AnnotationDataProvider, type AnnotationFile, type AnnotationFilePatch, type AnnotationFileSummary, type AnnotationScope, type ImageBlob, type JobGroupScope, type JobScope, type Unsubscribe, type UploadedImageRef } from './AnnotationDataProvider.js';
|
|
3
4
|
export declare class InMemoryAnnotationProvider implements AnnotationDataProvider {
|
|
4
5
|
private docs;
|
|
@@ -8,6 +9,8 @@ export declare class InMemoryAnnotationProvider implements AnnotationDataProvide
|
|
|
8
9
|
private listListeners;
|
|
9
10
|
private groupMeasurementListeners;
|
|
10
11
|
private jobMeasurementListeners;
|
|
12
|
+
private pages;
|
|
13
|
+
private pageListeners;
|
|
11
14
|
private nextId;
|
|
12
15
|
setMeasurements(scope: JobGroupScope, measurements: Measurement[]): void;
|
|
13
16
|
create(scope: AnnotationScope, seed: Partial<AnnotationFile>): Promise<string>;
|
|
@@ -21,6 +24,9 @@ export declare class InMemoryAnnotationProvider implements AnnotationDataProvide
|
|
|
21
24
|
uploadImage(scope: AnnotationScope, fileId: string, role: string, blob: ImageBlob): Promise<UploadedImageRef>;
|
|
22
25
|
getImageUrl(_scope: AnnotationScope, fileId: string, storagePath: string): Promise<string>;
|
|
23
26
|
deleteImage(_scope: AnnotationScope, fileId: string, storagePath: string): Promise<void>;
|
|
27
|
+
getPage(scope: AnnotationScope, fileId: string, pageIndex: number): Promise<PdfCanvasPageState | null>;
|
|
28
|
+
subscribePage(scope: AnnotationScope, fileId: string, pageIndex: number, onNext: (page: PdfCanvasPageState | null) => void): Unsubscribe;
|
|
29
|
+
writePage(scope: AnnotationScope, fileId: string, pageIndex: number, state: PdfCanvasPageState): Promise<void>;
|
|
24
30
|
private getBucket;
|
|
25
31
|
private notifyDoc;
|
|
26
32
|
private notifyList;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { FileUploadType } from '../../types/firestore.js';
|
|
2
|
+
import { pdfCanvasPageDocId } from '../canvas/pdfCanvasPages.js';
|
|
2
3
|
import { annotationScopeKey, isFieldOp, } from './AnnotationDataProvider.js';
|
|
3
4
|
const scopeKey = annotationScopeKey;
|
|
4
5
|
// Prefix of annotationScopeKey for job scopes, so collectJobMeasurements can
|
|
5
6
|
// match group keys by startsWith.
|
|
6
7
|
const jobKey = (s) => `job/${s.orgId}/${s.projectId}/${s.jobId}`;
|
|
8
|
+
const pageKey = (s, fileId, pageIndex) => `${scopeKey(s)}/${fileId}/pages/${pdfCanvasPageDocId(pageIndex)}`;
|
|
7
9
|
const summarize = (file) => ({
|
|
8
10
|
id: file.id,
|
|
9
11
|
name: file.name,
|
|
@@ -75,6 +77,8 @@ export class InMemoryAnnotationProvider {
|
|
|
75
77
|
this.listListeners = new Map();
|
|
76
78
|
this.groupMeasurementListeners = new Map();
|
|
77
79
|
this.jobMeasurementListeners = new Map();
|
|
80
|
+
this.pages = new Map();
|
|
81
|
+
this.pageListeners = new Map();
|
|
78
82
|
this.nextId = 1;
|
|
79
83
|
}
|
|
80
84
|
// Seed helpers for tests
|
|
@@ -182,6 +186,27 @@ export class InMemoryAnnotationProvider {
|
|
|
182
186
|
async deleteImage(_scope, fileId, storagePath) {
|
|
183
187
|
this.images.get(fileId)?.delete(storagePath);
|
|
184
188
|
}
|
|
189
|
+
async getPage(scope, fileId, pageIndex) {
|
|
190
|
+
return this.pages.get(pageKey(scope, fileId, pageIndex)) ?? null;
|
|
191
|
+
}
|
|
192
|
+
subscribePage(scope, fileId, pageIndex, onNext) {
|
|
193
|
+
const key = pageKey(scope, fileId, pageIndex);
|
|
194
|
+
const set = this.pageListeners.get(key) ?? new Set();
|
|
195
|
+
set.add(onNext);
|
|
196
|
+
this.pageListeners.set(key, set);
|
|
197
|
+
queueMicrotask(() => onNext(this.pages.get(key) ?? null));
|
|
198
|
+
return () => {
|
|
199
|
+
set.delete(onNext);
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async writePage(scope, fileId, pageIndex, state) {
|
|
203
|
+
const key = pageKey(scope, fileId, pageIndex);
|
|
204
|
+
// Same JSON round-trip as the real SDKs' effective behaviour: undefined
|
|
205
|
+
// keys vanish, object identity is not preserved across the write.
|
|
206
|
+
const stored = JSON.parse(JSON.stringify(state));
|
|
207
|
+
this.pages.set(key, stored);
|
|
208
|
+
this.pageListeners.get(key)?.forEach((cb) => cb(stored));
|
|
209
|
+
}
|
|
185
210
|
getBucket(scope) {
|
|
186
211
|
const key = scopeKey(scope);
|
|
187
212
|
let bucket = this.docs.get(key);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type AnnotationCanvasState, type AnnotationDocumentPatch } from '../../../types/annotation.js';
|
|
2
|
+
import type { PdfCanvasData, PdfCanvasPage } from '../../../types/pdfCanvas.js';
|
|
3
|
+
import { type PdfRotationDelta } from '../../canvas/pdfCanvasPages.js';
|
|
4
|
+
import { type AnnotationFile, type AnnotationScope, type ImageBlob } from '../AnnotationDataProvider.js';
|
|
5
|
+
import type { SaveStatus } from './useAnnotationCanvasDoc.js';
|
|
6
|
+
export interface UsePdfCanvasDocOptions {
|
|
7
|
+
scope: AnnotationScope | null;
|
|
8
|
+
fileId: string | null;
|
|
9
|
+
pageIndex: number;
|
|
10
|
+
onSaveError?: (error: unknown) => void;
|
|
11
|
+
captureThumbnail?: () => Promise<ImageBlob | null>;
|
|
12
|
+
debugLogging?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface UsePdfCanvasDocResult {
|
|
15
|
+
file: AnnotationFile | null;
|
|
16
|
+
pdfCanvas: PdfCanvasData | null;
|
|
17
|
+
pageCount: number;
|
|
18
|
+
page: PdfCanvasPage | null;
|
|
19
|
+
canvas: AnnotationCanvasState;
|
|
20
|
+
onCommit: (patch: AnnotationDocumentPatch) => void;
|
|
21
|
+
loading: boolean;
|
|
22
|
+
error: Error | null;
|
|
23
|
+
saveStatus: SaveStatus;
|
|
24
|
+
hasUnsavedChanges: boolean;
|
|
25
|
+
save: () => Promise<void>;
|
|
26
|
+
rotatePage: (delta: PdfRotationDelta) => Promise<void>;
|
|
27
|
+
refreshThumbnail: () => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare const usePdfCanvasDoc: (options: UsePdfCanvasDocOptions) => UsePdfCanvasDocResult;
|