Skip to content

Commit

Permalink
Cross-subgraph connections working
Browse files Browse the repository at this point in the history
 * Set up another global patch network watcher that listens for changes in connections to subgraph portals and handles swapping out the inner connectable nodes to the other side of the portal
 * Re-work some graph editor internals to avoid deleting and re-creating nodes which was causing patch network disconnect/events to get fired and creating infinite loops
  • Loading branch information
Ameobea committed Feb 10, 2024
1 parent c4bf3f1 commit 422ff6a
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 36 deletions.
1 change: 0 additions & 1 deletion src/graphEditor/LiteGraphTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export interface LiteGraphNode {
connect: (srcSlotIx: number, dstNode: LiteGraphNode, dstSlotIx: number) => void;
disconnectOutput: (srcSlot: number | string, dstNode: LiteGraphNode) => boolean;
pos: [number, number];
ignoreRemove?: boolean;
connectables?: AudioConnectables;
clearTriggeredSlot: (slotIx: number, linkIx?: number) => void;
triggerSlot: (slotIx: number, param?: any, linkIx?: number) => void;
Expand Down
11 changes: 7 additions & 4 deletions src/graphEditor/graphDiffing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,15 @@ export const updateGraph = (
if (!node) {
throw new Error("Tried to remove a node that didn't exist");
}
const { pos } = node;

// The old 4-year-old way of doing this:
// const { pos } = node;
// Don't trigger patch network actions since these changes are purely presentational
node.ignoreRemove = true;
graph.remove(node);
createAndAddNode(key, { pos });
// graph.remove(node);
// createAndAddNode(key, { pos });

// new way that seems to work with no problems:
(node as any).setConnectables?.(patchNetwork.connectables.get(key)!);
});

// At this point, all nodes should be created/removed and have up-to-date `AudioConnectables`. We must now run through the list
Expand Down
51 changes: 31 additions & 20 deletions src/graphEditor/nodes/AudioConnectablesNode.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/**
* Defines a graph node that wraps an `AudioConnectables` instance. It
*/
import { LiteGraph } from 'litegraph.js';
import * as R from 'ramda';
import { type LGraphNode, LiteGraph } from 'litegraph.js';

import type {
LiteGraphLink,
Expand All @@ -20,35 +19,47 @@ export function LGAudioConnectables(this: any) {
}

LGAudioConnectables.prototype.setConnectables = function (
this: any,
this: LGraphNode,
connectables: AudioConnectables
) {
// Store the raw inputs and outputs for later direct access
this.connectables = connectables;
this.connectables.vcId = this.id.toString();
(this as any).connectables = connectables;
(this as any).connectables.vcId = this.id.toString();

if (connectables.node) {
this.title = (connectables.node as any).name;
// Clear existing inputs and outputs that aren't in the new connectables
//
// We don't want to delete all inputs and outputs and recreate them because that
// triggers patch network changes
let i = 0;
while (this.inputs && i < this.inputs.length) {
if (!connectables.inputs.has(this.inputs[i].name)) {
this.removeInput(i);
i = 0;
}
i += 1;
}
i = 0;
while (this.outputs && i < this.outputs.length) {
if (!connectables.outputs.has(this.outputs[i].name)) {
this.removeOutput(i);
i = 0;
}
i += 1;
}

[...connectables.inputs.entries()].forEach(([name, input]) => {
if (input.node instanceof AudioParam) {
this.addProperty(name, input.node.value, input.type);
this.addProperty(name, input.node, input.type);
const value = (connectables.node as any)?.node?.[name]?.value;
if (!R.isNil(value)) {
this.setProperty(name, value);
}
this.addInput(name, input.type === 'any' ? 0 : input.type);
} else {
this.addInput(name, input.type === 'any' ? 0 : input.type);
if (!this.inputs || !this.inputs.find(i => i.name === name)) {
this.addInput(name, input.type === 'any' ? (0 as any) : input.type);
}
});

[...connectables.outputs.entries()].forEach(([name, output]) => {
// TODO: Look up this type dynamically?
this.addOutput(name, output.type === 'any' ? 0 : output.type);
if (!this.outputs || !this.outputs.find(i => i.name === name)) {
this.addOutput(name, output.type === 'any' ? (0 as any) : output.type);
}
});

this.graph?.setDirtyCanvas(true, false);
};

LGAudioConnectables.prototype.onPropertyChanged = function (name: string, value: any) {
Expand Down Expand Up @@ -108,4 +119,4 @@ LGAudioConnectables.prototype.onConnectionsChange = function (
};

export const registerAudioConnectablesNode = () =>
LiteGraph.registerNodeType('audio/audioConnectables', LGAudioConnectables);
LiteGraph.registerNodeType('audio/audioConnectables', LGAudioConnectables as any);
4 changes: 4 additions & 0 deletions src/graphEditor/nodes/CustomAudio/CustomAudio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,10 @@ const registerCustomAudioNode = (
}
};

CustomAudioNode.prototype.onRemoved = function (this: any) {
this.onRemovedCustom?.();
};

// Whenever any of the properties of the LG node are changed, they trigger the value of the underlying
// `AudioNode`/`AudioParam` of the `ForeignNode`'s `AudioConnectables` to be set to the new value.
//
Expand Down
141 changes: 131 additions & 10 deletions src/graphEditor/nodes/CustomAudio/Subgraph/SubgraphPortalNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
import { Map as ImmMap } from 'immutable';
import { getEngine } from 'src/util';
import type { LGraphNode } from 'litegraph.js';
import { actionCreators, dispatch, getState } from 'src/redux';
import { actionCreators, dispatch, getState, store } from 'src/redux';
import DummyNode from 'src/graphEditor/nodes/DummyNode';
import { PlaceholderOutput } from 'src/controlPanel/PlaceholderOutput';
import { get, writable, type Writable } from 'svelte/store';
Expand All @@ -30,12 +30,134 @@ export type PortMap = {
[name: string]: { type: ConnectableType; node: AudioNode | MIDINode };
};

let watcherInitialized = false;
const maybeInitSubgraphConnectablesWatcher = () => {
if (watcherInitialized) {
return;
}
watcherInitialized = true;

let lastConnections = getState().viewContextManager.patchNetwork.connections;
store.subscribe(() => {
const connections = getState().viewContextManager.patchNetwork.connections;
if (connections === lastConnections) {
return;
}
lastConnections = connections;

let connectables = getState().viewContextManager.patchNetwork.connectables;
for (const [tx] of connections) {
const txConnectables = connectables.get(tx.vcId);
if (txConnectables?.outputs.get(tx.name)?.node instanceof PlaceholderOutput) {
continue;
}
const txSubgraphNode = txConnectables?.node;
const subgraphPortName = tx.name;

if (txSubgraphNode instanceof SubgraphPortalNode) {
// We've found a subgraph that is serving as a tx for a connection
//
// We need find the rx connectable on the corresponding portal, find the tx connected
// to it, and connect it to the rx here
const matchingSubgraphPortals = Array.from(connectables.values()).filter(
c =>
!!c.node &&
c.node instanceof SubgraphPortalNode &&
c.node.rxSubgraphID === txSubgraphNode.txSubgraphID
);

const matchingConns: {
portal: SubgraphPortalNode;
tx: ConnectableDescriptor;
rx: ConnectableDescriptor;
}[] = [];
for (const portal of matchingSubgraphPortals) {
matchingConns.push(
...connections
.filter(([_tx2, rx2]) => rx2.vcId === portal.vcId && rx2.name === subgraphPortName)
.map(([tx2, rx2]) => ({
portal: portal.node! as any as SubgraphPortalNode,
tx: tx2,
rx: rx2,
}))
);
}

const registeredOutputs = get(txSubgraphNode.registeredOutputs);
if (matchingConns.length === 0) {
// No inputs connected to the other side of this portal
//
// If we previously had a node patched through that has since been disconnected, replace
// the node with a dummy node
if (!(registeredOutputs[subgraphPortName]?.node instanceof DummyNode)) {
// console.log('Unpatching tx connectables node from subgraph portal', {
// subgraphPortName,
// txSubgraphNode,
// });
txSubgraphNode.registeredOutputs.update(inputs => {
const newInputs = { ...inputs };
newInputs[subgraphPortName] = {
type: newInputs[subgraphPortName].type,
node: new DummyNode(subgraphPortName),
};
return newInputs;
});
updateConnectables(txSubgraphNode.vcId, txSubgraphNode.buildConnectables());
}
continue;
} else if (matchingConns.length > 1) {
console.warn('Found multiple matching connections for a subgraph portal', matchingConns);
}
const matchingConn = matchingConns[0];

const wantedTxConnectable = connectables
.get(matchingConn.tx.vcId)
?.outputs.get(matchingConn.tx.name);
if (!wantedTxConnectable) {
console.warn('Could not find matching tx connectable for subgraph portal', matchingConn);
continue;
}

// If the node has already been patched in, don't do it again
if (registeredOutputs[subgraphPortName]?.node === wantedTxConnectable.node) {
continue;
} else if (!registeredOutputs[subgraphPortName]) {
console.warn('Could not find matching input for subgraph portal', {
matchingConn,
registeredOutputs,
subgraphPortName,
});
continue;
} else if (registeredOutputs[subgraphPortName].type !== wantedTxConnectable.type) {
console.warn('Type mismatch for subgraph portal', {
matchingConn,
registeredOutputs,
subgraphPortName,
});
continue;
}

// console.log('Patching tx connectables node into subgraph portal', {
// subgraphPortName,
// wantedTxConnectables: wantedTxConnectable,
// });
txSubgraphNode.registeredOutputs.update(inputs => ({
...inputs,
[subgraphPortName]: { type: wantedTxConnectable.type, node: wantedTxConnectable.node },
}));
updateConnectables(txSubgraphNode.vcId, txSubgraphNode.buildConnectables());
connectables = getState().viewContextManager.patchNetwork.connectables;
}
}
});
};

export class SubgraphPortalNode implements ForeignNode {
private vcId: string;
private txSubgraphID!: string;
private rxSubgraphID!: string;
private registeredInputs: Writable<PortMap> = writable({});
private registeredOutputs: Writable<PortMap> = writable({});
public vcId: string;
public txSubgraphID!: string;
public rxSubgraphID!: string;
public registeredInputs: Writable<PortMap> = writable({});
public registeredOutputs: Writable<PortMap> = writable({});
private placeholderInput: PlaceholderInput;
private placeholderOutput: PlaceholderOutput;

Expand All @@ -54,6 +176,9 @@ export class SubgraphPortalNode implements ForeignNode {
if (!vcId) {
throw new Error('`SubgraphPortalNode` must be created with a `vcId`');
}

maybeInitSubgraphConnectablesWatcher();

this.vcId = vcId;
this.deserialize(params);

Expand Down Expand Up @@ -206,10 +331,6 @@ export class SubgraphPortalNode implements ForeignNode {
for (const connectables of getState().viewContextManager.patchNetwork.connectables.values()) {
if (connectables.node && connectables.node instanceof SubgraphPortalNode) {
if (connectables.node.txSubgraphID === this.rxSubgraphID) {
console.log(connectables.node, {
thisVcId: this.vcId,
otherVcId: connectables.node.vcId,
});
connectables.node.registeredInputs.update(inputs => ({
...inputs,
[outputName]: { type, node: new DummyNode(rxConnectableDescriptor.name) },
Expand Down
1 change: 0 additions & 1 deletion src/midiEditor/EditableInstanceName.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
export let setName: (name: string) => void;
export let left: number | undefined = undefined;
export let right: number | undefined = undefined;
export let style: string | undefined = undefined;
let isEditingName = false;
let nameWrapperHovered = false;
Expand Down

0 comments on commit 422ff6a

Please sign in to comment.