@bian-womp/spark-workbench 0.2.18 → 0.2.19

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/lib/cjs/index.cjs CHANGED
@@ -1183,6 +1183,83 @@ function summarizeDeep(value) {
1183
1183
  const NODE_HEADER_HEIGHT_PX = 24;
1184
1184
  const NODE_ROW_HEIGHT_PX = 22;
1185
1185
 
1186
+ function computeEffectiveHandles(node, registry) {
1187
+ const desc = registry.nodes.get(node.typeId);
1188
+ const resolved = node.resolvedHandles || {};
1189
+ const inputs = { ...desc?.inputs, ...resolved.inputs };
1190
+ const outputs = { ...desc?.outputs, ...resolved.outputs };
1191
+ const inputDefaults = { ...desc?.inputDefaults, ...resolved.inputDefaults };
1192
+ return { inputs, outputs, inputDefaults };
1193
+ }
1194
+ function countVisibleHandles(handles) {
1195
+ const inputIds = Object.keys(handles.inputs).filter((k) => !sparkGraph.isInputPrivate(handles.inputs, k));
1196
+ const outputIds = Object.keys(handles.outputs);
1197
+ return { inputsCount: inputIds.length, outputsCount: outputIds.length };
1198
+ }
1199
+ function estimateNodeSize(args) {
1200
+ const { node, registry, showValues, overrides } = args;
1201
+ const { inputs, outputs } = computeEffectiveHandles(node, registry);
1202
+ // Count only non-private inputs for rows on left
1203
+ const { inputsCount, outputsCount } = countVisibleHandles({
1204
+ inputs,
1205
+ outputs,
1206
+ });
1207
+ const rows = Math.max(inputsCount, outputsCount);
1208
+ const baseWidth = showValues ? 320 : 240;
1209
+ const width = overrides?.width ?? baseWidth;
1210
+ const height = overrides?.height ?? NODE_HEADER_HEIGHT_PX + rows * NODE_ROW_HEIGHT_PX;
1211
+ return { width, height, inputsCount, outputsCount, rowCount: rows };
1212
+ }
1213
+ function layoutNode(args) {
1214
+ const { node, registry, showValues, overrides } = args;
1215
+ const { inputs, outputs } = computeEffectiveHandles(node, registry);
1216
+ const inputOrder = Object.keys(inputs).filter((k) => !sparkGraph.isInputPrivate(inputs, k));
1217
+ const outputOrder = Object.keys(outputs);
1218
+ const { width, height } = estimateNodeSize({
1219
+ node,
1220
+ registry,
1221
+ showValues,
1222
+ overrides,
1223
+ });
1224
+ const HEADER = NODE_HEADER_HEIGHT_PX;
1225
+ const ROW = NODE_ROW_HEIGHT_PX;
1226
+ const handles = [
1227
+ ...inputOrder.map((id, i) => ({
1228
+ id,
1229
+ type: "target",
1230
+ position: react.Position.Left,
1231
+ x: 0,
1232
+ y: HEADER + i * ROW,
1233
+ width: 1,
1234
+ height: ROW + 2,
1235
+ })),
1236
+ ...outputOrder.map((id, i) => ({
1237
+ id,
1238
+ type: "source",
1239
+ position: react.Position.Right,
1240
+ x: width - 1,
1241
+ y: HEADER + i * ROW,
1242
+ width: 1,
1243
+ height: ROW + 2,
1244
+ })),
1245
+ ];
1246
+ const handleLayout = [
1247
+ ...inputOrder.map((id, i) => ({
1248
+ id,
1249
+ type: "target",
1250
+ position: react.Position.Left,
1251
+ y: HEADER + i * ROW + ROW / 2,
1252
+ })),
1253
+ ...outputOrder.map((id, i) => ({
1254
+ id,
1255
+ type: "source",
1256
+ position: react.Position.Right,
1257
+ y: HEADER + i * ROW + ROW / 2,
1258
+ })),
1259
+ ];
1260
+ return { width, height, inputOrder, outputOrder, handles, handleLayout };
1261
+ }
1262
+
1186
1263
  function toReactFlow(def, positions, registry, opts) {
1187
1264
  const EDGE_STYLE_ERROR = { stroke: "#ef4444", strokeWidth: 2 };
1188
1265
  const EDGE_STYLE_RUNNING = { stroke: "#3b82f6" };
@@ -1197,54 +1274,31 @@ function toReactFlow(def, positions, registry, opts) {
1197
1274
  connectedInputs[nid].add(hid);
1198
1275
  }
1199
1276
  const nodes = def.nodes.map((n) => {
1200
- const desc = registry.nodes.get(n.typeId);
1201
- // Prefer per-node resolved handles when present
1202
- const resolvedInputs = n.resolvedHandles?.inputs;
1203
- const resolvedOutputs = n.resolvedHandles?.outputs;
1204
- const inputSource = resolvedInputs ?? desc?.inputs ?? {};
1205
- const outputSource = resolvedOutputs ?? desc?.outputs ?? {};
1206
- const inputHandles = Object.entries(inputSource)
1207
- .filter(([id]) => !sparkGraph.isInputPrivate(inputSource, id))
1208
- .map(([id]) => ({ id, typeId: sparkGraph.getInputTypeId(inputSource, id) }));
1209
- const outputHandles = Object.entries(outputSource).map(([id, typeId]) => ({
1277
+ const { inputs: inputSource, outputs: outputSource } = computeEffectiveHandles(n, registry);
1278
+ const overrideSize = opts.getDefaultNodeSize?.(n.typeId);
1279
+ const geom = layoutNode({
1280
+ node: n,
1281
+ registry,
1282
+ showValues: opts.showValues,
1283
+ overrides: overrideSize,
1284
+ });
1285
+ const inputHandles = geom.inputOrder.map((id) => ({
1286
+ id,
1287
+ typeId: sparkGraph.getInputTypeId(inputSource, id),
1288
+ }));
1289
+ const outputHandles = geom.outputOrder.map((id) => ({
1210
1290
  id,
1211
- typeId: formatDeclaredTypeSignature(typeId),
1291
+ typeId: formatDeclaredTypeSignature(outputSource[id]),
1212
1292
  }));
1213
1293
  nodeHandleMap[n.nodeId] = {
1214
1294
  inputs: new Set(inputHandles.map((h) => h.id)),
1215
1295
  outputs: new Set(outputHandles.map((h) => h.id)),
1216
1296
  };
1217
- // Match DefaultNode sizing heuristics to avoid hidden nodes during re-measure
1218
- const HEADER_SIZE = NODE_HEADER_HEIGHT_PX;
1219
- const ROW_SIZE = NODE_ROW_HEIGHT_PX;
1220
- const maxRows = Math.max(inputHandles.length, outputHandles.length);
1221
- // Allow external override to dictate initial size
1222
- const overrideSize = opts.getDefaultNodeSize?.(n.typeId);
1223
- const initialWidth = overrideSize?.width ?? (opts.showValues ? 320 : 240);
1224
- const initialHeight = overrideSize?.height ?? HEADER_SIZE + maxRows * ROW_SIZE;
1297
+ // Shared sizing
1298
+ const initialWidth = geom.width;
1299
+ const initialHeight = geom.height;
1225
1300
  // Precompute handle bounds so edges can render immediately without waiting for measurement
1226
- const handles = [
1227
- // Inputs on the left as targets
1228
- ...inputHandles.map((h, i) => ({
1229
- id: h.id,
1230
- type: "target",
1231
- position: react.Position.Left,
1232
- x: 0,
1233
- y: HEADER_SIZE + i * ROW_SIZE,
1234
- width: 1,
1235
- height: ROW_SIZE + 2,
1236
- })),
1237
- // Outputs on the right as sources
1238
- ...outputHandles.map((h, i) => ({
1239
- id: h.id,
1240
- type: "source",
1241
- position: react.Position.Right,
1242
- x: initialWidth - 1,
1243
- y: HEADER_SIZE + i * ROW_SIZE,
1244
- width: 1,
1245
- height: ROW_SIZE + 2,
1246
- })),
1247
- ];
1301
+ const handles = geom.handles;
1248
1302
  return {
1249
1303
  id: n.nodeId,
1250
1304
  data: {
@@ -1256,20 +1310,7 @@ function toReactFlow(def, positions, registry, opts) {
1256
1310
  h.id,
1257
1311
  !!connectedInputs[n.nodeId]?.has(h.id),
1258
1312
  ])),
1259
- handleLayout: [
1260
- ...inputHandles.map((h, i) => ({
1261
- id: h.id,
1262
- type: "target",
1263
- position: react.Position.Left,
1264
- y: HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2,
1265
- })),
1266
- ...outputHandles.map((h, i) => ({
1267
- id: h.id,
1268
- type: "source",
1269
- position: react.Position.Right,
1270
- y: HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2,
1271
- })),
1272
- ],
1313
+ handleLayout: geom.handleLayout,
1273
1314
  showValues: opts.showValues,
1274
1315
  renderWidth: initialWidth,
1275
1316
  renderHeight: initialHeight,
@@ -1468,6 +1509,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1468
1509
  // Auto layout (simple layered layout)
1469
1510
  const runAutoLayout = React.useCallback(() => {
1470
1511
  const cur = wb.export();
1512
+ // Build DAG layers by indegree
1471
1513
  const indegree = {};
1472
1514
  const adj = {};
1473
1515
  for (const n of cur.nodes) {
@@ -1494,14 +1536,36 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1494
1536
  layers.push(layer);
1495
1537
  q.splice(0, q.length, ...next);
1496
1538
  }
1497
- const X = 720;
1498
- const Y = 600;
1539
+ // Size-aware placement: columns by layer, stacking nodes vertically in each column
1540
+ // Use the same sizing heuristic as mapping via estimateNodeSize
1541
+ const H_GAP = 160;
1542
+ const V_GAP = 24;
1499
1543
  const pos = {};
1500
- layers.forEach((layer, layerIndex) => {
1501
- layer.forEach((id, itemIndex) => {
1502
- pos[id] = { x: layerIndex * X, y: itemIndex * Y };
1503
- });
1504
- });
1544
+ let curX = 0;
1545
+ for (const layer of layers) {
1546
+ // Compute max width in this layer and individual heights
1547
+ let maxWidth = 0;
1548
+ const heights = {};
1549
+ for (const id of layer) {
1550
+ const node = cur.nodes.find((n) => n.nodeId === id);
1551
+ if (!node)
1552
+ continue;
1553
+ // Prefer showValues sizing similar to node rendering
1554
+ // Lazy import to avoid circular deps at module top
1555
+ const size = estimateNodeSize({ node, registry, showValues: true });
1556
+ heights[id] = size.height;
1557
+ if (size.width > maxWidth)
1558
+ maxWidth = size.width;
1559
+ }
1560
+ // Place nodes in this column
1561
+ let curY = 0;
1562
+ for (const id of layer) {
1563
+ const h = heights[id] ?? 0;
1564
+ pos[id] = { x: curX, y: curY };
1565
+ curY += h + V_GAP;
1566
+ }
1567
+ curX += maxWidth + H_GAP;
1568
+ }
1505
1569
  wb.setPositions(pos);
1506
1570
  }, [wb]);
1507
1571
  const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
@@ -1597,15 +1661,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1597
1661
  return add("runner", "error")(e);
1598
1662
  });
1599
1663
  const off3 = runner.on("invalidate", (e) => {
1600
- if (e?.reason === "graph-updated") {
1601
- setNodeStatus((s) => {
1602
- const next = {};
1603
- for (const n of wb.export().nodes) {
1604
- next[n.nodeId] = { ...s[n.nodeId], invalidated: true };
1605
- }
1606
- return next;
1607
- });
1608
- }
1609
1664
  // After build/update, pull resolved handles and merge in-place (no graphChanged)
1610
1665
  if (e?.reason === "graph-updated" || e?.reason === "graph-built") {
1611
1666
  refreshResolvedHandles();