@reekon-tools/boldr-utils 1.6.18 → 1.6.20
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 +79 -12
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +100 -13
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +5 -1
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +53 -4
- package/dist/annotation/canvas/Tool.d.ts +7 -2
- package/dist/annotation/canvas/measurementGeometry.d.ts +1 -1
- package/dist/annotation/canvas/measurementGeometry.js +7 -5
- package/dist/annotation/canvas/stampLayout.d.ts +8 -2
- package/dist/annotation/canvas/stampLayout.js +72 -9
- package/dist/annotation/canvas/tools/measurementLineTool.d.ts +12 -0
- package/dist/annotation/canvas/tools/measurementLineTool.js +95 -0
- package/dist/annotation/canvas/tools/measurementTool.js +8 -2
- package/dist/annotation/canvas/tools/panTool.js +1 -1
- package/dist/annotation/canvas/tools/selectTool.js +116 -13
- package/dist/annotation/canvas/tools/textEditing.d.ts +4 -0
- package/dist/annotation/canvas/tools/textEditing.js +36 -0
- package/dist/annotation/canvas/tools/textTool.js +3 -26
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +2 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +18 -0
- package/dist/exports.d.ts +1 -0
- package/dist/exports.js +1 -0
- package/dist/types/annotation.d.ts +4 -0
- package/dist/types/annotation.js +6 -0
- package/dist/types/firestore.d.ts +53 -0
- package/dist/types/firestore.js +49 -0
- 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
|
@@ -9,6 +9,16 @@ import { stampTileSize } from './stampLayout.js';
|
|
|
9
9
|
// Screen-px radius of a measurement-annotation endpoint handle (matches the
|
|
10
10
|
// native HANDLE_RADIUS_PX). Divided by zoom for a constant on-screen size.
|
|
11
11
|
const HANDLE_PX = 7;
|
|
12
|
+
// Screen-px stroke width of the handle's colored ring (matches native
|
|
13
|
+
// HANDLE_RING_PX); the white-disc + ring keeps the knob legible on any line.
|
|
14
|
+
const HANDLE_RING_PX = 2;
|
|
15
|
+
// Press-and-hold timing/tolerance. The DOM has no native long-press, so it's
|
|
16
|
+
// derived from a hold timer: pressing without moving past the slop for this
|
|
17
|
+
// long fires the tool's onLongPress; movement or an early release cancels it.
|
|
18
|
+
// 500ms / RN's default delayLongPress so web matches native + the stamp
|
|
19
|
+
// overlay's press target below.
|
|
20
|
+
const LONG_PRESS_MS = 500;
|
|
21
|
+
const LONG_PRESS_SLOP_PX = 10;
|
|
12
22
|
const DEFAULT_PAN_TRIGGERS = ['middleMouse', 'space'];
|
|
13
23
|
export const AnnotationCanvasInner = (props) => {
|
|
14
24
|
const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, gestures, width, height, style, activeToolId, tools, } = props;
|
|
@@ -36,7 +46,19 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
36
46
|
const containerRef = useRef(null);
|
|
37
47
|
const panGestureRef = useRef(null);
|
|
38
48
|
const spaceDownRef = useRef(false);
|
|
49
|
+
// Armed hold timer + its origin, for the derived long-press (see handlers).
|
|
50
|
+
const longPressTimerRef = useRef(null);
|
|
51
|
+
const longPressRef = useRef(null);
|
|
39
52
|
const activeTool = tools.find((t) => t.id === activeToolId) ?? null;
|
|
53
|
+
const clearLongPress = useCallback(() => {
|
|
54
|
+
if (longPressTimerRef.current != null) {
|
|
55
|
+
clearTimeout(longPressTimerRef.current);
|
|
56
|
+
longPressTimerRef.current = null;
|
|
57
|
+
}
|
|
58
|
+
longPressRef.current = null;
|
|
59
|
+
}, []);
|
|
60
|
+
// Drop any armed hold timer if the canvas unmounts mid-press.
|
|
61
|
+
useEffect(() => clearLongPress, [clearLongPress]);
|
|
40
62
|
const toCanvasPointer = useCallback((event) => {
|
|
41
63
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
42
64
|
const screen = {
|
|
@@ -79,8 +101,31 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
79
101
|
return;
|
|
80
102
|
}
|
|
81
103
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
const canvasEvent = toCanvasPointer(event);
|
|
105
|
+
state.dispatchPointerDown(canvasEvent);
|
|
106
|
+
// Arm the hold timer only when the active tool acts on a long-press (the
|
|
107
|
+
// select tool, to re-open the text editor). The pointer keeps driving the
|
|
108
|
+
// tool underneath; if the hold fires before any movement past the slop,
|
|
109
|
+
// dispatchLongPress runs with the (unmoved) down point. Movement or
|
|
110
|
+
// pointer up/cancel clears it below.
|
|
111
|
+
if (activeTool?.onLongPress && event.button === 0) {
|
|
112
|
+
clearLongPress();
|
|
113
|
+
const { screen, pointerId } = canvasEvent;
|
|
114
|
+
longPressRef.current = { pointerId, start: screen };
|
|
115
|
+
longPressTimerRef.current = setTimeout(() => {
|
|
116
|
+
longPressTimerRef.current = null;
|
|
117
|
+
const lp = longPressRef.current;
|
|
118
|
+
if (!lp)
|
|
119
|
+
return;
|
|
120
|
+
longPressRef.current = null;
|
|
121
|
+
state.dispatchLongPress({
|
|
122
|
+
pointerId: lp.pointerId,
|
|
123
|
+
screen: lp.start,
|
|
124
|
+
world: state.ctx.viewport.screenToWorld(lp.start),
|
|
125
|
+
});
|
|
126
|
+
}, LONG_PRESS_MS);
|
|
127
|
+
}
|
|
128
|
+
}, [state, toCanvasPointer, isPanTriggerDown, activeTool, clearLongPress]);
|
|
84
129
|
const handlePointerMove = useCallback((event) => {
|
|
85
130
|
const pan = panGestureRef.current;
|
|
86
131
|
if (pan && event.pointerId === pan.pointerId) {
|
|
@@ -99,20 +144,33 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
99
144
|
};
|
|
100
145
|
return;
|
|
101
146
|
}
|
|
147
|
+
// Moving past the slop turns the press into a drag — cancel the hold so a
|
|
148
|
+
// drag never also fires a long-press.
|
|
149
|
+
const lp = longPressRef.current;
|
|
150
|
+
if (lp && event.pointerId === lp.pointerId) {
|
|
151
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
152
|
+
const dx = event.clientX - (rect?.left ?? 0) - lp.start.x;
|
|
153
|
+
const dy = event.clientY - (rect?.top ?? 0) - lp.start.y;
|
|
154
|
+
if (dx * dx + dy * dy > LONG_PRESS_SLOP_PX * LONG_PRESS_SLOP_PX) {
|
|
155
|
+
clearLongPress();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
102
158
|
state.dispatchPointerMove(toCanvasPointer(event));
|
|
103
|
-
}, [state, toCanvasPointer]);
|
|
159
|
+
}, [state, toCanvasPointer, clearLongPress]);
|
|
104
160
|
const handlePointerUp = useCallback((event) => {
|
|
161
|
+
clearLongPress();
|
|
105
162
|
const pan = panGestureRef.current;
|
|
106
163
|
if (pan && event.pointerId === pan.pointerId) {
|
|
107
164
|
panGestureRef.current = null;
|
|
108
165
|
return;
|
|
109
166
|
}
|
|
110
167
|
state.dispatchPointerUp(toCanvasPointer(event));
|
|
111
|
-
}, [state, toCanvasPointer]);
|
|
168
|
+
}, [state, toCanvasPointer, clearLongPress]);
|
|
112
169
|
const handlePointerCancel = useCallback(() => {
|
|
170
|
+
clearLongPress();
|
|
113
171
|
panGestureRef.current = null;
|
|
114
172
|
state.dispatchPointerCancel();
|
|
115
|
-
}, [state]);
|
|
173
|
+
}, [state, clearLongPress]);
|
|
116
174
|
const handleWheel = useCallback((event) => {
|
|
117
175
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
118
176
|
const focal = {
|
|
@@ -206,19 +264,25 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
206
264
|
handleRadius: activeTool?.dragSelection
|
|
207
265
|
? HANDLE_PX / state.viewport.zoom
|
|
208
266
|
: undefined,
|
|
267
|
+
handleRingWidth: activeTool?.dragSelection
|
|
268
|
+
? HANDLE_RING_PX / state.viewport.zoom
|
|
269
|
+
: undefined,
|
|
209
270
|
customPreview,
|
|
210
271
|
}), renderMeasurementStamp && (_jsx("div", { style: {
|
|
211
272
|
position: 'absolute',
|
|
212
273
|
inset: 0,
|
|
213
274
|
pointerEvents: 'none',
|
|
214
275
|
}, children: state.effectiveCanvas.placedMeasurements.map((placed) => {
|
|
215
|
-
const size = stampTileSize(placed);
|
|
276
|
+
const size = stampTileSize(placed, state.effectiveCanvas.tileScaleFactor, state.tileViewportScale);
|
|
216
277
|
const cx = (placed.anchor.x - state.viewport.pan.x) * state.viewport.zoom;
|
|
217
278
|
const cy = (placed.anchor.y - state.viewport.pan.y) * state.viewport.zoom;
|
|
218
279
|
const isSelected = selection?.ids.includes(placed.id) ?? false;
|
|
219
280
|
const measurement = placed.measurementId
|
|
220
281
|
? (state.measurementsById.get(placed.measurementId) ?? null)
|
|
221
282
|
: null;
|
|
283
|
+
// Corner-pinned, tile-proportional remove target (see the style
|
|
284
|
+
// comment below) so it can't blanket a small tile and eat its grab.
|
|
285
|
+
const removeSize = Math.min(40, size * 0.4);
|
|
222
286
|
return (_jsxs("div", { style: {
|
|
223
287
|
position: 'absolute',
|
|
224
288
|
left: 0,
|
|
@@ -241,11 +305,15 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
241
305
|
if (!keepSelection)
|
|
242
306
|
state.ctx.setSelection(null);
|
|
243
307
|
}, style: {
|
|
308
|
+
// Sized as a fraction of the tile and corner-pinned so it
|
|
309
|
+
// never covers the center. A fixed 40px target blanketed
|
|
310
|
+
// small tiles (the tile-scale slider shrinks them), eating
|
|
311
|
+
// the center grab so a selected tile couldn't be dragged.
|
|
244
312
|
position: 'absolute',
|
|
245
|
-
top:
|
|
246
|
-
right:
|
|
247
|
-
width:
|
|
248
|
-
height:
|
|
313
|
+
top: 0,
|
|
314
|
+
right: 0,
|
|
315
|
+
width: removeSize,
|
|
316
|
+
height: removeSize,
|
|
249
317
|
cursor: 'pointer',
|
|
250
318
|
pointerEvents: 'auto',
|
|
251
319
|
} }))] }, placed.id));
|
|
@@ -255,8 +323,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
255
323
|
// long-press, so it's derived: pointerdown arms a timer; if it fires before
|
|
256
324
|
// the pointer lifts (or leaves/cancels), the long-press callback runs and the
|
|
257
325
|
// trailing click is swallowed. Mirrors the native overlay's TouchableOpacity
|
|
258
|
-
// onPress/onLongPress semantics (
|
|
259
|
-
const LONG_PRESS_MS = 500;
|
|
326
|
+
// onPress/onLongPress semantics (LONG_PRESS_MS, RN's default delayLongPress).
|
|
260
327
|
const StampPressTarget = ({ onPress, onLongPress, }) => {
|
|
261
328
|
const timerRef = useRef(null);
|
|
262
329
|
const longPressFiredRef = useRef(false);
|
|
@@ -12,9 +12,18 @@ import { buildShapeFromDrag } from './tools/shapeTool.js';
|
|
|
12
12
|
import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
|
|
13
13
|
let strokeCounter = 0;
|
|
14
14
|
const makeStrokeId = () => `stroke-${Date.now().toString(36)}-${(strokeCounter++).toString(36)}`;
|
|
15
|
+
// Press-and-hold timing/tolerance for the long-press gesture (matches the
|
|
16
|
+
// measurement-stamp overlay's TouchableOpacity feel — 500ms / RN default).
|
|
17
|
+
// Holding still this long without moving past the slop fires onLongPress;
|
|
18
|
+
// moving first lets the drag win instead.
|
|
19
|
+
const LONG_PRESS_MS = 500;
|
|
20
|
+
const LONG_PRESS_SLOP_PX = 10;
|
|
15
21
|
// Screen-px radius of a measurement-annotation endpoint handle dot. Divided by
|
|
16
22
|
// the live zoom so the handle is a constant on-screen size.
|
|
17
23
|
const HANDLE_RADIUS_PX = 7;
|
|
24
|
+
// Screen-px stroke width of the handle's colored ring (white-disc + ring, so the
|
|
25
|
+
// knob stays legible over a line of any color). Also zoom-divided.
|
|
26
|
+
const HANDLE_RING_PX = 2;
|
|
18
27
|
// Native fingerprint: one finger drives the active tool, two fingers
|
|
19
28
|
// pan/zoom the viewport. Tap counts as a brief pointer down+up so tools
|
|
20
29
|
// like measurement-stamp (which only listen to onPointerUp) work via tap.
|
|
@@ -152,6 +161,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
152
161
|
const shapeDraw = state.activeTool?.shapeDraw ?? null;
|
|
153
162
|
const panViewport = !!state.activeTool?.panViewport;
|
|
154
163
|
const dragSelection = state.activeTool?.dragSelection ?? null;
|
|
164
|
+
const longPressEnabled = !!state.activeTool?.onLongPress;
|
|
155
165
|
// In-flight shape rubber-band (line/arrow/rect/triangle/circle tools),
|
|
156
166
|
// owned by the UI thread — the shape twin of `livePoints`. The drag worklet
|
|
157
167
|
// tracks the start/current world points; derived paths below render the
|
|
@@ -315,6 +325,13 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
315
325
|
const [epDragId, setEpDragId] = useState(null);
|
|
316
326
|
const epCtx = useSharedValue({ ax: 0, ay: 0, bx: 0, by: 0, t0: 0.5, handle: 0 });
|
|
317
327
|
const epTargetRef = useRef(null);
|
|
328
|
+
// Endpoint drag of a line/arrow SHAPE (vs. a measurement line above).
|
|
329
|
+
// `shapeEpDragId` (React state) tells the Skia layer to render that shape's
|
|
330
|
+
// line from the live endpoints; the live geometry rides the SAME `epCtx` +
|
|
331
|
+
// `liveLineP1`/`liveLineP2` shared values (only one endpoint drag is ever in
|
|
332
|
+
// flight). A separate target ref keys the commit to buildShapeEndpointPatch.
|
|
333
|
+
const [shapeEpDragId, setShapeEpDragId] = useState(null);
|
|
334
|
+
const shapeEpTargetRef = useRef(null);
|
|
318
335
|
// Live line endpoints during an endpoint drag: the grabbed handle follows the
|
|
319
336
|
// finger (dragX/dragY); the other stays put. Fed to the Skia <Line> for the
|
|
320
337
|
// dragged annotation.
|
|
@@ -338,6 +355,10 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
338
355
|
'worklet';
|
|
339
356
|
return HANDLE_RADIUS_PX / zoom.value;
|
|
340
357
|
});
|
|
358
|
+
const handleRingWidth = useDerivedValue(() => {
|
|
359
|
+
'worklet';
|
|
360
|
+
return HANDLE_RING_PX / zoom.value;
|
|
361
|
+
});
|
|
341
362
|
// Rectangle-annotation corner drag. `rectDragId` (React state) marks which
|
|
342
363
|
// annotation's rect renders from the live geometry; `rectCtx` carries the
|
|
343
364
|
// fixed (opposite) corner and the grabbed corner's start position so the
|
|
@@ -465,6 +486,20 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
465
486
|
inFlightRef.current = null;
|
|
466
487
|
}
|
|
467
488
|
});
|
|
489
|
+
// Press-and-hold (one finger, no movement) → dispatch a long-press to the
|
|
490
|
+
// active tool (the select tool re-opens the text editor for a placed text
|
|
491
|
+
// shape). Races against the one-finger drag: holding still activates this
|
|
492
|
+
// at LONG_PRESS_MS, moving first activates the drag/pan instead (this one's
|
|
493
|
+
// maxDistance cancels it). A quick tap is shorter than the hold, so the tap
|
|
494
|
+
// gesture still owns selection.
|
|
495
|
+
const longPress = Gesture.LongPress()
|
|
496
|
+
.minDuration(LONG_PRESS_MS)
|
|
497
|
+
.maxDistance(LONG_PRESS_SLOP_PX)
|
|
498
|
+
.runOnJS(true)
|
|
499
|
+
.onStart((e) => {
|
|
500
|
+
const id = pointerIdRef.current++;
|
|
501
|
+
stateRef.current.dispatchLongPress(buildEvent(id, { x: e.x, y: e.y }));
|
|
502
|
+
});
|
|
468
503
|
const tap = Gesture.Tap()
|
|
469
504
|
.maxDuration(250)
|
|
470
505
|
.runOnJS(true)
|
|
@@ -738,9 +773,13 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
738
773
|
const world = st.ctx.viewport.screenToWorld(screen);
|
|
739
774
|
const zoomNow = st.ctx.viewport.state.zoom;
|
|
740
775
|
// Endpoint handles show only on the selected annotation, so check the
|
|
741
|
-
// current selection's handles before the general hit-test
|
|
776
|
+
// current selection's handles before the general hit-test — UNLESS the
|
|
777
|
+
// grab is on that element's tile, which must stay draggable (the tile
|
|
778
|
+
// is the move/slide affordance and wins over a handle sitting under it,
|
|
779
|
+
// so a selected tile isn't stuck until you deselect it).
|
|
742
780
|
const selId = st.ctx.selection?.ids[0];
|
|
743
|
-
if (selId
|
|
781
|
+
if (selId &&
|
|
782
|
+
!cfg.isSelectedTileGrab?.(st.ctx.document, selId, world, zoomNow, st.ctx.tileViewportScale)) {
|
|
744
783
|
const handle = cfg.hitTestHandle?.(st.ctx.document, selId, world, zoomNow);
|
|
745
784
|
if (handle) {
|
|
746
785
|
const m = st.ctx.document.placedMeasurements.find((x) => x.id === selId);
|
|
@@ -758,6 +797,27 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
758
797
|
return;
|
|
759
798
|
}
|
|
760
799
|
}
|
|
800
|
+
// Endpoint handle of a line/arrow shape — selected-element-only, and
|
|
801
|
+
// distinct from the measurement-line handle above (different storage).
|
|
802
|
+
const shapeHandle = cfg.hitTestShapeHandle?.(st.ctx.document, selId, world, zoomNow);
|
|
803
|
+
if (shapeHandle) {
|
|
804
|
+
const s = st.ctx.document.shapes.find((x) => x.id === selId);
|
|
805
|
+
const a = s?.geometry.points[0];
|
|
806
|
+
const b = s?.geometry.points[1];
|
|
807
|
+
if (a && b) {
|
|
808
|
+
epCtx.value = {
|
|
809
|
+
ax: a.x,
|
|
810
|
+
ay: a.y,
|
|
811
|
+
bx: b.x,
|
|
812
|
+
by: b.y,
|
|
813
|
+
t0: 0.5,
|
|
814
|
+
handle: shapeHandle === 'a' ? 0 : 1,
|
|
815
|
+
};
|
|
816
|
+
shapeEpTargetRef.current = { id: selId, handle: shapeHandle };
|
|
817
|
+
setShapeEpDragId(selId);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
761
821
|
// Corner resize handle (text shapes) — also selected-element-only.
|
|
762
822
|
const resizeGeom = cfg.hitTestResizeHandle?.(st.ctx.document, selId, world, zoomNow);
|
|
763
823
|
if (resizeGeom) {
|
|
@@ -787,7 +847,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
787
847
|
return;
|
|
788
848
|
}
|
|
789
849
|
}
|
|
790
|
-
const hit = cfg.hitTest(st.ctx.document, world, zoomNow);
|
|
850
|
+
const hit = cfg.hitTest(st.ctx.document, world, zoomNow, st.ctx.tileViewportScale);
|
|
791
851
|
if (!hit) {
|
|
792
852
|
st.ctx.setSelection(null);
|
|
793
853
|
dragTargetRef.current = null;
|
|
@@ -796,7 +856,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
796
856
|
st.ctx.setSelection({ ids: [hit.id] });
|
|
797
857
|
// Grabbing a line annotation's tile slides it; otherwise group-move.
|
|
798
858
|
const grab = hit.kind === 'measurement'
|
|
799
|
-
? cfg.classifyMeasurementGrab?.(st.ctx.document, hit.id, world, zoomNow)
|
|
859
|
+
? cfg.classifyMeasurementGrab?.(st.ctx.document, hit.id, world, zoomNow, st.ctx.tileViewportScale)
|
|
800
860
|
: 'move';
|
|
801
861
|
if (grab === 'slide') {
|
|
802
862
|
const m = st.ctx.document.placedMeasurements.find((x) => x.id === hit.id);
|
|
@@ -859,6 +919,18 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
859
919
|
setEpDragId(null);
|
|
860
920
|
return;
|
|
861
921
|
}
|
|
922
|
+
// Shape endpoint commit: move the grabbed endpoint by the world delta.
|
|
923
|
+
const sepT = shapeEpTargetRef.current;
|
|
924
|
+
if (sepT) {
|
|
925
|
+
if (dx !== 0 || dy !== 0) {
|
|
926
|
+
const patch = cfg.buildShapeEndpointPatch?.(st.ctx.document, sepT.id, sepT.handle, { x: dx, y: dy });
|
|
927
|
+
if (patch)
|
|
928
|
+
st.ctx.commit(patch);
|
|
929
|
+
}
|
|
930
|
+
shapeEpTargetRef.current = null;
|
|
931
|
+
setShapeEpDragId(null);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
862
934
|
// Slide commit: map the final world delta to a clamped/snapped linePos.
|
|
863
935
|
const slideT = slideTargetRef.current;
|
|
864
936
|
if (slideT) {
|
|
@@ -891,11 +963,13 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
891
963
|
dragTargetRef.current = null;
|
|
892
964
|
slideTargetRef.current = null;
|
|
893
965
|
epTargetRef.current = null;
|
|
966
|
+
shapeEpTargetRef.current = null;
|
|
894
967
|
resizeTargetRef.current = null;
|
|
895
968
|
rectTargetRef.current = null;
|
|
896
969
|
setDraggingId(null);
|
|
897
970
|
setSlidingId(null);
|
|
898
971
|
setEpDragId(null);
|
|
972
|
+
setShapeEpDragId(null);
|
|
899
973
|
setResizingId(null);
|
|
900
974
|
setRectDragId(null);
|
|
901
975
|
};
|
|
@@ -947,7 +1021,9 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
947
1021
|
: dragSelection
|
|
948
1022
|
? buildSelectDragPan(dragSelection)
|
|
949
1023
|
: toolPan;
|
|
950
|
-
|
|
1024
|
+
// Long-press only joins the race when the active tool acts on it, so it
|
|
1025
|
+
// never pre-empts a one-finger drag/draw on tools that ignore holds.
|
|
1026
|
+
return Gesture.Race(tap, ...(longPressEnabled ? [longPress] : []), Gesture.Simultaneous(viewportPan, pinch), oneFinger);
|
|
951
1027
|
}, [
|
|
952
1028
|
zoom,
|
|
953
1029
|
panX,
|
|
@@ -962,6 +1038,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
962
1038
|
shapeDraw,
|
|
963
1039
|
panViewport,
|
|
964
1040
|
dragSelection,
|
|
1041
|
+
longPressEnabled,
|
|
965
1042
|
dragX,
|
|
966
1043
|
dragY,
|
|
967
1044
|
dragEnded,
|
|
@@ -1018,6 +1095,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1018
1095
|
resizeTransform,
|
|
1019
1096
|
selectedId: selection?.ids[0] ?? null,
|
|
1020
1097
|
endpointDragId: epDragId,
|
|
1098
|
+
shapeEndpointDragId: shapeEpDragId,
|
|
1021
1099
|
liveLineP1,
|
|
1022
1100
|
liveLineP2,
|
|
1023
1101
|
rectDragId,
|
|
@@ -1033,19 +1111,28 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1033
1111
|
// measurement still shows its tile chrome but must not advertise
|
|
1034
1112
|
// draggability).
|
|
1035
1113
|
handleRadius: activeTool?.dragSelection ? handleRadius : undefined,
|
|
1114
|
+
handleRingWidth: activeTool?.dragSelection
|
|
1115
|
+
? handleRingWidth
|
|
1116
|
+
: undefined,
|
|
1036
1117
|
customPreview,
|
|
1037
1118
|
}) }) }), renderMeasurementStamp && (_jsx(View, { pointerEvents: "box-none", style: StyleSheet.absoluteFill, children: state.effectiveCanvas.placedMeasurements.map((placed) => (_jsx(MeasurementStampOverlayItem, { placed: placed, measurement: placed.measurementId
|
|
1038
1119
|
? (state.measurementsById.get(placed.measurementId) ?? null)
|
|
1039
|
-
: 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, onStampPress: onMeasurementStampPress, onStampLongPress: onMeasurementStampLongPress, onRemove: () => {
|
|
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: () => {
|
|
1040
1121
|
const { ops, keepSelection } = buildRemoveMeasurementOps(placed);
|
|
1041
1122
|
state.ctx.commit({ ops });
|
|
1042
1123
|
if (!keepSelection)
|
|
1043
1124
|
state.ctx.setSelection(null);
|
|
1044
1125
|
} }, placed.id))) }))] }));
|
|
1045
1126
|
};
|
|
1046
|
-
const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, rectCtx, renderMeasurementStamp, onStampPress, onStampLongPress, onRemove, }) => {
|
|
1047
|
-
const size = stampTileSize(placed);
|
|
1127
|
+
const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, rectCtx, renderMeasurementStamp, tileScaleFactor, tileViewportScale, onStampPress, onStampLongPress, onRemove, }) => {
|
|
1128
|
+
const size = stampTileSize(placed, tileScaleFactor, tileViewportScale);
|
|
1048
1129
|
const half = size / 2;
|
|
1130
|
+
// Remove-"X" touch target, sized as a fraction of the tile and corner-pinned
|
|
1131
|
+
// so it never reaches the center. A FIXED 36px+hitSlop target blanketed small
|
|
1132
|
+
// tiles (the tile-scale slider can shrink them to ~38px), swallowing the
|
|
1133
|
+
// center grab and making a selected tile impossible to drag (it had to be
|
|
1134
|
+
// deselected first). Capped so it doesn't grow past the old size on big tiles.
|
|
1135
|
+
const removeSize = Math.min(36, size * 0.4);
|
|
1049
1136
|
const anchorX = placed.anchor.x;
|
|
1050
1137
|
const anchorY = placed.anchor.y;
|
|
1051
1138
|
// doc → screen each frame, on the UI thread. Position is a translate
|
|
@@ -1113,11 +1200,11 @@ const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging,
|
|
|
1113
1200
|
selected,
|
|
1114
1201
|
size,
|
|
1115
1202
|
zoom: zoomSnapshot,
|
|
1116
|
-
}) }), 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",
|
|
1203
|
+
}) }), 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: {
|
|
1117
1204
|
position: 'absolute',
|
|
1118
|
-
top:
|
|
1119
|
-
right:
|
|
1120
|
-
width:
|
|
1121
|
-
height:
|
|
1205
|
+
top: 0,
|
|
1206
|
+
right: 0,
|
|
1207
|
+
width: removeSize,
|
|
1208
|
+
height: removeSize,
|
|
1122
1209
|
} }))] }));
|
|
1123
1210
|
};
|
|
@@ -59,6 +59,7 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
59
59
|
};
|
|
60
60
|
selectedId?: string | null;
|
|
61
61
|
endpointDragId?: string | null;
|
|
62
|
+
shapeEndpointDragId?: string | null;
|
|
62
63
|
liveLineP1?: AnimatedPoint;
|
|
63
64
|
liveLineP2?: AnimatedPoint;
|
|
64
65
|
rectDragId?: string | null;
|
|
@@ -71,7 +72,10 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
71
72
|
handleRadius?: number | {
|
|
72
73
|
value: number;
|
|
73
74
|
};
|
|
75
|
+
handleRingWidth?: number | {
|
|
76
|
+
value: number;
|
|
77
|
+
};
|
|
74
78
|
customPreview?: ReactNode;
|
|
75
79
|
}
|
|
76
|
-
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, resolveImageUrl, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, handleRadius, customPreview, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
80
|
+
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, resolveImageUrl, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, handleRadius, handleRingWidth, customPreview, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
77
81
|
export {};
|
|
@@ -22,6 +22,12 @@ const MEASUREMENT_HANDLE_COLOR = '#0066FF';
|
|
|
22
22
|
// hit-test the text resize handle, which sits on the padded box corner.
|
|
23
23
|
const SELECTION_COLOR = '#0066FF';
|
|
24
24
|
const SELECTION_STROKE = 1.5;
|
|
25
|
+
// A draggable selection handle (line endpoints, rect corners, text resize):
|
|
26
|
+
// a white disc with a colored ring, so it reads as a distinct, grabbable knob
|
|
27
|
+
// over a line of ANY color — including the default measurement-line blue, which
|
|
28
|
+
// a solid-colored handle would camouflage into. `c`/`r`/`ringWidth` may be
|
|
29
|
+
// Reanimated values (native live-drag) or plain numbers (web).
|
|
30
|
+
const Handle = ({ c, r, ringWidth, color = MEASUREMENT_HANDLE_COLOR, }) => (_jsxs(_Fragment, { children: [_jsx(Circle, { c: c, r: r, color: "#FFFFFF" }), _jsx(Circle, { c: c, r: r, color: color, style: "stroke", strokeWidth: ringWidth })] }));
|
|
25
31
|
// Bounds of a flat [x,y,x,y,…] stroke point array, or null if empty.
|
|
26
32
|
const strokeBounds = (points) => {
|
|
27
33
|
if (points.length < 2)
|
|
@@ -86,7 +92,46 @@ const SelectionBox = ({ bounds, isDragging, transform, }) => (_jsx(DraggableElem
|
|
|
86
92
|
// since the function-call pattern works identically on native we use it
|
|
87
93
|
// in both Inners for consistency. Don't add hooks here; this is a plain
|
|
88
94
|
// JSX-returning helper, not a component.
|
|
89
|
-
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, resolveImageUrl, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, handleRadius, customPreview, }) => (_jsx(Canvas, { style: { width, height }, children: _jsxs(Group, { transform: worldTransform, children: [effectiveCanvas.viewport.backgroundImage && (_jsx(BackgroundImageElement, { image: effectiveCanvas.viewport.backgroundImage, docWidth: effectiveCanvas.viewport.width, docHeight: effectiveCanvas.viewport.height, fit: effectiveCanvas.viewport.backgroundFit ?? 'contain', resolveUrl: resolveImageUrl })), effectiveCanvas.strokes.map((stroke) => (_jsx(DraggableElement, { isDragging: stroke.id === draggingId, transform: dragTransform, children: _jsx(StrokeElement, { stroke: stroke }) }, stroke.id))), effectiveCanvas.shapes.map((shape) =>
|
|
95
|
+
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, resolveImageUrl, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, handleRadius, handleRingWidth, customPreview, }) => (_jsx(Canvas, { style: { width, height }, children: _jsxs(Group, { transform: worldTransform, children: [effectiveCanvas.viewport.backgroundImage && (_jsx(BackgroundImageElement, { image: effectiveCanvas.viewport.backgroundImage, docWidth: effectiveCanvas.viewport.width, docHeight: effectiveCanvas.viewport.height, fit: effectiveCanvas.viewport.backgroundFit ?? 'contain', resolveUrl: resolveImageUrl })), effectiveCanvas.strokes.map((stroke) => (_jsx(DraggableElement, { isDragging: stroke.id === draggingId, transform: dragTransform, children: _jsx(StrokeElement, { stroke: stroke }) }, stroke.id))), effectiveCanvas.shapes.map((shape) => {
|
|
96
|
+
// Line/arrow shapes support endpoint editing. When selected they show
|
|
97
|
+
// grab handles at both ends; during an endpoint drag the line renders
|
|
98
|
+
// from the live endpoints (one following the finger) — the shape twin
|
|
99
|
+
// of the measurement-line endpoint drag below. Other shapes, and
|
|
100
|
+
// unselected lines, fall through to the plain ShapeElement render so
|
|
101
|
+
// their memoization is preserved.
|
|
102
|
+
if (shape.kind === 'line' || shape.kind === 'arrow') {
|
|
103
|
+
const isEndpointDrag = shape.id === shapeEndpointDragId;
|
|
104
|
+
const isSelected = shape.id === selectedId;
|
|
105
|
+
const [a, b] = shape.geometry.points;
|
|
106
|
+
if ((isEndpointDrag || isSelected) && a && b) {
|
|
107
|
+
const stroke = shape.style.stroke ?? '#000000';
|
|
108
|
+
const strokeWidth = shape.style.strokeWidth ?? 2;
|
|
109
|
+
const hasArrow = shape.kind === 'arrow' || shape.style.cap === 'arrow';
|
|
110
|
+
const handles = handleRadius != null ? (_jsxs(_Fragment, { children: [_jsx(Handle, { c: isEndpointDrag && liveLineP1 ? liveLineP1 : a, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR }), _jsx(Handle, { c: isEndpointDrag && liveLineP2 ? liveLineP2 : b, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR })] })) : null;
|
|
111
|
+
// Endpoint drag: render the body live from the moving endpoints
|
|
112
|
+
// (outside any group translate), suppressing the static arrowhead
|
|
113
|
+
// so it doesn't lag the finger — it reappears on commit. Selected
|
|
114
|
+
// but idle: reuse ShapeElement for the body (caps/arrowhead/dash)
|
|
115
|
+
// and overlay the handles, both wrapped so a group move tracks.
|
|
116
|
+
if (isEndpointDrag) {
|
|
117
|
+
const arrowPath = (() => {
|
|
118
|
+
if (!hasArrow)
|
|
119
|
+
return null;
|
|
120
|
+
const [apex, baseL, baseR] = arrowheadTriangle(b, a, strokeWidth);
|
|
121
|
+
const p = Skia.Path.Make();
|
|
122
|
+
p.moveTo(apex.x, apex.y);
|
|
123
|
+
p.lineTo(baseL.x, baseL.y);
|
|
124
|
+
p.lineTo(baseR.x, baseR.y);
|
|
125
|
+
p.close();
|
|
126
|
+
return p;
|
|
127
|
+
})();
|
|
128
|
+
return (_jsxs(Group, { children: [_jsx(Line, { p1: liveLineP1 ?? a, p2: liveLineP2 ?? b, color: stroke, style: "stroke", strokeWidth: strokeWidth, strokeCap: toSkiaStrokeCap(shape.style.cap), children: shape.style.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(strokeWidth) })) }), handles, arrowPath && (_jsx(Path, { path: arrowPath, color: stroke, style: "fill" }))] }, shape.id));
|
|
129
|
+
}
|
|
130
|
+
return (_jsxs(DraggableElement, { isDragging: shape.id === draggingId, transform: dragTransform, children: [_jsx(ShapeElement, { shape: shape, font: valueFont, textFontMgr: textFontMgr }), handles] }, shape.id));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return (_jsx(DraggableElement, { isDragging: shape.id === draggingId || shape.id === resizingId, transform: shape.id === resizingId ? resizeTransform : dragTransform, children: _jsx(ShapeElement, { shape: shape, font: valueFont, textFontMgr: textFontMgr }) }, shape.id));
|
|
134
|
+
}), effectiveCanvas.placedMeasurements.map((placed) => {
|
|
90
135
|
// Rectangle annotation: a stroked border whose center carries the
|
|
91
136
|
// tile. A corner drag renders from the live geometry (outside the
|
|
92
137
|
// group translate, like an endpoint drag); otherwise the committed
|
|
@@ -100,7 +145,7 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
|
|
|
100
145
|
return (_jsx(Rect, { x: liveRect.x, y: liveRect.y, width: liveRect.width, height: liveRect.height, color: rectColor, style: "stroke", strokeWidth: rectWidth, children: placed.lineDash && (_jsx(DashPathEffect, { intervals: dashIntervals(rectWidth) })) }, placed.id));
|
|
101
146
|
}
|
|
102
147
|
const n = normalizeRect(placed.rect);
|
|
103
|
-
return (_jsxs(DraggableElement, { isDragging: placed.id === draggingId, transform: dragTransform, children: [_jsx(Rect, { x: n.minX, y: n.minY, width: n.maxX - n.minX, height: n.maxY - n.minY, color: rectColor, style: "stroke", strokeWidth: rectWidth, children: placed.lineDash && (_jsx(DashPathEffect, { intervals: dashIntervals(rectWidth) })) }), isSelected && handleRadius != null && (_jsxs(_Fragment, { children: [_jsx(
|
|
148
|
+
return (_jsxs(DraggableElement, { isDragging: placed.id === draggingId, transform: dragTransform, children: [_jsx(Rect, { x: n.minX, y: n.minY, width: n.maxX - n.minX, height: n.maxY - n.minY, color: rectColor, style: "stroke", strokeWidth: rectWidth, children: placed.lineDash && (_jsx(DashPathEffect, { intervals: dashIntervals(rectWidth) })) }), isSelected && handleRadius != null && (_jsxs(_Fragment, { children: [_jsx(Handle, { c: { x: n.minX, y: n.minY }, r: handleRadius, ringWidth: handleRingWidth }), _jsx(Handle, { c: { x: n.maxX, y: n.minY }, r: handleRadius, ringWidth: handleRingWidth }), _jsx(Handle, { c: { x: n.minX, y: n.maxY }, r: handleRadius, ringWidth: handleRingWidth }), _jsx(Handle, { c: { x: n.maxX, y: n.maxY }, r: handleRadius, ringWidth: handleRingWidth })] }))] }, placed.id));
|
|
104
149
|
}
|
|
105
150
|
if (placementOf(placed) !== 'line' || !placed.line)
|
|
106
151
|
return null;
|
|
@@ -126,7 +171,7 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
|
|
|
126
171
|
p.close();
|
|
127
172
|
return p;
|
|
128
173
|
})();
|
|
129
|
-
const content = (_jsxs(_Fragment, { children: [_jsx(Line, { p1: p1, p2: p2, color: lineColor, style: "stroke", strokeWidth: lineWidth, strokeCap: toSkiaStrokeCap(placed.lineCap), children: placed.lineDash && (_jsx(DashPathEffect, { intervals: dashIntervals(lineWidth) })) }), arrowPath && (_jsx(Path, { path: arrowPath, color: lineColor, style: "fill" })), isSelected && handleRadius != null && (_jsxs(_Fragment, { children: [_jsx(
|
|
174
|
+
const content = (_jsxs(_Fragment, { children: [_jsx(Line, { p1: p1, p2: p2, color: lineColor, style: "stroke", strokeWidth: lineWidth, strokeCap: toSkiaStrokeCap(placed.lineCap), children: placed.lineDash && (_jsx(DashPathEffect, { intervals: dashIntervals(lineWidth) })) }), arrowPath && (_jsx(Path, { path: arrowPath, color: lineColor, style: "fill" })), isSelected && handleRadius != null && (_jsxs(_Fragment, { children: [_jsx(Handle, { c: p1, r: handleRadius, ringWidth: handleRingWidth }), _jsx(Handle, { c: p2, r: handleRadius, ringWidth: handleRingWidth })] }))] }));
|
|
130
175
|
return isEndpointDrag ? (_jsx(Group, { children: content }, placed.id)) : (_jsx(DraggableElement, { isDragging: placed.id === draggingId, transform: dragTransform, children: content }, placed.id));
|
|
131
176
|
}), (() => {
|
|
132
177
|
if (!selectedId)
|
|
@@ -139,6 +184,10 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
|
|
|
139
184
|
}
|
|
140
185
|
const shape = effectiveCanvas.shapes.find((s) => s.id === selectedId);
|
|
141
186
|
if (shape) {
|
|
187
|
+
// Line/arrow shapes show endpoint handles (drawn with the shape
|
|
188
|
+
// above), not a bounding box — matching the measurement-line UX.
|
|
189
|
+
if (shape.kind === 'line' || shape.kind === 'arrow')
|
|
190
|
+
return null;
|
|
142
191
|
// Text shapes derive their box from the estimated text bounds (the
|
|
143
192
|
// stored geometry is just the top-left anchor) and add a corner
|
|
144
193
|
// resize handle; both track the live resize transform so the chrome
|
|
@@ -152,7 +201,7 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
|
|
|
152
201
|
const isResizing = shape.id === resizingId;
|
|
153
202
|
const liveTransform = isResizing ? resizeTransform : dragTransform;
|
|
154
203
|
const resizeGeom = isText ? textResizeGeometry(shape) : null;
|
|
155
|
-
return (_jsxs(_Fragment, { children: [_jsx(SelectionBox, { bounds: b, isDragging: isDragging || isResizing, transform: liveTransform }), resizeGeom && handleRadius != null && (_jsx(DraggableElement, { isDragging: isDragging || isResizing, transform: liveTransform, children: _jsx(
|
|
204
|
+
return (_jsxs(_Fragment, { children: [_jsx(SelectionBox, { bounds: b, isDragging: isDragging || isResizing, transform: liveTransform }), resizeGeom && handleRadius != null && (_jsx(DraggableElement, { isDragging: isDragging || isResizing, transform: liveTransform, children: _jsx(Handle, { c: resizeGeom.handle, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR }) }))] }));
|
|
156
205
|
}
|
|
157
206
|
return null;
|
|
158
207
|
})(), penDrawingStroke && _jsx(StrokeElement, { stroke: penDrawingStroke }), shapePreview && (_jsxs(_Fragment, { children: [_jsx(Path, { path: shapePreview.path, color: shapePreview.color, style: "stroke", strokeWidth: shapePreview.width, strokeCap: toSkiaStrokeCap(shapePreview.cap), strokeJoin: "round", children: shapePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(shapePreview.width) })) }), _jsx(Path, { path: shapePreview.headPath, color: shapePreview.color, style: "fill" })] })), livePreview?.handoffPaths?.map((p, i) => (_jsx(Path, { path: p, color: livePreview.color, style: "stroke", strokeWidth: livePreview.width, strokeCap: toSkiaStrokeCap(livePreview.cap), strokeJoin: "round", opacity: livePreview.opacity, children: livePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(livePreview.width) })) }, i))), livePreview && (_jsx(Path, { path: livePreview.path, color: livePreview.color, style: "stroke", strokeWidth: livePreview.width, strokeCap: toSkiaStrokeCap(livePreview.cap), strokeJoin: "round", opacity: livePreview.opacity, children: livePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(livePreview.width) })) })), customPreview] }) }));
|
|
@@ -22,6 +22,7 @@ export interface ToolContext {
|
|
|
22
22
|
document: AnnotationCanvasState;
|
|
23
23
|
selection: Selection | null;
|
|
24
24
|
viewport: ViewportApi;
|
|
25
|
+
tileViewportScale: number;
|
|
25
26
|
preview(patch: AnnotationDocumentPatch): void;
|
|
26
27
|
commit(patch: AnnotationDocumentPatch): void;
|
|
27
28
|
setSelection(selection: Selection | null): void;
|
|
@@ -50,21 +51,24 @@ export interface ShapeDrawConfig {
|
|
|
50
51
|
}
|
|
51
52
|
export type DragElementKind = 'stroke' | 'shape' | 'measurement';
|
|
52
53
|
export interface DragSelectionConfig {
|
|
53
|
-
hitTest(doc: AnnotationCanvasState, world: Vec2, zoom: number): {
|
|
54
|
+
hitTest(doc: AnnotationCanvasState, world: Vec2, zoom: number, viewportTileScale?: number): {
|
|
54
55
|
id: AnnotationElementId;
|
|
55
56
|
kind: DragElementKind;
|
|
56
57
|
} | null;
|
|
57
58
|
buildTranslatePatch(doc: AnnotationCanvasState, id: AnnotationElementId, kind: DragElementKind, delta: Vec2): AnnotationDocumentPatch | null;
|
|
58
|
-
classifyMeasurementGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): 'slide' | 'move';
|
|
59
|
+
classifyMeasurementGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number): 'slide' | 'move';
|
|
59
60
|
buildSlidePatch?(doc: AnnotationCanvasState, id: AnnotationElementId, delta: Vec2, zoom: number): AnnotationDocumentPatch | null;
|
|
60
61
|
hitTestHandle?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): 'a' | 'b' | null;
|
|
61
62
|
buildEndpointPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, handle: 'a' | 'b', delta: Vec2): AnnotationDocumentPatch | null;
|
|
63
|
+
hitTestShapeHandle?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): 'a' | 'b' | null;
|
|
64
|
+
buildShapeEndpointPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, handle: 'a' | 'b', delta: Vec2): AnnotationDocumentPatch | null;
|
|
62
65
|
hitTestRectCorner?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): {
|
|
63
66
|
corner: RectCorner;
|
|
64
67
|
moving: Vec2;
|
|
65
68
|
fixed: Vec2;
|
|
66
69
|
} | null;
|
|
67
70
|
buildRectCornerPatch?(doc: AnnotationCanvasState, id: AnnotationElementId, corner: RectCorner, delta: Vec2): AnnotationDocumentPatch | null;
|
|
71
|
+
isSelectedTileGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number): boolean;
|
|
68
72
|
hitTestResizeHandle?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): ResizeGeometry | null;
|
|
69
73
|
buildResizePatch?(doc: AnnotationCanvasState, id: AnnotationElementId, delta: Vec2): AnnotationDocumentPatch | null;
|
|
70
74
|
}
|
|
@@ -80,6 +84,7 @@ export interface Tool {
|
|
|
80
84
|
onPointerDown?(event: CanvasPointerEvent, ctx: ToolContext, state: ToolState): ToolState | void;
|
|
81
85
|
onPointerMove?(event: CanvasPointerEvent, ctx: ToolContext, state: ToolState): ToolState | void;
|
|
82
86
|
onPointerUp?(event: CanvasPointerEvent, ctx: ToolContext, state: ToolState): ToolState | void;
|
|
87
|
+
onLongPress?(event: CanvasPointerEvent, ctx: ToolContext): void;
|
|
83
88
|
onCancel?(state: ToolState, ctx: ToolContext): void;
|
|
84
89
|
onDeactivate?(ctx: ToolContext): void;
|
|
85
90
|
renderPreview?(state: ToolState, ctx: ToolContext): ReactNode;
|
|
@@ -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) => boolean;
|
|
39
|
+
export declare const hitPlacedMeasurement: (m: PlacedMeasurementRef, p: Vec2, zoom?: number, tileScaleFactor?: number, viewportScale?: number) => boolean;
|
|
40
40
|
export interface RemoveMeasurementResult {
|
|
41
41
|
ops: AnnotationPatchOp[];
|
|
42
42
|
keepSelection: boolean;
|
|
@@ -92,9 +92,10 @@ export const oppositeRectCorner = (corner) => {
|
|
|
92
92
|
const STAMP_HIT_PADDING = 6;
|
|
93
93
|
// Screen-space grab tolerance (px) for a measurement-annotation line or rect
|
|
94
94
|
// border, converted to doc space via zoom (the body is a thin world-space
|
|
95
|
-
// stroke).
|
|
96
|
-
//
|
|
97
|
-
|
|
95
|
+
// stroke). Deliberately wider than a fingertip so a thin line isn't fiddly to
|
|
96
|
+
// grab on touch, and kept in sync with the select tool's SHAPE_GRAB_PX so
|
|
97
|
+
// tap-select and drag-grab agree on what counts as "on the measurement".
|
|
98
|
+
const LINE_GRAB_PX = 32;
|
|
98
99
|
const segmentDistSq = (p, a, b) => {
|
|
99
100
|
const abx = b.x - a.x;
|
|
100
101
|
const aby = b.y - a.y;
|
|
@@ -109,14 +110,15 @@ const segmentDistSq = (p, a, b) => {
|
|
|
109
110
|
// stamp tile around the anchor, the line body of a line annotation, or the
|
|
110
111
|
// border ring of a rectangle annotation (interiors stay transparent to hits so
|
|
111
112
|
// elements inside remain reachable).
|
|
112
|
-
export const hitPlacedMeasurement = (m, p, zoom = 1) => {
|
|
113
|
+
export const hitPlacedMeasurement = (m, p, zoom = 1, tileScaleFactor = 1, viewportScale = 1) => {
|
|
113
114
|
// The stamp renders as a constant *screen*-size square centered on the
|
|
114
115
|
// anchor, so its doc-space footprint shrinks as you zoom in. Convert the
|
|
115
116
|
// screen-space half-extent (+ padding) back to doc space via the zoom so
|
|
116
117
|
// the hit box always matches what's drawn.
|
|
117
118
|
// Unassociated inputs use the smaller input-tile footprint (#6); the helper
|
|
118
119
|
// folds in scale + the associated/blank size so the hit box matches the draw.
|
|
119
|
-
const half = (stampTileSize(m) / 2 + STAMP_HIT_PADDING) /
|
|
120
|
+
const half = (stampTileSize(m, tileScaleFactor, viewportScale) / 2 + STAMP_HIT_PADDING) /
|
|
121
|
+
zoom;
|
|
120
122
|
const dx = Math.abs(p.x - m.anchor.x);
|
|
121
123
|
const dy = Math.abs(p.y - m.anchor.y);
|
|
122
124
|
if (dx <= half && dy <= half)
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { PlacedMeasurementRef } from '../../types/annotation.js';
|
|
2
2
|
export declare const STAMP_TILE_SIZE = 96;
|
|
3
|
-
export declare const STAMP_INPUT_TILE_SIZE =
|
|
3
|
+
export declare const STAMP_INPUT_TILE_SIZE = 44;
|
|
4
|
+
export declare const DEFAULT_TILE_SCALE = 1;
|
|
5
|
+
export declare const TILE_SCALE_MIN = 0.4;
|
|
6
|
+
export declare const TILE_SCALE_MAX = 2;
|
|
7
|
+
export declare const clampTileScale: (v: number) => number;
|
|
8
|
+
export declare const renderedDocWidthAtFit: (canvasW: number, canvasH: number, docW: number, docH: number) => number;
|
|
9
|
+
export declare const viewportTileScale: (canvasW: number, canvasH: number, docW: number, docH: number) => number;
|
|
4
10
|
export declare const isUnassociatedStamp: (m: Pick<PlacedMeasurementRef, "measurementId" | "measurementPath">) => boolean;
|
|
5
|
-
export declare const stampTileSize: (m: Pick<PlacedMeasurementRef, "measurementId" | "measurementPath" | "scale"
|
|
11
|
+
export declare const stampTileSize: (m: Pick<PlacedMeasurementRef, "measurementId" | "measurementPath" | "scale">, tileScaleFactor?: number, viewportScale?: number) => number;
|