@bian-womp/spark-workbench 0.2.63 → 0.2.65

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
@@ -367,6 +367,142 @@ class InMemoryWorkbench extends AbstractWorkbench {
367
367
  for (const h of Array.from(set))
368
368
  h(payload);
369
369
  }
370
+ /**
371
+ * Copy selected nodes and their internal edges.
372
+ * Returns data in a format suitable for pasting.
373
+ * Positions are normalized relative to the selection bounds center.
374
+ * Uses the same logic as duplicate: copies inputs without bindings and supports copyOutputsFrom.
375
+ */
376
+ copySelection(runner, getNodeSize) {
377
+ const selection = this.getSelection();
378
+ if (selection.nodes.length === 0)
379
+ return null;
380
+ const def = this.export();
381
+ const positions = this.getPositions();
382
+ const selectedNodeSet = new Set(selection.nodes);
383
+ // Collect nodes to copy
384
+ const nodesToCopy = def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
385
+ if (nodesToCopy.length === 0)
386
+ return null;
387
+ // Calculate bounds
388
+ let minX = Infinity;
389
+ let minY = Infinity;
390
+ let maxX = -Infinity;
391
+ let maxY = -Infinity;
392
+ nodesToCopy.forEach((node) => {
393
+ const pos = positions[node.nodeId] || { x: 0, y: 0 };
394
+ const size = getNodeSize?.(node.nodeId, node.typeId) || {
395
+ width: 200,
396
+ height: 100,
397
+ };
398
+ minX = Math.min(minX, pos.x);
399
+ minY = Math.min(minY, pos.y);
400
+ maxX = Math.max(maxX, pos.x + size.width);
401
+ maxY = Math.max(maxY, pos.y + size.height);
402
+ });
403
+ const bounds = { minX, minY, maxX, maxY };
404
+ const centerX = (bounds.minX + bounds.maxX) / 2;
405
+ const centerY = (bounds.minY + bounds.maxY) / 2;
406
+ // Get inputs for each node
407
+ // Include values from inbound edges if those edges are selected
408
+ const allInputs = runner.getInputs(def);
409
+ const selectedEdgeSet = new Set(selection.edges);
410
+ const copiedNodes = nodesToCopy.map((node) => {
411
+ const pos = positions[node.nodeId] || { x: 0, y: 0 };
412
+ // Get all inbound edges for this node
413
+ const inboundEdges = def.edges.filter((e) => e.target.nodeId === node.nodeId);
414
+ // Build set of handles that have inbound edges
415
+ // But only exclude handles whose edges are NOT selected
416
+ const inboundHandlesToExclude = new Set(inboundEdges
417
+ .filter((e) => !selectedEdgeSet.has(e.id)) // Only exclude if edge is not selected
418
+ .map((e) => e.target.handle));
419
+ const allNodeInputs = allInputs[node.nodeId] || {};
420
+ // Include inputs that either:
421
+ // 1. Don't have inbound edges (literal values)
422
+ // 2. Have inbound edges that ARE selected (preserve the value from the edge)
423
+ const inputsToCopy = Object.fromEntries(Object.entries(allNodeInputs).filter(([handle]) => !inboundHandlesToExclude.has(handle)));
424
+ return {
425
+ typeId: node.typeId,
426
+ params: node.params,
427
+ resolvedHandles: node.resolvedHandles,
428
+ position: {
429
+ x: pos.x - centerX,
430
+ y: pos.y - centerY,
431
+ },
432
+ inputs: inputsToCopy,
433
+ originalNodeId: node.nodeId,
434
+ };
435
+ });
436
+ // Collect edges between copied nodes
437
+ const copiedEdges = def.edges
438
+ .filter((edge) => {
439
+ return (selectedNodeSet.has(edge.source.nodeId) &&
440
+ selectedNodeSet.has(edge.target.nodeId));
441
+ })
442
+ .map((edge) => ({
443
+ sourceNodeId: edge.source.nodeId,
444
+ sourceHandle: edge.source.handle,
445
+ targetNodeId: edge.target.nodeId,
446
+ targetHandle: edge.target.handle,
447
+ typeId: edge.typeId,
448
+ }));
449
+ return {
450
+ nodes: copiedNodes,
451
+ edges: copiedEdges,
452
+ bounds,
453
+ };
454
+ }
455
+ /**
456
+ * Paste copied graph data at the specified center position.
457
+ * Returns the mapping from original node IDs to new node IDs.
458
+ * Uses copyOutputsFrom to copy outputs from original nodes (like duplicate does).
459
+ */
460
+ pasteCopiedData(data, center) {
461
+ const nodeIdMap = new Map();
462
+ const edgeIds = [];
463
+ // Add nodes
464
+ for (const nodeData of data.nodes) {
465
+ const newNodeId = this.addNode({
466
+ typeId: nodeData.typeId,
467
+ params: nodeData.params,
468
+ resolvedHandles: nodeData.resolvedHandles,
469
+ position: {
470
+ x: nodeData.position.x + center.x,
471
+ y: nodeData.position.y + center.y,
472
+ },
473
+ }, {
474
+ inputs: nodeData.inputs,
475
+ copyOutputsFrom: nodeData.originalNodeId,
476
+ dry: true,
477
+ });
478
+ nodeIdMap.set(nodeData.originalNodeId, newNodeId);
479
+ }
480
+ // Add edges
481
+ for (const edgeData of data.edges) {
482
+ const newSourceNodeId = nodeIdMap.get(edgeData.sourceNodeId);
483
+ const newTargetNodeId = nodeIdMap.get(edgeData.targetNodeId);
484
+ if (newSourceNodeId && newTargetNodeId) {
485
+ const edgeId = this.connect({
486
+ source: {
487
+ nodeId: newSourceNodeId,
488
+ handle: edgeData.sourceHandle,
489
+ },
490
+ target: {
491
+ nodeId: newTargetNodeId,
492
+ handle: edgeData.targetHandle,
493
+ },
494
+ typeId: edgeData.typeId,
495
+ }, { dry: true });
496
+ edgeIds.push(edgeId);
497
+ }
498
+ }
499
+ // Select the newly pasted nodes
500
+ this.setSelection({
501
+ nodes: Array.from(nodeIdMap.values()),
502
+ edges: edgeIds,
503
+ });
504
+ return { nodeIdMap, edgeIds };
505
+ }
370
506
  }
371
507
 
372
508
  class CLIWorkbench {
@@ -3648,7 +3784,7 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3648
3784
  .map((e) => e.target.handle));
3649
3785
  const allInputs = runner.getInputs(def)[nodeId] || {};
3650
3786
  const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3651
- wb.addNode({
3787
+ const newNodeId = wb.addNode({
3652
3788
  typeId: n.typeId,
3653
3789
  params: n.params,
3654
3790
  position: { x: pos.x + 24, y: pos.y + 24 },
@@ -3658,6 +3794,11 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3658
3794
  copyOutputsFrom: nodeId,
3659
3795
  dry: true,
3660
3796
  });
3797
+ // Select the newly duplicated node
3798
+ wb.setSelection({
3799
+ nodes: [newNodeId],
3800
+ edges: [],
3801
+ });
3661
3802
  onClose();
3662
3803
  },
3663
3804
  onDuplicateWithEdges: async () => {
@@ -3689,6 +3830,11 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3689
3830
  typeId: edge.typeId,
3690
3831
  }, { dry: true });
3691
3832
  }
3833
+ // Select the newly duplicated node and edges
3834
+ wb.setSelection({
3835
+ nodes: [newNodeId],
3836
+ edges: [],
3837
+ });
3692
3838
  onClose();
3693
3839
  },
3694
3840
  onRunPull: async () => {