@bian-womp/spark-workbench 0.3.84 → 0.3.86
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 +295 -174
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts +10 -0
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/cjs/src/core/contracts.d.ts +32 -28
- package/lib/cjs/src/core/contracts.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/esm/index.js +295 -174
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/core/InMemoryWorkbench.d.ts +10 -0
- package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/esm/src/core/contracts.d.ts +32 -28
- package/lib/esm/src/core/contracts.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/package.json +4 -4
package/lib/cjs/index.cjs
CHANGED
|
@@ -139,6 +139,8 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
139
139
|
super(args);
|
|
140
140
|
this._def = { nodes: [], edges: [] };
|
|
141
141
|
this.listeners = new Map();
|
|
142
|
+
this._batchChanges = null;
|
|
143
|
+
this._batchDepth = 0;
|
|
142
144
|
this.positions = {};
|
|
143
145
|
this.sizes = {};
|
|
144
146
|
this.selection = {
|
|
@@ -410,16 +412,24 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
410
412
|
*/
|
|
411
413
|
deleteSelection(options) {
|
|
412
414
|
const selection = this.getSelection();
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
415
|
+
this.beginBatch();
|
|
416
|
+
let completed = false;
|
|
417
|
+
try {
|
|
418
|
+
// Delete all selected nodes (this will also remove connected edges)
|
|
419
|
+
for (const nodeId of selection.nodes) {
|
|
420
|
+
this.removeNode(nodeId);
|
|
421
|
+
}
|
|
422
|
+
// Delete remaining selected edges (edges not connected to deleted nodes)
|
|
423
|
+
for (const edgeId of selection.edges) {
|
|
424
|
+
this.disconnect(edgeId);
|
|
425
|
+
}
|
|
426
|
+
// Clear selection
|
|
427
|
+
this.setSelectionInternal({ nodes: [], edges: [] }, options);
|
|
428
|
+
completed = true;
|
|
416
429
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
this.disconnect(edgeId);
|
|
430
|
+
finally {
|
|
431
|
+
this.endBatch(completed ? options : { ...options, commit: false });
|
|
420
432
|
}
|
|
421
|
-
// Clear selection
|
|
422
|
-
this.setSelectionInternal({ nodes: [], edges: [] }, options);
|
|
423
433
|
}
|
|
424
434
|
setViewport(viewport) {
|
|
425
435
|
if (lod.isEqual(this.viewport, viewport))
|
|
@@ -519,11 +529,69 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
519
529
|
return () => set.delete(handler);
|
|
520
530
|
}
|
|
521
531
|
emit(event, payload) {
|
|
532
|
+
if (this._batchDepth > 0 && this._batchChanges !== null) {
|
|
533
|
+
if (event === "graphChanged") {
|
|
534
|
+
const d = payload;
|
|
535
|
+
if (d.change && d.change.type !== "load" && d.change.type !== "batch") {
|
|
536
|
+
this._batchChanges.push(d.change);
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (event === "graphUiChanged") {
|
|
541
|
+
const d = payload;
|
|
542
|
+
if (d.commit) {
|
|
543
|
+
const stripped = { ...d, commit: undefined };
|
|
544
|
+
const set = this.listeners.get(event);
|
|
545
|
+
if (set)
|
|
546
|
+
for (const h of Array.from(set))
|
|
547
|
+
h(stripped);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (event === "validationChanged") {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
522
555
|
const set = this.listeners.get(event);
|
|
523
556
|
if (set)
|
|
524
557
|
for (const h of Array.from(set))
|
|
525
558
|
h(payload);
|
|
526
559
|
}
|
|
560
|
+
beginBatch() {
|
|
561
|
+
if (this._batchDepth === 0) {
|
|
562
|
+
this._batchChanges = [];
|
|
563
|
+
}
|
|
564
|
+
this._batchDepth++;
|
|
565
|
+
}
|
|
566
|
+
endBatch(options) {
|
|
567
|
+
if (this._batchDepth <= 0)
|
|
568
|
+
return;
|
|
569
|
+
this._batchDepth--;
|
|
570
|
+
if (this._batchDepth > 0)
|
|
571
|
+
return;
|
|
572
|
+
const changes = this._batchChanges;
|
|
573
|
+
this._batchChanges = null;
|
|
574
|
+
if (!changes || changes.length === 0)
|
|
575
|
+
return;
|
|
576
|
+
const set = this.listeners.get("graphChanged");
|
|
577
|
+
if (set) {
|
|
578
|
+
const payload = {
|
|
579
|
+
def: this._def,
|
|
580
|
+
change: { type: "batch", changes },
|
|
581
|
+
...options,
|
|
582
|
+
};
|
|
583
|
+
for (const h of Array.from(set))
|
|
584
|
+
h(payload);
|
|
585
|
+
}
|
|
586
|
+
this.refreshValidation();
|
|
587
|
+
}
|
|
588
|
+
getInboundHandleIds(nodeId, shouldExcludeEdge) {
|
|
589
|
+
const inboundEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
590
|
+
return new Set(inboundEdges.filter((e) => shouldExcludeEdge(e)).map((e) => e.target.handle));
|
|
591
|
+
}
|
|
592
|
+
excludeHandlesFromInputs(allNodeInputs, excludedHandles) {
|
|
593
|
+
return Object.fromEntries(Object.entries(allNodeInputs).filter(([handle]) => !excludedHandles.has(handle)));
|
|
594
|
+
}
|
|
527
595
|
/**
|
|
528
596
|
* Copy selected nodes and their internal edges.
|
|
529
597
|
* Returns data in a format suitable for pasting.
|
|
@@ -569,18 +637,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
569
637
|
const pos = positions[node.nodeId];
|
|
570
638
|
const size = sizes[node.nodeId];
|
|
571
639
|
const customNodeData = this.getCustomNodeData(node.nodeId);
|
|
572
|
-
|
|
573
|
-
const inboundEdges = this.def.edges.filter((e) => e.target.nodeId === node.nodeId);
|
|
574
|
-
// Build set of handles that have inbound edges
|
|
575
|
-
// But only exclude handles whose edges are NOT selected
|
|
576
|
-
const inboundHandlesToExclude = new Set(inboundEdges
|
|
577
|
-
.filter((e) => !selectedEdgeSet.has(e.id)) // Only exclude if edge is not selected
|
|
578
|
-
.map((e) => e.target.handle));
|
|
640
|
+
const inboundHandlesToExclude = this.getInboundHandleIds(node.nodeId, (e) => !selectedEdgeSet.has(e.id));
|
|
579
641
|
const allNodeInputs = allInputs[node.nodeId] || {};
|
|
580
642
|
// Include inputs that either:
|
|
581
643
|
// 1. Don't have inbound edges (literal values)
|
|
582
644
|
// 2. Have inbound edges that ARE selected (preserve the value from the edge)
|
|
583
|
-
const inputsToCopy =
|
|
645
|
+
const inputsToCopy = this.excludeHandlesFromInputs(allNodeInputs, inboundHandlesToExclude);
|
|
584
646
|
return {
|
|
585
647
|
typeId: node.typeId,
|
|
586
648
|
params: node.params,
|
|
@@ -621,21 +683,21 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
621
683
|
* Returns the ID of the last created node (or undefined if none created).
|
|
622
684
|
*/
|
|
623
685
|
async bake(registry, runner, outputValue, outputTypeId, nodePosition, options) {
|
|
686
|
+
if (!outputTypeId || outputValue === undefined)
|
|
687
|
+
return undefined;
|
|
688
|
+
const bakeOpts = options || { commit: true, reason: "bake" };
|
|
689
|
+
this.beginBatch();
|
|
690
|
+
let completed = false;
|
|
624
691
|
try {
|
|
625
|
-
if (!outputTypeId || outputValue === undefined)
|
|
626
|
-
return undefined;
|
|
627
692
|
const unwrap = (v) => (sparkGraph.isTyped(v) ? sparkGraph.unwrapValue(v) : v);
|
|
628
693
|
const coerceIfNeeded = async (fromType, toTypes, value) => {
|
|
629
694
|
if (!toTypes)
|
|
630
695
|
return value;
|
|
631
696
|
const typesArray = Array.isArray(toTypes) ? toTypes : [toTypes];
|
|
632
|
-
// If output type matches any input type exactly, no coercion needed
|
|
633
697
|
if (typesArray.includes(fromType))
|
|
634
698
|
return value;
|
|
635
|
-
// If no coercion function available, return as-is
|
|
636
699
|
if (!runner?.coerce)
|
|
637
700
|
return value;
|
|
638
|
-
// Try coercing to each type until one succeeds
|
|
639
701
|
for (const toType of typesArray) {
|
|
640
702
|
try {
|
|
641
703
|
return await runner.coerce(fromType, toType, value);
|
|
@@ -644,7 +706,6 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
644
706
|
// Continue to next type
|
|
645
707
|
}
|
|
646
708
|
}
|
|
647
|
-
// If all coercion attempts failed, return value as-is
|
|
648
709
|
return value;
|
|
649
710
|
};
|
|
650
711
|
const pos = nodePosition;
|
|
@@ -699,13 +760,17 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
699
760
|
}
|
|
700
761
|
}
|
|
701
762
|
if (newNodeId) {
|
|
702
|
-
this.setSelectionInternal({ nodes: [newNodeId], edges: [] },
|
|
763
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, bakeOpts);
|
|
703
764
|
}
|
|
765
|
+
completed = true;
|
|
704
766
|
return newNodeId;
|
|
705
767
|
}
|
|
706
768
|
catch {
|
|
707
769
|
return undefined;
|
|
708
770
|
}
|
|
771
|
+
finally {
|
|
772
|
+
this.endBatch(completed ? bakeOpts : { ...bakeOpts, commit: false });
|
|
773
|
+
}
|
|
709
774
|
}
|
|
710
775
|
/**
|
|
711
776
|
* Duplicate all selected nodes.
|
|
@@ -717,88 +782,85 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
717
782
|
const selection = this.getSelection();
|
|
718
783
|
if (selection.nodes.length === 0)
|
|
719
784
|
return [];
|
|
720
|
-
const
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
if (
|
|
741
|
-
|
|
785
|
+
const reason = options?.reason || "duplicate-selection";
|
|
786
|
+
const batchOptions = { commit: options?.commit ?? true, reason, dry: true };
|
|
787
|
+
this.beginBatch();
|
|
788
|
+
let completed = false;
|
|
789
|
+
try {
|
|
790
|
+
const positions = this.getPositions();
|
|
791
|
+
const sizes = this.getSizes();
|
|
792
|
+
const newNodes = [];
|
|
793
|
+
const nodeIdMap = new Map();
|
|
794
|
+
const processedNodes = new Set();
|
|
795
|
+
const selectedNodeSet = new Set(selection.nodes);
|
|
796
|
+
const allInputs = runner.getInputs(this.def) || {};
|
|
797
|
+
const incomingEdgesByNode = new Map();
|
|
798
|
+
for (const nodeId of selection.nodes) {
|
|
799
|
+
const incomingEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
800
|
+
incomingEdgesByNode.set(nodeId, incomingEdges);
|
|
801
|
+
}
|
|
802
|
+
const isNodeReady = (nodeId) => {
|
|
803
|
+
const incomingEdges = incomingEdgesByNode.get(nodeId) || [];
|
|
804
|
+
for (const edge of incomingEdges) {
|
|
805
|
+
if (selectedNodeSet.has(edge.source.nodeId)) {
|
|
806
|
+
if (!processedNodes.has(edge.source.nodeId)) {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
742
809
|
}
|
|
743
810
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
const readyNodes = Array.from(remainingNodes).filter((nodeId) => isNodeReady(nodeId));
|
|
752
|
-
if (readyNodes.length === 0) {
|
|
753
|
-
// If no nodes are ready, there might be a cycle. Process remaining nodes anyway.
|
|
754
|
-
// This shouldn't happen in a DAG, but handle it gracefully.
|
|
755
|
-
readyNodes.push(...remainingNodes);
|
|
756
|
-
}
|
|
757
|
-
// Process each ready node
|
|
758
|
-
for (const nodeId of readyNodes) {
|
|
759
|
-
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
760
|
-
if (!n) {
|
|
761
|
-
remainingNodes.delete(nodeId);
|
|
762
|
-
continue;
|
|
811
|
+
return true;
|
|
812
|
+
};
|
|
813
|
+
let remainingNodes = new Set(selection.nodes);
|
|
814
|
+
while (remainingNodes.size > 0) {
|
|
815
|
+
const readyNodes = Array.from(remainingNodes).filter((nodeId) => isNodeReady(nodeId));
|
|
816
|
+
if (readyNodes.length === 0) {
|
|
817
|
+
readyNodes.push(...remainingNodes);
|
|
763
818
|
}
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
819
|
+
for (const nodeId of readyNodes) {
|
|
820
|
+
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
821
|
+
if (!n) {
|
|
822
|
+
remainingNodes.delete(nodeId);
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
const pos = positions[nodeId];
|
|
826
|
+
const size = sizes[nodeId];
|
|
827
|
+
const inputs = allInputs[nodeId] || {};
|
|
828
|
+
const newNodeId = this.addNode({
|
|
829
|
+
typeId: n.typeId,
|
|
830
|
+
params: n.params,
|
|
831
|
+
resolvedHandles: n.resolvedHandles,
|
|
832
|
+
}, {
|
|
833
|
+
inputs,
|
|
834
|
+
position: pos ? { x: pos.x + 24, y: pos.y + 24 } : undefined,
|
|
835
|
+
size,
|
|
836
|
+
copyOutputsFrom: nodeId,
|
|
837
|
+
dry: true,
|
|
838
|
+
reason,
|
|
839
|
+
});
|
|
840
|
+
newNodes.push(newNodeId);
|
|
841
|
+
nodeIdMap.set(nodeId, newNodeId);
|
|
842
|
+
processedNodes.add(nodeId);
|
|
843
|
+
const incomingEdges = incomingEdgesByNode.get(nodeId) || [];
|
|
844
|
+
for (const edge of incomingEdges) {
|
|
845
|
+
const sourceNodeId = nodeIdMap.get(edge.source.nodeId) || edge.source.nodeId;
|
|
846
|
+
this.connect({
|
|
847
|
+
source: { nodeId: sourceNodeId, handle: edge.source.handle },
|
|
848
|
+
target: { nodeId: newNodeId, handle: edge.target.handle },
|
|
849
|
+
typeId: edge.typeId,
|
|
850
|
+
}, { dry: true, reason });
|
|
851
|
+
}
|
|
852
|
+
remainingNodes.delete(nodeId);
|
|
793
853
|
}
|
|
794
|
-
remainingNodes.delete(nodeId);
|
|
795
854
|
}
|
|
855
|
+
if (newNodes.length > 0) {
|
|
856
|
+
this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason });
|
|
857
|
+
}
|
|
858
|
+
completed = true;
|
|
859
|
+
return newNodes;
|
|
796
860
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason: "duplicate-selection" });
|
|
861
|
+
finally {
|
|
862
|
+
this.endBatch(completed ? batchOptions : { ...batchOptions, commit: false });
|
|
800
863
|
}
|
|
801
|
-
return newNodes;
|
|
802
864
|
}
|
|
803
865
|
/**
|
|
804
866
|
* Duplicate a node and all its incoming edges.
|
|
@@ -810,37 +872,43 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
810
872
|
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
811
873
|
if (!n)
|
|
812
874
|
return undefined;
|
|
813
|
-
const
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
this.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
875
|
+
const reason = options?.reason || "duplicate-node";
|
|
876
|
+
const batchOptions = { commit: options?.commit ?? true, reason, dry: true };
|
|
877
|
+
this.beginBatch();
|
|
878
|
+
let completed = false;
|
|
879
|
+
try {
|
|
880
|
+
const pos = this.getPositions()[nodeId];
|
|
881
|
+
const size = this.getSizes()[nodeId];
|
|
882
|
+
const inputs = runner.getInputs(this.def)[nodeId] || {};
|
|
883
|
+
const newNodeId = this.addNode({
|
|
884
|
+
typeId: n.typeId,
|
|
885
|
+
params: n.params,
|
|
886
|
+
resolvedHandles: n.resolvedHandles,
|
|
887
|
+
}, {
|
|
888
|
+
inputs,
|
|
889
|
+
position: pos ? { x: pos.x + 24, y: pos.y + 24 } : undefined,
|
|
890
|
+
size,
|
|
891
|
+
copyOutputsFrom: nodeId,
|
|
892
|
+
dry: true,
|
|
893
|
+
reason,
|
|
894
|
+
});
|
|
895
|
+
const incomingEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
896
|
+
for (const edge of incomingEdges) {
|
|
897
|
+
this.connect({
|
|
898
|
+
source: edge.source,
|
|
899
|
+
target: { nodeId: newNodeId, handle: edge.target.handle },
|
|
900
|
+
typeId: edge.typeId,
|
|
901
|
+
}, { dry: true, reason });
|
|
902
|
+
}
|
|
903
|
+
if (newNodeId) {
|
|
904
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason });
|
|
905
|
+
}
|
|
906
|
+
completed = true;
|
|
907
|
+
return newNodeId;
|
|
908
|
+
}
|
|
909
|
+
finally {
|
|
910
|
+
this.endBatch(completed ? batchOptions : { ...batchOptions, commit: false });
|
|
911
|
+
}
|
|
844
912
|
}
|
|
845
913
|
/**
|
|
846
914
|
* Paste copied graph data at the specified center position.
|
|
@@ -850,54 +918,62 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
850
918
|
pasteCopiedData(data, center, options) {
|
|
851
919
|
const nodeIdMap = new Map();
|
|
852
920
|
const edgeIds = [];
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
reason: options?.reason,
|
|
921
|
+
const reason = options?.reason || "paste";
|
|
922
|
+
const batchOptions = { commit: options?.commit ?? true, reason, dry: true };
|
|
923
|
+
this.beginBatch();
|
|
924
|
+
let completed = false;
|
|
925
|
+
try {
|
|
926
|
+
for (const nodeData of data.nodes) {
|
|
927
|
+
const newNodeId = this.addNode({
|
|
928
|
+
typeId: nodeData.typeId,
|
|
929
|
+
params: nodeData.params,
|
|
930
|
+
resolvedHandles: nodeData.resolvedHandles,
|
|
931
|
+
}, {
|
|
932
|
+
inputs: nodeData.inputs,
|
|
933
|
+
position: nodeData.position
|
|
934
|
+
? {
|
|
935
|
+
x: nodeData.position.x + center.x,
|
|
936
|
+
y: nodeData.position.y + center.y,
|
|
937
|
+
}
|
|
938
|
+
: undefined,
|
|
939
|
+
size: nodeData.size,
|
|
940
|
+
copyOutputsFrom: nodeData.originalNodeId,
|
|
941
|
+
dry: true,
|
|
942
|
+
reason,
|
|
876
943
|
});
|
|
944
|
+
nodeIdMap.set(nodeData.originalNodeId, newNodeId);
|
|
945
|
+
if (nodeData.customData !== undefined) {
|
|
946
|
+
this.setCustomNodeData(newNodeId, lod.cloneDeep(nodeData.customData), {
|
|
947
|
+
commit: false,
|
|
948
|
+
reason: options?.reason,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
877
951
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
},
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
edgeIds.push(edgeId);
|
|
952
|
+
for (const edgeData of data.edges) {
|
|
953
|
+
const newSourceNodeId = nodeIdMap.get(edgeData.sourceNodeId);
|
|
954
|
+
const newTargetNodeId = nodeIdMap.get(edgeData.targetNodeId);
|
|
955
|
+
if (newSourceNodeId && newTargetNodeId) {
|
|
956
|
+
const edgeId = this.connect({
|
|
957
|
+
source: {
|
|
958
|
+
nodeId: newSourceNodeId,
|
|
959
|
+
handle: edgeData.sourceHandle,
|
|
960
|
+
},
|
|
961
|
+
target: {
|
|
962
|
+
nodeId: newTargetNodeId,
|
|
963
|
+
handle: edgeData.targetHandle,
|
|
964
|
+
},
|
|
965
|
+
typeId: edgeData.typeId,
|
|
966
|
+
}, { dry: true, reason });
|
|
967
|
+
edgeIds.push(edgeId);
|
|
968
|
+
}
|
|
896
969
|
}
|
|
970
|
+
this.setSelectionInternal({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
|
|
971
|
+
completed = true;
|
|
972
|
+
return { nodeIdMap, edgeIds };
|
|
973
|
+
}
|
|
974
|
+
finally {
|
|
975
|
+
this.endBatch(completed ? batchOptions : { ...batchOptions, commit: false });
|
|
897
976
|
}
|
|
898
|
-
// Select the newly pasted nodes
|
|
899
|
-
this.setSelectionInternal({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
|
|
900
|
-
return { nodeIdMap, edgeIds };
|
|
901
977
|
}
|
|
902
978
|
/**
|
|
903
979
|
* Get the currently copied graph data.
|
|
@@ -4382,17 +4458,62 @@ function WorkbenchProvider({ wb, runner, overrides, uiVersion, children, }) {
|
|
|
4382
4458
|
setRegistryVersion((v) => v + 1);
|
|
4383
4459
|
});
|
|
4384
4460
|
const offWbGraphChanged = wb.on("graphChanged", (event) => {
|
|
4385
|
-
// Clear validation errors for removed nodes
|
|
4386
4461
|
if (event.change?.type === "removeNode") {
|
|
4387
4462
|
const removedNodeId = event.change.nodeId;
|
|
4388
4463
|
setInputValidationErrors((prev) => prev.filter((err) => err.nodeId !== removedNodeId));
|
|
4389
4464
|
}
|
|
4465
|
+
else if (event.change?.type === "batch") {
|
|
4466
|
+
const removedIds = event.change.changes
|
|
4467
|
+
.filter((c) => c.type === "removeNode")
|
|
4468
|
+
.map((c) => c.nodeId);
|
|
4469
|
+
if (removedIds.length > 0) {
|
|
4470
|
+
setInputValidationErrors((prev) => prev.filter((err) => !removedIds.includes(err.nodeId)));
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4390
4473
|
return add("workbench", "graphChanged")(event);
|
|
4391
4474
|
});
|
|
4392
4475
|
const offWbGraphUiChangedForLog = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
4393
4476
|
const offWbValidationChanged = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
4394
4477
|
// Ensure newly added nodes start as invalidated until first evaluation
|
|
4395
4478
|
const offWbGraphChangedForUpdate = wb.on("graphChanged", async (event) => {
|
|
4479
|
+
// Handle batched compound operations (duplicate, paste, delete, bake)
|
|
4480
|
+
if (event.change?.type === "batch") {
|
|
4481
|
+
const { changes } = event.change;
|
|
4482
|
+
const isDry = !!event.dry;
|
|
4483
|
+
const batchReason = event.reason ?? "batch";
|
|
4484
|
+
try {
|
|
4485
|
+
if (runner.isRunning()) {
|
|
4486
|
+
const hasNodeOps = changes.some((c) => c.type === "addNode" && (c.inputs || c.copyOutputsFrom));
|
|
4487
|
+
await runner.update(event.def, { dry: isDry || hasNodeOps });
|
|
4488
|
+
for (const change of changes) {
|
|
4489
|
+
if (change.type === "addNode") {
|
|
4490
|
+
if (change.inputs) {
|
|
4491
|
+
await runner.setInputs(change.nodeId, change.inputs, { dry: isDry });
|
|
4492
|
+
}
|
|
4493
|
+
if (change.copyOutputsFrom) {
|
|
4494
|
+
await runner.copyOutputs(change.copyOutputsFrom, change.nodeId, { dry: isDry });
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
if (change.type === "setInputs") {
|
|
4498
|
+
await runner.setInputs(change.nodeId, change.inputs, { dry: isDry });
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
if (event.commit) {
|
|
4503
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
4504
|
+
const history = await runner.commit(batchReason).catch((err) => {
|
|
4505
|
+
console.error("[WorkbenchContext] Error committing batch:", err);
|
|
4506
|
+
return undefined;
|
|
4507
|
+
});
|
|
4508
|
+
if (history)
|
|
4509
|
+
wb.setHistory(history);
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
catch (err) {
|
|
4513
|
+
console.error("[WorkbenchContext] Error processing batch:", err);
|
|
4514
|
+
}
|
|
4515
|
+
return;
|
|
4516
|
+
}
|
|
4396
4517
|
// Build detailed reason from change type
|
|
4397
4518
|
let reason = "graph-changed";
|
|
4398
4519
|
if (event.change) {
|