@reekon-tools/boldr-utils 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.js +1 -0
  2. package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +3 -1
  3. package/dist/annotation/canvas/AnnotationCanvasSkia.js +2 -2
  4. package/dist/annotation/canvas/elements/BackgroundImageElement.d.ts +5 -1
  5. package/dist/annotation/canvas/elements/BackgroundImageElement.js +13 -99
  6. package/dist/annotation/canvas/elements/BackgroundSvg.d.ts +13 -0
  7. package/dist/annotation/canvas/elements/BackgroundSvg.js +337 -0
  8. package/dist/annotation/canvas/elements/backgroundUrl.d.ts +2 -0
  9. package/dist/annotation/canvas/elements/backgroundUrl.js +38 -0
  10. package/dist/annotation/canvas/svgRasterPlan.d.ts +56 -0
  11. package/dist/annotation/canvas/svgRasterPlan.js +225 -0
  12. package/package.json +1 -1
  13. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  14. package/dist/canvas/AnnotationCanvas.js +0 -10
  15. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  16. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  17. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  18. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  19. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  20. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  21. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  22. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  23. package/dist/canvas/Tool.d.ts +0 -38
  24. package/dist/canvas/Tool.js +0 -1
  25. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  26. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  27. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  28. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  29. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  30. package/dist/canvas/elements/ShapeElement.js +0 -62
  31. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  32. package/dist/canvas/elements/StrokeElement.js +0 -18
  33. package/dist/canvas/measurementPicker.d.ts +0 -10
  34. package/dist/canvas/measurementPicker.js +0 -1
  35. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  36. package/dist/canvas/measurementStampOverlay.js +0 -1
  37. package/dist/canvas/pointerAdapter.d.ts +0 -3
  38. package/dist/canvas/pointerAdapter.js +0 -19
  39. package/dist/canvas/stampLayout.d.ts +0 -5
  40. package/dist/canvas/stampLayout.js +0 -14
  41. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  42. package/dist/canvas/tools/measurementStampTool.js +0 -37
  43. package/dist/canvas/tools/panTool.d.ts +0 -5
  44. package/dist/canvas/tools/panTool.js +0 -25
  45. package/dist/canvas/tools/penTool.d.ts +0 -13
  46. package/dist/canvas/tools/penTool.js +0 -68
  47. package/dist/canvas/tools/selectTool.d.ts +0 -2
  48. package/dist/canvas/tools/selectTool.js +0 -182
  49. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  50. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  51. package/dist/canvas/viewport.d.ts +0 -16
  52. package/dist/canvas/viewport.js +0 -54
  53. package/dist/data/AnnotationDataContext.d.ts +0 -8
  54. package/dist/data/AnnotationDataContext.js +0 -11
  55. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  56. package/dist/data/AnnotationDataProvider.js +0 -4
  57. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  58. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  59. package/dist/data/canvasPersistence.d.ts +0 -3
  60. package/dist/data/canvasPersistence.js +0 -26
  61. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  62. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  63. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  64. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  65. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  66. package/dist/data/hooks/useAnnotationList.js +0 -26
  67. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  68. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  69. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  70. package/dist/hooks/useParseMeasurement.js +0 -14
  71. package/dist/utils/evaluateFormula.d.ts +0 -20
  72. package/dist/utils/evaluateFormula.js +0 -31
@@ -0,0 +1,225 @@
1
+ // A whole-SVG raster may not exceed this per side. Kept modest because a full
2
+ // raster is retained for the lifetime of the zoom level.
3
+ export const FULL_RASTER_CAP = 4096;
4
+ // A visible-region crop may not exceed this per side. Crops are transient and
5
+ // sized by the screen, and every WebGL implementation this app targets
6
+ // supports ≥8192px textures.
7
+ export const CROP_RASTER_CAP = 8192;
8
+ // Absolute pixel budget for one raster (≈96MB RGBA) — the caps above bound
9
+ // each side, this bounds the area.
10
+ export const MAX_RASTER_PIXELS = 24000000;
11
+ // Fraction of the visible region added on each side of a crop, so small pans
12
+ // stay inside the bitmap instead of re-rasterizing.
13
+ export const CROP_MARGIN = 0.35;
14
+ // Re-rasterize when the needed density outgrows the bitmap by this factor
15
+ // (sharpen promptly on zoom-in)…
16
+ export const REGEN_SCALE_UP = 1.3;
17
+ // …or when the bitmap is this much denser than needed (reclaim memory lazily
18
+ // on zoom-out; minified drawing looks fine meanwhile).
19
+ export const REGEN_SCALE_DOWN = 0.5;
20
+ const EMPTY_RECT = { x: 0, y: 0, width: 0, height: 0 };
21
+ const intersectRect = (a, b) => {
22
+ const x = Math.max(a.x, b.x);
23
+ const y = Math.max(a.y, b.y);
24
+ const right = Math.min(a.x + a.width, b.x + b.width);
25
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
26
+ if (right <= x || bottom <= y)
27
+ return null;
28
+ return { x, y, width: right - x, height: bottom - y };
29
+ };
30
+ // Does `outer` contain `inner`, within eps svg-local px? An empty inner
31
+ // (off-screen SVG) is trivially contained.
32
+ export const rectContains = (outer, inner, eps = 0.5) => inner.width <= 0 ||
33
+ inner.height <= 0 ||
34
+ (inner.x >= outer.x - eps &&
35
+ inner.y >= outer.y - eps &&
36
+ inner.x + inner.width <= outer.x + outer.width + eps &&
37
+ inner.y + inner.height <= outer.y + outer.height + eps);
38
+ // Clamp per-axis densities so `rect` rasterized at (zx, zy) fits the side cap
39
+ // and the pixel budget, and derive the integer bitmap size.
40
+ const capScales = (rect, zx, zy, cap) => {
41
+ let cx = Math.min(zx, cap / rect.width);
42
+ let cy = Math.min(zy, cap / rect.height);
43
+ const pixels = rect.width * cx * rect.height * cy;
44
+ if (pixels > MAX_RASTER_PIXELS) {
45
+ const r = Math.sqrt(MAX_RASTER_PIXELS / pixels);
46
+ cx *= r;
47
+ cy *= r;
48
+ }
49
+ const outWidth = Math.max(1, Math.ceil(rect.width * cx));
50
+ const outHeight = Math.max(1, Math.ceil(rect.height * cy));
51
+ return {
52
+ outWidth,
53
+ outHeight,
54
+ zx: outWidth / rect.width,
55
+ zy: outHeight / rect.height,
56
+ };
57
+ };
58
+ // Plan the raster the current viewport needs, or null when the inputs can't
59
+ // produce one (degenerate sizes).
60
+ export const planSvgRaster = (input) => {
61
+ const { widthPx, heightPx, dims, viewport, screenWidth, screenHeight } = input;
62
+ if (!(widthPx > 0) || !(heightPx > 0))
63
+ return null;
64
+ if (!(dims.width > 0) || !(dims.height > 0))
65
+ return null;
66
+ const zoom = viewport.zoom > 0 ? viewport.zoom : 1;
67
+ const dpr = input.dpr > 0 ? input.dpr : 1;
68
+ // Doc units per svg-local px (per-axis: 'stretch' fits are anisotropic).
69
+ const sx = dims.width / widthPx;
70
+ const sy = dims.height / heightPx;
71
+ // Device px per svg-local px the screen currently shows.
72
+ const zx = zoom * sx * dpr;
73
+ const zy = zoom * sy * dpr;
74
+ if (!Number.isFinite(zx) || !Number.isFinite(zy) || zx <= 0 || zy <= 0) {
75
+ return null;
76
+ }
77
+ const whole = { x: 0, y: 0, width: widthPx, height: heightPx };
78
+ const visibleDoc = {
79
+ x: viewport.pan.x,
80
+ y: viewport.pan.y,
81
+ width: Math.max(0, screenWidth) / zoom,
82
+ height: Math.max(0, screenHeight) / zoom,
83
+ };
84
+ const visible = intersectRect({
85
+ x: (visibleDoc.x - dims.x) / sx,
86
+ y: (visibleDoc.y - dims.y) / sy,
87
+ width: visibleDoc.width / sx,
88
+ height: visibleDoc.height / sy,
89
+ }, whole);
90
+ // Whole-SVG raster whenever the needed density fits the budget — pan never
91
+ // invalidates it. Also the fallback when the SVG is entirely off-screen
92
+ // (capped density; only used until something is visible again).
93
+ const fullFits = widthPx * zx <= FULL_RASTER_CAP &&
94
+ heightPx * zy <= FULL_RASTER_CAP &&
95
+ widthPx * zx * heightPx * zy <= MAX_RASTER_PIXELS;
96
+ if (fullFits || !visible) {
97
+ const capped = capScales(whole, zx, zy, FULL_RASTER_CAP);
98
+ return {
99
+ full: true,
100
+ rect: whole,
101
+ visible: visible ?? EMPTY_RECT,
102
+ ...capped,
103
+ };
104
+ }
105
+ const marginX = visible.width * CROP_MARGIN;
106
+ const marginY = visible.height * CROP_MARGIN;
107
+ const rect = intersectRect({
108
+ x: visible.x - marginX,
109
+ y: visible.y - marginY,
110
+ width: visible.width + marginX * 2,
111
+ height: visible.height + marginY * 2,
112
+ }, whole);
113
+ const capped = capScales(rect, zx, zy, CROP_RASTER_CAP);
114
+ return { full: false, rect, visible, ...capped };
115
+ };
116
+ // Should the applied raster be replaced by what `plan` asks for? Scale drift
117
+ // beyond the regen thresholds always regenerates; otherwise a full raster
118
+ // stands, and a crop stands while the visible region stays inside it.
119
+ export const shouldRegenerateRaster = (applied, plan) => {
120
+ if (!plan)
121
+ return false;
122
+ if (!applied)
123
+ return true;
124
+ const rx = plan.zx / applied.zx;
125
+ const ry = plan.zy / applied.zy;
126
+ if (rx > REGEN_SCALE_UP ||
127
+ ry > REGEN_SCALE_UP ||
128
+ rx < REGEN_SCALE_DOWN ||
129
+ ry < REGEN_SCALE_DOWN) {
130
+ return true;
131
+ }
132
+ if (applied.full)
133
+ return false;
134
+ return !rectContains(applied.rect, plan.visible);
135
+ };
136
+ // --- SVG root-size normalization (DOM-free half) -----------------------------
137
+ //
138
+ // The browser rasterizes an <img> SVG from its root width/height; a root
139
+ // without a viewBox doesn't scale at all (it clips). Given the root's
140
+ // attributes, decide the viewBox to enforce and the user-unit size to pin the
141
+ // root to — the coordinate space raster crops are expressed in.
142
+ // Parse an absolute SVG length attribute. Percentages and other relative
143
+ // units have no meaning outside a CSS layout context — treated as absent.
144
+ export const parseSvgLength = (value) => {
145
+ if (!value)
146
+ return null;
147
+ const trimmed = value.trim();
148
+ if (!/^\d*\.?\d+(px)?$/.test(trimmed))
149
+ return null;
150
+ const parsed = parseFloat(trimmed);
151
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
152
+ };
153
+ export const parseViewBox = (value) => {
154
+ if (!value)
155
+ return null;
156
+ const parts = value
157
+ .trim()
158
+ .split(/[\s,]+/)
159
+ .map(Number);
160
+ if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) {
161
+ return null;
162
+ }
163
+ const [x, y, width, height] = parts;
164
+ if (!(width > 0) || !(height > 0))
165
+ return null;
166
+ return { x, y, width, height };
167
+ };
168
+ export const svgIntrinsicSize = (viewBoxAttr, widthAttr, heightAttr, fallbackWidth, fallbackHeight) => {
169
+ const vb = parseViewBox(viewBoxAttr);
170
+ if (vb) {
171
+ return {
172
+ viewBox: viewBoxAttr.trim(),
173
+ minX: vb.x,
174
+ minY: vb.y,
175
+ width: vb.width,
176
+ height: vb.height,
177
+ };
178
+ }
179
+ const w = parseSvgLength(widthAttr);
180
+ const h = parseSvgLength(heightAttr);
181
+ if (w && h) {
182
+ return { viewBox: `0 0 ${w} ${h}`, minX: 0, minY: 0, width: w, height: h };
183
+ }
184
+ return {
185
+ viewBox: `0 0 ${fallbackWidth} ${fallbackHeight}`,
186
+ minX: 0,
187
+ minY: 0,
188
+ width: fallbackWidth,
189
+ height: fallbackHeight,
190
+ };
191
+ };
192
+ // Wrap a normalized SVG document in an outer <svg> whose viewBox is the crop
193
+ // window (user units) and whose width/height are the output size in device
194
+ // px. The wrapper's intrinsic size then EQUALS the raster destination, so
195
+ // drawImage(img, 0, 0) never scales — critical on Chromium, which rasterizes
196
+ // SVG drawImage sources at their intrinsic size and would otherwise magnify
197
+ // that bitmap (the "still blurry under zoom" failure). Nesting — rather than
198
+ // editing the inner root's viewBox — keeps percentage lengths and viewport-
199
+ // relative units inside the document resolving against the ORIGINAL viewport.
200
+ // preserveAspectRatio="none" absorbs the sub-pixel aspect drift from integer
201
+ // output sizes (and any anisotropic density caps).
202
+ export const wrapSvgForRaster = (innerXml, cropUserUnits, outWidth, outHeight) => `<svg xmlns="http://www.w3.org/2000/svg" width="${outWidth}" ` +
203
+ `height="${outHeight}" viewBox="${cropUserUnits.x} ${cropUserUnits.y} ` +
204
+ `${cropUserUnits.width} ${cropUserUnits.height}" ` +
205
+ `preserveAspectRatio="none">${innerXml}</svg>`;
206
+ // Skia's SVG module and browsers disagree on the default font-size when a
207
+ // document never specifies one: browsers use the CSS initial value 'medium'
208
+ // (16px), Skia's is larger — unsized <text> renders visibly bigger on native
209
+ // than the same SVG in a browser. Injecting an explicit, inheritable 16 on
210
+ // the root pins the two to the same spec-aligned default; any font-size the
211
+ // document does set (on the root or below) still wins through inheritance.
212
+ // String surgery, not DOM — the native path has no DOMParser.
213
+ export const ensureRootFontSize = (svgText, px = 16) => {
214
+ const match = /<svg\b[^>]*>/i.exec(svgText);
215
+ if (!match)
216
+ return svgText;
217
+ const tag = match[0];
218
+ if (/\bfont-size\s*=/i.test(tag))
219
+ return svgText;
220
+ if (/\bstyle\s*=\s*["'][^"']*font-size/i.test(tag))
221
+ return svgText;
222
+ const selfClosing = tag.endsWith('/>');
223
+ const insertAt = match.index + tag.length - (selfClosing ? 2 : 1);
224
+ return (svgText.slice(0, insertAt) + ` font-size="${px}"` + svgText.slice(insertAt));
225
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
5
  "author": "REEKON Tools",
6
6
  "license": "MIT",
@@ -1,11 +0,0 @@
1
- import { type ReactNode } from 'react';
2
- import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.js';
3
- export type { AnnotationCanvasHandle, GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
4
- export interface CanvasKitOpts {
5
- locateFile?: (file: string) => string;
6
- }
7
- export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
8
- fallback?: ReactNode;
9
- canvasKitOpts?: CanvasKitOpts;
10
- };
11
- export declare const AnnotationCanvas: ({ fallback, canvasKitOpts, ...rest }: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,10 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { WithSkiaWeb } from '@shopify/react-native-skia/lib/module/web/index.js';
3
- // Web-only entry point: lazy-loads CanvasKit wasm via WithSkiaWeb. Imperative
4
- // API (undo/redo/zoom) is exposed through the `imperativeRef` prop rather
5
- // than React refs, since WithSkiaWeb can't forward refs through its lazy
6
- // component boundary. The native build sits in `AnnotationCanvas.native.tsx`
7
- // and skips WithSkiaWeb entirely (no wasm to load).
8
- export const AnnotationCanvas = ({ fallback, canvasKitOpts, ...rest }) => (_jsx(WithSkiaWeb, { getComponent: () => import('./AnnotationCanvasInner.js').then((m) => ({
9
- default: m.AnnotationCanvasInner,
10
- })), fallback: fallback ?? null, componentProps: rest, opts: canvasKitOpts }));
@@ -1,8 +0,0 @@
1
- import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.native.js';
2
- export type { AnnotationCanvasHandle, } from './useAnnotationCanvasState.js';
3
- export type { GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
4
- export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
5
- fallback?: unknown;
6
- canvasKitOpts?: unknown;
7
- };
8
- export declare const AnnotationCanvas: (props: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { AnnotationCanvasInner } from './AnnotationCanvasInner.native.js';
3
- export const AnnotationCanvas = (props) => {
4
- const { fallback: _f, canvasKitOpts: _o, ...rest } = props;
5
- return _jsx(AnnotationCanvasInner, { ...rest });
6
- };
@@ -1,39 +0,0 @@
1
- import { type CSSProperties, type MutableRefObject } from 'react';
2
- import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
3
- import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
4
- import type { MeasurementRef } from './measurementPicker.js';
5
- import type { Tool } from './Tool.js';
6
- import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
7
- import { type ViewportState } from './viewport.js';
8
- import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
9
- export type { AnnotationCanvasHandle };
10
- export interface AnnotationCanvasInnerProps {
11
- canvas: AnnotationCanvasState;
12
- onCommit(patch: AnnotationDocumentPatch): void;
13
- tools: Tool[];
14
- activeToolId: string;
15
- selection: Selection | null;
16
- onSelectionChange(selection: Selection | null): void;
17
- measurements?: Measurement[];
18
- fallbackUnit?: Units;
19
- fractionalTolerance?: FractionalTolerance;
20
- decimalTolerance?: DecimalTolerance;
21
- resolveImageUrl?: (storagePath: string) => Promise<string>;
22
- pickMeasurement?: () => Promise<MeasurementRef | null>;
23
- renderMeasurementStamp?: RenderMeasurementStamp;
24
- stampFontSource?: unknown;
25
- stampValueFontSize?: number;
26
- stampLabelFontSize?: number;
27
- gestures?: GestureConfig;
28
- width: number;
29
- height: number;
30
- initialViewport?: ViewportState;
31
- style?: CSSProperties;
32
- imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
33
- }
34
- export type PanTrigger = 'middleMouse' | 'rightMouse' | 'space';
35
- export interface GestureConfig {
36
- wheel?: 'zoom' | 'pan' | 'auto';
37
- panTriggers?: PanTrigger[];
38
- }
39
- export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,219 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useFont } from '@shopify/react-native-skia';
3
- import { useCallback, useEffect, useRef, } from 'react';
4
- import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
5
- import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
6
- import { STAMP_TILE_SIZE } from './stampLayout.js';
7
- const DEFAULT_PAN_TRIGGERS = ['middleMouse', 'space'];
8
- export const AnnotationCanvasInner = (props) => {
9
- const { fallbackUnit, fractionalTolerance, decimalTolerance, resolveImageUrl, stampFontSource, stampValueFontSize = 14, stampLabelFontSize = 11, gestures, width, height, style, activeToolId, tools, } = props;
10
- const wheelMode = gestures?.wheel ?? 'auto';
11
- const panTriggers = gestures?.panTriggers ?? DEFAULT_PAN_TRIGGERS;
12
- const allowSpacePan = panTriggers.includes('space');
13
- const allowMiddlePan = panTriggers.includes('middleMouse');
14
- const allowRightPan = panTriggers.includes('rightMouse');
15
- const valueFont = useFont(stampFontSource, stampValueFontSize);
16
- const labelFont = useFont(stampFontSource, stampLabelFontSize);
17
- const state = useAnnotationCanvasState(props);
18
- const containerRef = useRef(null);
19
- const panGestureRef = useRef(null);
20
- const spaceDownRef = useRef(false);
21
- const activeTool = tools.find((t) => t.id === activeToolId) ?? null;
22
- const toCanvasPointer = useCallback((event) => {
23
- const rect = containerRef.current?.getBoundingClientRect();
24
- const screen = {
25
- x: event.clientX - (rect?.left ?? 0),
26
- y: event.clientY - (rect?.top ?? 0),
27
- };
28
- return {
29
- pointerId: event.pointerId,
30
- screen,
31
- world: state.ctx.viewport.screenToWorld(screen),
32
- pressure: event.pressure || undefined,
33
- shiftKey: event.shiftKey,
34
- altKey: event.altKey,
35
- metaKey: event.metaKey,
36
- ctrlKey: event.ctrlKey,
37
- };
38
- }, [state.ctx.viewport]);
39
- const isPanTriggerDown = useCallback((event) => {
40
- if (allowMiddlePan && event.button === 1)
41
- return true;
42
- if (allowRightPan && event.button === 2)
43
- return true;
44
- if (allowSpacePan && event.button === 0 && spaceDownRef.current)
45
- return true;
46
- return false;
47
- }, [allowMiddlePan, allowRightPan, allowSpacePan]);
48
- const handlePointerDown = useCallback((event) => {
49
- const rect = containerRef.current?.getBoundingClientRect();
50
- const screen = {
51
- x: event.clientX - (rect?.left ?? 0),
52
- y: event.clientY - (rect?.top ?? 0),
53
- };
54
- if (isPanTriggerDown(event)) {
55
- panGestureRef.current = { pointerId: event.pointerId, lastScreen: screen };
56
- event.currentTarget.setPointerCapture(event.pointerId);
57
- event.preventDefault();
58
- return;
59
- }
60
- event.currentTarget.setPointerCapture(event.pointerId);
61
- state.dispatchPointerDown(toCanvasPointer(event));
62
- }, [state, toCanvasPointer, isPanTriggerDown]);
63
- const handlePointerMove = useCallback((event) => {
64
- const pan = panGestureRef.current;
65
- if (pan && event.pointerId === pan.pointerId) {
66
- const rect = containerRef.current?.getBoundingClientRect();
67
- const screen = {
68
- x: event.clientX - (rect?.left ?? 0),
69
- y: event.clientY - (rect?.top ?? 0),
70
- };
71
- state.pan({
72
- x: screen.x - pan.lastScreen.x,
73
- y: screen.y - pan.lastScreen.y,
74
- });
75
- panGestureRef.current = { pointerId: pan.pointerId, lastScreen: screen };
76
- return;
77
- }
78
- state.dispatchPointerMove(toCanvasPointer(event));
79
- }, [state, toCanvasPointer]);
80
- const handlePointerUp = useCallback((event) => {
81
- const pan = panGestureRef.current;
82
- if (pan && event.pointerId === pan.pointerId) {
83
- panGestureRef.current = null;
84
- return;
85
- }
86
- state.dispatchPointerUp(toCanvasPointer(event));
87
- }, [state, toCanvasPointer]);
88
- const handlePointerCancel = useCallback(() => {
89
- panGestureRef.current = null;
90
- state.dispatchPointerCancel();
91
- }, [state]);
92
- const handleWheel = useCallback((event) => {
93
- const rect = containerRef.current?.getBoundingClientRect();
94
- const focal = {
95
- x: event.clientX - (rect?.left ?? 0),
96
- y: event.clientY - (rect?.top ?? 0),
97
- };
98
- if (event.ctrlKey || event.metaKey) {
99
- const factor = Math.exp(-event.deltaY / 200);
100
- state.zoom(focal, state.ctx.viewport.state.zoom * factor);
101
- return;
102
- }
103
- if (wheelMode === 'zoom') {
104
- const factor = Math.exp(-event.deltaY / 300);
105
- state.zoom(focal, state.ctx.viewport.state.zoom * factor);
106
- return;
107
- }
108
- if (event.shiftKey) {
109
- state.pan({ x: event.deltaY, y: event.deltaX });
110
- return;
111
- }
112
- state.pan({ x: event.deltaX, y: event.deltaY });
113
- }, [state, wheelMode]);
114
- const handleContextMenu = useCallback((event) => {
115
- if (allowRightPan)
116
- event.preventDefault();
117
- }, [allowRightPan]);
118
- useEffect(() => {
119
- const el = containerRef.current;
120
- if (!el)
121
- return;
122
- const stop = (e) => {
123
- if (e.target === el || el.contains(e.target)) {
124
- e.preventDefault();
125
- }
126
- };
127
- el.addEventListener('wheel', stop, { passive: false });
128
- return () => el.removeEventListener('wheel', stop);
129
- }, []);
130
- useEffect(() => {
131
- if (!allowSpacePan)
132
- return;
133
- const onKeyDown = (e) => {
134
- if (e.code === 'Space' && !e.repeat) {
135
- spaceDownRef.current = true;
136
- if (containerRef.current)
137
- containerRef.current.style.cursor = 'grab';
138
- }
139
- };
140
- const onKeyUp = (e) => {
141
- if (e.code === 'Space') {
142
- spaceDownRef.current = false;
143
- if (containerRef.current)
144
- containerRef.current.style.cursor = '';
145
- }
146
- };
147
- window.addEventListener('keydown', onKeyDown);
148
- window.addEventListener('keyup', onKeyUp);
149
- return () => {
150
- window.removeEventListener('keydown', onKeyDown);
151
- window.removeEventListener('keyup', onKeyUp);
152
- };
153
- }, [allowSpacePan]);
154
- const containerStyle = {
155
- position: 'relative',
156
- width,
157
- height,
158
- overflow: 'hidden',
159
- touchAction: 'none',
160
- userSelect: 'none',
161
- cursor: activeTool?.cursor ?? 'default',
162
- ...style,
163
- };
164
- const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
165
- const { renderMeasurementStamp, selection } = props;
166
- return (_jsxs("div", { ref: containerRef, style: containerStyle, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerCancel, onWheel: handleWheel, onContextMenu: handleContextMenu, children: [AnnotationCanvasSkia({
167
- width,
168
- height,
169
- effectiveCanvas: state.effectiveCanvas,
170
- worldTransform: state.worldTransform,
171
- measurementsById: state.measurementsById,
172
- fallbackUnit,
173
- fractionalTolerance,
174
- decimalTolerance,
175
- resolveImageUrl,
176
- valueFont,
177
- labelFont,
178
- hideMeasurementStamps: !!renderMeasurementStamp,
179
- penDrawingStroke: state.penDrawingStroke,
180
- customPreview,
181
- }), renderMeasurementStamp && (_jsx("div", { style: {
182
- position: 'absolute',
183
- inset: 0,
184
- pointerEvents: 'none',
185
- }, children: state.effectiveCanvas.placedMeasurements.map((placed) => {
186
- const size = STAMP_TILE_SIZE * (placed.scale ?? 1);
187
- const cx = (placed.anchor.x - state.viewport.pan.x) * state.viewport.zoom;
188
- const cy = (placed.anchor.y - state.viewport.pan.y) * state.viewport.zoom;
189
- const isSelected = selection?.ids.includes(placed.id) ?? false;
190
- return (_jsxs("div", { style: {
191
- position: 'absolute',
192
- left: 0,
193
- top: 0,
194
- width: size,
195
- height: size,
196
- transform: `translate(${cx - size / 2}px, ${cy - size / 2}px)`,
197
- }, children: [renderMeasurementStamp({
198
- placed,
199
- measurement: state.measurementsById.get(placed.measurementId) ?? null,
200
- selected: isSelected,
201
- size,
202
- zoom: state.viewport.zoom,
203
- }), isSelected && (_jsx("div", { role: "button", "aria-label": "Remove measurement", onPointerDown: (e) => {
204
- e.stopPropagation();
205
- state.ctx.commit({
206
- ops: [{ op: 'removeMeasurement', id: placed.id }],
207
- });
208
- state.ctx.setSelection(null);
209
- }, style: {
210
- position: 'absolute',
211
- top: -10,
212
- right: -10,
213
- width: 40,
214
- height: 40,
215
- cursor: 'pointer',
216
- pointerEvents: 'auto',
217
- } }))] }, placed.id));
218
- }) }))] }));
219
- };
@@ -1,35 +0,0 @@
1
- import { type MutableRefObject } from 'react';
2
- import { type ViewStyle } from 'react-native';
3
- import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
4
- import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
5
- import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
6
- import type { MeasurementRef } from './measurementPicker.js';
7
- import type { Tool } from './Tool.js';
8
- import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
9
- import type { ViewportState } from './viewport.js';
10
- export type { AnnotationCanvasHandle };
11
- export interface AnnotationCanvasInnerProps {
12
- canvas: AnnotationCanvasState;
13
- onCommit(patch: AnnotationDocumentPatch): void;
14
- tools: Tool[];
15
- activeToolId: string;
16
- selection: Selection | null;
17
- onSelectionChange(selection: Selection | null): void;
18
- measurements?: Measurement[];
19
- fallbackUnit?: Units;
20
- fractionalTolerance?: FractionalTolerance;
21
- decimalTolerance?: DecimalTolerance;
22
- resolveImageUrl?: (storagePath: string) => Promise<string>;
23
- pickMeasurement?: () => Promise<MeasurementRef | null>;
24
- renderMeasurementStamp?: RenderMeasurementStamp;
25
- stampFontSource?: unknown;
26
- stampValueFontSize?: number;
27
- stampLabelFontSize?: number;
28
- gestures?: unknown;
29
- width: number;
30
- height: number;
31
- initialViewport?: ViewportState;
32
- style?: ViewStyle;
33
- imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
34
- }
35
- export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;