@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,370 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { create, all } from 'mathjs';
|
|
3
|
+
import { ColumnType, DecimalTolerance, FractionalTolerance, } from '../types/firestore.js';
|
|
4
|
+
import { CALCULATOR_SCHEMA_VERSION, equationForField, isEquationTargetKind, isNumericFieldKind, } from './schema.js';
|
|
5
|
+
import { CALCULATOR_UNIT_INFO, unitDimension } from './units.js';
|
|
6
|
+
const math = create(all);
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Definition validation: zod for structure, plus the cross-field invariants
|
|
9
|
+
// zod can't express (reference resolution, cycles, solve wiring). Callable
|
|
10
|
+
// from the authoring UI, the runtime, and (later) a server-side publish
|
|
11
|
+
// function — keep it platform-neutral.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const calculatorUnitSchema = z
|
|
14
|
+
.string()
|
|
15
|
+
.refine((u) => u in CALCULATOR_UNIT_INFO, { message: 'Unknown unit' });
|
|
16
|
+
const solveHintsSchema = z.object({
|
|
17
|
+
solvable: z.boolean(),
|
|
18
|
+
governingEquationId: z.string().optional(),
|
|
19
|
+
min: z.number().finite().optional(),
|
|
20
|
+
max: z.number().finite().optional(),
|
|
21
|
+
guess: z.number().finite().optional(),
|
|
22
|
+
});
|
|
23
|
+
const fieldUnitConfigSchema = z.object({
|
|
24
|
+
dimension: z.enum(['length', 'area', 'volume']),
|
|
25
|
+
defaultUnit: calculatorUnitSchema,
|
|
26
|
+
allowedUnits: z.array(calculatorUnitSchema).min(1),
|
|
27
|
+
fractionalTolerance: z.enum(FractionalTolerance).optional(),
|
|
28
|
+
decimalTolerance: z.enum(DecimalTolerance).optional(),
|
|
29
|
+
});
|
|
30
|
+
const fieldBase = {
|
|
31
|
+
id: z.string().min(1),
|
|
32
|
+
name: z.string(),
|
|
33
|
+
role: z.enum(['input', 'output']),
|
|
34
|
+
description: z.string().optional(),
|
|
35
|
+
binding: z.object({ hotspotId: z.string().min(1) }).optional(),
|
|
36
|
+
};
|
|
37
|
+
const selectColumnDataSchema = z.object({ options: z.array(z.string()) });
|
|
38
|
+
const conversionTableColumnDataSchema = z.object({
|
|
39
|
+
conversions: z.array(z.object({ label: z.string(), value: z.string() })),
|
|
40
|
+
});
|
|
41
|
+
const instructionsColumnDataSchema = z.object({
|
|
42
|
+
text: z.string(),
|
|
43
|
+
textColor: z.string(),
|
|
44
|
+
backgroundColor: z.string(),
|
|
45
|
+
});
|
|
46
|
+
const calculatorFieldSchema = z.discriminatedUnion('kind', [
|
|
47
|
+
z.object({
|
|
48
|
+
...fieldBase,
|
|
49
|
+
kind: z.literal(ColumnType.Number),
|
|
50
|
+
defaultValue: z.number().optional(),
|
|
51
|
+
solve: solveHintsSchema.optional(),
|
|
52
|
+
}),
|
|
53
|
+
z.object({
|
|
54
|
+
...fieldBase,
|
|
55
|
+
kind: z.literal(ColumnType.Measurement),
|
|
56
|
+
unit: fieldUnitConfigSchema,
|
|
57
|
+
defaultValue: z.number().optional(),
|
|
58
|
+
solve: solveHintsSchema.optional(),
|
|
59
|
+
}),
|
|
60
|
+
z.object({
|
|
61
|
+
...fieldBase,
|
|
62
|
+
kind: z.literal(ColumnType.Angle),
|
|
63
|
+
angleUnit: z.enum(['deg', 'rad']).optional(),
|
|
64
|
+
defaultValue: z.number().optional(),
|
|
65
|
+
solve: solveHintsSchema.optional(),
|
|
66
|
+
}),
|
|
67
|
+
z.object({
|
|
68
|
+
...fieldBase,
|
|
69
|
+
kind: z.literal(ColumnType.SingleSelect),
|
|
70
|
+
columnData: selectColumnDataSchema,
|
|
71
|
+
defaultValue: z.string().optional(),
|
|
72
|
+
}),
|
|
73
|
+
z.object({
|
|
74
|
+
...fieldBase,
|
|
75
|
+
kind: z.literal(ColumnType.MultiSelect),
|
|
76
|
+
columnData: selectColumnDataSchema,
|
|
77
|
+
defaultValue: z.array(z.string()).optional(),
|
|
78
|
+
}),
|
|
79
|
+
z.object({
|
|
80
|
+
...fieldBase,
|
|
81
|
+
kind: z.literal(ColumnType.ConversionTable),
|
|
82
|
+
columnData: conversionTableColumnDataSchema,
|
|
83
|
+
defaultValue: z.string().optional(),
|
|
84
|
+
}),
|
|
85
|
+
z.object({
|
|
86
|
+
...fieldBase,
|
|
87
|
+
kind: z.literal(ColumnType.Instructions),
|
|
88
|
+
columnData: instructionsColumnDataSchema,
|
|
89
|
+
}),
|
|
90
|
+
]);
|
|
91
|
+
const calculatorEquationSchema = z.object({
|
|
92
|
+
id: z.string().min(1),
|
|
93
|
+
targetFieldId: z.string().min(1),
|
|
94
|
+
expression: z.string().min(1),
|
|
95
|
+
variableToFieldId: z.record(z.string(), z.string()),
|
|
96
|
+
});
|
|
97
|
+
export const calculatorDefinitionSchema = z.object({
|
|
98
|
+
id: z.string().min(1),
|
|
99
|
+
schemaVersion: z.number().int(),
|
|
100
|
+
version: z.number().int(),
|
|
101
|
+
name: z.string(),
|
|
102
|
+
description: z.string(),
|
|
103
|
+
folderId: z.string().nullable(),
|
|
104
|
+
fields: z.array(calculatorFieldSchema),
|
|
105
|
+
equations: z.array(calculatorEquationSchema),
|
|
106
|
+
diagramFileId: z.string().nullable().optional(),
|
|
107
|
+
isPublic: z.boolean(),
|
|
108
|
+
sharedUserIds: z.array(z.string()).optional(),
|
|
109
|
+
sharedOrgIds: z.array(z.string()).optional(),
|
|
110
|
+
createdBy: z
|
|
111
|
+
.object({
|
|
112
|
+
userId: z.string(),
|
|
113
|
+
firstName: z.string().optional(),
|
|
114
|
+
lastName: z.string().optional(),
|
|
115
|
+
})
|
|
116
|
+
.optional(),
|
|
117
|
+
// Firestore Timestamps / Dates — shape-checked upstream, not here.
|
|
118
|
+
createdAt: z.unknown().optional(),
|
|
119
|
+
updatedAt: z.unknown().optional(),
|
|
120
|
+
});
|
|
121
|
+
// Constants mathjs resolves without a scope entry.
|
|
122
|
+
const KNOWN_SYMBOLS = new Set([
|
|
123
|
+
'pi',
|
|
124
|
+
'PI',
|
|
125
|
+
'e',
|
|
126
|
+
'E',
|
|
127
|
+
'tau',
|
|
128
|
+
'phi',
|
|
129
|
+
'i',
|
|
130
|
+
'true',
|
|
131
|
+
'false',
|
|
132
|
+
'Infinity',
|
|
133
|
+
'NaN',
|
|
134
|
+
'null',
|
|
135
|
+
]);
|
|
136
|
+
/** Free variables of an expression (function names and constants excluded). */
|
|
137
|
+
export const expressionSymbols = (expression) => {
|
|
138
|
+
try {
|
|
139
|
+
const node = math.parse(expression);
|
|
140
|
+
const symbols = new Set();
|
|
141
|
+
node.traverse((n, path, parent) => {
|
|
142
|
+
const sym = n;
|
|
143
|
+
const par = parent;
|
|
144
|
+
if (sym.isSymbolNode &&
|
|
145
|
+
sym.name &&
|
|
146
|
+
!(par?.isFunctionNode && path === 'fn') &&
|
|
147
|
+
!KNOWN_SYMBOLS.has(sym.name)) {
|
|
148
|
+
symbols.add(sym.name);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return { ok: true, symbols: [...symbols] };
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
error: err instanceof Error ? err.message : String(err),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
// Does `equation` depend on `fieldId`, directly or through the equations of
|
|
161
|
+
// output fields it references? (Chained solving: the unknown may reach the
|
|
162
|
+
// governing equation through intermediate outputs.)
|
|
163
|
+
const equationReaches = (definition, equation, fieldId, seen = new Set()) => {
|
|
164
|
+
if (seen.has(equation.id))
|
|
165
|
+
return false;
|
|
166
|
+
seen.add(equation.id);
|
|
167
|
+
for (const mappedId of Object.values(equation.variableToFieldId)) {
|
|
168
|
+
if (mappedId === fieldId)
|
|
169
|
+
return true;
|
|
170
|
+
const sub = equationForField(definition, mappedId);
|
|
171
|
+
if (sub && equationReaches(definition, sub, fieldId, seen))
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
};
|
|
176
|
+
export const validateCalculatorDefinition = (input) => {
|
|
177
|
+
const issues = [];
|
|
178
|
+
const push = (code, path, message, severity = 'error') => issues.push({ code, path, message, severity });
|
|
179
|
+
const parsed = calculatorDefinitionSchema.safeParse(input);
|
|
180
|
+
if (!parsed.success) {
|
|
181
|
+
for (const issue of parsed.error.issues) {
|
|
182
|
+
push('structure', issue.path.join('.'), issue.message);
|
|
183
|
+
}
|
|
184
|
+
return { ok: false, issues };
|
|
185
|
+
}
|
|
186
|
+
const def = parsed.data;
|
|
187
|
+
if (def.schemaVersion > CALCULATOR_SCHEMA_VERSION) {
|
|
188
|
+
push('schema-version-unsupported', 'schemaVersion', `schemaVersion ${def.schemaVersion} is newer than this client supports (${CALCULATOR_SCHEMA_VERSION})`);
|
|
189
|
+
}
|
|
190
|
+
// --- Unique ids ---------------------------------------------------------
|
|
191
|
+
const fieldIds = new Set();
|
|
192
|
+
def.fields.forEach((field, i) => {
|
|
193
|
+
if (fieldIds.has(field.id)) {
|
|
194
|
+
push('duplicate-field-id', `fields.${i}.id`, `Duplicate field id "${field.id}"`);
|
|
195
|
+
}
|
|
196
|
+
fieldIds.add(field.id);
|
|
197
|
+
});
|
|
198
|
+
const equationIds = new Set();
|
|
199
|
+
def.equations.forEach((eq, i) => {
|
|
200
|
+
if (equationIds.has(eq.id)) {
|
|
201
|
+
push('duplicate-equation-id', `equations.${i}.id`, `Duplicate equation id "${eq.id}"`);
|
|
202
|
+
}
|
|
203
|
+
equationIds.add(eq.id);
|
|
204
|
+
});
|
|
205
|
+
const fieldById = new Map(def.fields.map((f) => [f.id, f]));
|
|
206
|
+
// --- Per-field integrity --------------------------------------------------
|
|
207
|
+
def.fields.forEach((field, i) => {
|
|
208
|
+
const path = `fields.${i}`;
|
|
209
|
+
if (field.kind === ColumnType.Measurement) {
|
|
210
|
+
const { dimension, defaultUnit, allowedUnits } = field.unit;
|
|
211
|
+
if (unitDimension(defaultUnit) !== dimension) {
|
|
212
|
+
push('unit-dimension-mismatch', `${path}.unit.defaultUnit`, `Default unit "${defaultUnit}" is not a ${dimension} unit`);
|
|
213
|
+
}
|
|
214
|
+
allowedUnits.forEach((u, j) => {
|
|
215
|
+
if (unitDimension(u) !== dimension) {
|
|
216
|
+
push('unit-dimension-mismatch', `${path}.unit.allowedUnits.${j}`, `Unit "${u}" is not a ${dimension} unit`);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
if (!allowedUnits.includes(defaultUnit)) {
|
|
220
|
+
push('default-unit-not-allowed', `${path}.unit.defaultUnit`, `Default unit "${defaultUnit}" is not in allowedUnits`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (field.kind === ColumnType.SingleSelect ||
|
|
224
|
+
field.kind === ColumnType.MultiSelect) {
|
|
225
|
+
if (field.columnData.options.length === 0) {
|
|
226
|
+
push('empty-options', `${path}.columnData.options`, 'Select field has no options');
|
|
227
|
+
}
|
|
228
|
+
const dupes = field.columnData.options.filter((o, j) => field.columnData.options.indexOf(o) !== j);
|
|
229
|
+
if (dupes.length > 0) {
|
|
230
|
+
push('duplicate-options', `${path}.columnData.options`, `Duplicate options: ${[...new Set(dupes)].join(', ')}`, 'warning');
|
|
231
|
+
}
|
|
232
|
+
const defaults = field.kind === ColumnType.MultiSelect
|
|
233
|
+
? (field.defaultValue ?? [])
|
|
234
|
+
: field.defaultValue != null
|
|
235
|
+
? [field.defaultValue]
|
|
236
|
+
: [];
|
|
237
|
+
for (const d of defaults) {
|
|
238
|
+
if (!field.columnData.options.includes(d)) {
|
|
239
|
+
push('default-not-an-option', `${path}.defaultValue`, `Default "${d}" is not one of the options`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (field.kind === ColumnType.ConversionTable) {
|
|
244
|
+
const { conversions } = field.columnData;
|
|
245
|
+
if (conversions.length === 0) {
|
|
246
|
+
push('empty-conversions', `${path}.columnData.conversions`, 'Conversion table is empty');
|
|
247
|
+
}
|
|
248
|
+
conversions.forEach((c, j) => {
|
|
249
|
+
if (!Number.isFinite(parseFloat(c.value))) {
|
|
250
|
+
push('conversion-value-not-numeric', `${path}.columnData.conversions.${j}.value`, `Conversion "${c.label}" has non-numeric value "${c.value}"`);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
const labels = conversions.map((c) => c.label);
|
|
254
|
+
if (new Set(labels).size !== labels.length) {
|
|
255
|
+
push('duplicate-conversion-labels', `${path}.columnData.conversions`, 'Conversion labels must be unique');
|
|
256
|
+
}
|
|
257
|
+
if (field.defaultValue != null && !labels.includes(field.defaultValue)) {
|
|
258
|
+
push('default-not-an-option', `${path}.defaultValue`, `Default "${field.defaultValue}" is not a conversion label`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
// --- Equations -----------------------------------------------------------
|
|
263
|
+
const targetCounts = new Map();
|
|
264
|
+
def.equations.forEach((eq) => {
|
|
265
|
+
targetCounts.set(eq.targetFieldId, (targetCounts.get(eq.targetFieldId) ?? 0) + 1);
|
|
266
|
+
});
|
|
267
|
+
def.equations.forEach((eq, i) => {
|
|
268
|
+
const path = `equations.${i}`;
|
|
269
|
+
const target = fieldById.get(eq.targetFieldId);
|
|
270
|
+
if (!target) {
|
|
271
|
+
push('unknown-target-field', `${path}.targetFieldId`, `Equation targets unknown field "${eq.targetFieldId}"`);
|
|
272
|
+
}
|
|
273
|
+
else if (!isEquationTargetKind(target.kind)) {
|
|
274
|
+
push('invalid-target-kind', `${path}.targetFieldId`, `Equation target "${target.name}" must be a number, measurement, or angle field`);
|
|
275
|
+
}
|
|
276
|
+
if ((targetCounts.get(eq.targetFieldId) ?? 0) > 1) {
|
|
277
|
+
push('multiple-equations-for-field', `${path}.targetFieldId`, `Field "${eq.targetFieldId}" is computed by more than one equation`);
|
|
278
|
+
}
|
|
279
|
+
for (const [variable, fieldId] of Object.entries(eq.variableToFieldId)) {
|
|
280
|
+
const mapped = fieldById.get(fieldId);
|
|
281
|
+
if (!mapped) {
|
|
282
|
+
push('unknown-mapped-field', `${path}.variableToFieldId.${variable}`, `Variable ${variable} maps to unknown field "${fieldId}"`);
|
|
283
|
+
}
|
|
284
|
+
else if (!isNumericFieldKind(mapped.kind)) {
|
|
285
|
+
push('non-numeric-mapped-field', `${path}.variableToFieldId.${variable}`, `Variable ${variable} maps to "${mapped.name}", which has no numeric value`);
|
|
286
|
+
}
|
|
287
|
+
if (fieldId === eq.targetFieldId) {
|
|
288
|
+
push('self-referencing-equation', `${path}.variableToFieldId.${variable}`, `Equation for "${eq.targetFieldId}" references its own target`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const symbols = expressionSymbols(eq.expression);
|
|
292
|
+
if (!symbols.ok) {
|
|
293
|
+
push('expression-invalid', `${path}.expression`, `Expression does not parse: ${symbols.error}`);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const mappedVars = new Set(Object.keys(eq.variableToFieldId));
|
|
297
|
+
for (const s of symbols.symbols) {
|
|
298
|
+
if (!mappedVars.has(s)) {
|
|
299
|
+
push('unmapped-symbol', `${path}.expression`, `Expression uses "${s}" but no variable mapping defines it`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
for (const v of mappedVars) {
|
|
303
|
+
if (!symbols.symbols.includes(v)) {
|
|
304
|
+
push('unused-variable', `${path}.variableToFieldId.${v}`, `Variable ${v} is mapped but never used in the expression`, 'warning');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
// --- Cycles across equations ----------------------------------------------
|
|
310
|
+
const eqByTarget = new Map(def.equations.map((e) => [e.targetFieldId, e]));
|
|
311
|
+
const visiting = new Set();
|
|
312
|
+
const done = new Set();
|
|
313
|
+
const hasCycle = (eq) => {
|
|
314
|
+
if (done.has(eq.id))
|
|
315
|
+
return false;
|
|
316
|
+
if (visiting.has(eq.id))
|
|
317
|
+
return true;
|
|
318
|
+
visiting.add(eq.id);
|
|
319
|
+
for (const fieldId of Object.values(eq.variableToFieldId)) {
|
|
320
|
+
const sub = eqByTarget.get(fieldId);
|
|
321
|
+
if (sub && hasCycle(sub))
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
visiting.delete(eq.id);
|
|
325
|
+
done.add(eq.id);
|
|
326
|
+
return false;
|
|
327
|
+
};
|
|
328
|
+
def.equations.forEach((eq, i) => {
|
|
329
|
+
if (!done.has(eq.id) && hasCycle(eq)) {
|
|
330
|
+
push('circular-equations', `equations.${i}`, `Equation "${eq.id}" participates in a circular dependency`);
|
|
331
|
+
visiting.clear();
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// --- Solve wiring -----------------------------------------------------------
|
|
335
|
+
def.fields.forEach((field, i) => {
|
|
336
|
+
if ((field.kind === ColumnType.Number ||
|
|
337
|
+
field.kind === ColumnType.Measurement ||
|
|
338
|
+
field.kind === ColumnType.Angle) &&
|
|
339
|
+
field.solve?.solvable) {
|
|
340
|
+
const path = `fields.${i}.solve`;
|
|
341
|
+
const { governingEquationId, min, max } = field.solve;
|
|
342
|
+
if (!governingEquationId) {
|
|
343
|
+
push('solvable-without-equation', `${path}.governingEquationId`, `Solvable field "${field.name}" has no governing equation`);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const eq = def.equations.find((e) => e.id === governingEquationId);
|
|
347
|
+
if (!eq) {
|
|
348
|
+
push('unknown-governing-equation', `${path}.governingEquationId`, `Governing equation "${governingEquationId}" does not exist`);
|
|
349
|
+
}
|
|
350
|
+
else if (!equationReaches(def, eq, field.id)) {
|
|
351
|
+
push('equation-does-not-reference-field', `${path}.governingEquationId`, `Equation "${governingEquationId}" never references "${field.name}", so it cannot be solved for it`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (min != null && max != null && min >= max) {
|
|
355
|
+
push('invalid-bracket', `${path}.min`, 'Bracket min must be below max');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
// --- Diagram binding integrity ----------------------------------------------
|
|
360
|
+
const hotspotIds = new Set();
|
|
361
|
+
def.fields.forEach((field, i) => {
|
|
362
|
+
if (field.binding) {
|
|
363
|
+
if (hotspotIds.has(field.binding.hotspotId)) {
|
|
364
|
+
push('duplicate-hotspot', `fields.${i}.binding.hotspotId`, `Hotspot "${field.binding.hotspotId}" is bound to more than one field`);
|
|
365
|
+
}
|
|
366
|
+
hotspotIds.add(field.binding.hotspotId);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
return { ok: !issues.some((i) => i.severity === 'error'), issues };
|
|
370
|
+
};
|
package/dist/exports.d.ts
CHANGED
|
@@ -10,7 +10,8 @@ export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, gen
|
|
|
10
10
|
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, } from './utils/groups.js';
|
|
11
11
|
export { numberToLetterIndex, formGroupInputLabel, listGroupMeasurementLabel, } from './utils/indexLabels.js';
|
|
12
12
|
export { toSelectedValues, formatSelectedValues, toStoredValue, } from './utils/selectValues.js';
|
|
13
|
-
export
|
|
13
|
+
export * from './calculator/index.js';
|
|
14
|
+
export { annotationScopeKey, isFieldOp, isTemplateScope, isCalculatorScope, type AnnotationDataProvider, type AnnotationFile, type AnnotationFilePatch, type AnnotationFileSummary, type AnnotationScope, type CalculatorScope, type FieldOp, type ImageBlob, type JobGroupScope, type JobScope, type Patch, type TemplateScope, type Unsubscribe, type UploadedImageRef, } from './annotation/data/AnnotationDataProvider.js';
|
|
14
15
|
export { AnnotationDataProviderContext, useAnnotationData, type AnnotationDataProviderProps, } from './annotation/data/AnnotationDataContext.js';
|
|
15
16
|
export { useAnnotationDoc, type UseAnnotationDocResult, } from './annotation/data/hooks/useAnnotationDoc.js';
|
|
16
17
|
export { useAnnotationList, type UseAnnotationListResult, } from './annotation/data/hooks/useAnnotationList.js';
|
package/dist/exports.js
CHANGED
|
@@ -14,8 +14,11 @@ export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, gen
|
|
|
14
14
|
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, } from './utils/groups.js';
|
|
15
15
|
export { numberToLetterIndex, formGroupInputLabel, listGroupMeasurementLabel, } from './utils/indexLabels.js';
|
|
16
16
|
export { toSelectedValues, formatSelectedValues, toStoredValue, } from './utils/selectValues.js';
|
|
17
|
+
// Construction Calculator shared module (schema, units, evaluator, solver,
|
|
18
|
+
// validation).
|
|
19
|
+
export * from './calculator/index.js';
|
|
17
20
|
// Annotation data layer (SDK-neutral; apps provide their own provider).
|
|
18
|
-
export { annotationScopeKey, isFieldOp, isTemplateScope, } from './annotation/data/AnnotationDataProvider.js';
|
|
21
|
+
export { annotationScopeKey, isFieldOp, isTemplateScope, isCalculatorScope, } from './annotation/data/AnnotationDataProvider.js';
|
|
19
22
|
export { AnnotationDataProviderContext, useAnnotationData, } from './annotation/data/AnnotationDataContext.js';
|
|
20
23
|
export { useAnnotationDoc, } from './annotation/data/hooks/useAnnotationDoc.js';
|
|
21
24
|
export { useAnnotationList, } from './annotation/data/hooks/useAnnotationList.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reekon-tools/boldr-utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
|
|
5
5
|
"author": "REEKON Tools",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"keywords": [],
|
|
36
36
|
"repository": "https://gitlab.com/reekon-tools/software/rock-pro/boldr-utils#",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"mathjs": "^11.11.0"
|
|
38
|
+
"mathjs": "^11.11.0",
|
|
39
|
+
"zod": "^4.4.3"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
42
|
"@shopify/react-native-skia": ">=2.5.0",
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.js';
|
|
3
|
-
export type { AnnotationCanvasHandle, GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
|
|
4
|
-
export interface CanvasKitOpts {
|
|
5
|
-
locateFile?: (file: string) => string;
|
|
6
|
-
}
|
|
7
|
-
export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
|
|
8
|
-
fallback?: ReactNode;
|
|
9
|
-
canvasKitOpts?: CanvasKitOpts;
|
|
10
|
-
};
|
|
11
|
-
export declare const AnnotationCanvas: ({ fallback, canvasKitOpts, ...rest }: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { WithSkiaWeb } from '@shopify/react-native-skia/lib/module/web/index.js';
|
|
3
|
-
// Web-only entry point: lazy-loads CanvasKit wasm via WithSkiaWeb. Imperative
|
|
4
|
-
// API (undo/redo/zoom) is exposed through the `imperativeRef` prop rather
|
|
5
|
-
// than React refs, since WithSkiaWeb can't forward refs through its lazy
|
|
6
|
-
// component boundary. The native build sits in `AnnotationCanvas.native.tsx`
|
|
7
|
-
// and skips WithSkiaWeb entirely (no wasm to load).
|
|
8
|
-
export const AnnotationCanvas = ({ fallback, canvasKitOpts, ...rest }) => (_jsx(WithSkiaWeb, { getComponent: () => import('./AnnotationCanvasInner.js').then((m) => ({
|
|
9
|
-
default: m.AnnotationCanvasInner,
|
|
10
|
-
})), fallback: fallback ?? null, componentProps: rest, opts: canvasKitOpts }));
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.native.js';
|
|
2
|
-
export type { AnnotationCanvasHandle, } from './useAnnotationCanvasState.js';
|
|
3
|
-
export type { GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
|
|
4
|
-
export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
|
|
5
|
-
fallback?: unknown;
|
|
6
|
-
canvasKitOpts?: unknown;
|
|
7
|
-
};
|
|
8
|
-
export declare const AnnotationCanvas: (props: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { AnnotationCanvasInner } from './AnnotationCanvasInner.native.js';
|
|
3
|
-
export const AnnotationCanvas = (props) => {
|
|
4
|
-
const { fallback: _f, canvasKitOpts: _o, ...rest } = props;
|
|
5
|
-
return _jsx(AnnotationCanvasInner, { ...rest });
|
|
6
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { type CSSProperties, type MutableRefObject } from 'react';
|
|
2
|
-
import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
|
|
3
|
-
import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
|
|
4
|
-
import type { MeasurementRef } from './measurementPicker.js';
|
|
5
|
-
import type { Tool } from './Tool.js';
|
|
6
|
-
import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
|
|
7
|
-
import { type ViewportState } from './viewport.js';
|
|
8
|
-
import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
|
|
9
|
-
export type { AnnotationCanvasHandle };
|
|
10
|
-
export interface AnnotationCanvasInnerProps {
|
|
11
|
-
canvas: AnnotationCanvasState;
|
|
12
|
-
onCommit(patch: AnnotationDocumentPatch): void;
|
|
13
|
-
tools: Tool[];
|
|
14
|
-
activeToolId: string;
|
|
15
|
-
selection: Selection | null;
|
|
16
|
-
onSelectionChange(selection: Selection | null): void;
|
|
17
|
-
measurements?: Measurement[];
|
|
18
|
-
fallbackUnit?: Units;
|
|
19
|
-
fractionalTolerance?: FractionalTolerance;
|
|
20
|
-
decimalTolerance?: DecimalTolerance;
|
|
21
|
-
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
22
|
-
pickMeasurement?: () => Promise<MeasurementRef | null>;
|
|
23
|
-
renderMeasurementStamp?: RenderMeasurementStamp;
|
|
24
|
-
stampFontSource?: unknown;
|
|
25
|
-
stampValueFontSize?: number;
|
|
26
|
-
stampLabelFontSize?: number;
|
|
27
|
-
gestures?: GestureConfig;
|
|
28
|
-
width: number;
|
|
29
|
-
height: number;
|
|
30
|
-
initialViewport?: ViewportState;
|
|
31
|
-
style?: CSSProperties;
|
|
32
|
-
imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
|
|
33
|
-
}
|
|
34
|
-
export type PanTrigger = 'middleMouse' | 'rightMouse' | 'space';
|
|
35
|
-
export interface GestureConfig {
|
|
36
|
-
wheel?: 'zoom' | 'pan' | 'auto';
|
|
37
|
-
panTriggers?: PanTrigger[];
|
|
38
|
-
}
|
|
39
|
-
export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;
|