@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.
- package/dist/annotation/canvas/AnnotationCanvasInner.js +1 -0
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +3 -1
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +2 -2
- package/dist/annotation/canvas/elements/BackgroundImageElement.d.ts +5 -1
- package/dist/annotation/canvas/elements/BackgroundImageElement.js +13 -99
- package/dist/annotation/canvas/elements/BackgroundSvg.d.ts +13 -0
- package/dist/annotation/canvas/elements/BackgroundSvg.js +337 -0
- package/dist/annotation/canvas/elements/backgroundUrl.d.ts +2 -0
- package/dist/annotation/canvas/elements/backgroundUrl.js +38 -0
- package/dist/annotation/canvas/svgRasterPlan.d.ts +56 -0
- package/dist/annotation/canvas/svgRasterPlan.js +225 -0
- package/package.json +1 -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/Tool.js +0 -1
- 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
|
@@ -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,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;
|