@bian-womp/spark-workbench 0.3.84 → 0.3.85
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 +297 -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 +297 -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,86 @@ 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 inboundHandlesToExclude = this.getInboundHandleIds(nodeId, () => true);
|
|
828
|
+
const inputs = this.excludeHandlesFromInputs(allInputs[nodeId] || {}, inboundHandlesToExclude);
|
|
829
|
+
const newNodeId = this.addNode({
|
|
830
|
+
typeId: n.typeId,
|
|
831
|
+
params: n.params,
|
|
832
|
+
resolvedHandles: n.resolvedHandles,
|
|
833
|
+
}, {
|
|
834
|
+
inputs,
|
|
835
|
+
position: pos ? { x: pos.x + 24, y: pos.y + 24 } : undefined,
|
|
836
|
+
size,
|
|
837
|
+
copyOutputsFrom: nodeId,
|
|
838
|
+
dry: true,
|
|
839
|
+
reason,
|
|
840
|
+
});
|
|
841
|
+
newNodes.push(newNodeId);
|
|
842
|
+
nodeIdMap.set(nodeId, newNodeId);
|
|
843
|
+
processedNodes.add(nodeId);
|
|
844
|
+
const incomingEdges = incomingEdgesByNode.get(nodeId) || [];
|
|
845
|
+
for (const edge of incomingEdges) {
|
|
846
|
+
const sourceNodeId = nodeIdMap.get(edge.source.nodeId) || edge.source.nodeId;
|
|
847
|
+
this.connect({
|
|
848
|
+
source: { nodeId: sourceNodeId, handle: edge.source.handle },
|
|
849
|
+
target: { nodeId: newNodeId, handle: edge.target.handle },
|
|
850
|
+
typeId: edge.typeId,
|
|
851
|
+
}, { dry: true, reason });
|
|
852
|
+
}
|
|
853
|
+
remainingNodes.delete(nodeId);
|
|
793
854
|
}
|
|
794
|
-
remainingNodes.delete(nodeId);
|
|
795
855
|
}
|
|
856
|
+
if (newNodes.length > 0) {
|
|
857
|
+
this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason });
|
|
858
|
+
}
|
|
859
|
+
completed = true;
|
|
860
|
+
return newNodes;
|
|
796
861
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason: "duplicate-selection" });
|
|
862
|
+
finally {
|
|
863
|
+
this.endBatch(completed ? batchOptions : { ...batchOptions, commit: false });
|
|
800
864
|
}
|
|
801
|
-
return newNodes;
|
|
802
865
|
}
|
|
803
866
|
/**
|
|
804
867
|
* Duplicate a node and all its incoming edges.
|
|
@@ -810,37 +873,44 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
810
873
|
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
811
874
|
if (!n)
|
|
812
875
|
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
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
876
|
+
const reason = options?.reason || "duplicate-node";
|
|
877
|
+
const batchOptions = { commit: options?.commit ?? true, reason, dry: true };
|
|
878
|
+
this.beginBatch();
|
|
879
|
+
let completed = false;
|
|
880
|
+
try {
|
|
881
|
+
const pos = this.getPositions()[nodeId];
|
|
882
|
+
const size = this.getSizes()[nodeId];
|
|
883
|
+
const inboundHandlesToExclude = this.getInboundHandleIds(nodeId, () => true);
|
|
884
|
+
const inputs = this.excludeHandlesFromInputs(runner.getInputs(this.def)[nodeId] || {}, inboundHandlesToExclude);
|
|
885
|
+
const newNodeId = this.addNode({
|
|
886
|
+
typeId: n.typeId,
|
|
887
|
+
params: n.params,
|
|
888
|
+
resolvedHandles: n.resolvedHandles,
|
|
889
|
+
}, {
|
|
890
|
+
inputs,
|
|
891
|
+
position: pos ? { x: pos.x + 24, y: pos.y + 24 } : undefined,
|
|
892
|
+
size,
|
|
893
|
+
copyOutputsFrom: nodeId,
|
|
894
|
+
dry: true,
|
|
895
|
+
reason,
|
|
896
|
+
});
|
|
897
|
+
const incomingEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
898
|
+
for (const edge of incomingEdges) {
|
|
899
|
+
this.connect({
|
|
900
|
+
source: edge.source,
|
|
901
|
+
target: { nodeId: newNodeId, handle: edge.target.handle },
|
|
902
|
+
typeId: edge.typeId,
|
|
903
|
+
}, { dry: true, reason });
|
|
904
|
+
}
|
|
905
|
+
if (newNodeId) {
|
|
906
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason });
|
|
907
|
+
}
|
|
908
|
+
completed = true;
|
|
909
|
+
return newNodeId;
|
|
910
|
+
}
|
|
911
|
+
finally {
|
|
912
|
+
this.endBatch(completed ? batchOptions : { ...batchOptions, commit: false });
|
|
913
|
+
}
|
|
844
914
|
}
|
|
845
915
|
/**
|
|
846
916
|
* Paste copied graph data at the specified center position.
|
|
@@ -850,54 +920,62 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
850
920
|
pasteCopiedData(data, center, options) {
|
|
851
921
|
const nodeIdMap = new Map();
|
|
852
922
|
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,
|
|
923
|
+
const reason = options?.reason || "paste";
|
|
924
|
+
const batchOptions = { commit: options?.commit ?? true, reason, dry: true };
|
|
925
|
+
this.beginBatch();
|
|
926
|
+
let completed = false;
|
|
927
|
+
try {
|
|
928
|
+
for (const nodeData of data.nodes) {
|
|
929
|
+
const newNodeId = this.addNode({
|
|
930
|
+
typeId: nodeData.typeId,
|
|
931
|
+
params: nodeData.params,
|
|
932
|
+
resolvedHandles: nodeData.resolvedHandles,
|
|
933
|
+
}, {
|
|
934
|
+
inputs: nodeData.inputs,
|
|
935
|
+
position: nodeData.position
|
|
936
|
+
? {
|
|
937
|
+
x: nodeData.position.x + center.x,
|
|
938
|
+
y: nodeData.position.y + center.y,
|
|
939
|
+
}
|
|
940
|
+
: undefined,
|
|
941
|
+
size: nodeData.size,
|
|
942
|
+
copyOutputsFrom: nodeData.originalNodeId,
|
|
943
|
+
dry: true,
|
|
944
|
+
reason,
|
|
876
945
|
});
|
|
946
|
+
nodeIdMap.set(nodeData.originalNodeId, newNodeId);
|
|
947
|
+
if (nodeData.customData !== undefined) {
|
|
948
|
+
this.setCustomNodeData(newNodeId, lod.cloneDeep(nodeData.customData), {
|
|
949
|
+
commit: false,
|
|
950
|
+
reason: options?.reason,
|
|
951
|
+
});
|
|
952
|
+
}
|
|
877
953
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
},
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
edgeIds.push(edgeId);
|
|
954
|
+
for (const edgeData of data.edges) {
|
|
955
|
+
const newSourceNodeId = nodeIdMap.get(edgeData.sourceNodeId);
|
|
956
|
+
const newTargetNodeId = nodeIdMap.get(edgeData.targetNodeId);
|
|
957
|
+
if (newSourceNodeId && newTargetNodeId) {
|
|
958
|
+
const edgeId = this.connect({
|
|
959
|
+
source: {
|
|
960
|
+
nodeId: newSourceNodeId,
|
|
961
|
+
handle: edgeData.sourceHandle,
|
|
962
|
+
},
|
|
963
|
+
target: {
|
|
964
|
+
nodeId: newTargetNodeId,
|
|
965
|
+
handle: edgeData.targetHandle,
|
|
966
|
+
},
|
|
967
|
+
typeId: edgeData.typeId,
|
|
968
|
+
}, { dry: true, reason });
|
|
969
|
+
edgeIds.push(edgeId);
|
|
970
|
+
}
|
|
896
971
|
}
|
|
972
|
+
this.setSelectionInternal({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
|
|
973
|
+
completed = true;
|
|
974
|
+
return { nodeIdMap, edgeIds };
|
|
975
|
+
}
|
|
976
|
+
finally {
|
|
977
|
+
this.endBatch(completed ? batchOptions : { ...batchOptions, commit: false });
|
|
897
978
|
}
|
|
898
|
-
// Select the newly pasted nodes
|
|
899
|
-
this.setSelectionInternal({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
|
|
900
|
-
return { nodeIdMap, edgeIds };
|
|
901
979
|
}
|
|
902
980
|
/**
|
|
903
981
|
* Get the currently copied graph data.
|
|
@@ -4382,17 +4460,62 @@ function WorkbenchProvider({ wb, runner, overrides, uiVersion, children, }) {
|
|
|
4382
4460
|
setRegistryVersion((v) => v + 1);
|
|
4383
4461
|
});
|
|
4384
4462
|
const offWbGraphChanged = wb.on("graphChanged", (event) => {
|
|
4385
|
-
// Clear validation errors for removed nodes
|
|
4386
4463
|
if (event.change?.type === "removeNode") {
|
|
4387
4464
|
const removedNodeId = event.change.nodeId;
|
|
4388
4465
|
setInputValidationErrors((prev) => prev.filter((err) => err.nodeId !== removedNodeId));
|
|
4389
4466
|
}
|
|
4467
|
+
else if (event.change?.type === "batch") {
|
|
4468
|
+
const removedIds = event.change.changes
|
|
4469
|
+
.filter((c) => c.type === "removeNode")
|
|
4470
|
+
.map((c) => c.nodeId);
|
|
4471
|
+
if (removedIds.length > 0) {
|
|
4472
|
+
setInputValidationErrors((prev) => prev.filter((err) => !removedIds.includes(err.nodeId)));
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4390
4475
|
return add("workbench", "graphChanged")(event);
|
|
4391
4476
|
});
|
|
4392
4477
|
const offWbGraphUiChangedForLog = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
4393
4478
|
const offWbValidationChanged = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
4394
4479
|
// Ensure newly added nodes start as invalidated until first evaluation
|
|
4395
4480
|
const offWbGraphChangedForUpdate = wb.on("graphChanged", async (event) => {
|
|
4481
|
+
// Handle batched compound operations (duplicate, paste, delete, bake)
|
|
4482
|
+
if (event.change?.type === "batch") {
|
|
4483
|
+
const { changes } = event.change;
|
|
4484
|
+
const isDry = !!event.dry;
|
|
4485
|
+
const batchReason = event.reason ?? "batch";
|
|
4486
|
+
try {
|
|
4487
|
+
if (runner.isRunning()) {
|
|
4488
|
+
const hasNodeOps = changes.some((c) => c.type === "addNode" && (c.inputs || c.copyOutputsFrom));
|
|
4489
|
+
await runner.update(event.def, { dry: isDry || hasNodeOps });
|
|
4490
|
+
for (const change of changes) {
|
|
4491
|
+
if (change.type === "addNode") {
|
|
4492
|
+
if (change.inputs) {
|
|
4493
|
+
await runner.setInputs(change.nodeId, change.inputs, { dry: isDry });
|
|
4494
|
+
}
|
|
4495
|
+
if (change.copyOutputsFrom) {
|
|
4496
|
+
await runner.copyOutputs(change.copyOutputsFrom, change.nodeId, { dry: isDry });
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
if (change.type === "setInputs") {
|
|
4500
|
+
await runner.setInputs(change.nodeId, change.inputs, { dry: isDry });
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
if (event.commit) {
|
|
4505
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
4506
|
+
const history = await runner.commit(batchReason).catch((err) => {
|
|
4507
|
+
console.error("[WorkbenchContext] Error committing batch:", err);
|
|
4508
|
+
return undefined;
|
|
4509
|
+
});
|
|
4510
|
+
if (history)
|
|
4511
|
+
wb.setHistory(history);
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
catch (err) {
|
|
4515
|
+
console.error("[WorkbenchContext] Error processing batch:", err);
|
|
4516
|
+
}
|
|
4517
|
+
return;
|
|
4518
|
+
}
|
|
4396
4519
|
// Build detailed reason from change type
|
|
4397
4520
|
let reason = "graph-changed";
|
|
4398
4521
|
if (event.change) {
|