@reekon-tools/boldr-utils 1.24.0 → 1.26.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 (78) hide show
  1. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +11 -0
  2. package/dist/annotation/canvas/useAnnotationCanvasState.js +17 -1
  3. package/dist/annotation/canvas/viewport.d.ts +10 -0
  4. package/dist/annotation/canvas/viewport.js +26 -0
  5. package/dist/annotation/data/hooks/usePdfCanvasDoc.js +22 -13
  6. package/dist/canvas/AnnotationCanvas.d.ts +11 -0
  7. package/dist/canvas/AnnotationCanvas.js +10 -0
  8. package/dist/canvas/AnnotationCanvas.native.d.ts +8 -0
  9. package/dist/canvas/AnnotationCanvas.native.js +6 -0
  10. package/dist/canvas/AnnotationCanvasInner.d.ts +39 -0
  11. package/dist/canvas/AnnotationCanvasInner.js +219 -0
  12. package/dist/canvas/AnnotationCanvasInner.native.d.ts +35 -0
  13. package/dist/canvas/AnnotationCanvasInner.native.js +138 -0
  14. package/dist/canvas/AnnotationCanvasSkia.d.ts +27 -0
  15. package/dist/canvas/AnnotationCanvasSkia.js +20 -0
  16. package/dist/canvas/Tool.d.ts +38 -0
  17. package/dist/canvas/Tool.js +1 -0
  18. package/dist/canvas/elements/BackgroundImageElement.d.ts +9 -0
  19. package/dist/canvas/elements/BackgroundImageElement.js +37 -0
  20. package/dist/canvas/elements/MeasurementStampElement.d.ts +13 -0
  21. package/dist/canvas/elements/MeasurementStampElement.js +30 -0
  22. package/dist/canvas/elements/ShapeElement.d.ts +7 -0
  23. package/dist/canvas/elements/ShapeElement.js +62 -0
  24. package/dist/canvas/elements/StrokeElement.d.ts +7 -0
  25. package/dist/canvas/elements/StrokeElement.js +18 -0
  26. package/dist/canvas/measurementPicker.d.ts +10 -0
  27. package/dist/canvas/measurementPicker.js +1 -0
  28. package/dist/canvas/measurementStampOverlay.d.ts +11 -0
  29. package/dist/canvas/measurementStampOverlay.js +1 -0
  30. package/dist/canvas/pointerAdapter.d.ts +3 -0
  31. package/dist/canvas/pointerAdapter.js +19 -0
  32. package/dist/canvas/stampLayout.d.ts +5 -0
  33. package/dist/canvas/stampLayout.js +14 -0
  34. package/dist/canvas/tools/measurementStampTool.d.ts +9 -0
  35. package/dist/canvas/tools/measurementStampTool.js +37 -0
  36. package/dist/canvas/tools/panTool.d.ts +5 -0
  37. package/dist/canvas/tools/panTool.js +25 -0
  38. package/dist/canvas/tools/penTool.d.ts +13 -0
  39. package/dist/canvas/tools/penTool.js +68 -0
  40. package/dist/canvas/tools/selectTool.d.ts +2 -0
  41. package/dist/canvas/tools/selectTool.js +182 -0
  42. package/dist/canvas/useAnnotationCanvasState.d.ts +54 -0
  43. package/dist/canvas/useAnnotationCanvasState.js +210 -0
  44. package/dist/canvas/viewport.d.ts +16 -0
  45. package/dist/canvas/viewport.js +54 -0
  46. package/dist/data/AnnotationDataContext.d.ts +8 -0
  47. package/dist/data/AnnotationDataContext.js +11 -0
  48. package/dist/data/AnnotationDataProvider.d.ts +65 -0
  49. package/dist/data/AnnotationDataProvider.js +4 -0
  50. package/dist/data/InMemoryAnnotationProvider.d.ts +30 -0
  51. package/dist/data/InMemoryAnnotationProvider.js +197 -0
  52. package/dist/data/canvasPersistence.d.ts +3 -0
  53. package/dist/data/canvasPersistence.js +26 -0
  54. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +33 -0
  55. package/dist/data/hooks/useAnnotationCanvasDoc.js +314 -0
  56. package/dist/data/hooks/useAnnotationDoc.d.ts +7 -0
  57. package/dist/data/hooks/useAnnotationDoc.js +33 -0
  58. package/dist/data/hooks/useAnnotationList.d.ts +7 -0
  59. package/dist/data/hooks/useAnnotationList.js +26 -0
  60. package/dist/data/hooks/useAnnotationMutations.d.ts +9 -0
  61. package/dist/data/hooks/useAnnotationMutations.js +11 -0
  62. package/dist/export/buildJobCsv.js +2 -3
  63. package/dist/exports.d.ts +2 -2
  64. package/dist/exports.js +2 -2
  65. package/dist/formulas/calculateFormula.d.ts +1 -1
  66. package/dist/formulas/calculateFormula.js +22 -24
  67. package/dist/formulas/evaluateFormula.d.ts +10 -0
  68. package/dist/formulas/evaluateFormula.js +17 -2
  69. package/dist/hooks/useParseMeasurement.d.ts +4 -0
  70. package/dist/hooks/useParseMeasurement.js +14 -0
  71. package/dist/index.d.ts +1 -1
  72. package/dist/index.js +1 -1
  73. package/dist/server.d.ts +2 -2
  74. package/dist/server.js +6 -2
  75. package/dist/types/firestore.d.ts +8 -0
  76. package/dist/utils/evaluateFormula.d.ts +20 -0
  77. package/dist/utils/evaluateFormula.js +31 -0
  78. package/package.json +1 -1
@@ -0,0 +1,9 @@
1
+ import type { AnnotationFile, ImageBlob, JobGroupScope, Patch, UploadedImageRef } from '../AnnotationDataProvider.js';
2
+ export interface AnnotationMutations {
3
+ create(seed: Partial<AnnotationFile>): Promise<string>;
4
+ update(fileId: string, patch: Patch<AnnotationFile>): Promise<void>;
5
+ remove(fileId: string): Promise<void>;
6
+ uploadImage(fileId: string, role: 'background' | 'thumbnail', blob: ImageBlob): Promise<UploadedImageRef>;
7
+ deleteImage(fileId: string, storagePath: string): Promise<void>;
8
+ }
9
+ export declare const useAnnotationMutations: (scope: JobGroupScope) => AnnotationMutations;
@@ -0,0 +1,11 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { useAnnotationData } from '../AnnotationDataContext.js';
3
+ export const useAnnotationMutations = (scope) => {
4
+ const provider = useAnnotationData();
5
+ const create = useCallback((seed) => provider.create(scope, seed), [provider, scope.orgId, scope.projectId, scope.jobId, scope.groupId]);
6
+ const update = useCallback((fileId, patch) => provider.update(scope, fileId, patch), [provider, scope.orgId, scope.projectId, scope.jobId, scope.groupId]);
7
+ const remove = useCallback((fileId) => provider.delete(scope, fileId), [provider, scope.orgId, scope.projectId, scope.jobId, scope.groupId]);
8
+ const uploadImage = useCallback((fileId, role, blob) => provider.uploadImage(scope, fileId, role, blob), [provider, scope.orgId, scope.projectId, scope.jobId, scope.groupId]);
9
+ const deleteImage = useCallback((fileId, storagePath) => provider.deleteImage(scope, fileId, storagePath), [provider, scope.orgId, scope.projectId, scope.jobId, scope.groupId]);
10
+ return useMemo(() => ({ create, update, remove, uploadImage, deleteImage }), [create, update, remove, uploadImage, deleteImage]);
11
+ };
@@ -15,8 +15,7 @@ const objectCell = (obj) => obj.url ? hyperlinkCell(obj.url, obj.name) : escapeC
15
15
  const toExportDate = (value) => {
16
16
  if (value instanceof Date)
17
17
  return value;
18
- if (value &&
19
- typeof value.toDate === 'function') {
18
+ if (value && typeof value.toDate === 'function') {
20
19
  return value.toDate();
21
20
  }
22
21
  return null;
@@ -30,7 +29,7 @@ const dateCell = (value) => {
30
29
  ? `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`
31
30
  : '';
32
31
  };
33
- const userCell = (user) => (user ? [user.firstName, user.lastName].filter(Boolean).join(' ') : '');
32
+ const userCell = (user) => user ? [user.firstName, user.lastName].filter(Boolean).join(' ') : '';
34
33
  // Pure CSV assembly for the job export, shared by rock-pro-app and
35
34
  // rock-desktop so the two platforms emit identical files. With no objects
36
35
  // selected and the legacy measurement/label/notes columns on, the
package/dist/exports.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, type EnhancedMapping, type FormulaDefinition, type FormulaEvaluationOptions, } from './formulas/evaluateFormula.js';
1
+ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, micrometersToUnitNumber, type EnhancedMapping, type FormulaDefinition, type FormulaEvaluationOptions, } from './formulas/evaluateFormula.js';
2
2
  export { calculateFormula } from './formulas/calculateFormula.js';
3
3
  export { convertMicrometers } from './utils/micrometersToUnit.js';
4
4
  export { parseMeasurement } from './utils/parseMeasurement.js';
@@ -37,7 +37,7 @@ export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './anno
37
37
  export type { CanvasBackgroundSlotArgs, RenderCanvasBackground, } from './annotation/canvas/backgroundSlot.js';
38
38
  export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
39
39
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, type GroupHeaderBadge, type GroupHeaderDescriptor, } from './annotation/canvas/groupHeaderLayout.js';
40
- export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, type ViewportApi, type ViewportState, type ZoomLimits, } from './annotation/canvas/viewport.js';
40
+ export { createViewportApi, fitToScreen, fitRectToScreen, fitDocRectViewport, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, type ViewportApi, type ViewportState, type ZoomLimits, } from './annotation/canvas/viewport.js';
41
41
  export { DEFAULT_PDF_PAGE_GAP, DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM, makePdfDocumentId, createPdfDocument, pdfPageDocRects, pdfDocumentExtent, pdfFirstPageDocRect, computeZoomFloor, type PdfDocRect, } from './annotation/canvas/pdfDocument.js';
42
42
  export { normalizePdfRotation, effectivePageRotation, pdfPageDisplaySize, pdfCanvasPageDocId, emptyPdfCanvasPageState, pdfCanvasPageDocument, pdfCanvasKnobsOf, pdfCanvasKnobsEqual, pageStateToCanvasState, canvasStateToPageState, rotatePointInPage, rotatePageState, rotatePdfCanvasPage, type PdfPageDisplaySize, type PdfRotationDelta, } from './annotation/canvas/pdfCanvasPages.js';
43
43
  export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerIsLocked, isBackgroundLayerLocked, defaultBackgroundLayerLocked, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
package/dist/exports.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // platform-specific `AnnotationCanvas` wrapper lives here. The two index
3
3
  // files (index.ts, index.native.ts) re-export from this and add their own
4
4
  // AnnotationCanvas import.
5
- export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, } from './formulas/evaluateFormula.js';
5
+ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, micrometersToUnitNumber, } from './formulas/evaluateFormula.js';
6
6
  export { calculateFormula } from './formulas/calculateFormula.js';
7
7
  export { convertMicrometers } from './utils/micrometersToUnit.js';
8
8
  export { parseMeasurement } from './utils/parseMeasurement.js';
@@ -43,7 +43,7 @@ export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
43
43
  export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
44
44
  export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, } from './annotation/canvas/stampLayout.js';
45
45
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, } from './annotation/canvas/groupHeaderLayout.js';
46
- export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './annotation/canvas/viewport.js';
46
+ export { createViewportApi, fitToScreen, fitRectToScreen, fitDocRectViewport, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './annotation/canvas/viewport.js';
47
47
  export { DEFAULT_PDF_PAGE_GAP, DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM, makePdfDocumentId, createPdfDocument, pdfPageDocRects, pdfDocumentExtent, pdfFirstPageDocRect, computeZoomFloor, } from './annotation/canvas/pdfDocument.js';
48
48
  export { normalizePdfRotation, effectivePageRotation, pdfPageDisplaySize, pdfCanvasPageDocId, emptyPdfCanvasPageState, pdfCanvasPageDocument, pdfCanvasKnobsOf, pdfCanvasKnobsEqual, pageStateToCanvasState, canvasStateToPageState, rotatePointInPage, rotatePageState, rotatePdfCanvasPage, } from './annotation/canvas/pdfCanvasPages.js';
49
49
  export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerIsLocked, isBackgroundLayerLocked, defaultBackgroundLayerLocked, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
@@ -1,2 +1,2 @@
1
1
  import { ColumnConfig, DecimalTolerance, FractionalTolerance, Formula, Measurement, Units } from '../types/firestore.js';
2
- export declare const calculateFormula: (formula: Formula, formulas: Formula[], columns: Record<string, string>, tableConfig: ColumnConfig[], measurements: Measurement[], unit: Units, fractionalTolerance: FractionalTolerance, decimalTolerance: DecimalTolerance) => string | number | null;
2
+ export declare const calculateFormula: (formula: Formula, formulas: Formula[], columns: Record<string, string>, tableConfig: ColumnConfig[], measurements: Measurement[], unit: Units, _fractionalTolerance?: FractionalTolerance, _decimalTolerance?: DecimalTolerance) => string | number | null;
@@ -1,25 +1,15 @@
1
- import { ColumnType, } from '../types/firestore.js';
2
- import { evaluateFormula } from './evaluateFormula.js';
3
- import { convertMicrometers } from '../utils/micrometersToUnit.js';
4
- function parseMixedNumber(str) {
5
- const parts = str.trim().split(' ');
6
- let whole = 0;
7
- let fraction = 0;
8
- if (parts.length === 2) {
9
- whole = parseFloat(parts[0]);
10
- const [num, den] = parts[1].split('/').map(Number);
11
- fraction = num / den;
12
- }
13
- else if (parts[0].includes('/')) {
14
- const [num, den] = parts[0].split('/').map(Number);
15
- fraction = num / den;
16
- }
17
- else {
18
- whole = parseFloat(parts[0]);
19
- }
20
- return whole + fraction;
21
- }
22
- export const calculateFormula = (formula, formulas, columns, tableConfig, measurements, unit, fractionalTolerance, decimalTolerance) => {
1
+ import { ColumnType, Units, } from '../types/firestore.js';
2
+ import { evaluateFormula, micrometersToUnitNumber } from './evaluateFormula.js';
3
+ const VALID_UNITS = new Set(Object.values(Units));
4
+ // A measurement operand resolves in ITS column's locked unit when one is set;
5
+ // an unrecognized/legacy override falls back to the caller's unit — never to
6
+ // normalizeUnitForMathJS's silent 'mm' default, which would be a wrong answer
7
+ // rather than a visible error.
8
+ const resolveColumnUnit = (override, fallback) => (override && VALID_UNITS.has(override) ? override : fallback);
9
+ export const calculateFormula = (formula, formulas, columns, tableConfig, measurements, unit,
10
+ // Unused since measurement values stopped round-tripping through the display
11
+ // string — kept so the 8-arg call sites in the apps don't break.
12
+ _fractionalTolerance, _decimalTolerance) => {
23
13
  // Convert Firestore Map to plain object if necessary
24
14
  let variableToColumnMap = {};
25
15
  if (formula.variableToColumnMap) {
@@ -45,8 +35,16 @@ export const calculateFormula = (formula, formulas, columns, tableConfig, measur
45
35
  const measurement = measurements.find((m) => m.id === measurementId);
46
36
  if (!measurement)
47
37
  continue;
48
- const { value, unit: displayUnit } = convertMicrometers(measurement.value, unit, fractionalTolerance, decimalTolerance);
49
- valueMap[columnId] = parseMixedNumber(value);
38
+ // Straight um -> display-unit number. The old path rendered the display
39
+ // STRING (convertMicrometers) and re-parsed it, which rounded the value
40
+ // to the display tolerance and returned NaN / dropped the inches for the
41
+ // composite feet-inches formats (`5' 3 1/2"`).
42
+ //
43
+ // Each operand resolves in its own column's unit (lock ?? user unit),
44
+ // so a locked column contributes the number its cell displays. With
45
+ // mixed locks the expression is numeric over display-space values by
46
+ // design; the RESULT (and typed constants) stay in the caller's unit.
47
+ valueMap[columnId] = micrometersToUnitNumber(measurement.value, resolveColumnUnit(column.unitOverride, unit));
50
48
  }
51
49
  else if (column.type === ColumnType.Angle) {
52
50
  const measurement = measurements.find((m) => m.id === measurementId);
@@ -17,6 +17,16 @@ export interface FormulaEvaluationOptions {
17
17
  formulas?: FormulaDefinition[];
18
18
  scope?: Record<string, number>;
19
19
  }
20
+ export declare function normalizeUnitForMathJS(unit: Units | string): string;
21
+ /**
22
+ * Convert a stored micrometer value to the number the formula scope expects
23
+ * for `unit` — the same conversion nested-formula results go through. This is
24
+ * the ONLY sanctioned way to put a measurement into a valueMap: converting via
25
+ * the display STRING (convertMicrometers + a string parser) both rounds the
26
+ * value to the display tolerance and falls apart on composite formats like
27
+ * `5' 3 1/2"`.
28
+ */
29
+ export declare function micrometersToUnitNumber(micrometers: number, unit: Units): number;
20
30
  export declare function evaluateFormula({ expression, mappings, valueMap, unit, formulas, scope, }: FormulaEvaluationOptions): number | string | null;
21
31
  export declare function createFormulaScope({ mappings, valueMap, unit, }: {
22
32
  mappings: Record<string, string>;
@@ -7,7 +7,7 @@ const compile = math.compile.bind(math);
7
7
  const mathUnit = math.unit.bind(math);
8
8
  // Cache for evaluated formulas to improve performance
9
9
  const formulaCache = new Map();
10
- function normalizeUnitForMathJS(unit) {
10
+ export function normalizeUnitForMathJS(unit) {
11
11
  switch (unit) {
12
12
  case Units.Centimeters:
13
13
  case 'cm':
@@ -30,6 +30,17 @@ function normalizeUnitForMathJS(unit) {
30
30
  return 'mm'; // fallback
31
31
  }
32
32
  }
33
+ /**
34
+ * Convert a stored micrometer value to the number the formula scope expects
35
+ * for `unit` — the same conversion nested-formula results go through. This is
36
+ * the ONLY sanctioned way to put a measurement into a valueMap: converting via
37
+ * the display STRING (convertMicrometers + a string parser) both rounds the
38
+ * value to the display tolerance and falls apart on composite formats like
39
+ * `5' 3 1/2"`.
40
+ */
41
+ export function micrometersToUnitNumber(micrometers, unit) {
42
+ return mathUnit(micrometers, 'um').toNumber(normalizeUnitForMathJS(unit));
43
+ }
33
44
  // Normalize mapping to EnhancedMapping format
34
45
  function normalizeMapping(mapping) {
35
46
  if (typeof mapping === 'string') {
@@ -112,7 +123,11 @@ export function evaluateFormula({ expression, mappings, valueMap, unit, formulas
112
123
  return 'Error: valueMap is required';
113
124
  }
114
125
  const evaluationStack = new Set(); // For circular dependency detection
115
- const cache = new Map(); // Local cache for this evaluation session
126
+ // Local cache for this evaluation session. Deliberately per-invocation:
127
+ // valueMap is rebuilt (with any column unit locks applied) on every
128
+ // calculateFormula call, so the `${formulaId}_${unit}` key needs no lock
129
+ // signature — a stale entry cannot outlive the call that created it.
130
+ const cache = new Map();
116
131
  function evaluateNestedFormula(formulaId, currentExpression, currentMappings) {
117
132
  // Check for circular dependency
118
133
  if (evaluationStack.has(formulaId)) {
@@ -0,0 +1,4 @@
1
+ export declare const useParseMeasurement: () => {
2
+ parseMeasurementInput: (input: string, defaultUnit?: string) => number | null;
3
+ error: string | null;
4
+ };
@@ -0,0 +1,14 @@
1
+ import { useState } from 'react';
2
+ import { parseMeasurement } from '../utils/parseMeasurement.js';
3
+ export const useParseMeasurement = () => {
4
+ const [error, setError] = useState(null);
5
+ const parseMeasurementInput = (input, defaultUnit = 'mm') => {
6
+ setError(null);
7
+ const result = parseMeasurement(input, defaultUnit);
8
+ if (result === null) {
9
+ setError('Invalid measurement. Please provide a valid number and unit.');
10
+ }
11
+ return result;
12
+ };
13
+ return { parseMeasurementInput, error };
14
+ };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from './exports.js';
2
- export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, type EnhancedMapping, type FormulaDefinition, type FormulaEvaluationOptions, } from './formulas/evaluateFormula.js';
2
+ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, micrometersToUnitNumber, type EnhancedMapping, type FormulaDefinition, type FormulaEvaluationOptions, } from './formulas/evaluateFormula.js';
3
3
  export { calculateFormula } from './formulas/calculateFormula.js';
4
4
  export { convertMicrometers } from './utils/micrometersToUnit.js';
5
5
  export { parseMeasurement } from './utils/parseMeasurement.js';
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // WithSkiaWeb. Metro/RN consumers resolve `index.native.ts` instead via
4
4
  // the `react-native` condition in package.json `exports`.
5
5
  export * from './exports.js';
6
- export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, } from './formulas/evaluateFormula.js';
6
+ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, micrometersToUnitNumber, } from './formulas/evaluateFormula.js';
7
7
  export { calculateFormula } from './formulas/calculateFormula.js';
8
8
  export { convertMicrometers } from './utils/micrometersToUnit.js';
9
9
  export { parseMeasurement } from './utils/parseMeasurement.js';
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, type EnhancedMapping, type FormulaDefinition, type FormulaEvaluationOptions, } from './formulas/evaluateFormula.js';
1
+ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, micrometersToUnitNumber, type EnhancedMapping, type FormulaDefinition, type FormulaEvaluationOptions, } from './formulas/evaluateFormula.js';
2
2
  export { calculateFormula } from './formulas/calculateFormula.js';
3
3
  export { convertMicrometers } from './utils/micrometersToUnit.js';
4
4
  export { parseMeasurement } from './utils/parseMeasurement.js';
@@ -18,5 +18,5 @@ export { numberToLetterIndex, formGroupInputLabel, listGroupMeasurementLabel, }
18
18
  export { toSelectedValues, formatSelectedValues, toStoredValue, } from './utils/selectValues.js';
19
19
  export * from './calculator/index.js';
20
20
  export * from './export/pdf/index.js';
21
- export { applyGroupFormulaUm, groupFormulaExpression, micrometersToUnitNumber, unitNumberToMicrometers, type GroupFormulaFrame, } from './formulas/groupFormula.js';
21
+ export { applyGroupFormulaUm, groupFormulaExpression, unitNumberToMicrometers, type GroupFormulaFrame, } from './formulas/groupFormula.js';
22
22
  export { MEASUREMENT_LINE_COLOR, MEASUREMENT_LINE_WIDTH, MEASUREMENT_RECT_WIDTH, } from './annotation/canvas/measurementLineStyle.js';
package/dist/server.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // root barrel, whose module graph statically pulls in `react` and
4
4
  // `@shopify/react-native-skia`. Only modules with no React/Skia anywhere
5
5
  // in their import graph may be re-exported from this file.
6
- export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, } from './formulas/evaluateFormula.js';
6
+ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, micrometersToUnitNumber, } from './formulas/evaluateFormula.js';
7
7
  export { calculateFormula } from './formulas/calculateFormula.js';
8
8
  export { convertMicrometers } from './utils/micrometersToUnit.js';
9
9
  export { parseMeasurement } from './utils/parseMeasurement.js';
@@ -25,5 +25,9 @@ export * from './calculator/index.js';
25
25
  // Canvas → annotated PDF export (Cloud Function renderer). Pure pdf-lib; no
26
26
  // React or Skia anywhere in its import graph.
27
27
  export * from './export/pdf/index.js';
28
- export { applyGroupFormulaUm, groupFormulaExpression, micrometersToUnitNumber, unitNumberToMicrometers, } from './formulas/groupFormula.js';
28
+ // groupFormula's own micrometersToUnitNumber (integer constants, NaN on
29
+ // unknown units) stays module-local: the canonical export of that name is the
30
+ // mathjs-exact one from evaluateFormula.js above, and no consumer imported
31
+ // the groupFormula variant.
32
+ export { applyGroupFormulaUm, groupFormulaExpression, unitNumberToMicrometers, } from './formulas/groupFormula.js';
29
33
  export { MEASUREMENT_LINE_COLOR, MEASUREMENT_LINE_WIDTH, MEASUREMENT_RECT_WIDTH, } from './annotation/canvas/measurementLineStyle.js';
@@ -453,6 +453,14 @@ export type ColumnConfig = ColumnConfigBase & ({
453
453
  } | {
454
454
  type: ColumnType.Measurement;
455
455
  columnData?: undefined;
456
+ /**
457
+ * Locks the column to a fixed display/input unit for all users;
458
+ * absent = follow each viewer's default unit. Formula operands from
459
+ * this column resolve in this unit (calculateFormula). Clearing the
460
+ * lock removes the key (present-as-undefined is rejected by RN
461
+ * Firebase and stripped by desktop's sanitizer).
462
+ */
463
+ unitOverride?: Units;
456
464
  } | {
457
465
  type: ColumnType.Boolean;
458
466
  columnData?: undefined;
@@ -0,0 +1,20 @@
1
+ type MeasurementMap = Map<string, {
2
+ id: string;
3
+ value: number;
4
+ }[]>;
5
+ interface FormulaEvaluationOptions {
6
+ expression: string;
7
+ mappings: Record<string, string>;
8
+ group: {
9
+ id: string;
10
+ columns: Map<string, any>;
11
+ };
12
+ columns: {
13
+ id: string;
14
+ type: string;
15
+ }[];
16
+ measurementMap: MeasurementMap;
17
+ defaultUnit?: string;
18
+ }
19
+ export declare function evaluateFormula({ expression, mappings, group, columns, measurementMap, defaultUnit, }: FormulaEvaluationOptions): number | string;
20
+ export {};
@@ -0,0 +1,31 @@
1
+ import { create, all } from 'mathjs';
2
+ const math = create(all);
3
+ export function evaluateFormula({ expression, mappings, group, columns, measurementMap, defaultUnit = 'um', }) {
4
+ const scope = {};
5
+ for (const [variable, columnId] of Object.entries(mappings)) {
6
+ const rawValue = group.columns.get(columnId);
7
+ const columnDef = columns.find((c) => c.id === columnId);
8
+ if (columnDef?.type === 'Measurement') {
9
+ const groupMeasurements = measurementMap.get(group.id) || [];
10
+ const measurement = groupMeasurements.find((m) => m.id === rawValue);
11
+ if (measurement?.value !== undefined) {
12
+ scope[variable] = math.unit(measurement.value, defaultUnit);
13
+ continue;
14
+ }
15
+ }
16
+ const parsedValue = typeof rawValue === 'number' ? rawValue : parseFloat(rawValue || '');
17
+ scope[variable] = isNaN(parsedValue) ? 0 : parsedValue;
18
+ }
19
+ try {
20
+ const compiled = math.compile(expression);
21
+ const result = compiled.evaluate(scope);
22
+ if (math.typeOf(result) === 'Unit') {
23
+ return result.toNumber(defaultUnit);
24
+ }
25
+ return result;
26
+ }
27
+ catch (err) {
28
+ console.error(`Formula evaluation failed:`, err);
29
+ return 'Error';
30
+ }
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.24.0",
3
+ "version": "1.26.0",
4
4
  "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
5
  "author": "REEKON Tools",
6
6
  "license": "MIT",