@reekon-tools/boldr-utils 1.10.3 → 1.12.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 (98) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +48 -7
  2. package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +2 -2
  3. package/dist/annotation/canvas/AnnotationCanvasSkia.js +16 -3
  4. package/dist/annotation/canvas/Tool.d.ts +8 -1
  5. package/dist/annotation/canvas/backgroundLayers.d.ts +20 -0
  6. package/dist/annotation/canvas/backgroundLayers.js +98 -0
  7. package/dist/annotation/canvas/elements/BackgroundImageElement.d.ts +10 -7
  8. package/dist/annotation/canvas/elements/BackgroundImageElement.js +45 -11
  9. package/dist/annotation/canvas/elements/BackgroundSvg.d.ts +3 -4
  10. package/dist/annotation/canvas/elements/BackgroundSvg.js +23 -29
  11. package/dist/annotation/canvas/elements/backgroundUrl.js +2 -2
  12. package/dist/annotation/canvas/tools/selectTool.js +183 -19
  13. package/dist/annotation/canvas/useAnnotationCanvasState.js +29 -20
  14. package/dist/annotation/canvas/viewport.js +7 -6
  15. package/dist/annotation/data/AnnotationDataProvider.d.ts +1 -1
  16. package/dist/annotation/data/InMemoryAnnotationProvider.d.ts +1 -1
  17. package/dist/annotation/data/hooks/useAnnotationCanvasDoc.d.ts +7 -1
  18. package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +124 -22
  19. package/dist/annotation/data/hooks/useAnnotationMutations.d.ts +1 -1
  20. package/dist/calculator/chips.d.ts +32 -0
  21. package/dist/calculator/chips.js +79 -0
  22. package/dist/calculator/display.d.ts +78 -0
  23. package/dist/calculator/display.js +223 -0
  24. package/dist/calculator/index.d.ts +3 -0
  25. package/dist/calculator/index.js +8 -3
  26. package/dist/calculator/runtime.d.ts +61 -0
  27. package/dist/calculator/runtime.js +430 -0
  28. package/dist/exports.d.ts +1 -0
  29. package/dist/exports.js +1 -0
  30. package/dist/theme/index.d.ts +3 -0
  31. package/dist/theme/index.js +3 -0
  32. package/dist/theme/preset.d.ts +120 -0
  33. package/dist/theme/preset.js +25 -0
  34. package/dist/theme/semantic.d.ts +136 -0
  35. package/dist/theme/semantic.js +116 -0
  36. package/dist/types/annotation.d.ts +27 -0
  37. package/dist/types/annotation.js +68 -0
  38. package/package.json +1 -1
  39. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  40. package/dist/canvas/AnnotationCanvas.js +0 -10
  41. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  42. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  43. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  44. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  45. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  46. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  47. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  48. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  49. package/dist/canvas/Tool.d.ts +0 -38
  50. package/dist/canvas/Tool.js +0 -1
  51. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  52. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  53. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  54. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  55. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  56. package/dist/canvas/elements/ShapeElement.js +0 -62
  57. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  58. package/dist/canvas/elements/StrokeElement.js +0 -18
  59. package/dist/canvas/measurementPicker.d.ts +0 -10
  60. package/dist/canvas/measurementPicker.js +0 -1
  61. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  62. package/dist/canvas/measurementStampOverlay.js +0 -1
  63. package/dist/canvas/pointerAdapter.d.ts +0 -3
  64. package/dist/canvas/pointerAdapter.js +0 -19
  65. package/dist/canvas/stampLayout.d.ts +0 -5
  66. package/dist/canvas/stampLayout.js +0 -14
  67. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  68. package/dist/canvas/tools/measurementStampTool.js +0 -37
  69. package/dist/canvas/tools/panTool.d.ts +0 -5
  70. package/dist/canvas/tools/panTool.js +0 -25
  71. package/dist/canvas/tools/penTool.d.ts +0 -13
  72. package/dist/canvas/tools/penTool.js +0 -68
  73. package/dist/canvas/tools/selectTool.d.ts +0 -2
  74. package/dist/canvas/tools/selectTool.js +0 -182
  75. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  76. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  77. package/dist/canvas/viewport.d.ts +0 -16
  78. package/dist/canvas/viewport.js +0 -54
  79. package/dist/data/AnnotationDataContext.d.ts +0 -8
  80. package/dist/data/AnnotationDataContext.js +0 -11
  81. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  82. package/dist/data/AnnotationDataProvider.js +0 -4
  83. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  84. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  85. package/dist/data/canvasPersistence.d.ts +0 -3
  86. package/dist/data/canvasPersistence.js +0 -26
  87. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  88. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  89. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  90. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  91. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  92. package/dist/data/hooks/useAnnotationList.js +0 -26
  93. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  94. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  95. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  96. package/dist/hooks/useParseMeasurement.js +0 -14
  97. package/dist/utils/evaluateFormula.d.ts +0 -20
  98. package/dist/utils/evaluateFormula.js +0 -31
@@ -0,0 +1,430 @@
1
+ // The derive pass: everything the calculator UI shows that isn't a raw entry.
2
+ // deriveRuntime(definition, entries) is a pure function — computed outputs,
3
+ // solved fields, conflicts, equation badges all fall out of it, recomputed
4
+ // after every mutation. Nothing here is stored, so it can never go stale.
5
+ //
6
+ // This is the ENGINE both runtimes share: mobile's editable run screen and the
7
+ // web read-only viewer. A saved instance therefore re-derives identically on
8
+ // either platform, which is the whole point — the numbers a crew reads on the
9
+ // desktop preview are the numbers the phone showed when it saved.
10
+ import { ColumnType } from '../types/firestore.js';
11
+ import { equationForField, findField, } from './schema.js';
12
+ import { evaluateOutputs } from './evaluate.js';
13
+ import { solveForField } from './solve.js';
14
+ import { valuesAgree } from './display.js';
15
+ export const EMPTY_DERIVED = {
16
+ resolved: {},
17
+ equations: [],
18
+ equationsByField: {},
19
+ };
20
+ const EMPTY_RESOLVED = {
21
+ conflict: null,
22
+ error: null,
23
+ missingFieldIds: [],
24
+ viaEquationId: null,
25
+ locked: false,
26
+ populationBlocked: false,
27
+ attribution: null,
28
+ };
29
+ const solveHintsOf = (field) => field.kind === ColumnType.Number ||
30
+ field.kind === ColumnType.Measurement ||
31
+ field.kind === ColumnType.Angle
32
+ ? field.solve
33
+ : undefined;
34
+ const entryOrigin = (entry) => entry.source === 'tool' ? 'tool' : 'user';
35
+ const isNumericField = (field) => field?.kind === ColumnType.Number ||
36
+ field?.kind === ColumnType.Measurement ||
37
+ field?.kind === ColumnType.Angle;
38
+ /** Variables ∪ target — the fields an equation ties together. */
39
+ const participantsOf = (equation) => [
40
+ ...new Set([
41
+ ...Object.values(equation.variableToFieldId),
42
+ equation.targetFieldId,
43
+ ]),
44
+ ];
45
+ /**
46
+ * The definition as the solver should see it while inverting `equation` for
47
+ * `fieldId` under the displacement rule below: whether this field may be
48
+ * calculated is a RUNTIME question there (the user left it unlocked), so the
49
+ * authored hints contribute their brackets but not their veto.
50
+ */
51
+ const definitionForDisplacement = (definition, fieldId, equation) => ({
52
+ equations: definition.equations,
53
+ fields: definition.fields.map((field) => {
54
+ if (field.id !== fieldId)
55
+ return field;
56
+ if (field.kind !== ColumnType.Number &&
57
+ field.kind !== ColumnType.Measurement &&
58
+ field.kind !== ColumnType.Angle) {
59
+ return field;
60
+ }
61
+ return {
62
+ ...field,
63
+ solve: {
64
+ ...field.solve,
65
+ solvable: true,
66
+ governingEquationId: equation.id,
67
+ },
68
+ };
69
+ }),
70
+ });
71
+ export const deriveRuntime = (definition, entries, options) => {
72
+ const solveEnabled = options?.solveEnabled ?? true;
73
+ // 1. Project entries to canonical values. Defaults are NOT merged here —
74
+ // numericFieldValue falls back to field.defaultValue during evaluation
75
+ // and solving, so equations see them for free.
76
+ const values = {};
77
+ for (const [fieldId, entry] of Object.entries(entries)) {
78
+ values[fieldId] = entry.value;
79
+ }
80
+ const fieldOrder = new Map(definition.fields.map((f, index) => [f.id, index]));
81
+ /** fieldId -> equation ids it participates in. */
82
+ const equationsByField = {};
83
+ for (const equation of definition.equations) {
84
+ for (const fieldId of participantsOf(equation)) {
85
+ (equationsByField[fieldId] ?? (equationsByField[fieldId] = [])).push(equation.id);
86
+ }
87
+ }
88
+ const equationById = new Map(definition.equations.map((e) => [e.id, e]));
89
+ const equationTargeting = (fieldId) => definition.equations.find((e) => e.targetFieldId === fieldId);
90
+ /**
91
+ * Entries the displacement rule set aside. Shadowing is PRESENTATION: the
92
+ * entry stays in state untouched, so clearing whatever over-determined the
93
+ * equation brings the original number straight back, and a saved instance
94
+ * re-derives identically on reopen.
95
+ */
96
+ const shadowed = new Set();
97
+ /** A value the user is currently holding — an entry no shadow has set aside. */
98
+ const isPinned = (shadows, fieldId) => entries[fieldId] != null && !shadows.has(fieldId);
99
+ /** Entries (minus shadows) ∪ solved — what the runtime knows right now. */
100
+ const knownValues = (shadows, solved) => {
101
+ const known = {};
102
+ for (const [id, value] of Object.entries(values)) {
103
+ if (!shadows.has(id))
104
+ known[id] = value;
105
+ }
106
+ for (const [id, s] of Object.entries(solved))
107
+ known[id] = s.value;
108
+ return known;
109
+ };
110
+ // 2. Solve pass. Two halves: PROPAGATION works out everything derivable from
111
+ // the values the user is holding, and DISPLACEMENT decides which held
112
+ // values have to give way when they can't all hold at once.
113
+ /**
114
+ * Every value the runtime can pin down for a given shadow set, to a fixpoint
115
+ * so each filled-in value cascades into the next equation. Two rules:
116
+ *
117
+ * - a SHADOWED field is taken over by the first equation that can invert for
118
+ * it. The lock is the gate the user actually operates, so the authored
119
+ * `solvable` hint gets no veto here — it only lends its brackets (see
120
+ * definitionForDisplacement). Which equation ends up with the field is not
121
+ * fixed: releasing another entry can hand it to a different one, which is
122
+ * exactly how the newest entry takes the wheel below.
123
+ * - an EMPTY field fills itself in only when its author marked it solvable.
124
+ * solveForField enforces the exactly-one-missing rule (missing-target /
125
+ * missing-inputs fail silently), so under-determined systems stay empty.
126
+ *
127
+ * Pure in `shadows`, so displacement can try a release and throw it away.
128
+ */
129
+ const propagate = (shadows) => {
130
+ const solved = {};
131
+ let progressed = true;
132
+ let sweeps = 0;
133
+ while (progressed && sweeps++ <= definition.fields.length) {
134
+ progressed = false;
135
+ for (const field of definition.fields) {
136
+ if (solved[field.id] || isPinned(shadows, field.id))
137
+ continue;
138
+ if (!shadows.has(field.id)) {
139
+ const hints = solveHintsOf(field);
140
+ if (!hints?.solvable || !hints.governingEquationId)
141
+ continue;
142
+ const result = solveForField({
143
+ definition,
144
+ fieldId: field.id,
145
+ values: knownValues(shadows, solved),
146
+ });
147
+ if (result.ok) {
148
+ solved[field.id] = {
149
+ value: result.value,
150
+ viaEquationId: hints.governingEquationId,
151
+ };
152
+ progressed = true;
153
+ }
154
+ continue;
155
+ }
156
+ // A displaced equation TARGET needs no inversion — the forward pass
157
+ // (step 3) recomputes it from everything else.
158
+ if (equationTargeting(field.id) || !isNumericField(field))
159
+ continue;
160
+ for (const equationId of equationsByField[field.id] ?? []) {
161
+ const equation = equationById.get(equationId);
162
+ if (!equation)
163
+ continue;
164
+ const result = solveForField({
165
+ definition: definitionForDisplacement(definition, field.id, equation),
166
+ fieldId: field.id,
167
+ values: knownValues(shadows, solved),
168
+ });
169
+ if (!result.ok)
170
+ continue;
171
+ solved[field.id] = { value: result.value, viaEquationId: equationId };
172
+ progressed = true;
173
+ break;
174
+ }
175
+ }
176
+ }
177
+ return solved;
178
+ };
179
+ const evaluateAll = (shadows, solved) => evaluateOutputs({ definition, values: knownValues(shadows, solved) });
180
+ /**
181
+ * Can this equation still be satisfied as things stand? Two ways it can't,
182
+ * and both are asking the same question — is there anywhere left to put the
183
+ * answer:
184
+ *
185
+ * (i) every participant is pinned by an entry, so the equation has no free
186
+ * field to calculate (Asana 1217042186684890 — filling in the last
187
+ * field should show a result, not demand one be cleared by hand);
188
+ * (ii) its target is pinned and disagrees with what the rest of the
189
+ * calculator makes of it. That's the red conflict chip, caught one step
190
+ * before step 4 draws it — and unlike (i) it fires even when the
191
+ * disagreement arrives through a field ANOTHER equation solved.
192
+ */
193
+ const isOverConstrained = (equation, shadows, results) => {
194
+ if (participantsOf(equation).every((id) => isPinned(shadows, id)))
195
+ return true;
196
+ if (!isPinned(shadows, equation.targetFieldId))
197
+ return false;
198
+ const target = findField(definition, equation.targetFieldId);
199
+ const result = results[equation.targetFieldId];
200
+ const entered = entries[equation.targetFieldId].value;
201
+ return (target != null &&
202
+ result?.ok === true &&
203
+ typeof entered === 'number' &&
204
+ !valuesAgree(target, entered, result.value));
205
+ };
206
+ /**
207
+ * Every field whose entry decides this equation's value: its participants,
208
+ * plus — transitively — the participants of whatever equation supplied a
209
+ * participant the user isn't holding.
210
+ *
211
+ * The reach matters (Asana 1217332117501004). In the reported cuboid, the
212
+ * surface-area equation reads a length the VOLUME equation solved, so the
213
+ * stale volume entry is what has to give way — and it appears nowhere in the
214
+ * surface-area equation itself. Walking only the participants leaves the
215
+ * newest entry with nothing it may displace, and it lands as a red chip.
216
+ */
217
+ const determinants = (equation, shadows, solved) => {
218
+ const fields = new Set();
219
+ const visited = new Set();
220
+ const queue = [equation.id];
221
+ while (queue.length > 0) {
222
+ const equationId = queue.pop();
223
+ if (visited.has(equationId))
224
+ continue;
225
+ visited.add(equationId);
226
+ for (const fieldId of participantsOf(equationById.get(equationId) ?? equation)) {
227
+ fields.add(fieldId);
228
+ // A held value is decided by its own entry and nothing further back.
229
+ if (isPinned(shadows, fieldId))
230
+ continue;
231
+ const via = solved[fieldId]?.viaEquationId ?? equationTargeting(fieldId)?.id;
232
+ if (via)
233
+ queue.push(via);
234
+ }
235
+ }
236
+ return [...fields];
237
+ };
238
+ let solved = solveEnabled ? propagate(shadowed) : {};
239
+ let results = evaluateAll(shadowed, solved);
240
+ if (solveEnabled) {
241
+ // DISPLACEMENT. While some equation can't be satisfied, the OLDEST unlocked
242
+ // entry deciding it yields to the calculation.
243
+ //
244
+ // Every lock the user set is a value they asked to hold, so what's left
245
+ // unlocked is what the calculation may move; age breaks the tie, oldest
246
+ // first, because the newer number is the better statement of intent.
247
+ //
248
+ // The NEWEST entry is spared: it's what the user just typed, and
249
+ // calculating over it would swallow the keystroke. The one exception is
250
+ // when it is the ONLY thing left unlocked — sparing it there just paints
251
+ // the red chip and makes them clear it by hand. Locking that last free
252
+ // field is what pins the whole equation and brings the chip back.
253
+ //
254
+ // A release only sticks if it lands: the field has to end up calculated AND
255
+ // the equation actually satisfied. Otherwise it's rolled back and the next
256
+ // oldest tries, then the next — and when none of them work, the conflict
257
+ // chip stands, which is the honest answer.
258
+ for (let round = 0; round <= definition.fields.length; round++) {
259
+ let released = false;
260
+ for (const equation of definition.equations) {
261
+ if (!isOverConstrained(equation, shadowed, results))
262
+ continue;
263
+ const byAge = determinants(equation, shadowed, solved)
264
+ .filter((id) => isPinned(shadowed, id) && !entries[id].locked)
265
+ .sort((a, b) => (entries[a].enteredAt ?? 0) - (entries[b].enteredAt ?? 0) ||
266
+ (fieldOrder.get(a) ?? 0) - (fieldOrder.get(b) ?? 0));
267
+ const candidates = byAge.length === 1 ? byAge : byAge.slice(0, -1);
268
+ for (const candidate of candidates) {
269
+ const trial = new Set(shadowed).add(candidate);
270
+ const trialSolved = propagate(trial);
271
+ const trialResults = evaluateAll(trial, trialSolved);
272
+ const calculated = trialSolved[candidate] != null ||
273
+ trialResults[candidate]?.ok === true;
274
+ if (!calculated)
275
+ continue;
276
+ if (isOverConstrained(equation, trial, trialResults))
277
+ continue;
278
+ shadowed.add(candidate);
279
+ solved = trialSolved;
280
+ results = trialResults;
281
+ released = true;
282
+ break;
283
+ }
284
+ // One release re-decides which equation owns which field, so rescan
285
+ // from the top rather than carrying on with stale statuses.
286
+ if (released)
287
+ break;
288
+ }
289
+ if (!released)
290
+ break;
291
+ }
292
+ }
293
+ // 3. The forward pass over entries ∪ solved (`results`, above) serves both
294
+ // output display and conflict detection.
295
+ // 4. Resolve each field.
296
+ const resolved = {};
297
+ for (const field of definition.fields) {
298
+ // A shadowed entry resolves exactly as if the field were empty — computed
299
+ // if it's an equation target, solved otherwise — so the row shows the
300
+ // calculated value, not the number the displacement rule set aside.
301
+ const entry = shadowed.has(field.id) ? undefined : entries[field.id];
302
+ const equation = equationForField(definition, field.id);
303
+ const result = equation ? results[field.id] : undefined;
304
+ const base = {
305
+ ...EMPTY_RESOLVED,
306
+ value: null,
307
+ origin: 'empty',
308
+ locked: entry?.locked ?? false,
309
+ attribution: entry?.attribution ?? null,
310
+ };
311
+ if (field.kind === ColumnType.Instructions) {
312
+ resolved[field.id] = base;
313
+ continue;
314
+ }
315
+ if (equation && result) {
316
+ if (result.ok) {
317
+ if (entry && typeof entry.value === 'number') {
318
+ resolved[field.id] = {
319
+ ...base,
320
+ value: entry.value,
321
+ origin: entryOrigin(entry),
322
+ viaEquationId: equation.id,
323
+ conflict: valuesAgree(field, entry.value, result.value)
324
+ ? null
325
+ : { computedValue: result.value },
326
+ };
327
+ }
328
+ else {
329
+ resolved[field.id] = {
330
+ ...base,
331
+ value: result.value,
332
+ origin: 'computed',
333
+ viaEquationId: equation.id,
334
+ };
335
+ }
336
+ }
337
+ else if (result.reason === 'missing-inputs') {
338
+ resolved[field.id] = entry
339
+ ? { ...base, value: entry.value, origin: entryOrigin(entry) }
340
+ : { ...base, missingFieldIds: result.missingFieldIds };
341
+ }
342
+ else {
343
+ resolved[field.id] = {
344
+ ...base,
345
+ value: entry?.value ?? null,
346
+ origin: entry ? entryOrigin(entry) : 'empty',
347
+ error: result.error,
348
+ };
349
+ }
350
+ continue;
351
+ }
352
+ if (entry) {
353
+ resolved[field.id] = {
354
+ ...base,
355
+ value: entry.value,
356
+ origin: entryOrigin(entry),
357
+ };
358
+ continue;
359
+ }
360
+ const solvedField = solved[field.id];
361
+ if (solvedField) {
362
+ resolved[field.id] = {
363
+ ...base,
364
+ value: solvedField.value,
365
+ origin: 'solved',
366
+ viaEquationId: solvedField.viaEquationId,
367
+ };
368
+ continue;
369
+ }
370
+ // Instructions were handled above, so every remaining kind may carry one.
371
+ if (field.defaultValue != null) {
372
+ resolved[field.id] = {
373
+ ...base,
374
+ value: field.defaultValue,
375
+ origin: 'default',
376
+ };
377
+ continue;
378
+ }
379
+ resolved[field.id] = base;
380
+ }
381
+ // 4b. Which calculated fields refuse entry (see ResolvedField.populationBlocked).
382
+ // A separate pass because it reads the resolved origin: only a field the
383
+ // runtime is CURRENTLY calculating can be blocked — an empty one is just
384
+ // waiting for a value, and one showing its own entry can be retyped or
385
+ // cleared.
386
+ for (const field of definition.fields) {
387
+ const current = resolved[field.id];
388
+ if (current.origin !== 'computed' && current.origin !== 'solved')
389
+ continue;
390
+ const equation = definition.equations.find((candidate) => candidate.id === current.viaEquationId);
391
+ if (!equation)
392
+ continue;
393
+ const others = participantsOf(equation).filter((id) => id !== field.id);
394
+ // `every` over the ENTERED test as well: a participant left to a default
395
+ // (no entry at all) can't be unlocked, so telling the user to unlock
396
+ // something wouldn't be true — that case keeps the conflict chip.
397
+ if (others.length > 0 &&
398
+ others.every((id) => entries[id]?.locked === true)) {
399
+ resolved[field.id] = { ...current, populationBlocked: true };
400
+ }
401
+ }
402
+ // 5. Equation statuses for the link/'=' badges.
403
+ const equations = definition.equations.map((equation) => {
404
+ const participantFieldIds = participantsOf(equation);
405
+ const solvedFieldId = Object.entries(solved).find(([, s]) => s.viaEquationId === equation.id)?.[0] ?? null;
406
+ const result = results[equation.targetFieldId];
407
+ const state = solvedFieldId
408
+ ? 'solved-a-field'
409
+ : result?.ok
410
+ ? 'computed'
411
+ : result && !result.ok && result.reason === 'missing-inputs'
412
+ ? 'blocked'
413
+ : 'error';
414
+ return {
415
+ equationId: equation.id,
416
+ targetFieldId: equation.targetFieldId,
417
+ participantFieldIds,
418
+ state,
419
+ solvedFieldId,
420
+ };
421
+ });
422
+ return { resolved, equations, equationsByField };
423
+ };
424
+ /**
425
+ * `populationBlocked` for a single field — the one rule the reducer's guards
426
+ * and the UI's entry affordances share, so a refused "+" and a refused tape
427
+ * reading can never disagree about which fields are enterable.
428
+ */
429
+ export const isPopulationBlocked = (definition, entries, fieldId) => deriveRuntime(definition, entries).resolved[fieldId]?.populationBlocked ===
430
+ true;
package/dist/exports.d.ts CHANGED
@@ -31,6 +31,7 @@ export type { MeasurementRef, PickMeasurement, } from './annotation/canvas/measu
31
31
  export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './annotation/canvas/measurementStampOverlay.js';
32
32
  export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
33
33
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
34
+ export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
34
35
  export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
35
36
  export { createSelectTool, type SelectToolOptions, } from './annotation/canvas/tools/selectTool.js';
36
37
  export { createMeasurementStampTool, type MeasurementStampToolOptions, } from './annotation/canvas/tools/measurementStampTool.js';
package/dist/exports.js CHANGED
@@ -36,6 +36,7 @@ export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
36
36
  export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
37
37
  export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
38
38
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
39
+ export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
39
40
  export { createPenTool, } from './annotation/canvas/tools/penTool.js';
40
41
  export { createSelectTool, } from './annotation/canvas/tools/selectTool.js';
41
42
  export { createMeasurementStampTool, } from './annotation/canvas/tools/measurementStampTool.js';
@@ -0,0 +1,3 @@
1
+ export * from './colors.js';
2
+ export * from './semantic.js';
3
+ export * from './preset.js';
@@ -0,0 +1,3 @@
1
+ export * from './colors.js';
2
+ export * from './semantic.js';
3
+ export * from './preset.js';
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Shared Tailwind preset. COLORS ONLY, by design.
3
+ *
4
+ * Must never define `content`, `plugins`, `presets`, or `darkMode`:
5
+ * NativeWind recurses `config.presets[].presets` hunting for its own marker
6
+ * preset, and a `content` key here would silently narrow the consuming app's
7
+ * globs.
8
+ *
9
+ * Consumed from CJS Tailwind configs, so this is a NAMED export with no
10
+ * default — that keeps ESM/CJS interop semantics irrelevant:
11
+ *
12
+ * const { boldrPreset } = require('@reekon-tools/boldr-utils/theme/preset');
13
+ * module.exports = { presets: [require('nativewind/preset'), boldrPreset] };
14
+ *
15
+ * Tailwind loads configs through jiti + sucrase, so requiring this ESM file
16
+ * from a CJS config works regardless of Node version.
17
+ *
18
+ * Note that Tailwind merges `theme.extend` per key with the *app* winning, and
19
+ * replaces a color key wholesale rather than deep-merging. An app that keeps
20
+ * its own `colors.background` will silently shadow this preset.
21
+ */
22
+ export declare const boldrPreset: {
23
+ theme: {
24
+ extend: {
25
+ colors: {
26
+ background: "#1D1F21";
27
+ foreground: {
28
+ DEFAULT: "#FFFFFF";
29
+ disabled: "#626364";
30
+ };
31
+ card: {
32
+ DEFAULT: "#2B2C2E";
33
+ foreground: "#FFFFFF";
34
+ };
35
+ popover: {
36
+ DEFAULT: "#2B2C2E";
37
+ foreground: "#FFFFFF";
38
+ };
39
+ muted: {
40
+ DEFAULT: "#424345";
41
+ foreground: "#9FA0A2";
42
+ };
43
+ accent: {
44
+ DEFAULT: "#424345";
45
+ foreground: "#FFFFFF";
46
+ };
47
+ secondary: {
48
+ DEFAULT: "#424345";
49
+ foreground: "#FFFFFF";
50
+ };
51
+ border: {
52
+ DEFAULT: "#424345";
53
+ strong: "#626364";
54
+ };
55
+ input: "#424345";
56
+ ring: "#FFAD00";
57
+ primary: {
58
+ DEFAULT: "#FFAD00";
59
+ foreground: "#1D1F21";
60
+ };
61
+ destructive: {
62
+ DEFAULT: "#DF6358";
63
+ foreground: "#1D1F21";
64
+ };
65
+ elevated: {
66
+ DEFAULT: "#424345";
67
+ foreground: "#FFFFFF";
68
+ };
69
+ interactive: {
70
+ DEFAULT: "#0066FF";
71
+ foreground: "#FFFFFF";
72
+ };
73
+ nav: {
74
+ DEFAULT: "#2D3843";
75
+ foreground: "#FFFFFF";
76
+ };
77
+ paper: {
78
+ DEFAULT: "#E8EAED";
79
+ foreground: "#1D1F21";
80
+ field: "#FFFFFF";
81
+ line: "#BCBEC2";
82
+ muted: "#626364";
83
+ };
84
+ tile: {
85
+ foreground: "#FFFFFF";
86
+ measurement: {
87
+ DEFAULT: "#0066FF";
88
+ badge: "#50A6FF";
89
+ };
90
+ formula: {
91
+ DEFAULT: "#BD30A8";
92
+ badge: "#6C155F";
93
+ };
94
+ count: {
95
+ DEFAULT: "#02274B";
96
+ foreground: "#FFFFFF";
97
+ };
98
+ };
99
+ diagram: {
100
+ 1: "#2E4C6A";
101
+ 2: "#4B6074";
102
+ 3: "#67819A";
103
+ };
104
+ tolerance: {
105
+ in: {
106
+ DEFAULT: "#58A942";
107
+ warning: "#00E920";
108
+ muted: "#8BB380";
109
+ };
110
+ out: {
111
+ DEFAULT: "#E83D2F";
112
+ warning: "#DA1100";
113
+ muted: "#DF6358";
114
+ };
115
+ };
116
+ };
117
+ };
118
+ };
119
+ };
120
+ export type BoldrPreset = typeof boldrPreset;
@@ -0,0 +1,25 @@
1
+ import { boldrColors } from './semantic.js';
2
+ /**
3
+ * Shared Tailwind preset. COLORS ONLY, by design.
4
+ *
5
+ * Must never define `content`, `plugins`, `presets`, or `darkMode`:
6
+ * NativeWind recurses `config.presets[].presets` hunting for its own marker
7
+ * preset, and a `content` key here would silently narrow the consuming app's
8
+ * globs.
9
+ *
10
+ * Consumed from CJS Tailwind configs, so this is a NAMED export with no
11
+ * default — that keeps ESM/CJS interop semantics irrelevant:
12
+ *
13
+ * const { boldrPreset } = require('@reekon-tools/boldr-utils/theme/preset');
14
+ * module.exports = { presets: [require('nativewind/preset'), boldrPreset] };
15
+ *
16
+ * Tailwind loads configs through jiti + sucrase, so requiring this ESM file
17
+ * from a CJS config works regardless of Node version.
18
+ *
19
+ * Note that Tailwind merges `theme.extend` per key with the *app* winning, and
20
+ * replaces a color key wholesale rather than deep-merging. An app that keeps
21
+ * its own `colors.background` will silently shadow this preset.
22
+ */
23
+ export const boldrPreset = {
24
+ theme: { extend: { colors: boldrColors } },
25
+ };