use super::{DEFAULT_CONNECTION, EdgeTarget, NodeMap, PendingEdge};
use crate::{context::AudioContext, node::FirewheelNode};
use bevy::prelude::*;
#[cfg(debug_assertions)]
use core::panic::Location;
#[derive(Debug, Default, Component)]
pub struct PendingConnections(Vec<PendingEdge>);
impl PendingConnections {
pub fn push(&mut self, connection: PendingEdge) {
self.0.push(connection)
}
}
pub trait Connect<'a>: Sized {
#[cfg_attr(debug_assertions, track_caller)]
#[inline]
fn connect(self, target: impl Into<EdgeTarget>) -> ConnectCommands<'a> {
self.connect_with(target, DEFAULT_CONNECTION)
}
#[cfg_attr(debug_assertions, track_caller)]
fn connect_with(
self,
target: impl Into<EdgeTarget>,
ports: &[(u32, u32)],
) -> ConnectCommands<'a>;
#[cfg_attr(debug_assertions, track_caller)]
#[inline]
fn chain_node<B: Bundle>(self, node: B) -> ConnectCommands<'a> {
self.chain_node_with(node, DEFAULT_CONNECTION)
}
#[cfg_attr(debug_assertions, track_caller)]
fn chain_node_with<B: Bundle>(self, node: B, ports: &[(u32, u32)]) -> ConnectCommands<'a>;
#[must_use]
fn head(&self) -> Entity;
#[must_use]
fn tail(&self) -> Entity;
}
impl<'a> Connect<'a> for EntityCommands<'a> {
fn connect_with(
mut self,
target: impl Into<EdgeTarget>,
ports: &[(u32, u32)],
) -> ConnectCommands<'a> {
let target = target.into();
let ports = ports.to_vec();
#[cfg(debug_assertions)]
let location = Location::caller();
self.entry::<PendingConnections>()
.or_default()
.and_modify(|mut pending| {
pending.push(PendingEdge::new_with_location(
target,
Some(ports),
#[cfg(debug_assertions)]
location,
));
});
ConnectCommands::new(self)
}
fn chain_node_with<B: Bundle>(mut self, node: B, ports: &[(u32, u32)]) -> ConnectCommands<'a> {
let new_id = self.commands().spawn(node).id();
let mut new_connection = self.connect_with(new_id, ports);
new_connection.head = new_id;
new_connection
}
#[inline(always)]
fn head(&self) -> Entity {
self.id()
}
#[inline(always)]
fn tail(&self) -> Entity {
self.id()
}
}
impl<'a> Connect<'a> for ConnectCommands<'a> {
#[cfg_attr(debug_assertions, track_caller)]
fn connect_with(
mut self,
target: impl Into<EdgeTarget>,
ports: &[(u32, u32)],
) -> ConnectCommands<'a> {
let tail = self.tail();
let mut commands = self.commands.commands();
let mut commands = commands.entity(tail);
let target = target.into();
let ports = ports.to_vec();
#[cfg(debug_assertions)]
let location = Location::caller();
commands
.entry::<PendingConnections>()
.or_default()
.and_modify(|mut pending| {
pending.push(PendingEdge::new_with_location(
target,
Some(ports),
#[cfg(debug_assertions)]
location,
));
});
self
}
fn chain_node_with<B: Bundle>(mut self, node: B, ports: &[(u32, u32)]) -> ConnectCommands<'a> {
let new_id = self.commands.commands().spawn(node).id();
let mut new_connection = self.connect_with(new_id, ports);
new_connection.tail = Some(new_id);
new_connection
}
#[inline(always)]
fn head(&self) -> Entity {
<Self>::head(self)
}
#[inline(always)]
fn tail(&self) -> Entity {
<Self>::tail(self)
}
}
pub struct ConnectCommands<'a> {
commands: EntityCommands<'a>,
head: Entity,
tail: Option<Entity>,
}
impl<'a> ConnectCommands<'a> {
pub(crate) fn new(commands: EntityCommands<'a>) -> Self {
Self {
head: commands.id(),
tail: None,
commands,
}
}
fn head(&self) -> Entity {
self.head
}
fn tail(&self) -> Entity {
self.tail.unwrap_or(self.head)
}
}
impl core::fmt::Debug for ConnectCommands<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConnectCommands")
.field("entity", &self.head)
.finish_non_exhaustive()
}
}
pub(crate) fn process_connections(
mut connections: Query<(&mut PendingConnections, &FirewheelNode)>,
targets: Query<&FirewheelNode>,
node_map: Res<NodeMap>,
mut context: ResMut<AudioContext>,
) {
let connections = connections
.iter_mut()
.filter(|(pending, _)| !pending.0.is_empty())
.collect::<Vec<_>>();
if connections.is_empty() {
return;
}
context.with(|context| {
for (mut pending, source_node) in connections.into_iter() {
pending.0.retain(|connection| {
let ports = connection.ports.as_deref().unwrap_or(DEFAULT_CONNECTION);
let target_entity = match connection.target {
EdgeTarget::Entity(entity) => entity,
EdgeTarget::Label(label) => {
let Some(entity) = node_map.get(&label) else {
#[cfg(debug_assertions)]
{
let location = connection.origin;
error_once!("failed to connect to node label `{label:?}` at {location}: no associated Firewheel node found");
}
#[cfg(not(debug_assertions))]
error_once!("failed to connect to node label `{label:?}`: no associated Firewheel node found");
return true;
};
*entity
}
EdgeTarget::Node(dest_node) => {
if let Err(e) = context.connect(source_node.0, dest_node, ports, false) {
error_once!("failed to connect audio node to target: {e}");
}
return false;
}
};
let target = match targets.get(target_entity) {
Ok(t) => t,
Err(_) => {
#[cfg(debug_assertions)]
{
let location = connection.origin;
error_once!("failed to connect to entity `{target_entity:?}` at {location}: no Firewheel node found");
}
#[cfg(not(debug_assertions))]
error_once!("failed to connect to entity `{target_entity:?}`: no Firewheel node found");
return false;
}
};
if let Err(e) = context.connect(source_node.0, target.0, ports, false) {
error_once!("failed to connect audio node to target: {e}");
}
false
});
}
});
}
#[cfg(test)]
mod test {
use crate::{context::AudioContext, prelude::MainBus, test::prepare_app};
use super::*;
use bevy::ecs::system::RunSystemOnce;
use firewheel::nodes::volume::VolumeNode;
#[derive(Component)]
struct One;
#[derive(Component)]
struct Two;
#[derive(Component)]
struct Three;
#[test]
fn test_chain() {
let mut app = prepare_app(|mut commands: Commands| {
commands
.spawn((VolumeNode::default(), One))
.chain_node((VolumeNode::default(), Two))
.connect(MainBus);
});
app.world_mut()
.run_system_once(
|mut context: ResMut<AudioContext>,
one: Single<&FirewheelNode, With<One>>,
two: Single<&FirewheelNode, With<Two>>,
main: Single<&FirewheelNode, With<MainBus>>| {
let one = one.into_inner();
let two = two.into_inner();
let main = main.into_inner();
context.with(|context| {
assert_eq!(context.nodes().len(), 5);
let outgoing_edges_one: Vec<_> = context
.edges()
.into_iter()
.filter(|e| e.src_node == one.0)
.collect();
let outgoing_edges_two: Vec<_> = context
.edges()
.into_iter()
.filter(|e| e.src_node == two.0)
.collect();
assert_eq!(outgoing_edges_one.len(), 2);
assert_eq!(outgoing_edges_two.len(), 2);
assert!(outgoing_edges_one.iter().all(|e| e.dst_node == two.0));
assert!(outgoing_edges_two.iter().all(|e| e.dst_node == main.0));
});
},
)
.unwrap();
}
#[test]
fn test_fanout() {
let mut app = prepare_app(|mut commands: Commands| {
let a = commands.spawn((VolumeNode::default(), One)).head();
let b = commands.spawn((VolumeNode::default(), Two)).head();
commands
.spawn((VolumeNode::default(), Three))
.connect(a)
.connect(b);
});
app.world_mut()
.run_system_once(
|mut context: ResMut<AudioContext>,
one: Single<&FirewheelNode, With<One>>,
two: Single<&FirewheelNode, With<Two>>,
three: Single<&FirewheelNode, With<Three>>| {
let one = one.into_inner();
let two = two.into_inner();
let three = three.into_inner();
context.with(|context| {
assert_eq!(context.nodes().len(), 6);
let outgoing_edges_three: Vec<_> = context
.edges()
.into_iter()
.filter(|e| e.src_node == three.0)
.collect();
assert_eq!(
outgoing_edges_three
.iter()
.filter(|e| e.dst_node == one.0)
.count(),
2
);
assert_eq!(
outgoing_edges_three
.iter()
.filter(|e| e.dst_node == two.0)
.count(),
2
);
});
},
)
.unwrap();
}
}