@reekon-tools/boldr-utils 1.6.11 → 1.6.13
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/{canvas → annotation/canvas}/AnnotationCanvasInner.d.ts +5 -3
- package/dist/{canvas → annotation/canvas}/AnnotationCanvasInner.js +36 -17
- package/dist/{canvas → annotation/canvas}/AnnotationCanvasInner.native.d.ts +5 -3
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +810 -0
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +61 -0
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +158 -0
- package/dist/annotation/canvas/Tool.d.ts +77 -0
- package/dist/{canvas → annotation/canvas}/elements/BackgroundImageElement.d.ts +2 -2
- package/dist/{canvas → annotation/canvas}/elements/BackgroundImageElement.js +17 -7
- package/dist/annotation/canvas/elements/ShapeElement.d.ts +7 -0
- package/dist/{canvas → annotation/canvas}/elements/ShapeElement.js +33 -5
- package/dist/annotation/canvas/elements/StrokeElement.d.ts +7 -0
- package/dist/annotation/canvas/elements/StrokeElement.js +45 -0
- package/dist/annotation/canvas/measurementGeometry.d.ts +43 -0
- package/dist/annotation/canvas/measurementGeometry.js +111 -0
- package/dist/{canvas → annotation/canvas}/measurementPicker.d.ts +1 -1
- package/dist/{canvas → annotation/canvas}/measurementStampOverlay.d.ts +2 -2
- package/dist/annotation/canvas/stampLayout.d.ts +1 -0
- package/dist/annotation/canvas/stampLayout.js +11 -0
- package/dist/annotation/canvas/strokeGeometry.d.ts +5 -0
- package/dist/annotation/canvas/strokeGeometry.js +41 -0
- package/dist/annotation/canvas/textGeometry.d.ts +24 -0
- package/dist/annotation/canvas/textGeometry.js +110 -0
- package/dist/{canvas → annotation/canvas}/tools/measurementStampTool.d.ts +1 -1
- package/dist/{canvas → annotation/canvas}/tools/measurementStampTool.js +1 -1
- package/dist/{canvas → annotation/canvas}/tools/panTool.js +3 -0
- package/dist/{canvas → annotation/canvas}/tools/penTool.d.ts +3 -1
- package/dist/{canvas → annotation/canvas}/tools/penTool.js +34 -5
- package/dist/annotation/canvas/tools/selectTool.js +446 -0
- package/dist/annotation/canvas/tools/textTool.d.ts +12 -0
- package/dist/annotation/canvas/tools/textTool.js +78 -0
- package/dist/{canvas → annotation/canvas}/useAnnotationCanvasState.d.ts +11 -3
- package/dist/{canvas → annotation/canvas}/useAnnotationCanvasState.js +142 -2
- package/dist/{canvas → annotation/canvas}/viewport.d.ts +1 -1
- package/dist/{data → annotation/data}/AnnotationDataProvider.d.ts +1 -1
- package/dist/{data → annotation/data}/InMemoryAnnotationProvider.d.ts +1 -1
- package/dist/{data → annotation/data}/InMemoryAnnotationProvider.js +1 -1
- package/dist/{data → annotation/data}/canvasPersistence.d.ts +1 -1
- package/dist/{data → annotation/data}/canvasPersistence.js +1 -1
- package/dist/annotation/data/coalescedRunner.d.ts +1 -0
- package/dist/annotation/data/coalescedRunner.js +48 -0
- package/dist/{data → annotation/data}/hooks/useAnnotationCanvasDoc.d.ts +1 -1
- package/dist/{data → annotation/data}/hooks/useAnnotationCanvasDoc.js +37 -16
- package/dist/exports.d.ts +23 -19
- package/dist/exports.js +18 -14
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.native.d.ts +1 -1
- package/dist/index.native.js +1 -1
- package/dist/types/annotation.d.ts +22 -3
- package/dist/types/firestore.d.ts +0 -1
- package/dist/{hooks → utils}/useParseMeasurement.js +1 -1
- package/package.json +1 -1
- 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/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/StrokeElement.d.ts +0 -7
- package/dist/canvas/elements/StrokeElement.js +0 -18
- package/dist/canvas/stampLayout.d.ts +0 -5
- package/dist/canvas/stampLayout.js +0 -14
- package/dist/canvas/tools/selectTool.js +0 -182
- package/dist/utils/evaluateFormula.d.ts +0 -20
- package/dist/utils/evaluateFormula.js +0 -31
- /package/dist/{canvas → annotation/canvas}/AnnotationCanvas.d.ts +0 -0
- /package/dist/{canvas → annotation/canvas}/AnnotationCanvas.js +0 -0
- /package/dist/{canvas → annotation/canvas}/AnnotationCanvas.native.d.ts +0 -0
- /package/dist/{canvas → annotation/canvas}/AnnotationCanvas.native.js +0 -0
- /package/dist/{canvas → annotation/canvas}/Tool.js +0 -0
- /package/dist/{canvas → annotation/canvas}/measurementPicker.js +0 -0
- /package/dist/{canvas → annotation/canvas}/measurementStampOverlay.js +0 -0
- /package/dist/{canvas → annotation/canvas}/pointerAdapter.d.ts +0 -0
- /package/dist/{canvas → annotation/canvas}/pointerAdapter.js +0 -0
- /package/dist/{canvas → annotation/canvas}/tools/panTool.d.ts +0 -0
- /package/dist/{canvas → annotation/canvas}/tools/selectTool.d.ts +0 -0
- /package/dist/{canvas → annotation/canvas}/viewport.js +0 -0
- /package/dist/{data → annotation/data}/AnnotationDataContext.d.ts +0 -0
- /package/dist/{data → annotation/data}/AnnotationDataContext.js +0 -0
- /package/dist/{data → annotation/data}/AnnotationDataProvider.js +0 -0
- /package/dist/{data → annotation/data}/hooks/useAnnotationDoc.d.ts +0 -0
- /package/dist/{data → annotation/data}/hooks/useAnnotationDoc.js +0 -0
- /package/dist/{data → annotation/data}/hooks/useAnnotationList.d.ts +0 -0
- /package/dist/{data → annotation/data}/hooks/useAnnotationList.js +0 -0
- /package/dist/{data → annotation/data}/hooks/useAnnotationMutations.d.ts +0 -0
- /package/dist/{data → annotation/data}/hooks/useAnnotationMutations.js +0 -0
- /package/dist/{hooks → utils}/useParseMeasurement.d.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { PlacedMeasurementRef } from '
|
|
3
|
-
import type { Measurement } from '
|
|
2
|
+
import type { PlacedMeasurementRef } from '../../types/annotation.js';
|
|
3
|
+
import type { Measurement } from '../../types/firestore.js';
|
|
4
4
|
export interface MeasurementStampRenderArgs {
|
|
5
5
|
placed: PlacedMeasurementRef;
|
|
6
6
|
measurement: Measurement | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const STAMP_TILE_SIZE = 96;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Shared layout constant for the placed measurement stamp. Lives in a
|
|
2
|
+
// Skia-free module so both the render path (the `renderMeasurementStamp`
|
|
3
|
+
// overlay — see measurementStampOverlay.ts) and the hit-test (selectTool)
|
|
4
|
+
// can import it without dragging @shopify/react-native-skia into the
|
|
5
|
+
// consumer's static import graph.
|
|
6
|
+
// Constant SCREEN-space edge length of a placed measurement rendered as a
|
|
7
|
+
// square tile. The tile is a fixed-size pin: its on-screen size is this times
|
|
8
|
+
// `placed.scale` and does NOT change with zoom (only its position tracks the
|
|
9
|
+
// canvas). Also drives the select-tool hit box, which converts it back to doc
|
|
10
|
+
// space via the zoom.
|
|
11
|
+
export const STAMP_TILE_SIZE = 96;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { StrokeCap, Vec2 } from '../../types/annotation.js';
|
|
2
|
+
export declare const toSkiaStrokeCap: (cap: StrokeCap | undefined) => "butt" | "round" | "square";
|
|
3
|
+
export declare const dashIntervals: (width: number) => [number, number];
|
|
4
|
+
export declare const arrowheadLength: (width: number) => number;
|
|
5
|
+
export declare const arrowheadTriangle: (end: Vec2, from: Vec2, width: number) => [Vec2, Vec2, Vec2];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Pure helpers for stroke end-cap rendering. Skia-free and dependency-free so
|
|
2
|
+
// they're safe in element renderers, the measurement-line pass, and unit tests.
|
|
3
|
+
// Map our end-cap style to a Skia-valid strokeCap. 'arrow' is NOT a Skia cap
|
|
4
|
+
// (we draw an arrowhead separately), so the base line uses a 'round' cap.
|
|
5
|
+
// Absent === 'round'.
|
|
6
|
+
export const toSkiaStrokeCap = (cap) => cap === 'butt' || cap === 'square' ? cap : 'round';
|
|
7
|
+
// Dash pattern ([on, off], doc units) for dashed strokes/lines/rect borders.
|
|
8
|
+
// Scales with the stroke width so thick strokes read as dashed rather than
|
|
9
|
+
// dotted, floored so thin strokes still show distinct dashes. Single source
|
|
10
|
+
// for every dashed paint in the canvas — keep all platforms identical.
|
|
11
|
+
export const dashIntervals = (width) => [
|
|
12
|
+
Math.max(width * 3, 6),
|
|
13
|
+
Math.max(width * 2, 4),
|
|
14
|
+
];
|
|
15
|
+
// Forward length of the arrowhead (line end → apex), as a multiple of stroke
|
|
16
|
+
// width (clamped to a minimum so thin strokes still get a visible head). Doc
|
|
17
|
+
// units. Kept small since the head is a solid filled triangle.
|
|
18
|
+
export const arrowheadLength = (width) => Math.max(width * 2, 6);
|
|
19
|
+
// Half the arrowhead's base width. Kept >= the round-cap radius (width / 2) so
|
|
20
|
+
// the triangle fully covers the shaft's end cap.
|
|
21
|
+
const arrowheadHalfBase = (width) => Math.max(width * 0.9, 4);
|
|
22
|
+
// A solid-triangle arrowhead pointing in the travel direction (from `from`
|
|
23
|
+
// toward `end`). Returns [apex, baseLeft, baseRight]: the apex sits FORWARD of
|
|
24
|
+
// the line end (so the point isn't swallowed by the shaft or its end cap), and
|
|
25
|
+
// the base straddles the end point (so the triangle hides the cap bulge).
|
|
26
|
+
export const arrowheadTriangle = (end, from, width) => {
|
|
27
|
+
const dx = end.x - from.x;
|
|
28
|
+
const dy = end.y - from.y;
|
|
29
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
30
|
+
const ux = dx / len; // unit travel direction
|
|
31
|
+
const uy = dy / len;
|
|
32
|
+
const px = -uy; // unit perpendicular
|
|
33
|
+
const py = ux;
|
|
34
|
+
const head = arrowheadLength(width);
|
|
35
|
+
const half = arrowheadHalfBase(width);
|
|
36
|
+
return [
|
|
37
|
+
{ x: end.x + ux * head, y: end.y + uy * head }, // apex, forward of the end
|
|
38
|
+
{ x: end.x + px * half, y: end.y + py * half }, // base left
|
|
39
|
+
{ x: end.x - px * half, y: end.y - py * half }, // base right
|
|
40
|
+
];
|
|
41
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AnnotationShape, Vec2 } from '../../types/annotation.js';
|
|
2
|
+
export declare const DEFAULT_TEXT_FONT_SIZE = 24;
|
|
3
|
+
export declare const MIN_TEXT_FONT_SIZE = 8;
|
|
4
|
+
export declare const MAX_TEXT_FONT_SIZE = 200;
|
|
5
|
+
export declare const TEXT_LINE_HEIGHT_FACTOR = 1.2;
|
|
6
|
+
export declare const TEXT_BASELINE_FACTOR = 0.8;
|
|
7
|
+
export declare const SELECTION_PAD = 6;
|
|
8
|
+
export declare const estimateLineWidth: (line: string, fontSize: number) => number;
|
|
9
|
+
export interface TextBounds {
|
|
10
|
+
minX: number;
|
|
11
|
+
minY: number;
|
|
12
|
+
maxX: number;
|
|
13
|
+
maxY: number;
|
|
14
|
+
}
|
|
15
|
+
export declare const textShapeBounds: (shape: AnnotationShape) => TextBounds | null;
|
|
16
|
+
export declare const hitTestTextShape: (shape: AnnotationShape, p: Vec2, padding?: number) => boolean;
|
|
17
|
+
export interface ResizeGeometry {
|
|
18
|
+
pivot: Vec2;
|
|
19
|
+
handle: Vec2;
|
|
20
|
+
minScale: number;
|
|
21
|
+
maxScale: number;
|
|
22
|
+
}
|
|
23
|
+
export declare const textResizeGeometry: (shape: AnnotationShape) => ResizeGeometry | null;
|
|
24
|
+
export declare const resizeScaleFromDrag: (geom: ResizeGeometry, delta: Vec2) => number;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Geometry for `kind: 'text'` shapes. A text shape stores only its TOP-LEFT
|
|
2
|
+
// anchor (`geometry.points[0]`), its content (`text`, '\n'-separated lines) and
|
|
3
|
+
// its size (`style.fontSize`, doc units); everything else — bounds, hit boxes,
|
|
4
|
+
// the resize handle, line layout — derives from those through this module.
|
|
5
|
+
//
|
|
6
|
+
// This module is deliberately Skia-free: tools (selectTool, textTool) need the
|
|
7
|
+
// same bounds the renderer uses, and tools must stay importable without
|
|
8
|
+
// touching @shopify/react-native-skia (see penTool.ts). So bounds are
|
|
9
|
+
// ESTIMATED from per-character advance-width factors rather than measured with
|
|
10
|
+
// a real SkFont. The renderer (ShapeElement) lays glyphs out with the same
|
|
11
|
+
// line-height/baseline factors, so boxes and glyphs agree vertically; widths
|
|
12
|
+
// are approximate, which is fine for selection chrome and hit-testing.
|
|
13
|
+
export const DEFAULT_TEXT_FONT_SIZE = 24;
|
|
14
|
+
export const MIN_TEXT_FONT_SIZE = 8;
|
|
15
|
+
export const MAX_TEXT_FONT_SIZE = 200;
|
|
16
|
+
// Line height and baseline offset (from the line top), as multiples of
|
|
17
|
+
// fontSize. The renderer uses the same factors so the estimated box always
|
|
18
|
+
// brackets the drawn glyphs vertically.
|
|
19
|
+
export const TEXT_LINE_HEIGHT_FACTOR = 1.2;
|
|
20
|
+
export const TEXT_BASELINE_FACTOR = 0.8;
|
|
21
|
+
// Selection-box padding in doc units. Lives here (Skia-free) rather than in
|
|
22
|
+
// AnnotationCanvasSkia so tools can hit-test selection chrome — the text
|
|
23
|
+
// resize handle sits on the padded box corner — without importing the Skia
|
|
24
|
+
// tree. AnnotationCanvasSkia imports this for every selection box.
|
|
25
|
+
export const SELECTION_PAD = 6;
|
|
26
|
+
// Advance width / fontSize per character class, tuned for a bold geometric
|
|
27
|
+
// sans (the app font is Metropolis Bold). Estimates, not measurements — see
|
|
28
|
+
// the module comment.
|
|
29
|
+
const NARROW = new Set("ijl!|.,:;'`".split(''));
|
|
30
|
+
const SLIM = new Set('ftr()[]{}"- '.split(''));
|
|
31
|
+
const WIDE = new Set('mwMW@%'.split(''));
|
|
32
|
+
const charWidthFactor = (ch) => {
|
|
33
|
+
if (NARROW.has(ch))
|
|
34
|
+
return 0.3;
|
|
35
|
+
if (SLIM.has(ch))
|
|
36
|
+
return 0.42;
|
|
37
|
+
if (WIDE.has(ch))
|
|
38
|
+
return 0.92;
|
|
39
|
+
if (ch >= 'A' && ch <= 'Z')
|
|
40
|
+
return 0.72;
|
|
41
|
+
return 0.6;
|
|
42
|
+
};
|
|
43
|
+
export const estimateLineWidth = (line, fontSize) => {
|
|
44
|
+
let w = 0;
|
|
45
|
+
for (const ch of line)
|
|
46
|
+
w += charWidthFactor(ch);
|
|
47
|
+
return w * fontSize;
|
|
48
|
+
};
|
|
49
|
+
const textFontSize = (shape) => shape.style.fontSize ?? DEFAULT_TEXT_FONT_SIZE;
|
|
50
|
+
// Estimated bounds of a text shape, or null when it isn't a renderable text
|
|
51
|
+
// shape (wrong kind, no anchor, no text).
|
|
52
|
+
export const textShapeBounds = (shape) => {
|
|
53
|
+
if (shape.kind !== 'text')
|
|
54
|
+
return null;
|
|
55
|
+
const origin = shape.geometry.points[0];
|
|
56
|
+
if (!origin || !shape.text)
|
|
57
|
+
return null;
|
|
58
|
+
const fontSize = textFontSize(shape);
|
|
59
|
+
const lines = shape.text.split('\n');
|
|
60
|
+
let width = 0;
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
const w = estimateLineWidth(line, fontSize);
|
|
63
|
+
if (w > width)
|
|
64
|
+
width = w;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
minX: origin.x,
|
|
68
|
+
minY: origin.y,
|
|
69
|
+
maxX: origin.x + width,
|
|
70
|
+
maxY: origin.y + lines.length * TEXT_LINE_HEIGHT_FACTOR * fontSize,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
// Whether a world point is on a text shape (its estimated box + padding).
|
|
74
|
+
export const hitTestTextShape = (shape, p, padding = SELECTION_PAD) => {
|
|
75
|
+
const b = textShapeBounds(shape);
|
|
76
|
+
if (!b)
|
|
77
|
+
return false;
|
|
78
|
+
return (p.x >= b.minX - padding &&
|
|
79
|
+
p.x <= b.maxX + padding &&
|
|
80
|
+
p.y >= b.minY - padding &&
|
|
81
|
+
p.y <= b.maxY + padding);
|
|
82
|
+
};
|
|
83
|
+
export const textResizeGeometry = (shape) => {
|
|
84
|
+
const b = textShapeBounds(shape);
|
|
85
|
+
if (!b)
|
|
86
|
+
return null;
|
|
87
|
+
const fontSize = textFontSize(shape);
|
|
88
|
+
return {
|
|
89
|
+
pivot: { x: b.minX, y: b.minY },
|
|
90
|
+
handle: { x: b.maxX + SELECTION_PAD, y: b.maxY + SELECTION_PAD },
|
|
91
|
+
minScale: MIN_TEXT_FONT_SIZE / fontSize,
|
|
92
|
+
maxScale: MAX_TEXT_FONT_SIZE / fontSize,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
// Uniform scale for a resize drag: how far the handle has moved from the
|
|
96
|
+
// pivot, relative to where it started, clamped to the geometry's scale range.
|
|
97
|
+
// The native resize preview worklet in AnnotationCanvasInner.native.tsx is a
|
|
98
|
+
// WORKLET TWIN of this math — keep them in sync.
|
|
99
|
+
export const resizeScaleFromDrag = (geom, delta) => {
|
|
100
|
+
const bx = geom.handle.x - geom.pivot.x;
|
|
101
|
+
const by = geom.handle.y - geom.pivot.y;
|
|
102
|
+
const baseLen = Math.sqrt(bx * bx + by * by);
|
|
103
|
+
let s = 1;
|
|
104
|
+
if (baseLen > 0) {
|
|
105
|
+
const nx = bx + delta.x;
|
|
106
|
+
const ny = by + delta.y;
|
|
107
|
+
s = Math.sqrt(nx * nx + ny * ny) / baseLen;
|
|
108
|
+
}
|
|
109
|
+
return Math.min(geom.maxScale, Math.max(geom.minScale, s));
|
|
110
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_LAYER_ID } from '
|
|
1
|
+
import { DEFAULT_LAYER_ID } from '../../../types/annotation.js';
|
|
2
2
|
let counter = 0;
|
|
3
3
|
const makeId = () => `measurement-${Date.now().toString(36)}-${(counter++).toString(36)}`;
|
|
4
4
|
const firstLayerId = (doc) => doc.layers[0]?.id ?? DEFAULT_LAYER_ID;
|
|
@@ -5,6 +5,9 @@ export const createPanTool = (options = {}) => ({
|
|
|
5
5
|
id: 'pan',
|
|
6
6
|
label: 'Hand',
|
|
7
7
|
cursor: options.cursor ?? 'grab',
|
|
8
|
+
// Native pans the viewport on the UI thread (see Tool.panViewport). The
|
|
9
|
+
// onPointer* handlers below remain the web/parity implementation.
|
|
10
|
+
panViewport: true,
|
|
8
11
|
onPointerDown(event, _ctx, _state) {
|
|
9
12
|
return { kind: 'panning', lastScreen: event.screen };
|
|
10
13
|
},
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { AnnotationStroke } from '
|
|
1
|
+
import type { AnnotationStroke, StrokeCap } from '../../../types/annotation.js';
|
|
2
2
|
import type { Tool } from '../Tool.js';
|
|
3
3
|
export interface PenToolOptions {
|
|
4
4
|
color?: string;
|
|
5
5
|
width?: number;
|
|
6
|
+
cap?: StrokeCap;
|
|
7
|
+
dash?: boolean;
|
|
6
8
|
minSampleDistance?: number;
|
|
7
9
|
variant?: 'pen' | 'marker' | 'highlighter';
|
|
8
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_LAYER_ID } from '
|
|
1
|
+
import { DEFAULT_LAYER_ID } from '../../../types/annotation.js';
|
|
2
2
|
const distSq = (ax, ay, bx, by) => {
|
|
3
3
|
const dx = ax - bx;
|
|
4
4
|
const dy = ay - by;
|
|
@@ -9,13 +9,22 @@ const makeId = (prefix) => `${prefix}-${Date.now().toString(36)}-${(counter++).t
|
|
|
9
9
|
export const createPenTool = (options = {}) => {
|
|
10
10
|
const color = options.color ?? '#111827';
|
|
11
11
|
const width = options.width ?? 2;
|
|
12
|
+
const cap = options.cap ?? 'round';
|
|
13
|
+
const dash = options.dash ?? false;
|
|
12
14
|
const variant = options.variant ?? 'pen';
|
|
13
15
|
const minSampleDistance = options.minSampleDistance ?? 1.5;
|
|
14
16
|
const minSampleDistanceSq = minSampleDistance * minSampleDistance;
|
|
17
|
+
// A degenerate single-point dot has no direction (so 'arrow' is meaningless)
|
|
18
|
+
// and is invisible with a 'butt' cap (zero-length path), so a dot falls back
|
|
19
|
+
// to a visible, directionless cap.
|
|
20
|
+
const dotCap = cap === 'butt' || cap === 'arrow' ? 'round' : cap;
|
|
15
21
|
return {
|
|
16
22
|
id: variant,
|
|
17
23
|
label: variant === 'pen' ? 'Pen' : variant === 'marker' ? 'Marker' : 'Highlighter',
|
|
18
24
|
cursor: 'crosshair',
|
|
25
|
+
// Drives UI-thread drawing on native (see FreehandConfig). The
|
|
26
|
+
// onPointerDown/Move/Up below remain the web/parity implementation.
|
|
27
|
+
freehand: { variant, color, width, cap, dash, minSampleDistance },
|
|
19
28
|
onPointerDown(event, ctx) {
|
|
20
29
|
const stroke = {
|
|
21
30
|
id: makeId('stroke'),
|
|
@@ -23,6 +32,8 @@ export const createPenTool = (options = {}) => {
|
|
|
23
32
|
tool: variant,
|
|
24
33
|
color,
|
|
25
34
|
width,
|
|
35
|
+
cap,
|
|
36
|
+
...(dash && { dash }),
|
|
26
37
|
points: [event.world.x, event.world.y],
|
|
27
38
|
pressure: event.pressure !== undefined ? [event.pressure] : undefined,
|
|
28
39
|
createdAt: Date.now(),
|
|
@@ -54,10 +65,28 @@ export const createPenTool = (options = {}) => {
|
|
|
54
65
|
const s = state;
|
|
55
66
|
if (s?.kind !== 'pen-drawing')
|
|
56
67
|
return;
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
const pts = s.stroke.points;
|
|
69
|
+
if (pts.length >= 4) {
|
|
70
|
+
// Two or more distinct samples → a normal stroke.
|
|
71
|
+
ctx.commit({ ops: [{ op: 'addStroke', stroke: s.stroke }] });
|
|
72
|
+
}
|
|
73
|
+
else if (pts.length === 2) {
|
|
74
|
+
// A click with no drag → a single dot. Duplicate the point so the
|
|
75
|
+
// stroke renders as a filled dot of diameter = width. A 'butt' cap
|
|
76
|
+
// would make the zero-length path invisible, so the dot uses dotCap.
|
|
77
|
+
ctx.commit({
|
|
78
|
+
ops: [
|
|
79
|
+
{
|
|
80
|
+
op: 'addStroke',
|
|
81
|
+
stroke: {
|
|
82
|
+
...s.stroke,
|
|
83
|
+
cap: dotCap,
|
|
84
|
+
points: [pts[0], pts[1], pts[0], pts[1]],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
61
90
|
},
|
|
62
91
|
// No renderPreview. The canvas inner detects PenDrawingState in
|
|
63
92
|
// toolState and renders the in-flight stroke directly with its own
|