@reekon-tools/boldr-utils 1.6.33 → 1.6.35
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/stampLayout.d.ts +2 -0
- package/dist/annotation/canvas/stampLayout.js +18 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +2 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +56 -12
- package/dist/exports.d.ts +1 -1
- package/dist/exports.js +1 -1
- package/dist/types/annotation.d.ts +4 -0
- package/dist/types/annotation.js +12 -0
- package/package.json +2 -1
|
@@ -5,5 +5,7 @@ export declare const TILE_SCALE_MIN = 0.4;
|
|
|
5
5
|
export declare const TILE_SCALE_MAX = 2;
|
|
6
6
|
export declare const resolveTileScaleFactor: (canvas: Pick<AnnotationCanvasState, "tileScaleFactor" | "tileScaleFactorMobile">, platform: TileScalePlatform) => number;
|
|
7
7
|
export declare const clampTileScale: (v: number) => number;
|
|
8
|
+
export declare const tileDocSizeForFactor: (factor: number, zoom: number) => number;
|
|
9
|
+
export declare const tileScaleFromDocSize: (docSize: number, zoom: number) => number;
|
|
8
10
|
export declare const isUnassociatedStamp: (m: Pick<PlacedMeasurementRef, "measurementId" | "measurementPath" | "columnId">) => boolean;
|
|
9
11
|
export declare const stampTileSize: (m: Pick<PlacedMeasurementRef, "scale">, tileScaleFactor?: number, viewportScale?: number) => number;
|
|
@@ -37,6 +37,24 @@ export const resolveTileScaleFactor = (canvas, platform) => (platform === 'mobil
|
|
|
37
37
|
// Clamp a tile-scale-factor candidate to the supported range. The single guard
|
|
38
38
|
// for the value before it lands in the document (slider input, restored docs).
|
|
39
39
|
export const clampTileScale = (v) => v < TILE_SCALE_MIN ? TILE_SCALE_MIN : v > TILE_SCALE_MAX ? TILE_SCALE_MAX : v;
|
|
40
|
+
// --- Cross-platform initial-size calibration ---------------------------------
|
|
41
|
+
// The platform knobs are screen-space multipliers, so the same value covers a
|
|
42
|
+
// very different share of the DRAWING on a phone than on a desktop pane. To
|
|
43
|
+
// hand mobile a sensible starting size, web persists the doc-space edge length
|
|
44
|
+
// a scale-1 tile covered as the user saw it (`tileDocSize`, stamped on tile
|
|
45
|
+
// placement and web slider commits); a mobile canvas with no own knob yet
|
|
46
|
+
// inverts that at its content-fit zoom so its tiles cover the SAME area of the
|
|
47
|
+
// image. Two inverses of one identity: screenSize = docSize × zoom.
|
|
48
|
+
// Doc-space edge length a scale-1 tile covers at `zoom` under `factor` — the
|
|
49
|
+
// web write side of the calibration. Non-positive zoom yields 0, which the
|
|
50
|
+
// setTileDocSize op ignores.
|
|
51
|
+
export const tileDocSizeForFactor = (factor, zoom) => zoom > 0 ? (STAMP_TILE_SIZE * factor) / zoom : 0;
|
|
52
|
+
// Tile-scale factor that makes a scale-1 tile cover `docSize` doc units at
|
|
53
|
+
// `zoom` — the mobile read side of the calibration. Deliberately NOT clamped
|
|
54
|
+
// to the slider range: faithfully matching the web look can land below
|
|
55
|
+
// TILE_SCALE_MIN on a phone, and rendering/hit-testing handle any positive
|
|
56
|
+
// value. The clamp applies once the user drives the slider themselves.
|
|
57
|
+
export const tileScaleFromDocSize = (docSize, zoom) => (docSize * zoom) / STAMP_TILE_SIZE;
|
|
40
58
|
// --- Tile sizing is independent of canvas size ------------------------------
|
|
41
59
|
// A measurement tile's footprint is base × per-tile `scale` × the document-wide
|
|
42
60
|
// `tileScaleFactor` (below) — and NOTHING tied to the canvas's pixel size, so
|
|
@@ -10,6 +10,8 @@ export interface AnnotationCanvasHandle {
|
|
|
10
10
|
canRedo(): boolean;
|
|
11
11
|
zoomToFit(): void;
|
|
12
12
|
resetView(): void;
|
|
13
|
+
getTileScaleFactor(): number;
|
|
14
|
+
getTileDocSize(factor: number): number;
|
|
13
15
|
placeMeasurementAtCenter(ref: MeasurementRef): void;
|
|
14
16
|
placeAnnotationAtCenter(opts?: {
|
|
15
17
|
defaultLengthDoc?: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { applyPatch, invertPatch, DEFAULT_LAYER_ID, } from '../../types/annotation.js';
|
|
3
|
-
import { resolveTileScaleFactor } from './stampLayout.js';
|
|
3
|
+
import { resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, } from './stampLayout.js';
|
|
4
4
|
import { createViewportApi, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
|
|
5
5
|
import { buildRemoveMeasurementOps, recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
|
|
6
6
|
// The viewport that frames the document's content on screen. When a background
|
|
@@ -19,6 +19,14 @@ const computeContentFit = (canvas, width, height) => {
|
|
|
19
19
|
: { x: 0, y: 0, width: viewport.width, height: viewport.height };
|
|
20
20
|
return fitRectToScreen(rect, width, height);
|
|
21
21
|
};
|
|
22
|
+
// Default leader length (doc units) for a placed measurement when no explicit
|
|
23
|
+
// length is given. A generous fraction of the document width, clamped, so the
|
|
24
|
+
// leader — and its draggable endcaps — stay clear of the fixed-size value tile
|
|
25
|
+
// at typical full-fit zoom (Asana 1216212322376723: leaders were collapsing
|
|
26
|
+
// under the tile). The tile is screen-constant while the line is doc-space, so
|
|
27
|
+
// this can't guarantee clearance on very large drawings (the clamp caps the
|
|
28
|
+
// length) or at extreme zoom-out — zoom in to grab the endcaps there.
|
|
29
|
+
const defaultLeaderLenDoc = (docWidth) => Math.min(800, Math.max(240, docWidth * 0.45));
|
|
22
30
|
// Platform-agnostic state machine for the annotation canvas. Web and native
|
|
23
31
|
// inners share this hook; each wraps it with platform-specific event
|
|
24
32
|
// capture and JSX (div + DOM events vs. GestureDetector + RN Views).
|
|
@@ -68,7 +76,21 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
68
76
|
// tools (hit boxes) and overlays (drawn size) share one value. Uses the
|
|
69
77
|
// effective (preview-applied) canvas for parity with what is on screen; the
|
|
70
78
|
// slider only ever commits, so this matches ctx.document in practice.
|
|
71
|
-
|
|
79
|
+
// A mobile canvas whose own knob is still unset derives its factor from the
|
|
80
|
+
// web-stamped doc-space calibration (tileDocSize) instead of web's raw
|
|
81
|
+
// screen-space value: at the initial content-fit view its tiles then cover
|
|
82
|
+
// the same area of the image the web user saw. The first mobile slider
|
|
83
|
+
// commit writes tileScaleFactorMobile and takes over for good.
|
|
84
|
+
const tileScalePlatformResolved = tileScalePlatform ?? 'web';
|
|
85
|
+
let tileScaleFactor = resolveTileScaleFactor(effectiveCanvas, tileScalePlatformResolved);
|
|
86
|
+
if (tileScalePlatformResolved === 'mobile' &&
|
|
87
|
+
effectiveCanvas.tileScaleFactorMobile == null &&
|
|
88
|
+
effectiveCanvas.tileDocSize != null &&
|
|
89
|
+
effectiveCanvas.tileDocSize > 0 &&
|
|
90
|
+
width > 0 &&
|
|
91
|
+
height > 0) {
|
|
92
|
+
tileScaleFactor = tileScaleFromDocSize(effectiveCanvas.tileDocSize, computeContentFit(effectiveCanvas, width, height).zoom);
|
|
93
|
+
}
|
|
72
94
|
const ctx = useMemo(() => ({
|
|
73
95
|
document: canvas,
|
|
74
96
|
selection,
|
|
@@ -79,11 +101,27 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
79
101
|
setPreviewPatch(patch);
|
|
80
102
|
},
|
|
81
103
|
commit(patch) {
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
// Placing a tile on WEB stamps the doc-space calibration: the area of
|
|
105
|
+
// the drawing a scale-1 tile covers at the zoom the user is looking
|
|
106
|
+
// at, right when they judged the size against the image. Mobile
|
|
107
|
+
// derives its initial factor from it (see tileScaleFactor above).
|
|
108
|
+
// Appended into the same patch so placement + calibration are one
|
|
109
|
+
// undo step.
|
|
110
|
+
let effective = patch;
|
|
111
|
+
if (tileScalePlatformResolved !== 'mobile' &&
|
|
112
|
+
patch.ops.some((o) => o.op === 'addMeasurement')) {
|
|
113
|
+
const docSize = tileDocSizeForFactor(tileScaleFactor, viewportApi.state.zoom);
|
|
114
|
+
if (docSize > 0) {
|
|
115
|
+
effective = {
|
|
116
|
+
ops: [...patch.ops, { op: 'setTileDocSize', value: docSize }],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const inverse = invertPatch(canvas, effective);
|
|
121
|
+
undoStackRef.current.push({ forward: effective, inverse });
|
|
84
122
|
redoStackRef.current = [];
|
|
85
123
|
setPreviewPatch(null);
|
|
86
|
-
onCommit(
|
|
124
|
+
onCommit(effective);
|
|
87
125
|
},
|
|
88
126
|
setSelection(s) {
|
|
89
127
|
onSelectionChange(s);
|
|
@@ -108,6 +146,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
108
146
|
viewportApi,
|
|
109
147
|
tileViewportScale,
|
|
110
148
|
tileScaleFactor,
|
|
149
|
+
tileScalePlatformResolved,
|
|
111
150
|
onCommit,
|
|
112
151
|
onSelectionChange,
|
|
113
152
|
pickMeasurement,
|
|
@@ -205,7 +244,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
205
244
|
return;
|
|
206
245
|
let line = m.line;
|
|
207
246
|
if (!line) {
|
|
208
|
-
const len =
|
|
247
|
+
const len = defaultLeaderLenDoc(canvas.viewport.width);
|
|
209
248
|
line = {
|
|
210
249
|
a: { x: m.anchor.x - len / 2, y: m.anchor.y },
|
|
211
250
|
b: { x: m.anchor.x + len / 2, y: m.anchor.y },
|
|
@@ -256,6 +295,12 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
256
295
|
// for a high-res background is the very crop this is meant to escape.
|
|
257
296
|
setViewport(computeContentFit(canvas, width, height));
|
|
258
297
|
},
|
|
298
|
+
getTileScaleFactor() {
|
|
299
|
+
return ctxRef.current.tileScaleFactor;
|
|
300
|
+
},
|
|
301
|
+
getTileDocSize(factor) {
|
|
302
|
+
return tileDocSizeForFactor(factor, ctxRef.current.viewport.state.zoom);
|
|
303
|
+
},
|
|
259
304
|
placeMeasurementAtCenter(ref) {
|
|
260
305
|
const c = ctxRef.current;
|
|
261
306
|
const anchor = c.viewport.screenToWorld({
|
|
@@ -286,10 +331,9 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
286
331
|
x: width / 2,
|
|
287
332
|
y: height / 2,
|
|
288
333
|
});
|
|
289
|
-
// Default line length: a
|
|
290
|
-
//
|
|
291
|
-
const len = opts?.defaultLengthDoc ??
|
|
292
|
-
Math.min(400, Math.max(120, canvas.viewport.width * 0.25));
|
|
334
|
+
// Default line length: a generous fraction of the doc width, clamped,
|
|
335
|
+
// so the leader and its endcaps clear the fixed-size tile at full-fit.
|
|
336
|
+
const len = opts?.defaultLengthDoc ?? defaultLeaderLenDoc(canvas.viewport.width);
|
|
293
337
|
const line = {
|
|
294
338
|
a: { x: center.x - len / 2, y: center.y },
|
|
295
339
|
b: { x: center.x + len / 2, y: center.y },
|
|
@@ -318,7 +362,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
318
362
|
// Reuse the existing rect if the annotation had one; otherwise
|
|
319
363
|
// synthesize a square centered on the anchor (same size clamp as
|
|
320
364
|
// the default line) so the tile doesn't jump.
|
|
321
|
-
const side =
|
|
365
|
+
const side = defaultLeaderLenDoc(canvas.viewport.width);
|
|
322
366
|
const rect = m.rect ?? {
|
|
323
367
|
a: { x: m.anchor.x - side / 2, y: m.anchor.y - side / 2 },
|
|
324
368
|
b: { x: m.anchor.x + side / 2, y: m.anchor.y + side / 2 },
|
|
@@ -341,7 +385,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
341
385
|
if (type === 'line') {
|
|
342
386
|
let line = m.line;
|
|
343
387
|
if (!line) {
|
|
344
|
-
const len =
|
|
388
|
+
const len = defaultLeaderLenDoc(canvas.viewport.width);
|
|
345
389
|
line = {
|
|
346
390
|
a: { x: m.anchor.x - len / 2, y: m.anchor.y },
|
|
347
391
|
b: { x: m.anchor.x + len / 2, y: m.anchor.y },
|
package/dist/exports.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type { GestureConfig, PanTrigger, AnnotationCanvasInnerProps, } from './a
|
|
|
23
23
|
export type { CanvasPointerEvent, RequestTextInput, ShapeDrawConfig, Tool, ToolContext, ToolState, } from './annotation/canvas/Tool.js';
|
|
24
24
|
export type { MeasurementRef, PickMeasurement, } from './annotation/canvas/measurementPicker.js';
|
|
25
25
|
export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './annotation/canvas/measurementStampOverlay.js';
|
|
26
|
-
export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
|
|
26
|
+
export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
|
|
27
27
|
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
|
|
28
28
|
export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
|
|
29
29
|
export { createSelectTool, type SelectToolOptions, } from './annotation/canvas/tools/selectTool.js';
|
package/dist/exports.js
CHANGED
|
@@ -23,7 +23,7 @@ export { useAnnotationMutations, } from './annotation/data/hooks/useAnnotationMu
|
|
|
23
23
|
export { useAnnotationCanvasDoc, } from './annotation/data/hooks/useAnnotationCanvasDoc.js';
|
|
24
24
|
export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
|
|
25
25
|
export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
|
|
26
|
-
export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
|
|
26
|
+
export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
|
|
27
27
|
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
|
|
28
28
|
export { createPenTool, } from './annotation/canvas/tools/penTool.js';
|
|
29
29
|
export { createSelectTool, } from './annotation/canvas/tools/selectTool.js';
|
|
@@ -111,6 +111,7 @@ export interface AnnotationCanvasState {
|
|
|
111
111
|
placedMeasurements: PlacedMeasurementRef[];
|
|
112
112
|
tileScaleFactor?: number;
|
|
113
113
|
tileScaleFactorMobile?: number;
|
|
114
|
+
tileDocSize?: number;
|
|
114
115
|
externalPayloadPath?: string;
|
|
115
116
|
}
|
|
116
117
|
export type AnnotationElement = (AnnotationStroke & {
|
|
@@ -160,6 +161,9 @@ export type AnnotationPatchOp = {
|
|
|
160
161
|
op: 'setTileScaleFactor';
|
|
161
162
|
value: number;
|
|
162
163
|
platform?: TileScalePlatform;
|
|
164
|
+
} | {
|
|
165
|
+
op: 'setTileDocSize';
|
|
166
|
+
value: number;
|
|
163
167
|
} | {
|
|
164
168
|
op: 'setLayers';
|
|
165
169
|
layers: AnnotationLayer[];
|
package/dist/types/annotation.js
CHANGED
|
@@ -77,6 +77,10 @@ const applyOp = (state, op) => {
|
|
|
77
77
|
return op.platform === 'mobile'
|
|
78
78
|
? { ...state, tileScaleFactorMobile: op.value }
|
|
79
79
|
: { ...state, tileScaleFactor: op.value };
|
|
80
|
+
case 'setTileDocSize':
|
|
81
|
+
// Guard nonsense values (a zero/negative doc size would derive a
|
|
82
|
+
// zero/negative mobile factor) — keep the existing calibration instead.
|
|
83
|
+
return op.value > 0 ? { ...state, tileDocSize: op.value } : state;
|
|
80
84
|
case 'setLayers':
|
|
81
85
|
return { ...state, layers: op.layers };
|
|
82
86
|
}
|
|
@@ -163,6 +167,14 @@ const invertOp = (before, op) => {
|
|
|
163
167
|
: before.tileScaleFactor) ?? 1,
|
|
164
168
|
...(op.platform && { platform: op.platform }),
|
|
165
169
|
};
|
|
170
|
+
case 'setTileDocSize':
|
|
171
|
+
// Restore the prior calibration when there was one. A doc that never
|
|
172
|
+
// had a calibration can't be reverted to "absent" through this op
|
|
173
|
+
// (applyOp ignores non-positive values), so the stamp survives undo —
|
|
174
|
+
// benign, since it still describes what the web user last saw.
|
|
175
|
+
return before.tileDocSize != null && before.tileDocSize > 0
|
|
176
|
+
? { op: 'setTileDocSize', value: before.tileDocSize }
|
|
177
|
+
: null;
|
|
166
178
|
case 'setLayers':
|
|
167
179
|
return { op: 'setLayers', layers: before.layers };
|
|
168
180
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reekon-tools/boldr-utils",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.35",
|
|
4
4
|
"description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
|
|
5
5
|
"author": "REEKON Tools",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsc",
|
|
26
26
|
"prepack": "yarn run build",
|
|
27
|
+
"sync:local": "./scripts/sync-local.sh",
|
|
27
28
|
"test": "vitest",
|
|
28
29
|
"coverage": "vitest run --coverage",
|
|
29
30
|
"format": "prettier --write .",
|