@reekon-tools/boldr-utils 1.7.5 → 1.8.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.
- package/dist/annotation/canvas/elements/BackgroundImageElement.js +15 -3
- package/dist/annotation/canvas/tools/measurementLineTool.d.ts +12 -0
- package/dist/annotation/canvas/tools/measurementLineTool.js +95 -0
- package/dist/calculator/categories.d.ts +76 -0
- package/dist/calculator/categories.js +160 -0
- package/dist/calculator/index.d.ts +1 -0
- package/dist/calculator/index.js +1 -0
- package/dist/calculator/schema.d.ts +12 -0
- package/dist/calculator/schema.js +3 -0
- package/dist/calculator/validate.d.ts +1 -0
- package/dist/calculator/validate.js +15 -0
- package/package.json +2 -1
- 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/Tool.js +0 -1
- 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
|
@@ -42,6 +42,11 @@ const useBackgroundUrl = (image, resolveUrl) => {
|
|
|
42
42
|
// on a null/undefined source (despite its DataSourceParam type), so it can't
|
|
43
43
|
// be called unconditionally from a component that usually renders raster
|
|
44
44
|
// backgrounds. An empty url resolves to null (nothing to draw yet).
|
|
45
|
+
// Loaded through Skia's own scheme-agnostic data loader rather than fetch():
|
|
46
|
+
// RN's Android networking rejects non-http(s) schemes, and offline-first
|
|
47
|
+
// consumers resolve the background to a local file:// URI (fromURI handles
|
|
48
|
+
// file://, http(s) and data: on native and web alike). fetch() remains as a
|
|
49
|
+
// fallback for any environment where fromURI can't service the URL.
|
|
45
50
|
const useBackgroundSvg = (url) => {
|
|
46
51
|
const [svg, setSvg] = useState(null);
|
|
47
52
|
useEffect(() => {
|
|
@@ -52,9 +57,16 @@ const useBackgroundSvg = (url) => {
|
|
|
52
57
|
let cancelled = false;
|
|
53
58
|
(async () => {
|
|
54
59
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
let next = null;
|
|
61
|
+
try {
|
|
62
|
+
const data = await Skia.Data.fromURI(url);
|
|
63
|
+
next = Skia.SVG.MakeFromData(data);
|
|
64
|
+
data.dispose();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
const res = await fetch(url);
|
|
68
|
+
next = Skia.SVG.MakeFromString(await res.text());
|
|
69
|
+
}
|
|
58
70
|
if (!cancelled)
|
|
59
71
|
setSvg(next);
|
|
60
72
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PlacedMeasurementRef } from '../../../types/annotation.js';
|
|
2
|
+
import type { Tool } from '../Tool.js';
|
|
3
|
+
export interface MeasurementLineToolOptions {
|
|
4
|
+
id?: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
minDragPx?: number;
|
|
7
|
+
autoSwitchToSelect?: boolean;
|
|
8
|
+
selectToolId?: string;
|
|
9
|
+
onAutoSwitch?: (toToolId: string) => void;
|
|
10
|
+
onPlaced?: (measurement: PlacedMeasurementRef) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare const createMeasurementLineTool: (options?: MeasurementLineToolOptions) => Tool;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DEFAULT_LAYER_ID } from '../../../types/annotation.js';
|
|
2
|
+
import { DEFAULT_LINE_POS, recomputeAnchor } from '../measurementGeometry.js';
|
|
3
|
+
let counter = 0;
|
|
4
|
+
const makeId = () => `annotation-${Date.now().toString(36)}-${(counter++).toString(36)}`;
|
|
5
|
+
const firstLayerId = (doc) => doc.layers[0]?.id ?? DEFAULT_LAYER_ID;
|
|
6
|
+
// Build the blank line-measurement annotation for a drag from `a` to `b`: a
|
|
7
|
+
// 2-point line with a value tile magnetized to its center (linePos 0.5). The
|
|
8
|
+
// payload mirrors useAnnotationCanvasState's placeAnnotationAtCenter so a drawn
|
|
9
|
+
// line and a (legacy) center-placed one are byte-identical once committed.
|
|
10
|
+
const buildLineMeasurement = (opts) => {
|
|
11
|
+
const line = { a: opts.a, b: opts.b };
|
|
12
|
+
return {
|
|
13
|
+
id: opts.id,
|
|
14
|
+
layerId: opts.layerId,
|
|
15
|
+
placement: 'line',
|
|
16
|
+
line,
|
|
17
|
+
linePos: DEFAULT_LINE_POS,
|
|
18
|
+
// Center of the line; recomputeAnchor keeps this in sync on edits.
|
|
19
|
+
anchor: recomputeAnchor(line, 'line', DEFAULT_LINE_POS, {
|
|
20
|
+
x: (opts.a.x + opts.b.x) / 2,
|
|
21
|
+
y: (opts.a.y + opts.b.y) / 2,
|
|
22
|
+
}),
|
|
23
|
+
showLabel: true,
|
|
24
|
+
showValue: true,
|
|
25
|
+
createdAt: Date.now(),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
// Drag-to-draw measurement-line tool. Press-drag rubber-bands a measurement
|
|
29
|
+
// annotation (a line with a blank value tile at its center); release commits
|
|
30
|
+
// it. Mirrors createShapeTool's interaction so "input lines" are placed the
|
|
31
|
+
// same way as shapes — draw to add, not tap-the-icon-to-add — replacing the
|
|
32
|
+
// old center-place affordance. The annotation stays blank (no measurement
|
|
33
|
+
// associated) until the user fills it via the tile / picker. Skia-free, like
|
|
34
|
+
// every tool factory; it renders its live preview through ctx.preview, so it
|
|
35
|
+
// works on web and (via the generic tool-pan dispatch) on native.
|
|
36
|
+
export const createMeasurementLineTool = (options = {}) => {
|
|
37
|
+
const minDragPx = options.minDragPx ?? 4;
|
|
38
|
+
const autoSwitchToSelect = options.autoSwitchToSelect ?? true;
|
|
39
|
+
const selectToolId = options.selectToolId ?? 'select';
|
|
40
|
+
return {
|
|
41
|
+
id: options.id ?? 'measure-line',
|
|
42
|
+
label: options.label ?? 'Measurement line',
|
|
43
|
+
cursor: 'crosshair',
|
|
44
|
+
onPointerDown(event) {
|
|
45
|
+
return {
|
|
46
|
+
kind: 'measurement-line-drawing',
|
|
47
|
+
id: makeId(),
|
|
48
|
+
startWorld: event.world,
|
|
49
|
+
startScreen: event.screen,
|
|
50
|
+
moved: false,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
onPointerMove(event, ctx, state) {
|
|
54
|
+
const s = state;
|
|
55
|
+
if (s?.kind !== 'measurement-line-drawing')
|
|
56
|
+
return s;
|
|
57
|
+
const measurement = buildLineMeasurement({
|
|
58
|
+
id: s.id,
|
|
59
|
+
layerId: firstLayerId(ctx.document),
|
|
60
|
+
a: s.startWorld,
|
|
61
|
+
b: event.world,
|
|
62
|
+
});
|
|
63
|
+
ctx.preview({ ops: [{ op: 'addMeasurement', measurement }] });
|
|
64
|
+
return { ...s, moved: true };
|
|
65
|
+
},
|
|
66
|
+
onPointerUp(event, ctx, state) {
|
|
67
|
+
const s = state;
|
|
68
|
+
if (s?.kind !== 'measurement-line-drawing')
|
|
69
|
+
return;
|
|
70
|
+
const dx = event.screen.x - s.startScreen.x;
|
|
71
|
+
const dy = event.screen.y - s.startScreen.y;
|
|
72
|
+
if (!s.moved || dx * dx + dy * dy < minDragPx * minDragPx) {
|
|
73
|
+
// Accidental tap — discard the rubber-band, commit nothing.
|
|
74
|
+
ctx.preview({ ops: [] });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const measurement = buildLineMeasurement({
|
|
78
|
+
id: s.id,
|
|
79
|
+
layerId: firstLayerId(ctx.document),
|
|
80
|
+
a: s.startWorld,
|
|
81
|
+
b: event.world,
|
|
82
|
+
});
|
|
83
|
+
ctx.commit({ ops: [{ op: 'addMeasurement', measurement }] });
|
|
84
|
+
// Leave it selected (and hand back to select) so the blank line can be
|
|
85
|
+
// filled / moved straight away — the same flow as the text tool.
|
|
86
|
+
ctx.setSelection({ ids: [measurement.id] });
|
|
87
|
+
options.onPlaced?.(measurement);
|
|
88
|
+
if (autoSwitchToSelect)
|
|
89
|
+
options.onAutoSwitch?.(selectToolId);
|
|
90
|
+
},
|
|
91
|
+
onCancel(_state, ctx) {
|
|
92
|
+
ctx.preview({ ops: [] });
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { CalculatorDefinition } from './schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* Category ids, as PERSISTED on `calculators/{id}.category`.
|
|
4
|
+
*
|
|
5
|
+
* Never rename a value — it is stored data. To retire a category, delete its
|
|
6
|
+
* entry from CALCULATOR_CATEGORIES and leave existing docs alone: they resolve
|
|
7
|
+
* to `undefined` and degrade into the "Other" bucket rather than breaking.
|
|
8
|
+
*/
|
|
9
|
+
export declare enum CalculatorCategoryId {
|
|
10
|
+
AreaAndVolume = "areaAndVolume",
|
|
11
|
+
Concrete = "concrete",
|
|
12
|
+
Electrical = "electrical",
|
|
13
|
+
Stairs = "stairs",
|
|
14
|
+
Framing = "framing",
|
|
15
|
+
FinishCarpentry = "finishCarpentry",
|
|
16
|
+
Hardscaping = "hardscaping",
|
|
17
|
+
Roofing = "roofing",
|
|
18
|
+
HVAC = "hvac",
|
|
19
|
+
Landscaping = "landscaping",
|
|
20
|
+
MaterialEstimator = "materialEstimator",
|
|
21
|
+
Optimizers = "optimizers",
|
|
22
|
+
Plumbing = "plumbing"
|
|
23
|
+
}
|
|
24
|
+
export interface CalculatorCategory {
|
|
25
|
+
id: CalculatorCategoryId;
|
|
26
|
+
/** Display label: the editor dropdown row AND the mobile category screen title. */
|
|
27
|
+
name: string;
|
|
28
|
+
/**
|
|
29
|
+
* Tile-glyph tint from the design. Uppercase 6-digit hex.
|
|
30
|
+
*
|
|
31
|
+
* This is the source of truth for the color: the icon generator bakes it
|
|
32
|
+
* into the per-app SVGs (see scripts/gen-category-icons.mjs), so a color
|
|
33
|
+
* change here needs a regenerate to reach the apps.
|
|
34
|
+
*/
|
|
35
|
+
color: string;
|
|
36
|
+
/**
|
|
37
|
+
* Glyph name in the legacy ROCK app's IcoMoon set, which is where this
|
|
38
|
+
* artwork comes from. Consumed only by the icon generator — clients load the
|
|
39
|
+
* emitted SVG by category id.
|
|
40
|
+
*/
|
|
41
|
+
glyph: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Canonical order, matching the design's tile grid.
|
|
45
|
+
*
|
|
46
|
+
* This array IS the ordering — there is deliberately no `sortIndex` field for
|
|
47
|
+
* it to drift out of sync with.
|
|
48
|
+
*/
|
|
49
|
+
export declare const CALCULATOR_CATEGORIES: readonly CalculatorCategory[];
|
|
50
|
+
export declare const CALCULATOR_CATEGORY_BY_ID: Readonly<Record<CalculatorCategoryId, CalculatorCategory>>;
|
|
51
|
+
/**
|
|
52
|
+
* Bucket key / route segment for calculators with no category, or with one
|
|
53
|
+
* that no longer exists. NOT a CalculatorCategoryId — guarded by a unit test,
|
|
54
|
+
* because a collision would silently swallow a real category.
|
|
55
|
+
*/
|
|
56
|
+
export declare const UNCATEGORIZED_CATEGORY_ID = "uncategorized";
|
|
57
|
+
export declare const UNCATEGORIZED_CATEGORY_NAME = "Other";
|
|
58
|
+
export type CalculatorCategoryBucketId = CalculatorCategoryId | typeof UNCATEGORIZED_CATEGORY_ID;
|
|
59
|
+
export declare const isCalculatorCategoryId: (value: unknown) => value is CalculatorCategoryId;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a stored category value. Returns undefined for missing, null, and
|
|
62
|
+
* retired ids — callers should treat all three as "Other" rather than trusting
|
|
63
|
+
* the raw field.
|
|
64
|
+
*/
|
|
65
|
+
export declare const findCalculatorCategory: (value: string | null | undefined) => CalculatorCategory | undefined;
|
|
66
|
+
export declare const calculatorCategoryOf: (calculator: Pick<CalculatorDefinition, "category">) => CalculatorCategory | undefined;
|
|
67
|
+
/** Display label for a stored value, falling back to "Other". */
|
|
68
|
+
export declare const calculatorCategoryName: (value: string | null | undefined) => string;
|
|
69
|
+
/**
|
|
70
|
+
* Group calculators by category id, collecting anything missing or
|
|
71
|
+
* unresolvable under UNCATEGORIZED_CATEGORY_ID.
|
|
72
|
+
*
|
|
73
|
+
* Keys exist only for non-empty buckets, which is what lets the mobile grid
|
|
74
|
+
* hide empty categories without a second pass.
|
|
75
|
+
*/
|
|
76
|
+
export declare const groupCalculatorsByCategory: <T extends Pick<CalculatorDefinition, "category">>(calculators: readonly T[]) => Map<CalculatorCategoryBucketId, T[]>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Calculator categories — the hard-coded taxonomy used to group published
|
|
3
|
+
// calculators in the mobile catalog and to label them in the web authoring
|
|
4
|
+
// tool.
|
|
5
|
+
//
|
|
6
|
+
// Deliberately NOT a Firestore collection: the set is authored here and ships
|
|
7
|
+
// inside the package, so both clients render the same list with no extra read,
|
|
8
|
+
// no rules surface, and no offline story to maintain.
|
|
9
|
+
//
|
|
10
|
+
// Distinct from `CalculatorDefinition.folderId`, which predates this and is
|
|
11
|
+
// still unused — `folderId` implies a hierarchy, a category is flat.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/**
|
|
14
|
+
* Category ids, as PERSISTED on `calculators/{id}.category`.
|
|
15
|
+
*
|
|
16
|
+
* Never rename a value — it is stored data. To retire a category, delete its
|
|
17
|
+
* entry from CALCULATOR_CATEGORIES and leave existing docs alone: they resolve
|
|
18
|
+
* to `undefined` and degrade into the "Other" bucket rather than breaking.
|
|
19
|
+
*/
|
|
20
|
+
export var CalculatorCategoryId;
|
|
21
|
+
(function (CalculatorCategoryId) {
|
|
22
|
+
CalculatorCategoryId["AreaAndVolume"] = "areaAndVolume";
|
|
23
|
+
CalculatorCategoryId["Concrete"] = "concrete";
|
|
24
|
+
CalculatorCategoryId["Electrical"] = "electrical";
|
|
25
|
+
CalculatorCategoryId["Stairs"] = "stairs";
|
|
26
|
+
CalculatorCategoryId["Framing"] = "framing";
|
|
27
|
+
CalculatorCategoryId["FinishCarpentry"] = "finishCarpentry";
|
|
28
|
+
CalculatorCategoryId["Hardscaping"] = "hardscaping";
|
|
29
|
+
CalculatorCategoryId["Roofing"] = "roofing";
|
|
30
|
+
CalculatorCategoryId["HVAC"] = "hvac";
|
|
31
|
+
CalculatorCategoryId["Landscaping"] = "landscaping";
|
|
32
|
+
CalculatorCategoryId["MaterialEstimator"] = "materialEstimator";
|
|
33
|
+
CalculatorCategoryId["Optimizers"] = "optimizers";
|
|
34
|
+
CalculatorCategoryId["Plumbing"] = "plumbing";
|
|
35
|
+
})(CalculatorCategoryId || (CalculatorCategoryId = {}));
|
|
36
|
+
/**
|
|
37
|
+
* Canonical order, matching the design's tile grid.
|
|
38
|
+
*
|
|
39
|
+
* This array IS the ordering — there is deliberately no `sortIndex` field for
|
|
40
|
+
* it to drift out of sync with.
|
|
41
|
+
*/
|
|
42
|
+
export const CALCULATOR_CATEGORIES = [
|
|
43
|
+
{
|
|
44
|
+
id: CalculatorCategoryId.AreaAndVolume,
|
|
45
|
+
name: 'Area and Volume',
|
|
46
|
+
color: '#FF3A6E',
|
|
47
|
+
glyph: 'shape_calculator',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: CalculatorCategoryId.Concrete,
|
|
51
|
+
name: 'Concrete',
|
|
52
|
+
color: '#E2AB6B',
|
|
53
|
+
glyph: 'concrete',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: CalculatorCategoryId.Electrical,
|
|
57
|
+
name: 'Electrical',
|
|
58
|
+
color: '#60D8FE',
|
|
59
|
+
glyph: 'electrical',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: CalculatorCategoryId.Stairs,
|
|
63
|
+
name: 'Stairs',
|
|
64
|
+
color: '#D09C9C',
|
|
65
|
+
glyph: 'stair_tread',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: CalculatorCategoryId.Framing,
|
|
69
|
+
name: 'Framing',
|
|
70
|
+
color: '#FABC61',
|
|
71
|
+
glyph: 'frame',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: CalculatorCategoryId.FinishCarpentry,
|
|
75
|
+
name: 'Finish Carpentry',
|
|
76
|
+
color: '#2E9E82',
|
|
77
|
+
glyph: 'finish_carpentry',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: CalculatorCategoryId.Hardscaping,
|
|
81
|
+
name: 'Hardscaping',
|
|
82
|
+
color: '#6B77E2',
|
|
83
|
+
glyph: 'hardscaping',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: CalculatorCategoryId.Roofing,
|
|
87
|
+
name: 'Roofing',
|
|
88
|
+
color: '#E48787',
|
|
89
|
+
glyph: 'roofing',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: CalculatorCategoryId.HVAC,
|
|
93
|
+
name: 'HVAC',
|
|
94
|
+
color: '#FEF260',
|
|
95
|
+
glyph: 'hvac',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: CalculatorCategoryId.Landscaping,
|
|
99
|
+
name: 'Landscaping',
|
|
100
|
+
color: '#C892FF',
|
|
101
|
+
glyph: 'landscaping',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: CalculatorCategoryId.MaterialEstimator,
|
|
105
|
+
name: 'Material Estimator',
|
|
106
|
+
color: '#FFAD00',
|
|
107
|
+
glyph: 'raw_materials',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: CalculatorCategoryId.Optimizers,
|
|
111
|
+
name: 'Optimizers',
|
|
112
|
+
color: '#CA5726',
|
|
113
|
+
glyph: 'wall',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: CalculatorCategoryId.Plumbing,
|
|
117
|
+
name: 'Plumbing',
|
|
118
|
+
color: '#008EFD',
|
|
119
|
+
glyph: 'plumbing',
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
export const CALCULATOR_CATEGORY_BY_ID = Object.fromEntries(CALCULATOR_CATEGORIES.map((category) => [category.id, category]));
|
|
123
|
+
/**
|
|
124
|
+
* Bucket key / route segment for calculators with no category, or with one
|
|
125
|
+
* that no longer exists. NOT a CalculatorCategoryId — guarded by a unit test,
|
|
126
|
+
* because a collision would silently swallow a real category.
|
|
127
|
+
*/
|
|
128
|
+
export const UNCATEGORIZED_CATEGORY_ID = 'uncategorized';
|
|
129
|
+
export const UNCATEGORIZED_CATEGORY_NAME = 'Other';
|
|
130
|
+
export const isCalculatorCategoryId = (value) => typeof value === 'string' && value in CALCULATOR_CATEGORY_BY_ID;
|
|
131
|
+
/**
|
|
132
|
+
* Resolve a stored category value. Returns undefined for missing, null, and
|
|
133
|
+
* retired ids — callers should treat all three as "Other" rather than trusting
|
|
134
|
+
* the raw field.
|
|
135
|
+
*/
|
|
136
|
+
export const findCalculatorCategory = (value) => isCalculatorCategoryId(value) ? CALCULATOR_CATEGORY_BY_ID[value] : undefined;
|
|
137
|
+
export const calculatorCategoryOf = (calculator) => findCalculatorCategory(calculator.category);
|
|
138
|
+
/** Display label for a stored value, falling back to "Other". */
|
|
139
|
+
export const calculatorCategoryName = (value) => findCalculatorCategory(value)?.name ?? UNCATEGORIZED_CATEGORY_NAME;
|
|
140
|
+
/**
|
|
141
|
+
* Group calculators by category id, collecting anything missing or
|
|
142
|
+
* unresolvable under UNCATEGORIZED_CATEGORY_ID.
|
|
143
|
+
*
|
|
144
|
+
* Keys exist only for non-empty buckets, which is what lets the mobile grid
|
|
145
|
+
* hide empty categories without a second pass.
|
|
146
|
+
*/
|
|
147
|
+
export const groupCalculatorsByCategory = (calculators) => {
|
|
148
|
+
const buckets = new Map();
|
|
149
|
+
for (const calculator of calculators) {
|
|
150
|
+
const key = isCalculatorCategoryId(calculator.category)
|
|
151
|
+
? calculator.category
|
|
152
|
+
: UNCATEGORIZED_CATEGORY_ID;
|
|
153
|
+
const bucket = buckets.get(key);
|
|
154
|
+
if (bucket)
|
|
155
|
+
bucket.push(calculator);
|
|
156
|
+
else
|
|
157
|
+
buckets.set(key, [calculator]);
|
|
158
|
+
}
|
|
159
|
+
return buckets;
|
|
160
|
+
};
|
package/dist/calculator/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// definition validation. Pure logic (mathjs + zod only — no Skia/React), safe
|
|
4
4
|
// on web, native, and Node.
|
|
5
5
|
export * from './schema.js';
|
|
6
|
+
export * from './categories.js';
|
|
6
7
|
export * from './units.js';
|
|
7
8
|
export * from './evaluate.js';
|
|
8
9
|
export * from './solve.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ColumnType, type ColumnConfig, type ConversionTableColumnData, type InstructionsColumnData, type SelectColumnData, type DecimalTolerance, type FractionalTolerance } from '../types/firestore.js';
|
|
2
2
|
import type { AngleUnit, CalculatorUnit, FieldDimension, MeasurementDimension } from './units.js';
|
|
3
|
+
import type { CalculatorCategoryId } from './categories.js';
|
|
3
4
|
/**
|
|
4
5
|
* Version of the definition format itself. Bumped only on breaking schema
|
|
5
6
|
* changes; independent of the npm package version and of each calculator's
|
|
@@ -123,6 +124,17 @@ export interface CalculatorDefinition {
|
|
|
123
124
|
name: string;
|
|
124
125
|
description: string;
|
|
125
126
|
folderId: string | null;
|
|
127
|
+
/**
|
|
128
|
+
* Hard-coded category (see categories.ts) driving the mobile category grid
|
|
129
|
+
* and the web library badge.
|
|
130
|
+
*
|
|
131
|
+
* AUTHORING METADATA ONLY — never needed to run a calculator, so it stays
|
|
132
|
+
* optional and nullable: docs authored before categories existed have no
|
|
133
|
+
* value and must keep validating. Always resolve through
|
|
134
|
+
* findCalculatorCategory() rather than comparing the raw field, so a retired
|
|
135
|
+
* id degrades to "Other" instead of vanishing.
|
|
136
|
+
*/
|
|
137
|
+
category?: CalculatorCategoryId | null;
|
|
126
138
|
/** Inputs + outputs in one list, discriminated by `role`. */
|
|
127
139
|
fields: CalculatorField[];
|
|
128
140
|
equations: CalculatorEquation[];
|
|
@@ -122,6 +122,9 @@ export const createEmptyCalculatorDefinition = (id) => ({
|
|
|
122
122
|
name: '',
|
|
123
123
|
description: '',
|
|
124
124
|
folderId: null,
|
|
125
|
+
// `null`, never undefined — this object is spread straight into setDoc and
|
|
126
|
+
// Firestore rejects undefined field values.
|
|
127
|
+
category: null,
|
|
125
128
|
fields: [],
|
|
126
129
|
equations: [],
|
|
127
130
|
diagramFileId: null,
|
|
@@ -7,6 +7,7 @@ export declare const calculatorDefinitionSchema: z.ZodObject<{
|
|
|
7
7
|
name: z.ZodString;
|
|
8
8
|
description: z.ZodString;
|
|
9
9
|
folderId: z.ZodNullable<z.ZodString>;
|
|
10
|
+
category: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
10
11
|
fields: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
11
12
|
kind: z.ZodLiteral<ColumnType.Number>;
|
|
12
13
|
defaultValue: z.ZodOptional<z.ZodNumber>;
|
|
@@ -3,6 +3,7 @@ import { create, all } from 'mathjs';
|
|
|
3
3
|
import { ColumnType, DecimalTolerance, FractionalTolerance, } from '../types/firestore.js';
|
|
4
4
|
import { CALCULATOR_SCHEMA_VERSION, equationForField, isEquationTargetKind, isNumericFieldKind, } from './schema.js';
|
|
5
5
|
import { CALCULATOR_UNIT_INFO, unitDimension } from './units.js';
|
|
6
|
+
import { isCalculatorCategoryId } from './categories.js';
|
|
6
7
|
const math = create(all);
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Definition validation: zod for structure, plus the cross-field invariants
|
|
@@ -101,6 +102,14 @@ export const calculatorDefinitionSchema = z.object({
|
|
|
101
102
|
name: z.string(),
|
|
102
103
|
description: z.string(),
|
|
103
104
|
folderId: z.string().nullable(),
|
|
105
|
+
// Deliberately z.string(), NOT z.enum(CalculatorCategoryId): a safeParse
|
|
106
|
+
// failure returns early below, so a retired or typo'd id would suppress
|
|
107
|
+
// every other invariant and mark the whole calculator broken. Unknown ids
|
|
108
|
+
// are reported further down as an advisory warning instead.
|
|
109
|
+
//
|
|
110
|
+
// The key must be listed even though it's optional — zod strips unknown
|
|
111
|
+
// keys, so omitting it here would make the check below never fire.
|
|
112
|
+
category: z.string().nullish(),
|
|
104
113
|
fields: z.array(calculatorFieldSchema),
|
|
105
114
|
equations: z.array(calculatorEquationSchema),
|
|
106
115
|
diagramFileId: z.string().nullable().optional(),
|
|
@@ -187,6 +196,12 @@ export const validateCalculatorDefinition = (input) => {
|
|
|
187
196
|
if (def.schemaVersion > CALCULATOR_SCHEMA_VERSION) {
|
|
188
197
|
push('schema-version-unsupported', 'schemaVersion', `schemaVersion ${def.schemaVersion} is newer than this client supports (${CALCULATOR_SCHEMA_VERSION})`);
|
|
189
198
|
}
|
|
199
|
+
// A category the client doesn't know is a labelling problem, not a broken
|
|
200
|
+
// calculator — an older app build reading a newly-added category must still
|
|
201
|
+
// run it. Warning severity keeps `ok` true.
|
|
202
|
+
if (def.category != null && !isCalculatorCategoryId(def.category)) {
|
|
203
|
+
push('unknown-category', 'category', `Category "${def.category}" is not a known calculator category`, 'warning');
|
|
204
|
+
}
|
|
190
205
|
// --- Unique ids ---------------------------------------------------------
|
|
191
206
|
const fieldIds = new Set();
|
|
192
207
|
def.fields.forEach((field, i) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reekon-tools/boldr-utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
|
|
5
5
|
"author": "REEKON Tools",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"build": "tsc",
|
|
26
26
|
"prepack": "yarn run build",
|
|
27
27
|
"sync:local": "./scripts/sync-local.sh",
|
|
28
|
+
"gen:category-icons": "tsc && node ./scripts/gen-category-icons.mjs",
|
|
28
29
|
"test": "vitest",
|
|
29
30
|
"coverage": "vitest run --coverage",
|
|
30
31
|
"format": "prettier --write .",
|
|
@@ -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;
|