rename grains to particle, add colormap adjust, table math

This commit is contained in:
2026-03-24 23:48:03 -07:00
parent edfdead4c1
commit 44de72d31b
12 changed files with 512 additions and 109 deletions

View File

@@ -178,6 +178,7 @@ function serializeGraph(nodes, edges, { excludeManualTrigger = false } = {}) {
for (const [name, spec] of Object.entries(required)) {
const [type] = Array.isArray(spec) ? spec : [spec];
if (DATA_TYPES.has(type)) continue; // socket, handled via edges
if (type === 'BUTTON') continue; // UI-only widget, not a backend input
if (widgetValues[name] !== undefined) {
inputs[name] = widgetValues[name];
}
@@ -604,6 +605,7 @@ function Flow() {
for (const [name, spec] of Object.entries(required)) {
const [type, opts] = Array.isArray(spec) ? spec : [spec, {}];
if (DATA_TYPES.has(type)) continue;
if (type === 'BUTTON') continue;
if (Array.isArray(type)) {
widgetValues[name] = type[0]; // combo default = first option
} else {
@@ -1026,7 +1028,7 @@ function Flow() {
const cat = n.data?.definition?.category;
const colors = {
io: '#37474f', filters: '#1a237e', level: '#1b5e20',
analysis: '#4a148c', grains: '#bf360c', display: '#212121',
analysis: '#4a148c', particles: '#bf360c', display: '#212121',
};
return colors[cat] || '#333';
}}

View File

@@ -26,7 +26,7 @@ const CAT_COLORS = {
modify: '#0f766e',
level: '#1b5e20',
analysis: '#4a148c',
grains: '#bf360c',
particles:'#bf360c',
display: '#212121',
};
@@ -171,6 +171,69 @@ function CollapsibleSection({ title, defaultOpen, children }) {
);
}
function getTableColumns(rows) {
const columns = [];
for (const row of rows) {
if (!row || typeof row !== 'object') continue;
for (const key of Object.keys(row)) {
if (!columns.includes(key)) columns.push(key);
}
}
return columns;
}
function formatTableCell(value) {
if (value == null) return '';
if (typeof value === 'number') {
if (!Number.isFinite(value)) return String(value);
const abs = Math.abs(value);
if (Number.isInteger(value) && abs < 1e6) return String(value);
if ((abs > 0 && abs < 1e-3) || abs >= 1e4) return value.toExponential(3);
return value.toFixed(4).replace(/\.?0+$/, '');
}
if (Array.isArray(value)) return value.join(', ');
return String(value);
}
function NodeTable({ rows }) {
const columns = getTableColumns(rows);
if (columns.length === 0) return null;
return (
<div className="node-table-wrap">
<div className="node-table-scroll">
<table className="node-table-grid">
<thead>
<tr>
{columns.map((column) => (
<th key={column} scope="col">{column}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, rowIndex) => (
<tr key={row.id ?? row.quantity ?? rowIndex}>
{columns.map((column) => {
const value = row?.[column];
return (
<td
key={`${rowIndex}-${column}`}
className={typeof value === 'number' ? 'node-table-num' : ''}
title={formatTableCell(value)}
>
{formatTableCell(value)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
// ── CustomNode component ──────────────────────────────────────────────
function CustomNode({ id, data }) {
@@ -411,21 +474,7 @@ function CustomNode({ id, data }) {
{/* Collapsible table data */}
{data.tableRows && data.tableRows.length > 0 && (
<CollapsibleSection title="Table" defaultOpen={true}>
<div className="node-table">
{data.tableRows.map((row, i) => {
let line;
if (row.quantity !== undefined) {
const val = typeof row.value === 'number' ? row.value.toExponential(3) : row.value;
line = `${row.quantity}: ${val} ${row.unit || ''}`;
} else {
line = Object.entries(row)
.slice(0, 3)
.map(([k, v]) => `${k}: ${typeof v === 'number' ? v.toExponential(2) : v}`)
.join(' ');
}
return <div key={i} className="table-line">{line}</div>;
})}
</div>
<NodeTable rows={data.tableRows} />
</CollapsibleSection>
)}
</div>
@@ -480,6 +529,26 @@ function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFile
);
}
if (type === 'BUTTON') {
const updates = opts?.set_widgets && typeof opts.set_widgets === 'object'
? Object.entries(opts.set_widgets)
: [];
return (
<button
className="nodrag widget-button"
type="button"
onClick={() => {
for (const [targetName, targetValue] of updates) {
onChange(nodeId, targetName, targetValue);
}
}}
>
{opts?.label || name}
</button>
);
}
if (type === 'FLOAT') {
if (opts?.slider) {
const rawMin = opts?.min_widget ? widgetValues?.[opts.min_widget] : opts?.min;

View File

@@ -227,6 +227,23 @@ html, body, #root {
accent-color: #3a7abf;
}
.widget-button {
flex: 1;
min-width: 0;
background: #0f3460;
color: #e0e0e0;
border: 1px solid #334155;
border-radius: 3px;
padding: 4px 8px;
font-size: 11px;
cursor: pointer;
}
.widget-button:hover {
background: #1a4a8a;
border-color: #3a7abf;
}
.slider-control {
display: flex;
align-items: center;
@@ -496,18 +513,56 @@ html, body, #root {
}
/* ── Node table ────────────────────────────────────────────────────── */
.node-table {
padding: 4px 10px;
.node-table-wrap {
padding: 4px 10px 8px;
}
.node-table-scroll {
max-height: 220px;
overflow: auto;
border: 1px solid #334155;
border-radius: 6px;
background: #0f172a;
}
.node-table-grid {
width: 100%;
border-collapse: collapse;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 10px;
color: #cbd5e1;
}
.table-line {
.node-table-grid th,
.node-table-grid td {
padding: 6px 8px;
border-bottom: 1px solid rgba(51, 65, 85, 0.75);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
text-align: left;
vertical-align: top;
}
.node-table-grid thead th {
position: sticky;
top: 0;
z-index: 1;
background: #16213e;
color: #94a3b8;
font-size: 9px;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.node-table-grid tbody tr:nth-child(even) {
background: rgba(30, 41, 59, 0.38);
}
.node-table-grid tbody tr:last-child td {
border-bottom: none;
}
.node-table-num {
text-align: right !important;
}
/* ── Node resize handles ───────────────────────────────────────────── */