@reekon-tools/boldr-utils 1.13.2 → 1.14.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.js +2 -2
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +7 -7
- package/dist/annotation/canvas/Tool.d.ts +4 -3
- package/dist/annotation/canvas/measurementGeometry.d.ts +1 -1
- package/dist/annotation/canvas/measurementGeometry.js +5 -4
- package/dist/annotation/canvas/stampLayout.d.ts +2 -1
- package/dist/annotation/canvas/stampLayout.js +18 -2
- package/dist/annotation/canvas/tools/panTool.js +1 -1
- package/dist/annotation/canvas/tools/selectTool.js +14 -10
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +2 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +24 -3
- 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 +25 -8
- package/package.json +1 -1
|
@@ -284,7 +284,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
284
284
|
const world = state.ctx.viewport.screenToWorld(screen);
|
|
285
285
|
const placed = [...state.effectiveCanvas.placedMeasurements]
|
|
286
286
|
.reverse()
|
|
287
|
-
.find((m) => hitPlacedMeasurement(m, world, state.viewport.zoom, state.tileScaleFactor, state.tileViewportScale));
|
|
287
|
+
.find((m) => hitPlacedMeasurement(m, world, state.viewport.zoom, state.tileScaleFactor, state.tileViewportScale, state.headerTileScaleFactor));
|
|
288
288
|
if (placed)
|
|
289
289
|
onMeasurementStampDoubleTap(placed);
|
|
290
290
|
}, [onMeasurementStampDoubleTap, state]);
|
|
@@ -320,7 +320,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
320
320
|
}, children: state.effectiveCanvas.placedMeasurements.map((placed) => {
|
|
321
321
|
// Square tile or wide group-header pill — one footprint source of
|
|
322
322
|
// truth shared with the hit-test (stampTileDims).
|
|
323
|
-
const { width, height } = stampTileDims(placed, state.tileScaleFactor, state.tileViewportScale);
|
|
323
|
+
const { width, height } = stampTileDims(placed, state.tileScaleFactor, state.tileViewportScale, state.headerTileScaleFactor);
|
|
324
324
|
const cx = (placed.anchor.x - state.viewport.pan.x) * state.viewport.zoom;
|
|
325
325
|
const cy = (placed.anchor.y - state.viewport.pan.y) * state.viewport.zoom;
|
|
326
326
|
const isSelected = selection?.ids.includes(placed.id) ?? false;
|
|
@@ -875,7 +875,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
875
875
|
return;
|
|
876
876
|
const world = st.ctx.viewport.screenToWorld(screen);
|
|
877
877
|
const zoomNow = st.ctx.viewport.state.zoom;
|
|
878
|
-
const hit = dragSelection.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor);
|
|
878
|
+
const hit = dragSelection.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor);
|
|
879
879
|
if (!hit || hit.kind !== 'measurement' || hit.id !== prevSelectedId) {
|
|
880
880
|
return;
|
|
881
881
|
}
|
|
@@ -1193,7 +1193,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1193
1193
|
// so a selected tile isn't stuck until you deselect it).
|
|
1194
1194
|
const selId = st.ctx.selection?.ids[0];
|
|
1195
1195
|
if (selId &&
|
|
1196
|
-
!cfg.isSelectedTileGrab?.(st.ctx.document, selId, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor)) {
|
|
1196
|
+
!cfg.isSelectedTileGrab?.(st.ctx.document, selId, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor)) {
|
|
1197
1197
|
const handle = cfg.hitTestHandle?.(st.ctx.document, selId, world, zoomNow);
|
|
1198
1198
|
if (handle) {
|
|
1199
1199
|
const m = st.ctx.document.placedMeasurements.find((x) => x.id === selId);
|
|
@@ -1293,7 +1293,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1293
1293
|
return;
|
|
1294
1294
|
}
|
|
1295
1295
|
}
|
|
1296
|
-
const hit = cfg.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor);
|
|
1296
|
+
const hit = cfg.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor);
|
|
1297
1297
|
if (!hit) {
|
|
1298
1298
|
// Backgrounds can cover the whole document, so they only get the
|
|
1299
1299
|
// drag once every annotation element misses, and only when ALREADY
|
|
@@ -1313,7 +1313,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1313
1313
|
st.ctx.setSelection({ ids: [hit.id] });
|
|
1314
1314
|
// Grabbing a line annotation's tile slides it; otherwise group-move.
|
|
1315
1315
|
const grab = hit.kind === 'measurement'
|
|
1316
|
-
? cfg.classifyMeasurementGrab?.(st.ctx.document, hit.id, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor)
|
|
1316
|
+
? cfg.classifyMeasurementGrab?.(st.ctx.document, hit.id, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor, st.ctx.headerTileScaleFactor)
|
|
1317
1317
|
: 'move';
|
|
1318
1318
|
if (grab === 'slide') {
|
|
1319
1319
|
const m = st.ctx.document.placedMeasurements.find((x) => x.id === hit.id);
|
|
@@ -1621,7 +1621,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1621
1621
|
// non-fullscreen canvas (e.g. a diagram strip) escape into surrounding UI.
|
|
1622
1622
|
_jsxs(GestureHandlerRootView, { style: [{ width, height, overflow: 'hidden' }, style], children: [backgroundLayers.map((layer) => (_jsx(BackgroundSkImageLoader, { layer: layer, resolveUrl: resolveImageUrl, onImage: onBackgroundSkImage }, layer.id))), _jsx(GestureDetector, { gesture: gesture, children: _jsx(View, { style: { width, height }, collapsable: false, children: AnnotationCanvasSkia(skiaProps) }) }), renderMeasurementStamp && (_jsx(View, { pointerEvents: "box-none", style: StyleSheet.absoluteFill, children: state.effectiveCanvas.placedMeasurements.map((placed) => (_jsx(MeasurementStampOverlayItem, { placed: placed, measurement: placed.measurementId
|
|
1623
1623
|
? (state.measurementsById.get(placed.measurementId) ?? null)
|
|
1624
|
-
: (resolveStampMeasurement?.(placed) ?? null), selected: selection?.ids.includes(placed.id) ?? false, dragging: draggingId === placed.id, sliding: slidingId === placed.id, endpointDragging: epDragId === placed.id, rectResizing: rectDragId === placed.id, zoomSnapshot: state.viewport.zoom, zoom: zoom, panX: panX, panY: panY, dragX: dragX, dragY: dragY, slideCtx: slideCtx, epCtx: epCtx, rectCtx: rectCtx, renderMeasurementStamp: renderMeasurementStamp, tileScaleFactor: state.tileScaleFactor, tileViewportScale: state.tileViewportScale, onStampPress: onMeasurementStampPress, onStampLongPress: onMeasurementStampLongPress, onRemove: onMeasurementStampRemove
|
|
1624
|
+
: (resolveStampMeasurement?.(placed) ?? null), selected: selection?.ids.includes(placed.id) ?? false, dragging: draggingId === placed.id, sliding: slidingId === placed.id, endpointDragging: epDragId === placed.id, rectResizing: rectDragId === placed.id, zoomSnapshot: state.viewport.zoom, zoom: zoom, panX: panX, panY: panY, dragX: dragX, dragY: dragY, slideCtx: slideCtx, epCtx: epCtx, rectCtx: rectCtx, renderMeasurementStamp: renderMeasurementStamp, tileScaleFactor: state.tileScaleFactor, headerTileScaleFactor: state.headerTileScaleFactor, tileViewportScale: state.tileViewportScale, onStampPress: onMeasurementStampPress, onStampLongPress: onMeasurementStampLongPress, onRemove: onMeasurementStampRemove
|
|
1625
1625
|
? () => {
|
|
1626
1626
|
const defaultRemove = () => {
|
|
1627
1627
|
const { ops, keepSelection } = buildRemoveMeasurementOps(placed);
|
|
@@ -1637,10 +1637,10 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1637
1637
|
wrapContent: loupeWrap,
|
|
1638
1638
|
}) }))] }));
|
|
1639
1639
|
};
|
|
1640
|
-
const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, rectCtx, renderMeasurementStamp, tileScaleFactor, tileViewportScale, onStampPress, onStampLongPress, onRemove, }) => {
|
|
1640
|
+
const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, rectCtx, renderMeasurementStamp, tileScaleFactor, headerTileScaleFactor, tileViewportScale, onStampPress, onStampLongPress, onRemove, }) => {
|
|
1641
1641
|
// Square tile or wide group-header pill — one footprint source of truth
|
|
1642
1642
|
// shared with the hit-test (stampTileDims).
|
|
1643
|
-
const { width, height } = stampTileDims(placed, tileScaleFactor, tileViewportScale);
|
|
1643
|
+
const { width, height } = stampTileDims(placed, tileScaleFactor, tileViewportScale, headerTileScaleFactor ?? tileScaleFactor);
|
|
1644
1644
|
const halfW = width / 2;
|
|
1645
1645
|
const halfH = height / 2;
|
|
1646
1646
|
// Remove-"X" touch target, sized as a fraction of the tile and corner-pinned
|
|
@@ -24,6 +24,7 @@ export interface ToolContext {
|
|
|
24
24
|
viewport: ViewportApi;
|
|
25
25
|
tileViewportScale: number;
|
|
26
26
|
tileScaleFactor: number;
|
|
27
|
+
headerTileScaleFactor: number;
|
|
27
28
|
preview(patch: AnnotationDocumentPatch): void;
|
|
28
29
|
commit(patch: AnnotationDocumentPatch): void;
|
|
29
30
|
setSelection(selection: Selection | null): void;
|
|
@@ -54,12 +55,12 @@ export interface ShapeDrawConfig {
|
|
|
54
55
|
}
|
|
55
56
|
export type DragElementKind = 'stroke' | 'shape' | 'measurement' | 'background';
|
|
56
57
|
export interface DragSelectionConfig {
|
|
57
|
-
hitTest(doc: AnnotationCanvasState, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number): {
|
|
58
|
+
hitTest(doc: AnnotationCanvasState, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number, headerTileScaleFactor?: number): {
|
|
58
59
|
id: AnnotationElementId;
|
|
59
60
|
kind: DragElementKind;
|
|
60
61
|
} | null;
|
|
61
62
|
buildTranslatePatch(doc: AnnotationCanvasState, id: AnnotationElementId, kind: DragElementKind, delta: Vec2): AnnotationDocumentPatch | null;
|
|
62
|
-
classifyMeasurementGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number): 'slide' | 'move';
|
|
63
|
+
classifyMeasurementGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number, headerTileScaleFactor?: number): 'slide' | 'move';
|
|
63
64
|
buildSlidePatch?(doc: AnnotationCanvasState, id: AnnotationElementId, delta: Vec2, zoom: number): AnnotationDocumentPatch | null;
|
|
64
65
|
hitTestHandle?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): 'a' | 'b' | null;
|
|
65
66
|
buildEndpointPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, handle: 'a' | 'b', delta: Vec2): AnnotationDocumentPatch | null;
|
|
@@ -77,7 +78,7 @@ export interface DragSelectionConfig {
|
|
|
77
78
|
fixed: Vec2;
|
|
78
79
|
} | null;
|
|
79
80
|
buildShapeCornerPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, corner: RectCorner, delta: Vec2): AnnotationDocumentPatch | null;
|
|
80
|
-
isSelectedTileGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number): boolean;
|
|
81
|
+
isSelectedTileGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number, headerTileScaleFactor?: number): boolean;
|
|
81
82
|
hitTestBackground?(doc: AnnotationCanvasState, world: Vec2): {
|
|
82
83
|
id: AnnotationElementId;
|
|
83
84
|
} | null;
|
|
@@ -36,7 +36,7 @@ export declare const rectCornerPoint: (rect: {
|
|
|
36
36
|
b: Vec2;
|
|
37
37
|
}, corner: RectCorner) => Vec2;
|
|
38
38
|
export declare const oppositeRectCorner: (corner: RectCorner) => RectCorner;
|
|
39
|
-
export declare const hitPlacedMeasurement: (m: PlacedMeasurementRef, p: Vec2, zoom?: number, tileScaleFactor?: number, viewportScale?: number) => boolean;
|
|
39
|
+
export declare const hitPlacedMeasurement: (m: PlacedMeasurementRef, p: Vec2, zoom?: number, tileScaleFactor?: number, viewportScale?: number, headerTileScaleFactor?: number) => boolean;
|
|
40
40
|
export interface RemoveMeasurementResult {
|
|
41
41
|
ops: AnnotationPatchOp[];
|
|
42
42
|
keepSelection: boolean;
|
|
@@ -110,14 +110,15 @@ const segmentDistSq = (p, a, b) => {
|
|
|
110
110
|
// around the anchor, the line body of a line annotation, or the
|
|
111
111
|
// border ring of a rectangle annotation (interiors stay transparent to hits so
|
|
112
112
|
// elements inside remain reachable).
|
|
113
|
-
export const hitPlacedMeasurement = (m, p, zoom = 1, tileScaleFactor = 1, viewportScale = 1) => {
|
|
113
|
+
export const hitPlacedMeasurement = (m, p, zoom = 1, tileScaleFactor = 1, viewportScale = 1, headerTileScaleFactor = tileScaleFactor) => {
|
|
114
114
|
// stampTileDims yields the tile's CURRENT screen-space box (the
|
|
115
115
|
// zoom-relative multiplier arrives via `viewportScale`); converting the
|
|
116
116
|
// half-extents (+ padding) back to doc space via the zoom keeps the hit box
|
|
117
117
|
// matching what's drawn — a doc-constant footprint (blank and populated
|
|
118
|
-
// stamps share the square footprint; group-header chips are a wide pill
|
|
119
|
-
//
|
|
120
|
-
|
|
118
|
+
// stamps share the square footprint; group-header chips are a wide pill
|
|
119
|
+
// sized by their OWN document-wide knob, `headerTileScaleFactor` — see
|
|
120
|
+
// stampTileDims in stampLayout).
|
|
121
|
+
const dims = stampTileDims(m, tileScaleFactor, viewportScale, headerTileScaleFactor);
|
|
121
122
|
const halfW = (dims.width / 2 + STAMP_HIT_PADDING) / zoom;
|
|
122
123
|
const halfH = (dims.height / 2 + STAMP_HIT_PADDING) / zoom;
|
|
123
124
|
const dx = Math.abs(p.x - m.anchor.x);
|
|
@@ -19,6 +19,7 @@ export declare const DEFAULT_TILE_SCALE = 1;
|
|
|
19
19
|
export declare const TILE_SCALE_MIN = 0.4;
|
|
20
20
|
export declare const TILE_SCALE_MAX = 2;
|
|
21
21
|
export declare const resolveTileScaleFactor: (canvas: Pick<AnnotationCanvasState, "tileScaleFactor" | "tileScaleFactorMobile">, platform: TileScalePlatform) => number;
|
|
22
|
+
export declare const resolveHeaderTileScaleFactor: (canvas: Pick<AnnotationCanvasState, "tileScaleFactor" | "tileScaleFactorMobile" | "headerTileScaleFactor" | "headerTileScaleFactorMobile">, platform: TileScalePlatform) => number;
|
|
22
23
|
export declare const clampTileScale: (v: number) => number;
|
|
23
24
|
export declare const tileDocSizeForFactor: (factor: number, zoom: number) => number;
|
|
24
25
|
export declare const tileScaleFromDocSize: (docSize: number, zoom: number) => number;
|
|
@@ -28,4 +29,4 @@ export interface StampTileDims {
|
|
|
28
29
|
width: number;
|
|
29
30
|
height: number;
|
|
30
31
|
}
|
|
31
|
-
export declare const stampTileDims: (m: Pick<PlacedMeasurementRef, "scale" | "isGroupHeader" | "collapsed">, tileScaleFactor?: number, viewportScale?: number) => StampTileDims;
|
|
32
|
+
export declare const stampTileDims: (m: Pick<PlacedMeasurementRef, "scale" | "isGroupHeader" | "collapsed">, tileScaleFactor?: number, viewportScale?: number, headerTileScaleFactor?: number) => StampTileDims;
|
|
@@ -80,6 +80,18 @@ export const TILE_SCALE_MAX = 2;
|
|
|
80
80
|
export const resolveTileScaleFactor = (canvas, platform) => (platform === 'mobile'
|
|
81
81
|
? (canvas.tileScaleFactorMobile ?? canvas.tileScaleFactor)
|
|
82
82
|
: canvas.tileScaleFactor) ?? DEFAULT_TILE_SCALE;
|
|
83
|
+
// Resolve the document-wide HEADER-scale knob for a platform — the group/form
|
|
84
|
+
// header counterpart of resolveTileScaleFactor (Asana 1217570500895543).
|
|
85
|
+
// Same platform fallback (mobile → web), then one more step: a document with
|
|
86
|
+
// no header knob at all falls back to the MEASUREMENT factor, the pre-split
|
|
87
|
+
// behavior where one knob drove every tile — so existing documents render
|
|
88
|
+
// identically until a user first moves the header slider. The ONE read path
|
|
89
|
+
// for the header fields — renderers, hit-testing, and the slider UIs must all
|
|
90
|
+
// resolve through here so they agree.
|
|
91
|
+
export const resolveHeaderTileScaleFactor = (canvas, platform) => (platform === 'mobile'
|
|
92
|
+
? (canvas.headerTileScaleFactorMobile ?? canvas.headerTileScaleFactor)
|
|
93
|
+
: canvas.headerTileScaleFactor) ??
|
|
94
|
+
resolveTileScaleFactor(canvas, platform);
|
|
83
95
|
// Clamp a tile-scale-factor candidate to the supported range. The single guard
|
|
84
96
|
// for the value before it lands in the document (slider input, restored docs).
|
|
85
97
|
export const clampTileScale = (v) => v < TILE_SCALE_MIN ? TILE_SCALE_MIN : v > TILE_SCALE_MAX ? TILE_SCALE_MAX : v;
|
|
@@ -136,11 +148,15 @@ export const isUnassociatedStamp = (m) => !m.isGroupHeader && !m.measurementId &
|
|
|
136
148
|
// canvas's content-fit reference zoom (see the note above), so tiles scale
|
|
137
149
|
// with the drawing as the user zooms.
|
|
138
150
|
export const stampTileSize = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1) => STAMP_TILE_SIZE * (m.scale ?? 1) * tileScaleFactor * viewportScale;
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
// `headerTileScaleFactor` is the header chips' own document-wide knob
|
|
152
|
+
// (resolveHeaderTileScaleFactor); it defaults to `tileScaleFactor` so callers
|
|
153
|
+
// that predate the header/measurement split keep their one-knob behavior.
|
|
154
|
+
export const stampTileDims = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1, headerTileScaleFactor = tileScaleFactor) => {
|
|
141
155
|
if (!m.isGroupHeader) {
|
|
156
|
+
const k = (m.scale ?? 1) * tileScaleFactor * viewportScale;
|
|
142
157
|
return { width: STAMP_TILE_SIZE * k, height: STAMP_TILE_SIZE * k };
|
|
143
158
|
}
|
|
159
|
+
const k = (m.scale ?? 1) * headerTileScaleFactor * viewportScale;
|
|
144
160
|
const base = groupHeaderBaseDims(m.collapsed);
|
|
145
161
|
return { width: base.width * k, height: base.height * k };
|
|
146
162
|
};
|
|
@@ -51,7 +51,7 @@ export const createPanTool = (options = {}) => ({
|
|
|
51
51
|
const measurements = ctx.document.placedMeasurements;
|
|
52
52
|
for (let i = measurements.length - 1; i >= 0; i--) {
|
|
53
53
|
const m = measurements[i];
|
|
54
|
-
if (hitPlacedMeasurement(m, event.world, zoom, ctx.tileScaleFactor, ctx.tileViewportScale)) {
|
|
54
|
+
if (hitPlacedMeasurement(m, event.world, zoom, ctx.tileScaleFactor, ctx.tileViewportScale, ctx.headerTileScaleFactor)) {
|
|
55
55
|
ctx.setSelection({ ids: [m.id] });
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
@@ -66,11 +66,15 @@ const segmentDistanceSq = (px, py, ax, ay, bx, by) => {
|
|
|
66
66
|
const findHit = (doc, world, zoom, viewportTileScale = 1,
|
|
67
67
|
// Platform-resolved tile-scale knob (ctx.tileScaleFactor). Defaults to the
|
|
68
68
|
// doc's web/legacy field so un-threaded callers keep the pre-split behavior.
|
|
69
|
-
tileScaleFactor = doc.tileScaleFactor
|
|
69
|
+
tileScaleFactor = doc.tileScaleFactor,
|
|
70
|
+
// Platform-resolved header-scale knob (ctx.headerTileScaleFactor). Defaults
|
|
71
|
+
// to the doc's web header field, falling back to the measurement knob the
|
|
72
|
+
// way resolveHeaderTileScaleFactor does.
|
|
73
|
+
headerTileScaleFactor = doc.headerTileScaleFactor ?? tileScaleFactor) => {
|
|
70
74
|
// Hit-test in z-order (top first): measurements > shapes > strokes.
|
|
71
75
|
for (let i = doc.placedMeasurements.length - 1; i >= 0; i--) {
|
|
72
76
|
const m = doc.placedMeasurements[i];
|
|
73
|
-
if (hitPlacedMeasurement(m, world, zoom, tileScaleFactor, viewportTileScale)) {
|
|
77
|
+
if (hitPlacedMeasurement(m, world, zoom, tileScaleFactor, viewportTileScale, headerTileScaleFactor)) {
|
|
74
78
|
return { id: m.id, kind: 'measurement' };
|
|
75
79
|
}
|
|
76
80
|
}
|
|
@@ -179,11 +183,11 @@ const translatePatch = (elementKind, id, doc, delta) => {
|
|
|
179
183
|
// is selected; without this, a handle sitting under the tile (a rectangle
|
|
180
184
|
// tile at the rect center, a line tile slid onto an endpoint) hijacks the grab
|
|
181
185
|
// and the tile becomes unmovable until deselected.
|
|
182
|
-
const isOnMeasurementTile = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor) => {
|
|
186
|
+
const isOnMeasurementTile = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor, headerTileScaleFactor = doc.headerTileScaleFactor ?? tileScaleFactor) => {
|
|
183
187
|
const m = doc.placedMeasurements.find((x) => x.id === id);
|
|
184
188
|
if (!m)
|
|
185
189
|
return false;
|
|
186
|
-
const dims = stampTileDims(m, tileScaleFactor, viewportTileScale);
|
|
190
|
+
const dims = stampTileDims(m, tileScaleFactor, viewportTileScale, headerTileScaleFactor);
|
|
187
191
|
const halfW = (dims.width / 2 + HIT_PADDING) / zoom;
|
|
188
192
|
const halfH = (dims.height / 2 + HIT_PADDING) / zoom;
|
|
189
193
|
return (Math.abs(world.x - m.anchor.x) <= halfW &&
|
|
@@ -193,14 +197,14 @@ const isOnMeasurementTile = (doc, id, world, zoom, viewportTileScale = 1, tileSc
|
|
|
193
197
|
// via DragSelectionConfig AND the web pointer handlers — one source of truth) ---
|
|
194
198
|
// Grabbing the tile of a line annotation slides it along the line; everything
|
|
195
199
|
// else (bare stamps, grabs on the line body) is a group move.
|
|
196
|
-
const classifyGrab = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor) => {
|
|
200
|
+
const classifyGrab = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor, headerTileScaleFactor = doc.headerTileScaleFactor ?? tileScaleFactor) => {
|
|
197
201
|
const m = doc.placedMeasurements.find((x) => x.id === id);
|
|
198
202
|
if (!m)
|
|
199
203
|
return 'move';
|
|
200
204
|
// Same footprint the tile is drawn at (wide pill for group headers; folds in
|
|
201
205
|
// the document-wide tile scale + viewport scale so slide-grab matches the
|
|
202
206
|
// draw).
|
|
203
|
-
const dims = stampTileDims(m, tileScaleFactor, viewportTileScale);
|
|
207
|
+
const dims = stampTileDims(m, tileScaleFactor, viewportTileScale, headerTileScaleFactor);
|
|
204
208
|
const halfW = (dims.width / 2 + HIT_PADDING) / zoom;
|
|
205
209
|
const halfH = (dims.height / 2 + HIT_PADDING) / zoom;
|
|
206
210
|
const onTile = Math.abs(world.x - m.anchor.x) <= halfW &&
|
|
@@ -600,7 +604,7 @@ export const createSelectTool = (options = {}) => ({
|
|
|
600
604
|
// reusing the same hit-test and translate logic the pointer handlers below
|
|
601
605
|
// use for web — one source of truth.
|
|
602
606
|
dragSelection: {
|
|
603
|
-
hitTest: (doc, world, zoom, viewportTileScale, tileScaleFactor) => findHit(doc, world, zoom, viewportTileScale, tileScaleFactor),
|
|
607
|
+
hitTest: (doc, world, zoom, viewportTileScale, tileScaleFactor, headerTileScaleFactor) => findHit(doc, world, zoom, viewportTileScale, tileScaleFactor, headerTileScaleFactor),
|
|
604
608
|
buildTranslatePatch: (doc, id, kind, delta) => {
|
|
605
609
|
const op = translatePatch(kind, id, doc, delta);
|
|
606
610
|
return op ? { ops: [op] } : null;
|
|
@@ -639,7 +643,7 @@ export const createSelectTool = (options = {}) => ({
|
|
|
639
643
|
// tile stays draggable (otherwise it can only be moved after deselecting).
|
|
640
644
|
const selId = ctx.selection?.ids[0];
|
|
641
645
|
if (selId &&
|
|
642
|
-
!isOnMeasurementTile(ctx.document, selId, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor)) {
|
|
646
|
+
!isOnMeasurementTile(ctx.document, selId, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor, ctx.headerTileScaleFactor)) {
|
|
643
647
|
const handle = findHandleHit(ctx.document, selId, world, zoom);
|
|
644
648
|
if (handle) {
|
|
645
649
|
ctx.setSelection({ ids: [selId] });
|
|
@@ -708,7 +712,7 @@ export const createSelectTool = (options = {}) => ({
|
|
|
708
712
|
};
|
|
709
713
|
}
|
|
710
714
|
}
|
|
711
|
-
const hit = findHit(ctx.document, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor);
|
|
715
|
+
const hit = findHit(ctx.document, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor, ctx.headerTileScaleFactor);
|
|
712
716
|
if (!hit) {
|
|
713
717
|
// Background layers only get a look-in when every annotation element
|
|
714
718
|
// misses — they can cover the whole document, so elements above always
|
|
@@ -753,7 +757,7 @@ export const createSelectTool = (options = {}) => ({
|
|
|
753
757
|
: null;
|
|
754
758
|
ctx.setSelection({ ids: [hit.id] });
|
|
755
759
|
const mode = hit.kind === 'measurement' &&
|
|
756
|
-
classifyGrab(ctx.document, hit.id, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor) === 'slide'
|
|
760
|
+
classifyGrab(ctx.document, hit.id, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor, ctx.headerTileScaleFactor) === 'slide'
|
|
757
761
|
? 'slide'
|
|
758
762
|
: 'move';
|
|
759
763
|
return {
|
|
@@ -24,6 +24,7 @@ export interface AnnotationCanvasHandle {
|
|
|
24
24
|
durationMs?: number;
|
|
25
25
|
}): void;
|
|
26
26
|
getTileScaleFactor(): number;
|
|
27
|
+
getHeaderTileScaleFactor(): number;
|
|
27
28
|
getTileDocSize(factor: number): number;
|
|
28
29
|
ensureMeasurementVisible(id: AnnotationElementId): void;
|
|
29
30
|
placeMeasurementAtCenter(ref: MeasurementRef): void;
|
|
@@ -84,6 +85,7 @@ export interface AnnotationCanvasStateApi {
|
|
|
84
85
|
ctx: ToolContext;
|
|
85
86
|
tileViewportScale: number;
|
|
86
87
|
tileScaleFactor: number;
|
|
88
|
+
headerTileScaleFactor: number;
|
|
87
89
|
penDrawingStroke: AnnotationStroke | null;
|
|
88
90
|
customPreviewState: ToolState;
|
|
89
91
|
dispatchPointerDown(event: CanvasPointerEvent): void;
|
|
@@ -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, stampTileDims, STAMP_TILE_SIZE, tileDocSizeForFactor, tileScaleFromDocSize, } from './stampLayout.js';
|
|
3
|
+
import { resolveHeaderTileScaleFactor, resolveTileScaleFactor, stampTileDims, STAMP_TILE_SIZE, tileDocSizeForFactor, tileScaleFromDocSize, } from './stampLayout.js';
|
|
4
4
|
import { createViewportApi, fitRectToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
|
|
5
5
|
import { backgroundLayerDocRect, backgroundLayersOf, backgroundLayersUnionRect, } from './backgroundLayers.js';
|
|
6
6
|
import { buildRemoveMeasurementOps, recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
|
|
@@ -157,13 +157,28 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
157
157
|
// commit writes tileScaleFactorMobile and takes over for good.
|
|
158
158
|
const tileScalePlatformResolved = tileScalePlatform ?? 'web';
|
|
159
159
|
let tileScaleFactor = resolveTileScaleFactor(effectiveCanvas, tileScalePlatformResolved);
|
|
160
|
+
// Group/form header chips carry their own document-wide knob (Asana
|
|
161
|
+
// 1217570500895543), resolved the same way. With no header knob set it
|
|
162
|
+
// equals the measurement factor (resolveHeaderTileScaleFactor's fallback) —
|
|
163
|
+
// the pre-split one-knob behavior.
|
|
164
|
+
let headerTileScaleFactor = resolveHeaderTileScaleFactor(effectiveCanvas, tileScalePlatformResolved);
|
|
160
165
|
if (tileScalePlatformResolved === 'mobile' &&
|
|
161
166
|
effectiveCanvas.tileScaleFactorMobile == null &&
|
|
162
167
|
effectiveCanvas.tileDocSize != null &&
|
|
163
168
|
effectiveCanvas.tileDocSize > 0 &&
|
|
164
169
|
width > 0 &&
|
|
165
170
|
height > 0) {
|
|
166
|
-
|
|
171
|
+
const calibrated = tileScaleFromDocSize(effectiveCanvas.tileDocSize, baseFitZoom);
|
|
172
|
+
// Scale the header factor by the same calibration ratio so its relation
|
|
173
|
+
// to the measurement factor survives the override: a doc with no header
|
|
174
|
+
// knob keeps headers exactly at the calibrated measurement factor, and a
|
|
175
|
+
// doc whose web user set a header knob keeps the same header-to-tile
|
|
176
|
+
// proportion the web user saw.
|
|
177
|
+
headerTileScaleFactor =
|
|
178
|
+
tileScaleFactor > 0
|
|
179
|
+
? headerTileScaleFactor * (calibrated / tileScaleFactor)
|
|
180
|
+
: calibrated;
|
|
181
|
+
tileScaleFactor = calibrated;
|
|
167
182
|
}
|
|
168
183
|
const ctx = useMemo(() => ({
|
|
169
184
|
document: canvas,
|
|
@@ -171,6 +186,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
171
186
|
viewport: viewportApi,
|
|
172
187
|
tileViewportScale,
|
|
173
188
|
tileScaleFactor,
|
|
189
|
+
headerTileScaleFactor,
|
|
174
190
|
preview(patch) {
|
|
175
191
|
setPreviewPatch(patch);
|
|
176
192
|
},
|
|
@@ -222,6 +238,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
222
238
|
viewportApi,
|
|
223
239
|
tileViewportScale,
|
|
224
240
|
tileScaleFactor,
|
|
241
|
+
headerTileScaleFactor,
|
|
225
242
|
tileScalePlatformResolved,
|
|
226
243
|
baseFitZoom,
|
|
227
244
|
onCommit,
|
|
@@ -425,6 +442,9 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
425
442
|
getTileScaleFactor() {
|
|
426
443
|
return ctxRef.current.tileScaleFactor;
|
|
427
444
|
},
|
|
445
|
+
getHeaderTileScaleFactor() {
|
|
446
|
+
return ctxRef.current.headerTileScaleFactor;
|
|
447
|
+
},
|
|
428
448
|
getTileDocSize(factor) {
|
|
429
449
|
// Tiles are doc-constant, so the doc size a factor yields derives
|
|
430
450
|
// from the content-fit reference, not the current zoom.
|
|
@@ -437,7 +457,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
437
457
|
return;
|
|
438
458
|
// The tile's CURRENT on-screen half-extents: stampTileDims with the
|
|
439
459
|
// zoom-relative tileViewportScale folded in.
|
|
440
|
-
const dims = stampTileDims(m, c.tileScaleFactor, c.tileViewportScale);
|
|
460
|
+
const dims = stampTileDims(m, c.tileScaleFactor, c.tileViewportScale, c.headerTileScaleFactor);
|
|
441
461
|
const halfW = dims.width / 2;
|
|
442
462
|
const halfH = dims.height / 2;
|
|
443
463
|
const s = c.viewport.worldToScreen(m.anchor);
|
|
@@ -734,6 +754,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
734
754
|
ctx,
|
|
735
755
|
tileViewportScale,
|
|
736
756
|
tileScaleFactor,
|
|
757
|
+
headerTileScaleFactor,
|
|
737
758
|
penDrawingStroke,
|
|
738
759
|
customPreviewState: toolState,
|
|
739
760
|
dispatchPointerDown,
|
package/dist/exports.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type { GestureConfig, PanTrigger, AnnotationCanvasInnerProps, } from './a
|
|
|
29
29
|
export type { CanvasPointerEvent, RequestTextInput, ShapeDrawConfig, Tool, ToolContext, ToolState, } from './annotation/canvas/Tool.js';
|
|
30
30
|
export type { MeasurementRef, PickMeasurement, } from './annotation/canvas/measurementPicker.js';
|
|
31
31
|
export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './annotation/canvas/measurementStampOverlay.js';
|
|
32
|
-
export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
|
|
32
|
+
export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
|
|
33
33
|
export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, type GroupHeaderBadge, type GroupHeaderDescriptor, } from './annotation/canvas/groupHeaderLayout.js';
|
|
34
34
|
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
|
|
35
35
|
export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
|
package/dist/exports.js
CHANGED
|
@@ -34,7 +34,7 @@ export { useAnnotationMutations, } from './annotation/data/hooks/useAnnotationMu
|
|
|
34
34
|
export { useAnnotationCanvasDoc, } from './annotation/data/hooks/useAnnotationCanvasDoc.js';
|
|
35
35
|
export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
|
|
36
36
|
export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
|
|
37
|
-
export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
|
|
37
|
+
export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
|
|
38
38
|
export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, } from './annotation/canvas/groupHeaderLayout.js';
|
|
39
39
|
export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
|
|
40
40
|
export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
|
|
@@ -120,6 +120,7 @@ export interface AnnotationViewport {
|
|
|
120
120
|
backgroundImages?: AnnotationBackgroundLayer[];
|
|
121
121
|
}
|
|
122
122
|
export type TileScalePlatform = 'web' | 'mobile';
|
|
123
|
+
export type TileScaleTarget = 'measurement' | 'header';
|
|
123
124
|
export interface AnnotationCanvasState {
|
|
124
125
|
schemaVersion: 1;
|
|
125
126
|
layers: AnnotationLayer[];
|
|
@@ -129,6 +130,8 @@ export interface AnnotationCanvasState {
|
|
|
129
130
|
placedMeasurements: PlacedMeasurementRef[];
|
|
130
131
|
tileScaleFactor?: number;
|
|
131
132
|
tileScaleFactorMobile?: number;
|
|
133
|
+
headerTileScaleFactor?: number;
|
|
134
|
+
headerTileScaleFactorMobile?: number;
|
|
132
135
|
tileDocSize?: number;
|
|
133
136
|
externalPayloadPath?: string;
|
|
134
137
|
}
|
|
@@ -190,6 +193,7 @@ export type AnnotationPatchOp = {
|
|
|
190
193
|
op: 'setTileScaleFactor';
|
|
191
194
|
value: number;
|
|
192
195
|
platform?: TileScalePlatform;
|
|
196
|
+
target?: TileScaleTarget;
|
|
193
197
|
} | {
|
|
194
198
|
op: 'setTileDocSize';
|
|
195
199
|
value: number;
|
package/dist/types/annotation.js
CHANGED
|
@@ -120,6 +120,11 @@ const applyOp = (state, op) => {
|
|
|
120
120
|
case 'setViewport':
|
|
121
121
|
return { ...state, viewport: { ...state.viewport, ...op.patch } };
|
|
122
122
|
case 'setTileScaleFactor':
|
|
123
|
+
if (op.target === 'header') {
|
|
124
|
+
return op.platform === 'mobile'
|
|
125
|
+
? { ...state, headerTileScaleFactorMobile: op.value }
|
|
126
|
+
: { ...state, headerTileScaleFactor: op.value };
|
|
127
|
+
}
|
|
123
128
|
return op.platform === 'mobile'
|
|
124
129
|
? { ...state, tileScaleFactorMobile: op.value }
|
|
125
130
|
: { ...state, tileScaleFactor: op.value };
|
|
@@ -223,18 +228,30 @@ const invertOp = (before, op) => {
|
|
|
223
228
|
}
|
|
224
229
|
return { op: 'setViewport', patch: inversePatch };
|
|
225
230
|
}
|
|
226
|
-
case 'setTileScaleFactor':
|
|
227
|
-
// Restore the prior EFFECTIVE factor for the same platform
|
|
228
|
-
// falls back to the shared/web value when it has no
|
|
229
|
-
// pre-split doc case — mirrors
|
|
230
|
-
//
|
|
231
|
+
case 'setTileScaleFactor': {
|
|
232
|
+
// Restore the prior EFFECTIVE factor for the same platform (and tile
|
|
233
|
+
// kind) knob. Mobile falls back to the shared/web value when it has no
|
|
234
|
+
// own value yet (the pre-split doc case — mirrors
|
|
235
|
+
// resolveTileScaleFactor), and the header knob further falls back to
|
|
236
|
+
// the measurement knob (mirrors resolveHeaderTileScaleFactor); absent →
|
|
237
|
+
// an explicit 1 (visually identical), keeping the `value: number`
|
|
238
|
+
// contract.
|
|
239
|
+
const measurementPrior = op.platform === 'mobile'
|
|
240
|
+
? (before.tileScaleFactorMobile ?? before.tileScaleFactor)
|
|
241
|
+
: before.tileScaleFactor;
|
|
242
|
+
const prior = op.target === 'header'
|
|
243
|
+
? ((op.platform === 'mobile'
|
|
244
|
+
? (before.headerTileScaleFactorMobile ??
|
|
245
|
+
before.headerTileScaleFactor)
|
|
246
|
+
: before.headerTileScaleFactor) ?? measurementPrior)
|
|
247
|
+
: measurementPrior;
|
|
231
248
|
return {
|
|
232
249
|
op: 'setTileScaleFactor',
|
|
233
|
-
value:
|
|
234
|
-
? (before.tileScaleFactorMobile ?? before.tileScaleFactor)
|
|
235
|
-
: before.tileScaleFactor) ?? 1,
|
|
250
|
+
value: prior ?? 1,
|
|
236
251
|
...(op.platform && { platform: op.platform }),
|
|
252
|
+
...(op.target && { target: op.target }),
|
|
237
253
|
};
|
|
254
|
+
}
|
|
238
255
|
case 'setTileDocSize':
|
|
239
256
|
// Restore the prior calibration when there was one. A doc that never
|
|
240
257
|
// had a calibration can't be reverted to "absent" through this op
|