use std::fs::File;
use std::path::Path;
use std::collections::BTreeSet;
use uuid::Uuid;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize, Serializer, de::Deserializer, ser::SerializeSeq};
use anyhow::{Result, anyhow};
#[cfg(feature = "egui")]
use crate::ui::*;
use crate::*;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EditorState {
size: emath::Vec2,
origin: emath::Vec2,
zoom: f32,
scroll_offset: emath::Vec2,
#[serde(skip)]
graph_pointer_pos: Option<emath::Vec2>,
#[serde(skip)]
add_node_at: Option<emath::Vec2>,
}
impl Default for EditorState {
fn default() -> Self {
let size = emath::vec2(10000.0, 10000.0);
let origin = size / 2.0;
Self {
size,
origin,
zoom: 0.5,
scroll_offset: origin - emath::vec2(450., 250.),
graph_pointer_pos: None,
add_node_at: None,
}
}
}
#[cfg(feature = "egui")]
impl EditorState {
fn get_zoomed(&self) -> (emath::Vec2, emath::Vec2, emath::Vec2, f32) {
let mut size = self.size;
let mut origin = self.origin;
let mut scroll_offset = self.scroll_offset;
size.zoom(self.zoom);
origin.zoom(self.zoom);
scroll_offset.zoom(self.zoom);
(size, origin, scroll_offset, self.zoom)
}
}
pub trait GetId {
fn id(&self) -> Uuid;
}
#[derive(Clone, Debug)]
struct IdMap<V>(pub(crate) IndexMap<Uuid, V>);
impl<V> Default for IdMap<V> {
fn default() -> Self {
Self(Default::default())
}
}
impl<V> Serialize for IdMap<V>
where
V: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
for n in self.0.values() {
seq.serialize_element(n)?;
}
seq.end()
}
}
impl<'de, V> Deserialize<'de> for IdMap<V>
where
V: GetId + serde::de::DeserializeOwned,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let nodes = Vec::<V>::deserialize(deserializer)?;
Ok(Self(nodes.into_iter().map(|n| (n.id(), n)).collect()))
}
}
#[derive(Clone, Default, Debug)]
struct ConnectionMap(pub(crate) IndexMap<InputId, OutputId>);
impl Serialize for ConnectionMap {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[derive(Serialize)]
struct Connection<'a> {
input: &'a InputId,
output: &'a OutputId,
}
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
for (input, output) in &self.0 {
seq.serialize_element(&Connection { input, output })?;
}
seq.end()
}
}
impl<'de> Deserialize<'de> for ConnectionMap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Connection {
input: InputId,
output: OutputId,
}
let connections = Vec::<Connection>::deserialize(deserializer)?;
Ok(Self(
connections
.into_iter()
.map(|c| (c.input, c.output))
.collect(),
))
}
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct NodeGraphProperty {
id: Uuid,
name: String,
description: String,
value: Value,
}
impl GetId for NodeGraphProperty {
fn id(&self) -> Uuid {
self.id
}
}
#[derive(Clone, Debug)]
pub struct NodeFinder {
pub registry: NodeRegistry,
pub node_filter: NodeFilter,
open: bool,
open_at: Option<emath::Pos2>,
}
impl Default for NodeFinder {
fn default() -> Self {
Self {
registry: NodeRegistry::build(),
node_filter: Default::default(),
open: false,
open_at: None,
}
}
}
impl NodeFinder {
pub fn open_at(&mut self, pos: emath::Pos2) {
self.open_at = Some(pos);
self.open = true;
}
pub fn close(&mut self) {
self.open = false;
self.open_at = None;
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<Node> {
if !self.open {
return None;
}
let mut area = egui::Area::new("NodeFinder".into());
if let Some(pos) = self.open_at.take() {
area = area.current_pos(pos);
}
let node = area.show(ui.ctx(), |ui| self.frame_ui(ui)).inner;
if node.is_some() {
self.close();
}
node
}
fn frame_ui(&mut self, ui: &mut egui::Ui) -> Option<Node> {
let style = ui.style();
let mut frame = egui::Frame::window(style);
frame.shadow = Default::default();
let mut node = None;
frame.show(ui, |ui| {
ui.vertical(|ui| {
ui.label("Create node");
self.node_filter.ui(ui);
node = self.registry.ui(ui, &self.node_filter);
});
});
node
}
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
struct MenuState {
hover_connection: Option<InputId>,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
struct DetailPanelState {
pub selected_node: Option<NodeId>,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct NodeGraph {
id: Uuid,
editor: EditorState,
properties: IdMap<NodeGraphProperty>,
nodes: IdMap<Node>,
groups: IdMap<NodeGroup>,
connections: ConnectionMap,
output: Option<NodeId>,
#[serde(skip)]
changed: usize,
#[serde(skip)]
hover_connection: Option<InputId>,
#[serde(skip)]
menu_state: Option<MenuState>,
#[serde(skip)]
details_state: DetailPanelState,
#[serde(skip)]
#[cfg(feature = "egui")]
ui_state: NodeGraphMeta,
#[serde(skip)]
node_finder: NodeFinder,
}
impl NodeGraph {
pub fn new() -> Self {
Self {
id: Uuid::new_v4(),
..Self::default()
}
}
pub fn add_group(&mut self, mut group: NodeGroup) -> NodeGroupId {
if self.groups.0.contains_key(&group.id) {
group.id = Uuid::new_v4();
}
let id = group.id;
self.groups.0.insert(id, group);
id
}
pub fn remove_group(&mut self, group_id: NodeGroupId, delete_nodes: bool) {
self.groups.0.shift_remove(&group_id);
if delete_nodes {
let mut nodes = Vec::new();
for (node_id, node) in &mut self.nodes.0 {
if node.group_id == group_id {
nodes.push(*node_id);
}
}
for node_id in nodes {
self.remove(node_id);
}
} else {
for (_, node) in &mut self.nodes.0 {
if node.group_id == group_id {
node.group_id = Uuid::nil();
}
}
}
}
pub fn resize_group(&mut self, group_id: NodeGroupId) {
if let Some(group) = self.groups.0.get_mut(&group_id) {
let mut area = emath::Rect::NOTHING;
for (_, node) in &mut self.nodes.0 {
if node.group_id == group_id {
area = area.union(node.rect());
}
}
group.set_area(area);
}
}
pub fn changed_counter(&self) -> usize {
self.changed
}
fn updated(&mut self) {
self.changed += 1;
}
pub fn add(&mut self, mut node: Node) -> NodeId {
self.updated();
if let Some(position) = &self.editor.add_node_at {
node.set_position(*position);
}
if self.contains(node.id()) {
node.new_id();
}
let id = node.id();
self.nodes.0.insert(id, node);
id
}
pub fn remove(&mut self, id: NodeId) -> Option<Node> {
self.updated();
self.connections.0.retain(|input, output| {
if output.node() == id {
let node = self.nodes.0.get_mut(&input.node());
if let Some(node) = node {
if let Err(err) = node.set_input(*input, Input::Disconnect) {
log::warn!("Failed to disconnect from input node: {err:?}");
}
}
false
} else if input.node() == id {
false
} else {
true
}
});
#[cfg(feature = "egui")]
{
self.ui_state.remove_node(id);
}
self.nodes.0.shift_remove(&id)
}
pub fn contains(&self, id: NodeId) -> bool {
self.nodes.0.contains_key(&id)
}
pub fn get_input_id<I: Into<InputKey>>(&self, id: NodeId, idx: I) -> Result<InputId> {
let node = self.get(id)?;
let idx = node.get_input_idx(&idx.into())?;
Ok(InputId::new(id, idx))
}
pub fn get_node_input<I: Into<InputKey>>(&self, id: NodeId, idx: I) -> Result<Input> {
self.get(id).and_then(|n| n.get_input(idx.into()))
}
pub fn set_node_input<I: Into<InputKey>>(
&mut self,
id: NodeId,
key: I,
value: Input,
) -> Result<Option<OutputId>> {
let key = key.into();
let node = self
.nodes
.0
.get_mut(&id)
.ok_or_else(|| anyhow!("Missing node: {id:?}"))?;
let input_id = node.get_input_idx(&key).map(|idx| InputId::new(id, idx))?;
match &value {
Input::Disconnect => {
if self.connections.0.shift_remove(&input_id).is_none() {
return Ok(None);
}
}
Input::Connect(output_id, _) => {
self.connections.0.insert(input_id, *output_id);
}
_ => {}
}
let old = node.set_input(key, value.clone())?;
self.updated();
Ok(old)
}
pub fn set_input(&mut self, input_id: InputId, value: Input) -> Result<Option<OutputId>> {
self.set_node_input(input_id.node(), input_id, value)
}
pub fn disconnect(&mut self, input: InputId) -> Result<()> {
self.set_input(input, Input::Disconnect)?;
Ok(())
}
pub fn connect(&mut self, input: InputId, output: OutputId, dt: DataType) -> Result<()> {
self.set_input(input, Input::Connect(output, Some(dt)))?;
Ok(())
}
pub fn get(&self, id: NodeId) -> Result<&Node> {
self
.nodes
.0
.get(&id)
.ok_or_else(|| anyhow!("Missing node: {id:?}"))
}
pub fn get_mut(&mut self, id: NodeId) -> Result<&mut Node> {
self.updated();
self
.nodes
.0
.get_mut(&id)
.ok_or_else(|| anyhow!("Missing node: {id:?}"))
}
pub fn set_output(&mut self, output: Option<NodeId>) {
self.updated();
self.output = output;
}
pub fn output(&self) -> Option<NodeId> {
self.output
}
}
#[cfg(feature = "egui")]
impl NodeGraph {
pub fn has_selected(&self) -> bool {
self.ui_state.has_selected()
}
pub fn hover_connection(&self) -> Option<InputId> {
self.hover_connection
}
pub fn open_node_finder(&mut self, ui: &egui::Ui) {
if let Some(pos) = ui.ctx().pointer_latest_pos() {
self.editor.add_node_at = self.editor.graph_pointer_pos;
self.node_finder.open_at(pos);
}
}
pub fn group_selected_nodes(&mut self) -> Option<NodeGroupId> {
let mut group = NodeGroup::new();
let mut empty = true;
for node_id in self.ui_state.take_selected() {
if let Some(node) = self.nodes.0.get_mut(&node_id) {
group.add_node(node);
empty = false;
}
}
if empty {
None
} else {
let id = group.id;
self.groups.0.insert(id, group);
Some(id)
}
}
pub fn select_node(&mut self, id: NodeId, select: bool) {
self.ui_state.frame_state_mut(id, |frame| {
frame.selected = select;
});
}
pub fn show(&mut self, ui: &mut egui::Ui) {
self.show_details(ui);
self.show_graph(ui);
}
pub fn show_details(&mut self, ui: &mut egui::Ui) {
egui::SidePanel::right("graph_details_panel")
.min_width(150.0)
.resizable(false)
.show_inside(ui, |ui| self.details_ui(ui));
}
pub fn details_ui(&mut self, ui: &mut egui::Ui) {
let mut updated = false;
if let Some(id) = self.details_state.selected_node {
if let Some(node) = self.nodes.0.get_mut(&id) {
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label("Name:");
ui.text_edit_singleline(&mut node.name);
});
if node.details_ui(ui, id) {
updated = true;
}
});
}
} else {
ui.label("Click node to view/edit details");
}
if updated {
self.updated();
}
}
pub fn show_graph(&mut self, ui: &mut egui::Ui) {
egui::CentralPanel::default().show_inside(ui, |ui| self.graph_ui(ui));
}
fn handle_clicked(&mut self, clear_on_click: bool) {
if clear_on_click {
self.details_state.selected_node = None;
self.ui_state.clear_selected();
}
}
pub fn graph_ui(&mut self, ui: &mut egui::Ui) {
if let Some(node) = self.node_finder.ui(ui) {
self.add(node);
}
let mut scrolling = true;
let mut selecting = true;
let mut clear_selected = true;
ui.input(|i| {
if i.modifiers.ctrl {
selecting = false;
}
if i.pointer.secondary_down() {
clear_selected = false;
scrolling = false;
}
if i.modifiers.shift {
clear_selected = false;
}
});
if ui.ui_contains_pointer() {
let z_delta = ui.input(|i| {
let scroll_delta = i.raw_scroll_delta.y;
if scroll_delta > 0.1 {
0.01
} else if scroll_delta < -0.1 {
-0.01
} else {
i.zoom_delta() - 1.0
}
});
if z_delta != 0.0 {
let zoom = (self.editor.zoom + z_delta).clamp(0.1, 1.0);
self.editor.zoom = zoom;
scrolling = false;
}
}
let (size, origin, scroll_offset, zoom) = self.editor.get_zoomed();
let scroll_area = egui::ScrollArea::both()
.scroll_source(egui::containers::scroll_area::ScrollSource {
scroll_bar: true,
drag: scrolling,
mouse_wheel: false,
})
.scroll_offset(scroll_offset);
let out = scroll_area.show(ui, |ui| {
let id = ui.next_auto_id();
let old_node_style = NodeStyle::get(ui);
let node_style = NodeStyle::zoom_style(ui, zoom);
ui.set_width(size.x);
ui.set_height(size.y);
let ui_min = ui.min_rect().min.to_vec2();
let origin = origin + ui_min;
let state = self.ui_state.clone();
state.load(ui, origin, ui_min, zoom);
let mut pointer_pos = emath::Pos2::default();
if let Some(pos) = ui.ctx().pointer_latest_pos() {
pointer_pos = pos;
if ui.ui_contains_pointer() {
self.editor.graph_pointer_pos = Some((pos - origin).to_vec2() / zoom);
}
}
let mut area_resp = None;
let mut select_state = None;
if selecting {
let rect = ui.available_rect_before_wrap();
let resp = ui.interact(rect, id, egui::Sense::click_and_drag());
if resp.clicked() {
self.handle_clicked(clear_selected);
}
state.selecting_mut(|selecting| {
if resp.drag_started() {
selecting.drag_started(pointer_pos, clear_selected);
self.node_finder.close();
} else if resp.drag_stopped() {
selecting.drag_released();
} else {
selecting.update(pointer_pos);
}
select_state = Some(selecting.clone());
});
area_resp = Some(resp);
}
let mut remove_group = None;
let mut resize_groups = BTreeSet::new();
let mut clicked_group = None;
for (group_id, group) in &mut self.groups.0 {
match state.render(ui, group) {
Some(NodeAction::Dragged(delta)) => {
let delta = delta / zoom;
for (_, node) in &mut self.nodes.0 {
if node.group_id == *group_id {
node.handle_move(delta);
}
}
}
Some(NodeAction::Clicked) => {
clicked_group = Some(*group_id);
}
Some(NodeAction::Delete(nodes)) => {
remove_group = Some((*group_id, nodes));
}
Some(NodeAction::JoinGroup(group_id)) => {
for node_id in self.ui_state.take_selected() {
if let Some(node) = self.nodes.0.get_mut(&node_id) {
node.group_id = group_id;
}
}
resize_groups.insert(group_id);
}
_ => (),
}
}
if let Some(group_id) = clicked_group {
self.handle_clicked(clear_selected);
self.select_node(group_id, true);
}
if let Some((group_id, remove_nodes)) = remove_group {
self.remove_group(group_id, remove_nodes);
}
let connection_style = NodeConnection::new(&node_style, ui_min);
self.render_connections(ui, id, &state, connection_style);
let mut remove_node = None;
let mut updated = false;
let mut clicked_node = None;
for (node_id, node) in &mut self.nodes.0 {
match state.render(ui, node) {
Some(NodeAction::Dragged(_) | NodeAction::Resize) => {
if !node.group_id.is_nil() {
resize_groups.insert(node.group_id);
}
}
Some(NodeAction::Clicked) => {
clicked_node = Some(*node_id);
}
Some(NodeAction::Delete(_)) => {
remove_node = Some(*node_id);
}
Some(NodeAction::LeaveGroup(group_id)) => {
resize_groups.insert(group_id);
}
_ => (),
}
updated |= node.updated;
}
if let Some(node_id) = clicked_node {
self.handle_clicked(clear_selected);
self.details_state.selected_node = Some(node_id);
self.select_node(node_id, true);
}
let outputs = state.take_updated_outputs();
if outputs.len() > 0 {
for (input, output) in self.connections.0.iter() {
if outputs.contains(output) {
if let Some(node) = self.nodes.0.get_mut(&input.node()) {
node.updated = true;
}
}
}
}
if updated {
self.updated();
}
if let Some(node_id) = remove_node {
self.remove(node_id);
}
for group_id in resize_groups {
self.resize_group(group_id);
}
state.unload(ui);
if let Some(selecting) = select_state {
selecting.ui(ui);
}
old_node_style.unzoom_style(ui, zoom);
area_resp
});
self.editor.scroll_offset = out.state.offset / zoom;
if let Some(resp) = out.inner {
resp.context_menu(|ui| self.context_menu(ui));
if !ui.ctx().is_popup_open() {
self.menu_state = None;
}
}
}
fn context_menu(&mut self, ui: &mut egui::Ui) {
let state = self
.menu_state
.get_or_insert_with(|| MenuState {
hover_connection: self.hover_connection,
})
.clone();
if ui.button("Create node").clicked() {
self.open_node_finder(ui);
ui.close_kind(egui::UiKind::Menu);
}
if self.has_selected() && ui.button("Group Nodes").clicked() {
self.group_selected_nodes();
ui.close_kind(egui::UiKind::Menu);
}
if let Some(input) = state.hover_connection {
if ui.button("Delete connection").clicked() {
if let Err(err) = self.disconnect(input) {
log::error!("Failed to delete connection: {err:?}");
}
ui.close_kind(egui::UiKind::Menu);
}
}
}
fn render_connections(
&mut self,
ui: &mut egui::Ui,
id: egui::Id,
state: &NodeGraphMeta,
conn: NodeConnection,
) {
state.drag_state_mut(|drag| {
if ui.ctx().drag_stopped_id() == Some(id) {
ui.ctx().stop_dragging();
if let Some((src, dst)) = drag.take_sockets() {
if let Some((dst, dt)) = dst {
if let Err(err) = self.connect(src, dst, dt) {
log::warn!("Failed to connect input[{src:?}] to output[{dst:?}]: {err:?}");
}
} else {
if let Err(err) = self.disconnect(src) {
log::warn!("Failed to disconnect input[{src:?}]: {err:?}");
}
}
}
} else if let Some(src) = &drag.src {
ui.ctx().set_dragged_id(id);
let dst = if let Some(dst) = &drag.dst {
ui.ctx().set_cursor_icon(egui::CursorIcon::Grab);
Some((conn.to_ui_pos(dst.center), dst.color))
} else if let Some(end) = ui.ctx().pointer_latest_pos() {
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
if let Some(last) = drag.pointer_last_pos {
let delta = (last - end) * 2.0;
ui.scroll_with_delta(delta);
}
drag.pointer_last_pos = Some(end);
Some((end, src.color))
} else {
None
};
if let Some((dst, color)) = dst {
let (start, end, color) = if let Some(input_id) = src.id.as_input_id() {
if let Err(err) = self.disconnect(input_id) {
log::warn!("Failed to disconnect input[{input_id:?}]: {err:?}");
}
(conn.to_ui_pos(src.center), dst, color)
} else {
(dst, conn.to_ui_pos(src.center), color)
};
conn.draw(ui, start, end, Some(color), false);
}
}
});
self.hover_connection = None;
for (input, output) in &self.connections.0 {
let meta = state.get_connection_meta(input, output);
if let Some((in_meta, out_meta)) = meta {
let start = conn.to_ui_pos(in_meta.center);
let end = conn.to_ui_pos(out_meta.center);
if conn
.draw(ui, start, end, Some(out_meta.color), true)
.is_some()
{
self.hover_connection = Some(*input);
}
}
}
}
}
#[derive(Clone)]
#[cfg(feature = "egui")]
pub struct NodeGraphEditor {
pub title: String,
pub size: emath::Vec2,
pub graph: NodeGraph,
}
#[cfg(feature = "egui")]
impl Default for NodeGraphEditor {
fn default() -> Self {
Self {
title: "Graph editor".to_string(),
size: (900., 500.).into(),
graph: Default::default(),
}
}
}
#[cfg(feature = "egui")]
impl NodeGraphEditor {
pub fn new() -> Self {
Self::default()
}
pub fn load<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let path = path.as_ref();
let file = File::open(path)?;
self.graph = serde_json::from_reader(file)?;
Ok(())
}
pub fn show(&mut self, ctx: &egui::Context) {
egui::Window::new(&self.title)
.default_size(self.size)
.show(ctx, |ui| self.graph.show(ui));
}
}