@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/esm/index.js
CHANGED
|
@@ -137,6 +137,8 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
137
137
|
super(args);
|
|
138
138
|
this._def = { nodes: [], edges: [] };
|
|
139
139
|
this.listeners = new Map();
|
|
140
|
+
this._batchChanges = null;
|
|
141
|
+
this._batchDepth = 0;
|
|
140
142
|
this.positions = {};
|
|
141
143
|
this.sizes = {};
|
|
142
144
|
this.selection = {
|
|
@@ -408,16 +410,24 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
408
410
|
*/
|
|
409
411
|
deleteSelection(options) {
|
|
410
412
|
const selection = this.getSelection();
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
413
|
+
this.beginBatch();
|
|
414
|
+
let completed = false;
|
|
415
|
+
try {
|
|
416
|
+
// Delete all selected nodes (this will also remove connected edges)
|
|
417
|
+
for (const nodeId of selection.nodes) {
|
|
418
|
+
this.removeNode(nodeId);
|
|
419
|
+
}
|
|
420
|
+
// Delete remaining selected edges (edges not connected to deleted nodes)
|
|
421
|
+
for (const edgeId of selection.edges) {
|
|
422
|
+
this.disconnect(edgeId);
|
|
423
|
+
}
|
|
424
|
+
// Clear selection
|
|
425
|
+
this.setSelectionInternal({ nodes: [], edges: [] }, options);
|
|
426
|
+
completed = true;
|
|
414
427
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
this.disconnect(edgeId);
|
|
428
|
+
finally {
|
|
429
|
+
this.endBatch(completed ? options : { ...options, commit: false });
|
|
418
430
|
}
|
|
419
|
-
// Clear selection
|
|
420
|
-
this.setSelectionInternal({ nodes: [], edges: [] }, options);
|
|
421
431
|
}
|
|
422
432
|
setViewport(viewport) {
|
|
423
433
|
if (lod.isEqual(this.viewport, viewport))
|
|
@@ -517,11 +527,69 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
517
527
|
return () => set.delete(handler);
|
|
518
528
|
}
|
|
519
529
|
emit(event, payload) {
|
|
530
|
+
if (this._batchDepth > 0 && this._batchChanges !== null) {
|
|
531
|
+
if (event === "graphChanged") {
|
|
532
|
+
const d = payload;
|
|
533
|
+
if (d.change && d.change.type !== "load" && d.change.type !== "batch") {
|
|
534
|
+
this._batchChanges.push(d.change);
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (event === "graphUiChanged") {
|
|
539
|
+
const d = payload;
|
|
540
|
+
if (d.commit) {
|
|
541
|
+
const stripped = { ...d, commit: undefined };
|
|
542
|
+
const set = this.listeners.get(event);
|
|
543
|
+
if (set)
|
|
544
|
+
for (const h of Array.from(set))
|
|
545
|
+
h(stripped);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (event === "validationChanged") {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
520
553
|
const set = this.listeners.get(event);
|
|
521
554
|
if (set)
|
|
522
555
|
for (const h of Array.from(set))
|
|
523
556
|
h(payload);
|
|
524
557
|
}
|
|
558
|
+
beginBatch() {
|
|
559
|
+
if (this._batchDepth === 0) {
|
|
560
|
+
this._batchChanges = [];
|
|
561
|
+
}
|
|
562
|
+
this._batchDepth++;
|
|
563
|
+
}
|
|
564
|
+
endBatch(options) {
|
|
565
|
+
if (this._batchDepth <= 0)
|
|
566
|
+
return;
|
|
567
|
+
this._batchDepth--;
|
|
568
|
+
if (this._batchDepth > 0)
|
|
569
|
+
return;
|
|
570
|
+
const changes = this._batchChanges;
|
|
571
|
+
this._batchChanges = null;
|
|
572
|
+
if (!changes || changes.length === 0)
|
|
573
|
+
return;
|
|
574
|
+
const set = this.listeners.get("graphChanged");
|
|
575
|
+
if (set) {
|
|
576
|
+
const payload = {
|
|
577
|
+
def: this._def,
|
|
578
|
+
change: { type: "batch", changes },
|
|
579
|
+
...options,
|
|
580
|
+
};
|
|
581
|
+
for (const h of Array.from(set))
|
|
582
|
+
h(payload);
|
|
583
|
+
}
|
|
584
|
+
this.refreshValidation();
|
|
585
|
+
}
|
|
586
|
+
getInboundHandleIds(nodeId, shouldExcludeEdge) {
|
|
587
|
+
const inboundEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
588
|
+
return new Set(inboundEdges.filter((e) => shouldExcludeEdge(e)).map((e) => e.target.handle));
|
|
589
|
+
}
|
|
590
|
+
excludeHandlesFromInputs(allNodeInputs, excludedHandles) {
|
|
591
|
+
return Object.fromEntries(Object.entries(allNodeInputs).filter(([handle]) => !excludedHandles.has(handle)));
|
|
592
|
+
}
|
|
525
593
|
/**
|
|
526
594
|
* Copy selected nodes and their internal edges.
|
|
527
595
|
* Returns data in a format suitable for pasting.
|
|
@@ -567,18 +635,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
567
635
|
const pos = positions[node.nodeId];
|
|
568
636
|
const size = sizes[node.nodeId];
|
|
569
637
|
const customNodeData = this.getCustomNodeData(node.nodeId);
|
|
570
|
-
|
|
571
|
-
const inboundEdges = this.def.edges.filter((e) => e.target.nodeId === node.nodeId);
|
|
572
|
-
// Build set of handles that have inbound edges
|
|
573
|
-
// But only exclude handles whose edges are NOT selected
|
|
574
|
-
const inboundHandlesToExclude = new Set(inboundEdges
|
|
575
|
-
.filter((e) => !selectedEdgeSet.has(e.id)) // Only exclude if edge is not selected
|
|
576
|
-
.map((e) => e.target.handle));
|
|
638
|
+
const inboundHandlesToExclude = this.getInboundHandleIds(node.nodeId, (e) => !selectedEdgeSet.has(e.id));
|
|
577
639
|
const allNodeInputs = allInputs[node.nodeId] || {};
|
|
578
640
|
// Include inputs that either:
|
|
579
641
|
// 1. Don't have inbound edges (literal values)
|
|
580
642
|
// 2. Have inbound edges that ARE selected (preserve the value from the edge)
|
|
581
|
-
const inputsToCopy =
|
|
643
|
+
const inputsToCopy = this.excludeHandlesFromInputs(allNodeInputs, inboundHandlesToExclude);
|
|
582
644
|
return {
|
|
583
645
|
typeId: node.typeId,
|
|
584
646
|
params: node.params,
|
|
@@ -619,21 +681,21 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
619
681
|
* Returns the ID of the last created node (or undefined if none created).
|
|
620
682
|
*/
|
|
621
683
|
async bake(registry, runner, outputValue, outputTypeId, nodePosition, options) {
|
|
684
|
+
if (!outputTypeId || outputValue === undefined)
|
|
685
|
+
return undefined;
|
|
686
|
+
const bakeOpts = options || { commit: true, reason: "bake" };
|
|
687
|
+
this.beginBatch();
|
|
688
|
+
let completed = false;
|
|
622
689
|
try {
|
|
623
|
-
if (!outputTypeId || outputValue === undefined)
|
|
624
|
-
return undefined;
|
|
625
690
|
const unwrap = (v) => (isTyped(v) ? unwrapValue(v) : v);
|
|
626
691
|
const coerceIfNeeded = async (fromType, toTypes, value) => {
|
|
627
692
|
if (!toTypes)
|
|
628
693
|
return value;
|
|
629
694
|
const typesArray = Array.isArray(toTypes) ? toTypes : [toTypes];
|
|
630
|
-
// If output type matches any input type exactly, no coercion needed
|
|
631
695
|
if (typesArray.includes(fromType))
|
|
632
696
|
return value;
|
|
633
|
-
// If no coercion function available, return as-is
|
|
634
697
|
if (!runner?.coerce)
|
|
635
698
|
return value;
|
|
636
|
-
// Try coercing to each type until one succeeds
|
|
637
699
|
for (const toType of typesArray) {
|
|
638
700
|
try {
|
|
639
701
|
return await runner.coerce(fromType, toType, value);
|
|
@@ -642,7 +704,6 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
642
704
|
// Continue to next type
|
|
643
705
|
}
|
|
644
706
|
}
|
|
645
|
-
// If all coercion attempts failed, return value as-is
|
|
646
707
|
return value;
|
|
647
708
|
};
|
|
648
709
|
const pos = nodePosition;
|
|
@@ -697,13 +758,17 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
697
758
|
}
|
|
698
759
|
}
|
|
699
760
|
if (newNodeId) {
|
|
700
|
-
this.setSelectionInternal({ nodes: [newNodeId], edges: [] },
|
|
761
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, bakeOpts);
|
|
701
762
|
}
|
|
763
|
+
completed = true;
|
|
702
764
|
return newNodeId;
|
|
703
765
|
}
|
|
704
766
|
catch {
|
|
705
767
|
return undefined;
|
|
706
768
|
}
|
|
769
|
+
finally {
|
|
770
|
+
this.endBatch(completed ? bakeOpts : { ...bakeOpts, commit: false });
|
|
771
|
+
}
|
|
707
772
|
}
|
|
708
773
|
/**
|
|
709
774
|
* Duplicate all selected nodes.
|
|
@@ -715,88 +780,86 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
715
780
|
const selection = this.getSelection();
|
|
716
781
|
if (selection.nodes.length === 0)
|
|
717
782
|
return [];
|
|
718
|
-
const
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (
|
|
739
|
-
|
|
783
|
+
const reason = options?.reason || "duplicate-selection";
|
|
784
|
+
const batchOptions = { commit: options?.commit ?? true, reason, dry: true };
|
|
785
|
+
this.beginBatch();
|
|
786
|
+
let completed = false;
|
|
787
|
+
try {
|
|
788
|
+
const positions = this.getPositions();
|
|
789
|
+
const sizes = this.getSizes();
|
|
790
|
+
const newNodes = [];
|
|
791
|
+
const nodeIdMap = new Map();
|
|
792
|
+
const processedNodes = new Set();
|
|
793
|
+
const selectedNodeSet = new Set(selection.nodes);
|
|
794
|
+
const allInputs = runner.getInputs(this.def) || {};
|
|
795
|
+
const incomingEdgesByNode = new Map();
|
|
796
|
+
for (const nodeId of selection.nodes) {
|
|
797
|
+
const incomingEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
798
|
+
incomingEdgesByNode.set(nodeId, incomingEdges);
|
|
799
|
+
}
|
|
800
|
+
const isNodeReady = (nodeId) => {
|
|
801
|
+
const incomingEdges = incomingEdgesByNode.get(nodeId) || [];
|
|
802
|
+
for (const edge of incomingEdges) {
|
|
803
|
+
if (selectedNodeSet.has(edge.source.nodeId)) {
|
|
804
|
+
if (!processedNodes.has(edge.source.nodeId)) {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
740
807
|
}
|
|
741
808
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const readyNodes = Array.from(remainingNodes).filter((nodeId) => isNodeReady(nodeId));
|
|
750
|
-
if (readyNodes.length === 0) {
|
|
751
|
-
// If no nodes are ready, there might be a cycle. Process remaining nodes anyway.
|
|
752
|
-
// This shouldn't happen in a DAG, but handle it gracefully.
|
|
753
|
-
readyNodes.push(...remainingNodes);
|
|
754
|
-
}
|
|
755
|
-
// Process each ready node
|
|
756
|
-
for (const nodeId of readyNodes) {
|
|
757
|
-
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
758
|
-
if (!n) {
|
|
759
|
-
remainingNodes.delete(nodeId);
|
|
760
|
-
continue;
|
|
809
|
+
return true;
|
|
810
|
+
};
|
|
811
|
+
let remainingNodes = new Set(selection.nodes);
|
|
812
|
+
while (remainingNodes.size > 0) {
|
|
813
|
+
const readyNodes = Array.from(remainingNodes).filter((nodeId) => isNodeReady(nodeId));
|
|
814
|
+
if (readyNodes.length === 0) {
|
|
815
|
+
readyNodes.push(...remainingNodes);
|
|
761
816
|
}
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
817
|
+
for (const nodeId of readyNodes) {
|
|
818
|
+
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
819
|
+
if (!n) {
|
|
820
|
+
remainingNodes.delete(nodeId);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const pos = positions[nodeId];
|
|
824
|
+
const size = sizes[nodeId];
|
|
825
|
+
const inboundHandlesToExclude = this.getInboundHandleIds(nodeId, () => true);
|
|
826
|
+
const inputs = this.excludeHandlesFromInputs(allInputs[nodeId] || {}, inboundHandlesToExclude);
|
|
827
|
+
const newNodeId = this.addNode({
|
|
828
|
+
typeId: n.typeId,
|
|
829
|
+
params: n.params,
|
|
830
|
+
resolvedHandles: n.resolvedHandles,
|
|
831
|
+
}, {
|
|
832
|
+
inputs,
|
|
833
|
+
position: pos ? { x: pos.x + 24, y: pos.y + 24 } : undefined,
|
|
834
|
+
size,
|
|
835
|
+
copyOutputsFrom: nodeId,
|
|
836
|
+
dry: true,
|
|
837
|
+
reason,
|
|
838
|
+
});
|
|
839
|
+
newNodes.push(newNodeId);
|
|
840
|
+
nodeIdMap.set(nodeId, newNodeId);
|
|
841
|
+
processedNodes.add(nodeId);
|
|
842
|
+
const incomingEdges = incomingEdgesByNode.get(nodeId) || [];
|
|
843
|
+
for (const edge of incomingEdges) {
|
|
844
|
+
const sourceNodeId = nodeIdMap.get(edge.source.nodeId) || edge.source.nodeId;
|
|
845
|
+
this.connect({
|
|
846
|
+
source: { nodeId: sourceNodeId, handle: edge.source.handle },
|
|
847
|
+
target: { nodeId: newNodeId, handle: edge.target.handle },
|
|
848
|
+
typeId: edge.typeId,
|
|
849
|
+
}, { dry: true, reason });
|
|
850
|
+
}
|
|
851
|
+
remainingNodes.delete(nodeId);
|
|
791
852
|
}
|
|
792
|
-
remainingNodes.delete(nodeId);
|
|
793
853
|
}
|
|
854
|
+
if (newNodes.length > 0) {
|
|
855
|
+
this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason });
|
|
856
|
+
}
|
|
857
|
+
completed = true;
|
|
858
|
+
return newNodes;
|
|
794
859
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason: "duplicate-selection" });
|
|
860
|
+
finally {
|
|
861
|
+
this.endBatch(completed ? batchOptions : { ...batchOptions, commit: false });
|
|
798
862
|
}
|
|
799
|
-
return newNodes;
|
|
800
863
|
}
|
|
801
864
|
/**
|
|
802
865
|
* Duplicate a node and all its incoming edges.
|
|
@@ -808,37 +871,44 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
808
871
|
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
809
872
|
if (!n)
|
|
810
873
|
return undefined;
|
|
811
|
-
const
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
|
|
874
|
+
const reason = options?.reason || "duplicate-node";
|
|
875
|
+
const batchOptions = { commit: options?.commit ?? true, reason, dry: true };
|
|
876
|
+
this.beginBatch();
|
|
877
|
+
let completed = false;
|
|
878
|
+
try {
|
|
879
|
+
const pos = this.getPositions()[nodeId];
|
|
880
|
+
const size = this.getSizes()[nodeId];
|
|
881
|
+
const inboundHandlesToExclude = this.getInboundHandleIds(nodeId, () => true);
|
|
882
|
+
const inputs = this.excludeHandlesFromInputs(runner.getInputs(this.def)[nodeId] || {}, inboundHandlesToExclude);
|
|
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
|
+
}
|
|
842
912
|
}
|
|
843
913
|
/**
|
|
844
914
|
* Paste copied graph data at the specified center position.
|
|
@@ -848,54 +918,62 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
848
918
|
pasteCopiedData(data, center, options) {
|
|
849
919
|
const nodeIdMap = new Map();
|
|
850
920
|
const edgeIds = [];
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
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,
|
|
874
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
|
+
}
|
|
875
951
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
},
|
|
891
|
-
|
|
892
|
-
}
|
|
893
|
-
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
|
+
}
|
|
894
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 });
|
|
895
976
|
}
|
|
896
|
-
// Select the newly pasted nodes
|
|
897
|
-
this.setSelectionInternal({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
|
|
898
|
-
return { nodeIdMap, edgeIds };
|
|
899
977
|
}
|
|
900
978
|
/**
|
|
901
979
|
* Get the currently copied graph data.
|
|
@@ -4380,17 +4458,62 @@ function WorkbenchProvider({ wb, runner, overrides, uiVersion, children, }) {
|
|
|
4380
4458
|
setRegistryVersion((v) => v + 1);
|
|
4381
4459
|
});
|
|
4382
4460
|
const offWbGraphChanged = wb.on("graphChanged", (event) => {
|
|
4383
|
-
// Clear validation errors for removed nodes
|
|
4384
4461
|
if (event.change?.type === "removeNode") {
|
|
4385
4462
|
const removedNodeId = event.change.nodeId;
|
|
4386
4463
|
setInputValidationErrors((prev) => prev.filter((err) => err.nodeId !== removedNodeId));
|
|
4387
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
|
+
}
|
|
4388
4473
|
return add("workbench", "graphChanged")(event);
|
|
4389
4474
|
});
|
|
4390
4475
|
const offWbGraphUiChangedForLog = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
4391
4476
|
const offWbValidationChanged = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
4392
4477
|
// Ensure newly added nodes start as invalidated until first evaluation
|
|
4393
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
|
+
}
|
|
4394
4517
|
// Build detailed reason from change type
|
|
4395
4518
|
let reason = "graph-changed";
|
|
4396
4519
|
if (event.change) {
|