@reekon-tools/boldr-utils 1.17.2 → 1.17.4
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 +54 -4
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +3 -1
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +6 -4
- package/dist/annotation/canvas/elements/PlaneElement.d.ts +2 -1
- package/dist/annotation/canvas/elements/PlaneElement.js +3 -2
- package/dist/export/buildJobCsv.d.ts +1 -0
- package/dist/export/buildJobCsv.js +24 -10
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Group,
|
|
2
|
+
import { Group, Path, Rect, RoundedRect, Skia, rect, rrect, useFont, } from '@shopify/react-native-skia';
|
|
3
3
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
4
4
|
import { StyleSheet, TouchableOpacity, View, } from 'react-native';
|
|
5
5
|
import { Gesture, GestureDetector, GestureHandlerRootView, } from 'react-native-gesture-handler';
|
|
@@ -44,8 +44,14 @@ const LOUPE_RADIUS = 16; // lens corner radius
|
|
|
44
44
|
const LOUPE_BORDER_WIDTH = 2;
|
|
45
45
|
const LOUPE_BORDER_COLOR = '#1F2937';
|
|
46
46
|
const LOUPE_BG_COLOR = '#FFFFFF';
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Black crosshair marking the dragged plane handle INSIDE the lens (replacing
|
|
48
|
+
// the disc, which covered the exact pixel being placed). Sizes are screen
|
|
49
|
+
// points inside the lens. The old fixed center crosshair was removed: the
|
|
50
|
+
// lens center is the FINGER, which sits off the handle by the grab offset,
|
|
51
|
+
// so it marked the wrong point.
|
|
52
|
+
const LOUPE_CROSS_COLOR = '#000000';
|
|
53
|
+
const LOUPE_CROSS_ARM_PX = 9; // half-length of each arm
|
|
54
|
+
const LOUPE_CROSS_WIDTH_PX = 1.5;
|
|
49
55
|
// Native fingerprint: one finger drives the active tool, two fingers
|
|
50
56
|
// pan/zoom the viewport. Tap counts as a brief pointer down+up so tools
|
|
51
57
|
// like measurement-stamp (which only listen to onPointerUp) work via tap.
|
|
@@ -478,6 +484,17 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
478
484
|
'worklet';
|
|
479
485
|
return HANDLE_RING_PX / zoom.value;
|
|
480
486
|
});
|
|
487
|
+
// Loupe variants: divided by the loupe's extra magnification too, so handles
|
|
488
|
+
// stay screen-constant INSIDE the lens as well — chrome shouldn't magnify
|
|
489
|
+
// with the content, and at 2× it covers the very point being placed.
|
|
490
|
+
const loupeHandleRadius = useDerivedValue(() => {
|
|
491
|
+
'worklet';
|
|
492
|
+
return HANDLE_RADIUS_PX / zoom.value / LOUPE_MAGNIFICATION;
|
|
493
|
+
});
|
|
494
|
+
const loupeHandleRingWidth = useDerivedValue(() => {
|
|
495
|
+
'worklet';
|
|
496
|
+
return HANDLE_RING_PX / zoom.value / LOUPE_MAGNIFICATION;
|
|
497
|
+
});
|
|
481
498
|
// ---- Plane-calibration handle drag (UI thread; Tool.planeEdit) ----------
|
|
482
499
|
// The plane tool's JS preview path re-rendered the whole canvas (and
|
|
483
500
|
// recomputed the photo-spanning grid) per pointer-move, dropping JS frames
|
|
@@ -513,6 +530,29 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
513
530
|
y: planeDragStartY.value + planeDragTY.value / zoom.value,
|
|
514
531
|
};
|
|
515
532
|
});
|
|
533
|
+
// Black crosshair marking the dragged handle inside the lens, replacing its
|
|
534
|
+
// disc there (the loupe render passes planeDragHandleHidden). Built in
|
|
535
|
+
// SCREEN space and drawn as lens chrome inside the clip (see loupeWrap), so
|
|
536
|
+
// its size is fixed pixels: project the handle through the same math as
|
|
537
|
+
// loupeTransform — lensCenter + MAG · (screen(handle) − finger). That
|
|
538
|
+
// offset from center is the grab offset the drag preserves, which is why a
|
|
539
|
+
// fixed center mark sat visibly OFF the point and was removed.
|
|
540
|
+
const loupeCrossPath = useDerivedValue(() => {
|
|
541
|
+
'worklet';
|
|
542
|
+
const path = Skia.Path.Make();
|
|
543
|
+
if (planeDragMode.value !== 1)
|
|
544
|
+
return path;
|
|
545
|
+
const p = planeDragHandle.value;
|
|
546
|
+
const sx = (p.x - panX.value) * zoom.value;
|
|
547
|
+
const sy = (p.y - panY.value) * zoom.value;
|
|
548
|
+
const x = loupeCx + LOUPE_MAGNIFICATION * (sx - magTouchX.value);
|
|
549
|
+
const y = loupeCy + LOUPE_MAGNIFICATION * (sy - magTouchY.value);
|
|
550
|
+
path.moveTo(x - LOUPE_CROSS_ARM_PX, y);
|
|
551
|
+
path.lineTo(x + LOUPE_CROSS_ARM_PX, y);
|
|
552
|
+
path.moveTo(x, y - LOUPE_CROSS_ARM_PX);
|
|
553
|
+
path.lineTo(x, y + LOUPE_CROSS_ARM_PX);
|
|
554
|
+
return path;
|
|
555
|
+
}, [loupeCx, loupeCy]);
|
|
516
556
|
// WORKLET TWIN of PlaneElement's outline path (quad ring / reference line;
|
|
517
557
|
// the scale ticks are skipped live and reappear on commit). The committed
|
|
518
558
|
// plane + dragged index are captured from React state — setPlaneDrag
|
|
@@ -1913,7 +1953,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1913
1953
|
// point under the finger. Drawn in the overlay canvas's screen space (the
|
|
1914
1954
|
// magnification lives in loupeTransform, applied to `content`).
|
|
1915
1955
|
const loupeClip = rrect(rect(loupeX, loupeY, loupeSize, loupeSize), LOUPE_RADIUS, LOUPE_RADIUS);
|
|
1916
|
-
const loupeWrap = (content) => (_jsxs(_Fragment, { children: [_jsxs(Group, { clip: loupeClip, children: [_jsx(Rect, { x: loupeX, y: loupeY, width: loupeSize, height: loupeSize, color: LOUPE_BG_COLOR }), content
|
|
1956
|
+
const loupeWrap = (content) => (_jsxs(_Fragment, { children: [_jsxs(Group, { clip: loupeClip, children: [_jsx(Rect, { x: loupeX, y: loupeY, width: loupeSize, height: loupeSize, color: LOUPE_BG_COLOR }), content, _jsx(Path, { path: loupeCrossPath, color: LOUPE_CROSS_COLOR, style: "stroke", strokeWidth: LOUPE_CROSS_WIDTH_PX })] }), _jsx(RoundedRect, { x: loupeX, y: loupeY, width: loupeSize, height: loupeSize, r: LOUPE_RADIUS, color: LOUPE_BORDER_COLOR, style: "stroke", strokeWidth: LOUPE_BORDER_WIDTH })] }));
|
|
1917
1957
|
return (
|
|
1918
1958
|
// overflow hidden mirrors the web container: the Skia surface clips its
|
|
1919
1959
|
// drawing to the canvas box by construction, but the stamp overlay is
|
|
@@ -1935,6 +1975,16 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1935
1975
|
...skiaProps,
|
|
1936
1976
|
worldTransform: loupeTransform,
|
|
1937
1977
|
wrapContent: loupeWrap,
|
|
1978
|
+
// Chrome stays screen-constant inside the lens: handles at their
|
|
1979
|
+
// normal pixel size, the plane outline at its normal apparent
|
|
1980
|
+
// thickness — only the CONTENT magnifies. At 2× the fat
|
|
1981
|
+
// calibration chrome covered the exact point being placed.
|
|
1982
|
+
handleRadius: loupeHandleRadius,
|
|
1983
|
+
handleRingWidth: loupeHandleRingWidth,
|
|
1984
|
+
planeOutlineScale: 1 / LOUPE_MAGNIFICATION,
|
|
1985
|
+
// The dragged handle renders as the black crosshair drawn in the
|
|
1986
|
+
// lens chrome (loupeCrossPath) instead of its disc.
|
|
1987
|
+
planeDragHandleHidden: true,
|
|
1938
1988
|
}) }))] }));
|
|
1939
1989
|
};
|
|
1940
1990
|
const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging, sliding, endpointDragging, rectResizing, zoomSnapshot, zoom, panX, panY, dragX, dragY, slideCtx, epCtx, rectCtx, planeEdgeDragging, planeTileCtx, planeDragTX, planeDragTY, renderMeasurementStamp, tileScaleFactor, headerTileScaleFactor, fileTileScaleFactor, tileViewportScale, onStampPress, onStampLongPress, onRemove, }) => {
|
|
@@ -98,9 +98,11 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
98
98
|
value: SkPath;
|
|
99
99
|
};
|
|
100
100
|
planeDragHandle?: AnimatedPoint;
|
|
101
|
+
planeOutlineScale?: number;
|
|
102
|
+
planeDragHandleHidden?: boolean;
|
|
101
103
|
customPreview?: ReactNode;
|
|
102
104
|
wrapContent?: (content: ReactNode) => ReactNode;
|
|
103
105
|
canvasRef?: RefObject<CanvasRef | null>;
|
|
104
106
|
}
|
|
105
|
-
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, customPreview, wrapContent, canvasRef, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
107
|
+
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale, planeDragHandleHidden, customPreview, wrapContent, canvasRef, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
106
108
|
export {};
|
|
@@ -80,7 +80,7 @@ const SelectionBox = ({ bounds, isDragging, transform, }) => (_jsx(DraggableElem
|
|
|
80
80
|
// since the function-call pattern works identically on native we use it
|
|
81
81
|
// in both Inners for consistency. Don't add hooks here; this is a plain
|
|
82
82
|
// JSX-returning helper, not a component.
|
|
83
|
-
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, customPreview, wrapContent, canvasRef, }) => {
|
|
83
|
+
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale = 1, planeDragHandleHidden, customPreview, wrapContent, canvasRef, }) => {
|
|
84
84
|
// Doc rect a SCALE-mode plane grid covers: the first background layer (the
|
|
85
85
|
// photo being calibrated), else the document rect. Pinned to layer 0 like
|
|
86
86
|
// the tile-sizing reference — adding a second image must not re-span the
|
|
@@ -99,10 +99,12 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
|
|
|
99
99
|
const statics = plane.mode === 'perspective'
|
|
100
100
|
? plane.corners
|
|
101
101
|
: [plane.refLine.a, plane.refLine.b];
|
|
102
|
-
return (_jsxs(Group, { children: [planeDragGridPath && (_jsx(Path, { path: planeDragGridPath, color: PLANE_GRID_COLOR, style: "stroke", strokeWidth: PLANE_GRID_WIDTH, opacity: PLANE_GRID_OPACITY })), _jsx(Path, { path: planeDragPath, color: PLANE_CHROME_COLOR, style: "stroke", strokeWidth: PLANE_OUTLINE_WIDTH, strokeJoin: "round" }), handleRadius != null &&
|
|
103
|
-
statics.map((p, i) => i === planeDragIndex ? null : (_jsxs(Group, { children: [_jsx(Circle, { c: p, r: handleRadius, color: PLANE_CHROME_COLOR }), _jsx(Circle, { c: p, r: handleRadius, color: "#FFFFFF", style: "stroke", strokeWidth: handleRingWidth })] }, i))), handleRadius != null &&
|
|
102
|
+
return (_jsxs(Group, { children: [planeDragGridPath && (_jsx(Path, { path: planeDragGridPath, color: PLANE_GRID_COLOR, style: "stroke", strokeWidth: PLANE_GRID_WIDTH, opacity: PLANE_GRID_OPACITY })), _jsx(Path, { path: planeDragPath, color: PLANE_CHROME_COLOR, style: "stroke", strokeWidth: PLANE_OUTLINE_WIDTH * planeOutlineScale, strokeJoin: "round" }), handleRadius != null &&
|
|
103
|
+
statics.map((p, i) => i === planeDragIndex ? null : (_jsxs(Group, { children: [_jsx(Circle, { c: p, r: handleRadius, color: PLANE_CHROME_COLOR }), _jsx(Circle, { c: p, r: handleRadius, color: "#FFFFFF", style: "stroke", strokeWidth: handleRingWidth })] }, i))), handleRadius != null &&
|
|
104
|
+
planeDragHandle &&
|
|
105
|
+
!planeDragHandleHidden && (_jsxs(_Fragment, { children: [_jsx(Circle, { c: planeDragHandle, r: handleRadius, color: PLANE_CHROME_COLOR }), _jsx(Circle, { c: planeDragHandle, r: handleRadius, color: "#FFFFFF", style: "stroke", strokeWidth: handleRingWidth })] }))] }, plane.id));
|
|
104
106
|
}
|
|
105
|
-
return (_jsx(PlaneElement, { plane: plane, scaleBounds: planeScaleBounds, showHandles: showPlaneHandles === true, handleRadius: showPlaneHandles ? handleRadius : undefined, handleRingWidth: showPlaneHandles ? handleRingWidth : undefined }, plane.id));
|
|
107
|
+
return (_jsx(PlaneElement, { plane: plane, scaleBounds: planeScaleBounds, showHandles: showPlaneHandles === true, handleRadius: showPlaneHandles ? handleRadius : undefined, handleRingWidth: showPlaneHandles ? handleRingWidth : undefined, outlineScale: planeOutlineScale }, plane.id));
|
|
106
108
|
}), effectiveCanvas.strokes.map((stroke) => (_jsx(DraggableElement, { isDragging: stroke.id === draggingId, transform: dragTransform, children: _jsx(StrokeElement, { stroke: stroke }) }, stroke.id))), effectiveCanvas.shapes.map((shape) => {
|
|
107
109
|
// Line/arrow shapes support endpoint editing. When selected they show
|
|
108
110
|
// grab handles at both ends; during an endpoint drag the line renders
|
|
@@ -18,6 +18,7 @@ export interface PlaneElementProps {
|
|
|
18
18
|
showHandles: boolean;
|
|
19
19
|
handleRadius?: AnimatedNumber;
|
|
20
20
|
handleRingWidth?: AnimatedNumber;
|
|
21
|
+
outlineScale?: number;
|
|
21
22
|
}
|
|
22
|
-
export declare const PlaneElement: import("react").MemoExoticComponent<({ plane, scaleBounds, showHandles, handleRadius, handleRingWidth, }: PlaneElementProps) => import("react/jsx-runtime").JSX.Element>;
|
|
23
|
+
export declare const PlaneElement: import("react").MemoExoticComponent<({ plane, scaleBounds, showHandles, handleRadius, handleRingWidth, outlineScale, }: PlaneElementProps) => import("react/jsx-runtime").JSX.Element>;
|
|
23
24
|
export {};
|
|
@@ -34,7 +34,7 @@ const segmentsPath = (segments) => {
|
|
|
34
34
|
// The grid respects the plane's stored gridSpacingUm (hosts stamp it at
|
|
35
35
|
// dimension-entry time from the unit the user typed); absent, it falls back
|
|
36
36
|
// to an auto "nice metric" spacing for the calibrated extent.
|
|
37
|
-
export const PlaneElement = memo(({ plane, scaleBounds, showHandles, handleRadius, handleRingWidth, }) => {
|
|
37
|
+
export const PlaneElement = memo(({ plane, scaleBounds, showHandles, handleRadius, handleRingWidth, outlineScale = 1, }) => {
|
|
38
38
|
const gridVisible = plane.gridVisible !== false;
|
|
39
39
|
const gridPath = useMemo(() => {
|
|
40
40
|
if (!gridVisible)
|
|
@@ -99,7 +99,7 @@ export const PlaneElement = memo(({ plane, scaleBounds, showHandles, handleRadiu
|
|
|
99
99
|
? plane.corners
|
|
100
100
|
: [plane.refLine.a, plane.refLine.b]
|
|
101
101
|
: [];
|
|
102
|
-
return (_jsxs(Group, { children: [gridPath && (_jsx(Path, { path: gridPath, color: PLANE_GRID_COLOR, style: "stroke", strokeWidth: PLANE_GRID_WIDTH, opacity: PLANE_GRID_OPACITY })), _jsx(Path, { path: outlinePath, color: PLANE_COLOR, style: "stroke", strokeWidth: PLANE_OUTLINE_WIDTH, strokeJoin: "round" }), handleRadius != null &&
|
|
102
|
+
return (_jsxs(Group, { children: [gridPath && (_jsx(Path, { path: gridPath, color: PLANE_GRID_COLOR, style: "stroke", strokeWidth: PLANE_GRID_WIDTH, opacity: PLANE_GRID_OPACITY })), _jsx(Path, { path: outlinePath, color: PLANE_COLOR, style: "stroke", strokeWidth: PLANE_OUTLINE_WIDTH * outlineScale, strokeJoin: "round" }), handleRadius != null &&
|
|
103
103
|
handlePoints.map((p, i) => (_jsxs(Group, { children: [_jsx(Circle, { c: p, r: handleRadius, color: PLANE_COLOR }), _jsx(Circle, { c: p, r: handleRadius, color: "#FFFFFF", style: "stroke", strokeWidth: handleRingWidth })] }, i)))] }));
|
|
104
104
|
},
|
|
105
105
|
// Value-compare the bounds rect: AnnotationCanvasSkia derives it fresh each
|
|
@@ -111,6 +111,7 @@ export const PlaneElement = memo(({ plane, scaleBounds, showHandles, handleRadiu
|
|
|
111
111
|
prev.showHandles === next.showHandles &&
|
|
112
112
|
prev.handleRadius === next.handleRadius &&
|
|
113
113
|
prev.handleRingWidth === next.handleRingWidth &&
|
|
114
|
+
prev.outlineScale === next.outlineScale &&
|
|
114
115
|
prev.scaleBounds.x === next.scaleBounds.x &&
|
|
115
116
|
prev.scaleBounds.y === next.scaleBounds.y &&
|
|
116
117
|
prev.scaleBounds.width === next.scaleBounds.width &&
|
|
@@ -15,6 +15,7 @@ export type BuildJobCsvInput = {
|
|
|
15
15
|
measurementsByGroup: Map<string, Measurement[]>;
|
|
16
16
|
objectsByGroup: Map<string, ExportObjectRow[]>;
|
|
17
17
|
formatMeasurementValue: (value: number, device?: string) => string;
|
|
18
|
+
applyGroupFormula?: (group: Group, measurement: Measurement) => number | null;
|
|
18
19
|
unitPrefs: {
|
|
19
20
|
defaultUnit?: Units;
|
|
20
21
|
fractionalTolerance?: FractionalTolerance;
|
|
@@ -39,7 +39,7 @@ const userCell = (user) => (user ? [user.firstName, user.lastName].filter(Boolea
|
|
|
39
39
|
// trailing "Objects" column (list groups) or extend the form row (form
|
|
40
40
|
// groups), one skipped column after the existing cells.
|
|
41
41
|
export function buildJobCsv(input) {
|
|
42
|
-
const { scope, activeGroupId, config, groups, sections, formulas, measurementsByGroup, objectsByGroup, formatMeasurementValue, unitPrefs, } = input;
|
|
42
|
+
const { scope, activeGroupId, config, groups, sections, formulas, measurementsByGroup, objectsByGroup, formatMeasurementValue, applyGroupFormula, unitPrefs, } = input;
|
|
43
43
|
const csvRows = [];
|
|
44
44
|
let listGroups;
|
|
45
45
|
let formGroups;
|
|
@@ -82,9 +82,13 @@ export function buildJobCsv(input) {
|
|
|
82
82
|
headerCells.push(escapeCSVField(''), escapeCSVField('Objects'));
|
|
83
83
|
}
|
|
84
84
|
csvRows.push(headerCells.join(','));
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
85
|
+
// File-scratch measurements (non-null fileId: created inside a CAD/PDF
|
|
86
|
+
// canvas, never saved into the group) live in the same subcollection
|
|
87
|
+
// but are internal — the group cards hide them, so exports must too.
|
|
88
|
+
// Then export rows in the card/table display order (incomplete first,
|
|
89
|
+
// then capture time), regardless of the order the caller fetched them
|
|
90
|
+
// in.
|
|
91
|
+
const measurements = sortMeasurementsByCompleted((measurementsByGroup.get(group.id) ?? []).filter((m) => m.fileId == null)).filter(measurementPassesFilter);
|
|
88
92
|
// Objects fill a parallel column, one per row from the top; extra rows
|
|
89
93
|
// are emitted when a group has more objects than measurements.
|
|
90
94
|
const rowCount = Math.max(measurements.length, objects.length);
|
|
@@ -94,12 +98,22 @@ export function buildJobCsv(input) {
|
|
|
94
98
|
if (includeField('measurement')) {
|
|
95
99
|
// Angles and unitless numbers store their value as entered, not as
|
|
96
100
|
// micrometers — running them through the length formatter would
|
|
97
|
-
// render 45° as ~0. Emit them as-is with their own suffix.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
// render 45° as ~0. Emit them as-is with their own suffix. Group
|
|
102
|
+
// formulas apply to length measurements only, same as the tiles.
|
|
103
|
+
let cell = '';
|
|
104
|
+
if (measurement) {
|
|
105
|
+
if (isScalarMeasurement(measurement.type)) {
|
|
106
|
+
cell = `${measurement.value}${scalarValueSuffix(measurement.type)}`;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const formulaValue = applyGroupFormula?.(group, measurement) ?? null;
|
|
110
|
+
cell =
|
|
111
|
+
formulaValue !== null
|
|
112
|
+
? formatMeasurementValue(formulaValue)
|
|
113
|
+
: formatMeasurementValue(measurement.value, measurement.device);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
row.push(escapeCSVField(cell));
|
|
103
117
|
}
|
|
104
118
|
if (includeField('label')) {
|
|
105
119
|
row.push(escapeCSVField(measurement?.label || ''));
|