split table into measurements and records, add units to value display

This commit is contained in:
2026-03-25 01:18:32 -07:00
parent d03590e326
commit e749d24cfe
11 changed files with 590 additions and 80 deletions

View File

@@ -4,7 +4,7 @@ import React, {
import {
ReactFlow, Background, Controls, MiniMap,
useNodesState, useEdgesState, addEdge, useReactFlow,
ReactFlowProvider, getNodesBounds, getViewportForBounds,
ReactFlowProvider, getViewportForBounds,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
@@ -18,20 +18,28 @@ import { serializeWorkflowState } from './workflowSerialization';
// ── Constants ─────────────────────────────────────────────────────────
const DATA_TYPES = new Set(['DATA_FIELD', 'IMAGE', 'LINE', 'TABLE', 'COORD', 'STATS_SOURCE']);
const DATA_TYPES = new Set([
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE', 'ANY_TABLE',
'COORD', 'STATS_SOURCE', 'VALUE_SOURCE',
]);
const SOCKET_COMPATIBILITY = {
STATS_SOURCE: new Set(['DATA_FIELD', 'IMAGE', 'LINE', 'TABLE']),
STATS_SOURCE: new Set(['DATA_FIELD', 'IMAGE', 'LINE', 'RECORD_TABLE']),
ANY_TABLE: new Set(['MEASURE_TABLE', 'RECORD_TABLE']),
VALUE_SOURCE: new Set(['FLOAT', 'MEASURE_TABLE']),
};
const TYPE_COLORS = {
DATA_FIELD: '#ff002f',
IMAGE: '#00ff08a0',
LINE: '#ffbe5c',
TABLE: '#35e2fd',
MEASURE_TABLE:'#35e2fd',
RECORD_TABLE:'#fbbf24',
ANY_TABLE: '#67e8f9',
COORD: '#e91ed1',
FLOAT: '#7dd3fc',
STATS_SOURCE:'#c084fc',
VALUE_SOURCE:'#60a5fa',
};
const NODE_TYPES = { custom: CustomNode };
@@ -56,6 +64,46 @@ function socketTypesCompatible(sourceType, targetType) {
return !!accepted?.has(sourceType);
}
function getRenderedNodeBounds(nodes) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
let found = false;
for (const node of nodes) {
const selectorId = typeof CSS !== 'undefined' && typeof CSS.escape === 'function'
? CSS.escape(String(node.id))
: String(node.id);
const el = document.querySelector(`.react-flow__node[data-id="${selectorId}"]`);
const width = el?.offsetWidth || node.measured?.width || node.width || 0;
const height = el?.offsetHeight || node.measured?.height || node.height || 0;
const x = node.positionAbsolute?.x ?? node.position?.x ?? 0;
const y = node.positionAbsolute?.y ?? node.position?.y ?? 0;
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
continue;
}
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x + width);
maxY = Math.max(maxY, y + height);
found = true;
}
if (!found) {
return null;
}
return {
x: minX,
y: minY,
width: Math.max(1, maxX - minX),
height: Math.max(1, maxY - minY),
};
}
async function waitForImageElement(img) {
if (img.complete && img.naturalWidth > 0) return;
if (typeof img.decode === 'function') {
@@ -463,7 +511,12 @@ function Flow() {
updateNodeData(msg.data.node_id, { tableRows: msg.data.rows });
break;
case 'scalar':
updateNodeData(msg.data.node_id, { scalarValue: msg.data.value });
updateNodeData(msg.data.node_id, {
scalarValue: {
value: msg.data.value,
unit: typeof msg.data.unit === 'string' ? msg.data.unit : '',
},
});
break;
case 'mesh3d':
updateNodeData(msg.data.node_id, { meshData: msg.data.mesh });
@@ -797,7 +850,10 @@ function Flow() {
const allNodes = reactFlow.getNodes();
if (allNodes.length === 0) throw new Error('No nodes to capture');
const bounds = getNodesBounds(allNodes);
const bounds = getRenderedNodeBounds(allNodes);
if (!bounds) {
throw new Error('Could not determine rendered node bounds');
}
const pad = 0.1; // 10% margin on each side
const imageWidth = Math.ceil(bounds.width * (1 + pad * 2));
const imageHeight = Math.ceil(bounds.height * (1 + pad * 2));