@reekon-tools/boldr-utils 1.21.1 → 1.22.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/AnnotationCanvasSkia.js +3 -11
- package/dist/annotation/canvas/measurementLineStyle.d.ts +3 -0
- package/dist/annotation/canvas/measurementLineStyle.js +14 -0
- package/dist/export/pdf/color.d.ts +7 -0
- package/dist/export/pdf/color.js +39 -0
- package/dist/export/pdf/emit.d.ts +37 -0
- package/dist/export/pdf/emit.js +129 -0
- package/dist/export/pdf/fonts.d.ts +38 -0
- package/dist/export/pdf/fonts.js +97 -0
- package/dist/export/pdf/index.d.ts +5 -0
- package/dist/export/pdf/index.js +5 -0
- package/dist/export/pdf/layers.d.ts +5 -0
- package/dist/export/pdf/layers.js +19 -0
- package/dist/export/pdf/marks.d.ts +20 -0
- package/dist/export/pdf/marks.js +336 -0
- package/dist/export/pdf/pageMapping.d.ts +49 -0
- package/dist/export/pdf/pageMapping.js +67 -0
- package/dist/export/pdf/renderCanvasToPdf.d.ts +2 -0
- package/dist/export/pdf/renderCanvasToPdf.js +201 -0
- package/dist/export/pdf/tileContent.d.ts +39 -0
- package/dist/export/pdf/tileContent.js +218 -0
- package/dist/export/pdf/tiles.d.ts +43 -0
- package/dist/export/pdf/tiles.js +392 -0
- package/dist/export/pdf/types.d.ts +101 -0
- package/dist/export/pdf/types.js +10 -0
- package/dist/exports.d.ts +1 -1
- package/dist/exports.js +1 -1
- package/dist/formulas/calculateFormula.d.ts +1 -1
- package/dist/formulas/calculateFormula.js +23 -10
- package/dist/formulas/evaluateFormula.d.ts +0 -10
- package/dist/formulas/evaluateFormula.js +1 -12
- package/dist/formulas/groupFormula.d.ts +43 -0
- package/dist/formulas/groupFormula.js +168 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.js +6 -1
- package/package.json +4 -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
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// Tile chrome: measurement tiles, header chips, object tiles, plane dimension
|
|
2
|
+
// chips and the blank input, drawn as PDF vectors. Each painter works in the
|
|
3
|
+
// tile's DESIGN space (the 96-unit STAMP_TILE_SIZE square, or a header's
|
|
4
|
+
// 200×56 / 90×65 box — the same natural-size-then-scale pattern the RN and
|
|
5
|
+
// DOM tiles use), and the renderer scales that box into the tile's doc-space
|
|
6
|
+
// footprint centred on the placed anchor.
|
|
7
|
+
//
|
|
8
|
+
// These are deliberately PDF-native renditions of the app's tiles — the same
|
|
9
|
+
// fills, type hierarchy and layout — not pixel captures of them.
|
|
10
|
+
import { clip, endPath, fill, popGraphicsState, pushGraphicsState, rectangle, stroke, } from 'pdf-lib';
|
|
11
|
+
import { FileUploadType } from '../../types/firestore.js';
|
|
12
|
+
import { STAMP_TILE_SIZE, stampTileDims, } from '../../annotation/canvas/stampLayout.js';
|
|
13
|
+
import { GROUP_HEADER_STYLE, } from '../../annotation/canvas/groupHeaderLayout.js';
|
|
14
|
+
import { FormColors, FormulaColors, GeneralColors, TileColors, } from '../../theme/colors.js';
|
|
15
|
+
import { circlePathOps, fillStateOps, imageOps, polylinePathOps, roundedRectPathOps, strokeStateOps, withMatrix, } from './emit.js';
|
|
16
|
+
const tileKind = (ref) => ref.fileId ? 'file' : ref.isGroupHeader ? 'header' : 'measurement';
|
|
17
|
+
/**
|
|
18
|
+
* Doc-space footprint of a placed tile. `fallbackDocEdge` is the doc-space
|
|
19
|
+
* edge a scale-1 measurement tile covers when neither the request nor the
|
|
20
|
+
* document carries a calibration.
|
|
21
|
+
*/
|
|
22
|
+
export const tileFootprint = (ref, canvas, calibration, fallbackDocEdge, design) => {
|
|
23
|
+
const kind = tileKind(ref);
|
|
24
|
+
const measurementEdge = calibration?.tileDocSize ?? canvas.tileDocSize ?? fallbackDocEdge;
|
|
25
|
+
const edge = kind === 'header'
|
|
26
|
+
? (calibration?.headerTileDocSize ?? measurementEdge)
|
|
27
|
+
: kind === 'file'
|
|
28
|
+
? (calibration?.fileTileDocSize ?? measurementEdge)
|
|
29
|
+
: measurementEdge;
|
|
30
|
+
// stampTileDims at unit factors and unit zoom = the base footprint × the
|
|
31
|
+
// tile's own `scale`; the calibration edge then converts base units to doc.
|
|
32
|
+
const base = stampTileDims(ref, 1, 1, 1, 1);
|
|
33
|
+
const k = edge / STAMP_TILE_SIZE;
|
|
34
|
+
const width = base.width * k;
|
|
35
|
+
const height = base.height * k;
|
|
36
|
+
return {
|
|
37
|
+
x: ref.anchor.x - width / 2,
|
|
38
|
+
y: ref.anchor.y - height / 2,
|
|
39
|
+
width,
|
|
40
|
+
height,
|
|
41
|
+
design,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export const designBoxFor = (ref, content) => content.kind === 'header'
|
|
45
|
+
? content.descriptor.design
|
|
46
|
+
: ref.isGroupHeader
|
|
47
|
+
? { width: 200, height: 56 }
|
|
48
|
+
: { width: STAMP_TILE_SIZE, height: STAMP_TILE_SIZE };
|
|
49
|
+
/** Doc ← design matrix for a footprint. */
|
|
50
|
+
export const designToDocMatrix = (f) => {
|
|
51
|
+
const k = f.width / f.design.width;
|
|
52
|
+
return [k, 0, 0, k, f.x, f.y];
|
|
53
|
+
};
|
|
54
|
+
/* ------------------------------------------------------------------ *
|
|
55
|
+
* Shared bits
|
|
56
|
+
* ------------------------------------------------------------------ */
|
|
57
|
+
const SQUARE = STAMP_TILE_SIZE;
|
|
58
|
+
const CARD_RADIUS = 6;
|
|
59
|
+
const filledRoundedRect = (x, y, w, h, r, color) => [
|
|
60
|
+
pushGraphicsState(),
|
|
61
|
+
...fillStateOps(color),
|
|
62
|
+
...roundedRectPathOps(x, y, w, h, r),
|
|
63
|
+
fill(),
|
|
64
|
+
popGraphicsState(),
|
|
65
|
+
];
|
|
66
|
+
const strokedRoundedRect = (x, y, w, h, r, color, width) => [
|
|
67
|
+
pushGraphicsState(),
|
|
68
|
+
...strokeStateOps({ color, width, join: 'miter' }),
|
|
69
|
+
...roundedRectPathOps(x + width / 2, y + width / 2, w - width, h - width, r),
|
|
70
|
+
stroke(),
|
|
71
|
+
popGraphicsState(),
|
|
72
|
+
];
|
|
73
|
+
/** Truncate to `maxWidth` with an ellipsis, like numberOfLines={1}. */
|
|
74
|
+
const truncate = (fonts, text, size, maxWidth) => {
|
|
75
|
+
if (fonts.width(text, size) <= maxWidth)
|
|
76
|
+
return text;
|
|
77
|
+
const ell = '…';
|
|
78
|
+
let t = text;
|
|
79
|
+
while (t.length > 0 && fonts.width(t + ell, size) > maxWidth)
|
|
80
|
+
t = t.slice(0, -1);
|
|
81
|
+
return t.length > 0 ? t + ell : '';
|
|
82
|
+
};
|
|
83
|
+
/* ------------------------------------------------------------------ *
|
|
84
|
+
* Measurement value row: tokens with stacked fractions
|
|
85
|
+
* ------------------------------------------------------------------ */
|
|
86
|
+
// Mirror of the mobile tileValueFont model at a 96-unit tile.
|
|
87
|
+
const DIGIT_ADVANCE_EM = 0.68;
|
|
88
|
+
const FRACTION_STACK_CHARS = 1.4;
|
|
89
|
+
const FIT_K = 0.9 / DIGIT_ADVANCE_EM;
|
|
90
|
+
const TILE_VALUE_MAX_TILE_SCALE = 0.8 / 3;
|
|
91
|
+
const UNIT_FONT = 12; // clamp(96 × 0.1, 12, 16)
|
|
92
|
+
const UNIT_RESERVE = Math.round(UNIT_FONT * (2 * 0.854 + 0.2));
|
|
93
|
+
const tokenize = (value) => value
|
|
94
|
+
.trim()
|
|
95
|
+
.split(/\s+/)
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.map((t) => {
|
|
98
|
+
if (!t.includes('/'))
|
|
99
|
+
return { text: t };
|
|
100
|
+
const [num, rest = ''] = t.split('/');
|
|
101
|
+
const den = rest.replace(/[^0-9]/g, '');
|
|
102
|
+
const suffix = rest.replace(/[0-9]/g, '');
|
|
103
|
+
return { text: t, fraction: { num: num ?? '', den, suffix } };
|
|
104
|
+
});
|
|
105
|
+
const effectiveLength = (tokens) => tokens.reduce((n, t, i) => n +
|
|
106
|
+
(i > 0 ? 1 : 0) +
|
|
107
|
+
(t.fraction
|
|
108
|
+
? FRACTION_STACK_CHARS + t.fraction.suffix.length
|
|
109
|
+
: t.text.length), 0);
|
|
110
|
+
/**
|
|
111
|
+
* Draw a converted value string centred at (cx, cy) in design units with
|
|
112
|
+
* whole parts large and fractions stacked over a rule. Returns the ops.
|
|
113
|
+
*/
|
|
114
|
+
const valueRowOps = (ctx, value, cx, cy, color, availableWidth, tileWidth) => {
|
|
115
|
+
const tokens = tokenize(value);
|
|
116
|
+
if (tokens.length === 0)
|
|
117
|
+
return [];
|
|
118
|
+
const chars = Math.max(effectiveLength(tokens), 1);
|
|
119
|
+
const fs = Math.min(TILE_VALUE_MAX_TILE_SCALE * tileWidth, (FIT_K / chars) * availableWidth);
|
|
120
|
+
const fracFs = fs * 0.8;
|
|
121
|
+
const gap = fs * 0.15;
|
|
122
|
+
// Measure the row.
|
|
123
|
+
const widths = tokens.map((t) => {
|
|
124
|
+
if (!t.fraction)
|
|
125
|
+
return ctx.fonts.width(t.text, fs);
|
|
126
|
+
const stack = Math.max(ctx.fonts.width(t.fraction.num, fracFs), ctx.fonts.width(t.fraction.den, fracFs));
|
|
127
|
+
return (stack + (t.fraction.suffix ? ctx.fonts.width(t.fraction.suffix, fs) : 0));
|
|
128
|
+
});
|
|
129
|
+
const total = widths.reduce((a, b) => a + b, 0) + gap * (tokens.length - 1);
|
|
130
|
+
let x = cx - total / 2;
|
|
131
|
+
const ops = [];
|
|
132
|
+
const baseline = cy + fs * 0.35; // cap-height centred on cy
|
|
133
|
+
tokens.forEach((t, i) => {
|
|
134
|
+
const w = widths[i];
|
|
135
|
+
if (!t.fraction) {
|
|
136
|
+
ops.push(...ctx.fonts.textOps(ctx.page, t.text, fs, color, x, baseline));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const stackW = Math.max(ctx.fonts.width(t.fraction.num, fracFs), ctx.fonts.width(t.fraction.den, fracFs));
|
|
140
|
+
const scx = x + stackW / 2;
|
|
141
|
+
// Numerator above the rule, denominator below; rule at the row centre.
|
|
142
|
+
ops.push(...ctx.fonts.textOps(ctx.page, t.fraction.num, fracFs, color, scx, cy - fracFs * 0.15, { align: 'center' }), pushGraphicsState(), ...fillStateOps(color), rectangle(x, cy - fracFs * 0.05, stackW, Math.max(fracFs * 0.08, 0.8)), fill(), popGraphicsState(), ...ctx.fonts.textOps(ctx.page, t.fraction.den, fracFs, color, scx, cy + fracFs * 0.85, { align: 'center' }));
|
|
143
|
+
if (t.fraction.suffix) {
|
|
144
|
+
ops.push(...ctx.fonts.textOps(ctx.page, t.fraction.suffix, fs, color, x + stackW, baseline));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
x += w + gap;
|
|
148
|
+
});
|
|
149
|
+
return ops;
|
|
150
|
+
};
|
|
151
|
+
/* ------------------------------------------------------------------ *
|
|
152
|
+
* Painters (design space, y-down)
|
|
153
|
+
* ------------------------------------------------------------------ */
|
|
154
|
+
const measurementTileOps = (ctx, c) => {
|
|
155
|
+
const ops = filledRoundedRect(0, 0, SQUARE, SQUARE, CARD_RADIUS, c.fill);
|
|
156
|
+
const white = GeneralColors.white;
|
|
157
|
+
// Header row: position label, bold, top-left.
|
|
158
|
+
if (c.positionLabel) {
|
|
159
|
+
ops.push(...ctx.fonts.textOps(ctx.page, c.positionLabel, 12, white, 6, 14));
|
|
160
|
+
}
|
|
161
|
+
// Value row, centred on the tile, between the two unit slots.
|
|
162
|
+
const hasUnit = !!c.unit;
|
|
163
|
+
const available = hasUnit ? SQUARE - 2 * UNIT_RESERVE : SQUARE - 8;
|
|
164
|
+
const valueText = (c.negative ? '-' : '') + c.value;
|
|
165
|
+
ops.push(...valueRowOps(ctx, valueText, SQUARE / 2, 48, white, available, SQUARE));
|
|
166
|
+
if (hasUnit) {
|
|
167
|
+
ops.push(...ctx.fonts.textOps(ctx.page, c.unit, UNIT_FONT, white, SQUARE - UNIT_RESERVE / 2, 52, {
|
|
168
|
+
align: 'center',
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
// Bottom label: bare white ink, or on the formula tile's dark-magenta bar.
|
|
172
|
+
if (c.label) {
|
|
173
|
+
const labelSize = 11;
|
|
174
|
+
if (c.formulaBar) {
|
|
175
|
+
ops.push(...filledRoundedRect(4, SQUARE - 20, SQUARE - 8, 16, 3, FormulaColors.darkPurple), ...strokedRoundedRect(4, SQUARE - 20, SQUARE - 8, 16, 3, FormulaColors.purple, 1));
|
|
176
|
+
}
|
|
177
|
+
const text = truncate(ctx.fonts, c.label, labelSize, SQUARE - 12);
|
|
178
|
+
ops.push(...ctx.fonts.textOps(ctx.page, text, labelSize, white, SQUARE / 2, SQUARE - 8, { align: 'center' }));
|
|
179
|
+
}
|
|
180
|
+
return ops;
|
|
181
|
+
};
|
|
182
|
+
const emptyTileOps = (ctx, c) => {
|
|
183
|
+
const ink = FormColors.lightGrey1;
|
|
184
|
+
const ops = [
|
|
185
|
+
...filledRoundedRect(0, 0, SQUARE, SQUARE, CARD_RADIUS, GeneralColors.grey2),
|
|
186
|
+
...strokedRoundedRect(0, 0, SQUARE, SQUARE, CARD_RADIUS, GeneralColors.grey3, 1),
|
|
187
|
+
];
|
|
188
|
+
if (c.topLeftLabel)
|
|
189
|
+
ops.push(...ctx.fonts.textOps(ctx.page, c.topLeftLabel, 10, ink, 8, 18));
|
|
190
|
+
if (c.topRightLabel) {
|
|
191
|
+
ops.push(...ctx.fonts.textOps(ctx.page, c.topRightLabel, 10, ink, SQUARE - 8, 18, {
|
|
192
|
+
align: 'right',
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
// The "+" well.
|
|
196
|
+
ops.push(...filledRoundedRect(8, 28, SQUARE - 16, 40, 4, GeneralColors.grey3));
|
|
197
|
+
const cx = SQUARE / 2;
|
|
198
|
+
const cy = 48;
|
|
199
|
+
ops.push(pushGraphicsState(), ...strokeStateOps({ color: ink, width: 2.5, cap: 'round' }), ...polylinePathOps([
|
|
200
|
+
{ x: cx - 8, y: cy },
|
|
201
|
+
{ x: cx + 8, y: cy },
|
|
202
|
+
]), stroke(), ...polylinePathOps([
|
|
203
|
+
{ x: cx, y: cy - 8 },
|
|
204
|
+
{ x: cx, y: cy + 8 },
|
|
205
|
+
]), stroke(), popGraphicsState());
|
|
206
|
+
if (c.bottomLabel) {
|
|
207
|
+
const text = truncate(ctx.fonts, c.bottomLabel, 10, SQUARE - 16);
|
|
208
|
+
ops.push(...ctx.fonts.textOps(ctx.page, text, 10, ink, cx, SQUARE - 10, {
|
|
209
|
+
align: 'center',
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
return ops;
|
|
213
|
+
};
|
|
214
|
+
const literalTileOps = (ctx, c) => {
|
|
215
|
+
const filled = c.value !== '';
|
|
216
|
+
const fillColor = filled ? TileColors.activeBlue : GeneralColors.grey2;
|
|
217
|
+
const ink = filled ? GeneralColors.white : GeneralColors.grey4;
|
|
218
|
+
const ops = filledRoundedRect(0, 0, SQUARE, SQUARE, CARD_RADIUS, fillColor);
|
|
219
|
+
ops.push(...ctx.fonts.textOps(ctx.page, c.topLeftLabel, 10, ink, 8, 18));
|
|
220
|
+
ops.push(...ctx.fonts.textOps(ctx.page, c.topRightLabel, 10, ink, SQUARE - 8, 18, {
|
|
221
|
+
align: 'right',
|
|
222
|
+
}));
|
|
223
|
+
ops.push(...filledRoundedRect(8, 28, SQUARE - 16, 40, 4, filled ? TileColors.activeBlue : GeneralColors.grey3));
|
|
224
|
+
if (filled) {
|
|
225
|
+
const size = ctx.fonts.fitSize(c.value, SQUARE - 24, 16, 6);
|
|
226
|
+
ops.push(...ctx.fonts.textOps(ctx.page, c.value, size, ink, SQUARE / 2, 48 + size * 0.35, { align: 'center' }));
|
|
227
|
+
}
|
|
228
|
+
const bottom = truncate(ctx.fonts, c.bottomLabel, 10, SQUARE - 16);
|
|
229
|
+
ops.push(...ctx.fonts.textOps(ctx.page, bottom, 10, ink, SQUARE / 2, SQUARE - 10, {
|
|
230
|
+
align: 'center',
|
|
231
|
+
}));
|
|
232
|
+
return ops;
|
|
233
|
+
};
|
|
234
|
+
const globeOps = (cx, cy, size, color) => {
|
|
235
|
+
const r = size / 2 - 1;
|
|
236
|
+
return [
|
|
237
|
+
pushGraphicsState(),
|
|
238
|
+
...strokeStateOps({ color, width: 1.75, join: 'round' }),
|
|
239
|
+
...circlePathOps(cx, cy, r),
|
|
240
|
+
stroke(),
|
|
241
|
+
...polylinePathOps([
|
|
242
|
+
{ x: cx - r, y: cy },
|
|
243
|
+
{ x: cx + r, y: cy },
|
|
244
|
+
]),
|
|
245
|
+
stroke(),
|
|
246
|
+
// Meridian as a narrow ellipse: scale x by 0.45 around cx.
|
|
247
|
+
...withMatrix([0.45, 0, 0, 1, cx * (1 - 0.45), 0], [...circlePathOps(cx, cy, r), stroke()]),
|
|
248
|
+
popGraphicsState(),
|
|
249
|
+
];
|
|
250
|
+
};
|
|
251
|
+
const headerTileOps = (ctx, c) => {
|
|
252
|
+
const d = c.descriptor;
|
|
253
|
+
const S = GROUP_HEADER_STYLE;
|
|
254
|
+
const W = d.design.width;
|
|
255
|
+
const H = d.design.height;
|
|
256
|
+
const ops = filledRoundedRect(0, 0, W, H, S.radius, S.background);
|
|
257
|
+
const inner = W - 2 * S.padding;
|
|
258
|
+
const nameBlock = d.name !== null ? S.nameGap + S.nameFontSize * 1.2 : 0;
|
|
259
|
+
const badgesBlock = d.collapsed
|
|
260
|
+
? d.badges.length * S.badgeHeight +
|
|
261
|
+
Math.max(0, d.badges.length - 1) * S.badgeGapCollapsed
|
|
262
|
+
: d.badges.length > 0
|
|
263
|
+
? S.badgeHeight
|
|
264
|
+
: 0;
|
|
265
|
+
const contentH = badgesBlock + nameBlock;
|
|
266
|
+
let y = (H - contentH) / 2;
|
|
267
|
+
if (d.badges.length > 0) {
|
|
268
|
+
if (d.collapsed) {
|
|
269
|
+
d.badges.forEach((b, i) => {
|
|
270
|
+
const by = y + i * (S.badgeHeight + S.badgeGapCollapsed);
|
|
271
|
+
ops.push(...filledRoundedRect(S.padding, by, inner, S.badgeHeight, S.badgeRadius, b.background));
|
|
272
|
+
if (d.showGlobe)
|
|
273
|
+
ops.push(...globeOps(W / 2, by + S.badgeHeight / 2, S.globeSize, S.globeColor));
|
|
274
|
+
else {
|
|
275
|
+
const text = truncate(ctx.fonts, b.text, S.badgeFontSize, inner - 10);
|
|
276
|
+
ops.push(...ctx.fonts.textOps(ctx.page, text, S.badgeFontSize, S.badgeTextColor, W / 2, by + S.badgeHeight / 2 + S.badgeFontSize * 0.35, { align: 'center' }));
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
const n = d.badges.length;
|
|
282
|
+
const bw = (inner - S.badgeGapExpanded * (n - 1)) / n;
|
|
283
|
+
d.badges.forEach((b, i) => {
|
|
284
|
+
const bx = S.padding + i * (bw + S.badgeGapExpanded);
|
|
285
|
+
ops.push(...filledRoundedRect(bx, y, bw, S.badgeHeight, S.badgeRadius, b.background));
|
|
286
|
+
if (d.showGlobe)
|
|
287
|
+
ops.push(...globeOps(bx + bw / 2, y + S.badgeHeight / 2, S.globeSize, S.globeColor));
|
|
288
|
+
else {
|
|
289
|
+
const text = truncate(ctx.fonts, b.text, S.badgeFontSize, bw - 10);
|
|
290
|
+
ops.push(...ctx.fonts.textOps(ctx.page, text, S.badgeFontSize, S.badgeTextColor, bx + bw / 2, y + S.badgeHeight / 2 + S.badgeFontSize * 0.35, { align: 'center' }));
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
y += badgesBlock;
|
|
295
|
+
}
|
|
296
|
+
if (d.name !== null) {
|
|
297
|
+
const text = truncate(ctx.fonts, d.name, S.nameFontSize, inner);
|
|
298
|
+
ops.push(...ctx.fonts.textOps(ctx.page, text, S.nameFontSize, S.nameColor, W / 2, y + S.nameGap + S.nameFontSize * 0.95, { align: 'center' }));
|
|
299
|
+
}
|
|
300
|
+
return ops;
|
|
301
|
+
};
|
|
302
|
+
const OBJECT_INSET = 4;
|
|
303
|
+
const OBJECT_BAR_HEIGHT = 20;
|
|
304
|
+
const objectTileOps = (ctx, c) => {
|
|
305
|
+
const x = OBJECT_INSET;
|
|
306
|
+
const y = OBJECT_INSET;
|
|
307
|
+
const w = SQUARE - 2 * OBJECT_INSET;
|
|
308
|
+
const h = SQUARE - 2 * OBJECT_INSET;
|
|
309
|
+
const ops = [];
|
|
310
|
+
const thumb = ctx.thumbnail;
|
|
311
|
+
if (!thumb) {
|
|
312
|
+
ops.push(...filledRoundedRect(x, y, w, h, CARD_RADIUS, GeneralColors.grey2));
|
|
313
|
+
const name = truncate(ctx.fonts, c.name || 'Object removed', 9, w - 8);
|
|
314
|
+
ops.push(...ctx.fonts.textOps(ctx.page, name, 9, GeneralColors.grey4, SQUARE / 2, SQUARE / 2 + 3, { align: 'center' }));
|
|
315
|
+
return ops;
|
|
316
|
+
}
|
|
317
|
+
// Thumbnail, cover-fit and clipped to the rounded card.
|
|
318
|
+
const scale = Math.max(w / thumb.width, h / thumb.height);
|
|
319
|
+
const iw = thumb.width * scale;
|
|
320
|
+
const ih = thumb.height * scale;
|
|
321
|
+
ops.push(pushGraphicsState(), ...roundedRectPathOps(x, y, w, h, CARD_RADIUS), clip(), endPath(), ...imageOps(thumb.name, x + (w - iw) / 2, y + (h - ih) / 2, iw, ih), popGraphicsState());
|
|
322
|
+
// Footer bar by object type: a layout group gets the magenta bar with white
|
|
323
|
+
// ink, a plain photo/background none, every document a white bar with dark ink.
|
|
324
|
+
const type = c.fileType;
|
|
325
|
+
const bar = type === FileUploadType.LayoutGroup
|
|
326
|
+
? { fill: FormulaColors.darkPurple, ink: GeneralColors.white }
|
|
327
|
+
: type === FileUploadType.Background || type === undefined
|
|
328
|
+
? null
|
|
329
|
+
: { fill: GeneralColors.white, ink: GeneralColors.darkGrey };
|
|
330
|
+
if (bar) {
|
|
331
|
+
ops.push(pushGraphicsState(), ...roundedRectPathOps(x, y, w, h, CARD_RADIUS), clip(), endPath(), ...fillStateOps(bar.fill), rectangle(x, y + h - OBJECT_BAR_HEIGHT, w, OBJECT_BAR_HEIGHT), fill(), popGraphicsState());
|
|
332
|
+
const name = truncate(ctx.fonts, c.name, 9, w - 8);
|
|
333
|
+
ops.push(...ctx.fonts.textOps(ctx.page, name, 9, bar.ink, SQUARE / 2, y + h - OBJECT_BAR_HEIGHT / 2 + 3, { align: 'center' }));
|
|
334
|
+
}
|
|
335
|
+
return ops;
|
|
336
|
+
};
|
|
337
|
+
const planeChipOps = (ctx, c) => {
|
|
338
|
+
const ops = filledRoundedRect(0, 0, SQUARE, SQUARE, 8, TileColors.activeBlue);
|
|
339
|
+
const usable = SQUARE - 22;
|
|
340
|
+
const text = c.unit ? `${c.value} ${c.unit}` : c.value;
|
|
341
|
+
ops.push(...valueRowOps(ctx, text, SQUARE / 2, SQUARE / 2, GeneralColors.white, usable, SQUARE));
|
|
342
|
+
return ops;
|
|
343
|
+
};
|
|
344
|
+
const instructionsTileOps = (ctx, c) => {
|
|
345
|
+
const ops = filledRoundedRect(0, 0, SQUARE, SQUARE, 8, c.backgroundColor || '#EDEDED');
|
|
346
|
+
const color = c.textColor || '#2D3843';
|
|
347
|
+
const size = 11;
|
|
348
|
+
const maxW = SQUARE - 16;
|
|
349
|
+
// Word-wrap to at most four lines, like numberOfLines={4}.
|
|
350
|
+
const lines = [];
|
|
351
|
+
let current = '';
|
|
352
|
+
for (const word of c.text.split(/\s+/).filter(Boolean)) {
|
|
353
|
+
const next = current ? `${current} ${word}` : word;
|
|
354
|
+
if (ctx.fonts.width(next, size) <= maxW || !current)
|
|
355
|
+
current = next;
|
|
356
|
+
else {
|
|
357
|
+
lines.push(current);
|
|
358
|
+
current = word;
|
|
359
|
+
}
|
|
360
|
+
if (lines.length === 4)
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
if (current && lines.length < 4)
|
|
364
|
+
lines.push(current);
|
|
365
|
+
const lh = size * 1.25;
|
|
366
|
+
const top = SQUARE / 2 - (lines.length * lh) / 2;
|
|
367
|
+
lines.forEach((line, i) => {
|
|
368
|
+
ops.push(...ctx.fonts.textOps(ctx.page, truncate(ctx.fonts, line, size, maxW), size, color, SQUARE / 2, top + i * lh + size * 0.9, { align: 'center' }));
|
|
369
|
+
});
|
|
370
|
+
return ops;
|
|
371
|
+
};
|
|
372
|
+
/** Operators for a tile in DESIGN space. */
|
|
373
|
+
export const tileDesignOps = (ctx, content) => {
|
|
374
|
+
switch (content.kind) {
|
|
375
|
+
case 'measurement':
|
|
376
|
+
return measurementTileOps(ctx, content);
|
|
377
|
+
case 'empty':
|
|
378
|
+
return emptyTileOps(ctx, content);
|
|
379
|
+
case 'literal':
|
|
380
|
+
return literalTileOps(ctx, content);
|
|
381
|
+
case 'header':
|
|
382
|
+
return headerTileOps(ctx, content);
|
|
383
|
+
case 'object':
|
|
384
|
+
return objectTileOps(ctx, content);
|
|
385
|
+
case 'planeDimension':
|
|
386
|
+
return planeChipOps(ctx, content);
|
|
387
|
+
case 'instructions':
|
|
388
|
+
return instructionsTileOps(ctx, content);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
/** Operators for a tile in DOC space: design ops under the footprint matrix. */
|
|
392
|
+
export const tileOps = (ctx, content, footprint) => withMatrix(designToDocMatrix(footprint), tileDesignOps(ctx, content));
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { GroupHeaderDescriptor } from '../../annotation/canvas/groupHeaderLayout.js';
|
|
2
|
+
import type { AnnotationCanvasState, PlacedMeasurementRef } from '../../types/annotation.js';
|
|
3
|
+
export interface EmbeddedImage {
|
|
4
|
+
bytes: Uint8Array;
|
|
5
|
+
mime: 'image/png' | 'image/jpeg';
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Doc-space edge length (PDF points) a scale-1 measurement tile covers, and
|
|
9
|
+
* the same for header chips and object tiles. Tile footprints on screen are
|
|
10
|
+
* zoom-relative, so only the client that is exporting knows how large its
|
|
11
|
+
* tiles are against the drawing; it computes these with
|
|
12
|
+
* `tileDocSizeForFactor(resolvedFactor, baseFitZoom)` (stampLayout.ts) and
|
|
13
|
+
* sends them along. Absent fields fall back to the persisted `tileDocSize`,
|
|
14
|
+
* then to a default derived from the first page.
|
|
15
|
+
*/
|
|
16
|
+
export interface PdfExportCalibration {
|
|
17
|
+
tileDocSize?: number;
|
|
18
|
+
headerTileDocSize?: number;
|
|
19
|
+
fileTileDocSize?: number;
|
|
20
|
+
}
|
|
21
|
+
/** What a measurement / angle / formula tile displays. */
|
|
22
|
+
export interface MeasurementTileContent {
|
|
23
|
+
kind: 'measurement';
|
|
24
|
+
/** Converted display string, e.g. "12 3/16", "5' 6 1/2\"", "45.2°". */
|
|
25
|
+
value: string;
|
|
26
|
+
/** Unit column text ("in", "mm", "ft"); empty for scalars. */
|
|
27
|
+
unit: string;
|
|
28
|
+
/** Header-row identifier ("A3", "B12"); empty hides it. */
|
|
29
|
+
positionLabel?: string;
|
|
30
|
+
/** Bottom label (measurement label / column name / override). */
|
|
31
|
+
label?: string;
|
|
32
|
+
/** Card fill (measurementTileFill precedence already applied). */
|
|
33
|
+
fill: string;
|
|
34
|
+
/** Formula tiles carry their label on a dark-magenta bar. */
|
|
35
|
+
formulaBar?: boolean;
|
|
36
|
+
negative?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** A FORM column holding a literal (text / number / select / boolean). */
|
|
39
|
+
export interface LiteralTileContent {
|
|
40
|
+
kind: 'literal';
|
|
41
|
+
value: string;
|
|
42
|
+
topLeftLabel: string;
|
|
43
|
+
topRightLabel: string;
|
|
44
|
+
bottomLabel: string;
|
|
45
|
+
}
|
|
46
|
+
/** The blank "+" input tile. */
|
|
47
|
+
export interface EmptyTileContent {
|
|
48
|
+
kind: 'empty';
|
|
49
|
+
topLeftLabel?: string;
|
|
50
|
+
topRightLabel?: string;
|
|
51
|
+
bottomLabel?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface HeaderTileContent {
|
|
54
|
+
kind: 'header';
|
|
55
|
+
descriptor: GroupHeaderDescriptor;
|
|
56
|
+
}
|
|
57
|
+
export interface ObjectTileContent {
|
|
58
|
+
kind: 'object';
|
|
59
|
+
name: string;
|
|
60
|
+
/** FileUploadType value; decides the footer bar treatment. */
|
|
61
|
+
fileType?: string;
|
|
62
|
+
thumbnail?: EmbeddedImage;
|
|
63
|
+
}
|
|
64
|
+
export interface PlaneDimensionTileContent {
|
|
65
|
+
kind: 'planeDimension';
|
|
66
|
+
value: string;
|
|
67
|
+
unit: string;
|
|
68
|
+
}
|
|
69
|
+
export interface InstructionsTileContent {
|
|
70
|
+
kind: 'instructions';
|
|
71
|
+
text: string;
|
|
72
|
+
textColor?: string;
|
|
73
|
+
backgroundColor?: string;
|
|
74
|
+
}
|
|
75
|
+
export type ResolvedTileContent = MeasurementTileContent | LiteralTileContent | EmptyTileContent | HeaderTileContent | ObjectTileContent | PlaneDimensionTileContent | InstructionsTileContent;
|
|
76
|
+
export interface SkippedItem {
|
|
77
|
+
id: string;
|
|
78
|
+
kind: 'layer' | 'tile' | 'shape' | 'stroke' | 'text';
|
|
79
|
+
reason: string;
|
|
80
|
+
}
|
|
81
|
+
export interface RenderCanvasToPdfInput {
|
|
82
|
+
/** The original PDF the canvas is drawn over. */
|
|
83
|
+
sourcePdf: Uint8Array;
|
|
84
|
+
canvas: AnnotationCanvasState;
|
|
85
|
+
/** Content for each placed tile; `undefined` renders the blank tile. */
|
|
86
|
+
resolveTile: (ref: PlacedMeasurementRef) => ResolvedTileContent | undefined;
|
|
87
|
+
/** Decoded bytes per raster background layer id. Missing → skipped. */
|
|
88
|
+
layerImages: Record<string, EmbeddedImage>;
|
|
89
|
+
calibration?: PdfExportCalibration;
|
|
90
|
+
/** TTF/OTF bytes of the primary font (Metropolis Bold in the apps). */
|
|
91
|
+
primaryFont: Uint8Array;
|
|
92
|
+
}
|
|
93
|
+
export interface RenderCanvasToPdfResult {
|
|
94
|
+
pdf: Uint8Array;
|
|
95
|
+
pageCount: number;
|
|
96
|
+
skipped: SkippedItem[];
|
|
97
|
+
}
|
|
98
|
+
export declare class PdfExportError extends Error {
|
|
99
|
+
readonly code: 'no-pdf-document' | 'page-count-mismatch' | 'page-geometry-mismatch' | 'encrypted' | 'corrupt';
|
|
100
|
+
constructor(code: PdfExportError['code'], message: string);
|
|
101
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Public types of the canvas → PDF exporter. The renderer is PURE: it never
|
|
2
|
+
// touches Firestore or Storage. Whoever calls it (the Cloud Function) resolves
|
|
3
|
+
// every tile's content and every layer's bytes first and hands them in.
|
|
4
|
+
export class PdfExportError extends Error {
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'PdfExportError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
package/dist/exports.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache,
|
|
1
|
+
export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, type EnhancedMapping, type FormulaDefinition, type FormulaEvaluationOptions, } from './formulas/evaluateFormula.js';
|
|
2
2
|
export { calculateFormula } from './formulas/calculateFormula.js';
|
|
3
3
|
export { convertMicrometers } from './utils/micrometersToUnit.js';
|
|
4
4
|
export { parseMeasurement } from './utils/parseMeasurement.js';
|
package/dist/exports.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// platform-specific `AnnotationCanvas` wrapper lives here. The two index
|
|
3
3
|
// files (index.ts, index.native.ts) re-export from this and add their own
|
|
4
4
|
// AnnotationCanvas import.
|
|
5
|
-
export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache,
|
|
5
|
+
export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, } from './formulas/evaluateFormula.js';
|
|
6
6
|
export { calculateFormula } from './formulas/calculateFormula.js';
|
|
7
7
|
export { convertMicrometers } from './utils/micrometersToUnit.js';
|
|
8
8
|
export { parseMeasurement } from './utils/parseMeasurement.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { ColumnConfig, DecimalTolerance, FractionalTolerance, Formula, Measurement, Units } from '../types/firestore.js';
|
|
2
|
-
export declare const calculateFormula: (formula: Formula, formulas: Formula[], columns: Record<string, string>, tableConfig: ColumnConfig[], measurements: Measurement[], unit: Units,
|
|
2
|
+
export declare const calculateFormula: (formula: Formula, formulas: Formula[], columns: Record<string, string>, tableConfig: ColumnConfig[], measurements: Measurement[], unit: Units, fractionalTolerance: FractionalTolerance, decimalTolerance: DecimalTolerance) => string | number | null;
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { ColumnType, } from '../types/firestore.js';
|
|
2
|
-
import { evaluateFormula
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { evaluateFormula } from './evaluateFormula.js';
|
|
3
|
+
import { convertMicrometers } from '../utils/micrometersToUnit.js';
|
|
4
|
+
function parseMixedNumber(str) {
|
|
5
|
+
const parts = str.trim().split(' ');
|
|
6
|
+
let whole = 0;
|
|
7
|
+
let fraction = 0;
|
|
8
|
+
if (parts.length === 2) {
|
|
9
|
+
whole = parseFloat(parts[0]);
|
|
10
|
+
const [num, den] = parts[1].split('/').map(Number);
|
|
11
|
+
fraction = num / den;
|
|
12
|
+
}
|
|
13
|
+
else if (parts[0].includes('/')) {
|
|
14
|
+
const [num, den] = parts[0].split('/').map(Number);
|
|
15
|
+
fraction = num / den;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
whole = parseFloat(parts[0]);
|
|
19
|
+
}
|
|
20
|
+
return whole + fraction;
|
|
21
|
+
}
|
|
22
|
+
export const calculateFormula = (formula, formulas, columns, tableConfig, measurements, unit, fractionalTolerance, decimalTolerance) => {
|
|
7
23
|
// Convert Firestore Map to plain object if necessary
|
|
8
24
|
let variableToColumnMap = {};
|
|
9
25
|
if (formula.variableToColumnMap) {
|
|
@@ -29,11 +45,8 @@ _fractionalTolerance, _decimalTolerance) => {
|
|
|
29
45
|
const measurement = measurements.find((m) => m.id === measurementId);
|
|
30
46
|
if (!measurement)
|
|
31
47
|
continue;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// to the display tolerance and returned NaN / dropped the inches for the
|
|
35
|
-
// composite feet-inches formats (`5' 3 1/2"`).
|
|
36
|
-
valueMap[columnId] = micrometersToUnitNumber(measurement.value, unit);
|
|
48
|
+
const { value, unit: displayUnit } = convertMicrometers(measurement.value, unit, fractionalTolerance, decimalTolerance);
|
|
49
|
+
valueMap[columnId] = parseMixedNumber(value);
|
|
37
50
|
}
|
|
38
51
|
else if (column.type === ColumnType.Angle) {
|
|
39
52
|
const measurement = measurements.find((m) => m.id === measurementId);
|
|
@@ -17,16 +17,6 @@ export interface FormulaEvaluationOptions {
|
|
|
17
17
|
formulas?: FormulaDefinition[];
|
|
18
18
|
scope?: Record<string, number>;
|
|
19
19
|
}
|
|
20
|
-
export declare function normalizeUnitForMathJS(unit: Units | string): string;
|
|
21
|
-
/**
|
|
22
|
-
* Convert a stored micrometer value to the number the formula scope expects
|
|
23
|
-
* for `unit` — the same conversion nested-formula results go through. This is
|
|
24
|
-
* the ONLY sanctioned way to put a measurement into a valueMap: converting via
|
|
25
|
-
* the display STRING (convertMicrometers + a string parser) both rounds the
|
|
26
|
-
* value to the display tolerance and falls apart on composite formats like
|
|
27
|
-
* `5' 3 1/2"`.
|
|
28
|
-
*/
|
|
29
|
-
export declare function micrometersToUnitNumber(micrometers: number, unit: Units): number;
|
|
30
20
|
export declare function evaluateFormula({ expression, mappings, valueMap, unit, formulas, scope, }: FormulaEvaluationOptions): number | string | null;
|
|
31
21
|
export declare function createFormulaScope({ mappings, valueMap, unit, }: {
|
|
32
22
|
mappings: Record<string, string>;
|
|
@@ -7,7 +7,7 @@ const compile = math.compile.bind(math);
|
|
|
7
7
|
const mathUnit = math.unit.bind(math);
|
|
8
8
|
// Cache for evaluated formulas to improve performance
|
|
9
9
|
const formulaCache = new Map();
|
|
10
|
-
|
|
10
|
+
function normalizeUnitForMathJS(unit) {
|
|
11
11
|
switch (unit) {
|
|
12
12
|
case Units.Centimeters:
|
|
13
13
|
case 'cm':
|
|
@@ -30,17 +30,6 @@ export function normalizeUnitForMathJS(unit) {
|
|
|
30
30
|
return 'mm'; // fallback
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Convert a stored micrometer value to the number the formula scope expects
|
|
35
|
-
* for `unit` — the same conversion nested-formula results go through. This is
|
|
36
|
-
* the ONLY sanctioned way to put a measurement into a valueMap: converting via
|
|
37
|
-
* the display STRING (convertMicrometers + a string parser) both rounds the
|
|
38
|
-
* value to the display tolerance and falls apart on composite formats like
|
|
39
|
-
* `5' 3 1/2"`.
|
|
40
|
-
*/
|
|
41
|
-
export function micrometersToUnitNumber(micrometers, unit) {
|
|
42
|
-
return mathUnit(micrometers, 'um').toNumber(normalizeUnitForMathJS(unit));
|
|
43
|
-
}
|
|
44
33
|
// Normalize mapping to EnhancedMapping format
|
|
45
34
|
function normalizeMapping(mapping) {
|
|
46
35
|
if (typeof mapping === 'string') {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Units } from '../types/firestore.js';
|
|
2
|
+
/** Frame types of the stack-expression formula the apps persist. */
|
|
3
|
+
export declare enum GroupFormulaFrameType {
|
|
4
|
+
Operator = "operator",
|
|
5
|
+
Number = "number",
|
|
6
|
+
Paren = "paren",
|
|
7
|
+
Fraction = "fraction",
|
|
8
|
+
FeetInches = "feetInches",
|
|
9
|
+
MeasurementObj = "measurement",
|
|
10
|
+
InspectionInput = "InspectionInput",
|
|
11
|
+
Func = "func",
|
|
12
|
+
Output = "output"
|
|
13
|
+
}
|
|
14
|
+
export interface GroupFormulaFraction {
|
|
15
|
+
int: string;
|
|
16
|
+
numerator: string;
|
|
17
|
+
denominator: string;
|
|
18
|
+
}
|
|
19
|
+
export interface GroupFormulaFeetInches {
|
|
20
|
+
feet: string;
|
|
21
|
+
int: string;
|
|
22
|
+
numerator: string;
|
|
23
|
+
denominator: string;
|
|
24
|
+
}
|
|
25
|
+
export interface GroupFormulaFrame {
|
|
26
|
+
type: GroupFormulaFrameType | string;
|
|
27
|
+
value: unknown;
|
|
28
|
+
measurement?: {
|
|
29
|
+
value: number;
|
|
30
|
+
};
|
|
31
|
+
func?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare const micrometersToUnitNumber: (um: number, unit: Units) => number;
|
|
34
|
+
export declare const unitNumberToMicrometers: (value: number, unit: Units) => number;
|
|
35
|
+
/** The mathjs expression string a stack formula evaluates to, for `valueUm`. */
|
|
36
|
+
export declare const groupFormulaExpression: (frames: GroupFormulaFrame[], valueUm: number, unit: Units | undefined) => string;
|
|
37
|
+
/**
|
|
38
|
+
* Apply a group's stored `formula` to a measurement value (micrometers) and
|
|
39
|
+
* return the result in micrometers, or null when the group has no formula or
|
|
40
|
+
* the expression does not evaluate to a finite number (the caller falls back
|
|
41
|
+
* to the raw value, exactly like the tiles do).
|
|
42
|
+
*/
|
|
43
|
+
export declare const applyGroupFormulaUm: (formula: unknown, valueUm: number, unit: Units | undefined) => number | null;
|