@reekon-tools/boldr-utils 1.6.28 → 1.6.30
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 +8 -3
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +21 -17
- package/dist/annotation/canvas/Tool.d.ts +4 -3
- package/dist/annotation/canvas/stampLayout.d.ts +2 -1
- package/dist/annotation/canvas/stampLayout.js +23 -11
- package/dist/annotation/canvas/tools/measurementLineTool.d.ts +12 -0
- package/dist/annotation/canvas/tools/measurementLineTool.js +95 -0
- package/dist/annotation/canvas/tools/panTool.js +1 -1
- package/dist/annotation/canvas/tools/selectTool.d.ts +4 -1
- package/dist/annotation/canvas/tools/selectTool.js +26 -14
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +5 -1
- package/dist/annotation/canvas/useAnnotationCanvasState.js +56 -2
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.d.ts +1 -0
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +12 -4
- package/dist/exports.d.ts +2 -2
- package/dist/exports.js +2 -2
- package/dist/types/annotation.d.ts +3 -0
- package/dist/types/annotation.js +14 -4
- package/package.json +1 -1
- package/dist/canvas/AnnotationCanvas.d.ts +0 -11
- package/dist/canvas/AnnotationCanvas.js +0 -10
- package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
- package/dist/canvas/AnnotationCanvas.native.js +0 -6
- package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
- package/dist/canvas/AnnotationCanvasInner.js +0 -219
- package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
- package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
- package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
- package/dist/canvas/AnnotationCanvasSkia.js +0 -20
- package/dist/canvas/Tool.d.ts +0 -38
- package/dist/canvas/Tool.js +0 -1
- package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
- package/dist/canvas/elements/BackgroundImageElement.js +0 -37
- package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
- package/dist/canvas/elements/MeasurementStampElement.js +0 -30
- package/dist/canvas/elements/ShapeElement.d.ts +0 -7
- package/dist/canvas/elements/ShapeElement.js +0 -62
- package/dist/canvas/elements/StrokeElement.d.ts +0 -7
- package/dist/canvas/elements/StrokeElement.js +0 -18
- package/dist/canvas/measurementPicker.d.ts +0 -10
- package/dist/canvas/measurementPicker.js +0 -1
- package/dist/canvas/measurementStampOverlay.d.ts +0 -11
- package/dist/canvas/measurementStampOverlay.js +0 -1
- package/dist/canvas/pointerAdapter.d.ts +0 -3
- package/dist/canvas/pointerAdapter.js +0 -19
- package/dist/canvas/stampLayout.d.ts +0 -5
- package/dist/canvas/stampLayout.js +0 -14
- package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
- package/dist/canvas/tools/measurementStampTool.js +0 -37
- package/dist/canvas/tools/panTool.d.ts +0 -5
- package/dist/canvas/tools/panTool.js +0 -25
- package/dist/canvas/tools/penTool.d.ts +0 -13
- package/dist/canvas/tools/penTool.js +0 -68
- package/dist/canvas/tools/selectTool.d.ts +0 -2
- package/dist/canvas/tools/selectTool.js +0 -182
- package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
- package/dist/canvas/useAnnotationCanvasState.js +0 -210
- package/dist/canvas/viewport.d.ts +0 -16
- package/dist/canvas/viewport.js +0 -54
- package/dist/data/AnnotationDataContext.d.ts +0 -8
- package/dist/data/AnnotationDataContext.js +0 -11
- package/dist/data/AnnotationDataProvider.d.ts +0 -65
- package/dist/data/AnnotationDataProvider.js +0 -4
- package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
- package/dist/data/InMemoryAnnotationProvider.js +0 -197
- package/dist/data/canvasPersistence.d.ts +0 -3
- package/dist/data/canvasPersistence.js +0 -26
- package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
- package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
- package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
- package/dist/data/hooks/useAnnotationDoc.js +0 -33
- package/dist/data/hooks/useAnnotationList.d.ts +0 -7
- package/dist/data/hooks/useAnnotationList.js +0 -26
- package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
- package/dist/data/hooks/useAnnotationMutations.js +0 -11
- package/dist/hooks/useParseMeasurement.d.ts +0 -4
- package/dist/hooks/useParseMeasurement.js +0 -14
- package/dist/utils/evaluateFormula.d.ts +0 -20
- package/dist/utils/evaluateFormula.js +0 -31
|
@@ -42,7 +42,12 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
42
42
|
provider.registerFont(textTypeface, TEXT_FONT_FAMILY);
|
|
43
43
|
return provider;
|
|
44
44
|
}, [textTypeface]);
|
|
45
|
-
|
|
45
|
+
// This is the web renderer, so the canvas reads web's tile-scale knob (web
|
|
46
|
+
// and mobile persist separate values — see resolveTileScaleFactor).
|
|
47
|
+
const state = useAnnotationCanvasState({
|
|
48
|
+
...props,
|
|
49
|
+
tileScalePlatform: 'web',
|
|
50
|
+
});
|
|
46
51
|
const containerRef = useRef(null);
|
|
47
52
|
const panGestureRef = useRef(null);
|
|
48
53
|
const spaceDownRef = useRef(false);
|
|
@@ -261,7 +266,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
261
266
|
const world = state.ctx.viewport.screenToWorld(screen);
|
|
262
267
|
const placed = [...state.effectiveCanvas.placedMeasurements]
|
|
263
268
|
.reverse()
|
|
264
|
-
.find((m) => hitPlacedMeasurement(m, world, state.viewport.zoom, state.
|
|
269
|
+
.find((m) => hitPlacedMeasurement(m, world, state.viewport.zoom, state.tileScaleFactor, state.tileViewportScale));
|
|
265
270
|
if (placed)
|
|
266
271
|
onMeasurementStampDoubleTap(placed);
|
|
267
272
|
}, [onMeasurementStampDoubleTap, state]);
|
|
@@ -293,7 +298,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
293
298
|
inset: 0,
|
|
294
299
|
pointerEvents: 'none',
|
|
295
300
|
}, children: state.effectiveCanvas.placedMeasurements.map((placed) => {
|
|
296
|
-
const size = stampTileSize(placed, state.
|
|
301
|
+
const size = stampTileSize(placed, state.tileScaleFactor, state.tileViewportScale);
|
|
297
302
|
const cx = (placed.anchor.x - state.viewport.pan.x) * state.viewport.zoom;
|
|
298
303
|
const cy = (placed.anchor.y - state.viewport.pan.y) * state.viewport.zoom;
|
|
299
304
|
const isSelected = selection?.ids.includes(placed.id) ?? false;
|
|
@@ -58,7 +58,13 @@ const LOUPE_CROSSHAIR_ARM = 9; // half-length of each crosshair arm, points
|
|
|
58
58
|
export const AnnotationCanvasInner = (props) => {
|
|
59
59
|
const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, } = props;
|
|
60
60
|
const valueFont = useFont(stampFontSource, stampValueFontSize);
|
|
61
|
-
|
|
61
|
+
// This is the native (mobile) renderer, so the canvas reads mobile's
|
|
62
|
+
// tile-scale knob (web and mobile persist separate values — see
|
|
63
|
+
// resolveTileScaleFactor).
|
|
64
|
+
const state = useAnnotationCanvasState({
|
|
65
|
+
...props,
|
|
66
|
+
tileScalePlatform: 'mobile',
|
|
67
|
+
});
|
|
62
68
|
// Decode the background image ONCE here and share it with both the main
|
|
63
69
|
// canvas and the magnifier loupe. If each canvas loaded its own, the loupe
|
|
64
70
|
// would flash its blank base every gesture while it re-resolved + re-decoded.
|
|
@@ -746,7 +752,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
746
752
|
return;
|
|
747
753
|
const world = st.ctx.viewport.screenToWorld(screen);
|
|
748
754
|
const zoomNow = st.ctx.viewport.state.zoom;
|
|
749
|
-
const hit = dragSelection.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale);
|
|
755
|
+
const hit = dragSelection.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor);
|
|
750
756
|
if (!hit || hit.kind !== 'measurement' || hit.id !== prevSelectedId) {
|
|
751
757
|
return;
|
|
752
758
|
}
|
|
@@ -1012,7 +1018,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1012
1018
|
// so a selected tile isn't stuck until you deselect it).
|
|
1013
1019
|
const selId = st.ctx.selection?.ids[0];
|
|
1014
1020
|
if (selId &&
|
|
1015
|
-
!cfg.isSelectedTileGrab?.(st.ctx.document, selId, world, zoomNow, st.ctx.tileViewportScale)) {
|
|
1021
|
+
!cfg.isSelectedTileGrab?.(st.ctx.document, selId, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor)) {
|
|
1016
1022
|
const handle = cfg.hitTestHandle?.(st.ctx.document, selId, world, zoomNow);
|
|
1017
1023
|
if (handle) {
|
|
1018
1024
|
const m = st.ctx.document.placedMeasurements.find((x) => x.id === selId);
|
|
@@ -1112,7 +1118,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1112
1118
|
return;
|
|
1113
1119
|
}
|
|
1114
1120
|
}
|
|
1115
|
-
const hit = cfg.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale);
|
|
1121
|
+
const hit = cfg.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor);
|
|
1116
1122
|
if (!hit) {
|
|
1117
1123
|
st.ctx.setSelection(null);
|
|
1118
1124
|
dragTargetRef.current = null;
|
|
@@ -1121,7 +1127,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1121
1127
|
st.ctx.setSelection({ ids: [hit.id] });
|
|
1122
1128
|
// Grabbing a line annotation's tile slides it; otherwise group-move.
|
|
1123
1129
|
const grab = hit.kind === 'measurement'
|
|
1124
|
-
? cfg.classifyMeasurementGrab?.(st.ctx.document, hit.id, world, zoomNow, st.ctx.tileViewportScale)
|
|
1130
|
+
? cfg.classifyMeasurementGrab?.(st.ctx.document, hit.id, world, zoomNow, st.ctx.tileViewportScale, st.ctx.tileScaleFactor)
|
|
1125
1131
|
: 'move';
|
|
1126
1132
|
if (grab === 'slide') {
|
|
1127
1133
|
const m = st.ctx.document.placedMeasurements.find((x) => x.id === hit.id);
|
|
@@ -1423,19 +1429,17 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1423
1429
|
const loupeWrap = (content) => (_jsxs(_Fragment, { children: [_jsxs(Group, { clip: loupeClip, children: [_jsx(Rect, { x: loupeX, y: loupeY, width: loupeSize, height: loupeSize, color: LOUPE_BG_COLOR }), content] }), _jsx(RoundedRect, { x: loupeX, y: loupeY, width: loupeSize, height: loupeSize, r: LOUPE_RADIUS, color: LOUPE_BORDER_COLOR, style: "stroke", strokeWidth: LOUPE_BORDER_WIDTH }), _jsx(Line, { p1: { x: loupeCx - LOUPE_CROSSHAIR_ARM, y: loupeCy }, p2: { x: loupeCx + LOUPE_CROSSHAIR_ARM, y: loupeCy }, color: LOUPE_CROSSHAIR_COLOR, strokeWidth: 1.5 }), _jsx(Line, { p1: { x: loupeCx, y: loupeCy - LOUPE_CROSSHAIR_ARM }, p2: { x: loupeCx, y: loupeCy + LOUPE_CROSSHAIR_ARM }, color: LOUPE_CROSSHAIR_COLOR, strokeWidth: 1.5 })] }));
|
|
1424
1430
|
return (_jsxs(GestureHandlerRootView, { style: [{ width, height }, style], children: [_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
|
|
1425
1431
|
? (state.measurementsById.get(placed.measurementId) ?? null)
|
|
1426
|
-
: (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.
|
|
1427
|
-
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1432
|
+
: (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
|
|
1433
|
+
? () => {
|
|
1434
|
+
const defaultRemove = () => {
|
|
1435
|
+
const { ops, keepSelection } = buildRemoveMeasurementOps(placed);
|
|
1436
|
+
state.ctx.commit({ ops });
|
|
1437
|
+
if (!keepSelection)
|
|
1438
|
+
state.ctx.setSelection(null);
|
|
1439
|
+
};
|
|
1434
1440
|
onMeasurementStampRemove(placed, defaultRemove);
|
|
1435
|
-
return;
|
|
1436
1441
|
}
|
|
1437
|
-
|
|
1438
|
-
} }, placed.id))) })), magnifying && (_jsx(View, { pointerEvents: "none", style: StyleSheet.absoluteFill, children: AnnotationCanvasSkia({
|
|
1442
|
+
: undefined }, placed.id))) })), magnifying && (_jsx(View, { pointerEvents: "none", style: StyleSheet.absoluteFill, children: AnnotationCanvasSkia({
|
|
1439
1443
|
...skiaProps,
|
|
1440
1444
|
worldTransform: loupeTransform,
|
|
1441
1445
|
wrapContent: loupeWrap,
|
|
@@ -1517,7 +1521,7 @@ const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging,
|
|
|
1517
1521
|
selected,
|
|
1518
1522
|
size,
|
|
1519
1523
|
zoom: zoomSnapshot,
|
|
1520
|
-
}) }), onStampPress && (_jsx(TouchableOpacity, { accessibilityRole: "button", accessibilityLabel: "Open measurement", onPress: () => onStampPress(placed), onLongPress: onStampLongPress ? () => onStampLongPress(placed) : undefined, style: StyleSheet.absoluteFill })), selected && measurement && (_jsx(TouchableOpacity, { accessibilityRole: "button", accessibilityLabel: "Remove measurement", onPress: onRemove, style: {
|
|
1524
|
+
}) }), onStampPress && (_jsx(TouchableOpacity, { accessibilityRole: "button", accessibilityLabel: "Open measurement", onPress: () => onStampPress(placed), onLongPress: onStampLongPress ? () => onStampLongPress(placed) : undefined, style: StyleSheet.absoluteFill })), selected && measurement && onRemove && (_jsx(TouchableOpacity, { accessibilityRole: "button", accessibilityLabel: "Remove measurement", onPress: onRemove, style: {
|
|
1521
1525
|
position: 'absolute',
|
|
1522
1526
|
top: 0,
|
|
1523
1527
|
right: 0,
|
|
@@ -23,6 +23,7 @@ export interface ToolContext {
|
|
|
23
23
|
selection: Selection | null;
|
|
24
24
|
viewport: ViewportApi;
|
|
25
25
|
tileViewportScale: number;
|
|
26
|
+
tileScaleFactor: number;
|
|
26
27
|
preview(patch: AnnotationDocumentPatch): void;
|
|
27
28
|
commit(patch: AnnotationDocumentPatch): void;
|
|
28
29
|
setSelection(selection: Selection | null): void;
|
|
@@ -53,12 +54,12 @@ export interface ShapeDrawConfig {
|
|
|
53
54
|
}
|
|
54
55
|
export type DragElementKind = 'stroke' | 'shape' | 'measurement';
|
|
55
56
|
export interface DragSelectionConfig {
|
|
56
|
-
hitTest(doc: AnnotationCanvasState, world: Vec2, zoom: number, viewportTileScale?: number): {
|
|
57
|
+
hitTest(doc: AnnotationCanvasState, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number): {
|
|
57
58
|
id: AnnotationElementId;
|
|
58
59
|
kind: DragElementKind;
|
|
59
60
|
} | null;
|
|
60
61
|
buildTranslatePatch(doc: AnnotationCanvasState, id: AnnotationElementId, kind: DragElementKind, delta: Vec2): AnnotationDocumentPatch | null;
|
|
61
|
-
classifyMeasurementGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number): 'slide' | 'move';
|
|
62
|
+
classifyMeasurementGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number): 'slide' | 'move';
|
|
62
63
|
buildSlidePatch?(doc: AnnotationCanvasState, id: AnnotationElementId, delta: Vec2, zoom: number): AnnotationDocumentPatch | null;
|
|
63
64
|
hitTestHandle?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): 'a' | 'b' | null;
|
|
64
65
|
buildEndpointPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, handle: 'a' | 'b', delta: Vec2): AnnotationDocumentPatch | null;
|
|
@@ -76,7 +77,7 @@ export interface DragSelectionConfig {
|
|
|
76
77
|
fixed: Vec2;
|
|
77
78
|
} | null;
|
|
78
79
|
buildShapeCornerPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, corner: RectCorner, delta: Vec2): AnnotationDocumentPatch | null;
|
|
79
|
-
isSelectedTileGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number): boolean;
|
|
80
|
+
isSelectedTileGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number): boolean;
|
|
80
81
|
hitTestResizeHandle?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): ResizeGeometry | null;
|
|
81
82
|
buildResizePatch?(doc: AnnotationCanvasState, id: AnnotationElementId, delta: Vec2): AnnotationDocumentPatch | null;
|
|
82
83
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { PlacedMeasurementRef } from '../../types/annotation.js';
|
|
1
|
+
import type { AnnotationCanvasState, PlacedMeasurementRef, TileScalePlatform } from '../../types/annotation.js';
|
|
2
2
|
export declare const STAMP_TILE_SIZE = 96;
|
|
3
3
|
export declare const DEFAULT_TILE_SCALE = 1;
|
|
4
4
|
export declare const TILE_SCALE_MIN = 0.4;
|
|
5
5
|
export declare const TILE_SCALE_MAX = 2;
|
|
6
|
+
export declare const resolveTileScaleFactor: (canvas: Pick<AnnotationCanvasState, "tileScaleFactor" | "tileScaleFactorMobile">, platform: TileScalePlatform) => number;
|
|
6
7
|
export declare const clampTileScale: (v: number) => number;
|
|
7
8
|
export declare const isUnassociatedStamp: (m: Pick<PlacedMeasurementRef, "measurementId" | "measurementPath" | "columnId">) => boolean;
|
|
8
9
|
export declare const stampTileSize: (m: Pick<PlacedMeasurementRef, "scale">, tileScaleFactor?: number, viewportScale?: number) => number;
|
|
@@ -12,16 +12,28 @@
|
|
|
12
12
|
// user who set the tile-scale slider against blank tiles saw their tiles
|
|
13
13
|
// balloon once populated (Asana 1216233394800417).
|
|
14
14
|
export const STAMP_TILE_SIZE = 96;
|
|
15
|
-
// Document-wide tile scale factor
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
15
|
+
// Document-wide tile scale factor: one knob that shrinks/grows EVERY
|
|
16
|
+
// measurement tile on the canvas at once, on top of each tile's own `scale`.
|
|
17
|
+
// Lets a user pull tiles down on a dense drawing where lines crowd together,
|
|
18
|
+
// or bump them up on a sparse one. Like `scale` it is purely a screen-space
|
|
19
|
+
// multiplier — it does NOT change with zoom. Absent === DEFAULT_TILE_SCALE
|
|
20
|
+
// (visually identical to documents written before the knob existed).
|
|
21
|
+
// Web and mobile persist separate knobs (AnnotationCanvasState.tileScaleFactor
|
|
22
|
+
// vs .tileScaleFactorMobile) — read them through resolveTileScaleFactor below.
|
|
22
23
|
export const DEFAULT_TILE_SCALE = 1;
|
|
23
24
|
export const TILE_SCALE_MIN = 0.4;
|
|
24
25
|
export const TILE_SCALE_MAX = 2;
|
|
26
|
+
// Resolve the document-wide tile-scale knob for a platform. Web and mobile
|
|
27
|
+
// persist separate values because a scale calibrated against a desktop window
|
|
28
|
+
// is wrong on a phone screen (and vice versa): web reads `tileScaleFactor`
|
|
29
|
+
// (also the legacy shared field from before the split); mobile reads its own
|
|
30
|
+
// `tileScaleFactorMobile` and falls back to the shared field, so a pre-split
|
|
31
|
+
// document keeps its current mobile look until a mobile user first moves the
|
|
32
|
+
// slider. The ONE read path for these fields — renderers, hit-testing, and
|
|
33
|
+
// the slider UIs must all resolve through here so they agree.
|
|
34
|
+
export const resolveTileScaleFactor = (canvas, platform) => (platform === 'mobile'
|
|
35
|
+
? (canvas.tileScaleFactorMobile ?? canvas.tileScaleFactor)
|
|
36
|
+
: canvas.tileScaleFactor) ?? DEFAULT_TILE_SCALE;
|
|
25
37
|
// Clamp a tile-scale-factor candidate to the supported range. The single guard
|
|
26
38
|
// for the value before it lands in the document (slider input, restored docs).
|
|
27
39
|
export const clampTileScale = (v) => v < TILE_SCALE_MIN ? TILE_SCALE_MIN : v > TILE_SCALE_MAX ? TILE_SCALE_MAX : v;
|
|
@@ -54,8 +66,8 @@ export const isUnassociatedStamp = (m) => !m.measurementId && !m.measurementPath
|
|
|
54
66
|
// value. The ONE source of truth for tile footprint — render overlay,
|
|
55
67
|
// hit-test, and slide-grab classification all call this so the drawn tile and
|
|
56
68
|
// its touch box always agree. `tileScaleFactor` is the document-wide knob
|
|
57
|
-
// (default 1,
|
|
58
|
-
//
|
|
59
|
-
// is always 1 — the old per-canvas
|
|
60
|
-
// above). Independent of zoom.
|
|
69
|
+
// (default 1), driven by the "Tile size" slider and already resolved for the
|
|
70
|
+
// caller's platform via resolveTileScaleFactor above. `viewportScale` is
|
|
71
|
+
// retained for call compatibility and is always 1 — the old per-canvas
|
|
72
|
+
// multiplier was retired (see the note above). Independent of zoom.
|
|
61
73
|
export const stampTileSize = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1) => STAMP_TILE_SIZE * (m.scale ?? 1) * tileScaleFactor * viewportScale;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PlacedMeasurementRef } from '../../../types/annotation.js';
|
|
2
|
+
import type { Tool } from '../Tool.js';
|
|
3
|
+
export interface MeasurementLineToolOptions {
|
|
4
|
+
id?: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
minDragPx?: number;
|
|
7
|
+
autoSwitchToSelect?: boolean;
|
|
8
|
+
selectToolId?: string;
|
|
9
|
+
onAutoSwitch?: (toToolId: string) => void;
|
|
10
|
+
onPlaced?: (measurement: PlacedMeasurementRef) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare const createMeasurementLineTool: (options?: MeasurementLineToolOptions) => Tool;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DEFAULT_LAYER_ID } from '../../../types/annotation.js';
|
|
2
|
+
import { DEFAULT_LINE_POS, recomputeAnchor } from '../measurementGeometry.js';
|
|
3
|
+
let counter = 0;
|
|
4
|
+
const makeId = () => `annotation-${Date.now().toString(36)}-${(counter++).toString(36)}`;
|
|
5
|
+
const firstLayerId = (doc) => doc.layers[0]?.id ?? DEFAULT_LAYER_ID;
|
|
6
|
+
// Build the blank line-measurement annotation for a drag from `a` to `b`: a
|
|
7
|
+
// 2-point line with a value tile magnetized to its center (linePos 0.5). The
|
|
8
|
+
// payload mirrors useAnnotationCanvasState's placeAnnotationAtCenter so a drawn
|
|
9
|
+
// line and a (legacy) center-placed one are byte-identical once committed.
|
|
10
|
+
const buildLineMeasurement = (opts) => {
|
|
11
|
+
const line = { a: opts.a, b: opts.b };
|
|
12
|
+
return {
|
|
13
|
+
id: opts.id,
|
|
14
|
+
layerId: opts.layerId,
|
|
15
|
+
placement: 'line',
|
|
16
|
+
line,
|
|
17
|
+
linePos: DEFAULT_LINE_POS,
|
|
18
|
+
// Center of the line; recomputeAnchor keeps this in sync on edits.
|
|
19
|
+
anchor: recomputeAnchor(line, 'line', DEFAULT_LINE_POS, {
|
|
20
|
+
x: (opts.a.x + opts.b.x) / 2,
|
|
21
|
+
y: (opts.a.y + opts.b.y) / 2,
|
|
22
|
+
}),
|
|
23
|
+
showLabel: true,
|
|
24
|
+
showValue: true,
|
|
25
|
+
createdAt: Date.now(),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
// Drag-to-draw measurement-line tool. Press-drag rubber-bands a measurement
|
|
29
|
+
// annotation (a line with a blank value tile at its center); release commits
|
|
30
|
+
// it. Mirrors createShapeTool's interaction so "input lines" are placed the
|
|
31
|
+
// same way as shapes — draw to add, not tap-the-icon-to-add — replacing the
|
|
32
|
+
// old center-place affordance. The annotation stays blank (no measurement
|
|
33
|
+
// associated) until the user fills it via the tile / picker. Skia-free, like
|
|
34
|
+
// every tool factory; it renders its live preview through ctx.preview, so it
|
|
35
|
+
// works on web and (via the generic tool-pan dispatch) on native.
|
|
36
|
+
export const createMeasurementLineTool = (options = {}) => {
|
|
37
|
+
const minDragPx = options.minDragPx ?? 4;
|
|
38
|
+
const autoSwitchToSelect = options.autoSwitchToSelect ?? true;
|
|
39
|
+
const selectToolId = options.selectToolId ?? 'select';
|
|
40
|
+
return {
|
|
41
|
+
id: options.id ?? 'measure-line',
|
|
42
|
+
label: options.label ?? 'Measurement line',
|
|
43
|
+
cursor: 'crosshair',
|
|
44
|
+
onPointerDown(event) {
|
|
45
|
+
return {
|
|
46
|
+
kind: 'measurement-line-drawing',
|
|
47
|
+
id: makeId(),
|
|
48
|
+
startWorld: event.world,
|
|
49
|
+
startScreen: event.screen,
|
|
50
|
+
moved: false,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
onPointerMove(event, ctx, state) {
|
|
54
|
+
const s = state;
|
|
55
|
+
if (s?.kind !== 'measurement-line-drawing')
|
|
56
|
+
return s;
|
|
57
|
+
const measurement = buildLineMeasurement({
|
|
58
|
+
id: s.id,
|
|
59
|
+
layerId: firstLayerId(ctx.document),
|
|
60
|
+
a: s.startWorld,
|
|
61
|
+
b: event.world,
|
|
62
|
+
});
|
|
63
|
+
ctx.preview({ ops: [{ op: 'addMeasurement', measurement }] });
|
|
64
|
+
return { ...s, moved: true };
|
|
65
|
+
},
|
|
66
|
+
onPointerUp(event, ctx, state) {
|
|
67
|
+
const s = state;
|
|
68
|
+
if (s?.kind !== 'measurement-line-drawing')
|
|
69
|
+
return;
|
|
70
|
+
const dx = event.screen.x - s.startScreen.x;
|
|
71
|
+
const dy = event.screen.y - s.startScreen.y;
|
|
72
|
+
if (!s.moved || dx * dx + dy * dy < minDragPx * minDragPx) {
|
|
73
|
+
// Accidental tap — discard the rubber-band, commit nothing.
|
|
74
|
+
ctx.preview({ ops: [] });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const measurement = buildLineMeasurement({
|
|
78
|
+
id: s.id,
|
|
79
|
+
layerId: firstLayerId(ctx.document),
|
|
80
|
+
a: s.startWorld,
|
|
81
|
+
b: event.world,
|
|
82
|
+
});
|
|
83
|
+
ctx.commit({ ops: [{ op: 'addMeasurement', measurement }] });
|
|
84
|
+
// Leave it selected (and hand back to select) so the blank line can be
|
|
85
|
+
// filled / moved straight away — the same flow as the text tool.
|
|
86
|
+
ctx.setSelection({ ids: [measurement.id] });
|
|
87
|
+
options.onPlaced?.(measurement);
|
|
88
|
+
if (autoSwitchToSelect)
|
|
89
|
+
options.onAutoSwitch?.(selectToolId);
|
|
90
|
+
},
|
|
91
|
+
onCancel(_state, ctx) {
|
|
92
|
+
ctx.preview({ ops: [] });
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
};
|
|
@@ -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.
|
|
54
|
+
if (hitPlacedMeasurement(m, event.world, zoom, ctx.tileScaleFactor, ctx.tileViewportScale)) {
|
|
55
55
|
ctx.setSelection({ ids: [m.id] });
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
@@ -56,11 +56,14 @@ const segmentDistanceSq = (px, py, ax, ay, bx, by) => {
|
|
|
56
56
|
const dy = py - cy;
|
|
57
57
|
return dx * dx + dy * dy;
|
|
58
58
|
};
|
|
59
|
-
const findHit = (doc, world, zoom, viewportTileScale = 1
|
|
59
|
+
const findHit = (doc, world, zoom, viewportTileScale = 1,
|
|
60
|
+
// Platform-resolved tile-scale knob (ctx.tileScaleFactor). Defaults to the
|
|
61
|
+
// doc's web/legacy field so un-threaded callers keep the pre-split behavior.
|
|
62
|
+
tileScaleFactor = doc.tileScaleFactor) => {
|
|
60
63
|
// Hit-test in z-order (top first): measurements > shapes > strokes.
|
|
61
64
|
for (let i = doc.placedMeasurements.length - 1; i >= 0; i--) {
|
|
62
65
|
const m = doc.placedMeasurements[i];
|
|
63
|
-
if (hitPlacedMeasurement(m, world, zoom,
|
|
66
|
+
if (hitPlacedMeasurement(m, world, zoom, tileScaleFactor, viewportTileScale)) {
|
|
64
67
|
return { id: m.id, kind: 'measurement' };
|
|
65
68
|
}
|
|
66
69
|
}
|
|
@@ -150,12 +153,11 @@ const translatePatch = (elementKind, id, doc, delta) => {
|
|
|
150
153
|
// is selected; without this, a handle sitting under the tile (a rectangle
|
|
151
154
|
// tile at the rect center, a line tile slid onto an endpoint) hijacks the grab
|
|
152
155
|
// and the tile becomes unmovable until deselected.
|
|
153
|
-
const isOnMeasurementTile = (doc, id, world, zoom, viewportTileScale = 1) => {
|
|
156
|
+
const isOnMeasurementTile = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor) => {
|
|
154
157
|
const m = doc.placedMeasurements.find((x) => x.id === id);
|
|
155
158
|
if (!m)
|
|
156
159
|
return false;
|
|
157
|
-
const half = (stampTileSize(m,
|
|
158
|
-
HIT_PADDING) /
|
|
160
|
+
const half = (stampTileSize(m, tileScaleFactor, viewportTileScale) / 2 + HIT_PADDING) /
|
|
159
161
|
zoom;
|
|
160
162
|
return (Math.abs(world.x - m.anchor.x) <= half &&
|
|
161
163
|
Math.abs(world.y - m.anchor.y) <= half);
|
|
@@ -164,15 +166,14 @@ const isOnMeasurementTile = (doc, id, world, zoom, viewportTileScale = 1) => {
|
|
|
164
166
|
// via DragSelectionConfig AND the web pointer handlers — one source of truth) ---
|
|
165
167
|
// Grabbing the tile of a line annotation slides it along the line; everything
|
|
166
168
|
// else (bare stamps, grabs on the line body) is a group move.
|
|
167
|
-
const classifyGrab = (doc, id, world, zoom, viewportTileScale = 1) => {
|
|
169
|
+
const classifyGrab = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFactor = doc.tileScaleFactor) => {
|
|
168
170
|
const m = doc.placedMeasurements.find((x) => x.id === id);
|
|
169
171
|
if (!m)
|
|
170
172
|
return 'move';
|
|
171
173
|
// Same footprint the tile is drawn at (smaller for unassociated inputs, #6;
|
|
172
174
|
// folds in the document-wide tile scale + viewport scale so slide-grab
|
|
173
175
|
// matches the draw).
|
|
174
|
-
const half = (stampTileSize(m,
|
|
175
|
-
HIT_PADDING) /
|
|
176
|
+
const half = (stampTileSize(m, tileScaleFactor, viewportTileScale) / 2 + HIT_PADDING) /
|
|
176
177
|
zoom;
|
|
177
178
|
const onTile = Math.abs(world.x - m.anchor.x) <= half &&
|
|
178
179
|
Math.abs(world.y - m.anchor.y) <= half;
|
|
@@ -472,7 +473,7 @@ const dragPatch = (s, doc, delta, zoom) => {
|
|
|
472
473
|
const op = translatePatch(s.elementKind, s.id, doc, delta);
|
|
473
474
|
return op ? { ops: [op] } : null;
|
|
474
475
|
};
|
|
475
|
-
export const createSelectTool = () => ({
|
|
476
|
+
export const createSelectTool = (options = {}) => ({
|
|
476
477
|
id: 'select',
|
|
477
478
|
label: 'Select',
|
|
478
479
|
cursor: 'default',
|
|
@@ -480,7 +481,7 @@ export const createSelectTool = () => ({
|
|
|
480
481
|
// reusing the same hit-test and translate logic the pointer handlers below
|
|
481
482
|
// use for web — one source of truth.
|
|
482
483
|
dragSelection: {
|
|
483
|
-
hitTest: (doc, world, zoom, viewportTileScale) => findHit(doc, world, zoom, viewportTileScale),
|
|
484
|
+
hitTest: (doc, world, zoom, viewportTileScale, tileScaleFactor) => findHit(doc, world, zoom, viewportTileScale, tileScaleFactor),
|
|
484
485
|
buildTranslatePatch: (doc, id, kind, delta) => {
|
|
485
486
|
const op = translatePatch(kind, id, doc, delta);
|
|
486
487
|
return op ? { ops: [op] } : null;
|
|
@@ -515,7 +516,7 @@ export const createSelectTool = () => ({
|
|
|
515
516
|
// tile stays draggable (otherwise it can only be moved after deselecting).
|
|
516
517
|
const selId = ctx.selection?.ids[0];
|
|
517
518
|
if (selId &&
|
|
518
|
-
!isOnMeasurementTile(ctx.document, selId, world, zoom, ctx.tileViewportScale)) {
|
|
519
|
+
!isOnMeasurementTile(ctx.document, selId, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor)) {
|
|
519
520
|
const handle = findHandleHit(ctx.document, selId, world, zoom);
|
|
520
521
|
if (handle) {
|
|
521
522
|
ctx.setSelection({ ids: [selId] });
|
|
@@ -579,9 +580,14 @@ export const createSelectTool = () => ({
|
|
|
579
580
|
};
|
|
580
581
|
}
|
|
581
582
|
}
|
|
582
|
-
const hit = findHit(ctx.document, world, zoom, ctx.tileViewportScale);
|
|
583
|
+
const hit = findHit(ctx.document, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor);
|
|
583
584
|
if (!hit) {
|
|
584
585
|
ctx.setSelection(null);
|
|
586
|
+
// Nothing under the pointer: with panOnEmptyDrag the gesture pans the
|
|
587
|
+
// viewport instead of dead-ending, so bare drags navigate the canvas.
|
|
588
|
+
if (options.panOnEmptyDrag) {
|
|
589
|
+
return { kind: 'panning', lastScreen: event.screen };
|
|
590
|
+
}
|
|
585
591
|
return { kind: 'idle' };
|
|
586
592
|
}
|
|
587
593
|
// Re-tapping an already-selected text shape (without dragging) opens its
|
|
@@ -595,8 +601,7 @@ export const createSelectTool = () => ({
|
|
|
595
601
|
: null;
|
|
596
602
|
ctx.setSelection({ ids: [hit.id] });
|
|
597
603
|
const mode = hit.kind === 'measurement' &&
|
|
598
|
-
classifyGrab(ctx.document, hit.id, world, zoom, ctx.tileViewportScale) ===
|
|
599
|
-
'slide'
|
|
604
|
+
classifyGrab(ctx.document, hit.id, world, zoom, ctx.tileViewportScale, ctx.tileScaleFactor) === 'slide'
|
|
600
605
|
? 'slide'
|
|
601
606
|
: 'move';
|
|
602
607
|
return {
|
|
@@ -610,6 +615,13 @@ export const createSelectTool = () => ({
|
|
|
610
615
|
},
|
|
611
616
|
onPointerMove(event, ctx, state) {
|
|
612
617
|
const s = state;
|
|
618
|
+
if (s?.kind === 'panning') {
|
|
619
|
+
ctx.applyPan({
|
|
620
|
+
x: event.screen.x - s.lastScreen.x,
|
|
621
|
+
y: event.screen.y - s.lastScreen.y,
|
|
622
|
+
});
|
|
623
|
+
return { ...s, lastScreen: event.screen };
|
|
624
|
+
}
|
|
613
625
|
if (s?.kind !== 'dragging')
|
|
614
626
|
return s;
|
|
615
627
|
const delta = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Measurement } from '../../types/firestore.js';
|
|
2
|
-
import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationElementId, type AnnotationStroke, type MeasurementPlacement, type Selection, type Vec2 } from '../../types/annotation.js';
|
|
2
|
+
import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationElementId, type AnnotationStroke, type MeasurementPlacement, type Selection, type TileScalePlatform, type Vec2 } from '../../types/annotation.js';
|
|
3
3
|
import type { MeasurementRef } from './measurementPicker.js';
|
|
4
4
|
import type { CanvasPointerEvent, RequestTextInput, Tool, ToolContext, ToolState } from './Tool.js';
|
|
5
5
|
import { type ViewportState } from './viewport.js';
|
|
@@ -15,6 +15,8 @@ export interface AnnotationCanvasHandle {
|
|
|
15
15
|
defaultLengthDoc?: number;
|
|
16
16
|
}): void;
|
|
17
17
|
setAnnotationType(id: AnnotationElementId, type: MeasurementPlacement): void;
|
|
18
|
+
centerOnLine(id: AnnotationElementId): void;
|
|
19
|
+
removeMeasurementValue(id: AnnotationElementId): void;
|
|
18
20
|
associateMeasurement(id: AnnotationElementId, ref: MeasurementRef): void;
|
|
19
21
|
bindColumn(id: AnnotationElementId, binding: {
|
|
20
22
|
groupId: string;
|
|
@@ -39,6 +41,7 @@ export interface UseAnnotationCanvasStateProps {
|
|
|
39
41
|
width: number;
|
|
40
42
|
height: number;
|
|
41
43
|
initialViewport?: ViewportState;
|
|
44
|
+
tileScalePlatform?: TileScalePlatform;
|
|
42
45
|
imperativeRef?: {
|
|
43
46
|
current: AnnotationCanvasHandle | null;
|
|
44
47
|
};
|
|
@@ -58,6 +61,7 @@ export interface AnnotationCanvasStateApi {
|
|
|
58
61
|
toolState: ToolState;
|
|
59
62
|
ctx: ToolContext;
|
|
60
63
|
tileViewportScale: number;
|
|
64
|
+
tileScaleFactor: number;
|
|
61
65
|
penDrawingStroke: AnnotationStroke | null;
|
|
62
66
|
customPreviewState: ToolState;
|
|
63
67
|
dispatchPointerDown(event: CanvasPointerEvent): void;
|
|
@@ -1,7 +1,8 @@
|
|
|
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
4
|
import { createViewportApi, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
|
|
4
|
-
import { recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
|
|
5
|
+
import { buildRemoveMeasurementOps, recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
|
|
5
6
|
// The viewport that frames the document's content on screen. When a background
|
|
6
7
|
// image is present we fit its rendered rect (so the whole image shows, filling
|
|
7
8
|
// the viewport regardless of its pixel resolution — never the native-resolution
|
|
@@ -22,7 +23,7 @@ const computeContentFit = (canvas, width, height) => {
|
|
|
22
23
|
// inners share this hook; each wraps it with platform-specific event
|
|
23
24
|
// capture and JSX (div + DOM events vs. GestureDetector + RN Views).
|
|
24
25
|
export const useAnnotationCanvasState = (props) => {
|
|
25
|
-
const { canvas, onCommit, tools, activeToolId, selection, onSelectionChange, measurements, pickMeasurement, requestTextInput, width, height, initialViewport, imperativeRef, } = props;
|
|
26
|
+
const { canvas, onCommit, tools, activeToolId, selection, onSelectionChange, measurements, pickMeasurement, requestTextInput, width, height, initialViewport, tileScalePlatform, imperativeRef, } = props;
|
|
26
27
|
const [viewport, setViewport] = useState(initialViewport ?? DEFAULT_VIEWPORT);
|
|
27
28
|
const [toolState, setToolState] = useState(undefined);
|
|
28
29
|
const [previewPatch, setPreviewPatch] = useState(null);
|
|
@@ -63,11 +64,17 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
63
64
|
// (hit box) thread it; keeping them in lockstep at 1 means both still agree.
|
|
64
65
|
// (See stampLayout.ts for why the old per-canvas multiplier was retired.)
|
|
65
66
|
const tileViewportScale = 1;
|
|
67
|
+
// The platform's tile-scale knob, resolved once from the effective canvas so
|
|
68
|
+
// tools (hit boxes) and overlays (drawn size) share one value. Uses the
|
|
69
|
+
// effective (preview-applied) canvas for parity with what is on screen; the
|
|
70
|
+
// slider only ever commits, so this matches ctx.document in practice.
|
|
71
|
+
const tileScaleFactor = resolveTileScaleFactor(effectiveCanvas, tileScalePlatform ?? 'web');
|
|
66
72
|
const ctx = useMemo(() => ({
|
|
67
73
|
document: canvas,
|
|
68
74
|
selection,
|
|
69
75
|
viewport: viewportApi,
|
|
70
76
|
tileViewportScale,
|
|
77
|
+
tileScaleFactor,
|
|
71
78
|
preview(patch) {
|
|
72
79
|
setPreviewPatch(patch);
|
|
73
80
|
},
|
|
@@ -100,6 +107,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
100
107
|
selection,
|
|
101
108
|
viewportApi,
|
|
102
109
|
tileViewportScale,
|
|
110
|
+
tileScaleFactor,
|
|
103
111
|
onCommit,
|
|
104
112
|
onSelectionChange,
|
|
105
113
|
pickMeasurement,
|
|
@@ -331,6 +339,46 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
331
339
|
ops: [{ op: 'updateMeasurement', id, patch: { placement: 'none' } }],
|
|
332
340
|
});
|
|
333
341
|
},
|
|
342
|
+
centerOnLine(id) {
|
|
343
|
+
const c = ctxRef.current;
|
|
344
|
+
const m = c.document.placedMeasurements.find((x) => x.id === id);
|
|
345
|
+
if (!m)
|
|
346
|
+
return;
|
|
347
|
+
// Same default-line synthesis as setAnnotationType('line') so centering
|
|
348
|
+
// a floating tile also gives it a line to sit on.
|
|
349
|
+
let line = m.line;
|
|
350
|
+
if (!line) {
|
|
351
|
+
const len = Math.min(400, Math.max(120, canvas.viewport.width * 0.25));
|
|
352
|
+
line = {
|
|
353
|
+
a: { x: m.anchor.x - len / 2, y: m.anchor.y },
|
|
354
|
+
b: { x: m.anchor.x + len / 2, y: m.anchor.y },
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
c.commit({
|
|
358
|
+
ops: [
|
|
359
|
+
{
|
|
360
|
+
op: 'updateMeasurement',
|
|
361
|
+
id,
|
|
362
|
+
patch: {
|
|
363
|
+
placement: 'line',
|
|
364
|
+
line,
|
|
365
|
+
linePos: 0.5,
|
|
366
|
+
anchor: recomputeAnchor(line, 'line', 0.5, m.anchor),
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
});
|
|
371
|
+
},
|
|
372
|
+
removeMeasurementValue(id) {
|
|
373
|
+
const c = ctxRef.current;
|
|
374
|
+
const m = c.document.placedMeasurements.find((x) => x.id === id);
|
|
375
|
+
if (!m)
|
|
376
|
+
return;
|
|
377
|
+
const { ops, keepSelection } = buildRemoveMeasurementOps(m);
|
|
378
|
+
c.commit({ ops });
|
|
379
|
+
if (!keepSelection)
|
|
380
|
+
c.setSelection(null);
|
|
381
|
+
},
|
|
334
382
|
associateMeasurement(id, ref) {
|
|
335
383
|
const c = ctxRef.current;
|
|
336
384
|
c.commit({
|
|
@@ -342,6 +390,11 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
342
390
|
measurementId: ref.measurementId,
|
|
343
391
|
measurementPath: ref.measurementPath,
|
|
344
392
|
groupId: ref.groupId,
|
|
393
|
+
// A direct association replaces any FORM-column binding — the
|
|
394
|
+
// tile now resolves through the measurement, not
|
|
395
|
+
// group.columns[columnId]. Cleared keys serialize away at the
|
|
396
|
+
// persistence boundary; undo restores the prior binding.
|
|
397
|
+
columnId: undefined,
|
|
345
398
|
labelOverride: ref.label,
|
|
346
399
|
unitOverride: ref.unit,
|
|
347
400
|
},
|
|
@@ -443,6 +496,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
443
496
|
toolState,
|
|
444
497
|
ctx,
|
|
445
498
|
tileViewportScale,
|
|
499
|
+
tileScaleFactor,
|
|
446
500
|
penDrawingStroke,
|
|
447
501
|
customPreviewState: toolState,
|
|
448
502
|
dispatchPointerDown,
|