@reekon-tools/boldr-utils 1.6.20 → 1.6.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/annotation/canvas/AnnotationCanvasInner.d.ts +3 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.js +35 -8
- package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +3 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +13 -6
- package/dist/annotation/canvas/stampLayout.d.ts +2 -4
- package/dist/annotation/canvas/stampLayout.js +31 -51
- package/dist/annotation/canvas/tools/selectTool.js +47 -15
- package/dist/annotation/canvas/tools/textEditing.js +2 -2
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +8 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +40 -6
- package/dist/types/annotation.d.ts +1 -0
- package/package.json +1 -1
|
@@ -24,6 +24,9 @@ export interface AnnotationCanvasInnerProps {
|
|
|
24
24
|
renderMeasurementStamp?: RenderMeasurementStamp;
|
|
25
25
|
onMeasurementStampPress?: (placed: PlacedMeasurementRef) => void;
|
|
26
26
|
onMeasurementStampLongPress?: (placed: PlacedMeasurementRef) => void;
|
|
27
|
+
onMeasurementStampDoubleTap?: (placed: PlacedMeasurementRef) => void;
|
|
28
|
+
resolveStampMeasurement?: (placed: PlacedMeasurementRef) => Measurement | null;
|
|
29
|
+
onMeasurementStampRemove?: (placed: PlacedMeasurementRef, defaultRemove: () => void) => void;
|
|
27
30
|
stampFontSource?: unknown;
|
|
28
31
|
stampValueFontSize?: number;
|
|
29
32
|
stampLabelFontSize?: number;
|
|
@@ -3,7 +3,7 @@ import { Skia, useFont, useTypeface } from '@shopify/react-native-skia';
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, } from 'react';
|
|
4
4
|
import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
|
|
5
5
|
import { TEXT_FONT_FAMILY } from './elements/ShapeElement.js';
|
|
6
|
-
import { buildRemoveMeasurementOps } from './measurementGeometry.js';
|
|
6
|
+
import { buildRemoveMeasurementOps, hitPlacedMeasurement, } from './measurementGeometry.js';
|
|
7
7
|
import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
|
|
8
8
|
import { stampTileSize } from './stampLayout.js';
|
|
9
9
|
// Screen-px radius of a measurement-annotation endpoint handle (matches the
|
|
@@ -244,8 +244,28 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
244
244
|
...style,
|
|
245
245
|
};
|
|
246
246
|
const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
|
|
247
|
-
const { renderMeasurementStamp, onMeasurementStampPress, onMeasurementStampLongPress, selection, } = props;
|
|
248
|
-
|
|
247
|
+
const { renderMeasurementStamp, onMeasurementStampPress, onMeasurementStampLongPress, onMeasurementStampDoubleTap, resolveStampMeasurement, onMeasurementStampRemove, selection, } = props;
|
|
248
|
+
// Double-click a stamp → fire the consumer callback (e.g. open its value
|
|
249
|
+
// editor). Runs the SAME hit-test the select tool uses (top-of-z-order), on
|
|
250
|
+
// the gesture container, so the stamp overlay stays pointer-events:none and
|
|
251
|
+
// drag-to-move / single-tap select are unaffected. dblclick is a distinct
|
|
252
|
+
// browser event that doesn't suppress the preceding pointerdown/up select.
|
|
253
|
+
const handleDoubleClick = useCallback((event) => {
|
|
254
|
+
if (!onMeasurementStampDoubleTap)
|
|
255
|
+
return;
|
|
256
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
257
|
+
const screen = {
|
|
258
|
+
x: event.clientX - (rect?.left ?? 0),
|
|
259
|
+
y: event.clientY - (rect?.top ?? 0),
|
|
260
|
+
};
|
|
261
|
+
const world = state.ctx.viewport.screenToWorld(screen);
|
|
262
|
+
const placed = [...state.effectiveCanvas.placedMeasurements]
|
|
263
|
+
.reverse()
|
|
264
|
+
.find((m) => hitPlacedMeasurement(m, world, state.viewport.zoom, state.effectiveCanvas.tileScaleFactor, state.tileViewportScale));
|
|
265
|
+
if (placed)
|
|
266
|
+
onMeasurementStampDoubleTap(placed);
|
|
267
|
+
}, [onMeasurementStampDoubleTap, state]);
|
|
268
|
+
return (_jsxs("div", { ref: containerRef, style: containerStyle, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerCancel, onDoubleClick: handleDoubleClick, onWheel: handleWheel, onContextMenu: handleContextMenu, children: [AnnotationCanvasSkia({
|
|
249
269
|
width,
|
|
250
270
|
height,
|
|
251
271
|
effectiveCanvas: state.effectiveCanvas,
|
|
@@ -279,7 +299,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
279
299
|
const isSelected = selection?.ids.includes(placed.id) ?? false;
|
|
280
300
|
const measurement = placed.measurementId
|
|
281
301
|
? (state.measurementsById.get(placed.measurementId) ?? null)
|
|
282
|
-
: null;
|
|
302
|
+
: (resolveStampMeasurement?.(placed) ?? null);
|
|
283
303
|
// Corner-pinned, tile-proportional remove target (see the style
|
|
284
304
|
// comment below) so it can't blanket a small tile and eat its grab.
|
|
285
305
|
const removeSize = Math.min(40, size * 0.4);
|
|
@@ -300,10 +320,17 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
300
320
|
? () => onMeasurementStampLongPress(placed)
|
|
301
321
|
: undefined })), isSelected && measurement && (_jsx("div", { role: "button", "aria-label": "Remove measurement", onPointerDown: (e) => {
|
|
302
322
|
e.stopPropagation();
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
323
|
+
const defaultRemove = () => {
|
|
324
|
+
const { ops, keepSelection } = buildRemoveMeasurementOps(placed);
|
|
325
|
+
state.ctx.commit({ ops });
|
|
326
|
+
if (!keepSelection)
|
|
327
|
+
state.ctx.setSelection(null);
|
|
328
|
+
};
|
|
329
|
+
if (onMeasurementStampRemove) {
|
|
330
|
+
onMeasurementStampRemove(placed, defaultRemove);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
defaultRemove();
|
|
307
334
|
}, style: {
|
|
308
335
|
// Sized as a fraction of the tile and corner-pinned so it
|
|
309
336
|
// never covers the center. A fixed 40px target blanketed
|
|
@@ -25,6 +25,9 @@ export interface AnnotationCanvasInnerProps {
|
|
|
25
25
|
renderMeasurementStamp?: RenderMeasurementStamp;
|
|
26
26
|
onMeasurementStampPress?: (placed: PlacedMeasurementRef) => void;
|
|
27
27
|
onMeasurementStampLongPress?: (placed: PlacedMeasurementRef) => void;
|
|
28
|
+
onMeasurementStampDoubleTap?: (placed: PlacedMeasurementRef) => void;
|
|
29
|
+
resolveStampMeasurement?: (placed: PlacedMeasurementRef) => Measurement | null;
|
|
30
|
+
onMeasurementStampRemove?: (placed: PlacedMeasurementRef, defaultRemove: () => void) => void;
|
|
28
31
|
stampFontSource?: unknown;
|
|
29
32
|
stampValueFontSize?: number;
|
|
30
33
|
stampLabelFontSize?: number;
|
|
@@ -1049,7 +1049,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1049
1049
|
]);
|
|
1050
1050
|
const activeTool = props.tools.find((t) => t.id === props.activeToolId) ?? null;
|
|
1051
1051
|
const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
|
|
1052
|
-
const { renderMeasurementStamp, onMeasurementStampPress, onMeasurementStampLongPress, selection, } = props;
|
|
1052
|
+
const { renderMeasurementStamp, onMeasurementStampPress, onMeasurementStampLongPress, resolveStampMeasurement, onMeasurementStampRemove, selection, } = props;
|
|
1053
1053
|
return (_jsxs(GestureHandlerRootView, { style: [{ width, height }, style], children: [_jsx(GestureDetector, { gesture: gesture, children: _jsx(View, { style: { width, height }, collapsable: false, children: AnnotationCanvasSkia({
|
|
1054
1054
|
width,
|
|
1055
1055
|
height,
|
|
@@ -1117,11 +1117,18 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1117
1117
|
customPreview,
|
|
1118
1118
|
}) }) }), renderMeasurementStamp && (_jsx(View, { pointerEvents: "box-none", style: StyleSheet.absoluteFill, children: state.effectiveCanvas.placedMeasurements.map((placed) => (_jsx(MeasurementStampOverlayItem, { placed: placed, measurement: placed.measurementId
|
|
1119
1119
|
? (state.measurementsById.get(placed.measurementId) ?? null)
|
|
1120
|
-
: 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.effectiveCanvas.tileScaleFactor, tileViewportScale: state.tileViewportScale, onStampPress: onMeasurementStampPress, onStampLongPress: onMeasurementStampLongPress, onRemove: () => {
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1120
|
+
: (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.effectiveCanvas.tileScaleFactor, tileViewportScale: state.tileViewportScale, onStampPress: onMeasurementStampPress, onStampLongPress: onMeasurementStampLongPress, onRemove: () => {
|
|
1121
|
+
const defaultRemove = () => {
|
|
1122
|
+
const { ops, keepSelection } = buildRemoveMeasurementOps(placed);
|
|
1123
|
+
state.ctx.commit({ ops });
|
|
1124
|
+
if (!keepSelection)
|
|
1125
|
+
state.ctx.setSelection(null);
|
|
1126
|
+
};
|
|
1127
|
+
if (onMeasurementStampRemove) {
|
|
1128
|
+
onMeasurementStampRemove(placed, defaultRemove);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
defaultRemove();
|
|
1125
1132
|
} }, placed.id))) }))] }));
|
|
1126
1133
|
};
|
|
1127
1134
|
const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, rectCtx, renderMeasurementStamp, tileScaleFactor, tileViewportScale, onStampPress, onStampLongPress, onRemove, }) => {
|
|
@@ -5,7 +5,5 @@ export declare const DEFAULT_TILE_SCALE = 1;
|
|
|
5
5
|
export declare const TILE_SCALE_MIN = 0.4;
|
|
6
6
|
export declare const TILE_SCALE_MAX = 2;
|
|
7
7
|
export declare const clampTileScale: (v: number) => number;
|
|
8
|
-
export declare const
|
|
9
|
-
export declare const
|
|
10
|
-
export declare const isUnassociatedStamp: (m: Pick<PlacedMeasurementRef, "measurementId" | "measurementPath">) => boolean;
|
|
11
|
-
export declare const stampTileSize: (m: Pick<PlacedMeasurementRef, "measurementId" | "measurementPath" | "scale">, tileScaleFactor?: number, viewportScale?: number) => number;
|
|
8
|
+
export declare const isUnassociatedStamp: (m: Pick<PlacedMeasurementRef, "measurementId" | "measurementPath" | "columnId">) => boolean;
|
|
9
|
+
export declare const stampTileSize: (m: Pick<PlacedMeasurementRef, "measurementId" | "measurementPath" | "columnId" | "scale">, tileScaleFactor?: number, viewportScale?: number) => number;
|
|
@@ -30,60 +30,40 @@ export const TILE_SCALE_MAX = 2;
|
|
|
30
30
|
// Clamp a tile-scale-factor candidate to the supported range. The single guard
|
|
31
31
|
// for the value before it lands in the document (slider input, restored docs).
|
|
32
32
|
export const clampTileScale = (v) => v < TILE_SCALE_MIN ? TILE_SCALE_MIN : v > TILE_SCALE_MAX ? TILE_SCALE_MAX : v;
|
|
33
|
-
// ---
|
|
34
|
-
// A
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// = min(canvasW, docW * canvasH / docH)
|
|
52
|
-
// This — not the raw canvas size — governs the tile's fraction of the drawing,
|
|
53
|
-
// so the same document at the same canvas aspect yields the same value on web
|
|
54
|
-
// and native. Falls back to canvasW when doc dimensions are unknown.
|
|
55
|
-
export const renderedDocWidthAtFit = (canvasW, canvasH, docW, docH) => {
|
|
56
|
-
if (!(docW > 0) || !(docH > 0) || !(canvasW > 0) || !(canvasH > 0)) {
|
|
57
|
-
return canvasW > 0 ? canvasW : TILE_VIEWPORT_REFERENCE_PX;
|
|
58
|
-
}
|
|
59
|
-
return Math.min(canvasW, (docW * canvasH) / docH);
|
|
60
|
-
};
|
|
61
|
-
// Multiplier folded into the tile footprint so tiles are a consistent fraction
|
|
62
|
-
// of the rendered drawing on any canvas. Zoom-INDEPENDENT (a function of canvas
|
|
63
|
-
// + doc dimensions only), so the tile still never changes size as the user
|
|
64
|
-
// pinches/wheels. Defaults to 1 (callers without canvas dimensions are
|
|
65
|
-
// unaffected — e.g. legacy tests).
|
|
66
|
-
export const viewportTileScale = (canvasW, canvasH, docW, docH) => {
|
|
67
|
-
const s = renderedDocWidthAtFit(canvasW, canvasH, docW, docH) /
|
|
68
|
-
TILE_VIEWPORT_REFERENCE_PX;
|
|
69
|
-
return s < TILE_VIEWPORT_SCALE_MIN
|
|
70
|
-
? TILE_VIEWPORT_SCALE_MIN
|
|
71
|
-
: s > TILE_VIEWPORT_SCALE_MAX
|
|
72
|
-
? TILE_VIEWPORT_SCALE_MAX
|
|
73
|
-
: s;
|
|
74
|
-
};
|
|
33
|
+
// --- Tile sizing is independent of canvas size ------------------------------
|
|
34
|
+
// A measurement tile's footprint is base × per-tile `scale` × the document-wide
|
|
35
|
+
// `tileScaleFactor` (below) — and NOTHING tied to the canvas's pixel size, so
|
|
36
|
+
// the same document shows the same tiles at any canvas/window size.
|
|
37
|
+
//
|
|
38
|
+
// Earlier builds multiplied the footprint by a per-canvas `viewportTileScale`
|
|
39
|
+
// derived from how large the document rendered when fit to the canvas, to keep a
|
|
40
|
+
// tile a constant fraction of the drawing across devices. But that made a tile's
|
|
41
|
+
// size change whenever the canvas was resized (e.g. dragging a desktop pane) —
|
|
42
|
+
// the same document read with different-sized tiles at different window sizes.
|
|
43
|
+
// That auto-multiplier is retired in favor of the user-driven "Tile size"
|
|
44
|
+
// slider (the document-wide `tileScaleFactor`), which is explicit, persisted,
|
|
45
|
+
// and consistent everywhere. Mobile is unchanged — its old factor was already
|
|
46
|
+
// pinned at 1, calibrated to a phone's width.
|
|
47
|
+
//
|
|
48
|
+
// `stampTileSize` keeps its `viewportScale` parameter (default 1) only so the
|
|
49
|
+
// render overlay, hit-test, and tools that thread it stay call-compatible; it is
|
|
50
|
+
// now always 1.
|
|
75
51
|
// A placed measurement is an unassociated input until a measurement reference
|
|
76
|
-
// is attached (id or path)
|
|
77
|
-
|
|
52
|
+
// is attached (id or path) OR it is bound to a form column (`columnId`). Such
|
|
53
|
+
// stamps use STAMP_INPUT_TILE_SIZE. A column-bound tile is a full editable
|
|
54
|
+
// input *card* (it shows the column's label + value editor), so it takes the
|
|
55
|
+
// full STAMP_TILE_SIZE like an associated tile — only the truly blank "+"
|
|
56
|
+
// placeholder uses the compact input size.
|
|
57
|
+
export const isUnassociatedStamp = (m) => !m.measurementId && !m.measurementPath && !m.columnId;
|
|
78
58
|
// Screen-space edge length for a placed stamp: the compact input size while
|
|
79
59
|
// unassociated, full size once a measurement is attached, then scaled by the
|
|
80
|
-
// per-stamp `scale
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
60
|
+
// per-stamp `scale` and the document-wide `tileScaleFactor`. The ONE source of
|
|
61
|
+
// truth for tile footprint — render overlay, hit-test, and slide-grab
|
|
62
|
+
// classification all call this so the drawn tile and its touch box always agree.
|
|
63
|
+
// `tileScaleFactor` is the document-wide knob (default 1, from
|
|
64
|
+
// `AnnotationCanvasState.tileScaleFactor`, driven by the "Tile size" slider).
|
|
65
|
+
// `viewportScale` is retained for call compatibility and is always 1 — the old
|
|
66
|
+
// per-canvas multiplier was retired (see the note above). Independent of zoom.
|
|
87
67
|
export const stampTileSize = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1) => (isUnassociatedStamp(m) ? STAMP_INPUT_TILE_SIZE : STAMP_TILE_SIZE) *
|
|
88
68
|
(m.scale ?? 1) *
|
|
89
69
|
tileScaleFactor *
|
|
@@ -2,8 +2,20 @@ import { stampTileSize } from '../stampLayout.js';
|
|
|
2
2
|
import { placementOf, linePosOf, snapLinePos, lerp, recomputeAnchor, rectCenter, rectCornerPoint, oppositeRectCorner, hitPlacedMeasurement, } from '../measurementGeometry.js';
|
|
3
3
|
import { hitShapeOutline } from '../shapeGeometry.js';
|
|
4
4
|
import { DEFAULT_TEXT_FONT_SIZE, resizeScaleFromDrag, textResizeGeometry, textShapeBounds, } from '../textGeometry.js';
|
|
5
|
-
import { editTextShape
|
|
5
|
+
import { editTextShape } from './textEditing.js';
|
|
6
6
|
const HIT_PADDING = 6;
|
|
7
|
+
// Whether an element id refers to a text shape (findHit reports text, lines,
|
|
8
|
+
// rects and ellipses all as kind 'shape'; only text supports tap-to-edit).
|
|
9
|
+
const isTextShape = (doc, id) => doc.shapes.some((s) => s.id === id && s.kind === 'text');
|
|
10
|
+
// Re-tap-to-edit for text. The pointer-down records a text shape's id here only
|
|
11
|
+
// when that shape was *already* selected before this gesture; a release with no
|
|
12
|
+
// movement then re-opens its editor. Held at module scope (not in ToolState)
|
|
13
|
+
// because the native tap gesture synthesizes pointer-down and pointer-up in a
|
|
14
|
+
// single React tick — a ToolState set on down isn't visible to up, but this is.
|
|
15
|
+
// Only one select gesture is ever in flight, so a shared cell is safe. A first
|
|
16
|
+
// tap (shape not yet selected) leaves this null, so it only selects; the second
|
|
17
|
+
// tap edits. Cleared on drag, release, and cancel.
|
|
18
|
+
let pendingTextEditId = null;
|
|
7
19
|
// Hit-test in doc-space. Crude but fast — good enough for v1; tools can
|
|
8
20
|
// override via `hitTest` for more precision later.
|
|
9
21
|
const hitStroke = (stroke, p) => {
|
|
@@ -389,6 +401,10 @@ export const createSelectTool = () => ({
|
|
|
389
401
|
onPointerDown(event, ctx) {
|
|
390
402
|
const { world } = event;
|
|
391
403
|
const zoom = ctx.viewport.state.zoom;
|
|
404
|
+
// Reset the re-tap-to-edit latch; only a body grab of an already-selected
|
|
405
|
+
// text shape (below) re-arms it. Grabbing a handle or empty canvas leaves
|
|
406
|
+
// it cleared, so neither can leak an edit into the next release.
|
|
407
|
+
pendingTextEditId = null;
|
|
392
408
|
// Endpoint/resize handles show only on the selected element — check first,
|
|
393
409
|
// UNLESS the grab is on that element's tile: the tile is the move/slide
|
|
394
410
|
// affordance and must win over a handle sitting under it, so a selected
|
|
@@ -451,6 +467,15 @@ export const createSelectTool = () => ({
|
|
|
451
467
|
ctx.setSelection(null);
|
|
452
468
|
return { kind: 'idle' };
|
|
453
469
|
}
|
|
470
|
+
// Re-tapping an already-selected text shape (without dragging) opens its
|
|
471
|
+
// editor on release — see onPointerUp. `selId` is the selection from before
|
|
472
|
+
// this down, so a first tap only selects; the second tap edits.
|
|
473
|
+
pendingTextEditId =
|
|
474
|
+
hit.kind === 'shape' &&
|
|
475
|
+
hit.id === selId &&
|
|
476
|
+
isTextShape(ctx.document, hit.id)
|
|
477
|
+
? hit.id
|
|
478
|
+
: null;
|
|
454
479
|
ctx.setSelection({ ids: [hit.id] });
|
|
455
480
|
const mode = hit.kind === 'measurement' &&
|
|
456
481
|
classifyGrab(ctx.document, hit.id, world, zoom, ctx.tileViewportScale) ===
|
|
@@ -474,32 +499,39 @@ export const createSelectTool = () => ({
|
|
|
474
499
|
x: event.world.x - s.start.x,
|
|
475
500
|
y: event.world.y - s.start.y,
|
|
476
501
|
};
|
|
502
|
+
// Any real movement turns this into a drag, not a re-tap — disarm the edit.
|
|
503
|
+
if (delta.x !== 0 || delta.y !== 0)
|
|
504
|
+
pendingTextEditId = null;
|
|
477
505
|
const patch = dragPatch(s, ctx.document, delta, ctx.viewport.state.zoom);
|
|
478
506
|
if (patch)
|
|
479
507
|
ctx.preview(patch);
|
|
480
508
|
return { ...s, delta };
|
|
481
509
|
},
|
|
482
510
|
onPointerUp(_event, ctx, state) {
|
|
511
|
+
const editId = pendingTextEditId;
|
|
512
|
+
pendingTextEditId = null;
|
|
483
513
|
const s = state;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
514
|
+
// A moved selection commits its drag and is never a tap-to-edit.
|
|
515
|
+
if (s?.kind === 'dragging' && (s.delta.x !== 0 || s.delta.y !== 0)) {
|
|
516
|
+
const patch = dragPatch(s, ctx.document, s.delta, ctx.viewport.state.zoom);
|
|
517
|
+
if (patch)
|
|
518
|
+
ctx.commit(patch);
|
|
487
519
|
return;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
520
|
+
}
|
|
521
|
+
// No movement: re-tapping an already-selected text shape re-opens its
|
|
522
|
+
// editor (the same edit flow as tapping it with the text tool, via the
|
|
523
|
+
// shared editTextShape). `editId` was latched on the down, so this survives
|
|
524
|
+
// the native tap's synchronous down+up where `state` cannot.
|
|
525
|
+
if (editId) {
|
|
526
|
+
const shape = ctx.document.shapes.find((sh) => sh.id === editId);
|
|
527
|
+
if (shape && shape.kind === 'text')
|
|
528
|
+
editTextShape(ctx, shape);
|
|
529
|
+
}
|
|
491
530
|
},
|
|
492
531
|
onCancel(_state, ctx) {
|
|
532
|
+
pendingTextEditId = null;
|
|
493
533
|
ctx.preview({ ops: [] });
|
|
494
534
|
},
|
|
495
|
-
// Long-pressing a placed text shape re-opens the editor (the same edit flow
|
|
496
|
-
// as tapping it with the text tool, via the shared editTextShape). A hold on
|
|
497
|
-
// any other element — or empty canvas — is ignored.
|
|
498
|
-
onLongPress(event, ctx) {
|
|
499
|
-
const shape = findTextShapeAt(ctx.document, event.world);
|
|
500
|
-
if (shape)
|
|
501
|
-
editTextShape(ctx, shape);
|
|
502
|
-
},
|
|
503
535
|
hitTest(element, p) {
|
|
504
536
|
if (element.kind === 'measurement')
|
|
505
537
|
return hitPlacedMeasurement(element, p);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { hitTestTextShape } from '../textGeometry.js';
|
|
2
2
|
// Topmost text shape under a world point (z-order, top first), or null. Shared
|
|
3
|
-
// by the text tool (tap-to-edit) and the select tool (
|
|
4
|
-
// press resolves to the same element from either tool.
|
|
3
|
+
// by the text tool (tap-to-edit) and the select tool (tap-a-selected-shape-to-
|
|
4
|
+
// edit) so a press resolves to the same element from either tool.
|
|
5
5
|
export const findTextShapeAt = (doc, world) => {
|
|
6
6
|
for (let i = doc.shapes.length - 1; i >= 0; i--) {
|
|
7
7
|
const s = doc.shapes[i];
|
|
@@ -16,6 +16,14 @@ export interface AnnotationCanvasHandle {
|
|
|
16
16
|
}): void;
|
|
17
17
|
setAnnotationType(id: AnnotationElementId, type: MeasurementPlacement): void;
|
|
18
18
|
associateMeasurement(id: AnnotationElementId, ref: MeasurementRef): void;
|
|
19
|
+
bindColumn(id: AnnotationElementId, binding: {
|
|
20
|
+
groupId: string;
|
|
21
|
+
columnId: string;
|
|
22
|
+
}): void;
|
|
23
|
+
placeColumnTileAtCenter(binding: {
|
|
24
|
+
groupId: string;
|
|
25
|
+
columnId: string;
|
|
26
|
+
}): AnnotationElementId;
|
|
19
27
|
deleteSelected(): void;
|
|
20
28
|
}
|
|
21
29
|
export interface UseAnnotationCanvasStateProps {
|
|
@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
2
2
|
import { applyPatch, invertPatch, DEFAULT_LAYER_ID, } from '../../types/annotation.js';
|
|
3
3
|
import { createViewportApi, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
|
|
4
4
|
import { recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
|
|
5
|
-
import { viewportTileScale } from './stampLayout.js';
|
|
6
5
|
// Platform-agnostic state machine for the annotation canvas. Web and native
|
|
7
6
|
// inners share this hook; each wraps it with platform-specific event
|
|
8
7
|
// capture and JSX (div + DOM events vs. GestureDetector + RN Views).
|
|
@@ -22,11 +21,13 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
22
21
|
return map;
|
|
23
22
|
}, [measurements]);
|
|
24
23
|
const viewportApi = useMemo(() => createViewportApi(viewport), [viewport]);
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// the
|
|
29
|
-
|
|
24
|
+
// Tiles are sized independently of the canvas: their footprint is base ×
|
|
25
|
+
// per-tile scale × the document-wide `tileScaleFactor` ("Tile size" slider),
|
|
26
|
+
// never the canvas pixel size — so resizing the pane no longer rescales tiles.
|
|
27
|
+
// Retained as a (constant 1) value because the overlays (drawn size) and tools
|
|
28
|
+
// (hit box) thread it; keeping them in lockstep at 1 means both still agree.
|
|
29
|
+
// (See stampLayout.ts for why the old per-canvas multiplier was retired.)
|
|
30
|
+
const tileViewportScale = 1;
|
|
30
31
|
const ctx = useMemo(() => ({
|
|
31
32
|
document: canvas,
|
|
32
33
|
selection,
|
|
@@ -317,6 +318,39 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
317
318
|
],
|
|
318
319
|
});
|
|
319
320
|
},
|
|
321
|
+
bindColumn(id, binding) {
|
|
322
|
+
const c = ctxRef.current;
|
|
323
|
+
c.commit({
|
|
324
|
+
ops: [
|
|
325
|
+
{
|
|
326
|
+
op: 'updateMeasurement',
|
|
327
|
+
id,
|
|
328
|
+
patch: { groupId: binding.groupId, columnId: binding.columnId },
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
});
|
|
332
|
+
},
|
|
333
|
+
placeColumnTileAtCenter(binding) {
|
|
334
|
+
const c = ctxRef.current;
|
|
335
|
+
const anchor = c.viewport.screenToWorld({
|
|
336
|
+
x: width / 2,
|
|
337
|
+
y: height / 2,
|
|
338
|
+
});
|
|
339
|
+
const placed = {
|
|
340
|
+
id: `measurement-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`,
|
|
341
|
+
layerId: c.document.layers[0]?.id ?? DEFAULT_LAYER_ID,
|
|
342
|
+
groupId: binding.groupId,
|
|
343
|
+
columnId: binding.columnId,
|
|
344
|
+
anchor,
|
|
345
|
+
showLabel: true,
|
|
346
|
+
showValue: true,
|
|
347
|
+
createdAt: Date.now(),
|
|
348
|
+
};
|
|
349
|
+
c.commit({ ops: [{ op: 'addMeasurement', measurement: placed }] });
|
|
350
|
+
// Select it so the consumer can immediately move it / open its editor.
|
|
351
|
+
c.setSelection({ ids: [placed.id] });
|
|
352
|
+
return placed.id;
|
|
353
|
+
},
|
|
320
354
|
deleteSelected() {
|
|
321
355
|
const c = ctxRef.current;
|
|
322
356
|
const ids = c.selection?.ids;
|