@reekon-tools/boldr-utils 1.6.15 → 1.6.18

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.
Files changed (69) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.js +18 -2
  2. package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +3 -2
  3. package/dist/annotation/canvas/AnnotationCanvasSkia.js +1 -1
  4. package/dist/annotation/canvas/elements/ShapeElement.d.ts +4 -2
  5. package/dist/annotation/canvas/elements/ShapeElement.js +36 -11
  6. package/dist/canvas/AnnotationCanvas.d.ts +11 -0
  7. package/dist/canvas/AnnotationCanvas.js +10 -0
  8. package/dist/canvas/AnnotationCanvas.native.d.ts +8 -0
  9. package/dist/canvas/AnnotationCanvas.native.js +6 -0
  10. package/dist/canvas/AnnotationCanvasInner.d.ts +39 -0
  11. package/dist/canvas/AnnotationCanvasInner.js +219 -0
  12. package/dist/canvas/AnnotationCanvasInner.native.d.ts +35 -0
  13. package/dist/canvas/AnnotationCanvasInner.native.js +138 -0
  14. package/dist/canvas/AnnotationCanvasSkia.d.ts +27 -0
  15. package/dist/canvas/AnnotationCanvasSkia.js +20 -0
  16. package/dist/canvas/Tool.d.ts +38 -0
  17. package/dist/canvas/Tool.js +1 -0
  18. package/dist/canvas/elements/BackgroundImageElement.d.ts +9 -0
  19. package/dist/canvas/elements/BackgroundImageElement.js +37 -0
  20. package/dist/canvas/elements/MeasurementStampElement.d.ts +13 -0
  21. package/dist/canvas/elements/MeasurementStampElement.js +30 -0
  22. package/dist/canvas/elements/ShapeElement.d.ts +7 -0
  23. package/dist/canvas/elements/ShapeElement.js +62 -0
  24. package/dist/canvas/elements/StrokeElement.d.ts +7 -0
  25. package/dist/canvas/elements/StrokeElement.js +18 -0
  26. package/dist/canvas/measurementPicker.d.ts +10 -0
  27. package/dist/canvas/measurementPicker.js +1 -0
  28. package/dist/canvas/measurementStampOverlay.d.ts +11 -0
  29. package/dist/canvas/measurementStampOverlay.js +1 -0
  30. package/dist/canvas/pointerAdapter.d.ts +3 -0
  31. package/dist/canvas/pointerAdapter.js +19 -0
  32. package/dist/canvas/stampLayout.d.ts +5 -0
  33. package/dist/canvas/stampLayout.js +14 -0
  34. package/dist/canvas/tools/measurementStampTool.d.ts +9 -0
  35. package/dist/canvas/tools/measurementStampTool.js +37 -0
  36. package/dist/canvas/tools/panTool.d.ts +5 -0
  37. package/dist/canvas/tools/panTool.js +25 -0
  38. package/dist/canvas/tools/penTool.d.ts +13 -0
  39. package/dist/canvas/tools/penTool.js +68 -0
  40. package/dist/canvas/tools/selectTool.d.ts +2 -0
  41. package/dist/canvas/tools/selectTool.js +182 -0
  42. package/dist/canvas/useAnnotationCanvasState.d.ts +54 -0
  43. package/dist/canvas/useAnnotationCanvasState.js +210 -0
  44. package/dist/canvas/viewport.d.ts +16 -0
  45. package/dist/canvas/viewport.js +54 -0
  46. package/dist/data/AnnotationDataContext.d.ts +8 -0
  47. package/dist/data/AnnotationDataContext.js +11 -0
  48. package/dist/data/AnnotationDataProvider.d.ts +65 -0
  49. package/dist/data/AnnotationDataProvider.js +4 -0
  50. package/dist/data/InMemoryAnnotationProvider.d.ts +30 -0
  51. package/dist/data/InMemoryAnnotationProvider.js +197 -0
  52. package/dist/data/canvasPersistence.d.ts +3 -0
  53. package/dist/data/canvasPersistence.js +26 -0
  54. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +33 -0
  55. package/dist/data/hooks/useAnnotationCanvasDoc.js +314 -0
  56. package/dist/data/hooks/useAnnotationDoc.d.ts +7 -0
  57. package/dist/data/hooks/useAnnotationDoc.js +33 -0
  58. package/dist/data/hooks/useAnnotationList.d.ts +7 -0
  59. package/dist/data/hooks/useAnnotationList.js +26 -0
  60. package/dist/data/hooks/useAnnotationMutations.d.ts +9 -0
  61. package/dist/data/hooks/useAnnotationMutations.js +11 -0
  62. package/dist/hooks/useParseMeasurement.d.ts +4 -0
  63. package/dist/hooks/useParseMeasurement.js +14 -0
  64. package/dist/types/firestore.d.ts +1 -0
  65. package/dist/utils/evaluateFormula.d.ts +20 -0
  66. package/dist/utils/evaluateFormula.js +31 -0
  67. package/package.json +1 -1
  68. package/dist/annotation/canvas/tools/measurementLineTool.d.ts +0 -12
  69. package/dist/annotation/canvas/tools/measurementLineTool.js +0 -95
@@ -0,0 +1,182 @@
1
+ import { STAMP_TILE_SIZE } from '../stampLayout.js';
2
+ const HIT_PADDING = 6;
3
+ // Hit-test in doc-space. Crude but fast — good enough for v1; tools can
4
+ // override via `hitTest` for more precision later.
5
+ const hitStroke = (stroke, p) => {
6
+ const r = stroke.width / 2 + HIT_PADDING;
7
+ const r2 = r * r;
8
+ for (let i = 0; i < stroke.points.length - 2; i += 2) {
9
+ if (segmentDistanceSq(p.x, p.y, stroke.points[i], stroke.points[i + 1], stroke.points[i + 2], stroke.points[i + 3]) <= r2) {
10
+ return true;
11
+ }
12
+ }
13
+ return false;
14
+ };
15
+ const hitMeasurement = (m, p, zoom = 1) => {
16
+ // The stamp renders as a constant *screen*-size square centered on the
17
+ // anchor, so its doc-space footprint shrinks as you zoom in. Convert the
18
+ // screen-space half-extent (+ padding) back to doc space via the zoom so
19
+ // the hit box always matches what's drawn.
20
+ const scale = m.scale ?? 1;
21
+ const half = ((STAMP_TILE_SIZE * scale) / 2 + HIT_PADDING) / zoom;
22
+ const dx = Math.abs(p.x - m.anchor.x);
23
+ const dy = Math.abs(p.y - m.anchor.y);
24
+ return dx <= half && dy <= half;
25
+ };
26
+ const segmentDistanceSq = (px, py, ax, ay, bx, by) => {
27
+ const abx = bx - ax;
28
+ const aby = by - ay;
29
+ const lenSq = abx * abx + aby * aby;
30
+ let t = lenSq === 0 ? 0 : ((px - ax) * abx + (py - ay) * aby) / lenSq;
31
+ t = Math.max(0, Math.min(1, t));
32
+ const cx = ax + t * abx;
33
+ const cy = ay + t * aby;
34
+ const dx = px - cx;
35
+ const dy = py - cy;
36
+ return dx * dx + dy * dy;
37
+ };
38
+ const findHit = (doc, world, zoom) => {
39
+ // Hit-test in z-order (top first): measurements > shapes > strokes.
40
+ for (let i = doc.placedMeasurements.length - 1; i >= 0; i--) {
41
+ const m = doc.placedMeasurements[i];
42
+ if (hitMeasurement(m, world, zoom))
43
+ return { id: m.id, kind: 'measurement' };
44
+ }
45
+ for (let i = doc.shapes.length - 1; i >= 0; i--) {
46
+ // Default shape hit test: bounding box of the shape's points + padding.
47
+ const s = doc.shapes[i];
48
+ const pts = s.geometry.points;
49
+ if (pts.length === 0)
50
+ continue;
51
+ let minX = pts[0].x;
52
+ let maxX = pts[0].x;
53
+ let minY = pts[0].y;
54
+ let maxY = pts[0].y;
55
+ for (let j = 1; j < pts.length; j++) {
56
+ const p = pts[j];
57
+ if (p.x < minX)
58
+ minX = p.x;
59
+ if (p.x > maxX)
60
+ maxX = p.x;
61
+ if (p.y < minY)
62
+ minY = p.y;
63
+ if (p.y > maxY)
64
+ maxY = p.y;
65
+ }
66
+ if (world.x >= minX - HIT_PADDING &&
67
+ world.x <= maxX + HIT_PADDING &&
68
+ world.y >= minY - HIT_PADDING &&
69
+ world.y <= maxY + HIT_PADDING) {
70
+ return { id: s.id, kind: 'shape' };
71
+ }
72
+ }
73
+ for (let i = doc.strokes.length - 1; i >= 0; i--) {
74
+ const s = doc.strokes[i];
75
+ if (hitStroke(s, world))
76
+ return { id: s.id, kind: 'stroke' };
77
+ }
78
+ return null;
79
+ };
80
+ const translatePatch = (elementKind, id, doc, delta) => {
81
+ if (elementKind === 'measurement') {
82
+ const m = doc.placedMeasurements.find((x) => x.id === id);
83
+ if (!m)
84
+ return null;
85
+ return {
86
+ op: 'updateMeasurement',
87
+ id,
88
+ patch: {
89
+ anchor: { x: m.anchor.x + delta.x, y: m.anchor.y + delta.y },
90
+ leader: m.leader
91
+ ? {
92
+ from: {
93
+ x: m.leader.from.x + delta.x,
94
+ y: m.leader.from.y + delta.y,
95
+ },
96
+ to: {
97
+ x: m.leader.to.x + delta.x,
98
+ y: m.leader.to.y + delta.y,
99
+ },
100
+ }
101
+ : undefined,
102
+ },
103
+ };
104
+ }
105
+ if (elementKind === 'shape') {
106
+ const s = doc.shapes.find((x) => x.id === id);
107
+ if (!s)
108
+ return null;
109
+ return {
110
+ op: 'updateShape',
111
+ id,
112
+ patch: {
113
+ geometry: {
114
+ ...s.geometry,
115
+ points: s.geometry.points.map((p) => ({
116
+ x: p.x + delta.x,
117
+ y: p.y + delta.y,
118
+ })),
119
+ },
120
+ },
121
+ };
122
+ }
123
+ const stroke = doc.strokes.find((x) => x.id === id);
124
+ if (!stroke)
125
+ return null;
126
+ const points = stroke.points.slice();
127
+ for (let i = 0; i < points.length; i += 2) {
128
+ points[i] = points[i] + delta.x;
129
+ points[i + 1] = points[i + 1] + delta.y;
130
+ }
131
+ return { op: 'updateStroke', id, patch: { points } };
132
+ };
133
+ export const createSelectTool = () => ({
134
+ id: 'select',
135
+ label: 'Select',
136
+ cursor: 'default',
137
+ onPointerDown(event, ctx) {
138
+ const hit = findHit(ctx.document, event.world, ctx.viewport.state.zoom);
139
+ if (!hit) {
140
+ ctx.setSelection(null);
141
+ return { kind: 'idle' };
142
+ }
143
+ ctx.setSelection({ ids: [hit.id] });
144
+ return {
145
+ kind: 'dragging',
146
+ id: hit.id,
147
+ elementKind: hit.kind,
148
+ start: event.world,
149
+ delta: { x: 0, y: 0 },
150
+ };
151
+ },
152
+ onPointerMove(event, ctx, state) {
153
+ const s = state;
154
+ if (s?.kind !== 'dragging')
155
+ return s;
156
+ const delta = { x: event.world.x - s.start.x, y: event.world.y - s.start.y };
157
+ const op = translatePatch(s.elementKind, s.id, ctx.document, delta);
158
+ if (op)
159
+ ctx.preview({ ops: [op] });
160
+ return { ...s, delta };
161
+ },
162
+ onPointerUp(_event, ctx, state) {
163
+ const s = state;
164
+ if (s?.kind !== 'dragging')
165
+ return;
166
+ if (s.delta.x === 0 && s.delta.y === 0)
167
+ return;
168
+ const op = translatePatch(s.elementKind, s.id, ctx.document, s.delta);
169
+ if (op)
170
+ ctx.commit({ ops: [op] });
171
+ },
172
+ onCancel(_state, ctx) {
173
+ ctx.preview({ ops: [] });
174
+ },
175
+ hitTest(element, p) {
176
+ if (element.kind === 'measurement')
177
+ return hitMeasurement(element, p);
178
+ if (element.kind === 'stroke')
179
+ return hitStroke(element, p);
180
+ return false;
181
+ },
182
+ });
@@ -0,0 +1,54 @@
1
+ import type { Measurement } from '../types/firestore.js';
2
+ import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationStroke, type Selection, type Vec2 } from '../types/annotation.js';
3
+ import type { MeasurementRef } from './measurementPicker.js';
4
+ import type { CanvasPointerEvent, Tool, ToolContext, ToolState } from './Tool.js';
5
+ import { type ViewportState } from './viewport.js';
6
+ export interface AnnotationCanvasHandle {
7
+ undo(): void;
8
+ redo(): void;
9
+ canUndo(): boolean;
10
+ canRedo(): boolean;
11
+ zoomToFit(): void;
12
+ resetView(): void;
13
+ placeMeasurementAtCenter(ref: MeasurementRef): void;
14
+ }
15
+ export interface UseAnnotationCanvasStateProps {
16
+ canvas: AnnotationCanvasState;
17
+ onCommit(patch: AnnotationDocumentPatch): void;
18
+ tools: Tool[];
19
+ activeToolId: string;
20
+ selection: Selection | null;
21
+ onSelectionChange(selection: Selection | null): void;
22
+ measurements?: Measurement[];
23
+ pickMeasurement?: () => Promise<MeasurementRef | null>;
24
+ width: number;
25
+ height: number;
26
+ initialViewport?: ViewportState;
27
+ imperativeRef?: {
28
+ current: AnnotationCanvasHandle | null;
29
+ };
30
+ }
31
+ export interface AnnotationCanvasStateApi {
32
+ effectiveCanvas: AnnotationCanvasState;
33
+ worldTransform: Array<{
34
+ scale: number;
35
+ } | {
36
+ translateX: number;
37
+ } | {
38
+ translateY: number;
39
+ }>;
40
+ viewport: ViewportState;
41
+ measurementsById: Map<string, Measurement>;
42
+ activeTool: Tool | null;
43
+ toolState: ToolState;
44
+ ctx: ToolContext;
45
+ penDrawingStroke: AnnotationStroke | null;
46
+ customPreviewState: ToolState;
47
+ dispatchPointerDown(event: CanvasPointerEvent): void;
48
+ dispatchPointerMove(event: CanvasPointerEvent): void;
49
+ dispatchPointerUp(event: CanvasPointerEvent): void;
50
+ dispatchPointerCancel(): void;
51
+ pan(deltaScreen: Vec2): void;
52
+ zoom(focalScreen: Vec2, nextZoom: number): void;
53
+ }
54
+ export declare const useAnnotationCanvasState: (props: UseAnnotationCanvasStateProps) => AnnotationCanvasStateApi;
@@ -0,0 +1,210 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { applyPatch, invertPatch, DEFAULT_LAYER_ID, } from '../types/annotation.js';
3
+ import { createViewportApi, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
4
+ // Platform-agnostic state machine for the annotation canvas. Web and native
5
+ // inners share this hook; each wraps it with platform-specific event
6
+ // capture and JSX (div + DOM events vs. GestureDetector + RN Views).
7
+ export const useAnnotationCanvasState = (props) => {
8
+ const { canvas, onCommit, tools, activeToolId, selection, onSelectionChange, measurements, pickMeasurement, width, height, initialViewport, imperativeRef, } = props;
9
+ const [viewport, setViewport] = useState(initialViewport ?? DEFAULT_VIEWPORT);
10
+ const [toolState, setToolState] = useState(undefined);
11
+ const [previewPatch, setPreviewPatch] = useState(null);
12
+ const undoStackRef = useRef([]);
13
+ const redoStackRef = useRef([]);
14
+ const activePointerIdRef = useRef(null);
15
+ const activeTool = useMemo(() => tools.find((t) => t.id === activeToolId) ?? null, [tools, activeToolId]);
16
+ const effectiveCanvas = useMemo(() => (previewPatch ? applyPatch(canvas, previewPatch) : canvas), [canvas, previewPatch]);
17
+ const measurementsById = useMemo(() => {
18
+ const map = new Map();
19
+ measurements?.forEach((m) => map.set(m.id, m));
20
+ return map;
21
+ }, [measurements]);
22
+ const viewportApi = useMemo(() => createViewportApi(viewport), [viewport]);
23
+ const ctx = useMemo(() => ({
24
+ document: canvas,
25
+ selection,
26
+ viewport: viewportApi,
27
+ preview(patch) {
28
+ setPreviewPatch(patch);
29
+ },
30
+ commit(patch) {
31
+ const inverse = invertPatch(canvas, patch);
32
+ undoStackRef.current.push({ forward: patch, inverse });
33
+ redoStackRef.current = [];
34
+ setPreviewPatch(null);
35
+ onCommit(patch);
36
+ },
37
+ setSelection(s) {
38
+ onSelectionChange(s);
39
+ },
40
+ requestPickMeasurement() {
41
+ return pickMeasurement ? pickMeasurement() : Promise.resolve(null);
42
+ },
43
+ applyPan(deltaScreen) {
44
+ setViewport((v) => panBy(v, deltaScreen));
45
+ },
46
+ applyZoom(focalScreen, nextZoom) {
47
+ setViewport((v) => zoomAt(v, focalScreen, nextZoom));
48
+ },
49
+ }), [
50
+ canvas,
51
+ selection,
52
+ viewportApi,
53
+ onCommit,
54
+ onSelectionChange,
55
+ pickMeasurement,
56
+ ]);
57
+ // Live ctx for imperative handle methods (which are created in an effect and
58
+ // would otherwise capture a stale ctx/viewport).
59
+ const ctxRef = useRef(ctx);
60
+ ctxRef.current = ctx;
61
+ const dispatchPointerDown = useCallback((event) => {
62
+ if (!activeTool)
63
+ return;
64
+ activePointerIdRef.current = event.pointerId;
65
+ const next = activeTool.onPointerDown?.(event, ctx, toolState);
66
+ if (next !== undefined)
67
+ setToolState(next);
68
+ }, [activeTool, ctx, toolState]);
69
+ const dispatchPointerMove = useCallback((event) => {
70
+ if (!activeTool)
71
+ return;
72
+ if (activePointerIdRef.current !== null &&
73
+ event.pointerId !== activePointerIdRef.current) {
74
+ return;
75
+ }
76
+ const next = activeTool.onPointerMove?.(event, ctx, toolState);
77
+ if (next !== undefined)
78
+ setToolState(next);
79
+ }, [activeTool, ctx, toolState]);
80
+ const dispatchPointerUp = useCallback((event) => {
81
+ if (!activeTool)
82
+ return;
83
+ if (activePointerIdRef.current !== null &&
84
+ event.pointerId !== activePointerIdRef.current) {
85
+ return;
86
+ }
87
+ activeTool.onPointerUp?.(event, ctx, toolState);
88
+ activePointerIdRef.current = null;
89
+ setToolState(undefined);
90
+ }, [activeTool, ctx, toolState]);
91
+ const dispatchPointerCancel = useCallback(() => {
92
+ if (activeTool)
93
+ activeTool.onCancel?.(toolState, ctx);
94
+ activePointerIdRef.current = null;
95
+ setToolState(undefined);
96
+ setPreviewPatch(null);
97
+ }, [activeTool, ctx, toolState]);
98
+ const pan = useCallback((deltaScreen) => {
99
+ setViewport((v) => panBy(v, deltaScreen));
100
+ }, []);
101
+ const zoom = useCallback((focalScreen, nextZoom) => {
102
+ setViewport((v) => zoomAt(v, focalScreen, nextZoom));
103
+ }, []);
104
+ // Imperative API mirror — set via prop so it survives WithSkiaWeb's lazy
105
+ // boundary on web.
106
+ useEffect(() => {
107
+ if (!imperativeRef)
108
+ return;
109
+ imperativeRef.current = {
110
+ undo() {
111
+ const entry = undoStackRef.current.pop();
112
+ if (!entry)
113
+ return;
114
+ redoStackRef.current.push(entry);
115
+ onCommit(entry.inverse);
116
+ },
117
+ redo() {
118
+ const entry = redoStackRef.current.pop();
119
+ if (!entry)
120
+ return;
121
+ undoStackRef.current.push(entry);
122
+ onCommit(entry.forward);
123
+ },
124
+ canUndo() {
125
+ return undoStackRef.current.length > 0;
126
+ },
127
+ canRedo() {
128
+ return redoStackRef.current.length > 0;
129
+ },
130
+ zoomToFit() {
131
+ setViewport(() => {
132
+ const docW = canvas.viewport.width;
133
+ const docH = canvas.viewport.height;
134
+ const z = Math.min(width / docW, height / docH);
135
+ const offsetX = (width - docW * z) / 2;
136
+ const offsetY = (height - docH * z) / 2;
137
+ return { zoom: z, pan: { x: -offsetX / z, y: -offsetY / z } };
138
+ });
139
+ },
140
+ resetView() {
141
+ setViewport(DEFAULT_VIEWPORT);
142
+ },
143
+ placeMeasurementAtCenter(ref) {
144
+ const c = ctxRef.current;
145
+ const anchor = c.viewport.screenToWorld({
146
+ x: width / 2,
147
+ y: height / 2,
148
+ });
149
+ const placed = {
150
+ id: `measurement-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`,
151
+ layerId: c.document.layers[0]?.id ?? DEFAULT_LAYER_ID,
152
+ measurementPath: ref.measurementPath,
153
+ measurementId: ref.measurementId,
154
+ groupId: ref.groupId,
155
+ anchor,
156
+ labelOverride: ref.label,
157
+ unitOverride: ref.unit,
158
+ showLabel: true,
159
+ showValue: true,
160
+ createdAt: Date.now(),
161
+ };
162
+ c.commit({ ops: [{ op: 'addMeasurement', measurement: placed }] });
163
+ // Select it so the consumer can immediately move it (the consumer is
164
+ // responsible for switching to its move/select tool).
165
+ c.setSelection({ ids: [placed.id] });
166
+ },
167
+ };
168
+ return () => {
169
+ if (imperativeRef)
170
+ imperativeRef.current = null;
171
+ };
172
+ }, [
173
+ imperativeRef,
174
+ onCommit,
175
+ canvas.viewport.width,
176
+ canvas.viewport.height,
177
+ width,
178
+ height,
179
+ ]);
180
+ const worldTransform = useMemo(() => [
181
+ { scale: viewport.zoom },
182
+ { translateX: -viewport.pan.x },
183
+ { translateY: -viewport.pan.y },
184
+ ], [viewport]);
185
+ // Detect the built-in pen tool's in-flight state shape so the canvas can
186
+ // render the preview (the tool factory is Skia-free and can't render).
187
+ const penDrawingStroke = toolState &&
188
+ typeof toolState === 'object' &&
189
+ 'kind' in toolState &&
190
+ toolState.kind === 'pen-drawing'
191
+ ? toolState.stroke
192
+ : null;
193
+ return {
194
+ effectiveCanvas,
195
+ worldTransform,
196
+ viewport,
197
+ measurementsById,
198
+ activeTool,
199
+ toolState,
200
+ ctx,
201
+ penDrawingStroke,
202
+ customPreviewState: toolState,
203
+ dispatchPointerDown,
204
+ dispatchPointerMove,
205
+ dispatchPointerUp,
206
+ dispatchPointerCancel,
207
+ pan,
208
+ zoom,
209
+ };
210
+ };
@@ -0,0 +1,16 @@
1
+ import type { Vec2 } from '../types/annotation.js';
2
+ export interface ViewportState {
3
+ zoom: number;
4
+ pan: Vec2;
5
+ }
6
+ export interface ViewportApi {
7
+ state: ViewportState;
8
+ screenToWorld(screen: Vec2): Vec2;
9
+ worldToScreen(world: Vec2): Vec2;
10
+ worldDistanceToScreen(distance: number): number;
11
+ }
12
+ export declare const createViewportApi: (state: ViewportState) => ViewportApi;
13
+ export declare const DEFAULT_VIEWPORT: ViewportState;
14
+ export declare const fitToScreen: (docWidth: number, docHeight: number, screenWidth: number, screenHeight: number, padding?: number) => ViewportState;
15
+ export declare const zoomAt: (state: ViewportState, focalScreen: Vec2, nextZoom: number) => ViewportState;
16
+ export declare const panBy: (state: ViewportState, deltaScreen: Vec2) => ViewportState;
@@ -0,0 +1,54 @@
1
+ export const createViewportApi = (state) => ({
2
+ state,
3
+ screenToWorld: ({ x, y }) => ({
4
+ x: x / state.zoom + state.pan.x,
5
+ y: y / state.zoom + state.pan.y,
6
+ }),
7
+ worldToScreen: ({ x, y }) => ({
8
+ x: (x - state.pan.x) * state.zoom,
9
+ y: (y - state.pan.y) * state.zoom,
10
+ }),
11
+ worldDistanceToScreen: (d) => d * state.zoom,
12
+ });
13
+ export const DEFAULT_VIEWPORT = {
14
+ zoom: 1,
15
+ pan: { x: 0, y: 0 },
16
+ };
17
+ // Returns a viewport that fits `docWidth x docHeight` into `screenWidth x
18
+ // screenHeight` with optional padding (screen-space pixels).
19
+ export const fitToScreen = (docWidth, docHeight, screenWidth, screenHeight, padding = 16) => {
20
+ const availableW = Math.max(1, screenWidth - padding * 2);
21
+ const availableH = Math.max(1, screenHeight - padding * 2);
22
+ const zoom = Math.min(availableW / docWidth, availableH / docHeight);
23
+ const renderedW = docWidth * zoom;
24
+ const renderedH = docHeight * zoom;
25
+ const offsetX = (screenWidth - renderedW) / 2;
26
+ const offsetY = (screenHeight - renderedH) / 2;
27
+ return {
28
+ zoom,
29
+ pan: { x: -offsetX / zoom, y: -offsetY / zoom },
30
+ };
31
+ };
32
+ // Zoom toward a focal screen point so the world point under the cursor stays
33
+ // fixed. Used for wheel-zoom on web and pinch on native.
34
+ export const zoomAt = (state, focalScreen, nextZoom) => {
35
+ const clampedZoom = Math.max(0.05, Math.min(50, nextZoom));
36
+ const focalWorld = {
37
+ x: focalScreen.x / state.zoom + state.pan.x,
38
+ y: focalScreen.y / state.zoom + state.pan.y,
39
+ };
40
+ return {
41
+ zoom: clampedZoom,
42
+ pan: {
43
+ x: focalWorld.x - focalScreen.x / clampedZoom,
44
+ y: focalWorld.y - focalScreen.y / clampedZoom,
45
+ },
46
+ };
47
+ };
48
+ export const panBy = (state, deltaScreen) => ({
49
+ zoom: state.zoom,
50
+ pan: {
51
+ x: state.pan.x - deltaScreen.x / state.zoom,
52
+ y: state.pan.y - deltaScreen.y / state.zoom,
53
+ },
54
+ });
@@ -0,0 +1,8 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { AnnotationDataProvider } from './AnnotationDataProvider.js';
3
+ export interface AnnotationDataProviderProps {
4
+ value: AnnotationDataProvider;
5
+ children: ReactNode;
6
+ }
7
+ export declare const AnnotationDataProviderContext: ({ value, children, }: AnnotationDataProviderProps) => import("react/jsx-runtime").JSX.Element;
8
+ export declare const useAnnotationData: () => AnnotationDataProvider;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from 'react';
3
+ const AnnotationDataContext = createContext(null);
4
+ export const AnnotationDataProviderContext = ({ value, children, }) => (_jsx(AnnotationDataContext.Provider, { value: value, children: children }));
5
+ export const useAnnotationData = () => {
6
+ const provider = useContext(AnnotationDataContext);
7
+ if (!provider) {
8
+ throw new Error('useAnnotationData must be used inside <AnnotationDataProviderContext>');
9
+ }
10
+ return provider;
11
+ };
@@ -0,0 +1,65 @@
1
+ import type { FileUpload, FileUploadType, Measurement } from '../types/firestore.js';
2
+ export interface JobScope {
3
+ orgId: string;
4
+ projectId: string;
5
+ jobId: string;
6
+ }
7
+ export interface JobGroupScope extends JobScope {
8
+ groupId: string;
9
+ }
10
+ export type Unsubscribe = () => void;
11
+ export type FieldOp = {
12
+ kind: 'serverTimestamp';
13
+ } | {
14
+ kind: 'arrayUnion';
15
+ values: unknown[];
16
+ } | {
17
+ kind: 'arrayRemove';
18
+ values: unknown[];
19
+ } | {
20
+ kind: 'increment';
21
+ by: number;
22
+ } | {
23
+ kind: 'delete';
24
+ };
25
+ export declare const isFieldOp: (v: unknown) => v is FieldOp;
26
+ export type Patch<T> = {
27
+ [K in keyof T]?: T[K] | FieldOp;
28
+ };
29
+ export interface ImageBlob {
30
+ data: Blob | ArrayBuffer | string;
31
+ contentType: string;
32
+ filename?: string;
33
+ }
34
+ export interface UploadedImageRef {
35
+ storagePath: string;
36
+ downloadUrl: string;
37
+ width?: number;
38
+ height?: number;
39
+ bytes?: number;
40
+ }
41
+ export type AnnotationFile = Extract<FileUpload, {
42
+ type: FileUploadType.Canvas;
43
+ }>;
44
+ export interface AnnotationFileSummary {
45
+ id: string;
46
+ name: string;
47
+ thumbnailUrl: string | null;
48
+ fileType: 'sketch' | 'document';
49
+ isLabel?: boolean;
50
+ createdAt?: Date;
51
+ createdBy?: AnnotationFile['createdBy'];
52
+ }
53
+ export interface AnnotationDataProvider {
54
+ create(scope: JobGroupScope, seed: Partial<AnnotationFile>): Promise<string>;
55
+ get(scope: JobGroupScope, fileId: string): Promise<AnnotationFile | null>;
56
+ update(scope: JobGroupScope, fileId: string, patch: Patch<AnnotationFile>): Promise<void>;
57
+ delete(scope: JobGroupScope, fileId: string): Promise<void>;
58
+ subscribe(scope: JobGroupScope, fileId: string, onNext: (doc: AnnotationFile | null) => void, onError?: (err: Error) => void): Unsubscribe;
59
+ list(scope: JobGroupScope, onNext: (files: AnnotationFileSummary[]) => void, onError?: (err: Error) => void): Unsubscribe;
60
+ subscribeGroupMeasurements(scope: JobGroupScope, onNext: (measurements: Measurement[]) => void, onError?: (err: Error) => void): Unsubscribe;
61
+ subscribeJobMeasurements(scope: JobScope, onNext: (measurements: Measurement[]) => void, onError?: (err: Error) => void): Unsubscribe;
62
+ uploadImage(scope: JobGroupScope, fileId: string, role: 'background' | 'thumbnail', blob: ImageBlob): Promise<UploadedImageRef>;
63
+ getImageUrl(scope: JobGroupScope, fileId: string, storagePath: string): Promise<string>;
64
+ deleteImage(scope: JobGroupScope, fileId: string, storagePath: string): Promise<void>;
65
+ }
@@ -0,0 +1,4 @@
1
+ export const isFieldOp = (v) => !!v &&
2
+ typeof v === 'object' &&
3
+ 'kind' in v &&
4
+ typeof v.kind === 'string';
@@ -0,0 +1,30 @@
1
+ import { type Measurement } from '../types/firestore.js';
2
+ import { type AnnotationDataProvider, type AnnotationFile, type AnnotationFileSummary, type ImageBlob, type JobGroupScope, type JobScope, type Patch, type Unsubscribe, type UploadedImageRef } from './AnnotationDataProvider.js';
3
+ export declare class InMemoryAnnotationProvider implements AnnotationDataProvider {
4
+ private docs;
5
+ private measurements;
6
+ private images;
7
+ private docListeners;
8
+ private listListeners;
9
+ private groupMeasurementListeners;
10
+ private jobMeasurementListeners;
11
+ private nextId;
12
+ setMeasurements(scope: JobGroupScope, measurements: Measurement[]): void;
13
+ create(scope: JobGroupScope, seed: Partial<AnnotationFile>): Promise<string>;
14
+ get(scope: JobGroupScope, fileId: string): Promise<AnnotationFile | null>;
15
+ update(scope: JobGroupScope, fileId: string, patch: Patch<AnnotationFile>): Promise<void>;
16
+ delete(scope: JobGroupScope, fileId: string): Promise<void>;
17
+ subscribe(scope: JobGroupScope, fileId: string, onNext: (doc: AnnotationFile | null) => void): Unsubscribe;
18
+ list(scope: JobGroupScope, onNext: (files: AnnotationFileSummary[]) => void): Unsubscribe;
19
+ subscribeGroupMeasurements(scope: JobGroupScope, onNext: (measurements: Measurement[]) => void): Unsubscribe;
20
+ subscribeJobMeasurements(scope: JobScope, onNext: (measurements: Measurement[]) => void): Unsubscribe;
21
+ uploadImage(scope: JobGroupScope, fileId: string, role: 'background' | 'thumbnail', blob: ImageBlob): Promise<UploadedImageRef>;
22
+ getImageUrl(_scope: JobGroupScope, fileId: string, storagePath: string): Promise<string>;
23
+ deleteImage(_scope: JobGroupScope, fileId: string, storagePath: string): Promise<void>;
24
+ private getBucket;
25
+ private notifyDoc;
26
+ private notifyList;
27
+ private notifyGroupMeasurements;
28
+ private notifyJobMeasurements;
29
+ private collectJobMeasurements;
30
+ }