@reekon-tools/boldr-utils 1.10.3 → 1.11.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.
Files changed (75) hide show
  1. package/dist/calculator/chips.d.ts +32 -0
  2. package/dist/calculator/chips.js +79 -0
  3. package/dist/calculator/display.d.ts +78 -0
  4. package/dist/calculator/display.js +223 -0
  5. package/dist/calculator/index.d.ts +3 -0
  6. package/dist/calculator/index.js +8 -3
  7. package/dist/calculator/runtime.d.ts +61 -0
  8. package/dist/calculator/runtime.js +430 -0
  9. package/dist/theme/index.d.ts +3 -0
  10. package/dist/theme/index.js +3 -0
  11. package/dist/theme/preset.d.ts +120 -0
  12. package/dist/theme/preset.js +25 -0
  13. package/dist/theme/semantic.d.ts +136 -0
  14. package/dist/theme/semantic.js +116 -0
  15. package/package.json +1 -1
  16. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  17. package/dist/canvas/AnnotationCanvas.js +0 -10
  18. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  19. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  20. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  21. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  22. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  23. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  24. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  25. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  26. package/dist/canvas/Tool.d.ts +0 -38
  27. package/dist/canvas/Tool.js +0 -1
  28. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  29. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  30. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  31. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  32. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  33. package/dist/canvas/elements/ShapeElement.js +0 -62
  34. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  35. package/dist/canvas/elements/StrokeElement.js +0 -18
  36. package/dist/canvas/measurementPicker.d.ts +0 -10
  37. package/dist/canvas/measurementPicker.js +0 -1
  38. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  39. package/dist/canvas/measurementStampOverlay.js +0 -1
  40. package/dist/canvas/pointerAdapter.d.ts +0 -3
  41. package/dist/canvas/pointerAdapter.js +0 -19
  42. package/dist/canvas/stampLayout.d.ts +0 -5
  43. package/dist/canvas/stampLayout.js +0 -14
  44. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  45. package/dist/canvas/tools/measurementStampTool.js +0 -37
  46. package/dist/canvas/tools/panTool.d.ts +0 -5
  47. package/dist/canvas/tools/panTool.js +0 -25
  48. package/dist/canvas/tools/penTool.d.ts +0 -13
  49. package/dist/canvas/tools/penTool.js +0 -68
  50. package/dist/canvas/tools/selectTool.d.ts +0 -2
  51. package/dist/canvas/tools/selectTool.js +0 -182
  52. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  53. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  54. package/dist/canvas/viewport.d.ts +0 -16
  55. package/dist/canvas/viewport.js +0 -54
  56. package/dist/data/AnnotationDataContext.d.ts +0 -8
  57. package/dist/data/AnnotationDataContext.js +0 -11
  58. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  59. package/dist/data/AnnotationDataProvider.js +0 -4
  60. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  61. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  62. package/dist/data/canvasPersistence.d.ts +0 -3
  63. package/dist/data/canvasPersistence.js +0 -26
  64. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  65. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  66. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  67. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  68. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  69. package/dist/data/hooks/useAnnotationList.js +0 -26
  70. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  71. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  72. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  73. package/dist/hooks/useParseMeasurement.js +0 -14
  74. package/dist/utils/evaluateFormula.d.ts +0 -20
  75. package/dist/utils/evaluateFormula.js +0 -31
@@ -0,0 +1,32 @@
1
+ import type { ResolvedField } from './runtime.js';
2
+ export declare const CALC_COLORS: {
3
+ /** Ink on the white/light-grey chips. */
4
+ readonly text: "#181A1C";
5
+ readonly chipBg: "#E8EAED";
6
+ readonly chipBorder: "#BCBEC2";
7
+ readonly letterBg: "#E8EAED";
8
+ readonly focusRing: "#FFAD00";
9
+ readonly focusFill: "#FFF8DB";
10
+ /** A value the user (or a tool) typed. */
11
+ readonly entered: "#0066FF";
12
+ readonly enteredLetter: "#50A6FF";
13
+ /** A value the equation solved for. */
14
+ readonly computed: "#BD30A8";
15
+ readonly computedLetter: "#6C155F";
16
+ /**
17
+ * A value that disagrees with its equation. RED, not a fourth blue: blue
18
+ * means entered and purple means calculated, so a navy chip read as a third
19
+ * kind of VALUE rather than as a problem — and its number stayed legible
20
+ * enough to cut against. Red is the app's warning colour everywhere else.
21
+ */
22
+ readonly conflict: "#E83D2F";
23
+ readonly conflictLetter: "#DA1100";
24
+ readonly conflictClear: "#E83D2F";
25
+ };
26
+ export type ChipState = 'empty' | 'entered' | 'computed' | 'conflict' | 'default';
27
+ export declare const chipStateOf: (resolved: ResolvedField) => ChipState;
28
+ export declare const CHIP_STYLE: Record<ChipState, {
29
+ background: string;
30
+ letterBackground: string;
31
+ text: string;
32
+ }>;
@@ -0,0 +1,79 @@
1
+ // The value-state palette every calculator surface paints with: mobile's table
2
+ // rows and diagram cards, and the web viewer's read-only cards. It lives here
3
+ // (not in either app) because the COLOR IS THE MEANING — blue says a human
4
+ // typed it, purple says the calculator worked it out, red says the two
5
+ // disagree. A surface that re-derives that mapping locally is one refactor away
6
+ // from telling a crew a computed number was measured.
7
+ //
8
+ // The runtime is a light paper island on both platforms — frame `5287:108428`
9
+ // samples a `#2B2C2E` header over an `#E8EAED` canvas with `#FFFFFF` field
10
+ // cards. That is the exact opposite of the BASIC calculator (`5365:17187`),
11
+ // which is fully dark. Do not "fix" one to match the other.
12
+ import { FormColors, FormulaColors, GeneralColors, RedColors, TileColors, YellowColors, } from '../theme/colors.js';
13
+ export const CALC_COLORS = {
14
+ /** Ink on the white/light-grey chips. */
15
+ text: GeneralColors.darkGrey,
16
+ chipBg: FormColors.lightGrey2,
17
+ chipBorder: FormColors.lightGrey1,
18
+ letterBg: FormColors.lightGrey2,
19
+ focusRing: YellowColors.mainYellow,
20
+ // A pale cream wash for "focused and empty, waiting for the keypad". It only
21
+ // exists on the light canvas and has no palette equivalent — mainYellow at
22
+ // low alpha goes muddy over lightGrey2.
23
+ focusFill: '#FFF8DB',
24
+ /** A value the user (or a tool) typed. */
25
+ entered: TileColors.activeBlue,
26
+ enteredLetter: TileColors.lightBlue1,
27
+ /** A value the equation solved for. */
28
+ computed: FormulaColors.purple,
29
+ computedLetter: FormulaColors.darkPurple,
30
+ /**
31
+ * A value that disagrees with its equation. RED, not a fourth blue: blue
32
+ * means entered and purple means calculated, so a navy chip read as a third
33
+ * kind of VALUE rather than as a problem — and its number stayed legible
34
+ * enough to cut against. Red is the app's warning colour everywhere else.
35
+ */
36
+ conflict: RedColors.red,
37
+ conflictLetter: RedColors.warningRed,
38
+ conflictClear: RedColors.red,
39
+ };
40
+ export const chipStateOf = (resolved) => {
41
+ if (resolved.conflict)
42
+ return 'conflict';
43
+ if (resolved.origin === 'user' || resolved.origin === 'tool')
44
+ return 'entered';
45
+ if (resolved.origin === 'computed' || resolved.origin === 'solved')
46
+ return 'computed';
47
+ if (resolved.origin === 'default')
48
+ return 'default';
49
+ return 'empty';
50
+ };
51
+ export const CHIP_STYLE = {
52
+ empty: {
53
+ background: GeneralColors.white,
54
+ letterBackground: CALC_COLORS.letterBg,
55
+ text: CALC_COLORS.text,
56
+ },
57
+ default: {
58
+ background: GeneralColors.white,
59
+ letterBackground: CALC_COLORS.letterBg,
60
+ text: CALC_COLORS.text,
61
+ },
62
+ entered: {
63
+ background: CALC_COLORS.entered,
64
+ letterBackground: CALC_COLORS.enteredLetter,
65
+ text: GeneralColors.white,
66
+ },
67
+ computed: {
68
+ background: CALC_COLORS.computed,
69
+ letterBackground: CALC_COLORS.computedLetter,
70
+ text: GeneralColors.white,
71
+ },
72
+ conflict: {
73
+ background: CALC_COLORS.conflict,
74
+ letterBackground: CALC_COLORS.conflictLetter,
75
+ // Full white: the chip shows a dash, and a dash has to be legible. The old
76
+ // 55% wash existed to de-emphasise a number that is no longer drawn.
77
+ text: GeneralColors.white,
78
+ },
79
+ };
@@ -0,0 +1,78 @@
1
+ import { type CalculatorUnit } from './units.js';
2
+ import type { CalculatorDefinition, CalculatorField } from './schema.js';
3
+ import type { ResolvedField } from './runtime.js';
4
+ export type UnitSystem = 'imperial' | 'metric';
5
+ export type DisplayUnitOverrides = Readonly<Record<string, CalculatorUnit>>;
6
+ /** The unit a field currently displays in (session override > field default). */
7
+ export declare const displayUnitFor: (field: CalculatorField, overrides?: DisplayUnitOverrides) => CalculatorUnit | null;
8
+ /** Units the picker offers for a field; empty for unit-less kinds. */
9
+ export declare const allowedDisplayUnits: (field: CalculatorField) => CalculatorUnit[];
10
+ /**
11
+ * Parse raw keypad text (in the given display unit) into a canonical value.
12
+ * Fractions and mixed numbers are accepted for measurements and angles
13
+ * (mirrors the web editor's EditEquation semantics). Returns null when the
14
+ * draft isn't parseable yet — callers keep the last good value.
15
+ */
16
+ export declare const parseFieldInput: (field: CalculatorField, unit: CalculatorUnit | null, raw: string) => number | null;
17
+ /** Format a canonical value for display in the given unit. */
18
+ export declare const formatFieldValue: (field: CalculatorField, unit: CalculatorUnit | null, canonical: number) => {
19
+ value: string;
20
+ unit: string;
21
+ };
22
+ /**
23
+ * What a conflicting field shows INSTEAD of its number. A value the runtime
24
+ * knows disagrees with its equation must not be readable as a measurement —
25
+ * someone cuts to what's on the screen — so the number is withheld rather than
26
+ * dimmed, and the detail sheet is where the entered value is recoverable.
27
+ * Same em dash the empty-output placeholder uses.
28
+ */
29
+ export declare const CONFLICT_DASH = "\u2014";
30
+ /**
31
+ * The text every value surface shows for a resolved field — table row, diagram
32
+ * stamp, and the detail sheet's card all route through here so the conflict
33
+ * dash can't ship in one of them and not the others. Null means "nothing to
34
+ * show": the caller draws its own empty placeholder.
35
+ */
36
+ export declare const resolvedValueText: (field: CalculatorField, unit: CalculatorUnit | null, resolved: ResolvedField) => string | null;
37
+ /**
38
+ * The letter chip a field wears in both the diagram badges and the table
39
+ * rows: sequential A, B, C… over the definition's fields (Instructions
40
+ * excluded — they carry no value).
41
+ */
42
+ export declare const letterLabelFor: (definition: Pick<CalculatorDefinition, "fields">, fieldId: string) => string;
43
+ /**
44
+ * Whether a user-entered value and a freshly computed one count as the same
45
+ * number: they format identically in the field's DEFAULT unit at its
46
+ * configured tolerance (i.e. the user couldn't tell them apart), with a tiny
47
+ * absolute/relative epsilon as a float-noise backstop. Deliberately ignores
48
+ * session unit overrides so conflict-ness doesn't change with the unit
49
+ * toggle.
50
+ */
51
+ export declare const valuesAgree: (field: CalculatorField, entered: number, computed: number) => boolean;
52
+ /** The structured draft the fraction keypad edits. All parts are digit text. */
53
+ export interface KeypadParts {
54
+ negative: boolean;
55
+ /** Feet, in feet-inches mode only. */
56
+ feet: string;
57
+ /** Whole number (or decimal text in decimal mode). */
58
+ whole: string;
59
+ numerator: string;
60
+ denominator: string;
61
+ }
62
+ export declare const emptyKeypadParts: () => KeypadParts;
63
+ export type KeypadMode = 'decimal' | 'fractional' | 'feetFractional';
64
+ /** Which keypad layout a display unit gets. */
65
+ export declare const keypadModeForUnit: (unit: CalculatorUnit | null) => KeypadMode;
66
+ /**
67
+ * Convert a keypad draft into a canonical value for the field. Returns null
68
+ * when the draft is empty/incomplete (callers keep the previous value).
69
+ * Feet-inches composes feet + fractional inches directly, since a single
70
+ * "F W N/D" string has no parseable representation in a feet unit.
71
+ */
72
+ export declare const keypadPartsToCanonical: (field: CalculatorField, unit: CalculatorUnit | null, parts: KeypadParts) => number | null;
73
+ /**
74
+ * Overrides implementing the English/Metric toggle: every measurement field
75
+ * flips to its dimension's preferred unit (constrained to allowedUnits).
76
+ * Fields whose pick equals their default get no override. Angles stay put.
77
+ */
78
+ export declare const mapUnitSystem: (definition: Pick<CalculatorDefinition, "fields">, system: UnitSystem) => Record<string, CalculatorUnit>;
@@ -0,0 +1,223 @@
1
+ // Display-unit edge of the calculator runtime: parsing keypad input into
2
+ // canonical values, formatting canonical values back out, per-field unit
3
+ // choices, and the letter labels shared by diagram badges and table rows.
4
+ // Pure functions only, shared by every consuming runtime (mobile's run screen,
5
+ // the web read-only viewer) so a value can't render two ways.
6
+ import { ColumnType, Units } from '../types/firestore.js';
7
+ import { formatCanonical, parseToCanonical, unitsForDimension, } from './units.js';
8
+ /** The unit a field currently displays in (session override > field default). */
9
+ export const displayUnitFor = (field, overrides = {}) => {
10
+ switch (field.kind) {
11
+ case ColumnType.Measurement:
12
+ return overrides[field.id] ?? field.unit.defaultUnit;
13
+ case ColumnType.Angle:
14
+ return overrides[field.id] ?? field.angleUnit ?? 'deg';
15
+ default:
16
+ return null;
17
+ }
18
+ };
19
+ /** Units the picker offers for a field; empty for unit-less kinds. */
20
+ export const allowedDisplayUnits = (field) => {
21
+ switch (field.kind) {
22
+ case ColumnType.Measurement: {
23
+ const allowed = field.unit.allowedUnits?.length
24
+ ? field.unit.allowedUnits
25
+ : unitsForDimension(field.unit.dimension);
26
+ return allowed.includes(field.unit.defaultUnit)
27
+ ? allowed
28
+ : [field.unit.defaultUnit, ...allowed];
29
+ }
30
+ case ColumnType.Angle:
31
+ return ['deg', 'rad'];
32
+ default:
33
+ return [];
34
+ }
35
+ };
36
+ /**
37
+ * Parse raw keypad text (in the given display unit) into a canonical value.
38
+ * Fractions and mixed numbers are accepted for measurements and angles
39
+ * (mirrors the web editor's EditEquation semantics). Returns null when the
40
+ * draft isn't parseable yet — callers keep the last good value.
41
+ */
42
+ export const parseFieldInput = (field, unit, raw) => {
43
+ switch (field.kind) {
44
+ case ColumnType.Measurement:
45
+ case ColumnType.Angle:
46
+ return unit ? parseToCanonical(raw, unit) : null;
47
+ case ColumnType.Number: {
48
+ const trimmed = raw.trim();
49
+ if (trimmed === '')
50
+ return null;
51
+ const parsed = Number(trimmed);
52
+ return Number.isFinite(parsed) ? parsed : null;
53
+ }
54
+ default:
55
+ return null;
56
+ }
57
+ };
58
+ // Matches the web editor's unit-less number formatting (6 significant
59
+ // figures, no trailing zeros).
60
+ const trimNumber = (value) => Number.isFinite(value) ? Number(value.toPrecision(6)).toString() : 'NaN';
61
+ /** Format a canonical value for display in the given unit. */
62
+ export const formatFieldValue = (field, unit, canonical) => {
63
+ if (!unit)
64
+ return { value: trimNumber(canonical), unit: '' };
65
+ return formatCanonical(canonical, unit, field.kind === ColumnType.Measurement
66
+ ? {
67
+ fractionalTolerance: field.unit.fractionalTolerance,
68
+ decimalTolerance: field.unit.decimalTolerance,
69
+ }
70
+ : undefined);
71
+ };
72
+ /**
73
+ * What a conflicting field shows INSTEAD of its number. A value the runtime
74
+ * knows disagrees with its equation must not be readable as a measurement —
75
+ * someone cuts to what's on the screen — so the number is withheld rather than
76
+ * dimmed, and the detail sheet is where the entered value is recoverable.
77
+ * Same em dash the empty-output placeholder uses.
78
+ */
79
+ export const CONFLICT_DASH = '—';
80
+ /**
81
+ * The text every value surface shows for a resolved field — table row, diagram
82
+ * stamp, and the detail sheet's card all route through here so the conflict
83
+ * dash can't ship in one of them and not the others. Null means "nothing to
84
+ * show": the caller draws its own empty placeholder.
85
+ */
86
+ export const resolvedValueText = (field, unit, resolved) => {
87
+ if (resolved.conflict)
88
+ return CONFLICT_DASH;
89
+ if (resolved.value == null)
90
+ return null;
91
+ if (typeof resolved.value === 'number') {
92
+ return formatFieldValue(field, unit, resolved.value).value;
93
+ }
94
+ return Array.isArray(resolved.value)
95
+ ? resolved.value.join(', ')
96
+ : String(resolved.value);
97
+ };
98
+ // 0 -> A, 25 -> Z, 26 -> AA ...
99
+ const indexToLetters = (index) => {
100
+ let label = '';
101
+ let i = index;
102
+ do {
103
+ label = String.fromCharCode(65 + (i % 26)) + label;
104
+ i = Math.floor(i / 26) - 1;
105
+ } while (i >= 0);
106
+ return label;
107
+ };
108
+ /**
109
+ * The letter chip a field wears in both the diagram badges and the table
110
+ * rows: sequential A, B, C… over the definition's fields (Instructions
111
+ * excluded — they carry no value).
112
+ */
113
+ export const letterLabelFor = (definition, fieldId) => {
114
+ let index = 0;
115
+ for (const field of definition.fields) {
116
+ if (field.kind === ColumnType.Instructions)
117
+ continue;
118
+ if (field.id === fieldId)
119
+ return indexToLetters(index);
120
+ index++;
121
+ }
122
+ return '';
123
+ };
124
+ /**
125
+ * Whether a user-entered value and a freshly computed one count as the same
126
+ * number: they format identically in the field's DEFAULT unit at its
127
+ * configured tolerance (i.e. the user couldn't tell them apart), with a tiny
128
+ * absolute/relative epsilon as a float-noise backstop. Deliberately ignores
129
+ * session unit overrides so conflict-ness doesn't change with the unit
130
+ * toggle.
131
+ */
132
+ export const valuesAgree = (field, entered, computed) => {
133
+ const unit = displayUnitFor(field, {});
134
+ const enteredText = formatFieldValue(field, unit, entered).value;
135
+ const computedText = formatFieldValue(field, unit, computed).value;
136
+ if (enteredText === computedText)
137
+ return true;
138
+ const epsilon = Math.max(1e-6, 1e-9 * Math.abs(computed));
139
+ return Math.abs(entered - computed) <= epsilon;
140
+ };
141
+ // Preferred display units per dimension for the header's unit-system toggle,
142
+ // best-first. The pick is constrained to each field's allowedUnits.
143
+ const SYSTEM_PREFERENCES = {
144
+ imperial: {
145
+ length: [
146
+ Units.FractionalInches,
147
+ Units.FeetInchesFractional,
148
+ Units.Inches,
149
+ Units.Feet,
150
+ Units.FeetInchesDecimal,
151
+ ],
152
+ area: ['sq_ft', 'sq_in', 'sq_yd'],
153
+ volume: ['cu_ft', 'cu_in', 'cu_yd', 'gallon'],
154
+ },
155
+ metric: {
156
+ length: [Units.Millimeters, Units.Centimeters, Units.Meters],
157
+ area: ['sq_m', 'sq_cm', 'sq_mm'],
158
+ volume: ['cu_m', 'liter', 'cu_cm', 'cu_mm'],
159
+ },
160
+ };
161
+ export const emptyKeypadParts = () => ({
162
+ negative: false,
163
+ feet: '',
164
+ whole: '',
165
+ numerator: '',
166
+ denominator: '',
167
+ });
168
+ /** Which keypad layout a display unit gets. */
169
+ export const keypadModeForUnit = (unit) => {
170
+ if (unit === Units.FractionalInches)
171
+ return 'fractional';
172
+ if (unit === Units.FeetInchesFractional)
173
+ return 'feetFractional';
174
+ return 'decimal';
175
+ };
176
+ const fractionText = (parts) => {
177
+ const { whole, numerator, denominator } = parts;
178
+ const hasFraction = numerator !== '' && denominator !== '' && denominator !== '0';
179
+ const body = hasFraction
180
+ ? `${whole || '0'} ${numerator}/${denominator}`
181
+ : whole;
182
+ return body;
183
+ };
184
+ /**
185
+ * Convert a keypad draft into a canonical value for the field. Returns null
186
+ * when the draft is empty/incomplete (callers keep the previous value).
187
+ * Feet-inches composes feet + fractional inches directly, since a single
188
+ * "F W N/D" string has no parseable representation in a feet unit.
189
+ */
190
+ export const keypadPartsToCanonical = (field, unit, parts) => {
191
+ const sign = parts.negative ? -1 : 1;
192
+ if (keypadModeForUnit(unit) === 'feetFractional') {
193
+ if (parts.feet === '' && fractionText(parts) === '')
194
+ return null;
195
+ const feetUm = parseToCanonical(parts.feet || '0', Units.Feet);
196
+ const inchesUm = parseToCanonical(fractionText(parts) || '0', Units.Inches);
197
+ if (feetUm == null || inchesUm == null)
198
+ return null;
199
+ return sign * (feetUm + inchesUm);
200
+ }
201
+ const text = fractionText(parts);
202
+ if (text === '')
203
+ return null;
204
+ const parsed = parseFieldInput(field, unit, text);
205
+ return parsed == null ? null : sign * parsed;
206
+ };
207
+ /**
208
+ * Overrides implementing the English/Metric toggle: every measurement field
209
+ * flips to its dimension's preferred unit (constrained to allowedUnits).
210
+ * Fields whose pick equals their default get no override. Angles stay put.
211
+ */
212
+ export const mapUnitSystem = (definition, system) => {
213
+ const overrides = {};
214
+ for (const field of definition.fields) {
215
+ if (field.kind !== ColumnType.Measurement)
216
+ continue;
217
+ const allowed = allowedDisplayUnits(field);
218
+ const pick = SYSTEM_PREFERENCES[system][field.unit.dimension].find((unit) => allowed.includes(unit));
219
+ if (pick && pick !== field.unit.defaultUnit)
220
+ overrides[field.id] = pick;
221
+ }
222
+ return overrides;
223
+ };
@@ -6,4 +6,7 @@ export * from './expressionUnits.js';
6
6
  export * from './evaluate.js';
7
7
  export * from './solve.js';
8
8
  export * from './instance.js';
9
+ export * from './runtime.js';
10
+ export * from './display.js';
11
+ export * from './chips.js';
9
12
  export { calculatorDefinitionSchema, expressionSymbols, validateCalculatorDefinition, type IssueSeverity, type ValidationIssue, type ValidationResult, } from './validate.js';
@@ -1,7 +1,9 @@
1
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.
2
+ // per-field unit system, forward evaluator, two-way numeric solver, definition
3
+ // validation, and the consumption-time runtime (derive pass + display
4
+ // formatting + value-state palette) that turns a saved instance into what a
5
+ // screen shows. Pure logic (mathjs + zod only — no Skia/React), safe on web,
6
+ // native, and Node.
5
7
  export * from './schema.js';
6
8
  export * from './categories.js';
7
9
  export * from './units.js';
@@ -10,4 +12,7 @@ export * from './expressionUnits.js';
10
12
  export * from './evaluate.js';
11
13
  export * from './solve.js';
12
14
  export * from './instance.js';
15
+ export * from './runtime.js';
16
+ export * from './display.js';
17
+ export * from './chips.js';
13
18
  export { calculatorDefinitionSchema, expressionSymbols, validateCalculatorDefinition, } from './validate.js';
@@ -0,0 +1,61 @@
1
+ import { type CalculatorDefinition, type CalculatorFieldValue } from './schema.js';
2
+ import type { CalculatorEntryAttribution, CalculatorFieldEntry } from './instance.js';
3
+ export type ResolvedOrigin = 'user' | 'tool' | 'computed' | 'solved' | 'default' | 'empty';
4
+ export interface ResolvedField {
5
+ /** Canonical value to display; null when empty/blocked. */
6
+ value: CalculatorFieldValue | null;
7
+ origin: ResolvedOrigin;
8
+ /**
9
+ * Over-constrained: this entry sits on an equation target whose freshly
10
+ * computed value disagrees beyond the field's display tolerance. Reachable
11
+ * only when the displacement pass below can't move anything — EVERY entry
12
+ * deciding it is locked, or nothing left is invertible. UI: RED chip
13
+ * with the value withheld behind a dash — clearing the entry, or unlocking a
14
+ * participant, resolves it.
15
+ */
16
+ conflict: {
17
+ computedValue: number;
18
+ } | null;
19
+ /** Eval/solve error attributable to this field's equation. */
20
+ error: string | null;
21
+ /** When its equation is blocked: the field ids still needed. */
22
+ missingFieldIds: string[];
23
+ /** Which equation computed/solved this value. */
24
+ viaEquationId: string | null;
25
+ locked: boolean;
26
+ /**
27
+ * Populating this field can only end in a conflict, so entry is refused
28
+ * outright (Asana 1217172568173859). True when the row is showing a
29
+ * CALCULATED value and every other field on its governing equation is
30
+ * entered AND locked: the displacement pass then has nothing left to
31
+ * yield, so a typed value would land as the red conflict chip instead of
32
+ * steering the calculation. Unlocking any one of them clears it.
33
+ */
34
+ populationBlocked: boolean;
35
+ attribution: CalculatorEntryAttribution | null;
36
+ }
37
+ export interface EquationStatus {
38
+ equationId: string;
39
+ targetFieldId: string;
40
+ /** Variables ∪ target — drives the link badges on participating rows. */
41
+ participantFieldIds: string[];
42
+ state: 'computed' | 'solved-a-field' | 'blocked' | 'error';
43
+ /** The field this equation was inverted for (wears the '=' badge). */
44
+ solvedFieldId: string | null;
45
+ }
46
+ export interface DerivedRuntime {
47
+ resolved: Record<string, ResolvedField>;
48
+ equations: EquationStatus[];
49
+ /** fieldId -> equation ids it participates in (focus highlighting). */
50
+ equationsByField: Record<string, string[]>;
51
+ }
52
+ export declare const EMPTY_DERIVED: DerivedRuntime;
53
+ export declare const deriveRuntime: (definition: Pick<CalculatorDefinition, "fields" | "equations">, entries: Readonly<Record<string, CalculatorFieldEntry>>, options?: {
54
+ solveEnabled?: boolean;
55
+ }) => DerivedRuntime;
56
+ /**
57
+ * `populationBlocked` for a single field — the one rule the reducer's guards
58
+ * and the UI's entry affordances share, so a refused "+" and a refused tape
59
+ * reading can never disagree about which fields are enterable.
60
+ */
61
+ export declare const isPopulationBlocked: (definition: Pick<CalculatorDefinition, "fields" | "equations">, entries: Readonly<Record<string, CalculatorFieldEntry>>, fieldId: string) => boolean;