Skip to content

Commit

Permalink
Implement recursive subgraph deletion
Browse files Browse the repository at this point in the history
  • Loading branch information
Ameobea committed Feb 11, 2024
1 parent 46cce6e commit 35518e4
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 20 deletions.
1 change: 1 addition & 0 deletions engine/engine/src/js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ extern "C" {
);
pub fn add_view_context(id: &str, name: &str, subgraph_id: &str);
pub fn add_foreign_connectable(fc_json: &str);
pub fn delete_foreign_connectable(id: &str);
pub fn delete_view_context(id: &str);
pub fn set_active_vc_id(new_id: &str);
pub fn set_subgraphs(active_subgraph_id: &str, subgraphs_by_id_json: &str);
Expand Down
159 changes: 150 additions & 9 deletions engine/engine/src/view_context/manager.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::str::FromStr;

use common::uuid_v4;
use fxhash::FxHashMap;
use fxhash::{FxHashMap, FxHashSet};
use serde::{Deserialize, Serialize};
use serde_json::json;
use uuid::Uuid;
Expand Down Expand Up @@ -391,14 +391,6 @@ impl ViewContextManager {
new_subgraph_id
}

pub fn delete_subgraph(&mut self, subgraph_id: Uuid) {
self
.active_view_history
.filter(|active_view| active_view.subgraph_id != subgraph_id);

todo!();
}

pub fn set_active_subgraph(&mut self, subgraph_id: Uuid, skip_history: bool) {
if self.active_subgraph_id == subgraph_id {
return;
Expand Down Expand Up @@ -443,6 +435,149 @@ impl ViewContextManager {
);
}

/// Recursively deletes the specified subgraph, all VCs and foreign connectables within it, and
/// all child subgraphs as determined by subgraph portals.
pub fn delete_subgraph(&mut self, subgraph_id_to_delete: Uuid) {
struct SubgraphConn {
pub tx: Uuid,
pub rx: Uuid,
pub portal_id: String,
}

// First, we determine the hierarchy of all subgraphs based on the subgraph portals
let mut all_subgraph_connections: Vec<SubgraphConn> = Vec::new();
for fc in &self.foreign_connectables {
if fc._type == "customAudio/subgraphPortal" {
let serialized_state = fc.serialized_state.as_ref().unwrap();
let tx_subgraph_id = Uuid::from_str(
serialized_state["txSubgraphID"]
.as_str()
.expect("txSubgraphID was not a string"),
)
.expect("txSubgraphID was not a valid UUID");
let rx_subgraph_id = Uuid::from_str(
serialized_state["rxSubgraphID"]
.as_str()
.expect("rxSubgraphID was not a string"),
)
.expect("rxSubgraphID was not a valid UUID");
all_subgraph_connections.push(SubgraphConn {
tx: tx_subgraph_id,
rx: rx_subgraph_id,
portal_id: fc.id.clone(),
});
}
}

// Now, we need to find all subgraphs that are children of the subgraph we're deleting
//
// However, there's a complication. All subgraph portals are bidirectional. We only want to
// delete subgraphs that are children of the subgraph we're deleting, not siblings or parents.
//
// To achieve this, we first do a BFS starting from the root subgraph to identify all subgraphs
// that are above the subgraph we're deleting.
//
// Then, we do a BFS starting from the subgraph we're deleting to identify all subgraphs that
// are below it.
let mut safe_subgraphs: FxHashSet<Uuid> = FxHashSet::default();
safe_subgraphs.insert(Uuid::nil());
let mut subgraphs_to_delete: FxHashSet<Uuid> = FxHashSet::default();
let mut queue: Vec<Uuid> = vec![Uuid::nil()];
while !queue.is_empty() {
let subgraph_id = queue.pop().unwrap();
for conn in &all_subgraph_connections {
if conn.tx == subgraph_id {
if conn.rx == subgraph_id_to_delete {
continue;
}

let is_new = safe_subgraphs.insert(conn.rx);
if is_new {
queue.push(conn.rx);
}
}
}
}

subgraphs_to_delete.insert(subgraph_id_to_delete);
queue.push(subgraph_id_to_delete);
while !queue.is_empty() {
let subgraph_id = queue.pop().unwrap();
for conn in &all_subgraph_connections {
if conn.tx == subgraph_id {
if subgraphs_to_delete.contains(&conn.rx) || safe_subgraphs.contains(&conn.rx) {
continue;
}

subgraphs_to_delete.insert(conn.rx);
queue.push(conn.rx);
}
}
}

// Find all VCs and FCs that are in the subgraphs we're deleting
let vcs_to_delete: Vec<Uuid> = self
.contexts
.iter()
.filter(|vc| subgraphs_to_delete.contains(&vc.definition.subgraph_id))
.map(|vc| vc.id)
.collect();
let mut fc_ids_to_delete: Vec<String> = self
.foreign_connectables
.iter()
.filter(|fc| subgraphs_to_delete.contains(&fc.subgraph_id))
.map(|fc| fc.id.clone())
.collect();

// Also delete all subgraph portals that point to any of the deleted subgraphs
for conn in &all_subgraph_connections {
if subgraphs_to_delete.contains(&conn.tx) || subgraphs_to_delete.contains(&conn.rx) {
fc_ids_to_delete.push(conn.portal_id.clone());
}
}
fc_ids_to_delete.sort_unstable();
fc_ids_to_delete.dedup();

// Delete all VCs and FCs that are in the subgraphs we're deleting
info!(
"About to delete {} VCs and {} FCs while deleting subgraph id={subgraph_id_to_delete}",
vcs_to_delete.len(),
fc_ids_to_delete.len(),
);
for vc_id in &vcs_to_delete {
self.delete_vc_by_id(*vc_id);
}
for fc_id in &fc_ids_to_delete {
self.delete_foreign_connectable_by_id(fc_id);
}

// Finally, delete the subgraphs themselves
for subgraph_id in &subgraphs_to_delete {
self.subgraphs_by_id.remove(&subgraph_id);
if self.active_subgraph_id == *subgraph_id {
self.active_subgraph_id = Uuid::nil();
}
}
info!(
"Deleted subgraph id={} along with {} other child subgraph(s)",
subgraph_id_to_delete,
subgraphs_to_delete.len() - 1
);

// Update view history to remove entries referencing deleted subgraphs or VCs
self.active_view_history.filter(|active_view| {
subgraphs_to_delete.contains(&active_view.subgraph_id)
|| vcs_to_delete.contains(&active_view.vc_id)
});

// Save the updated state
js::set_subgraphs(
&self.active_subgraph_id.to_string(),
&serde_json::to_string(&self.subgraphs_by_id).unwrap(),
);
self.save_all();
}

fn set_view(&mut self, subgraph_id: Uuid, vc_id: Uuid) -> Result<(), ()> {
if self.subgraphs_by_id.contains_key(&subgraph_id)
&& self.contexts.iter().any(|vc| vc.id == vc_id)
Expand Down Expand Up @@ -560,6 +695,12 @@ impl ViewContextManager {
self.save_all();
}

fn delete_foreign_connectable_by_id(&mut self, id: &str) {
self.foreign_connectables.retain(|fc| fc.id != id);
js::delete_foreign_connectable(&id.to_string());
self.save_all();
}

fn serialize(&self) -> ViewContextManagerState {
// TODO: Actually make use of the `touched` flag optimization here.
let mut view_context_definitions = Vec::new();
Expand Down
6 changes: 4 additions & 2 deletions src/controls/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ export function renderModalWithControls<T>(
});
}

export function renderSvelteModalWithControls<T>(
export function renderSvelteModalWithControls<T, Props extends ModalCompProps<T>>(
Comp: typeof SvelteComponent<ModalCompProps<T>>,
clickBackdropToClose = true
clickBackdropToClose = true,
extraProps?: Partial<Omit<Props, 'onSubmit' | 'onCancel'>>
): Promise<T> {
const bodyNode = document.getElementsByTagName('body')[0]!;
const modalNode = document.createElement('div');
Expand Down Expand Up @@ -90,6 +91,7 @@ export function renderSvelteModalWithControls<T>(
unmount();
reject();
},
...(extraProps || {}),
},
});
});
Expand Down
49 changes: 43 additions & 6 deletions src/graphEditor/GraphEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { updateGraph } from 'src/graphEditor/graphDiffing';
import { LGAudioConnectables } from 'src/graphEditor/nodes/AudioConnectablesNode';
import FlatButton from 'src/misc/FlatButton';
import { actionCreators, dispatch, getState, type ReduxStore } from 'src/redux';
import { UnreachableError, filterNils, getEngine, tryParseJson } from 'src/util';
import { NIL_UUID, UnreachableError, filterNils, getEngine, tryParseJson } from 'src/util';
import { ViewContextDescriptors } from 'src/ViewContextManager/AddModulePicker';
import {
getIsVcHidden,
Expand All @@ -33,9 +33,32 @@ import type { AudioConnectables } from 'src/patchNetwork';
import { audioNodeGetters, buildNewForeignConnectableID } from 'src/graphEditor/nodes/CustomAudio';
import { removeNode } from 'src/patchNetwork/interface';
import { handleGlobalMouseDown } from 'src';
import { SubgraphPortalNode } from 'src/graphEditor/nodes/CustomAudio/Subgraph/SubgraphPortalNode';
import { renderSvelteModalWithControls } from 'src/controls/Modal';
import ConfirmReset from 'src/sampler/SamplerUI/ConfirmReset.svelte';
import type { SveltePropTypesOf } from 'src/svelteUtils';

const ctx = new AudioContext();

const confirmAndDeleteSubgraph = async (subgraphID: string) => {
const subgraphName = getState().viewContextManager.subgraphsByID[subgraphID]?.name ?? 'Unknown';
try {
await renderSvelteModalWithControls<void, SveltePropTypesOf<typeof ConfirmReset>>(
ConfirmReset,
true,
{
message: `Are you sure you want to delete the subgraph "${subgraphName}"?`,
cancelMessage: 'Cancel',
resetMessage: 'Delete',
}
);
} catch (_err) {
return; // cancelled
}

getEngine()!.delete_subgraph(subgraphID);
};

LGraphCanvas.prototype.getCanvasMenuOptions = () => [];
const oldGetNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions;
LGraphCanvas.prototype.getNodeMenuOptions = function (node: LGraphNode) {
Expand Down Expand Up @@ -73,16 +96,30 @@ LGraphCanvas.prototype.getNodeMenuOptions = function (node: LGraphNode) {
return true;
});

// Patch the remove option to delete the node directly from the patch network
const removeOption = filteredOptions.find(opt => opt?.content === 'Remove');
if (!removeOption) {
throw new Error('Failed to find "Remove" option in node menu');
}

removeOption.callback = (_value, _options, _event, _parentMenu, node) => {
const vcId = node.id.toString();
removeNode(vcId);
};
const innerNode = ((node as any).connectables as AudioConnectables | undefined)?.node;
if (innerNode && innerNode instanceof SubgraphPortalNode) {
// If this portal is linking to the root node, don't allow deletion
if (innerNode.rxSubgraphID === NIL_UUID) {
filteredOptions = filteredOptions.filter(opt => opt?.content !== 'Remove');
} else {
// Replace the "Remove" option with "Delete Subgraph"
removeOption.content = 'Delete Subgraph';
removeOption.callback = () =>
void confirmAndDeleteSubgraph((innerNode as SubgraphPortalNode).rxSubgraphID);
}
} else {
// Patch the remove option to delete the node directly from the patch network

removeOption.callback = (_value, _options, _event, _parentMenu, node) => {
const vcId = node.id.toString();
removeNode(vcId);
};
}

return filteredOptions;
};
Expand Down
10 changes: 7 additions & 3 deletions src/sampler/SamplerUI/ConfirmReset.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
<script lang="ts">
export let onSubmit: () => void;
export let onCancel: () => void;
export let message = 'Are you sure you want to reset completely to scratch?';
export let resetMessage = 'Reset';
export let cancelMessage = 'Cancel';
</script>

<div class="root">
<p>Are you sure you want to reset completely to scratch?</p>
<p>{message}</p>
<div class="buttons-container">
<button on:click={onSubmit}>Reset</button>
<button on:click={onCancel}>Cancel</button>
<button on:click={onSubmit}>{resetMessage}</button>
<button on:click={onCancel}>{cancelMessage}</button>
</div>
</div>

Expand Down
2 changes: 2 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ export const initGlobals = () => {
};
};

export const NIL_UUID = '00000000-0000-0000-0000-000000000000';

export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type ValueOf<T> = T[keyof T];
export type ArrayElementOf<T> = T extends (infer U)[] ? U : never;
Expand Down
4 changes: 4 additions & 0 deletions src/vcInterop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ export const add_foreign_connectable = (fcJSON: string) => {
);
};

export const delete_foreign_connectable = (id: string) => {
dispatch(actionCreators.viewContextManager.REMOVE_PATCH_NETWORK_NODE(id));
};

export const delete_view_context = (id: string) => {
dispatch(actionCreators.viewContextManager.REMOVE_PATCH_NETWORK_NODE(id));
dispatch(actionCreators.viewContextManager.DELETE_VIEW_CONTEXT(id));
Expand Down

0 comments on commit 35518e4

Please sign in to comment.