@reekon-tools/boldr-utils 1.19.4 → 1.19.6
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.d.ts +2 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.js +3 -1
- package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +2 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +118 -8
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +23 -1
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +89 -13
- package/dist/annotation/canvas/Tool.d.ts +4 -1
- package/dist/annotation/canvas/backgroundLayers.d.ts +20 -1
- package/dist/annotation/canvas/backgroundLayers.js +94 -3
- package/dist/annotation/canvas/elements/BackgroundImageElement.d.ts +6 -3
- package/dist/annotation/canvas/elements/BackgroundImageElement.js +16 -8
- package/dist/annotation/canvas/elements/BackgroundSvg.d.ts +1 -0
- package/dist/annotation/canvas/elements/BackgroundSvg.js +4 -4
- package/dist/annotation/canvas/elements/backgroundUrl.d.ts +2 -1
- package/dist/annotation/canvas/elements/backgroundUrl.js +15 -2
- package/dist/annotation/canvas/fileTileSwap.d.ts +12 -0
- package/dist/annotation/canvas/fileTileSwap.js +87 -0
- package/dist/annotation/canvas/stampLayout.d.ts +1 -1
- package/dist/annotation/canvas/stampLayout.js +10 -8
- package/dist/annotation/canvas/tools/selectTool.d.ts +18 -0
- package/dist/annotation/canvas/tools/selectTool.js +158 -42
- package/dist/annotation/canvas/useAnnotationCanvasState.js +3 -1
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +5 -1
- package/dist/exports.d.ts +1 -0
- package/dist/exports.js +1 -0
- package/dist/types/annotation.d.ts +10 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Group, Image, useImage, } from '@shopify/react-native-skia';
|
|
2
|
+
import { Group, Image, rect, useImage, } from '@shopify/react-native-skia';
|
|
3
3
|
import { memo, useEffect, useRef } from 'react';
|
|
4
4
|
import { backgroundLayerDocRect } from '../backgroundLayers.js';
|
|
5
5
|
import { useBackgroundUrl } from './backgroundUrl.js';
|
|
@@ -11,8 +11,8 @@ import { BackgroundSvgElement } from './BackgroundSvg.js';
|
|
|
11
11
|
// re-decodes. Returns null until loaded, or when `image` is null/undefined.
|
|
12
12
|
// Hooks run unconditionally regardless of `image`. Raster only — SVG
|
|
13
13
|
// backgrounds self-load in BackgroundSvgElement (parsing an SVG is cheap).
|
|
14
|
-
export const useBackgroundSkImage = (image, resolveUrl) => {
|
|
15
|
-
const url = useBackgroundUrl(image, resolveUrl);
|
|
14
|
+
export const useBackgroundSkImage = (image, resolveUrl, deferToResolver) => {
|
|
15
|
+
const url = useBackgroundUrl(image, resolveUrl, deferToResolver);
|
|
16
16
|
return useImage(url || null);
|
|
17
17
|
};
|
|
18
18
|
// Null-rendering decode host: one mounted instance per background layer, so a
|
|
@@ -20,10 +20,10 @@ export const useBackgroundSkImage = (image, resolveUrl) => {
|
|
|
20
20
|
// mapping over hooks. The consumer (the native Inner) mounts one per layer,
|
|
21
21
|
// accumulates the reported images into a Record keyed by layer id, and feeds
|
|
22
22
|
// that to every canvas sharing the decode (main + magnifier loupe).
|
|
23
|
-
export const BackgroundSkImageLoader = ({ layer, resolveUrl, onImage, }) => {
|
|
23
|
+
export const BackgroundSkImageLoader = ({ layer, resolveUrl, deferToResolver, onImage, }) => {
|
|
24
24
|
// SVG layers never draw through an SkImage (BackgroundSvgElement owns its
|
|
25
25
|
// own vector pipeline), so skip the decode and report null.
|
|
26
|
-
const image = useBackgroundSkImage(layer.format === 'svg' ? null : layer, resolveUrl);
|
|
26
|
+
const image = useBackgroundSkImage(layer.format === 'svg' ? null : layer, resolveUrl, deferToResolver);
|
|
27
27
|
// The callback rides a ref so an inline consumer closure doesn't re-report
|
|
28
28
|
// (and re-render the collector) every render.
|
|
29
29
|
const onImageRef = useRef(onImage);
|
|
@@ -38,12 +38,12 @@ export const BackgroundSkImageLoader = ({ layer, resolveUrl, onImage, }) => {
|
|
|
38
38
|
// A component (not inlined into the scene helper) because each layer owns
|
|
39
39
|
// per-image hooks — URL resolution and raster decode — and AnnotationCanvasSkia
|
|
40
40
|
// is a hook-free plain function.
|
|
41
|
-
export const BackgroundLayerElement = memo(({ layer, resolveUrl, skImage: skImageProp, viewport, screenWidth, screenHeight, }) => {
|
|
41
|
+
export const BackgroundLayerElement = memo(({ layer, resolveUrl, deferToResolver, skImage: skImageProp, viewport, screenWidth, screenHeight, suppressCrop, }) => {
|
|
42
42
|
const isSvg = layer.format === 'svg';
|
|
43
43
|
// Self-load only when no pre-decoded image is provided; the hook still runs
|
|
44
44
|
// unconditionally (with a null source, so it no-ops) to satisfy hook rules.
|
|
45
45
|
const provided = skImageProp !== undefined;
|
|
46
|
-
const loaded = useBackgroundSkImage(provided || isSvg ? null : layer, resolveUrl);
|
|
46
|
+
const loaded = useBackgroundSkImage(provided || isSvg ? null : layer, resolveUrl, deferToResolver);
|
|
47
47
|
const skImage = provided ? skImageProp : loaded;
|
|
48
48
|
const t = layer.transform;
|
|
49
49
|
let content = null;
|
|
@@ -53,13 +53,21 @@ export const BackgroundLayerElement = memo(({ layer, resolveUrl, skImage: skImag
|
|
|
53
53
|
// through the browser at viewport resolution. Both draw at intrinsic
|
|
54
54
|
// size — the Group below owns the placement — but the web rasterizer
|
|
55
55
|
// still needs the doc rect to crop against the visible region.
|
|
56
|
-
content = (_jsx(BackgroundSvgElement, { image: layer, dims: backgroundLayerDocRect(layer), resolveUrl: resolveUrl, viewport: viewport, screenWidth: screenWidth, screenHeight: screenHeight }));
|
|
56
|
+
content = (_jsx(BackgroundSvgElement, { image: layer, dims: backgroundLayerDocRect(layer), resolveUrl: resolveUrl, deferToResolver: deferToResolver, viewport: viewport, screenWidth: screenWidth, screenHeight: screenHeight }));
|
|
57
57
|
}
|
|
58
58
|
else if (skImage) {
|
|
59
59
|
content = (_jsx(Image, { image: skImage, x: 0, y: 0, width: layer.widthPx, height: layer.heightPx, fit: "fill" }));
|
|
60
60
|
}
|
|
61
61
|
if (!content)
|
|
62
62
|
return null;
|
|
63
|
+
// Committed crop window: clip in the layer's intrinsic pixel space (inside
|
|
64
|
+
// the transform, so it scales with the image). Non-destructive — the full
|
|
65
|
+
// image is drawn, the clip just windows it. See suppressCrop for the one
|
|
66
|
+
// case that skips it.
|
|
67
|
+
const crop = !suppressCrop && layer.crop ? layer.crop : null;
|
|
68
|
+
if (crop) {
|
|
69
|
+
content = (_jsx(Group, { clip: rect(crop.x, crop.y, crop.width, crop.height), children: content }));
|
|
70
|
+
}
|
|
63
71
|
return (_jsx(Group, { transform: [
|
|
64
72
|
{ translateX: t.x },
|
|
65
73
|
{ translateY: t.y },
|
|
@@ -116,8 +116,8 @@ const useBackgroundSkSvg = (url) => {
|
|
|
116
116
|
}, []);
|
|
117
117
|
return svg;
|
|
118
118
|
};
|
|
119
|
-
const SkSvgBackground = ({ image, resolveUrl }) => {
|
|
120
|
-
const url = useBackgroundUrl(image, resolveUrl);
|
|
119
|
+
const SkSvgBackground = ({ image, resolveUrl, deferToResolver, }) => {
|
|
120
|
+
const url = useBackgroundUrl(image, resolveUrl, deferToResolver);
|
|
121
121
|
const svg = useBackgroundSkSvg(url);
|
|
122
122
|
if (!svg)
|
|
123
123
|
return null;
|
|
@@ -169,8 +169,8 @@ const loadSvgImage = (xml) => new Promise((resolve, reject) => {
|
|
|
169
169
|
};
|
|
170
170
|
img.src = objectUrl;
|
|
171
171
|
});
|
|
172
|
-
const BrowserSvgBackground = ({ image, dims, resolveUrl, viewport, screenWidth, screenHeight, }) => {
|
|
173
|
-
const url = useBackgroundUrl(image, resolveUrl);
|
|
172
|
+
const BrowserSvgBackground = ({ image, dims, resolveUrl, deferToResolver, viewport, screenWidth, screenHeight, }) => {
|
|
173
|
+
const url = useBackgroundUrl(image, resolveUrl, deferToResolver);
|
|
174
174
|
const [svgText, setSvgText] = useState(null);
|
|
175
175
|
useEffect(() => {
|
|
176
176
|
if (!url) {
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { AnnotationBackgroundImage } from '../../../types/annotation.js';
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const initialBackgroundUrl: (image: AnnotationBackgroundImage | null | undefined, resolveUrl: ((path: string) => Promise<string>) | undefined, deferToResolver: boolean | undefined) => string;
|
|
3
|
+
export declare const useBackgroundUrl: (image: AnnotationBackgroundImage | null | undefined, resolveUrl?: (path: string) => Promise<string>, deferToResolver?: boolean) => string;
|
|
@@ -2,8 +2,21 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
// Resolve a background image's (possibly expired) storage URL. Shared by the
|
|
3
3
|
// raster loader (BackgroundLayerElement / BackgroundSkImageLoader) and both
|
|
4
4
|
// SVG render paths (BackgroundSvg).
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
//
|
|
6
|
+
// `deferToResolver` — start from '' instead of seeding the doc's stored
|
|
7
|
+
// `downloadUrl` while the resolver runs. A consumer whose resolver is
|
|
8
|
+
// LOCAL-FIRST (a cached copy on disk answers without the network) must set
|
|
9
|
+
// this: the seed is handed to the image loader on the very first render, so
|
|
10
|
+
// it starts a full-resolution download from Storage that the local answer
|
|
11
|
+
// then races — wasted bandwidth at best, and the loader keeps whichever
|
|
12
|
+
// result lands LAST, so the remote fetch can override the local decode.
|
|
13
|
+
// A consumer whose resolver is network-only (web/desktop re-resolving a
|
|
14
|
+
// possibly-expired URL) should leave it off: there the seed is a warm start —
|
|
15
|
+
// the resolver normally confirms the same URL, and the early fetch is the
|
|
16
|
+
// image download it would have started anyway.
|
|
17
|
+
export const initialBackgroundUrl = (image, resolveUrl, deferToResolver) => (deferToResolver && resolveUrl ? '' : (image?.downloadUrl ?? ''));
|
|
18
|
+
export const useBackgroundUrl = (image, resolveUrl, deferToResolver) => {
|
|
19
|
+
const [url, setUrl] = useState(() => initialBackgroundUrl(image, resolveUrl, deferToResolver));
|
|
7
20
|
// Hold resolveUrl in a ref so re-resolution is keyed only on the image
|
|
8
21
|
// identity, not the function's. Consumers commonly pass an inline closure
|
|
9
22
|
// (a new reference each render); depending on it directly would re-run a
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationElementId } from '../../types/annotation.js';
|
|
2
|
+
import type { FileUpload } from '../../types/firestore.js';
|
|
3
|
+
export interface FileTileImageSource {
|
|
4
|
+
storagePath: string;
|
|
5
|
+
downloadUrl: string;
|
|
6
|
+
widthPx: number;
|
|
7
|
+
heightPx: number;
|
|
8
|
+
format?: 'raster' | 'svg';
|
|
9
|
+
}
|
|
10
|
+
export declare const fileTileImageSource: (file: Pick<FileUpload, "type" | "fileData"> | null | undefined) => FileTileImageSource | null;
|
|
11
|
+
export declare const expandFileTileToBackground: (doc: AnnotationCanvasState, placedId: AnnotationElementId, layerId: AnnotationElementId, image: FileTileImageSource) => AnnotationDocumentPatch | null;
|
|
12
|
+
export declare const collapseBackgroundToFileTile: (doc: AnnotationCanvasState, layerId: AnnotationElementId, tileId: AnnotationElementId, now?: number) => AnnotationDocumentPatch | null;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DEFAULT_LAYER_ID, } from '../../types/annotation.js';
|
|
2
|
+
import { backgroundLayerDocRect, backgroundLayersOf, } from './backgroundLayers.js';
|
|
3
|
+
import { imageDocRect } from './viewport.js';
|
|
4
|
+
export const fileTileImageSource = (file) => {
|
|
5
|
+
if (!file)
|
|
6
|
+
return null;
|
|
7
|
+
// Structural, not type-switched: Canvas, Annotation and Template docs all
|
|
8
|
+
// carry AnnotationFileData; anything else (Label, Note, Calculator, …) has
|
|
9
|
+
// no `canvas` key and falls out here.
|
|
10
|
+
const data = file.fileData;
|
|
11
|
+
if (!data?.canvas || data.isLabel)
|
|
12
|
+
return null;
|
|
13
|
+
if (data.canvas.viewport.pdfDocument)
|
|
14
|
+
return null;
|
|
15
|
+
const raster = backgroundLayersOf(data.canvas.viewport).find((l) => l.format !== 'svg' && l.storagePath && l.widthPx > 0);
|
|
16
|
+
if (!raster)
|
|
17
|
+
return null;
|
|
18
|
+
return {
|
|
19
|
+
storagePath: raster.storagePath,
|
|
20
|
+
downloadUrl: raster.downloadUrl,
|
|
21
|
+
widthPx: raster.widthPx,
|
|
22
|
+
heightPx: raster.heightPx,
|
|
23
|
+
...(raster.format ? { format: raster.format } : {}),
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
// Patch that replaces the object tile `placedId` with a background layer
|
|
27
|
+
// showing `image`: the image contain-fits the document rect (the size a
|
|
28
|
+
// first-placed background gets) and centers on the tile's anchor, so it
|
|
29
|
+
// appears where the tile was. The layer starts FREE — the point of expanding
|
|
30
|
+
// is to arrange the photo, and the pill's lock is one tap away. Null when the
|
|
31
|
+
// tile is missing or isn't an object tile.
|
|
32
|
+
export const expandFileTileToBackground = (doc, placedId, layerId, image) => {
|
|
33
|
+
const placed = doc.placedMeasurements.find((m) => m.id === placedId);
|
|
34
|
+
if (!placed?.fileId)
|
|
35
|
+
return null;
|
|
36
|
+
const fitted = imageDocRect(image.widthPx, image.heightPx, doc.viewport.width, doc.viewport.height, 'contain');
|
|
37
|
+
const layer = {
|
|
38
|
+
id: layerId,
|
|
39
|
+
storagePath: image.storagePath,
|
|
40
|
+
downloadUrl: image.downloadUrl,
|
|
41
|
+
widthPx: image.widthPx,
|
|
42
|
+
heightPx: image.heightPx,
|
|
43
|
+
...(image.format ? { format: image.format } : {}),
|
|
44
|
+
transform: {
|
|
45
|
+
x: placed.anchor.x - fitted.width / 2,
|
|
46
|
+
y: placed.anchor.y - fitted.height / 2,
|
|
47
|
+
scaleX: image.widthPx > 0 ? fitted.width / image.widthPx : 1,
|
|
48
|
+
scaleY: image.heightPx > 0 ? fitted.height / image.heightPx : 1,
|
|
49
|
+
},
|
|
50
|
+
locked: false,
|
|
51
|
+
fileId: placed.fileId,
|
|
52
|
+
...(placed.filePath ? { filePath: placed.filePath } : {}),
|
|
53
|
+
...(placed.groupId ? { fileGroupId: placed.groupId } : {}),
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
ops: [
|
|
57
|
+
{ op: 'removeMeasurement', id: placedId },
|
|
58
|
+
{ op: 'addBackgroundImage', image: layer },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
// Patch that replaces the linked background layer `layerId` with an object
|
|
63
|
+
// tile at the layer's visible center — the inverse switch. The tile's fields
|
|
64
|
+
// come back off the layer (see the fileId trio on AnnotationBackgroundLayer),
|
|
65
|
+
// at the default tile scale; the crop window, if any, is simply dropped (a
|
|
66
|
+
// tile is a thumbnail, not a window). Null for a layer that isn't linked.
|
|
67
|
+
export const collapseBackgroundToFileTile = (doc, layerId, tileId, now = Date.now()) => {
|
|
68
|
+
const layer = backgroundLayersOf(doc.viewport).find((l) => l.id === layerId);
|
|
69
|
+
if (!layer?.fileId)
|
|
70
|
+
return null;
|
|
71
|
+
const r = backgroundLayerDocRect(layer);
|
|
72
|
+
const tile = {
|
|
73
|
+
id: tileId,
|
|
74
|
+
layerId: doc.layers[0]?.id ?? DEFAULT_LAYER_ID,
|
|
75
|
+
fileId: layer.fileId,
|
|
76
|
+
...(layer.filePath ? { filePath: layer.filePath } : {}),
|
|
77
|
+
...(layer.fileGroupId ? { groupId: layer.fileGroupId } : {}),
|
|
78
|
+
anchor: { x: r.x + r.width / 2, y: r.y + r.height / 2 },
|
|
79
|
+
createdAt: now,
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
ops: [
|
|
83
|
+
{ op: 'removeBackgroundImage', id: layerId },
|
|
84
|
+
{ op: 'addMeasurement', measurement: tile },
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
};
|
|
@@ -20,7 +20,7 @@ export declare const TILE_SCALE_MIN = 0.05;
|
|
|
20
20
|
export declare const TILE_SCALE_MAX = 2;
|
|
21
21
|
export declare const resolveTileScaleFactor: (canvas: Pick<AnnotationCanvasState, "tileScaleFactor" | "tileScaleFactorMobile">, platform: TileScalePlatform) => number;
|
|
22
22
|
export declare const resolveHeaderTileScaleFactor: (canvas: Pick<AnnotationCanvasState, "tileScaleFactor" | "tileScaleFactorMobile" | "headerTileScaleFactor" | "headerTileScaleFactorMobile">, platform: TileScalePlatform) => number;
|
|
23
|
-
export declare const resolveFileTileScaleFactor: (canvas: Pick<AnnotationCanvasState, "tileScaleFactor" | "tileScaleFactorMobile"
|
|
23
|
+
export declare const resolveFileTileScaleFactor: (canvas: Pick<AnnotationCanvasState, "tileScaleFactor" | "tileScaleFactorMobile">, platform: TileScalePlatform) => number;
|
|
24
24
|
export declare const clampTileScale: (v: number) => number;
|
|
25
25
|
export declare const tileDocSizeForFactor: (factor: number, zoom: number) => number;
|
|
26
26
|
export declare const tileScaleFromDocSize: (docSize: number, zoom: number) => number;
|
|
@@ -97,14 +97,16 @@ export const resolveTileScaleFactor = (canvas, platform) => (platform === 'mobil
|
|
|
97
97
|
export const resolveHeaderTileScaleFactor = (canvas, platform) => (platform === 'mobile'
|
|
98
98
|
? (canvas.headerTileScaleFactorMobile ?? canvas.headerTileScaleFactor)
|
|
99
99
|
: canvas.headerTileScaleFactor) ?? resolveTileScaleFactor(canvas, platform);
|
|
100
|
-
// Resolve the
|
|
101
|
-
// knob (Asana 1217570500895545),
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
// Resolve the OBJECT-tile scale for a platform: the MEASUREMENT factor.
|
|
101
|
+
// Object tiles briefly had their own third knob (Asana 1217570500895545), but
|
|
102
|
+
// it was retired — an object thumbnail must size consistently with the
|
|
103
|
+
// measurement tiles beside it, so its slider now drives (and follows) the one
|
|
104
|
+
// shared factor. The `fileTileScaleFactor(Mobile)` fields it wrote persist on
|
|
105
|
+
// old documents and old undo payloads still write them; they are deliberately
|
|
106
|
+
// IGNORED here — resolving them would let a stale divergent value shadow the
|
|
107
|
+
// shared knob forever. Kept as a named resolver (not inlined at call sites)
|
|
108
|
+
// so the read path stays greppable and the retirement is documented once.
|
|
109
|
+
export const resolveFileTileScaleFactor = (canvas, platform) => resolveTileScaleFactor(canvas, platform);
|
|
108
110
|
// Clamp a tile-scale-factor candidate to the supported range. The single guard
|
|
109
111
|
// for the value before it lands in the document (slider input, restored docs).
|
|
110
112
|
export const clampTileScale = (v) => v < TILE_SCALE_MIN ? TILE_SCALE_MIN : v > TILE_SCALE_MAX ? TILE_SCALE_MAX : v;
|
|
@@ -1,5 +1,23 @@
|
|
|
1
|
+
import type { Vec2 } from '../../../types/annotation.js';
|
|
2
|
+
import { type BackgroundCropEdge } from '../backgroundLayers.js';
|
|
1
3
|
import type { Tool } from '../Tool.js';
|
|
2
4
|
export interface SelectToolOptions {
|
|
3
5
|
panOnEmptyDrag?: boolean;
|
|
4
6
|
}
|
|
7
|
+
export declare const cropDragVisibleRect: (start: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}, full: {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}, edge: BackgroundCropEdge, delta: Vec2) => {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
};
|
|
5
23
|
export declare const createSelectTool: (options?: SelectToolOptions) => Tool;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { backgroundLayerDocRect, backgroundLayerIsLocked, backgroundLayersOf, isBackgroundLayerLocked, } from '../backgroundLayers.js';
|
|
1
|
+
import { MAX_BACKGROUND_DOC_EXTENT, MIN_BACKGROUND_DOC_EXTENT, backgroundCropBars, backgroundLayerDocRect, backgroundLayerFullDocRect, backgroundLayerIsLocked, backgroundLayersOf, isBackgroundLayerLocked, } from '../backgroundLayers.js';
|
|
2
2
|
import { stampTileDims } from '../stampLayout.js';
|
|
3
3
|
import { placementOf, linePosOf, snapLinePos, lerp, recomputeAnchor, rectCenter, rectCornerPoint, oppositeRectCorner, hitPlacedMeasurement, } from '../measurementGeometry.js';
|
|
4
4
|
import { hitShapeOutline, visualShapeBounds } from '../shapeGeometry.js';
|
|
@@ -468,12 +468,8 @@ const shapeCornerPatch = (doc, id, corner, delta) => {
|
|
|
468
468
|
// other, but never moves or scales:
|
|
469
469
|
// the drag/resize hit-tests below refuse it, and a gesture on it behaves like
|
|
470
470
|
// empty canvas (pan) while leaving the selection in place. ---
|
|
471
|
-
// Doc-space
|
|
472
|
-
//
|
|
473
|
-
// ceiling keeps the doc rect (which feeds fit math and the web SVG raster
|
|
474
|
-
// planner) from exploding to numerically silly sizes.
|
|
475
|
-
const MIN_BACKGROUND_DOC_EXTENT = 16;
|
|
476
|
-
const MAX_BACKGROUND_DOC_EXTENT = 65536;
|
|
471
|
+
// Doc-space size clamps live in backgroundLayers.ts (the native crop preview
|
|
472
|
+
// worklet shares the floor).
|
|
477
473
|
const backgroundLayerById = (doc, id) => backgroundLayersOf(doc.viewport).find((l) => l.id === id) ?? null;
|
|
478
474
|
const pointInBackgroundLayer = (layer, p) => {
|
|
479
475
|
const r = backgroundLayerDocRect(layer);
|
|
@@ -511,19 +507,30 @@ const findSelectedBackgroundHit = (doc, world, selectedIds) => {
|
|
|
511
507
|
}
|
|
512
508
|
return null;
|
|
513
509
|
};
|
|
514
|
-
// Corner-scale geometry for a background layer:
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
|
|
510
|
+
// Corner-scale geometry for a background layer: handle = the grabbed corner
|
|
511
|
+
// of its VISIBLE (cropped) doc rect, pivot = the opposite corner, which stays
|
|
512
|
+
// put — so the patch scales scaleX/scaleY AND repositions transform.x/y about
|
|
513
|
+
// the pivot (unlike text, whose top-left anchor is always the pivot). The
|
|
514
|
+
// chrome draws all four handles un-padded on the exact rect corners; the grab
|
|
515
|
+
// tolerance lives in the hit radius, not the geometry.
|
|
516
|
+
const BACKGROUND_RESIZE_CORNERS = [
|
|
517
|
+
'tl',
|
|
518
|
+
'tr',
|
|
519
|
+
'bl',
|
|
520
|
+
'br',
|
|
521
|
+
];
|
|
522
|
+
const backgroundResizeGeometry = (layer, corner) => {
|
|
520
523
|
const r = backgroundLayerDocRect(layer);
|
|
521
524
|
const maxDim = Math.max(r.width, r.height);
|
|
522
525
|
if (!(maxDim > 0))
|
|
523
526
|
return null;
|
|
527
|
+
const cornerPoint = (c) => ({
|
|
528
|
+
x: c === 'tl' || c === 'bl' ? r.x : r.x + r.width,
|
|
529
|
+
y: c === 'tl' || c === 'tr' ? r.y : r.y + r.height,
|
|
530
|
+
});
|
|
524
531
|
return {
|
|
525
|
-
pivot:
|
|
526
|
-
handle:
|
|
532
|
+
pivot: cornerPoint(oppositeRectCorner(corner)),
|
|
533
|
+
handle: cornerPoint(corner),
|
|
527
534
|
minScale: MIN_BACKGROUND_DOC_EXTENT / maxDim,
|
|
528
535
|
maxScale: MAX_BACKGROUND_DOC_EXTENT / maxDim,
|
|
529
536
|
};
|
|
@@ -532,41 +539,51 @@ const backgroundResizeGeometry = (layer) => {
|
|
|
532
539
|
// native UI-thread drag via DragSelectionConfig AND the web pointer handlers) ---
|
|
533
540
|
// Resize geometry when the (selected) text shape's or background layer's
|
|
534
541
|
// corner handle is under `world`, else null. A text handle sits on the padded
|
|
535
|
-
// selection box's bottom-right corner
|
|
536
|
-
// rect
|
|
537
|
-
// endpoint handles either way.
|
|
542
|
+
// selection box's bottom-right corner; a background offers all FOUR exact doc
|
|
543
|
+
// rect corners, each scaling about its opposite (see AnnotationCanvasSkia);
|
|
544
|
+
// grab radius matches the measurement endpoint handles either way.
|
|
538
545
|
const findResizeHandleHit = (doc, id, world, zoom) => {
|
|
546
|
+
const r2 = (HANDLE_GRAB_PX / zoom) ** 2;
|
|
547
|
+
const within = (geom) => {
|
|
548
|
+
const dx = world.x - geom.handle.x;
|
|
549
|
+
const dy = world.y - geom.handle.y;
|
|
550
|
+
return dx * dx + dy * dy <= r2;
|
|
551
|
+
};
|
|
539
552
|
const s = doc.shapes.find((x) => x.id === id);
|
|
540
|
-
|
|
553
|
+
if (s) {
|
|
554
|
+
const geom = textResizeGeometry(s);
|
|
555
|
+
return geom && within(geom) ? geom : null;
|
|
556
|
+
}
|
|
557
|
+
// A locked background cannot scale, so its corners offer no handles (the
|
|
541
558
|
// chrome draws none either — see AnnotationCanvasSkia).
|
|
542
|
-
|
|
543
|
-
? null
|
|
544
|
-
: backgroundLayerById(doc, id);
|
|
545
|
-
const geom = s
|
|
546
|
-
? textResizeGeometry(s)
|
|
547
|
-
: layer
|
|
548
|
-
? backgroundResizeGeometry(layer)
|
|
549
|
-
: null;
|
|
550
|
-
if (!geom)
|
|
559
|
+
if (isBackgroundLayerLocked(doc.viewport, id))
|
|
551
560
|
return null;
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
561
|
+
const layer = backgroundLayerById(doc, id);
|
|
562
|
+
if (!layer)
|
|
563
|
+
return null;
|
|
564
|
+
for (const corner of BACKGROUND_RESIZE_CORNERS) {
|
|
565
|
+
const geom = backgroundResizeGeometry(layer, corner);
|
|
566
|
+
if (geom && within(geom))
|
|
567
|
+
return geom;
|
|
568
|
+
}
|
|
569
|
+
return null;
|
|
556
570
|
};
|
|
557
571
|
// Scale the element by the drag (clamped to the geometry's scale range, so it
|
|
558
|
-
// matches the native live preview exactly).
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
// scaleX/scaleY
|
|
562
|
-
//
|
|
563
|
-
|
|
572
|
+
// matches the native live preview exactly). A text shape keeps its top-left
|
|
573
|
+
// anchor (the pivot) and bakes the scale into fontSize. A background scales
|
|
574
|
+
// uniformly about `geom.pivot` — the corner OPPOSITE the grabbed one — so
|
|
575
|
+
// scaleX/scaleY multiply AND transform.x/y move to keep the pivot fixed
|
|
576
|
+
// (both axes scale together, preserving the aspect ratio even on layers
|
|
577
|
+
// synthesized from a legacy 'stretch' fit). `geom` is the grab-start geometry
|
|
578
|
+
// the hit-test chose; without it (defensive) the bottom-right corner's
|
|
579
|
+
// geometry is assumed, which reproduces the pre-four-corner behavior.
|
|
580
|
+
const resizePatch = (doc, id, delta, grabGeom) => {
|
|
564
581
|
const s = doc.shapes.find((x) => x.id === id);
|
|
565
582
|
if (!s) {
|
|
566
583
|
const layer = backgroundLayerById(doc, id);
|
|
567
584
|
if (!layer || isBackgroundLayerLocked(doc.viewport, id))
|
|
568
585
|
return null;
|
|
569
|
-
const geom = backgroundResizeGeometry(layer);
|
|
586
|
+
const geom = grabGeom ?? backgroundResizeGeometry(layer, 'br');
|
|
570
587
|
if (!geom)
|
|
571
588
|
return null;
|
|
572
589
|
const scale = resizeScaleFromDrag(geom, delta);
|
|
@@ -577,7 +594,8 @@ const resizePatch = (doc, id, delta) => {
|
|
|
577
594
|
id,
|
|
578
595
|
patch: {
|
|
579
596
|
transform: {
|
|
580
|
-
|
|
597
|
+
x: geom.pivot.x + (layer.transform.x - geom.pivot.x) * scale,
|
|
598
|
+
y: geom.pivot.y + (layer.transform.y - geom.pivot.y) * scale,
|
|
581
599
|
scaleX: layer.transform.scaleX * scale,
|
|
582
600
|
scaleY: layer.transform.scaleY * scale,
|
|
583
601
|
},
|
|
@@ -597,6 +615,82 @@ const resizePatch = (doc, id, delta) => {
|
|
|
597
615
|
],
|
|
598
616
|
};
|
|
599
617
|
};
|
|
618
|
+
// --- Background crop (the four edge bars on a selected, unlocked layer) ----
|
|
619
|
+
// Which crop bar of the given (selected) background layer is under `world`,
|
|
620
|
+
// or null. Bars exist only on an unlocked RASTER layer (an SVG layer's
|
|
621
|
+
// doc-rect math — the web rasterizer — never sees a crop). The bar rect is
|
|
622
|
+
// the drawn gizmo (backgroundCropBars) padded by the standard screen-constant
|
|
623
|
+
// grab tolerance.
|
|
624
|
+
const findBackgroundCropBarHit = (doc, id, world, zoom) => {
|
|
625
|
+
if (isBackgroundLayerLocked(doc.viewport, id))
|
|
626
|
+
return null;
|
|
627
|
+
const layer = backgroundLayerById(doc, id);
|
|
628
|
+
if (!layer || layer.format === 'svg')
|
|
629
|
+
return null;
|
|
630
|
+
const pad = HANDLE_GRAB_PX / zoom;
|
|
631
|
+
for (const bar of backgroundCropBars(layer)) {
|
|
632
|
+
if (world.x >= bar.x - pad &&
|
|
633
|
+
world.x <= bar.x + bar.width + pad &&
|
|
634
|
+
world.y >= bar.y - pad &&
|
|
635
|
+
world.y <= bar.y + bar.height + pad) {
|
|
636
|
+
return bar.edge;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return null;
|
|
640
|
+
};
|
|
641
|
+
// New VISIBLE doc rect for a crop-bar drag: the grabbed edge follows the
|
|
642
|
+
// delta, clamped so the rect never leaves the full image's bounds (dragging
|
|
643
|
+
// back out un-crops — the bytes were never touched) and never collapses below
|
|
644
|
+
// the same doc-extent floor a corner resize enforces. The native live-preview
|
|
645
|
+
// worklet in AnnotationCanvasInner.native.tsx is a WORKLET TWIN of this math
|
|
646
|
+
// — keep them in sync.
|
|
647
|
+
export const cropDragVisibleRect = (start, full, edge, delta) => {
|
|
648
|
+
let { x, y, width, height } = start;
|
|
649
|
+
if (edge === 'top') {
|
|
650
|
+
const newY = Math.min(Math.max(start.y + delta.y, full.y), start.y + start.height - MIN_BACKGROUND_DOC_EXTENT);
|
|
651
|
+
height = start.y + start.height - newY;
|
|
652
|
+
y = newY;
|
|
653
|
+
}
|
|
654
|
+
else if (edge === 'bottom') {
|
|
655
|
+
height = Math.min(Math.max(start.height + delta.y, MIN_BACKGROUND_DOC_EXTENT), full.y + full.height - start.y);
|
|
656
|
+
}
|
|
657
|
+
else if (edge === 'left') {
|
|
658
|
+
const newX = Math.min(Math.max(start.x + delta.x, full.x), start.x + start.width - MIN_BACKGROUND_DOC_EXTENT);
|
|
659
|
+
width = start.x + start.width - newX;
|
|
660
|
+
x = newX;
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
width = Math.min(Math.max(start.width + delta.x, MIN_BACKGROUND_DOC_EXTENT), full.x + full.width - start.x);
|
|
664
|
+
}
|
|
665
|
+
return { x, y, width, height };
|
|
666
|
+
};
|
|
667
|
+
// Patch that commits a crop-bar drag: the new visible rect (clamped exactly
|
|
668
|
+
// like the live preview) converted back to an intrinsic-pixel crop window.
|
|
669
|
+
const backgroundCropPatch = (doc, id, edge, delta) => {
|
|
670
|
+
const layer = backgroundLayerById(doc, id);
|
|
671
|
+
if (!layer || isBackgroundLayerLocked(doc.viewport, id))
|
|
672
|
+
return null;
|
|
673
|
+
const { scaleX, scaleY } = layer.transform;
|
|
674
|
+
if (!(scaleX > 0) || !(scaleY > 0))
|
|
675
|
+
return null;
|
|
676
|
+
const visible = cropDragVisibleRect(backgroundLayerDocRect(layer), backgroundLayerFullDocRect(layer), edge, delta);
|
|
677
|
+
return {
|
|
678
|
+
ops: [
|
|
679
|
+
{
|
|
680
|
+
op: 'updateBackgroundImage',
|
|
681
|
+
id,
|
|
682
|
+
patch: {
|
|
683
|
+
crop: {
|
|
684
|
+
x: (visible.x - layer.transform.x) / scaleX,
|
|
685
|
+
y: (visible.y - layer.transform.y) / scaleY,
|
|
686
|
+
width: visible.width / scaleX,
|
|
687
|
+
height: visible.height / scaleY,
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
],
|
|
692
|
+
};
|
|
693
|
+
};
|
|
600
694
|
// Patch for the current drag mode (web pointer path), from a world-space delta.
|
|
601
695
|
const dragPatch = (s, doc, delta, zoom) => {
|
|
602
696
|
if (s.mode === 'endpoint' && s.handle) {
|
|
@@ -609,7 +703,10 @@ const dragPatch = (s, doc, delta, zoom) => {
|
|
|
609
703
|
if (s.mode === 'slide')
|
|
610
704
|
return slidePatch(doc, s.id, delta, zoom);
|
|
611
705
|
if (s.mode === 'resize')
|
|
612
|
-
return resizePatch(doc, s.id, delta);
|
|
706
|
+
return resizePatch(doc, s.id, delta, s.geom);
|
|
707
|
+
if (s.mode === 'bg-crop' && s.cropEdge) {
|
|
708
|
+
return backgroundCropPatch(doc, s.id, s.cropEdge, delta);
|
|
709
|
+
}
|
|
613
710
|
if (s.mode === 'rect-corner' && s.corner) {
|
|
614
711
|
return rectCornerPatch(doc, s.id, s.corner, delta);
|
|
615
712
|
}
|
|
@@ -643,6 +740,8 @@ export const createSelectTool = (options = {}) => ({
|
|
|
643
740
|
buildResizePatch: resizePatch,
|
|
644
741
|
hitTestBackground: findBackgroundHit,
|
|
645
742
|
hitTestSelectedBackground: findSelectedBackgroundHit,
|
|
743
|
+
hitTestBackgroundCropBar: findBackgroundCropBarHit,
|
|
744
|
+
buildBackgroundCropPatch: backgroundCropPatch,
|
|
646
745
|
hitTestRectCorner: findRectCornerHit,
|
|
647
746
|
buildRectCornerPatch: rectCornerPatch,
|
|
648
747
|
hitTestShapeCorner: findShapeCornerHit,
|
|
@@ -693,7 +792,8 @@ export const createSelectTool = (options = {}) => ({
|
|
|
693
792
|
delta: { x: 0, y: 0 },
|
|
694
793
|
};
|
|
695
794
|
}
|
|
696
|
-
|
|
795
|
+
const resizeGeom = findResizeHandleHit(ctx.document, selId, world, zoom);
|
|
796
|
+
if (resizeGeom) {
|
|
697
797
|
return {
|
|
698
798
|
kind: 'dragging',
|
|
699
799
|
id: selId,
|
|
@@ -704,6 +804,22 @@ export const createSelectTool = (options = {}) => ({
|
|
|
704
804
|
? 'background'
|
|
705
805
|
: 'shape',
|
|
706
806
|
mode: 'resize',
|
|
807
|
+
// The grabbed corner's geometry — a background scales about the
|
|
808
|
+
// OPPOSITE corner, so the patch must reuse what the grab chose.
|
|
809
|
+
geom: resizeGeom,
|
|
810
|
+
start: world,
|
|
811
|
+
delta: { x: 0, y: 0 },
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
// Crop bar on a selected, unlocked background layer.
|
|
815
|
+
const cropEdge = findBackgroundCropBarHit(ctx.document, selId, world, zoom);
|
|
816
|
+
if (cropEdge) {
|
|
817
|
+
return {
|
|
818
|
+
kind: 'dragging',
|
|
819
|
+
id: selId,
|
|
820
|
+
elementKind: 'background',
|
|
821
|
+
mode: 'bg-crop',
|
|
822
|
+
cropEdge,
|
|
707
823
|
start: world,
|
|
708
824
|
delta: { x: 0, y: 0 },
|
|
709
825
|
};
|
|
@@ -194,7 +194,9 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
194
194
|
// equals the measurement factor (resolveHeaderTileScaleFactor's fallback) —
|
|
195
195
|
// the pre-split one-knob behavior.
|
|
196
196
|
let headerTileScaleFactor = resolveHeaderTileScaleFactor(effectiveCanvas, tileScalePlatformResolved);
|
|
197
|
-
// Object tiles
|
|
197
|
+
// Object tiles share the measurement factor — their own knob was retired
|
|
198
|
+
// (see resolveFileTileScaleFactor), so this always equals tileScaleFactor
|
|
199
|
+
// and rides the same calibration below.
|
|
198
200
|
let fileTileScaleFactor = resolveFileTileScaleFactor(effectiveCanvas, tileScalePlatformResolved);
|
|
199
201
|
if (tileScalePlatformResolved === 'mobile' &&
|
|
200
202
|
effectiveCanvas.tileScaleFactorMobile == null &&
|
|
@@ -429,7 +429,11 @@ export const useAnnotationCanvasDoc = (options) => {
|
|
|
429
429
|
? backgroundLayersOf(viewport).find((l) => l.id === id)
|
|
430
430
|
: undefined;
|
|
431
431
|
const fileId = fileIdRef.current ?? createdIdRef.current;
|
|
432
|
-
|
|
432
|
+
// A LINKED layer (an expanded file tile — see fileTileSwap.ts) does not
|
|
433
|
+
// own its Storage object: its storagePath points into the SOURCE file's
|
|
434
|
+
// folder, and deleting it would destroy that file's photo. Remove the
|
|
435
|
+
// layer only.
|
|
436
|
+
if (layer && fileId && !layer.fileId) {
|
|
433
437
|
try {
|
|
434
438
|
await deleteImage(fileId, layer.storagePath);
|
|
435
439
|
}
|
package/dist/exports.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, type Gro
|
|
|
37
37
|
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, type ViewportApi, type ViewportState, type ZoomLimits, } from './annotation/canvas/viewport.js';
|
|
38
38
|
export { DEFAULT_PDF_PAGE_GAP, DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM, makePdfDocumentId, createPdfDocument, pdfPageDocRects, pdfDocumentExtent, pdfFirstPageDocRect, computeZoomFloor, type PdfDocRect, } from './annotation/canvas/pdfDocument.js';
|
|
39
39
|
export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerIsLocked, isBackgroundLayerLocked, defaultBackgroundLayerLocked, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
|
|
40
|
+
export { fileTileImageSource, expandFileTileToBackground, collapseBackgroundToFileTile, type FileTileImageSource, } from './annotation/canvas/fileTileSwap.js';
|
|
40
41
|
export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
|
|
41
42
|
export { createSelectTool, type SelectToolOptions, } from './annotation/canvas/tools/selectTool.js';
|
|
42
43
|
export { createMeasurementStampTool, type MeasurementStampToolOptions, } from './annotation/canvas/tools/measurementStampTool.js';
|
package/dist/exports.js
CHANGED
|
@@ -41,6 +41,7 @@ export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, } from '
|
|
|
41
41
|
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './annotation/canvas/viewport.js';
|
|
42
42
|
export { DEFAULT_PDF_PAGE_GAP, DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM, makePdfDocumentId, createPdfDocument, pdfPageDocRects, pdfDocumentExtent, pdfFirstPageDocRect, computeZoomFloor, } from './annotation/canvas/pdfDocument.js';
|
|
43
43
|
export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerIsLocked, isBackgroundLayerLocked, defaultBackgroundLayerLocked, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
|
|
44
|
+
export { fileTileImageSource, expandFileTileToBackground, collapseBackgroundToFileTile, } from './annotation/canvas/fileTileSwap.js';
|
|
44
45
|
export { createPenTool, } from './annotation/canvas/tools/penTool.js';
|
|
45
46
|
export { createSelectTool, } from './annotation/canvas/tools/selectTool.js';
|
|
46
47
|
export { createMeasurementStampTool, } from './annotation/canvas/tools/measurementStampTool.js';
|