@reekon-tools/boldr-utils 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +48 -7
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +2 -2
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +16 -3
- package/dist/annotation/canvas/Tool.d.ts +8 -1
- package/dist/annotation/canvas/backgroundLayers.d.ts +20 -0
- package/dist/annotation/canvas/backgroundLayers.js +98 -0
- package/dist/annotation/canvas/elements/BackgroundImageElement.d.ts +10 -7
- package/dist/annotation/canvas/elements/BackgroundImageElement.js +45 -11
- package/dist/annotation/canvas/elements/BackgroundSvg.d.ts +3 -4
- package/dist/annotation/canvas/elements/BackgroundSvg.js +23 -29
- package/dist/annotation/canvas/elements/backgroundUrl.js +2 -2
- package/dist/annotation/canvas/groupHeaderLayout.d.ts +72 -0
- package/dist/annotation/canvas/groupHeaderLayout.js +115 -0
- package/dist/annotation/canvas/stampLayout.d.ts +15 -2
- package/dist/annotation/canvas/stampLayout.js +46 -12
- package/dist/annotation/canvas/tools/selectTool.js +183 -19
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +1 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +40 -20
- package/dist/annotation/canvas/viewport.js +7 -6
- package/dist/annotation/data/AnnotationDataProvider.d.ts +1 -1
- package/dist/annotation/data/InMemoryAnnotationProvider.d.ts +1 -1
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.d.ts +7 -1
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +124 -22
- package/dist/annotation/data/hooks/useAnnotationMutations.d.ts +1 -1
- package/dist/canvas/AnnotationCanvas.d.ts +11 -0
- package/dist/canvas/AnnotationCanvas.js +10 -0
- package/dist/canvas/AnnotationCanvas.native.d.ts +8 -0
- package/dist/canvas/AnnotationCanvas.native.js +6 -0
- package/dist/canvas/AnnotationCanvasInner.d.ts +39 -0
- package/dist/canvas/AnnotationCanvasInner.js +219 -0
- package/dist/canvas/AnnotationCanvasInner.native.d.ts +35 -0
- package/dist/canvas/AnnotationCanvasInner.native.js +138 -0
- package/dist/canvas/AnnotationCanvasSkia.d.ts +27 -0
- package/dist/canvas/AnnotationCanvasSkia.js +20 -0
- package/dist/canvas/Tool.d.ts +38 -0
- package/dist/canvas/Tool.js +1 -0
- package/dist/canvas/elements/BackgroundImageElement.d.ts +9 -0
- package/dist/canvas/elements/BackgroundImageElement.js +37 -0
- package/dist/canvas/elements/MeasurementStampElement.d.ts +13 -0
- package/dist/canvas/elements/MeasurementStampElement.js +30 -0
- package/dist/canvas/elements/ShapeElement.d.ts +7 -0
- package/dist/canvas/elements/ShapeElement.js +62 -0
- package/dist/canvas/elements/StrokeElement.d.ts +7 -0
- package/dist/canvas/elements/StrokeElement.js +18 -0
- package/dist/canvas/measurementPicker.d.ts +10 -0
- package/dist/canvas/measurementPicker.js +1 -0
- package/dist/canvas/measurementStampOverlay.d.ts +11 -0
- package/dist/canvas/measurementStampOverlay.js +1 -0
- package/dist/canvas/pointerAdapter.d.ts +3 -0
- package/dist/canvas/pointerAdapter.js +19 -0
- package/dist/canvas/stampLayout.d.ts +5 -0
- package/dist/canvas/stampLayout.js +14 -0
- package/dist/canvas/tools/measurementStampTool.d.ts +9 -0
- package/dist/canvas/tools/measurementStampTool.js +37 -0
- package/dist/canvas/tools/panTool.d.ts +5 -0
- package/dist/canvas/tools/panTool.js +25 -0
- package/dist/canvas/tools/penTool.d.ts +13 -0
- package/dist/canvas/tools/penTool.js +68 -0
- package/dist/canvas/tools/selectTool.d.ts +2 -0
- package/dist/canvas/tools/selectTool.js +182 -0
- package/dist/canvas/useAnnotationCanvasState.d.ts +54 -0
- package/dist/canvas/useAnnotationCanvasState.js +210 -0
- package/dist/canvas/viewport.d.ts +16 -0
- package/dist/canvas/viewport.js +54 -0
- package/dist/data/AnnotationDataContext.d.ts +8 -0
- package/dist/data/AnnotationDataContext.js +11 -0
- package/dist/data/AnnotationDataProvider.d.ts +65 -0
- package/dist/data/AnnotationDataProvider.js +4 -0
- package/dist/data/InMemoryAnnotationProvider.d.ts +30 -0
- package/dist/data/InMemoryAnnotationProvider.js +197 -0
- package/dist/data/canvasPersistence.d.ts +3 -0
- package/dist/data/canvasPersistence.js +26 -0
- package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +33 -0
- package/dist/data/hooks/useAnnotationCanvasDoc.js +314 -0
- package/dist/data/hooks/useAnnotationDoc.d.ts +7 -0
- package/dist/data/hooks/useAnnotationDoc.js +33 -0
- package/dist/data/hooks/useAnnotationList.d.ts +7 -0
- package/dist/data/hooks/useAnnotationList.js +26 -0
- package/dist/data/hooks/useAnnotationMutations.d.ts +9 -0
- package/dist/data/hooks/useAnnotationMutations.js +11 -0
- package/dist/exports.d.ts +3 -1
- package/dist/exports.js +3 -1
- package/dist/hooks/useParseMeasurement.d.ts +4 -0
- package/dist/hooks/useParseMeasurement.js +14 -0
- package/dist/types/annotation.d.ts +28 -0
- package/dist/types/annotation.js +68 -0
- package/dist/utils/evaluateFormula.d.ts +20 -0
- package/dist/utils/evaluateFormula.js +31 -0
- package/package.json +1 -1
- package/dist/theme/index.d.ts +0 -3
- package/dist/theme/index.js +0 -3
- package/dist/theme/preset.d.ts +0 -120
- package/dist/theme/preset.js +0 -25
- package/dist/theme/semantic.d.ts +0 -136
- package/dist/theme/semantic.js +0 -116
|
@@ -18,7 +18,7 @@ export declare class InMemoryAnnotationProvider implements AnnotationDataProvide
|
|
|
18
18
|
list(scope: AnnotationScope, onNext: (files: AnnotationFileSummary[]) => void): Unsubscribe;
|
|
19
19
|
subscribeGroupMeasurements(scope: JobGroupScope, onNext: (measurements: Measurement[]) => void): Unsubscribe;
|
|
20
20
|
subscribeJobMeasurements(scope: JobScope, onNext: (measurements: Measurement[]) => void): Unsubscribe;
|
|
21
|
-
uploadImage(scope: AnnotationScope, fileId: string, role:
|
|
21
|
+
uploadImage(scope: AnnotationScope, fileId: string, role: string, blob: ImageBlob): Promise<UploadedImageRef>;
|
|
22
22
|
getImageUrl(_scope: AnnotationScope, fileId: string, storagePath: string): Promise<string>;
|
|
23
23
|
deleteImage(_scope: AnnotationScope, fileId: string, storagePath: string): Promise<void>;
|
|
24
24
|
private getBucket;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationViewport, type BackgroundFit } from '../../../types/annotation.js';
|
|
1
|
+
import { type AnnotationBackgroundLayer, type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationViewport, type BackgroundFit } from '../../../types/annotation.js';
|
|
2
2
|
import type { AnnotationScope, ImageBlob } from '../AnnotationDataProvider.js';
|
|
3
3
|
export type SaveStatus = 'idle' | 'dirty' | 'saving' | 'saved' | 'error';
|
|
4
4
|
export interface UseAnnotationCanvasDocOptions {
|
|
@@ -26,6 +26,12 @@ export interface UseAnnotationCanvasDocResult {
|
|
|
26
26
|
save: () => Promise<void>;
|
|
27
27
|
refreshThumbnail: () => Promise<void>;
|
|
28
28
|
ensureFileId: () => Promise<string>;
|
|
29
|
+
addBackgroundImage: (blob: ImageBlob, dims: {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
}) => Promise<string>;
|
|
33
|
+
updateBackgroundImage: (id: string, patch: Partial<Omit<AnnotationBackgroundLayer, 'id'>>) => void;
|
|
34
|
+
removeBackgroundImage: (id: string) => Promise<void>;
|
|
29
35
|
setBackgroundImage: (blob: ImageBlob, dims: {
|
|
30
36
|
width: number;
|
|
31
37
|
height: number;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { applyPatch, createEmptyCanvasState, } from '../../../types/annotation.js';
|
|
3
|
+
import { backgroundLayersOf, placeNewBackgroundLayer, } from '../../canvas/backgroundLayers.js';
|
|
4
|
+
import { imageDocRect } from '../../canvas/viewport.js';
|
|
3
5
|
import { FileUploadType } from '../../../types/firestore.js';
|
|
4
6
|
import { useAnnotationDoc } from './useAnnotationDoc.js';
|
|
5
7
|
import { useAnnotationMutations } from './useAnnotationMutations.js';
|
|
@@ -26,6 +28,15 @@ const buildFileData = (fileType, isLabel, canvas, canvasRev) => ({
|
|
|
26
28
|
// AnnotationFileData). Uniqueness only needs to hold across the handful of
|
|
27
29
|
// clients that ever touch one annotation doc.
|
|
28
30
|
const makeClientId = () => `${Date.now().toString(36)}-${Math.floor(Math.random() * 0x100000000).toString(36)}`;
|
|
31
|
+
// Background layer ids carry the same uniqueness contract as the tools'
|
|
32
|
+
// stroke/shape ids (timestamp + module counter): unique within one document
|
|
33
|
+
// across the handful of clients that touch it. Never 'background-legacy' —
|
|
34
|
+
// that id is reserved for the layer synthesized from the legacy single slot.
|
|
35
|
+
let backgroundIdCounter = 0;
|
|
36
|
+
const makeBackgroundLayerId = () => `background-${Date.now().toString(36)}-${(backgroundIdCounter++).toString(36)}`;
|
|
37
|
+
// The renderer decodes 'svg' via Skia's SVG module and everything else via
|
|
38
|
+
// the bitmap decoder, so an SVG left unstamped silently fails to draw.
|
|
39
|
+
const backgroundFormatOf = (blob) => blob.contentType === 'image/svg+xml' ? 'svg' : 'raster';
|
|
29
40
|
// Orchestrates load + auto-save for the annotation canvas. Hydrates the working
|
|
30
41
|
// state from the persisted doc, applies commits optimistically, and persists
|
|
31
42
|
// (debounced) through the data provider — creating the file on first save when
|
|
@@ -354,45 +365,133 @@ export const useAnnotationCanvasDoc = (options) => {
|
|
|
354
365
|
}
|
|
355
366
|
return id;
|
|
356
367
|
}, [flush, fallbackViewport]);
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
const
|
|
368
|
+
const addBackgroundImage = useCallback(async (blob, dims) => {
|
|
369
|
+
const fileId = await ensureFileId();
|
|
370
|
+
const layerId = makeBackgroundLayerId();
|
|
371
|
+
const ref = await uploadImage(fileId, `backgrounds/${layerId}`, blob);
|
|
372
|
+
// ensureFileId seeds the working state for a never-saved canvas, but a
|
|
373
|
+
// still-hydrating existing doc can reach here with no working state —
|
|
374
|
+
// fall back to the same empty canvas hydration would start from.
|
|
375
|
+
const { viewport } = workingRef.current ?? createEmptyCanvasState(fallbackViewport);
|
|
360
376
|
onCommit({
|
|
361
377
|
ops: [
|
|
362
378
|
{
|
|
363
|
-
op: '
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
379
|
+
op: 'addBackgroundImage',
|
|
380
|
+
image: {
|
|
381
|
+
id: layerId,
|
|
382
|
+
storagePath: ref.storagePath,
|
|
383
|
+
downloadUrl: ref.downloadUrl,
|
|
384
|
+
widthPx: dims.width,
|
|
385
|
+
heightPx: dims.height,
|
|
386
|
+
format: backgroundFormatOf(blob),
|
|
387
|
+
transform: placeNewBackgroundLayer(backgroundLayersOf(viewport), dims, viewport),
|
|
372
388
|
},
|
|
373
389
|
},
|
|
374
390
|
],
|
|
375
391
|
});
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
392
|
+
return layerId;
|
|
393
|
+
}, [ensureFileId, uploadImage, onCommit, fallbackViewport]);
|
|
394
|
+
const updateBackgroundImage = useCallback((id, patch) => {
|
|
395
|
+
onCommit({ ops: [{ op: 'updateBackgroundImage', id, patch }] });
|
|
396
|
+
}, [onCommit]);
|
|
397
|
+
const removeBackgroundImage = useCallback(async (id) => {
|
|
398
|
+
const viewport = workingRef.current?.viewport;
|
|
399
|
+
const layer = viewport
|
|
400
|
+
? backgroundLayersOf(viewport).find((l) => l.id === id)
|
|
401
|
+
: undefined;
|
|
402
|
+
const fileId = fileIdRef.current ?? createdIdRef.current;
|
|
403
|
+
if (layer && fileId) {
|
|
381
404
|
try {
|
|
382
|
-
await deleteImage(
|
|
405
|
+
await deleteImage(fileId, layer.storagePath);
|
|
383
406
|
}
|
|
384
407
|
catch (e) {
|
|
385
|
-
// Non-fatal: still
|
|
386
|
-
//
|
|
408
|
+
// Non-fatal: still remove the layer even if the storage object is
|
|
409
|
+
// already gone.
|
|
387
410
|
console.warn('[useAnnotationCanvasDoc] failed to delete background image', e);
|
|
388
411
|
}
|
|
389
412
|
}
|
|
413
|
+
onCommit({ ops: [{ op: 'removeBackgroundImage', id }] });
|
|
414
|
+
}, [deleteImage, onCommit]);
|
|
415
|
+
const setBackgroundImage = useCallback(async (blob, dims, fit = 'contain') => {
|
|
416
|
+
const fileId = await ensureFileId();
|
|
417
|
+
const prior = workingRef.current
|
|
418
|
+
? backgroundLayersOf(workingRef.current.viewport)
|
|
419
|
+
: [];
|
|
420
|
+
// Upload before touching the prior layers' objects: a failed upload
|
|
421
|
+
// must leave the doc — and every object it references — intact. The
|
|
422
|
+
// prior objects are deleted only after the replacement is committed.
|
|
423
|
+
const layerId = makeBackgroundLayerId();
|
|
424
|
+
const ref = await uploadImage(fileId, `backgrounds/${layerId}`, blob);
|
|
425
|
+
const { viewport } = workingRef.current ?? createEmptyCanvasState(fallbackViewport);
|
|
426
|
+
// The fit argument still decides where the replacement lands (the same
|
|
427
|
+
// imageDocRect placement the legacy single slot rendered at), so
|
|
428
|
+
// existing callers keep their visual behavior. `backgroundFit` is also
|
|
429
|
+
// still written: old clients render the mirrored legacy slot with it.
|
|
430
|
+
const rect = imageDocRect(dims.width, dims.height, viewport.width, viewport.height, fit);
|
|
390
431
|
onCommit({
|
|
391
432
|
ops: [
|
|
433
|
+
...prior.map((l) => ({
|
|
434
|
+
op: 'removeBackgroundImage',
|
|
435
|
+
id: l.id,
|
|
436
|
+
})),
|
|
392
437
|
{
|
|
393
|
-
op: '
|
|
394
|
-
|
|
438
|
+
op: 'addBackgroundImage',
|
|
439
|
+
image: {
|
|
440
|
+
id: layerId,
|
|
441
|
+
storagePath: ref.storagePath,
|
|
442
|
+
downloadUrl: ref.downloadUrl,
|
|
443
|
+
widthPx: dims.width,
|
|
444
|
+
heightPx: dims.height,
|
|
445
|
+
format: backgroundFormatOf(blob),
|
|
446
|
+
transform: {
|
|
447
|
+
x: rect.x,
|
|
448
|
+
y: rect.y,
|
|
449
|
+
scaleX: dims.width > 0 ? rect.width / dims.width : 1,
|
|
450
|
+
scaleY: dims.height > 0 ? rect.height / dims.height : 1,
|
|
451
|
+
},
|
|
452
|
+
},
|
|
395
453
|
},
|
|
454
|
+
{ op: 'setViewport', patch: { backgroundFit: fit } },
|
|
455
|
+
],
|
|
456
|
+
});
|
|
457
|
+
for (const layer of prior) {
|
|
458
|
+
try {
|
|
459
|
+
await deleteImage(fileId, layer.storagePath);
|
|
460
|
+
}
|
|
461
|
+
catch (e) {
|
|
462
|
+
// Non-fatal: the replacement is already committed; an undeleted
|
|
463
|
+
// prior object is merely orphaned.
|
|
464
|
+
console.warn('[useAnnotationCanvasDoc] failed to delete background image', e);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}, [ensureFileId, uploadImage, deleteImage, onCommit, fallbackViewport]);
|
|
468
|
+
const clearBackgroundImage = useCallback(async () => {
|
|
469
|
+
const layers = workingRef.current
|
|
470
|
+
? backgroundLayersOf(workingRef.current.viewport)
|
|
471
|
+
: [];
|
|
472
|
+
const id = fileIdRef.current ?? createdIdRef.current;
|
|
473
|
+
if (id) {
|
|
474
|
+
for (const layer of layers) {
|
|
475
|
+
try {
|
|
476
|
+
await deleteImage(id, layer.storagePath);
|
|
477
|
+
}
|
|
478
|
+
catch (e) {
|
|
479
|
+
// Non-fatal: still clear the viewport reference even if the storage
|
|
480
|
+
// object is already gone.
|
|
481
|
+
console.warn('[useAnnotationCanvasDoc] failed to delete background image', e);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
onCommit({
|
|
486
|
+
ops: [
|
|
487
|
+
...layers.map((l) => ({
|
|
488
|
+
op: 'removeBackgroundImage',
|
|
489
|
+
id: l.id,
|
|
490
|
+
})),
|
|
491
|
+
// A fit means nothing without a background; clearing it preserves
|
|
492
|
+
// this method's original contract. The explicit `undefined` is
|
|
493
|
+
// stripped by the write path's JSON round-trip.
|
|
494
|
+
{ op: 'setViewport', patch: { backgroundFit: undefined } },
|
|
396
495
|
],
|
|
397
496
|
});
|
|
398
497
|
}, [deleteImage, onCommit]);
|
|
@@ -414,6 +513,9 @@ export const useAnnotationCanvasDoc = (options) => {
|
|
|
414
513
|
save,
|
|
415
514
|
refreshThumbnail,
|
|
416
515
|
ensureFileId,
|
|
516
|
+
addBackgroundImage,
|
|
517
|
+
updateBackgroundImage,
|
|
518
|
+
removeBackgroundImage,
|
|
417
519
|
setBackgroundImage,
|
|
418
520
|
clearBackgroundImage,
|
|
419
521
|
};
|
|
@@ -3,7 +3,7 @@ export interface AnnotationMutations {
|
|
|
3
3
|
create(seed: Partial<AnnotationFile>): Promise<string>;
|
|
4
4
|
update(fileId: string, patch: AnnotationFilePatch): Promise<void>;
|
|
5
5
|
remove(fileId: string): Promise<void>;
|
|
6
|
-
uploadImage(fileId: string, role:
|
|
6
|
+
uploadImage(fileId: string, role: string, blob: ImageBlob): Promise<UploadedImageRef>;
|
|
7
7
|
deleteImage(fileId: string, storagePath: string): Promise<void>;
|
|
8
8
|
}
|
|
9
9
|
export declare const useAnnotationMutations: (scope: AnnotationScope) => AnnotationMutations;
|
|
@@ -0,0 +1,11 @@
|
|
|
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;
|
|
@@ -0,0 +1,10 @@
|
|
|
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 }));
|
|
@@ -0,0 +1,8 @@
|
|
|
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;
|
|
@@ -0,0 +1,6 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
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;
|
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
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;
|