@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,207 @@
|
|
|
1
|
+
import { create, all } from 'mathjs';
|
|
2
|
+
import { ColumnType } from '../types/firestore.js';
|
|
3
|
+
import { equationForField, findField } from './schema.js';
|
|
4
|
+
// Trig in calculator equations works in DEGREES: angle fields are stored in
|
|
5
|
+
// degrees canonically, and construction authors write `H = W * tan(A)`
|
|
6
|
+
// expecting A in degrees. mathjs has no `angle` config option (evaluateFormula
|
|
7
|
+
// passes `{ angle: 'deg' }`, but mathjs ignores it and runs radians), so we
|
|
8
|
+
// override the trig functions with degree-aware wrappers — the documented
|
|
9
|
+
// mathjs recipe for degree mode.
|
|
10
|
+
const math = create(all);
|
|
11
|
+
const DEG = Math.PI / 180;
|
|
12
|
+
math.import({
|
|
13
|
+
sin: (x) => Math.sin(x * DEG),
|
|
14
|
+
cos: (x) => Math.cos(x * DEG),
|
|
15
|
+
tan: (x) => Math.tan(x * DEG),
|
|
16
|
+
sec: (x) => 1 / Math.cos(x * DEG),
|
|
17
|
+
csc: (x) => 1 / Math.sin(x * DEG),
|
|
18
|
+
cot: (x) => 1 / Math.tan(x * DEG),
|
|
19
|
+
asin: (x) => Math.asin(x) / DEG,
|
|
20
|
+
acos: (x) => Math.acos(x) / DEG,
|
|
21
|
+
atan: (x) => Math.atan(x) / DEG,
|
|
22
|
+
atan2: (y, x) => Math.atan2(y, x) / DEG,
|
|
23
|
+
}, { override: true });
|
|
24
|
+
const compile = math.compile.bind(math);
|
|
25
|
+
// Small compile cache — the editor preview re-evaluates on every keystroke.
|
|
26
|
+
const compileCache = new Map();
|
|
27
|
+
const COMPILE_CACHE_MAX = 200;
|
|
28
|
+
const compiled = (expression) => {
|
|
29
|
+
const hit = compileCache.get(expression);
|
|
30
|
+
if (hit)
|
|
31
|
+
return hit;
|
|
32
|
+
const node = compile(expression);
|
|
33
|
+
if (compileCache.size >= COMPILE_CACHE_MAX)
|
|
34
|
+
compileCache.clear();
|
|
35
|
+
compileCache.set(expression, node);
|
|
36
|
+
return node;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The unit-agnostic expression core: compile + evaluate + finiteness checks,
|
|
40
|
+
* no unit coercion. Scope values and the result share whatever unit system
|
|
41
|
+
* the caller established (canonical, for calculator equations).
|
|
42
|
+
*/
|
|
43
|
+
export const evaluateExpression = (expression, scope) => {
|
|
44
|
+
let node;
|
|
45
|
+
try {
|
|
46
|
+
node = compiled(expression);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: `Invalid expression: ${err instanceof Error ? err.message : String(err)}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
let result;
|
|
55
|
+
try {
|
|
56
|
+
// Spread so mathjs can't mutate the caller's scope.
|
|
57
|
+
result = node.evaluate({ ...scope });
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: `Evaluation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (typeof result !== 'number') {
|
|
66
|
+
return { ok: false, error: 'Expression did not produce a number' };
|
|
67
|
+
}
|
|
68
|
+
if (Number.isNaN(result))
|
|
69
|
+
return { ok: false, error: 'Result is NaN' };
|
|
70
|
+
if (!Number.isFinite(result)) {
|
|
71
|
+
return { ok: false, error: 'Result is not finite (division by zero?)' };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, value: result };
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* The canonical numeric value a field contributes to an equation scope, or
|
|
77
|
+
* null when the field has no usable value. Select and instructions fields are
|
|
78
|
+
* non-computational (validation rejects mapping them into equations).
|
|
79
|
+
*/
|
|
80
|
+
export const numericFieldValue = (field, value) => {
|
|
81
|
+
switch (field.kind) {
|
|
82
|
+
case ColumnType.Number:
|
|
83
|
+
case ColumnType.Measurement:
|
|
84
|
+
case ColumnType.Angle: {
|
|
85
|
+
const v = value ?? field.defaultValue;
|
|
86
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
87
|
+
}
|
|
88
|
+
case ColumnType.ConversionTable: {
|
|
89
|
+
// Value is the selected conversion's label; the numeric factor comes
|
|
90
|
+
// from the matched entry (Conversion.value is a string app-wide).
|
|
91
|
+
const label = value ?? field.defaultValue;
|
|
92
|
+
if (typeof label !== 'string')
|
|
93
|
+
return null;
|
|
94
|
+
const entry = field.columnData.conversions.find((c) => c.label === label);
|
|
95
|
+
if (!entry)
|
|
96
|
+
return null;
|
|
97
|
+
const parsed = parseFloat(entry.value);
|
|
98
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
99
|
+
}
|
|
100
|
+
default:
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const evaluateEquationInContext = (equation, ctx) => {
|
|
105
|
+
const cached = ctx.cache.get(equation.id);
|
|
106
|
+
if (cached)
|
|
107
|
+
return cached;
|
|
108
|
+
if (ctx.stack.has(equation.id)) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
reason: 'error',
|
|
112
|
+
error: `Circular equation reference involving ${equation.id}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
ctx.stack.add(equation.id);
|
|
116
|
+
try {
|
|
117
|
+
const scope = {};
|
|
118
|
+
const missing = [];
|
|
119
|
+
for (const [variable, fieldId] of Object.entries(equation.variableToFieldId)) {
|
|
120
|
+
const resolved = resolveFieldNumeric(fieldId, ctx);
|
|
121
|
+
if (!resolved.ok) {
|
|
122
|
+
if (resolved.reason === 'missing-inputs') {
|
|
123
|
+
missing.push(...resolved.missingFieldIds);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
return resolved;
|
|
127
|
+
}
|
|
128
|
+
scope[variable] = resolved.value;
|
|
129
|
+
}
|
|
130
|
+
if (missing.length > 0) {
|
|
131
|
+
const result = {
|
|
132
|
+
ok: false,
|
|
133
|
+
reason: 'missing-inputs',
|
|
134
|
+
missingFieldIds: [...new Set(missing)],
|
|
135
|
+
};
|
|
136
|
+
ctx.cache.set(equation.id, result);
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
const evaluated = evaluateExpression(equation.expression, scope);
|
|
140
|
+
const result = evaluated.ok
|
|
141
|
+
? { ok: true, value: evaluated.value }
|
|
142
|
+
: { ok: false, reason: 'error', error: evaluated.error };
|
|
143
|
+
ctx.cache.set(equation.id, result);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
ctx.stack.delete(equation.id);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
// An output field with an equation resolves to its COMPUTED value; if the
|
|
151
|
+
// computation is blocked on missing inputs but the user supplied a value for
|
|
152
|
+
// the output directly (the two-way-solving posture), the supplied value is
|
|
153
|
+
// used instead. Everything else resolves from `values`/defaults.
|
|
154
|
+
const resolveFieldNumeric = (fieldId, ctx) => {
|
|
155
|
+
const field = findField(ctx.definition, fieldId);
|
|
156
|
+
if (!field) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
reason: 'error',
|
|
160
|
+
error: `Equation references unknown field ${fieldId}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const equation = equationForField(ctx.definition, fieldId);
|
|
164
|
+
if (equation) {
|
|
165
|
+
const computed = evaluateEquationInContext(equation, ctx);
|
|
166
|
+
if (computed.ok)
|
|
167
|
+
return computed;
|
|
168
|
+
if (computed.reason === 'missing-inputs') {
|
|
169
|
+
const supplied = numericFieldValue(field, ctx.values[fieldId]);
|
|
170
|
+
if (supplied != null)
|
|
171
|
+
return { ok: true, value: supplied };
|
|
172
|
+
}
|
|
173
|
+
return computed;
|
|
174
|
+
}
|
|
175
|
+
const value = numericFieldValue(field, ctx.values[fieldId]);
|
|
176
|
+
if (value == null) {
|
|
177
|
+
return { ok: false, reason: 'missing-inputs', missingFieldIds: [fieldId] };
|
|
178
|
+
}
|
|
179
|
+
return { ok: true, value };
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Evaluate one equation against canonical field values. Variables referencing
|
|
183
|
+
* other output fields recurse into their equations (cycle-guarded).
|
|
184
|
+
*/
|
|
185
|
+
export const evaluateEquation = ({ definition, equation, values, }) => evaluateEquationInContext(equation, {
|
|
186
|
+
definition,
|
|
187
|
+
values,
|
|
188
|
+
stack: new Set(),
|
|
189
|
+
cache: new Map(),
|
|
190
|
+
});
|
|
191
|
+
/**
|
|
192
|
+
* Evaluate every equation in the definition. Returns a result per target
|
|
193
|
+
* field id — the live-preview / runtime "recompute all outputs" entry point.
|
|
194
|
+
*/
|
|
195
|
+
export const evaluateOutputs = ({ definition, values, }) => {
|
|
196
|
+
const ctx = {
|
|
197
|
+
definition,
|
|
198
|
+
values,
|
|
199
|
+
stack: new Set(),
|
|
200
|
+
cache: new Map(),
|
|
201
|
+
};
|
|
202
|
+
const results = {};
|
|
203
|
+
for (const equation of definition.equations) {
|
|
204
|
+
results[equation.targetFieldId] = evaluateEquationInContext(equation, ctx);
|
|
205
|
+
}
|
|
206
|
+
return results;
|
|
207
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './schema.js';
|
|
2
|
+
export * from './units.js';
|
|
3
|
+
export * from './evaluate.js';
|
|
4
|
+
export * from './solve.js';
|
|
5
|
+
export * from './instance.js';
|
|
6
|
+
export { calculatorDefinitionSchema, expressionSymbols, validateCalculatorDefinition, type IssueSeverity, type ValidationIssue, type ValidationResult, } from './validate.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Construction Calculator shared module: the calculator-definition schema,
|
|
2
|
+
// per-field unit system, forward evaluator, two-way numeric solver, and
|
|
3
|
+
// definition validation. Pure logic (mathjs + zod only — no Skia/React), safe
|
|
4
|
+
// on web, native, and Node.
|
|
5
|
+
export * from './schema.js';
|
|
6
|
+
export * from './units.js';
|
|
7
|
+
export * from './evaluate.js';
|
|
8
|
+
export * from './solve.js';
|
|
9
|
+
export * from './instance.js';
|
|
10
|
+
export { calculatorDefinitionSchema, expressionSymbols, validateCalculatorDefinition, } from './validate.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AnnotationCanvasState } from '../types/annotation.js';
|
|
2
|
+
import type { CalculatorDefinition } from './schema.js';
|
|
3
|
+
import type { CalculatorUnit } from './units.js';
|
|
4
|
+
export type CalculatorEntrySource = 'user' | 'tool';
|
|
5
|
+
export interface CalculatorEntryAttribution {
|
|
6
|
+
deviceId: string;
|
|
7
|
+
deviceName: string | null;
|
|
8
|
+
userName: string | null;
|
|
9
|
+
/** Epoch ms. */
|
|
10
|
+
at: number;
|
|
11
|
+
}
|
|
12
|
+
export interface CalculatorFieldEntry {
|
|
13
|
+
value: number | string | string[];
|
|
14
|
+
source: CalculatorEntrySource;
|
|
15
|
+
attribution: CalculatorEntryAttribution | null;
|
|
16
|
+
/** Pinned: edits, clears, and tape readings are refused until unlocked. */
|
|
17
|
+
locked: boolean;
|
|
18
|
+
/** Epoch ms. */
|
|
19
|
+
enteredAt: number;
|
|
20
|
+
}
|
|
21
|
+
export interface CalculatorInstanceFileData {
|
|
22
|
+
/** Source doc id in the top-level `calculators` collection. */
|
|
23
|
+
calculatorId: string;
|
|
24
|
+
/** definition.version at save time. */
|
|
25
|
+
sourceVersion: number;
|
|
26
|
+
definition: CalculatorDefinition;
|
|
27
|
+
entries: Record<string, CalculatorFieldEntry>;
|
|
28
|
+
/** Session display-unit choices, keyed by field id. */
|
|
29
|
+
displayUnitOverrides?: Record<string, CalculatorUnit>;
|
|
30
|
+
/**
|
|
31
|
+
* Embedded copy of the source diagram canvas. Its background image is
|
|
32
|
+
* re-uploaded under the instance file's own Storage path (and
|
|
33
|
+
* viewport.backgroundImage.storagePath repointed) at save time.
|
|
34
|
+
*/
|
|
35
|
+
canvas?: AnnotationCanvasState;
|
|
36
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { ColumnType, type ColumnConfig, type ConversionTableColumnData, type InstructionsColumnData, type SelectColumnData, type DecimalTolerance, type FractionalTolerance } from '../types/firestore.js';
|
|
2
|
+
import type { AngleUnit, CalculatorUnit, FieldDimension, MeasurementDimension } from './units.js';
|
|
3
|
+
/**
|
|
4
|
+
* Version of the definition format itself. Bumped only on breaking schema
|
|
5
|
+
* changes; independent of the npm package version and of each calculator's
|
|
6
|
+
* own content `version`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CALCULATOR_SCHEMA_VERSION = 1;
|
|
9
|
+
/**
|
|
10
|
+
* Which table a field appears in (the Figma mockup's Input Fields / Output
|
|
11
|
+
* Fields split). Mostly cosmetic: an input with solve hints can be computed
|
|
12
|
+
* from outputs at runtime, and outputs are just fields targeted by equations.
|
|
13
|
+
*/
|
|
14
|
+
export type FieldRole = 'input' | 'output';
|
|
15
|
+
/** Field kinds reuse the app-wide ColumnType enum (calculator subset). */
|
|
16
|
+
export type CalculatorFieldKind = ColumnType.Number | ColumnType.Measurement | ColumnType.Angle | ColumnType.SingleSelect | ColumnType.MultiSelect | ColumnType.ConversionTable | ColumnType.Instructions;
|
|
17
|
+
export declare const CALCULATOR_FIELD_KINDS: CalculatorFieldKind[];
|
|
18
|
+
/**
|
|
19
|
+
* Per-field unit configuration (templates have only a global user unit).
|
|
20
|
+
* `dimension` covers the volume-calculator case: inputs in inches, output in
|
|
21
|
+
* cubic feet — each field picks its own dimension and display units.
|
|
22
|
+
*/
|
|
23
|
+
export interface FieldUnitConfig {
|
|
24
|
+
dimension: MeasurementDimension;
|
|
25
|
+
/** Default display unit; must belong to `dimension`. */
|
|
26
|
+
defaultUnit: CalculatorUnit;
|
|
27
|
+
/** Units the runtime lets the user toggle between; must match `dimension`. */
|
|
28
|
+
allowedUnits: CalculatorUnit[];
|
|
29
|
+
fractionalTolerance?: FractionalTolerance;
|
|
30
|
+
decimalTolerance?: DecimalTolerance;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The schema half of two-way solving. A solvable field can be computed by
|
|
34
|
+
* numerically inverting its governing equation when the user supplies that
|
|
35
|
+
* equation's output value instead (see solve.ts). `min`/`max` bracket the
|
|
36
|
+
* root search in CANONICAL units; when absent the solver falls back to
|
|
37
|
+
* generous per-dimension defaults, so brackets are only needed to isolate a
|
|
38
|
+
* specific root (e.g. the positive branch of a quadratic) or to speed search.
|
|
39
|
+
*/
|
|
40
|
+
export interface SolveHints {
|
|
41
|
+
solvable: boolean;
|
|
42
|
+
/** The ONE equation numerically inverted for this field. */
|
|
43
|
+
governingEquationId?: string;
|
|
44
|
+
min?: number;
|
|
45
|
+
max?: number;
|
|
46
|
+
/** Optional Newton seed (canonical units). */
|
|
47
|
+
guess?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Binds a field to a diagram hotspot. `hotspotId` is the id of the placed
|
|
51
|
+
* canvas tile — the tile itself carries the reverse link by storing the field
|
|
52
|
+
* id in `PlacedMeasurementRef.columnId` (the existing template-canvas
|
|
53
|
+
* column-anchor channel, reused with field-id semantics).
|
|
54
|
+
*/
|
|
55
|
+
export interface DiagramBinding {
|
|
56
|
+
hotspotId: string;
|
|
57
|
+
}
|
|
58
|
+
interface CalculatorFieldBase {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
role: FieldRole;
|
|
62
|
+
description?: string;
|
|
63
|
+
binding?: DiagramBinding;
|
|
64
|
+
}
|
|
65
|
+
export type CalculatorField = CalculatorFieldBase & ({
|
|
66
|
+
kind: ColumnType.Number;
|
|
67
|
+
defaultValue?: number;
|
|
68
|
+
solve?: SolveHints;
|
|
69
|
+
} | {
|
|
70
|
+
kind: ColumnType.Measurement;
|
|
71
|
+
unit: FieldUnitConfig;
|
|
72
|
+
/** Canonical (µm / µm² / µm³ per unit.dimension). */
|
|
73
|
+
defaultValue?: number;
|
|
74
|
+
solve?: SolveHints;
|
|
75
|
+
} | {
|
|
76
|
+
kind: ColumnType.Angle;
|
|
77
|
+
/** Display unit; canonical storage is always degrees. */
|
|
78
|
+
angleUnit?: AngleUnit;
|
|
79
|
+
defaultValue?: number;
|
|
80
|
+
solve?: SolveHints;
|
|
81
|
+
} | {
|
|
82
|
+
kind: ColumnType.SingleSelect;
|
|
83
|
+
columnData: SelectColumnData;
|
|
84
|
+
defaultValue?: string;
|
|
85
|
+
} | {
|
|
86
|
+
kind: ColumnType.MultiSelect;
|
|
87
|
+
columnData: SelectColumnData;
|
|
88
|
+
defaultValue?: string[];
|
|
89
|
+
} | {
|
|
90
|
+
kind: ColumnType.ConversionTable;
|
|
91
|
+
columnData: ConversionTableColumnData;
|
|
92
|
+
/** The selected conversion's label. */
|
|
93
|
+
defaultValue?: string;
|
|
94
|
+
} | {
|
|
95
|
+
kind: ColumnType.Instructions;
|
|
96
|
+
columnData: InstructionsColumnData;
|
|
97
|
+
});
|
|
98
|
+
/**
|
|
99
|
+
* A forward equation computing one output field. Mirrors the existing
|
|
100
|
+
* `Formula` shape (`expression` + variable map), but variables bind to field
|
|
101
|
+
* ids and the equation lives inside the definition rather than in the org
|
|
102
|
+
* `formulas` collection. Variables may reference other OUTPUT fields — their
|
|
103
|
+
* equations are evaluated recursively (cycles are rejected by validation).
|
|
104
|
+
*/
|
|
105
|
+
export interface CalculatorEquation {
|
|
106
|
+
id: string;
|
|
107
|
+
/** The output field this equation computes. */
|
|
108
|
+
targetFieldId: string;
|
|
109
|
+
/** math.js forward expression, e.g. 'A * B * C'. */
|
|
110
|
+
expression: string;
|
|
111
|
+
/** Variable letter -> field id. */
|
|
112
|
+
variableToFieldId: Record<string, string>;
|
|
113
|
+
}
|
|
114
|
+
/** A runtime/preview value for one field, in canonical units. */
|
|
115
|
+
export type CalculatorFieldValue = number | string | string[] | null;
|
|
116
|
+
export type CalculatorValues = Record<string, CalculatorFieldValue>;
|
|
117
|
+
export interface CalculatorDefinition {
|
|
118
|
+
id: string;
|
|
119
|
+
/** CALCULATOR_SCHEMA_VERSION captured at author time. */
|
|
120
|
+
schemaVersion: number;
|
|
121
|
+
/** Monotonic content revision; bumped on publish. Offline-cache key with updatedAt. */
|
|
122
|
+
version: number;
|
|
123
|
+
name: string;
|
|
124
|
+
description: string;
|
|
125
|
+
folderId: string | null;
|
|
126
|
+
/** Inputs + outputs in one list, discriminated by `role`. */
|
|
127
|
+
fields: CalculatorField[];
|
|
128
|
+
equations: CalculatorEquation[];
|
|
129
|
+
/** Diagram file (files subcollection doc carrying the Skia canvas). */
|
|
130
|
+
diagramFileId?: string | null;
|
|
131
|
+
/** Public calculators are readable by everyone; rules/query key. */
|
|
132
|
+
isPublic: boolean;
|
|
133
|
+
sharedUserIds?: string[];
|
|
134
|
+
sharedOrgIds?: string[];
|
|
135
|
+
createdBy?: {
|
|
136
|
+
userId: string;
|
|
137
|
+
firstName?: string;
|
|
138
|
+
lastName?: string;
|
|
139
|
+
};
|
|
140
|
+
createdAt?: Date;
|
|
141
|
+
updatedAt?: Date;
|
|
142
|
+
}
|
|
143
|
+
export declare const fieldsByRole: (definition: Pick<CalculatorDefinition, "fields">, role: FieldRole) => CalculatorField[];
|
|
144
|
+
export declare const findField: (definition: Pick<CalculatorDefinition, "fields">, fieldId: string) => CalculatorField | undefined;
|
|
145
|
+
export declare const findEquation: (definition: Pick<CalculatorDefinition, "equations">, equationId: string) => CalculatorEquation | undefined;
|
|
146
|
+
/** The equation targeting an output field, if any. */
|
|
147
|
+
export declare const equationForField: (definition: Pick<CalculatorDefinition, "equations">, fieldId: string) => CalculatorEquation | undefined;
|
|
148
|
+
/** The dimension a field's numeric value carries. */
|
|
149
|
+
export declare const fieldDimension: (field: CalculatorField) => FieldDimension;
|
|
150
|
+
/** Fields whose value participates in equations as a number. */
|
|
151
|
+
export declare const isNumericFieldKind: (kind: CalculatorFieldKind) => kind is ColumnType.Number | ColumnType.Measurement | ColumnType.Angle | ColumnType.ConversionTable;
|
|
152
|
+
/** Field kinds an equation may target (compute into). */
|
|
153
|
+
export declare const isEquationTargetKind: (kind: CalculatorFieldKind) => kind is ColumnType.Number | ColumnType.Measurement | ColumnType.Angle;
|
|
154
|
+
/**
|
|
155
|
+
* Synthesize a ColumnConfig for a field so the existing table/tile components
|
|
156
|
+
* (built around ColumnConfig) can render calculator fields unchanged.
|
|
157
|
+
*/
|
|
158
|
+
export declare const fieldToColumnConfig: (field: CalculatorField) => ColumnConfig;
|
|
159
|
+
/** Default per-field unit config for a fresh measurement field. */
|
|
160
|
+
export declare const defaultFieldUnitConfig: (dimension?: MeasurementDimension) => FieldUnitConfig;
|
|
161
|
+
/** Seed for the create-new-calculator stub doc (mirrors the template flow). */
|
|
162
|
+
export declare const createEmptyCalculatorDefinition: (id: string) => Omit<CalculatorDefinition, "createdAt" | "updatedAt" | "createdBy">;
|
|
163
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ColumnType, Units, } from '../types/firestore.js';
|
|
2
|
+
import { unitsForDimension } from './units.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// The Construction Calculator definition — the shared artifact the web
|
|
5
|
+
// authoring tool produces, the mobile runtime interprets, and sync ships.
|
|
6
|
+
//
|
|
7
|
+
// Distinct from the loosely-typed Template/ColumnConfig model, but reuses its
|
|
8
|
+
// vocabulary where it fits: field kinds ARE ColumnType values (subset) and the
|
|
9
|
+
// per-kind payloads reuse the existing column-data shapes, so the table UI
|
|
10
|
+
// and the runtime tiles can share components with templates.
|
|
11
|
+
//
|
|
12
|
+
// CANONICAL VALUES: every numeric value in a definition (defaultValue, solve
|
|
13
|
+
// brackets) and every runtime field value is canonical — µm for length, µm²
|
|
14
|
+
// area, µm³ volume, degrees for angle, raw for dimensionless (see units.ts).
|
|
15
|
+
//
|
|
16
|
+
// EXPRESSION SEMANTICS: equations are math.js expressions evaluated over a
|
|
17
|
+
// canonical scope. Numeric literals inside an expression are therefore
|
|
18
|
+
// canonical too (a bare `2` multiplying a length is a plain scalar; a bare
|
|
19
|
+
// additive length constant would be micrometers). Authors should express
|
|
20
|
+
// length constants as input fields with a defaultValue instead of literals.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/**
|
|
23
|
+
* Version of the definition format itself. Bumped only on breaking schema
|
|
24
|
+
* changes; independent of the npm package version and of each calculator's
|
|
25
|
+
* own content `version`.
|
|
26
|
+
*/
|
|
27
|
+
export const CALCULATOR_SCHEMA_VERSION = 1;
|
|
28
|
+
export const CALCULATOR_FIELD_KINDS = [
|
|
29
|
+
ColumnType.Number,
|
|
30
|
+
ColumnType.Measurement,
|
|
31
|
+
ColumnType.Angle,
|
|
32
|
+
ColumnType.SingleSelect,
|
|
33
|
+
ColumnType.MultiSelect,
|
|
34
|
+
ColumnType.ConversionTable,
|
|
35
|
+
ColumnType.Instructions,
|
|
36
|
+
];
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Helpers
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
export const fieldsByRole = (definition, role) => definition.fields.filter((f) => f.role === role);
|
|
41
|
+
export const findField = (definition, fieldId) => definition.fields.find((f) => f.id === fieldId);
|
|
42
|
+
export const findEquation = (definition, equationId) => definition.equations.find((e) => e.id === equationId);
|
|
43
|
+
/** The equation targeting an output field, if any. */
|
|
44
|
+
export const equationForField = (definition, fieldId) => definition.equations.find((e) => e.targetFieldId === fieldId);
|
|
45
|
+
/** The dimension a field's numeric value carries. */
|
|
46
|
+
export const fieldDimension = (field) => {
|
|
47
|
+
switch (field.kind) {
|
|
48
|
+
case ColumnType.Measurement:
|
|
49
|
+
return field.unit.dimension;
|
|
50
|
+
case ColumnType.Angle:
|
|
51
|
+
return 'angle';
|
|
52
|
+
default:
|
|
53
|
+
return 'none';
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/** Fields whose value participates in equations as a number. */
|
|
57
|
+
export const isNumericFieldKind = (kind) => kind === ColumnType.Number ||
|
|
58
|
+
kind === ColumnType.Measurement ||
|
|
59
|
+
kind === ColumnType.Angle ||
|
|
60
|
+
kind === ColumnType.ConversionTable;
|
|
61
|
+
/** Field kinds an equation may target (compute into). */
|
|
62
|
+
export const isEquationTargetKind = (kind) => kind === ColumnType.Number ||
|
|
63
|
+
kind === ColumnType.Measurement ||
|
|
64
|
+
kind === ColumnType.Angle;
|
|
65
|
+
/**
|
|
66
|
+
* Synthesize a ColumnConfig for a field so the existing table/tile components
|
|
67
|
+
* (built around ColumnConfig) can render calculator fields unchanged.
|
|
68
|
+
*/
|
|
69
|
+
export const fieldToColumnConfig = (field) => {
|
|
70
|
+
switch (field.kind) {
|
|
71
|
+
case ColumnType.SingleSelect:
|
|
72
|
+
return {
|
|
73
|
+
id: field.id,
|
|
74
|
+
name: field.name,
|
|
75
|
+
type: ColumnType.SingleSelect,
|
|
76
|
+
columnData: field.columnData,
|
|
77
|
+
};
|
|
78
|
+
case ColumnType.MultiSelect:
|
|
79
|
+
return {
|
|
80
|
+
id: field.id,
|
|
81
|
+
name: field.name,
|
|
82
|
+
type: ColumnType.MultiSelect,
|
|
83
|
+
columnData: field.columnData,
|
|
84
|
+
};
|
|
85
|
+
case ColumnType.ConversionTable:
|
|
86
|
+
return {
|
|
87
|
+
id: field.id,
|
|
88
|
+
name: field.name,
|
|
89
|
+
type: ColumnType.ConversionTable,
|
|
90
|
+
columnData: field.columnData,
|
|
91
|
+
};
|
|
92
|
+
case ColumnType.Instructions:
|
|
93
|
+
return {
|
|
94
|
+
id: field.id,
|
|
95
|
+
name: field.name,
|
|
96
|
+
type: ColumnType.Instructions,
|
|
97
|
+
columnData: field.columnData,
|
|
98
|
+
};
|
|
99
|
+
case ColumnType.Number:
|
|
100
|
+
return { id: field.id, name: field.name, type: ColumnType.Number };
|
|
101
|
+
case ColumnType.Angle:
|
|
102
|
+
return { id: field.id, name: field.name, type: ColumnType.Angle };
|
|
103
|
+
case ColumnType.Measurement:
|
|
104
|
+
return { id: field.id, name: field.name, type: ColumnType.Measurement };
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
/** Default per-field unit config for a fresh measurement field. */
|
|
108
|
+
export const defaultFieldUnitConfig = (dimension = 'length') => ({
|
|
109
|
+
dimension,
|
|
110
|
+
defaultUnit: dimension === 'length'
|
|
111
|
+
? Units.FractionalInches
|
|
112
|
+
: dimension === 'area'
|
|
113
|
+
? 'sq_ft'
|
|
114
|
+
: 'cu_ft',
|
|
115
|
+
allowedUnits: unitsForDimension(dimension),
|
|
116
|
+
});
|
|
117
|
+
/** Seed for the create-new-calculator stub doc (mirrors the template flow). */
|
|
118
|
+
export const createEmptyCalculatorDefinition = (id) => ({
|
|
119
|
+
id,
|
|
120
|
+
schemaVersion: CALCULATOR_SCHEMA_VERSION,
|
|
121
|
+
version: 0,
|
|
122
|
+
name: '',
|
|
123
|
+
description: '',
|
|
124
|
+
folderId: null,
|
|
125
|
+
fields: [],
|
|
126
|
+
equations: [],
|
|
127
|
+
diagramFileId: null,
|
|
128
|
+
isPublic: false,
|
|
129
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CalculatorDefinition, CalculatorValues } from './schema.js';
|
|
2
|
+
export type SolveFailure = {
|
|
3
|
+
kind: 'unknown-field';
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'not-solvable';
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'no-governing-equation';
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'missing-target';
|
|
10
|
+
targetFieldId: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'missing-inputs';
|
|
13
|
+
missingFieldIds: string[];
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'invalid-bracket';
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'no-sign-change';
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'eval-error';
|
|
20
|
+
error: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'non-convergent';
|
|
23
|
+
};
|
|
24
|
+
export type SolveResult = {
|
|
25
|
+
ok: true;
|
|
26
|
+
value: number;
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
failure: SolveFailure;
|
|
30
|
+
};
|
|
31
|
+
export interface SolveOptions {
|
|
32
|
+
maxIter?: number;
|
|
33
|
+
/** Points probed when the bracket endpoints don't straddle a root. */
|
|
34
|
+
scanSamples?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Numerically solve a solvable field from its governing equation, given the
|
|
38
|
+
* equation's output value (and every other referenced value) in `values`.
|
|
39
|
+
*/
|
|
40
|
+
export declare const solveForField: ({ definition, fieldId, values, options, }: {
|
|
41
|
+
definition: Pick<CalculatorDefinition, "fields" | "equations">;
|
|
42
|
+
fieldId: string;
|
|
43
|
+
/** Canonical values; must include the governing equation's target value. */
|
|
44
|
+
values: CalculatorValues;
|
|
45
|
+
options?: SolveOptions;
|
|
46
|
+
}) => SolveResult;
|