@reekon-tools/boldr-utils 1.6.23 → 1.6.25
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 +1 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +1 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +387 -77
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +14 -2
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +55 -42
- package/dist/annotation/canvas/Tool.d.ts +8 -0
- package/dist/annotation/canvas/elements/BackgroundImageElement.d.ts +4 -1
- package/dist/annotation/canvas/elements/BackgroundImageElement.js +21 -19
- package/dist/annotation/canvas/elements/ShapeElement.js +45 -25
- package/dist/annotation/canvas/elements/StrokeElement.js +26 -11
- package/dist/annotation/canvas/shapeGeometry.d.ts +7 -0
- package/dist/annotation/canvas/shapeGeometry.js +36 -0
- package/dist/annotation/canvas/tools/penTool.d.ts +1 -0
- package/dist/annotation/canvas/tools/penTool.js +11 -1
- package/dist/annotation/canvas/tools/selectTool.js +118 -1
- package/dist/annotation/canvas/tools/shapeTool.d.ts +2 -0
- package/dist/annotation/canvas/tools/shapeTool.js +13 -2
- package/dist/annotation/canvas/useAnnotationCanvasState.js +41 -10
- package/dist/annotation/canvas/viewport.d.ts +13 -1
- package/dist/annotation/canvas/viewport.js +32 -0
- package/dist/exports.d.ts +1 -1
- package/dist/exports.js +1 -1
- package/dist/types/annotation.d.ts +3 -0
- package/dist/types/annotation.js +8 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { stampTileSize } from '../stampLayout.js';
|
|
2
2
|
import { placementOf, linePosOf, snapLinePos, lerp, recomputeAnchor, rectCenter, rectCornerPoint, oppositeRectCorner, hitPlacedMeasurement, } from '../measurementGeometry.js';
|
|
3
|
-
import { hitShapeOutline } from '../shapeGeometry.js';
|
|
3
|
+
import { hitShapeOutline, visualShapeBounds } from '../shapeGeometry.js';
|
|
4
4
|
import { DEFAULT_TEXT_FONT_SIZE, resizeScaleFromDrag, textResizeGeometry, textShapeBounds, } from '../textGeometry.js';
|
|
5
5
|
import { editTextShape } from './textEditing.js';
|
|
6
6
|
const HIT_PADDING = 6;
|
|
@@ -315,6 +315,105 @@ const rectCornerPatch = (doc, id, corner, delta) => {
|
|
|
315
315
|
],
|
|
316
316
|
};
|
|
317
317
|
};
|
|
318
|
+
// --- Geometric-shape corner resize (rect/ellipse/polygon). The shape twin of
|
|
319
|
+
// the rectangle-annotation corner drag above: drag one bounding-box corner to
|
|
320
|
+
// scale length and width about the opposite (fixed) corner. Shared by the
|
|
321
|
+
// native UI-thread drag via DragSelectionConfig AND the web pointer handlers. ---
|
|
322
|
+
// Doc-space floor on a resized shape's width/height. Keeps a corner drag from
|
|
323
|
+
// collapsing a shape to a line (and from crossing the fixed corner, which would
|
|
324
|
+
// mirror a triangle), so the scale stays positive on both axes. The native
|
|
325
|
+
// shapeResizeScale worklet inlines the same value — keep them in sync.
|
|
326
|
+
const MIN_SHAPE_EXTENT = 1;
|
|
327
|
+
// Which shapes get the corner-scale handles: rect (two-corner geometry, scaled
|
|
328
|
+
// exactly like the rectangle annotation), polygon/triangle (all points scaled
|
|
329
|
+
// about the fixed corner) and ellipse. Lines/arrows resize via their endpoint
|
|
330
|
+
// handles and text via its own font-size handle, so both are excluded here.
|
|
331
|
+
const isCornerResizableShape = (s) => (s.kind === 'rect' || s.kind === 'ellipse' || s.kind === 'polygon') &&
|
|
332
|
+
s.geometry.points.length >= 2;
|
|
333
|
+
// The shape's visual bounds expressed as the {a,b} rect the rectCornerPoint/
|
|
334
|
+
// oppositeRectCorner helpers consume (so shapes reuse the exact corner math the
|
|
335
|
+
// rectangle annotation already uses). Uses visualShapeBounds, so the corner
|
|
336
|
+
// handles/hit-test sit on the same box the selection outline draws — for an
|
|
337
|
+
// ellipse that's the circle's bounding square, not the raw drag rect.
|
|
338
|
+
const shapeCornerRect = (s) => {
|
|
339
|
+
const vb = visualShapeBounds(s);
|
|
340
|
+
if (!vb)
|
|
341
|
+
return null;
|
|
342
|
+
return { a: { x: vb.minX, y: vb.minY }, b: { x: vb.maxX, y: vb.maxY } };
|
|
343
|
+
};
|
|
344
|
+
// Which bounding-box corner of a (selected) resizable shape is under `world`,
|
|
345
|
+
// or null. Mirrors findRectCornerHit but reads the box from the shape's points.
|
|
346
|
+
const findShapeCornerHit = (doc, id, world, zoom) => {
|
|
347
|
+
const s = doc.shapes.find((x) => x.id === id);
|
|
348
|
+
if (!s || !isCornerResizableShape(s))
|
|
349
|
+
return null;
|
|
350
|
+
const rect = shapeCornerRect(s);
|
|
351
|
+
if (!rect)
|
|
352
|
+
return null;
|
|
353
|
+
const r2 = (HANDLE_GRAB_PX / zoom) ** 2;
|
|
354
|
+
let best = null;
|
|
355
|
+
for (const corner of ['tl', 'tr', 'bl', 'br']) {
|
|
356
|
+
const p = rectCornerPoint(rect, corner);
|
|
357
|
+
const d = (world.x - p.x) ** 2 + (world.y - p.y) ** 2;
|
|
358
|
+
if (d <= r2 && (!best || d < best.d))
|
|
359
|
+
best = { corner, d };
|
|
360
|
+
}
|
|
361
|
+
if (!best)
|
|
362
|
+
return null;
|
|
363
|
+
return {
|
|
364
|
+
corner: best.corner,
|
|
365
|
+
moving: rectCornerPoint(rect, best.corner),
|
|
366
|
+
fixed: rectCornerPoint(rect, oppositeRectCorner(best.corner)),
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
// Scale every geometry point about the fixed (opposite) corner so the grabbed
|
|
370
|
+
// corner follows the drag. The moving corner is clamped so it can't cross or
|
|
371
|
+
// collapse onto the fixed corner (min extent on each axis), keeping the scale
|
|
372
|
+
// positive — no flip, no zero-size shape.
|
|
373
|
+
//
|
|
374
|
+
// rect/polygon scale length and width INDEPENDENTLY (a two-point rect's grabbed
|
|
375
|
+
// corner tracks the finger; a polygon's whole outline scales). An ellipse
|
|
376
|
+
// renders as a circle (radius = half its larger extent), so it scales
|
|
377
|
+
// UNIFORMLY off the diagonal instead — otherwise a non-uniform drag would
|
|
378
|
+
// preview as an oval and then snap back to a circle on commit.
|
|
379
|
+
const shapeCornerPatch = (doc, id, corner, delta) => {
|
|
380
|
+
const s = doc.shapes.find((x) => x.id === id);
|
|
381
|
+
if (!s || !isCornerResizableShape(s))
|
|
382
|
+
return null;
|
|
383
|
+
const rect = shapeCornerRect(s);
|
|
384
|
+
if (!rect)
|
|
385
|
+
return null;
|
|
386
|
+
const fixed = rectCornerPoint(rect, oppositeRectCorner(corner));
|
|
387
|
+
const moving = rectCornerPoint(rect, corner);
|
|
388
|
+
const denomX = moving.x - fixed.x;
|
|
389
|
+
const denomY = moving.y - fixed.y;
|
|
390
|
+
// Clamp the dragged offset to the same side of the fixed corner, magnitude
|
|
391
|
+
// >= MIN_SHAPE_EXTENT, so the scale factor never flips sign or hits zero.
|
|
392
|
+
const clampOffset = (offset, sign) => sign >= 0
|
|
393
|
+
? Math.max(MIN_SHAPE_EXTENT, offset)
|
|
394
|
+
: Math.min(-MIN_SHAPE_EXTENT, offset);
|
|
395
|
+
const offX = clampOffset(moving.x + delta.x - fixed.x, denomX >= 0 ? 1 : -1);
|
|
396
|
+
const offY = clampOffset(moving.y + delta.y - fixed.y, denomY >= 0 ? 1 : -1);
|
|
397
|
+
let sx = denomX !== 0 ? offX / denomX : 1;
|
|
398
|
+
let sy = denomY !== 0 ? offY / denomY : 1;
|
|
399
|
+
if (s.kind === 'ellipse') {
|
|
400
|
+
// Uniform: ratio of the new corner-distance to the old, so the circle
|
|
401
|
+
// grows/shrinks about the fixed corner without distorting.
|
|
402
|
+
const oldDiag = Math.hypot(denomX, denomY);
|
|
403
|
+
const s0 = oldDiag !== 0 ? Math.hypot(offX, offY) / oldDiag : 1;
|
|
404
|
+
sx = s0;
|
|
405
|
+
sy = s0;
|
|
406
|
+
}
|
|
407
|
+
const points = s.geometry.points.map((p) => ({
|
|
408
|
+
x: fixed.x + (p.x - fixed.x) * sx,
|
|
409
|
+
y: fixed.y + (p.y - fixed.y) * sy,
|
|
410
|
+
}));
|
|
411
|
+
return {
|
|
412
|
+
ops: [
|
|
413
|
+
{ op: 'updateShape', id, patch: { geometry: { ...s.geometry, points } } },
|
|
414
|
+
],
|
|
415
|
+
};
|
|
416
|
+
};
|
|
318
417
|
// --- Text-shape resize (corner-scale about the top-left anchor; shared by the
|
|
319
418
|
// native UI-thread drag via DragSelectionConfig AND the web pointer handlers) ---
|
|
320
419
|
// Resize geometry when the (selected) text shape's corner handle is under
|
|
@@ -367,6 +466,9 @@ const dragPatch = (s, doc, delta, zoom) => {
|
|
|
367
466
|
if (s.mode === 'rect-corner' && s.corner) {
|
|
368
467
|
return rectCornerPatch(doc, s.id, s.corner, delta);
|
|
369
468
|
}
|
|
469
|
+
if (s.mode === 'shape-corner' && s.corner) {
|
|
470
|
+
return shapeCornerPatch(doc, s.id, s.corner, delta);
|
|
471
|
+
}
|
|
370
472
|
const op = translatePatch(s.elementKind, s.id, doc, delta);
|
|
371
473
|
return op ? { ops: [op] } : null;
|
|
372
474
|
};
|
|
@@ -394,6 +496,8 @@ export const createSelectTool = () => ({
|
|
|
394
496
|
buildResizePatch: resizePatch,
|
|
395
497
|
hitTestRectCorner: findRectCornerHit,
|
|
396
498
|
buildRectCornerPatch: rectCornerPatch,
|
|
499
|
+
hitTestShapeCorner: findShapeCornerHit,
|
|
500
|
+
buildShapeCornerPatch: shapeCornerPatch,
|
|
397
501
|
},
|
|
398
502
|
// Web pointer path. Mirrors the native UI-thread drag using the same shared
|
|
399
503
|
// helpers: an endpoint handle on the selected annotation resizes the line;
|
|
@@ -448,6 +552,19 @@ export const createSelectTool = () => ({
|
|
|
448
552
|
delta: { x: 0, y: 0 },
|
|
449
553
|
};
|
|
450
554
|
}
|
|
555
|
+
const shapeCorner = findShapeCornerHit(ctx.document, selId, world, zoom);
|
|
556
|
+
if (shapeCorner) {
|
|
557
|
+
ctx.setSelection({ ids: [selId] });
|
|
558
|
+
return {
|
|
559
|
+
kind: 'dragging',
|
|
560
|
+
id: selId,
|
|
561
|
+
elementKind: 'shape',
|
|
562
|
+
mode: 'shape-corner',
|
|
563
|
+
corner: shapeCorner.corner,
|
|
564
|
+
start: world,
|
|
565
|
+
delta: { x: 0, y: 0 },
|
|
566
|
+
};
|
|
567
|
+
}
|
|
451
568
|
const rectCorner = findRectCornerHit(ctx.document, selId, world, zoom);
|
|
452
569
|
if (rectCorner) {
|
|
453
570
|
ctx.setSelection({ ids: [selId] });
|
|
@@ -8,6 +8,7 @@ export interface ShapeToolOptions {
|
|
|
8
8
|
color?: string;
|
|
9
9
|
width?: number;
|
|
10
10
|
cap?: StrokeCap;
|
|
11
|
+
startCap?: StrokeCap;
|
|
11
12
|
dash?: boolean;
|
|
12
13
|
minDragPx?: number;
|
|
13
14
|
}
|
|
@@ -18,6 +19,7 @@ export declare const buildShapeFromDrag: (opts: {
|
|
|
18
19
|
color: string;
|
|
19
20
|
width: number;
|
|
20
21
|
cap?: StrokeCap;
|
|
22
|
+
startCap?: StrokeCap;
|
|
21
23
|
dash?: boolean;
|
|
22
24
|
layerId: string;
|
|
23
25
|
id?: string;
|
|
@@ -24,10 +24,12 @@ export const buildShapeFromDrag = (opts) => ({
|
|
|
24
24
|
strokeWidth: opts.width,
|
|
25
25
|
...(opts.dash && { dash: true }),
|
|
26
26
|
// Caps only mean something on an open line; 'round' is the implicit
|
|
27
|
-
// default so it stays un-persisted.
|
|
27
|
+
// default so it stays un-persisted. Only 'arrow' matters for the start.
|
|
28
28
|
...(opts.kind === 'line' &&
|
|
29
29
|
opts.cap &&
|
|
30
30
|
opts.cap !== 'round' && { cap: opts.cap }),
|
|
31
|
+
...(opts.kind === 'line' &&
|
|
32
|
+
opts.startCap === 'arrow' && { startCap: 'arrow' }),
|
|
31
33
|
},
|
|
32
34
|
createdAt: Date.now(),
|
|
33
35
|
});
|
|
@@ -42,6 +44,7 @@ export const createShapeTool = (options = {}) => {
|
|
|
42
44
|
const color = options.color ?? '#111827';
|
|
43
45
|
const width = options.width ?? 2;
|
|
44
46
|
const cap = options.cap;
|
|
47
|
+
const startCap = options.startCap;
|
|
45
48
|
const dash = options.dash ?? false;
|
|
46
49
|
const minDragPx = options.minDragPx ?? 4;
|
|
47
50
|
return {
|
|
@@ -49,7 +52,14 @@ export const createShapeTool = (options = {}) => {
|
|
|
49
52
|
label: options.label ?? DEFAULT_LABELS[kind],
|
|
50
53
|
cursor: 'crosshair',
|
|
51
54
|
// Drives UI-thread rubber-banding on native (see ShapeDrawConfig).
|
|
52
|
-
shapeDraw: {
|
|
55
|
+
shapeDraw: {
|
|
56
|
+
kind,
|
|
57
|
+
color,
|
|
58
|
+
width,
|
|
59
|
+
...(cap && { cap }),
|
|
60
|
+
...(startCap === 'arrow' && { startCap: 'arrow' }),
|
|
61
|
+
dash,
|
|
62
|
+
},
|
|
53
63
|
onPointerDown(event, ctx) {
|
|
54
64
|
return {
|
|
55
65
|
kind: 'shape-drawing',
|
|
@@ -60,6 +70,7 @@ export const createShapeTool = (options = {}) => {
|
|
|
60
70
|
color,
|
|
61
71
|
width,
|
|
62
72
|
cap,
|
|
73
|
+
startCap,
|
|
63
74
|
dash,
|
|
64
75
|
layerId: firstLayerId(ctx.document),
|
|
65
76
|
}),
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { applyPatch, invertPatch, DEFAULT_LAYER_ID, } from '../../types/annotation.js';
|
|
3
|
-
import { createViewportApi, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
|
|
3
|
+
import { createViewportApi, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
|
|
4
4
|
import { recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
|
|
5
|
+
// The viewport that frames the document's content on screen. When a background
|
|
6
|
+
// image is present we fit its rendered rect (so the whole image shows, filling
|
|
7
|
+
// the viewport regardless of its pixel resolution — never the native-resolution
|
|
8
|
+
// top-left crop a 1:1 viewport gives a high-res image); otherwise we fit the
|
|
9
|
+
// document rect. Shared by the load-time auto-fit and the zoomToFit/resetView
|
|
10
|
+
// handle methods so they stay in lockstep.
|
|
11
|
+
const computeContentFit = (canvas, width, height) => {
|
|
12
|
+
if (!(width > 0) || !(height > 0))
|
|
13
|
+
return DEFAULT_VIEWPORT;
|
|
14
|
+
const { viewport } = canvas;
|
|
15
|
+
const bg = viewport.backgroundImage;
|
|
16
|
+
const rect = bg
|
|
17
|
+
? imageDocRect(bg.widthPx, bg.heightPx, viewport.width, viewport.height, viewport.backgroundFit ?? 'contain')
|
|
18
|
+
: { x: 0, y: 0, width: viewport.width, height: viewport.height };
|
|
19
|
+
return fitRectToScreen(rect, width, height);
|
|
20
|
+
};
|
|
5
21
|
// Platform-agnostic state machine for the annotation canvas. Web and native
|
|
6
22
|
// inners share this hook; each wraps it with platform-specific event
|
|
7
23
|
// capture and JSX (div + DOM events vs. GestureDetector + RN Views).
|
|
@@ -21,6 +37,25 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
21
37
|
return map;
|
|
22
38
|
}, [measurements]);
|
|
23
39
|
const viewportApi = useMemo(() => createViewportApi(viewport), [viewport]);
|
|
40
|
+
// Frame the document the first time it carries a background image. The doc
|
|
41
|
+
// hydrates asynchronously (it opens empty, then the persisted canvas — with
|
|
42
|
+
// its background — arrives), and a high-res image at the 1:1 default viewport
|
|
43
|
+
// would otherwise show only its top-left corner. We fit it to the screen
|
|
44
|
+
// exactly once per mount; the user's own pan/zoom afterwards is preserved.
|
|
45
|
+
// (Adding a background mid-session also fits it, so you see the whole image
|
|
46
|
+
// you just dropped in.) Canvases that never gain a background keep the 1:1
|
|
47
|
+
// default — for a screen-sized document that already frames it correctly.
|
|
48
|
+
const didInitialFitRef = useRef(false);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (didInitialFitRef.current)
|
|
51
|
+
return;
|
|
52
|
+
if (!canvas.viewport.backgroundImage)
|
|
53
|
+
return;
|
|
54
|
+
if (!(width > 0) || !(height > 0))
|
|
55
|
+
return;
|
|
56
|
+
didInitialFitRef.current = true;
|
|
57
|
+
setViewport(computeContentFit(canvas, width, height));
|
|
58
|
+
}, [canvas, width, height]);
|
|
24
59
|
// Tiles are sized independently of the canvas: their footprint is base ×
|
|
25
60
|
// per-tile scale × the document-wide `tileScaleFactor` ("Tile size" slider),
|
|
26
61
|
// never the canvas pixel size — so resizing the pane no longer rescales tiles.
|
|
@@ -174,17 +209,13 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
174
209
|
return redoStackRef.current.length > 0;
|
|
175
210
|
},
|
|
176
211
|
zoomToFit() {
|
|
177
|
-
setViewport((
|
|
178
|
-
const docW = canvas.viewport.width;
|
|
179
|
-
const docH = canvas.viewport.height;
|
|
180
|
-
const z = Math.min(width / docW, height / docH);
|
|
181
|
-
const offsetX = (width - docW * z) / 2;
|
|
182
|
-
const offsetY = (height - docH * z) / 2;
|
|
183
|
-
return { zoom: z, pan: { x: -offsetX / z, y: -offsetY / z } };
|
|
184
|
-
});
|
|
212
|
+
setViewport(computeContentFit(canvas, width, height));
|
|
185
213
|
},
|
|
186
214
|
resetView() {
|
|
187
|
-
|
|
215
|
+
// "Reset view" frames the whole document/image again — the same fit the
|
|
216
|
+
// canvas opens with — rather than snapping to a 1:1 top-left view, which
|
|
217
|
+
// for a high-res background is the very crop this is meant to escape.
|
|
218
|
+
setViewport(computeContentFit(canvas, width, height));
|
|
188
219
|
},
|
|
189
220
|
placeMeasurementAtCenter(ref) {
|
|
190
221
|
const c = ctxRef.current;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Vec2 } from '../../types/annotation.js';
|
|
1
|
+
import type { BackgroundFit, Vec2 } from '../../types/annotation.js';
|
|
2
2
|
export interface ViewportState {
|
|
3
3
|
zoom: number;
|
|
4
4
|
pan: Vec2;
|
|
@@ -12,5 +12,17 @@ export interface ViewportApi {
|
|
|
12
12
|
export declare const createViewportApi: (state: ViewportState) => ViewportApi;
|
|
13
13
|
export declare const DEFAULT_VIEWPORT: ViewportState;
|
|
14
14
|
export declare const fitToScreen: (docWidth: number, docHeight: number, screenWidth: number, screenHeight: number, padding?: number) => ViewportState;
|
|
15
|
+
export declare const imageDocRect: (imgW: number, imgH: number, docW: number, docH: number, fit?: BackgroundFit) => {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
};
|
|
21
|
+
export declare const fitRectToScreen: (rect: {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}, screenWidth: number, screenHeight: number, padding?: number) => ViewportState;
|
|
15
27
|
export declare const zoomAt: (state: ViewportState, focalScreen: Vec2, nextZoom: number) => ViewportState;
|
|
16
28
|
export declare const panBy: (state: ViewportState, deltaScreen: Vec2) => ViewportState;
|
|
@@ -29,6 +29,38 @@ export const fitToScreen = (docWidth, docHeight, screenWidth, screenHeight, padd
|
|
|
29
29
|
pan: { x: -offsetX / zoom, y: -offsetY / zoom },
|
|
30
30
|
};
|
|
31
31
|
};
|
|
32
|
+
// The doc-space rectangle a background image occupies, given the image's
|
|
33
|
+
// intrinsic pixel size, the document dimensions, and the fit mode. This mirrors
|
|
34
|
+
// the math in BackgroundImageElement (which renders the image into this rect),
|
|
35
|
+
// so "fit the image to the screen" lines up exactly with what is drawn.
|
|
36
|
+
export const imageDocRect = (imgW, imgH, docW, docH, fit = 'contain') => {
|
|
37
|
+
// Unknown/zero image dimensions (or an explicit stretch) fill the doc — the
|
|
38
|
+
// same guard BackgroundImageElement uses to avoid NaN geometry.
|
|
39
|
+
if (!(imgW > 0) || !(imgH > 0) || fit === 'stretch') {
|
|
40
|
+
return { x: 0, y: 0, width: docW, height: docH };
|
|
41
|
+
}
|
|
42
|
+
const scale = fit === 'cover'
|
|
43
|
+
? Math.max(docW / imgW, docH / imgH)
|
|
44
|
+
: Math.min(docW / imgW, docH / imgH);
|
|
45
|
+
const w = imgW * scale;
|
|
46
|
+
const h = imgH * scale;
|
|
47
|
+
return { x: (docW - w) / 2, y: (docH - h) / 2, width: w, height: h };
|
|
48
|
+
};
|
|
49
|
+
// Fit a doc-space rectangle into the screen: the largest zoom that keeps the
|
|
50
|
+
// whole rect visible, centered, with optional screen-space padding.
|
|
51
|
+
export const fitRectToScreen = (rect, screenWidth, screenHeight, padding = 0) => {
|
|
52
|
+
const availableW = Math.max(1, screenWidth - padding * 2);
|
|
53
|
+
const availableH = Math.max(1, screenHeight - padding * 2);
|
|
54
|
+
const zoom = Math.min(availableW / Math.max(1, rect.width), availableH / Math.max(1, rect.height));
|
|
55
|
+
const renderedW = rect.width * zoom;
|
|
56
|
+
const renderedH = rect.height * zoom;
|
|
57
|
+
const offsetX = (screenWidth - renderedW) / 2;
|
|
58
|
+
const offsetY = (screenHeight - renderedH) / 2;
|
|
59
|
+
return {
|
|
60
|
+
zoom,
|
|
61
|
+
pan: { x: rect.x - offsetX / zoom, y: rect.y - offsetY / zoom },
|
|
62
|
+
};
|
|
63
|
+
};
|
|
32
64
|
// Zoom toward a focal screen point so the world point under the cursor stays
|
|
33
65
|
// fixed. Used for wheel-zoom on web and pinch on native.
|
|
34
66
|
export const zoomAt = (state, focalScreen, nextZoom) => {
|
package/dist/exports.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type { CanvasPointerEvent, RequestTextInput, ShapeDrawConfig, Tool, ToolC
|
|
|
22
22
|
export type { MeasurementRef, PickMeasurement, } from './annotation/canvas/measurementPicker.js';
|
|
23
23
|
export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './annotation/canvas/measurementStampOverlay.js';
|
|
24
24
|
export { STAMP_TILE_SIZE, STAMP_INPUT_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
|
|
25
|
-
export { createViewportApi, fitToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
|
|
25
|
+
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
|
|
26
26
|
export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
|
|
27
27
|
export { createSelectTool } from './annotation/canvas/tools/selectTool.js';
|
|
28
28
|
export { createMeasurementStampTool, type MeasurementStampToolOptions, } from './annotation/canvas/tools/measurementStampTool.js';
|
package/dist/exports.js
CHANGED
|
@@ -22,7 +22,7 @@ export { useAnnotationCanvasDoc, } from './annotation/data/hooks/useAnnotationCa
|
|
|
22
22
|
export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
|
|
23
23
|
export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
|
|
24
24
|
export { STAMP_TILE_SIZE, STAMP_INPUT_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
|
|
25
|
-
export { createViewportApi, fitToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
|
|
25
|
+
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
|
|
26
26
|
export { createPenTool, } from './annotation/canvas/tools/penTool.js';
|
|
27
27
|
export { createSelectTool } from './annotation/canvas/tools/selectTool.js';
|
|
28
28
|
export { createMeasurementStampTool, } from './annotation/canvas/tools/measurementStampTool.js';
|
|
@@ -13,6 +13,7 @@ export interface AnnotationStroke {
|
|
|
13
13
|
color: string;
|
|
14
14
|
width: number;
|
|
15
15
|
cap?: StrokeCap;
|
|
16
|
+
startCap?: StrokeCap;
|
|
16
17
|
dash?: boolean;
|
|
17
18
|
points: number[];
|
|
18
19
|
pressure?: number[];
|
|
@@ -29,6 +30,7 @@ export interface AnnotationShapeStyle {
|
|
|
29
30
|
dash?: boolean;
|
|
30
31
|
textDecoration?: AnnotationTextDecoration;
|
|
31
32
|
cap?: StrokeCap;
|
|
33
|
+
startCap?: StrokeCap;
|
|
32
34
|
}
|
|
33
35
|
export interface AnnotationShape {
|
|
34
36
|
id: AnnotationElementId;
|
|
@@ -65,6 +67,7 @@ export interface PlacedMeasurementRef {
|
|
|
65
67
|
lineColor?: string;
|
|
66
68
|
lineWidth?: number;
|
|
67
69
|
lineCap?: StrokeCap;
|
|
70
|
+
lineStartCap?: StrokeCap;
|
|
68
71
|
lineDash?: boolean;
|
|
69
72
|
leader?: {
|
|
70
73
|
from: Vec2;
|
package/dist/types/annotation.js
CHANGED
|
@@ -12,8 +12,14 @@ export const createEmptyCanvasState = (viewport) => ({
|
|
|
12
12
|
viewport: {
|
|
13
13
|
width: viewport?.width ?? 1000,
|
|
14
14
|
height: viewport?.height ?? 1000,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// Only include the optional keys when defined — emitting explicit
|
|
16
|
+
// `undefined` values breaks Firestore writes (RN rejects undefined fields).
|
|
17
|
+
...(viewport?.backgroundImage !== undefined
|
|
18
|
+
? { backgroundImage: viewport.backgroundImage }
|
|
19
|
+
: {}),
|
|
20
|
+
...(viewport?.backgroundFit !== undefined
|
|
21
|
+
? { backgroundFit: viewport.backgroundFit }
|
|
22
|
+
: {}),
|
|
17
23
|
},
|
|
18
24
|
strokes: [],
|
|
19
25
|
shapes: [],
|