@reekon-tools/boldr-utils 1.6.15 → 1.6.18
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 +18 -2
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +3 -2
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +1 -1
- package/dist/annotation/canvas/elements/ShapeElement.d.ts +4 -2
- package/dist/annotation/canvas/elements/ShapeElement.js +36 -11
- package/dist/canvas/AnnotationCanvas.d.ts +11 -0
- package/dist/canvas/AnnotationCanvas.js +10 -0
- package/dist/canvas/AnnotationCanvas.native.d.ts +8 -0
- package/dist/canvas/AnnotationCanvas.native.js +6 -0
- package/dist/canvas/AnnotationCanvasInner.d.ts +39 -0
- package/dist/canvas/AnnotationCanvasInner.js +219 -0
- package/dist/canvas/AnnotationCanvasInner.native.d.ts +35 -0
- package/dist/canvas/AnnotationCanvasInner.native.js +138 -0
- package/dist/canvas/AnnotationCanvasSkia.d.ts +27 -0
- package/dist/canvas/AnnotationCanvasSkia.js +20 -0
- package/dist/canvas/Tool.d.ts +38 -0
- package/dist/canvas/Tool.js +1 -0
- package/dist/canvas/elements/BackgroundImageElement.d.ts +9 -0
- package/dist/canvas/elements/BackgroundImageElement.js +37 -0
- package/dist/canvas/elements/MeasurementStampElement.d.ts +13 -0
- package/dist/canvas/elements/MeasurementStampElement.js +30 -0
- package/dist/canvas/elements/ShapeElement.d.ts +7 -0
- package/dist/canvas/elements/ShapeElement.js +62 -0
- package/dist/canvas/elements/StrokeElement.d.ts +7 -0
- package/dist/canvas/elements/StrokeElement.js +18 -0
- package/dist/canvas/measurementPicker.d.ts +10 -0
- package/dist/canvas/measurementPicker.js +1 -0
- package/dist/canvas/measurementStampOverlay.d.ts +11 -0
- package/dist/canvas/measurementStampOverlay.js +1 -0
- package/dist/canvas/pointerAdapter.d.ts +3 -0
- package/dist/canvas/pointerAdapter.js +19 -0
- package/dist/canvas/stampLayout.d.ts +5 -0
- package/dist/canvas/stampLayout.js +14 -0
- package/dist/canvas/tools/measurementStampTool.d.ts +9 -0
- package/dist/canvas/tools/measurementStampTool.js +37 -0
- package/dist/canvas/tools/panTool.d.ts +5 -0
- package/dist/canvas/tools/panTool.js +25 -0
- package/dist/canvas/tools/penTool.d.ts +13 -0
- package/dist/canvas/tools/penTool.js +68 -0
- package/dist/canvas/tools/selectTool.d.ts +2 -0
- package/dist/canvas/tools/selectTool.js +182 -0
- package/dist/canvas/useAnnotationCanvasState.d.ts +54 -0
- package/dist/canvas/useAnnotationCanvasState.js +210 -0
- package/dist/canvas/viewport.d.ts +16 -0
- package/dist/canvas/viewport.js +54 -0
- package/dist/data/AnnotationDataContext.d.ts +8 -0
- package/dist/data/AnnotationDataContext.js +11 -0
- package/dist/data/AnnotationDataProvider.d.ts +65 -0
- package/dist/data/AnnotationDataProvider.js +4 -0
- package/dist/data/InMemoryAnnotationProvider.d.ts +30 -0
- package/dist/data/InMemoryAnnotationProvider.js +197 -0
- package/dist/data/canvasPersistence.d.ts +3 -0
- package/dist/data/canvasPersistence.js +26 -0
- package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +33 -0
- package/dist/data/hooks/useAnnotationCanvasDoc.js +314 -0
- package/dist/data/hooks/useAnnotationDoc.d.ts +7 -0
- package/dist/data/hooks/useAnnotationDoc.js +33 -0
- package/dist/data/hooks/useAnnotationList.d.ts +7 -0
- package/dist/data/hooks/useAnnotationList.js +26 -0
- package/dist/data/hooks/useAnnotationMutations.d.ts +9 -0
- package/dist/data/hooks/useAnnotationMutations.js +11 -0
- package/dist/hooks/useParseMeasurement.d.ts +4 -0
- package/dist/hooks/useParseMeasurement.js +14 -0
- package/dist/types/firestore.d.ts +1 -0
- package/dist/utils/evaluateFormula.d.ts +20 -0
- package/dist/utils/evaluateFormula.js +31 -0
- package/package.json +1 -1
- package/dist/annotation/canvas/tools/measurementLineTool.d.ts +0 -12
- package/dist/annotation/canvas/tools/measurementLineTool.js +0 -95
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type SkFont } from '@shopify/react-native-skia';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type { AnnotationCanvasState, AnnotationStroke } from '../types/annotation.js';
|
|
4
|
+
import { DecimalTolerance, FractionalTolerance, Units, type Measurement } from '../types/firestore.js';
|
|
5
|
+
export interface AnnotationCanvasSkiaProps {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
effectiveCanvas: AnnotationCanvasState;
|
|
9
|
+
worldTransform: Array<{
|
|
10
|
+
scale: number;
|
|
11
|
+
} | {
|
|
12
|
+
translateX: number;
|
|
13
|
+
} | {
|
|
14
|
+
translateY: number;
|
|
15
|
+
}>;
|
|
16
|
+
measurementsById: Map<string, Measurement>;
|
|
17
|
+
fallbackUnit?: Units;
|
|
18
|
+
fractionalTolerance?: FractionalTolerance;
|
|
19
|
+
decimalTolerance?: DecimalTolerance;
|
|
20
|
+
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
21
|
+
valueFont: SkFont | null;
|
|
22
|
+
labelFont: SkFont | null;
|
|
23
|
+
hideMeasurementStamps?: boolean;
|
|
24
|
+
penDrawingStroke: AnnotationStroke | null;
|
|
25
|
+
customPreview?: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, measurementsById, fallbackUnit, fractionalTolerance, decimalTolerance, resolveImageUrl, valueFont, labelFont, hideMeasurementStamps, penDrawingStroke, customPreview, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Canvas, Group } from '@shopify/react-native-skia';
|
|
3
|
+
import { Units, } from '../types/firestore.js';
|
|
4
|
+
import { BackgroundImageElement } from './elements/BackgroundImageElement.js';
|
|
5
|
+
import { MeasurementStampElement } from './elements/MeasurementStampElement.js';
|
|
6
|
+
import { ShapeElement } from './elements/ShapeElement.js';
|
|
7
|
+
import { StrokeElement } from './elements/StrokeElement.js';
|
|
8
|
+
// Platform-agnostic Skia subtree shared by web and native Inners.
|
|
9
|
+
//
|
|
10
|
+
// Call this as a FUNCTION (`AnnotationCanvasSkia({ ... })`), not as a JSX
|
|
11
|
+
// component (`<AnnotationCanvasSkia ... />`). Used as a component, it adds
|
|
12
|
+
// a React component boundary between the parent and Skia's `<Canvas>` that
|
|
13
|
+
// breaks Skia's reconciler when the page first calls `useFont` — the JS
|
|
14
|
+
// thread hangs in `MakeFreeTypeFaceFromData`. Symptoms only appeared on
|
|
15
|
+
// Vite's dev server with a symlinked boldr-utils + React Refresh, but
|
|
16
|
+
// since the function-call pattern works identically on native we use it
|
|
17
|
+
// in both Inners for consistency. Don't add hooks here; this is a plain
|
|
18
|
+
// JSX-returning helper, not a component.
|
|
19
|
+
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, measurementsById, fallbackUnit, fractionalTolerance, decimalTolerance, resolveImageUrl, valueFont, labelFont, hideMeasurementStamps, penDrawingStroke, 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(StrokeElement, { stroke: stroke }, stroke.id))), effectiveCanvas.shapes.map((shape) => (_jsx(ShapeElement, { shape: shape, font: valueFont }, shape.id))), !hideMeasurementStamps &&
|
|
20
|
+
effectiveCanvas.placedMeasurements.map((placed) => (_jsx(MeasurementStampElement, { placed: placed, measurement: measurementsById.get(placed.measurementId) ?? null, fallbackUnit: fallbackUnit ?? Units.Millimeters, fractionalTolerance: fractionalTolerance, decimalTolerance: decimalTolerance, valueFont: valueFont, labelFont: labelFont }, placed.id))), penDrawingStroke && _jsx(StrokeElement, { stroke: penDrawingStroke }), customPreview] }) }));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactNode, ComponentType } from 'react';
|
|
2
|
+
import type { AnnotationCanvasState, AnnotationDocumentPatch, AnnotationElement, Selection, Vec2 } from '../types/annotation.js';
|
|
3
|
+
import type { MeasurementRef } from './measurementPicker.js';
|
|
4
|
+
import type { ViewportApi } from './viewport.js';
|
|
5
|
+
export interface CanvasPointerEvent {
|
|
6
|
+
pointerId: number;
|
|
7
|
+
world: Vec2;
|
|
8
|
+
screen: Vec2;
|
|
9
|
+
pressure?: number;
|
|
10
|
+
shiftKey?: boolean;
|
|
11
|
+
altKey?: boolean;
|
|
12
|
+
metaKey?: boolean;
|
|
13
|
+
ctrlKey?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ToolContext {
|
|
16
|
+
document: AnnotationCanvasState;
|
|
17
|
+
selection: Selection | null;
|
|
18
|
+
viewport: ViewportApi;
|
|
19
|
+
preview(patch: AnnotationDocumentPatch): void;
|
|
20
|
+
commit(patch: AnnotationDocumentPatch): void;
|
|
21
|
+
setSelection(selection: Selection | null): void;
|
|
22
|
+
requestPickMeasurement(): Promise<MeasurementRef | null>;
|
|
23
|
+
applyPan(deltaScreen: Vec2): void;
|
|
24
|
+
applyZoom(focalScreen: Vec2, nextZoom: number): void;
|
|
25
|
+
}
|
|
26
|
+
export type ToolState = unknown;
|
|
27
|
+
export interface Tool {
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
icon?: ComponentType;
|
|
31
|
+
cursor?: string;
|
|
32
|
+
onPointerDown?(event: CanvasPointerEvent, ctx: ToolContext, state: ToolState): ToolState | void;
|
|
33
|
+
onPointerMove?(event: CanvasPointerEvent, ctx: ToolContext, state: ToolState): ToolState | void;
|
|
34
|
+
onPointerUp?(event: CanvasPointerEvent, ctx: ToolContext, state: ToolState): ToolState | void;
|
|
35
|
+
onCancel?(state: ToolState, ctx: ToolContext): void;
|
|
36
|
+
renderPreview?(state: ToolState, ctx: ToolContext): ReactNode;
|
|
37
|
+
hitTest?(element: AnnotationElement, worldPoint: Vec2): boolean;
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnnotationBackgroundImage, BackgroundFit } from '../../types/annotation.js';
|
|
2
|
+
export interface BackgroundImageElementProps {
|
|
3
|
+
image: AnnotationBackgroundImage;
|
|
4
|
+
docWidth: number;
|
|
5
|
+
docHeight: number;
|
|
6
|
+
fit?: BackgroundFit;
|
|
7
|
+
resolveUrl?: (path: string) => Promise<string>;
|
|
8
|
+
}
|
|
9
|
+
export declare const BackgroundImageElement: ({ image, docWidth, docHeight, fit, resolveUrl, }: BackgroundImageElementProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Image, useImage } from '@shopify/react-native-skia';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
const computeFit = (imgW, imgH, docW, docH, fit) => {
|
|
5
|
+
if (fit === 'stretch') {
|
|
6
|
+
return { x: 0, y: 0, width: docW, height: docH };
|
|
7
|
+
}
|
|
8
|
+
const scale = fit === 'cover'
|
|
9
|
+
? Math.max(docW / imgW, docH / imgH)
|
|
10
|
+
: Math.min(docW / imgW, docH / imgH);
|
|
11
|
+
const w = imgW * scale;
|
|
12
|
+
const h = imgH * scale;
|
|
13
|
+
return { x: (docW - w) / 2, y: (docH - h) / 2, width: w, height: h };
|
|
14
|
+
};
|
|
15
|
+
export const BackgroundImageElement = ({ image, docWidth, docHeight, fit = 'contain', resolveUrl, }) => {
|
|
16
|
+
const [url, setUrl] = useState(image.downloadUrl);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
let cancelled = false;
|
|
19
|
+
if (resolveUrl) {
|
|
20
|
+
resolveUrl(image.storagePath).then((next) => {
|
|
21
|
+
if (!cancelled)
|
|
22
|
+
setUrl(next);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
setUrl(image.downloadUrl);
|
|
27
|
+
}
|
|
28
|
+
return () => {
|
|
29
|
+
cancelled = true;
|
|
30
|
+
};
|
|
31
|
+
}, [image.downloadUrl, image.storagePath, resolveUrl]);
|
|
32
|
+
const skImage = useImage(url);
|
|
33
|
+
if (!skImage)
|
|
34
|
+
return null;
|
|
35
|
+
const dims = computeFit(image.widthPx, image.heightPx, docWidth, docHeight, fit);
|
|
36
|
+
return (_jsx(Image, { image: skImage, x: dims.x, y: dims.y, width: dims.width, height: dims.height, fit: "fill" }));
|
|
37
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SkFont } from '@shopify/react-native-skia';
|
|
2
|
+
import type { PlacedMeasurementRef } from '../../types/annotation.js';
|
|
3
|
+
import { DecimalTolerance, FractionalTolerance, type Measurement, type Units } from '../../types/firestore.js';
|
|
4
|
+
export interface MeasurementStampElementProps {
|
|
5
|
+
placed: PlacedMeasurementRef;
|
|
6
|
+
measurement: Measurement | null;
|
|
7
|
+
fallbackUnit: Units;
|
|
8
|
+
fractionalTolerance?: FractionalTolerance;
|
|
9
|
+
decimalTolerance?: DecimalTolerance;
|
|
10
|
+
valueFont?: SkFont | null;
|
|
11
|
+
labelFont?: SkFont | null;
|
|
12
|
+
}
|
|
13
|
+
export declare const MeasurementStampElement: ({ placed, measurement, fallbackUnit, fractionalTolerance, decimalTolerance, valueFont, labelFont, }: MeasurementStampElementProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Group, Line, RoundedRect, Text, } from '@shopify/react-native-skia';
|
|
3
|
+
import { DecimalTolerance, FractionalTolerance, } from '../../types/firestore.js';
|
|
4
|
+
import { convertMicrometers } from '../../utils/micrometersToUnit.js';
|
|
5
|
+
import { STAMP_HEIGHT, STAMP_PADDING_X, STAMP_PADDING_Y, STAMP_WIDTH, } from '../stampLayout.js';
|
|
6
|
+
const VALUE_FONT_BASELINE_OFFSET = 14;
|
|
7
|
+
const LABEL_FONT_BASELINE_OFFSET = 24;
|
|
8
|
+
const formatValue = (measurement, unit, fractionalTolerance, decimalTolerance) => {
|
|
9
|
+
if (!measurement)
|
|
10
|
+
return '—';
|
|
11
|
+
const result = convertMicrometers(measurement.value, unit, fractionalTolerance, decimalTolerance);
|
|
12
|
+
return `${result.value}${result.unit ? ` ${result.unit}` : ''}`;
|
|
13
|
+
};
|
|
14
|
+
export const MeasurementStampElement = ({ placed, measurement, fallbackUnit, fractionalTolerance = FractionalTolerance.Sixteenth, decimalTolerance = DecimalTolerance.Hundredth, valueFont, labelFont, }) => {
|
|
15
|
+
const unit = placed.unitOverride ?? measurement?.unit ?? fallbackUnit;
|
|
16
|
+
const valueText = formatValue(measurement, unit, fractionalTolerance, decimalTolerance);
|
|
17
|
+
const label = placed.labelOverride ??
|
|
18
|
+
measurement?.label ??
|
|
19
|
+
(measurement
|
|
20
|
+
? `M${measurement.measurementIndex ?? ''}`
|
|
21
|
+
: '');
|
|
22
|
+
const showLabel = placed.showLabel !== false && !!label;
|
|
23
|
+
const showValue = placed.showValue !== false;
|
|
24
|
+
const scale = placed.scale ?? 1;
|
|
25
|
+
// Fixed-size stamp — no measureText calls. Keeps rendering deterministic
|
|
26
|
+
// even when no font is loaded (browser/native both).
|
|
27
|
+
const baseX = placed.anchor.x - (STAMP_WIDTH * scale) / 2;
|
|
28
|
+
const baseY = placed.anchor.y - (STAMP_HEIGHT * scale) / 2;
|
|
29
|
+
return (_jsxs(Group, { transform: [{ translateX: baseX }, { translateY: baseY }, { scale }], children: [placed.leader && (_jsx(Line, { p1: placed.leader.from, p2: placed.leader.to, color: "#3B82F6", style: "stroke", strokeWidth: 1.5 })), _jsx(RoundedRect, { x: 0, y: 0, width: STAMP_WIDTH, height: STAMP_HEIGHT, r: 6, color: "#FFFFFFEE" }), _jsx(RoundedRect, { x: 0, y: 0, width: STAMP_WIDTH, height: STAMP_HEIGHT, r: 6, color: "#3B82F6", style: "stroke", strokeWidth: 1.25 }), showValue && valueFont && (_jsx(Text, { x: STAMP_PADDING_X, y: STAMP_PADDING_Y + VALUE_FONT_BASELINE_OFFSET, text: valueText, font: valueFont, color: "#111827" })), showLabel && labelFont && (_jsx(Text, { x: STAMP_PADDING_X, y: STAMP_PADDING_Y + LABEL_FONT_BASELINE_OFFSET, text: label, font: labelFont, color: "#6B7280" }))] }));
|
|
30
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SkFont } from '@shopify/react-native-skia';
|
|
2
|
+
import type { AnnotationShape } from '../../types/annotation.js';
|
|
3
|
+
export interface ShapeElementProps {
|
|
4
|
+
shape: AnnotationShape;
|
|
5
|
+
font?: SkFont | null;
|
|
6
|
+
}
|
|
7
|
+
export declare const ShapeElement: ({ shape, font }: ShapeElementProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Circle, Line, Path, Rect, Skia, Text, } from '@shopify/react-native-skia';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
const polygonPath = (points) => {
|
|
5
|
+
const path = Skia.Path.Make();
|
|
6
|
+
if (points.length === 0)
|
|
7
|
+
return path;
|
|
8
|
+
path.moveTo(points[0].x, points[0].y);
|
|
9
|
+
for (let i = 1; i < points.length; i++) {
|
|
10
|
+
path.lineTo(points[i].x, points[i].y);
|
|
11
|
+
}
|
|
12
|
+
path.close();
|
|
13
|
+
return path;
|
|
14
|
+
};
|
|
15
|
+
export const ShapeElement = ({ shape, font }) => {
|
|
16
|
+
const { kind, geometry, style, text } = shape;
|
|
17
|
+
const stroke = style.stroke ?? '#000000';
|
|
18
|
+
const fill = style.fill;
|
|
19
|
+
const strokeWidth = style.strokeWidth ?? 2;
|
|
20
|
+
const polyPath = useMemo(() => (kind === 'polygon' ? polygonPath(geometry.points) : null), [kind, geometry.points]);
|
|
21
|
+
switch (kind) {
|
|
22
|
+
case 'rect': {
|
|
23
|
+
const [a, b] = geometry.points;
|
|
24
|
+
if (!a || !b)
|
|
25
|
+
return null;
|
|
26
|
+
const x = Math.min(a.x, b.x);
|
|
27
|
+
const y = Math.min(a.y, b.y);
|
|
28
|
+
const w = Math.abs(b.x - a.x);
|
|
29
|
+
const h = Math.abs(b.y - a.y);
|
|
30
|
+
return (_jsxs(_Fragment, { children: [fill && _jsx(Rect, { x: x, y: y, width: w, height: h, color: fill }), _jsx(Rect, { x: x, y: y, width: w, height: h, color: stroke, style: "stroke", strokeWidth: strokeWidth })] }));
|
|
31
|
+
}
|
|
32
|
+
case 'ellipse': {
|
|
33
|
+
const [a, b] = geometry.points;
|
|
34
|
+
if (!a || !b)
|
|
35
|
+
return null;
|
|
36
|
+
const cx = (a.x + b.x) / 2;
|
|
37
|
+
const cy = (a.y + b.y) / 2;
|
|
38
|
+
const r = Math.max(Math.abs(b.x - a.x), Math.abs(b.y - a.y)) / 2;
|
|
39
|
+
return (_jsxs(_Fragment, { children: [fill && _jsx(Circle, { cx: cx, cy: cy, r: r, color: fill }), _jsx(Circle, { cx: cx, cy: cy, r: r, color: stroke, style: "stroke", strokeWidth: strokeWidth })] }));
|
|
40
|
+
}
|
|
41
|
+
case 'line':
|
|
42
|
+
case 'arrow': {
|
|
43
|
+
const [a, b] = geometry.points;
|
|
44
|
+
if (!a || !b)
|
|
45
|
+
return null;
|
|
46
|
+
return (_jsx(Line, { p1: a, p2: b, color: stroke, style: "stroke", strokeWidth: strokeWidth, strokeCap: "round" }));
|
|
47
|
+
}
|
|
48
|
+
case 'polygon': {
|
|
49
|
+
if (!polyPath)
|
|
50
|
+
return null;
|
|
51
|
+
return (_jsxs(_Fragment, { children: [fill && _jsx(Path, { path: polyPath, color: fill }), _jsx(Path, { path: polyPath, color: stroke, style: "stroke", strokeWidth: strokeWidth })] }));
|
|
52
|
+
}
|
|
53
|
+
case 'text': {
|
|
54
|
+
const [origin] = geometry.points;
|
|
55
|
+
if (!origin || !text)
|
|
56
|
+
return null;
|
|
57
|
+
if (!font)
|
|
58
|
+
return null;
|
|
59
|
+
return (_jsx(Text, { x: origin.x, y: origin.y, text: text, font: font, color: stroke }));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SkPath } from '@shopify/react-native-skia';
|
|
2
|
+
import type { AnnotationStroke } from '../../types/annotation.js';
|
|
3
|
+
export declare const pointsToSkPath: (points: number[]) => SkPath;
|
|
4
|
+
export interface StrokeElementProps {
|
|
5
|
+
stroke: AnnotationStroke;
|
|
6
|
+
}
|
|
7
|
+
export declare const StrokeElement: ({ stroke }: StrokeElementProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Path, Skia } from '@shopify/react-native-skia';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
export const pointsToSkPath = (points) => {
|
|
5
|
+
const path = Skia.Path.Make();
|
|
6
|
+
if (points.length < 2)
|
|
7
|
+
return path;
|
|
8
|
+
path.moveTo(points[0], points[1]);
|
|
9
|
+
for (let i = 2; i < points.length; i += 2) {
|
|
10
|
+
path.lineTo(points[i], points[i + 1]);
|
|
11
|
+
}
|
|
12
|
+
return path;
|
|
13
|
+
};
|
|
14
|
+
export const StrokeElement = ({ stroke }) => {
|
|
15
|
+
const path = useMemo(() => pointsToSkPath(stroke.points), [stroke.points]);
|
|
16
|
+
const opacity = stroke.tool === 'highlighter' ? 0.3 : 1;
|
|
17
|
+
return (_jsx(Path, { path: path, color: stroke.color, style: "stroke", strokeWidth: stroke.width, strokeCap: "round", strokeJoin: "round", opacity: opacity }));
|
|
18
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Units } from '../types/firestore.js';
|
|
2
|
+
export interface MeasurementRef {
|
|
3
|
+
measurementId: string;
|
|
4
|
+
measurementPath: string;
|
|
5
|
+
groupId: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
value?: number;
|
|
8
|
+
unit?: Units;
|
|
9
|
+
}
|
|
10
|
+
export type PickMeasurement = () => Promise<MeasurementRef | null>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { PlacedMeasurementRef } from '../types/annotation.js';
|
|
3
|
+
import type { Measurement } from '../types/firestore.js';
|
|
4
|
+
export interface MeasurementStampRenderArgs {
|
|
5
|
+
placed: PlacedMeasurementRef;
|
|
6
|
+
measurement: Measurement | null;
|
|
7
|
+
selected: boolean;
|
|
8
|
+
size: number;
|
|
9
|
+
zoom: number;
|
|
10
|
+
}
|
|
11
|
+
export type RenderMeasurementStamp = (args: MeasurementStampRenderArgs) => ReactNode;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Web DOM PointerEvent → CanvasPointerEvent. The native variant will live
|
|
2
|
+
// in pointerAdapter.native.ts and will translate gesture-handler events.
|
|
3
|
+
export const domEventToCanvasPointerEvent = (event, canvas, viewport) => {
|
|
4
|
+
const rect = canvas.getBoundingClientRect();
|
|
5
|
+
const screen = {
|
|
6
|
+
x: event.clientX - rect.left,
|
|
7
|
+
y: event.clientY - rect.top,
|
|
8
|
+
};
|
|
9
|
+
return {
|
|
10
|
+
pointerId: event.pointerId,
|
|
11
|
+
screen,
|
|
12
|
+
world: viewport.screenToWorld(screen),
|
|
13
|
+
pressure: event.pressure || undefined,
|
|
14
|
+
shiftKey: event.shiftKey,
|
|
15
|
+
altKey: event.altKey,
|
|
16
|
+
metaKey: event.metaKey,
|
|
17
|
+
ctrlKey: event.ctrlKey,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Shared layout constants for the placed measurement stamp. Lives in a
|
|
2
|
+
// Skia-free module so both render (MeasurementStampElement) and hit-test
|
|
3
|
+
// (selectTool) can import it without dragging @shopify/react-native-skia
|
|
4
|
+
// into the consumer's static import graph.
|
|
5
|
+
export const STAMP_WIDTH = 120;
|
|
6
|
+
export const STAMP_HEIGHT = 44;
|
|
7
|
+
export const STAMP_PADDING_X = 10;
|
|
8
|
+
export const STAMP_PADDING_Y = 6;
|
|
9
|
+
// Constant SCREEN-space edge length of a placed measurement rendered as a
|
|
10
|
+
// square tile (the overlay path — see measurementStampOverlay.ts). The tile
|
|
11
|
+
// is a fixed-size pin: its on-screen size is this times `placed.scale` and
|
|
12
|
+
// does NOT change with zoom (only its position tracks the canvas). Also drives
|
|
13
|
+
// the select-tool hit box, which converts it back to doc space via the zoom.
|
|
14
|
+
export const STAMP_TILE_SIZE = 120;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PlacedMeasurementRef } from '../../types/annotation.js';
|
|
2
|
+
import type { Tool } from '../Tool.js';
|
|
3
|
+
export interface MeasurementStampToolOptions {
|
|
4
|
+
autoSwitchToSelect?: boolean;
|
|
5
|
+
onPlaced?: (ref: PlacedMeasurementRef) => void;
|
|
6
|
+
onAutoSwitch?: (toToolId: string) => void;
|
|
7
|
+
selectToolId?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const createMeasurementStampTool: (options?: MeasurementStampToolOptions) => Tool;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DEFAULT_LAYER_ID } from '../../types/annotation.js';
|
|
2
|
+
let counter = 0;
|
|
3
|
+
const makeId = () => `measurement-${Date.now().toString(36)}-${(counter++).toString(36)}`;
|
|
4
|
+
const firstLayerId = (doc) => doc.layers[0]?.id ?? DEFAULT_LAYER_ID;
|
|
5
|
+
export const createMeasurementStampTool = (options = {}) => {
|
|
6
|
+
const autoSwitchToSelect = options.autoSwitchToSelect ?? true;
|
|
7
|
+
const selectToolId = options.selectToolId ?? 'select';
|
|
8
|
+
return {
|
|
9
|
+
id: 'measurement',
|
|
10
|
+
label: 'Place measurement',
|
|
11
|
+
cursor: 'copy',
|
|
12
|
+
onPointerUp(event, ctx) {
|
|
13
|
+
// Tap-to-place. Open the consumer's picker and commit on selection.
|
|
14
|
+
void ctx.requestPickMeasurement().then((ref) => {
|
|
15
|
+
if (!ref)
|
|
16
|
+
return;
|
|
17
|
+
const placed = {
|
|
18
|
+
id: makeId(),
|
|
19
|
+
layerId: firstLayerId(ctx.document),
|
|
20
|
+
measurementPath: ref.measurementPath,
|
|
21
|
+
measurementId: ref.measurementId,
|
|
22
|
+
groupId: ref.groupId,
|
|
23
|
+
anchor: event.world,
|
|
24
|
+
labelOverride: ref.label,
|
|
25
|
+
unitOverride: ref.unit,
|
|
26
|
+
showLabel: true,
|
|
27
|
+
showValue: true,
|
|
28
|
+
createdAt: Date.now(),
|
|
29
|
+
};
|
|
30
|
+
ctx.commit({ ops: [{ op: 'addMeasurement', measurement: placed }] });
|
|
31
|
+
options.onPlaced?.(placed);
|
|
32
|
+
if (autoSwitchToSelect)
|
|
33
|
+
options.onAutoSwitch?.(selectToolId);
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Built-in "Hand" tool. When active, any drag pans the viewport. Tools that
|
|
2
|
+
// want one-off pan triggers (middle-mouse, space-drag) get those via the
|
|
3
|
+
// AnnotationCanvas `gestures` prop without needing to switch tools.
|
|
4
|
+
export const createPanTool = (options = {}) => ({
|
|
5
|
+
id: 'pan',
|
|
6
|
+
label: 'Hand',
|
|
7
|
+
cursor: options.cursor ?? 'grab',
|
|
8
|
+
onPointerDown(event, _ctx, _state) {
|
|
9
|
+
return { kind: 'panning', lastScreen: event.screen };
|
|
10
|
+
},
|
|
11
|
+
onPointerMove(event, ctx, state) {
|
|
12
|
+
const s = state;
|
|
13
|
+
if (s?.kind !== 'panning')
|
|
14
|
+
return s;
|
|
15
|
+
const delta = {
|
|
16
|
+
x: event.screen.x - s.lastScreen.x,
|
|
17
|
+
y: event.screen.y - s.lastScreen.y,
|
|
18
|
+
};
|
|
19
|
+
ctx.applyPan(delta);
|
|
20
|
+
return { kind: 'panning', lastScreen: event.screen };
|
|
21
|
+
},
|
|
22
|
+
onPointerUp(_event, _ctx, _state) {
|
|
23
|
+
// No commit — viewport state is canvas-internal, not persisted.
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AnnotationStroke } from '../../types/annotation.js';
|
|
2
|
+
import type { Tool } from '../Tool.js';
|
|
3
|
+
export interface PenToolOptions {
|
|
4
|
+
color?: string;
|
|
5
|
+
width?: number;
|
|
6
|
+
minSampleDistance?: number;
|
|
7
|
+
variant?: 'pen' | 'marker' | 'highlighter';
|
|
8
|
+
}
|
|
9
|
+
export interface PenDrawingState {
|
|
10
|
+
kind: 'pen-drawing';
|
|
11
|
+
stroke: AnnotationStroke;
|
|
12
|
+
}
|
|
13
|
+
export declare const createPenTool: (options?: PenToolOptions) => Tool;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { DEFAULT_LAYER_ID } from '../../types/annotation.js';
|
|
2
|
+
const distSq = (ax, ay, bx, by) => {
|
|
3
|
+
const dx = ax - bx;
|
|
4
|
+
const dy = ay - by;
|
|
5
|
+
return dx * dx + dy * dy;
|
|
6
|
+
};
|
|
7
|
+
let counter = 0;
|
|
8
|
+
const makeId = (prefix) => `${prefix}-${Date.now().toString(36)}-${(counter++).toString(36)}`;
|
|
9
|
+
export const createPenTool = (options = {}) => {
|
|
10
|
+
const color = options.color ?? '#111827';
|
|
11
|
+
const width = options.width ?? 2;
|
|
12
|
+
const variant = options.variant ?? 'pen';
|
|
13
|
+
const minSampleDistance = options.minSampleDistance ?? 1.5;
|
|
14
|
+
const minSampleDistanceSq = minSampleDistance * minSampleDistance;
|
|
15
|
+
return {
|
|
16
|
+
id: variant,
|
|
17
|
+
label: variant === 'pen' ? 'Pen' : variant === 'marker' ? 'Marker' : 'Highlighter',
|
|
18
|
+
cursor: 'crosshair',
|
|
19
|
+
onPointerDown(event, ctx) {
|
|
20
|
+
const stroke = {
|
|
21
|
+
id: makeId('stroke'),
|
|
22
|
+
layerId: ctx.document.layers[0]?.id ?? DEFAULT_LAYER_ID,
|
|
23
|
+
tool: variant,
|
|
24
|
+
color,
|
|
25
|
+
width,
|
|
26
|
+
points: [event.world.x, event.world.y],
|
|
27
|
+
pressure: event.pressure !== undefined ? [event.pressure] : undefined,
|
|
28
|
+
createdAt: Date.now(),
|
|
29
|
+
};
|
|
30
|
+
return { kind: 'pen-drawing', stroke };
|
|
31
|
+
},
|
|
32
|
+
onPointerMove(event, _ctx, state) {
|
|
33
|
+
const s = state;
|
|
34
|
+
if (s?.kind !== 'pen-drawing')
|
|
35
|
+
return s;
|
|
36
|
+
const len = s.stroke.points.length;
|
|
37
|
+
const lastX = s.stroke.points[len - 2];
|
|
38
|
+
const lastY = s.stroke.points[len - 1];
|
|
39
|
+
if (distSq(lastX, lastY, event.world.x, event.world.y) < minSampleDistanceSq) {
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
kind: 'pen-drawing',
|
|
44
|
+
stroke: {
|
|
45
|
+
...s.stroke,
|
|
46
|
+
points: [...s.stroke.points, event.world.x, event.world.y],
|
|
47
|
+
pressure: s.stroke.pressure && event.pressure !== undefined
|
|
48
|
+
? [...s.stroke.pressure, event.pressure]
|
|
49
|
+
: s.stroke.pressure,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
onPointerUp(_event, ctx, state) {
|
|
54
|
+
const s = state;
|
|
55
|
+
if (s?.kind !== 'pen-drawing')
|
|
56
|
+
return;
|
|
57
|
+
// Need at least two distinct samples to be a stroke.
|
|
58
|
+
if (s.stroke.points.length < 4)
|
|
59
|
+
return;
|
|
60
|
+
ctx.commit({ ops: [{ op: 'addStroke', stroke: s.stroke }] });
|
|
61
|
+
},
|
|
62
|
+
// No renderPreview. The canvas inner detects PenDrawingState in
|
|
63
|
+
// toolState and renders the in-flight stroke directly with its own
|
|
64
|
+
// (Skia-aware) StrokeElement. Keeping tools Skia-free is what lets
|
|
65
|
+
// consumers import them statically without breaking WithSkiaWeb's
|
|
66
|
+
// lazy-load ordering on web.
|
|
67
|
+
};
|
|
68
|
+
};
|