@bian-womp/spark-workbench 0.3.46 → 0.3.48

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
@@ -609,51 +609,6 @@ class InMemoryWorkbench extends AbstractWorkbench {
609
609
  bounds,
610
610
  };
611
611
  }
612
- /**
613
- * Duplicate all selected nodes.
614
- * Returns the list of newly created node IDs.
615
- * Each duplicated node is offset by 24px in both x and y directions.
616
- * Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
617
- */
618
- duplicateSelection(runner, options) {
619
- const selection = this.getSelection();
620
- if (selection.nodes.length === 0)
621
- return [];
622
- const positions = this.getPositions();
623
- const sizes = this.getSizes();
624
- const newNodes = [];
625
- // Get inputs without bindings (literal values only)
626
- const allInputs = runner.getInputs(this.def) || {};
627
- // Duplicate each selected node
628
- for (const nodeId of selection.nodes) {
629
- const n = this.def.nodes.find((n) => n.nodeId === nodeId);
630
- if (!n)
631
- continue;
632
- const pos = positions[nodeId];
633
- const size = sizes[nodeId];
634
- const inboundHandles = new Set(this.def.edges
635
- .filter((e) => e.target.nodeId === nodeId)
636
- .map((e) => e.target.handle));
637
- const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
638
- const newNodeId = this.addNode({
639
- typeId: n.typeId,
640
- params: n.params,
641
- resolvedHandles: n.resolvedHandles,
642
- }, {
643
- inputs: inputsWithoutBindings,
644
- position: pos ? { x: pos.x + 24, y: pos.y + 24 } : undefined,
645
- size,
646
- copyOutputsFrom: nodeId,
647
- dry: true,
648
- });
649
- newNodes.push(newNodeId);
650
- }
651
- // Select all newly duplicated nodes
652
- if (newNodes.length > 0) {
653
- this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason: "duplicate-selection" });
654
- }
655
- return newNodes;
656
- }
657
612
  /**
658
613
  * Bake an output value from a node into a new node.
659
614
  * Creates a new node based on the output type's bakeTarget configuration.
@@ -747,37 +702,97 @@ class InMemoryWorkbench extends AbstractWorkbench {
747
702
  }
748
703
  }
749
704
  /**
750
- * Duplicate a single node.
751
- * Returns the ID of the newly created node.
752
- * The duplicated node is offset by 24px in both x and y directions.
705
+ * Duplicate all selected nodes.
706
+ * Returns the list of newly created node IDs.
707
+ * Each duplicated node is offset by 24px in both x and y directions.
753
708
  * Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
754
709
  */
755
- duplicateNode(nodeId, runner, options) {
756
- const n = this.def.nodes.find((n) => n.nodeId === nodeId);
757
- if (!n)
758
- return undefined;
759
- const pos = this.getPositions()[nodeId];
760
- const size = this.getSizes()[nodeId];
761
- // Get inputs without bindings (literal values only)
762
- const allInputs = runner.getInputs(this.def)[nodeId] || {};
763
- const inboundHandles = new Set(this.def.edges
764
- .filter((e) => e.target.nodeId === nodeId)
765
- .map((e) => e.target.handle));
766
- const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
767
- const newNodeId = this.addNode({
768
- typeId: n.typeId,
769
- params: n.params,
770
- resolvedHandles: n.resolvedHandles,
771
- }, {
772
- inputs: inputsWithoutBindings,
773
- position: pos ? { x: pos.x + 24, y: pos.y + 24 } : undefined,
774
- size,
775
- copyOutputsFrom: nodeId,
776
- dry: true,
777
- });
778
- // Select the newly duplicated node
779
- this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node" });
780
- return newNodeId;
710
+ duplicateSelection(runner, options) {
711
+ const selection = this.getSelection();
712
+ if (selection.nodes.length === 0)
713
+ return [];
714
+ const positions = this.getPositions();
715
+ const sizes = this.getSizes();
716
+ const newNodes = [];
717
+ const nodeIdMap = new Map(); // Map from original nodeId to new nodeId
718
+ const processedNodes = new Set(); // Track which nodes have been duplicated
719
+ const selectedNodeSet = new Set(selection.nodes);
720
+ const allInputs = runner.getInputs(this.def) || {};
721
+ // Build a map of incoming edges for each selected node
722
+ const incomingEdgesByNode = new Map();
723
+ for (const nodeId of selection.nodes) {
724
+ const incomingEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
725
+ incomingEdgesByNode.set(nodeId, incomingEdges);
726
+ }
727
+ // Helper function to check if a node is ready to be processed
728
+ // (all its dependencies from selected nodes have been processed)
729
+ const isNodeReady = (nodeId) => {
730
+ const incomingEdges = incomingEdgesByNode.get(nodeId) || [];
731
+ for (const edge of incomingEdges) {
732
+ // If the source is a selected node, it must have been processed
733
+ if (selectedNodeSet.has(edge.source.nodeId)) {
734
+ if (!processedNodes.has(edge.source.nodeId)) {
735
+ return false;
736
+ }
737
+ }
738
+ }
739
+ return true;
740
+ };
741
+ // Process nodes in topological order
742
+ let remainingNodes = new Set(selection.nodes);
743
+ while (remainingNodes.size > 0) {
744
+ // Find nodes that are ready to be processed (no unprocessed dependencies)
745
+ const readyNodes = Array.from(remainingNodes).filter((nodeId) => isNodeReady(nodeId));
746
+ if (readyNodes.length === 0) {
747
+ // If no nodes are ready, there might be a cycle. Process remaining nodes anyway.
748
+ // This shouldn't happen in a DAG, but handle it gracefully.
749
+ readyNodes.push(...remainingNodes);
750
+ }
751
+ // Process each ready node
752
+ for (const nodeId of readyNodes) {
753
+ const n = this.def.nodes.find((n) => n.nodeId === nodeId);
754
+ if (!n) {
755
+ remainingNodes.delete(nodeId);
756
+ continue;
757
+ }
758
+ const pos = positions[nodeId];
759
+ const size = sizes[nodeId];
760
+ // Get all inputs (including those with bindings, since edges will be duplicated)
761
+ const inputs = allInputs[nodeId] || {};
762
+ // Create the duplicated node
763
+ const newNodeId = this.addNode({
764
+ typeId: n.typeId,
765
+ params: n.params,
766
+ resolvedHandles: n.resolvedHandles,
767
+ }, {
768
+ inputs,
769
+ position: pos ? { x: pos.x + 24, y: pos.y + 24 } : undefined,
770
+ size,
771
+ copyOutputsFrom: nodeId,
772
+ dry: true,
773
+ });
774
+ newNodes.push(newNodeId);
775
+ nodeIdMap.set(nodeId, newNodeId);
776
+ processedNodes.add(nodeId);
777
+ // Connect incoming edges for this node
778
+ const incomingEdges = incomingEdgesByNode.get(nodeId) || [];
779
+ for (const edge of incomingEdges) {
780
+ // Determine the source node: use duplicated node if it was duplicated, otherwise use original
781
+ const sourceNodeId = nodeIdMap.get(edge.source.nodeId) || edge.source.nodeId;
782
+ this.connect({
783
+ source: { nodeId: sourceNodeId, handle: edge.source.handle },
784
+ target: { nodeId: newNodeId, handle: edge.target.handle },
785
+ typeId: edge.typeId,
786
+ }, { dry: true });
787
+ }
788
+ remainingNodes.delete(nodeId);
789
+ }
790
+ }
791
+ // Select all newly duplicated nodes
792
+ if (newNodes.length > 0) {
793
+ this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason: "duplicate-selection" });
794
+ }
795
+ return newNodes;
781
796
  }
782
797
  /**
783
798
  * Duplicate a node and all its incoming edges.
@@ -785,7 +800,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
785
800
  * The duplicated node is offset by 24px in both x and y directions.
786
801
  * All incoming edges are duplicated to point to the new node.
787
802
  */
788
- duplicateNodeWithEdges(nodeId, runner, options) {
803
+ duplicateNode(nodeId, runner, options) {
789
804
  const n = this.def.nodes.find((n) => n.nodeId === nodeId);
790
805
  if (!n)
791
806
  return undefined;
@@ -817,7 +832,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
817
832
  }
818
833
  // Select the newly duplicated node
819
834
  if (newNodeId) {
820
- this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node-with-edges" });
835
+ this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node" });
821
836
  }
822
837
  return newNodeId;
823
838
  }
@@ -5091,9 +5106,9 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
5091
5106
  onClose();
5092
5107
  },
5093
5108
  onDuplicate: async () => {
5094
- wb.duplicateNodeWithEdges(nodeId, runner, {
5109
+ wb.duplicateNode(nodeId, runner, {
5095
5110
  commit: true,
5096
- reason: "duplicate-node-with-edges",
5111
+ reason: "duplicate-node",
5097
5112
  });
5098
5113
  onClose();
5099
5114
  },