@reekon-tools/boldr-utils 1.12.0 → 1.13.0
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/groupHeaderLayout.d.ts +72 -0
- package/dist/annotation/canvas/groupHeaderLayout.js +115 -0
- package/dist/annotation/canvas/stampLayout.d.ts +15 -2
- package/dist/annotation/canvas/stampLayout.js +46 -12
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +1 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +11 -0
- 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/exports.d.ts +2 -1
- package/dist/exports.js +2 -1
- package/dist/hooks/useParseMeasurement.d.ts +4 -0
- package/dist/hooks/useParseMeasurement.js +14 -0
- package/dist/types/annotation.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/theme/index.d.ts +0 -3
- package/dist/theme/index.js +0 -3
- package/dist/theme/preset.d.ts +0 -120
- package/dist/theme/preset.js +0 -25
- package/dist/theme/semantic.d.ts +0 -136
- package/dist/theme/semantic.js +0 -116
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type Group, type Section } from '../../types/firestore.js';
|
|
2
|
+
export declare const GROUP_HEADER_STYLE: {
|
|
3
|
+
readonly background: "#424345";
|
|
4
|
+
readonly radius: 8;
|
|
5
|
+
readonly padding: 5;
|
|
6
|
+
readonly selectedBorderWidth: 3;
|
|
7
|
+
readonly selectedBorderColor: "#FFAD00";
|
|
8
|
+
readonly badgeHeight: 25;
|
|
9
|
+
readonly badgeRadius: 4;
|
|
10
|
+
readonly badgeFontSize: 13;
|
|
11
|
+
readonly badgeTextColor: "#FFFFFF";
|
|
12
|
+
readonly badgeGapExpanded: 5;
|
|
13
|
+
readonly badgeGapCollapsed: 5;
|
|
14
|
+
readonly nameGap: 3;
|
|
15
|
+
readonly nameFontSize: 15;
|
|
16
|
+
readonly nameColor: "#FFFFFF";
|
|
17
|
+
readonly globeSize: 17;
|
|
18
|
+
readonly globeColor: "#FFFFFF";
|
|
19
|
+
};
|
|
20
|
+
export interface GroupHeaderBadge {
|
|
21
|
+
/** Stable React key — 'table' | 'form' | 'group'. */
|
|
22
|
+
key: 'table' | 'form' | 'group';
|
|
23
|
+
/** Rendered text, e.g. "# Table 2". Empty for the default group's globe. */
|
|
24
|
+
text: string;
|
|
25
|
+
background: string;
|
|
26
|
+
}
|
|
27
|
+
export interface GroupHeaderDescriptor {
|
|
28
|
+
collapsed: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Badges in render order. A FORM group reads "# Table N" then "# Form N"
|
|
31
|
+
* (the ordering the job screen's form cards settled); a LIST group reads a
|
|
32
|
+
* single "# Group N". Expanded, they sit side by side and share the row's
|
|
33
|
+
* width; collapsed, they stack full-width. Empty only when the source group
|
|
34
|
+
* is missing entirely.
|
|
35
|
+
*/
|
|
36
|
+
badges: GroupHeaderBadge[];
|
|
37
|
+
/**
|
|
38
|
+
* The default group has no index to show, so its single chip carries a globe
|
|
39
|
+
* glyph instead of text — the same stand-in the measurement picker uses.
|
|
40
|
+
* When true, `badges` holds exactly the one chip to draw the globe inside.
|
|
41
|
+
*/
|
|
42
|
+
showGlobe: boolean;
|
|
43
|
+
/** Group name. `null` when collapsed — a collapsed header is badges only. */
|
|
44
|
+
name: string | null;
|
|
45
|
+
/** Natural box to lay the chip out at, before the overlay transform. */
|
|
46
|
+
design: {
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve what a header chip shows. `group` is looked up live by the host from
|
|
53
|
+
* `placed.groupId` so renames and re-indexes appear without touching the
|
|
54
|
+
* canvas document; `section` is that group's section, needed only for a FORM
|
|
55
|
+
* header's "# Table N". Both may be undefined — a group deleted after its
|
|
56
|
+
* header was placed still has to render something rather than crash.
|
|
57
|
+
*/
|
|
58
|
+
export declare const describeGroupHeader: ({ group, section, collapsed, }: {
|
|
59
|
+
group: Group | undefined | null;
|
|
60
|
+
section?: Section | undefined | null;
|
|
61
|
+
collapsed?: boolean;
|
|
62
|
+
}) => GroupHeaderDescriptor;
|
|
63
|
+
/**
|
|
64
|
+
* Signature of everything `describeGroupHeader` would render, for hosts that
|
|
65
|
+
* cache on tile content (mobile re-bakes its canvas thumbnail when this
|
|
66
|
+
* changes). Kept beside the resolver so the two can't drift.
|
|
67
|
+
*/
|
|
68
|
+
export declare const groupHeaderSignature: (input: {
|
|
69
|
+
group: Group | undefined | null;
|
|
70
|
+
section?: Section | undefined | null;
|
|
71
|
+
collapsed?: boolean;
|
|
72
|
+
}) => string;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Presentation model for the group-header chip placed on an annotation canvas
|
|
2
|
+
// (`PlacedMeasurementRef.isGroupHeader`), shared by mobile and web.
|
|
3
|
+
//
|
|
4
|
+
// The chip itself can't be shared — this package ships DOM and React Native
|
|
5
|
+
// forks of the canvas host, and there is no react-native-web in the graph, so
|
|
6
|
+
// a single component would only render on one of them. What CAN be shared is
|
|
7
|
+
// everything that isn't a view node: which badges a header shows, what they
|
|
8
|
+
// say, what colour they are, and every measurement in the Figma frames. Both
|
|
9
|
+
// hosts render the SAME descriptor into ~40 lines of platform view code, so a
|
|
10
|
+
// change to the header's content or metrics lands in one place.
|
|
11
|
+
//
|
|
12
|
+
// Figma (BOLDR PRO / APP STRUCTURE, "HEADER SETTINGS - MVP" 490:4189):
|
|
13
|
+
// 491:5371 expanded group · 491:5654 collapsed group
|
|
14
|
+
// 491:5381 expanded form · 491:5659 collapsed form
|
|
15
|
+
// Asana 1217382797393698.
|
|
16
|
+
import { GroupType } from '../../types/firestore.js';
|
|
17
|
+
import { GeneralColors, TileColors, YellowColors } from '../../theme/colors.js';
|
|
18
|
+
import { GROUP_HEADER_DESIGN } from './stampLayout.js';
|
|
19
|
+
// Every length here is in the DESIGN space the frames were drawn in — the
|
|
20
|
+
// 200×56 / 90×65 boxes of GROUP_HEADER_DESIGN. Hosts lay the chip out at that
|
|
21
|
+
// natural size and uniformly transform-scale it into the overlay box
|
|
22
|
+
// stampTileDims hands them, so these numbers never need scaling by hand.
|
|
23
|
+
export const GROUP_HEADER_STYLE = {
|
|
24
|
+
// Outer chip.
|
|
25
|
+
background: GeneralColors.grey2,
|
|
26
|
+
radius: 8,
|
|
27
|
+
padding: 5,
|
|
28
|
+
// Selection ring. Drawn INSIDE the chip's box (the footprint is the
|
|
29
|
+
// hit-test box too, so an outset ring would lie about what is grabbable).
|
|
30
|
+
selectedBorderWidth: 3,
|
|
31
|
+
selectedBorderColor: YellowColors.mainYellow,
|
|
32
|
+
// Index badges.
|
|
33
|
+
badgeHeight: 25,
|
|
34
|
+
badgeRadius: 4,
|
|
35
|
+
badgeFontSize: 13,
|
|
36
|
+
badgeTextColor: GeneralColors.white,
|
|
37
|
+
// Gap between the two badges of a form header: horizontal when expanded
|
|
38
|
+
// (they sit side by side), vertical when collapsed (they stack).
|
|
39
|
+
badgeGapExpanded: 5,
|
|
40
|
+
badgeGapCollapsed: 5,
|
|
41
|
+
// Group name, expanded only.
|
|
42
|
+
nameGap: 3,
|
|
43
|
+
nameFontSize: 15,
|
|
44
|
+
nameColor: GeneralColors.white,
|
|
45
|
+
// Globe glyph that stands in for the index badge on the default group.
|
|
46
|
+
globeSize: 17,
|
|
47
|
+
globeColor: GeneralColors.white,
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Resolve what a header chip shows. `group` is looked up live by the host from
|
|
51
|
+
* `placed.groupId` so renames and re-indexes appear without touching the
|
|
52
|
+
* canvas document; `section` is that group's section, needed only for a FORM
|
|
53
|
+
* header's "# Table N". Both may be undefined — a group deleted after its
|
|
54
|
+
* header was placed still has to render something rather than crash.
|
|
55
|
+
*/
|
|
56
|
+
export const describeGroupHeader = ({ group, section, collapsed = false, }) => {
|
|
57
|
+
const design = collapsed
|
|
58
|
+
? GROUP_HEADER_DESIGN.collapsed
|
|
59
|
+
: GROUP_HEADER_DESIGN.expanded;
|
|
60
|
+
const base = {
|
|
61
|
+
collapsed,
|
|
62
|
+
name: collapsed ? null : (group?.name ?? 'Group'),
|
|
63
|
+
design,
|
|
64
|
+
};
|
|
65
|
+
if (!group)
|
|
66
|
+
return { ...base, badges: [], showGlobe: false };
|
|
67
|
+
// Default group (index 0): no number to show — the globe chip stands in.
|
|
68
|
+
if (group.groupIndex === 0) {
|
|
69
|
+
return {
|
|
70
|
+
...base,
|
|
71
|
+
showGlobe: true,
|
|
72
|
+
badges: [{ key: 'group', text: '', background: TileColors.darkBlue }],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const badges = [];
|
|
76
|
+
if (group.type === GroupType.FORM) {
|
|
77
|
+
// "# Table N" only when the section resolved — a form header with a
|
|
78
|
+
// missing section drops to its "# Form N" badge alone rather than
|
|
79
|
+
// rendering an empty chip.
|
|
80
|
+
if (typeof section?.index === 'number') {
|
|
81
|
+
badges.push({
|
|
82
|
+
key: 'table',
|
|
83
|
+
text: `# Table ${section.index + 1}`,
|
|
84
|
+
background: TileColors.lightBlue1,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
badges.push({
|
|
88
|
+
key: 'form',
|
|
89
|
+
text: `# Form ${group.groupIndex + 1}`,
|
|
90
|
+
background: TileColors.activeBlue,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
badges.push({
|
|
95
|
+
key: 'group',
|
|
96
|
+
text: `# Group ${group.groupIndex + 1}`,
|
|
97
|
+
background: TileColors.darkBlue,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return { ...base, badges, showGlobe: false };
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Signature of everything `describeGroupHeader` would render, for hosts that
|
|
104
|
+
* cache on tile content (mobile re-bakes its canvas thumbnail when this
|
|
105
|
+
* changes). Kept beside the resolver so the two can't drift.
|
|
106
|
+
*/
|
|
107
|
+
export const groupHeaderSignature = (input) => {
|
|
108
|
+
const d = describeGroupHeader(input);
|
|
109
|
+
return [
|
|
110
|
+
d.collapsed ? 'c' : 'e',
|
|
111
|
+
d.showGlobe ? 'globe' : '',
|
|
112
|
+
d.badges.map((b) => `${b.key}:${b.text}`).join(','),
|
|
113
|
+
d.name ?? '',
|
|
114
|
+
].join('~');
|
|
115
|
+
};
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import type { AnnotationCanvasState, PlacedMeasurementRef, TileScalePlatform } from '../../types/annotation.js';
|
|
2
2
|
export declare const STAMP_TILE_SIZE = 96;
|
|
3
3
|
export declare const GROUP_HEADER_TILE_WIDTH = 240;
|
|
4
|
-
export declare const GROUP_HEADER_TILE_HEIGHT =
|
|
4
|
+
export declare const GROUP_HEADER_TILE_HEIGHT = 67.2;
|
|
5
|
+
export declare const GROUP_HEADER_DESIGN: {
|
|
6
|
+
readonly expanded: {
|
|
7
|
+
readonly width: 200;
|
|
8
|
+
readonly height: 56;
|
|
9
|
+
};
|
|
10
|
+
readonly collapsed: {
|
|
11
|
+
readonly width: 90;
|
|
12
|
+
readonly height: 65;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export declare const GROUP_HEADER_COLLAPSED_WIDTH: number;
|
|
16
|
+
export declare const GROUP_HEADER_COLLAPSED_HEIGHT: number;
|
|
17
|
+
export declare const groupHeaderBaseDims: (collapsed?: boolean) => StampTileDims;
|
|
5
18
|
export declare const DEFAULT_TILE_SCALE = 1;
|
|
6
19
|
export declare const TILE_SCALE_MIN = 0.4;
|
|
7
20
|
export declare const TILE_SCALE_MAX = 2;
|
|
@@ -15,4 +28,4 @@ export interface StampTileDims {
|
|
|
15
28
|
width: number;
|
|
16
29
|
height: number;
|
|
17
30
|
}
|
|
18
|
-
export declare const stampTileDims: (m: Pick<PlacedMeasurementRef, "scale" | "isGroupHeader">, tileScaleFactor?: number, viewportScale?: number) => StampTileDims;
|
|
31
|
+
export declare const stampTileDims: (m: Pick<PlacedMeasurementRef, "scale" | "isGroupHeader" | "collapsed">, tileScaleFactor?: number, viewportScale?: number) => StampTileDims;
|
|
@@ -15,13 +15,48 @@
|
|
|
15
15
|
// tile-scale slider against blank tiles saw their tiles balloon once
|
|
16
16
|
// populated (Asana 1216233394800417).
|
|
17
17
|
export const STAMP_TILE_SIZE = 96;
|
|
18
|
-
// Base footprint of
|
|
19
|
-
// rather than the square tile
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
18
|
+
// Base footprint of an EXPANDED group-header chip (`isGroupHeader`) — a wide
|
|
19
|
+
// pill rather than the square tile. Sized exactly like STAMP_TILE_SIZE:
|
|
20
|
+
// `placed.scale` × the document-wide tile-scale factor, scaling with zoom from
|
|
21
|
+
// the content-fit reference. The width is the header's established base and
|
|
22
|
+
// does not change; the height now follows the design's 200×56 proportion
|
|
23
|
+
// (see GROUP_HEADER_DESIGN below) rather than the legacy Nutrient stamp's
|
|
24
|
+
// 150×40, a ~5% difference on existing documents.
|
|
23
25
|
export const GROUP_HEADER_TILE_WIDTH = 240;
|
|
24
|
-
export const GROUP_HEADER_TILE_HEIGHT =
|
|
26
|
+
export const GROUP_HEADER_TILE_HEIGHT = 67.2;
|
|
27
|
+
// Natural design-space size of each header state, straight off the Figma
|
|
28
|
+
// frames (491:5371 expanded / 491:5654 collapsed — the two form variants,
|
|
29
|
+
// 491:5381 / 491:5659, use the same boxes). Renderers lay the chip out at
|
|
30
|
+
// THESE dimensions and uniformly transform-scale it into the overlay box, the
|
|
31
|
+
// same natural-size-then-scale pattern the measurement tiles use, so type and
|
|
32
|
+
// badges keep their proportions under the tile-scale slider. Collapsed is one
|
|
33
|
+
// fixed box for both group and form headers — a form stacks its two badges to
|
|
34
|
+
// fill it, a group centers its single badge — so a canvas of collapsed
|
|
35
|
+
// headers reads as a tidy row of equal chips.
|
|
36
|
+
export const GROUP_HEADER_DESIGN = {
|
|
37
|
+
expanded: { width: 200, height: 56 },
|
|
38
|
+
collapsed: { width: 90, height: 65 },
|
|
39
|
+
};
|
|
40
|
+
// Design units → base units. Pinned by the expanded header keeping its
|
|
41
|
+
// established 240-unit width, and applied to BOTH states so the two share one
|
|
42
|
+
// scale: a badge is the same size whether its header is expanded or collapsed.
|
|
43
|
+
const HEADER_BASE_SCALE = GROUP_HEADER_TILE_WIDTH / GROUP_HEADER_DESIGN.expanded.width;
|
|
44
|
+
// Base footprint of a COLLAPSED group-header chip — the design's 90×65 at the
|
|
45
|
+
// same scale. Wider-than-tall no longer holds here: a collapsed header is
|
|
46
|
+
// slightly TALLER than an expanded one (it stacks badges where the expanded
|
|
47
|
+
// one puts them side by side and hangs the name underneath).
|
|
48
|
+
export const GROUP_HEADER_COLLAPSED_WIDTH = GROUP_HEADER_DESIGN.collapsed.width * HEADER_BASE_SCALE;
|
|
49
|
+
export const GROUP_HEADER_COLLAPSED_HEIGHT = GROUP_HEADER_DESIGN.collapsed.height * HEADER_BASE_SCALE;
|
|
50
|
+
// Unscaled footprint of a group header in each state — the one place the
|
|
51
|
+
// expanded/collapsed box choice is made. Callers that already hold a
|
|
52
|
+
// PlacedMeasurementRef should go through stampTileDims instead, which folds in
|
|
53
|
+
// `scale`, the document-wide factor and the zoom.
|
|
54
|
+
export const groupHeaderBaseDims = (collapsed) => collapsed
|
|
55
|
+
? {
|
|
56
|
+
width: GROUP_HEADER_COLLAPSED_WIDTH,
|
|
57
|
+
height: GROUP_HEADER_COLLAPSED_HEIGHT,
|
|
58
|
+
}
|
|
59
|
+
: { width: GROUP_HEADER_TILE_WIDTH, height: GROUP_HEADER_TILE_HEIGHT };
|
|
25
60
|
// Document-wide tile scale factor: one knob that shrinks/grows EVERY
|
|
26
61
|
// measurement tile on the canvas at once, on top of each tile's own `scale`.
|
|
27
62
|
// Lets a user pull tiles down on a dense drawing where lines crowd together,
|
|
@@ -103,10 +138,9 @@ export const isUnassociatedStamp = (m) => !m.isGroupHeader && !m.measurementId &
|
|
|
103
138
|
export const stampTileSize = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1) => STAMP_TILE_SIZE * (m.scale ?? 1) * tileScaleFactor * viewportScale;
|
|
104
139
|
export const stampTileDims = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1) => {
|
|
105
140
|
const k = (m.scale ?? 1) * tileScaleFactor * viewportScale;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
: { width: STAMP_TILE_SIZE * k, height: STAMP_TILE_SIZE * k };
|
|
141
|
+
if (!m.isGroupHeader) {
|
|
142
|
+
return { width: STAMP_TILE_SIZE * k, height: STAMP_TILE_SIZE * k };
|
|
143
|
+
}
|
|
144
|
+
const base = groupHeaderBaseDims(m.collapsed);
|
|
145
|
+
return { width: base.width * k, height: base.height * k };
|
|
112
146
|
};
|
|
@@ -46,6 +46,7 @@ export interface AnnotationCanvasHandle {
|
|
|
46
46
|
placeGroupHeaderAtCenter(ref: {
|
|
47
47
|
groupId: string;
|
|
48
48
|
}): AnnotationElementId;
|
|
49
|
+
setGroupHeaderCollapsed(id: AnnotationElementId, collapsed: boolean): void;
|
|
49
50
|
deleteSelected(): void;
|
|
50
51
|
}
|
|
51
52
|
export interface UseAnnotationCanvasStateProps {
|
|
@@ -661,6 +661,17 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
661
661
|
c.setSelection({ ids: [placed.id] });
|
|
662
662
|
return placed.id;
|
|
663
663
|
},
|
|
664
|
+
setGroupHeaderCollapsed(id, collapsed) {
|
|
665
|
+
const c = ctxRef.current;
|
|
666
|
+
const target = c.document.placedMeasurements.find((m) => m.id === id);
|
|
667
|
+
if (!target?.isGroupHeader)
|
|
668
|
+
return;
|
|
669
|
+
if (!!target.collapsed === collapsed)
|
|
670
|
+
return;
|
|
671
|
+
c.commit({
|
|
672
|
+
ops: [{ op: 'updateMeasurement', id, patch: { collapsed } }],
|
|
673
|
+
});
|
|
674
|
+
},
|
|
664
675
|
deleteSelected() {
|
|
665
676
|
const c = ctxRef.current;
|
|
666
677
|
const ids = c.selection?.ids;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.js';
|
|
3
|
+
export type { AnnotationCanvasHandle, GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
|
|
4
|
+
export interface CanvasKitOpts {
|
|
5
|
+
locateFile?: (file: string) => string;
|
|
6
|
+
}
|
|
7
|
+
export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
|
|
8
|
+
fallback?: ReactNode;
|
|
9
|
+
canvasKitOpts?: CanvasKitOpts;
|
|
10
|
+
};
|
|
11
|
+
export declare const AnnotationCanvas: ({ fallback, canvasKitOpts, ...rest }: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { WithSkiaWeb } from '@shopify/react-native-skia/lib/module/web/index.js';
|
|
3
|
+
// Web-only entry point: lazy-loads CanvasKit wasm via WithSkiaWeb. Imperative
|
|
4
|
+
// API (undo/redo/zoom) is exposed through the `imperativeRef` prop rather
|
|
5
|
+
// than React refs, since WithSkiaWeb can't forward refs through its lazy
|
|
6
|
+
// component boundary. The native build sits in `AnnotationCanvas.native.tsx`
|
|
7
|
+
// and skips WithSkiaWeb entirely (no wasm to load).
|
|
8
|
+
export const AnnotationCanvas = ({ fallback, canvasKitOpts, ...rest }) => (_jsx(WithSkiaWeb, { getComponent: () => import('./AnnotationCanvasInner.js').then((m) => ({
|
|
9
|
+
default: m.AnnotationCanvasInner,
|
|
10
|
+
})), fallback: fallback ?? null, componentProps: rest, opts: canvasKitOpts }));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.native.js';
|
|
2
|
+
export type { AnnotationCanvasHandle, } from './useAnnotationCanvasState.js';
|
|
3
|
+
export type { GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
|
|
4
|
+
export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
|
|
5
|
+
fallback?: unknown;
|
|
6
|
+
canvasKitOpts?: unknown;
|
|
7
|
+
};
|
|
8
|
+
export declare const AnnotationCanvas: (props: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { AnnotationCanvasInner } from './AnnotationCanvasInner.native.js';
|
|
3
|
+
export const AnnotationCanvas = (props) => {
|
|
4
|
+
const { fallback: _f, canvasKitOpts: _o, ...rest } = props;
|
|
5
|
+
return _jsx(AnnotationCanvasInner, { ...rest });
|
|
6
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type CSSProperties, type MutableRefObject } from 'react';
|
|
2
|
+
import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
|
|
3
|
+
import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
|
|
4
|
+
import type { MeasurementRef } from './measurementPicker.js';
|
|
5
|
+
import type { Tool } from './Tool.js';
|
|
6
|
+
import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
|
|
7
|
+
import { type ViewportState } from './viewport.js';
|
|
8
|
+
import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
|
|
9
|
+
export type { AnnotationCanvasHandle };
|
|
10
|
+
export interface AnnotationCanvasInnerProps {
|
|
11
|
+
canvas: AnnotationCanvasState;
|
|
12
|
+
onCommit(patch: AnnotationDocumentPatch): void;
|
|
13
|
+
tools: Tool[];
|
|
14
|
+
activeToolId: string;
|
|
15
|
+
selection: Selection | null;
|
|
16
|
+
onSelectionChange(selection: Selection | null): void;
|
|
17
|
+
measurements?: Measurement[];
|
|
18
|
+
fallbackUnit?: Units;
|
|
19
|
+
fractionalTolerance?: FractionalTolerance;
|
|
20
|
+
decimalTolerance?: DecimalTolerance;
|
|
21
|
+
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
22
|
+
pickMeasurement?: () => Promise<MeasurementRef | null>;
|
|
23
|
+
renderMeasurementStamp?: RenderMeasurementStamp;
|
|
24
|
+
stampFontSource?: unknown;
|
|
25
|
+
stampValueFontSize?: number;
|
|
26
|
+
stampLabelFontSize?: number;
|
|
27
|
+
gestures?: GestureConfig;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
initialViewport?: ViewportState;
|
|
31
|
+
style?: CSSProperties;
|
|
32
|
+
imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
|
|
33
|
+
}
|
|
34
|
+
export type PanTrigger = 'middleMouse' | 'rightMouse' | 'space';
|
|
35
|
+
export interface GestureConfig {
|
|
36
|
+
wheel?: 'zoom' | 'pan' | 'auto';
|
|
37
|
+
panTriggers?: PanTrigger[];
|
|
38
|
+
}
|
|
39
|
+
export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFont } from '@shopify/react-native-skia';
|
|
3
|
+
import { useCallback, useEffect, useRef, } from 'react';
|
|
4
|
+
import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
|
|
5
|
+
import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
|
|
6
|
+
import { STAMP_TILE_SIZE } from './stampLayout.js';
|
|
7
|
+
const DEFAULT_PAN_TRIGGERS = ['middleMouse', 'space'];
|
|
8
|
+
export const AnnotationCanvasInner = (props) => {
|
|
9
|
+
const { fallbackUnit, fractionalTolerance, decimalTolerance, resolveImageUrl, stampFontSource, stampValueFontSize = 14, stampLabelFontSize = 11, gestures, width, height, style, activeToolId, tools, } = props;
|
|
10
|
+
const wheelMode = gestures?.wheel ?? 'auto';
|
|
11
|
+
const panTriggers = gestures?.panTriggers ?? DEFAULT_PAN_TRIGGERS;
|
|
12
|
+
const allowSpacePan = panTriggers.includes('space');
|
|
13
|
+
const allowMiddlePan = panTriggers.includes('middleMouse');
|
|
14
|
+
const allowRightPan = panTriggers.includes('rightMouse');
|
|
15
|
+
const valueFont = useFont(stampFontSource, stampValueFontSize);
|
|
16
|
+
const labelFont = useFont(stampFontSource, stampLabelFontSize);
|
|
17
|
+
const state = useAnnotationCanvasState(props);
|
|
18
|
+
const containerRef = useRef(null);
|
|
19
|
+
const panGestureRef = useRef(null);
|
|
20
|
+
const spaceDownRef = useRef(false);
|
|
21
|
+
const activeTool = tools.find((t) => t.id === activeToolId) ?? null;
|
|
22
|
+
const toCanvasPointer = useCallback((event) => {
|
|
23
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
24
|
+
const screen = {
|
|
25
|
+
x: event.clientX - (rect?.left ?? 0),
|
|
26
|
+
y: event.clientY - (rect?.top ?? 0),
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
pointerId: event.pointerId,
|
|
30
|
+
screen,
|
|
31
|
+
world: state.ctx.viewport.screenToWorld(screen),
|
|
32
|
+
pressure: event.pressure || undefined,
|
|
33
|
+
shiftKey: event.shiftKey,
|
|
34
|
+
altKey: event.altKey,
|
|
35
|
+
metaKey: event.metaKey,
|
|
36
|
+
ctrlKey: event.ctrlKey,
|
|
37
|
+
};
|
|
38
|
+
}, [state.ctx.viewport]);
|
|
39
|
+
const isPanTriggerDown = useCallback((event) => {
|
|
40
|
+
if (allowMiddlePan && event.button === 1)
|
|
41
|
+
return true;
|
|
42
|
+
if (allowRightPan && event.button === 2)
|
|
43
|
+
return true;
|
|
44
|
+
if (allowSpacePan && event.button === 0 && spaceDownRef.current)
|
|
45
|
+
return true;
|
|
46
|
+
return false;
|
|
47
|
+
}, [allowMiddlePan, allowRightPan, allowSpacePan]);
|
|
48
|
+
const handlePointerDown = useCallback((event) => {
|
|
49
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
50
|
+
const screen = {
|
|
51
|
+
x: event.clientX - (rect?.left ?? 0),
|
|
52
|
+
y: event.clientY - (rect?.top ?? 0),
|
|
53
|
+
};
|
|
54
|
+
if (isPanTriggerDown(event)) {
|
|
55
|
+
panGestureRef.current = { pointerId: event.pointerId, lastScreen: screen };
|
|
56
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
61
|
+
state.dispatchPointerDown(toCanvasPointer(event));
|
|
62
|
+
}, [state, toCanvasPointer, isPanTriggerDown]);
|
|
63
|
+
const handlePointerMove = useCallback((event) => {
|
|
64
|
+
const pan = panGestureRef.current;
|
|
65
|
+
if (pan && event.pointerId === pan.pointerId) {
|
|
66
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
67
|
+
const screen = {
|
|
68
|
+
x: event.clientX - (rect?.left ?? 0),
|
|
69
|
+
y: event.clientY - (rect?.top ?? 0),
|
|
70
|
+
};
|
|
71
|
+
state.pan({
|
|
72
|
+
x: screen.x - pan.lastScreen.x,
|
|
73
|
+
y: screen.y - pan.lastScreen.y,
|
|
74
|
+
});
|
|
75
|
+
panGestureRef.current = { pointerId: pan.pointerId, lastScreen: screen };
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
state.dispatchPointerMove(toCanvasPointer(event));
|
|
79
|
+
}, [state, toCanvasPointer]);
|
|
80
|
+
const handlePointerUp = useCallback((event) => {
|
|
81
|
+
const pan = panGestureRef.current;
|
|
82
|
+
if (pan && event.pointerId === pan.pointerId) {
|
|
83
|
+
panGestureRef.current = null;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
state.dispatchPointerUp(toCanvasPointer(event));
|
|
87
|
+
}, [state, toCanvasPointer]);
|
|
88
|
+
const handlePointerCancel = useCallback(() => {
|
|
89
|
+
panGestureRef.current = null;
|
|
90
|
+
state.dispatchPointerCancel();
|
|
91
|
+
}, [state]);
|
|
92
|
+
const handleWheel = useCallback((event) => {
|
|
93
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
94
|
+
const focal = {
|
|
95
|
+
x: event.clientX - (rect?.left ?? 0),
|
|
96
|
+
y: event.clientY - (rect?.top ?? 0),
|
|
97
|
+
};
|
|
98
|
+
if (event.ctrlKey || event.metaKey) {
|
|
99
|
+
const factor = Math.exp(-event.deltaY / 200);
|
|
100
|
+
state.zoom(focal, state.ctx.viewport.state.zoom * factor);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (wheelMode === 'zoom') {
|
|
104
|
+
const factor = Math.exp(-event.deltaY / 300);
|
|
105
|
+
state.zoom(focal, state.ctx.viewport.state.zoom * factor);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (event.shiftKey) {
|
|
109
|
+
state.pan({ x: event.deltaY, y: event.deltaX });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
state.pan({ x: event.deltaX, y: event.deltaY });
|
|
113
|
+
}, [state, wheelMode]);
|
|
114
|
+
const handleContextMenu = useCallback((event) => {
|
|
115
|
+
if (allowRightPan)
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
}, [allowRightPan]);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const el = containerRef.current;
|
|
120
|
+
if (!el)
|
|
121
|
+
return;
|
|
122
|
+
const stop = (e) => {
|
|
123
|
+
if (e.target === el || el.contains(e.target)) {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
el.addEventListener('wheel', stop, { passive: false });
|
|
128
|
+
return () => el.removeEventListener('wheel', stop);
|
|
129
|
+
}, []);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!allowSpacePan)
|
|
132
|
+
return;
|
|
133
|
+
const onKeyDown = (e) => {
|
|
134
|
+
if (e.code === 'Space' && !e.repeat) {
|
|
135
|
+
spaceDownRef.current = true;
|
|
136
|
+
if (containerRef.current)
|
|
137
|
+
containerRef.current.style.cursor = 'grab';
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const onKeyUp = (e) => {
|
|
141
|
+
if (e.code === 'Space') {
|
|
142
|
+
spaceDownRef.current = false;
|
|
143
|
+
if (containerRef.current)
|
|
144
|
+
containerRef.current.style.cursor = '';
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
window.addEventListener('keydown', onKeyDown);
|
|
148
|
+
window.addEventListener('keyup', onKeyUp);
|
|
149
|
+
return () => {
|
|
150
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
151
|
+
window.removeEventListener('keyup', onKeyUp);
|
|
152
|
+
};
|
|
153
|
+
}, [allowSpacePan]);
|
|
154
|
+
const containerStyle = {
|
|
155
|
+
position: 'relative',
|
|
156
|
+
width,
|
|
157
|
+
height,
|
|
158
|
+
overflow: 'hidden',
|
|
159
|
+
touchAction: 'none',
|
|
160
|
+
userSelect: 'none',
|
|
161
|
+
cursor: activeTool?.cursor ?? 'default',
|
|
162
|
+
...style,
|
|
163
|
+
};
|
|
164
|
+
const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
|
|
165
|
+
const { renderMeasurementStamp, selection } = props;
|
|
166
|
+
return (_jsxs("div", { ref: containerRef, style: containerStyle, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerCancel, onWheel: handleWheel, onContextMenu: handleContextMenu, children: [AnnotationCanvasSkia({
|
|
167
|
+
width,
|
|
168
|
+
height,
|
|
169
|
+
effectiveCanvas: state.effectiveCanvas,
|
|
170
|
+
worldTransform: state.worldTransform,
|
|
171
|
+
measurementsById: state.measurementsById,
|
|
172
|
+
fallbackUnit,
|
|
173
|
+
fractionalTolerance,
|
|
174
|
+
decimalTolerance,
|
|
175
|
+
resolveImageUrl,
|
|
176
|
+
valueFont,
|
|
177
|
+
labelFont,
|
|
178
|
+
hideMeasurementStamps: !!renderMeasurementStamp,
|
|
179
|
+
penDrawingStroke: state.penDrawingStroke,
|
|
180
|
+
customPreview,
|
|
181
|
+
}), renderMeasurementStamp && (_jsx("div", { style: {
|
|
182
|
+
position: 'absolute',
|
|
183
|
+
inset: 0,
|
|
184
|
+
pointerEvents: 'none',
|
|
185
|
+
}, children: state.effectiveCanvas.placedMeasurements.map((placed) => {
|
|
186
|
+
const size = STAMP_TILE_SIZE * (placed.scale ?? 1);
|
|
187
|
+
const cx = (placed.anchor.x - state.viewport.pan.x) * state.viewport.zoom;
|
|
188
|
+
const cy = (placed.anchor.y - state.viewport.pan.y) * state.viewport.zoom;
|
|
189
|
+
const isSelected = selection?.ids.includes(placed.id) ?? false;
|
|
190
|
+
return (_jsxs("div", { style: {
|
|
191
|
+
position: 'absolute',
|
|
192
|
+
left: 0,
|
|
193
|
+
top: 0,
|
|
194
|
+
width: size,
|
|
195
|
+
height: size,
|
|
196
|
+
transform: `translate(${cx - size / 2}px, ${cy - size / 2}px)`,
|
|
197
|
+
}, children: [renderMeasurementStamp({
|
|
198
|
+
placed,
|
|
199
|
+
measurement: state.measurementsById.get(placed.measurementId) ?? null,
|
|
200
|
+
selected: isSelected,
|
|
201
|
+
size,
|
|
202
|
+
zoom: state.viewport.zoom,
|
|
203
|
+
}), isSelected && (_jsx("div", { role: "button", "aria-label": "Remove measurement", onPointerDown: (e) => {
|
|
204
|
+
e.stopPropagation();
|
|
205
|
+
state.ctx.commit({
|
|
206
|
+
ops: [{ op: 'removeMeasurement', id: placed.id }],
|
|
207
|
+
});
|
|
208
|
+
state.ctx.setSelection(null);
|
|
209
|
+
}, style: {
|
|
210
|
+
position: 'absolute',
|
|
211
|
+
top: -10,
|
|
212
|
+
right: -10,
|
|
213
|
+
width: 40,
|
|
214
|
+
height: 40,
|
|
215
|
+
cursor: 'pointer',
|
|
216
|
+
pointerEvents: 'auto',
|
|
217
|
+
} }))] }, placed.id));
|
|
218
|
+
}) }))] }));
|
|
219
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type MutableRefObject } from 'react';
|
|
2
|
+
import { type ViewStyle } from 'react-native';
|
|
3
|
+
import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
|
|
4
|
+
import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
|
|
5
|
+
import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
|
|
6
|
+
import type { MeasurementRef } from './measurementPicker.js';
|
|
7
|
+
import type { Tool } from './Tool.js';
|
|
8
|
+
import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
|
|
9
|
+
import type { ViewportState } from './viewport.js';
|
|
10
|
+
export type { AnnotationCanvasHandle };
|
|
11
|
+
export interface AnnotationCanvasInnerProps {
|
|
12
|
+
canvas: AnnotationCanvasState;
|
|
13
|
+
onCommit(patch: AnnotationDocumentPatch): void;
|
|
14
|
+
tools: Tool[];
|
|
15
|
+
activeToolId: string;
|
|
16
|
+
selection: Selection | null;
|
|
17
|
+
onSelectionChange(selection: Selection | null): void;
|
|
18
|
+
measurements?: Measurement[];
|
|
19
|
+
fallbackUnit?: Units;
|
|
20
|
+
fractionalTolerance?: FractionalTolerance;
|
|
21
|
+
decimalTolerance?: DecimalTolerance;
|
|
22
|
+
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
23
|
+
pickMeasurement?: () => Promise<MeasurementRef | null>;
|
|
24
|
+
renderMeasurementStamp?: RenderMeasurementStamp;
|
|
25
|
+
stampFontSource?: unknown;
|
|
26
|
+
stampValueFontSize?: number;
|
|
27
|
+
stampLabelFontSize?: number;
|
|
28
|
+
gestures?: unknown;
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
initialViewport?: ViewportState;
|
|
32
|
+
style?: ViewStyle;
|
|
33
|
+
imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
|
|
34
|
+
}
|
|
35
|
+
export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;
|