@reekon-tools/boldr-utils 1.6.39 → 1.7.1
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.native.js +71 -2
- package/dist/annotation/canvas/tools/measurementLineTool.d.ts +12 -0
- package/dist/annotation/canvas/tools/measurementLineTool.js +95 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +11 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +97 -28
- package/dist/annotation/data/AnnotationDataProvider.d.ts +6 -1
- package/dist/annotation/data/AnnotationDataProvider.js +4 -1
- package/dist/calculator/evaluate.d.ts +49 -0
- package/dist/calculator/evaluate.js +207 -0
- package/dist/calculator/index.d.ts +6 -0
- package/dist/calculator/index.js +10 -0
- package/dist/calculator/instance.d.ts +36 -0
- package/dist/calculator/schema.d.ts +163 -0
- package/dist/calculator/schema.js +129 -0
- package/dist/calculator/solve.d.ts +46 -0
- package/dist/calculator/solve.js +227 -0
- package/dist/calculator/units.d.ts +50 -0
- package/dist/calculator/units.js +137 -0
- package/dist/calculator/validate.d.ts +192 -0
- package/dist/calculator/validate.js +370 -0
- package/dist/exports.d.ts +2 -1
- package/dist/exports.js +4 -1
- package/package.json +3 -2
- 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/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
- /package/dist/{canvas/Tool.js → calculator/instance.js} +0 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { ColumnType } from '../types/firestore.js';
|
|
2
|
+
import { fieldDimension, findEquation, findField } from './schema.js';
|
|
3
|
+
import { evaluateEquation, numericFieldValue } from './evaluate.js';
|
|
4
|
+
// Generous: the alternating secant/bisection worst case over the widest
|
|
5
|
+
// default bracket (volume, [0, 1e30]) needs ~170 halvings-equivalent.
|
|
6
|
+
const DEFAULT_MAX_ITER = 256;
|
|
7
|
+
const DEFAULT_SCAN_SAMPLES = 128;
|
|
8
|
+
// Generous per-dimension fallback brackets so common monotonic equations
|
|
9
|
+
// solve with zero authoring effort; author hints exist to isolate a specific
|
|
10
|
+
// root, not as a prerequisite. Lengths/areas/volumes are non-negative (which
|
|
11
|
+
// also picks the physical branch of even-power equations).
|
|
12
|
+
const DEFAULT_BRACKETS = {
|
|
13
|
+
length: [0, 1e10], // 10 km in µm
|
|
14
|
+
area: [0, 1e20],
|
|
15
|
+
volume: [0, 1e30],
|
|
16
|
+
angle: [-360, 360],
|
|
17
|
+
none: [-1e12, 1e12],
|
|
18
|
+
};
|
|
19
|
+
// Convergence: bracket width below the dimension's absolute floor or a
|
|
20
|
+
// relative epsilon of the estimate, whichever is larger.
|
|
21
|
+
const ABS_TOL = {
|
|
22
|
+
length: 1e-3, // 1 nm — far below any display tolerance
|
|
23
|
+
area: 1e-3,
|
|
24
|
+
volume: 1e-3,
|
|
25
|
+
angle: 1e-9,
|
|
26
|
+
none: 1e-12,
|
|
27
|
+
};
|
|
28
|
+
const REL_TOL = 1e-12;
|
|
29
|
+
/**
|
|
30
|
+
* Numerically solve a solvable field from its governing equation, given the
|
|
31
|
+
* equation's output value (and every other referenced value) in `values`.
|
|
32
|
+
*/
|
|
33
|
+
export const solveForField = ({ definition, fieldId, values, options, }) => {
|
|
34
|
+
const field = findField(definition, fieldId);
|
|
35
|
+
if (!field)
|
|
36
|
+
return { ok: false, failure: { kind: 'unknown-field' } };
|
|
37
|
+
if (field.kind !== ColumnType.Number &&
|
|
38
|
+
field.kind !== ColumnType.Measurement &&
|
|
39
|
+
field.kind !== ColumnType.Angle) {
|
|
40
|
+
return { ok: false, failure: { kind: 'not-solvable' } };
|
|
41
|
+
}
|
|
42
|
+
const hints = field.solve;
|
|
43
|
+
if (!hints?.solvable) {
|
|
44
|
+
return { ok: false, failure: { kind: 'not-solvable' } };
|
|
45
|
+
}
|
|
46
|
+
const equation = hints.governingEquationId
|
|
47
|
+
? findEquation(definition, hints.governingEquationId)
|
|
48
|
+
: undefined;
|
|
49
|
+
if (!equation) {
|
|
50
|
+
return { ok: false, failure: { kind: 'no-governing-equation' } };
|
|
51
|
+
}
|
|
52
|
+
const targetField = findField(definition, equation.targetFieldId);
|
|
53
|
+
const targetValue = targetField
|
|
54
|
+
? numericFieldValue(targetField, values[equation.targetFieldId])
|
|
55
|
+
: null;
|
|
56
|
+
if (targetValue == null) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
failure: {
|
|
60
|
+
kind: 'missing-target',
|
|
61
|
+
targetFieldId: equation.targetFieldId,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// g(x) — forward value with the unknown pinned to x, minus the target.
|
|
66
|
+
// Returns null at points where the expression is undefined (e.g. a
|
|
67
|
+
// division by zero at x = 0); the search skips such points.
|
|
68
|
+
let lastError = null;
|
|
69
|
+
const g = (x) => {
|
|
70
|
+
const result = evaluateEquation({
|
|
71
|
+
definition,
|
|
72
|
+
equation,
|
|
73
|
+
values: { ...values, [fieldId]: x },
|
|
74
|
+
});
|
|
75
|
+
if (result.ok)
|
|
76
|
+
return result.value - targetValue;
|
|
77
|
+
if (result.reason === 'missing-inputs') {
|
|
78
|
+
// Missing something other than the unknown — not point-dependent, bail.
|
|
79
|
+
throw {
|
|
80
|
+
solveFailure: {
|
|
81
|
+
kind: 'missing-inputs',
|
|
82
|
+
missingFieldIds: result.missingFieldIds.filter((id) => id !== fieldId),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
lastError = result.error;
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
const dimension = fieldDimension(field);
|
|
90
|
+
const [defaultLo, defaultHi] = DEFAULT_BRACKETS[dimension];
|
|
91
|
+
const lo0 = hints.min ?? defaultLo;
|
|
92
|
+
const hi0 = hints.max ?? defaultHi;
|
|
93
|
+
if (!Number.isFinite(lo0) || !Number.isFinite(hi0) || lo0 >= hi0) {
|
|
94
|
+
return { ok: false, failure: { kind: 'invalid-bracket' } };
|
|
95
|
+
}
|
|
96
|
+
const residualTol = Math.max(1e-9, Math.abs(targetValue) * 1e-9);
|
|
97
|
+
const absTol = ABS_TOL[dimension];
|
|
98
|
+
const converged = (lo, hi) => hi - lo <= Math.max(absTol, REL_TOL * Math.max(Math.abs(lo), Math.abs(hi)));
|
|
99
|
+
try {
|
|
100
|
+
// --- Find a sign-change bracket -------------------------------------
|
|
101
|
+
let lo = lo0;
|
|
102
|
+
let hi = hi0;
|
|
103
|
+
let gLo = g(lo);
|
|
104
|
+
let gHi = g(hi);
|
|
105
|
+
if (gLo != null && Math.abs(gLo) <= residualTol) {
|
|
106
|
+
return { ok: true, value: lo };
|
|
107
|
+
}
|
|
108
|
+
if (gHi != null && Math.abs(gHi) <= residualTol) {
|
|
109
|
+
return { ok: true, value: hi };
|
|
110
|
+
}
|
|
111
|
+
const straddles = (a, b) => a != null && b != null && Math.sign(a) !== Math.sign(b);
|
|
112
|
+
if (!straddles(gLo, gHi)) {
|
|
113
|
+
// Probe interior points (also around the Newton seed, if given) for an
|
|
114
|
+
// adjacent evaluable pair with opposite signs.
|
|
115
|
+
const samples = options?.scanSamples ?? DEFAULT_SCAN_SAMPLES;
|
|
116
|
+
let found = false;
|
|
117
|
+
let prevX = lo;
|
|
118
|
+
let prevG = gLo;
|
|
119
|
+
for (let i = 1; i <= samples; i++) {
|
|
120
|
+
const x = i === samples ? hi : lo0 + ((hi0 - lo0) * i) / samples;
|
|
121
|
+
const gx = g(x);
|
|
122
|
+
if (gx != null && Math.abs(gx) <= residualTol) {
|
|
123
|
+
return { ok: true, value: x };
|
|
124
|
+
}
|
|
125
|
+
if (straddles(prevG, gx)) {
|
|
126
|
+
lo = prevX;
|
|
127
|
+
hi = x;
|
|
128
|
+
gLo = prevG;
|
|
129
|
+
gHi = gx;
|
|
130
|
+
found = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
if (gx != null) {
|
|
134
|
+
prevX = x;
|
|
135
|
+
prevG = gx;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!found) {
|
|
139
|
+
return lastError != null && gLo == null && gHi == null
|
|
140
|
+
? { ok: false, failure: { kind: 'eval-error', error: lastError } }
|
|
141
|
+
: { ok: false, failure: { kind: 'no-sign-change' } };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// --- Root refinement: secant (regula falsi) fast-path guarded by ------
|
|
145
|
+
// bisection, so invertible/monotonic equations converge fast and
|
|
146
|
+
// anything pathological still halves the bracket every step.
|
|
147
|
+
const maxIter = options?.maxIter ?? DEFAULT_MAX_ITER;
|
|
148
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
149
|
+
if (gLo == null || gHi == null)
|
|
150
|
+
break; // defensive; shouldn't happen
|
|
151
|
+
let x;
|
|
152
|
+
const denom = gHi - gLo;
|
|
153
|
+
if (denom !== 0) {
|
|
154
|
+
// Secant estimate, clamped to the interior of the bracket.
|
|
155
|
+
const secant = hi - (gHi * (hi - lo)) / denom;
|
|
156
|
+
const mid = (lo + hi) / 2;
|
|
157
|
+
x =
|
|
158
|
+
secant > lo && secant < hi
|
|
159
|
+
? // Blend toward bisection to avoid regula-falsi stagnation on
|
|
160
|
+
// one stuck endpoint.
|
|
161
|
+
iter % 2 === 0
|
|
162
|
+
? secant
|
|
163
|
+
: mid
|
|
164
|
+
: mid;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
x = (lo + hi) / 2;
|
|
168
|
+
}
|
|
169
|
+
const gx = g(x);
|
|
170
|
+
if (gx == null) {
|
|
171
|
+
// Undefined interior point — fall back to plain midpoint stepping
|
|
172
|
+
// toward the evaluable side.
|
|
173
|
+
const mid = (lo + hi) / 2;
|
|
174
|
+
const gMid = x === mid ? gx : g(mid);
|
|
175
|
+
if (gMid == null) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
failure: {
|
|
179
|
+
kind: 'eval-error',
|
|
180
|
+
error: lastError ?? 'Expression undefined inside bracket',
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
x = mid;
|
|
185
|
+
if (Math.sign(gMid) === Math.sign(gLo)) {
|
|
186
|
+
lo = x;
|
|
187
|
+
gLo = gMid;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
hi = x;
|
|
191
|
+
gHi = gMid;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
if (Math.abs(gx) <= residualTol)
|
|
196
|
+
return { ok: true, value: x };
|
|
197
|
+
if (Math.sign(gx) === Math.sign(gLo)) {
|
|
198
|
+
lo = x;
|
|
199
|
+
gLo = gx;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
hi = x;
|
|
203
|
+
gHi = gx;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (converged(lo, hi)) {
|
|
207
|
+
return { ok: true, value: (lo + hi) / 2 };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { ok: false, failure: { kind: 'non-convergent' } };
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
if (err && typeof err === 'object' && 'solveFailure' in err) {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
failure: err.solveFailure,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
failure: {
|
|
222
|
+
kind: 'eval-error',
|
|
223
|
+
error: err instanceof Error ? err.message : String(err),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { DecimalTolerance, FractionalTolerance, Units } from '../types/firestore.js';
|
|
2
|
+
export type MeasurementDimension = 'length' | 'area' | 'volume';
|
|
3
|
+
export type FieldDimension = MeasurementDimension | 'angle' | 'none';
|
|
4
|
+
export type AreaUnit = 'sq_mm' | 'sq_cm' | 'sq_m' | 'sq_in' | 'sq_ft' | 'sq_yd';
|
|
5
|
+
export type VolumeUnit = 'cu_mm' | 'cu_cm' | 'cu_m' | 'cu_in' | 'cu_ft' | 'cu_yd' | 'liter' | 'gallon';
|
|
6
|
+
export type AngleUnit = 'deg' | 'rad';
|
|
7
|
+
/** Every unit a calculator field may display in. Length reuses `Units`. */
|
|
8
|
+
export type CalculatorUnit = Units | AreaUnit | VolumeUnit | AngleUnit;
|
|
9
|
+
export interface CalculatorUnitInfo {
|
|
10
|
+
dimension: FieldDimension;
|
|
11
|
+
/**
|
|
12
|
+
* Real math.js unit token used for conversion. The fractional/composite
|
|
13
|
+
* length units (`in_frac`, `ft_in_*`) are display-only and collapse to
|
|
14
|
+
* their real unit here (mirrors normalizeUnitForMathJS in evaluateFormula).
|
|
15
|
+
*/
|
|
16
|
+
mathUnit: string;
|
|
17
|
+
/** Short display label, e.g. 'ft³'. */
|
|
18
|
+
label: string;
|
|
19
|
+
}
|
|
20
|
+
export declare const CALCULATOR_UNIT_INFO: Record<CalculatorUnit, CalculatorUnitInfo>;
|
|
21
|
+
/** The math.js token for a dimension's canonical unit. '' for dimensionless. */
|
|
22
|
+
export declare const canonicalMathUnit: (dimension: FieldDimension) => string;
|
|
23
|
+
export declare const unitDimension: (unit: CalculatorUnit) => FieldDimension;
|
|
24
|
+
export declare const unitsForDimension: (dimension: FieldDimension) => CalculatorUnit[];
|
|
25
|
+
/** Short label for a unit ('ft³', 'in', '°'). */
|
|
26
|
+
export declare const calculatorUnitLabel: (unit: CalculatorUnit) => string;
|
|
27
|
+
/** Convert a display-unit numeric value to the dimension's canonical unit. */
|
|
28
|
+
export declare const toCanonical: (value: number, unit: CalculatorUnit) => number;
|
|
29
|
+
/** Convert a canonical value to a display-unit numeric value. */
|
|
30
|
+
export declare const fromCanonical: (value: number, unit: CalculatorUnit) => number;
|
|
31
|
+
/**
|
|
32
|
+
* Parse user input in a display unit into a canonical value. Accepts plain
|
|
33
|
+
* numbers, fractions and mixed numbers ("12 1/2"). Length units additionally
|
|
34
|
+
* accept a trailing unit token (delegated to parseMeasurement). Returns null
|
|
35
|
+
* on unparseable input.
|
|
36
|
+
*/
|
|
37
|
+
export declare const parseToCanonical: (input: string, unit: CalculatorUnit) => number | null;
|
|
38
|
+
export interface FormatTolerances {
|
|
39
|
+
fractionalTolerance?: FractionalTolerance;
|
|
40
|
+
decimalTolerance?: DecimalTolerance;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Format a canonical value for display in the given unit. Lengths delegate to
|
|
44
|
+
* convertMicrometers (the one place that renders fractional-inch / feet-inch
|
|
45
|
+
* strings); other dimensions format decimally per the decimal tolerance.
|
|
46
|
+
*/
|
|
47
|
+
export declare const formatCanonical: (value: number | null | undefined, unit: CalculatorUnit, tolerances?: FormatTolerances) => {
|
|
48
|
+
value: string;
|
|
49
|
+
unit: string;
|
|
50
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { create, all } from 'mathjs';
|
|
2
|
+
import { DecimalTolerance, FractionalTolerance, Units, convertUnitsToReadableShort, } from '../types/firestore.js';
|
|
3
|
+
import { convertMicrometers } from '../utils/micrometersToUnit.js';
|
|
4
|
+
import { parseMeasurement } from '../utils/parseMeasurement.js';
|
|
5
|
+
const math = create(all);
|
|
6
|
+
const LENGTH_UNIT_INFO = {
|
|
7
|
+
[Units.Millimeters]: { dimension: 'length', mathUnit: 'mm', label: 'mm' },
|
|
8
|
+
[Units.Centimeters]: { dimension: 'length', mathUnit: 'cm', label: 'cm' },
|
|
9
|
+
[Units.Meters]: { dimension: 'length', mathUnit: 'm', label: 'm' },
|
|
10
|
+
[Units.Inches]: { dimension: 'length', mathUnit: 'in', label: 'in' },
|
|
11
|
+
[Units.FractionalInches]: {
|
|
12
|
+
dimension: 'length',
|
|
13
|
+
mathUnit: 'in',
|
|
14
|
+
label: 'in',
|
|
15
|
+
},
|
|
16
|
+
[Units.Feet]: { dimension: 'length', mathUnit: 'ft', label: 'ft' },
|
|
17
|
+
[Units.FeetInchesDecimal]: {
|
|
18
|
+
dimension: 'length',
|
|
19
|
+
mathUnit: 'ft',
|
|
20
|
+
label: 'ft-in',
|
|
21
|
+
},
|
|
22
|
+
[Units.FeetInchesFractional]: {
|
|
23
|
+
dimension: 'length',
|
|
24
|
+
mathUnit: 'ft',
|
|
25
|
+
label: 'ft-in',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
export const CALCULATOR_UNIT_INFO = {
|
|
29
|
+
...LENGTH_UNIT_INFO,
|
|
30
|
+
sq_mm: { dimension: 'area', mathUnit: 'mm^2', label: 'mm²' },
|
|
31
|
+
sq_cm: { dimension: 'area', mathUnit: 'cm^2', label: 'cm²' },
|
|
32
|
+
sq_m: { dimension: 'area', mathUnit: 'm^2', label: 'm²' },
|
|
33
|
+
sq_in: { dimension: 'area', mathUnit: 'in^2', label: 'in²' },
|
|
34
|
+
sq_ft: { dimension: 'area', mathUnit: 'ft^2', label: 'ft²' },
|
|
35
|
+
sq_yd: { dimension: 'area', mathUnit: 'yd^2', label: 'yd²' },
|
|
36
|
+
cu_mm: { dimension: 'volume', mathUnit: 'mm^3', label: 'mm³' },
|
|
37
|
+
cu_cm: { dimension: 'volume', mathUnit: 'cm^3', label: 'cm³' },
|
|
38
|
+
cu_m: { dimension: 'volume', mathUnit: 'm^3', label: 'm³' },
|
|
39
|
+
cu_in: { dimension: 'volume', mathUnit: 'in^3', label: 'in³' },
|
|
40
|
+
cu_ft: { dimension: 'volume', mathUnit: 'ft^3', label: 'ft³' },
|
|
41
|
+
cu_yd: { dimension: 'volume', mathUnit: 'yd^3', label: 'yd³' },
|
|
42
|
+
liter: { dimension: 'volume', mathUnit: 'L', label: 'L' },
|
|
43
|
+
gallon: { dimension: 'volume', mathUnit: 'gal', label: 'gal' },
|
|
44
|
+
deg: { dimension: 'angle', mathUnit: 'deg', label: '°' },
|
|
45
|
+
rad: { dimension: 'angle', mathUnit: 'rad', label: 'rad' },
|
|
46
|
+
};
|
|
47
|
+
/** The math.js token for a dimension's canonical unit. '' for dimensionless. */
|
|
48
|
+
export const canonicalMathUnit = (dimension) => {
|
|
49
|
+
switch (dimension) {
|
|
50
|
+
case 'length':
|
|
51
|
+
return 'um';
|
|
52
|
+
case 'area':
|
|
53
|
+
return 'um^2';
|
|
54
|
+
case 'volume':
|
|
55
|
+
return 'um^3';
|
|
56
|
+
case 'angle':
|
|
57
|
+
return 'deg';
|
|
58
|
+
case 'none':
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
export const unitDimension = (unit) => CALCULATOR_UNIT_INFO[unit]?.dimension ?? 'none';
|
|
63
|
+
export const unitsForDimension = (dimension) => Object.keys(CALCULATOR_UNIT_INFO).filter((u) => CALCULATOR_UNIT_INFO[u].dimension === dimension);
|
|
64
|
+
/** Short label for a unit ('ft³', 'in', '°'). */
|
|
65
|
+
export const calculatorUnitLabel = (unit) => {
|
|
66
|
+
const lengthLabel = convertUnitsToReadableShort(unit);
|
|
67
|
+
return CALCULATOR_UNIT_INFO[unit]?.label ?? lengthLabel ?? String(unit);
|
|
68
|
+
};
|
|
69
|
+
// All units here are pure scale factors relative to their canonical unit
|
|
70
|
+
// (no affine offsets like °F), so conversion is a cached multiply.
|
|
71
|
+
const factorCache = new Map();
|
|
72
|
+
const canonicalFactor = (unit) => {
|
|
73
|
+
const cached = factorCache.get(unit);
|
|
74
|
+
if (cached != null)
|
|
75
|
+
return cached;
|
|
76
|
+
const info = CALCULATOR_UNIT_INFO[unit];
|
|
77
|
+
if (!info)
|
|
78
|
+
throw new Error(`Unknown calculator unit: ${unit}`);
|
|
79
|
+
const canonical = canonicalMathUnit(info.dimension);
|
|
80
|
+
const factor = canonical === '' ? 1 : math.unit(1, info.mathUnit).toNumber(canonical);
|
|
81
|
+
factorCache.set(unit, factor);
|
|
82
|
+
return factor;
|
|
83
|
+
};
|
|
84
|
+
/** Convert a display-unit numeric value to the dimension's canonical unit. */
|
|
85
|
+
export const toCanonical = (value, unit) => value * canonicalFactor(unit);
|
|
86
|
+
/** Convert a canonical value to a display-unit numeric value. */
|
|
87
|
+
export const fromCanonical = (value, unit) => value / canonicalFactor(unit);
|
|
88
|
+
const decimalPlaces = (tolerance) =>
|
|
89
|
+
// '0.5' -> 1, '0.01' -> 2, ... (the string-length trick used app-wide).
|
|
90
|
+
tolerance.length - 2;
|
|
91
|
+
// "12 1/2" -> "12 + 1/2" (same normalization parseMeasurement applies).
|
|
92
|
+
const normalizeMixedFraction = (input) => input.trim().replace(/(\d+)\s+(\d+)\/(\d+)/g, '$1 + $2/$3');
|
|
93
|
+
/**
|
|
94
|
+
* Parse user input in a display unit into a canonical value. Accepts plain
|
|
95
|
+
* numbers, fractions and mixed numbers ("12 1/2"). Length units additionally
|
|
96
|
+
* accept a trailing unit token (delegated to parseMeasurement). Returns null
|
|
97
|
+
* on unparseable input.
|
|
98
|
+
*/
|
|
99
|
+
export const parseToCanonical = (input, unit) => {
|
|
100
|
+
const info = CALCULATOR_UNIT_INFO[unit];
|
|
101
|
+
if (!info || input.trim() === '')
|
|
102
|
+
return null;
|
|
103
|
+
if (info.dimension === 'length') {
|
|
104
|
+
// parseMeasurement already returns micrometers (canonical).
|
|
105
|
+
return parseMeasurement(input, info.mathUnit);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const result = math.evaluate(normalizeMixedFraction(input));
|
|
109
|
+
if (typeof result !== 'number' || !Number.isFinite(result))
|
|
110
|
+
return null;
|
|
111
|
+
return toCanonical(result, unit);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Format a canonical value for display in the given unit. Lengths delegate to
|
|
119
|
+
* convertMicrometers (the one place that renders fractional-inch / feet-inch
|
|
120
|
+
* strings); other dimensions format decimally per the decimal tolerance.
|
|
121
|
+
*/
|
|
122
|
+
export const formatCanonical = (value, unit, tolerances) => {
|
|
123
|
+
const fractional = tolerances?.fractionalTolerance ?? FractionalTolerance.Sixteenth;
|
|
124
|
+
const decimal = tolerances?.decimalTolerance ?? DecimalTolerance.Hundredth;
|
|
125
|
+
const info = CALCULATOR_UNIT_INFO[unit];
|
|
126
|
+
if (value == null || Number.isNaN(value) || !info) {
|
|
127
|
+
return { value: 'NaN', unit: info?.label ?? '' };
|
|
128
|
+
}
|
|
129
|
+
if (info.dimension === 'length') {
|
|
130
|
+
return convertMicrometers(value, unit, fractional, decimal);
|
|
131
|
+
}
|
|
132
|
+
const display = fromCanonical(value, unit);
|
|
133
|
+
return {
|
|
134
|
+
value: display.toFixed(decimalPlaces(decimal)),
|
|
135
|
+
unit: info.label,
|
|
136
|
+
};
|
|
137
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ColumnType, DecimalTolerance, FractionalTolerance } from '../types/firestore.js';
|
|
3
|
+
export declare const calculatorDefinitionSchema: z.ZodObject<{
|
|
4
|
+
id: z.ZodString;
|
|
5
|
+
schemaVersion: z.ZodNumber;
|
|
6
|
+
version: z.ZodNumber;
|
|
7
|
+
name: z.ZodString;
|
|
8
|
+
description: z.ZodString;
|
|
9
|
+
folderId: z.ZodNullable<z.ZodString>;
|
|
10
|
+
fields: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
11
|
+
kind: z.ZodLiteral<ColumnType.Number>;
|
|
12
|
+
defaultValue: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
solve: z.ZodOptional<z.ZodObject<{
|
|
14
|
+
solvable: z.ZodBoolean;
|
|
15
|
+
governingEquationId: z.ZodOptional<z.ZodString>;
|
|
16
|
+
min: z.ZodOptional<z.ZodNumber>;
|
|
17
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
18
|
+
guess: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
}, z.core.$strip>>;
|
|
20
|
+
id: z.ZodString;
|
|
21
|
+
name: z.ZodString;
|
|
22
|
+
role: z.ZodEnum<{
|
|
23
|
+
input: "input";
|
|
24
|
+
output: "output";
|
|
25
|
+
}>;
|
|
26
|
+
description: z.ZodOptional<z.ZodString>;
|
|
27
|
+
binding: z.ZodOptional<z.ZodObject<{
|
|
28
|
+
hotspotId: z.ZodString;
|
|
29
|
+
}, z.core.$strip>>;
|
|
30
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
31
|
+
kind: z.ZodLiteral<ColumnType.Measurement>;
|
|
32
|
+
unit: z.ZodObject<{
|
|
33
|
+
dimension: z.ZodEnum<{
|
|
34
|
+
length: "length";
|
|
35
|
+
area: "area";
|
|
36
|
+
volume: "volume";
|
|
37
|
+
}>;
|
|
38
|
+
defaultUnit: z.ZodString;
|
|
39
|
+
allowedUnits: z.ZodArray<z.ZodString>;
|
|
40
|
+
fractionalTolerance: z.ZodOptional<z.ZodEnum<typeof FractionalTolerance>>;
|
|
41
|
+
decimalTolerance: z.ZodOptional<z.ZodEnum<typeof DecimalTolerance>>;
|
|
42
|
+
}, z.core.$strip>;
|
|
43
|
+
defaultValue: z.ZodOptional<z.ZodNumber>;
|
|
44
|
+
solve: z.ZodOptional<z.ZodObject<{
|
|
45
|
+
solvable: z.ZodBoolean;
|
|
46
|
+
governingEquationId: z.ZodOptional<z.ZodString>;
|
|
47
|
+
min: z.ZodOptional<z.ZodNumber>;
|
|
48
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
49
|
+
guess: z.ZodOptional<z.ZodNumber>;
|
|
50
|
+
}, z.core.$strip>>;
|
|
51
|
+
id: z.ZodString;
|
|
52
|
+
name: z.ZodString;
|
|
53
|
+
role: z.ZodEnum<{
|
|
54
|
+
input: "input";
|
|
55
|
+
output: "output";
|
|
56
|
+
}>;
|
|
57
|
+
description: z.ZodOptional<z.ZodString>;
|
|
58
|
+
binding: z.ZodOptional<z.ZodObject<{
|
|
59
|
+
hotspotId: z.ZodString;
|
|
60
|
+
}, z.core.$strip>>;
|
|
61
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
62
|
+
kind: z.ZodLiteral<ColumnType.Angle>;
|
|
63
|
+
angleUnit: z.ZodOptional<z.ZodEnum<{
|
|
64
|
+
deg: "deg";
|
|
65
|
+
rad: "rad";
|
|
66
|
+
}>>;
|
|
67
|
+
defaultValue: z.ZodOptional<z.ZodNumber>;
|
|
68
|
+
solve: z.ZodOptional<z.ZodObject<{
|
|
69
|
+
solvable: z.ZodBoolean;
|
|
70
|
+
governingEquationId: z.ZodOptional<z.ZodString>;
|
|
71
|
+
min: z.ZodOptional<z.ZodNumber>;
|
|
72
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
73
|
+
guess: z.ZodOptional<z.ZodNumber>;
|
|
74
|
+
}, z.core.$strip>>;
|
|
75
|
+
id: z.ZodString;
|
|
76
|
+
name: z.ZodString;
|
|
77
|
+
role: z.ZodEnum<{
|
|
78
|
+
input: "input";
|
|
79
|
+
output: "output";
|
|
80
|
+
}>;
|
|
81
|
+
description: z.ZodOptional<z.ZodString>;
|
|
82
|
+
binding: z.ZodOptional<z.ZodObject<{
|
|
83
|
+
hotspotId: z.ZodString;
|
|
84
|
+
}, z.core.$strip>>;
|
|
85
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
86
|
+
kind: z.ZodLiteral<ColumnType.SingleSelect>;
|
|
87
|
+
columnData: z.ZodObject<{
|
|
88
|
+
options: z.ZodArray<z.ZodString>;
|
|
89
|
+
}, z.core.$strip>;
|
|
90
|
+
defaultValue: z.ZodOptional<z.ZodString>;
|
|
91
|
+
id: z.ZodString;
|
|
92
|
+
name: z.ZodString;
|
|
93
|
+
role: z.ZodEnum<{
|
|
94
|
+
input: "input";
|
|
95
|
+
output: "output";
|
|
96
|
+
}>;
|
|
97
|
+
description: z.ZodOptional<z.ZodString>;
|
|
98
|
+
binding: z.ZodOptional<z.ZodObject<{
|
|
99
|
+
hotspotId: z.ZodString;
|
|
100
|
+
}, z.core.$strip>>;
|
|
101
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
102
|
+
kind: z.ZodLiteral<ColumnType.MultiSelect>;
|
|
103
|
+
columnData: z.ZodObject<{
|
|
104
|
+
options: z.ZodArray<z.ZodString>;
|
|
105
|
+
}, z.core.$strip>;
|
|
106
|
+
defaultValue: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
107
|
+
id: z.ZodString;
|
|
108
|
+
name: z.ZodString;
|
|
109
|
+
role: z.ZodEnum<{
|
|
110
|
+
input: "input";
|
|
111
|
+
output: "output";
|
|
112
|
+
}>;
|
|
113
|
+
description: z.ZodOptional<z.ZodString>;
|
|
114
|
+
binding: z.ZodOptional<z.ZodObject<{
|
|
115
|
+
hotspotId: z.ZodString;
|
|
116
|
+
}, z.core.$strip>>;
|
|
117
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
118
|
+
kind: z.ZodLiteral<ColumnType.ConversionTable>;
|
|
119
|
+
columnData: z.ZodObject<{
|
|
120
|
+
conversions: z.ZodArray<z.ZodObject<{
|
|
121
|
+
label: z.ZodString;
|
|
122
|
+
value: z.ZodString;
|
|
123
|
+
}, z.core.$strip>>;
|
|
124
|
+
}, z.core.$strip>;
|
|
125
|
+
defaultValue: z.ZodOptional<z.ZodString>;
|
|
126
|
+
id: z.ZodString;
|
|
127
|
+
name: z.ZodString;
|
|
128
|
+
role: z.ZodEnum<{
|
|
129
|
+
input: "input";
|
|
130
|
+
output: "output";
|
|
131
|
+
}>;
|
|
132
|
+
description: z.ZodOptional<z.ZodString>;
|
|
133
|
+
binding: z.ZodOptional<z.ZodObject<{
|
|
134
|
+
hotspotId: z.ZodString;
|
|
135
|
+
}, z.core.$strip>>;
|
|
136
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
137
|
+
kind: z.ZodLiteral<ColumnType.Instructions>;
|
|
138
|
+
columnData: z.ZodObject<{
|
|
139
|
+
text: z.ZodString;
|
|
140
|
+
textColor: z.ZodString;
|
|
141
|
+
backgroundColor: z.ZodString;
|
|
142
|
+
}, z.core.$strip>;
|
|
143
|
+
id: z.ZodString;
|
|
144
|
+
name: z.ZodString;
|
|
145
|
+
role: z.ZodEnum<{
|
|
146
|
+
input: "input";
|
|
147
|
+
output: "output";
|
|
148
|
+
}>;
|
|
149
|
+
description: z.ZodOptional<z.ZodString>;
|
|
150
|
+
binding: z.ZodOptional<z.ZodObject<{
|
|
151
|
+
hotspotId: z.ZodString;
|
|
152
|
+
}, z.core.$strip>>;
|
|
153
|
+
}, z.core.$strip>], "kind">>;
|
|
154
|
+
equations: z.ZodArray<z.ZodObject<{
|
|
155
|
+
id: z.ZodString;
|
|
156
|
+
targetFieldId: z.ZodString;
|
|
157
|
+
expression: z.ZodString;
|
|
158
|
+
variableToFieldId: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
159
|
+
}, z.core.$strip>>;
|
|
160
|
+
diagramFileId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
161
|
+
isPublic: z.ZodBoolean;
|
|
162
|
+
sharedUserIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
163
|
+
sharedOrgIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
164
|
+
createdBy: z.ZodOptional<z.ZodObject<{
|
|
165
|
+
userId: z.ZodString;
|
|
166
|
+
firstName: z.ZodOptional<z.ZodString>;
|
|
167
|
+
lastName: z.ZodOptional<z.ZodString>;
|
|
168
|
+
}, z.core.$strip>>;
|
|
169
|
+
createdAt: z.ZodOptional<z.ZodUnknown>;
|
|
170
|
+
updatedAt: z.ZodOptional<z.ZodUnknown>;
|
|
171
|
+
}, z.core.$strip>;
|
|
172
|
+
export type IssueSeverity = 'error' | 'warning';
|
|
173
|
+
export interface ValidationIssue {
|
|
174
|
+
code: string;
|
|
175
|
+
path: string;
|
|
176
|
+
message: string;
|
|
177
|
+
severity: IssueSeverity;
|
|
178
|
+
}
|
|
179
|
+
export interface ValidationResult {
|
|
180
|
+
/** True when there are no error-severity issues (warnings allowed). */
|
|
181
|
+
ok: boolean;
|
|
182
|
+
issues: ValidationIssue[];
|
|
183
|
+
}
|
|
184
|
+
/** Free variables of an expression (function names and constants excluded). */
|
|
185
|
+
export declare const expressionSymbols: (expression: string) => {
|
|
186
|
+
ok: true;
|
|
187
|
+
symbols: string[];
|
|
188
|
+
} | {
|
|
189
|
+
ok: false;
|
|
190
|
+
error: string;
|
|
191
|
+
};
|
|
192
|
+
export declare const validateCalculatorDefinition: (input: unknown) => ValidationResult;
|