Add node graph editor

This commit is contained in:
Michael Mikovsky
2024-11-25 16:21:24 -07:00
parent acc2dfd74a
commit 549d3bff59
11 changed files with 381 additions and 88 deletions
+2
View File
@@ -1,3 +1,5 @@
.idea/
debug/ debug/
target/ target/
dist/ dist/
+2
View File
@@ -12,8 +12,10 @@ eframe = { version = "0.29.1", features = [
"persistence",]} "persistence",]}
egui = { version = "0.29.1", features = ["callstack", "default", "log"] } egui = { version = "0.29.1", features = ["callstack", "default", "log"] }
egui-snarl = {version = "0.5.0", features = ["serde"]}
glam = "0.29.2" glam = "0.29.2"
rand = "0.8.5" rand = "0.8.5"
serde = { version = "1.0.215", features = ["derive"] } serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133" serde_json = "1.0.133"
syn = "2.0.89"
typetag = "0.2.18" typetag = "0.2.18"
+76 -10
View File
@@ -1,14 +1,80 @@
[
{ {
"Properties": { "pane": {
"mode": "Hidden", "type": "BluePane"
"position": null,
"size": null,
"order": 1
}, },
"Console": { "id": "BLUE",
"mode": "Tiled", "mode": "Left"
"position": null, },
"size": null, {
"order": 0 "pane": {
"type": "GreenPane"
},
"id": "Green",
"mode": "Bottom"
},
{
"pane": {
"type": "PointRendererPane"
},
"id": "Point Cloud",
"mode": "Center"
},
{
"pane": {
"type": "PipelinePane",
"snarl": {
"nodes": {
"0": {
"value": {
"type": "Node1"
},
"pos": {
"x": -56.0,
"y": -52.5
},
"open": true
},
"1": {
"value": {
"type": "Node1"
},
"pos": {
"x": -61.21814,
"y": 155.28181
},
"open": true
}
},
"wires": [
{
"out_pin": {
"node": 1,
"output": 0
},
"in_pin": {
"node": 0,
"input": 0
}
},
{
"out_pin": {
"node": 0,
"output": 0
},
"in_pin": {
"node": 1,
"input": 0
} }
} }
]
},
"style": {
"select_rect_contained": null
},
"snarl_ui_id": 12028535709621507733
},
"id": "Pipeline Pane",
"mode": "Right"
}
]
+1 -1
View File
@@ -4,7 +4,7 @@
// use egui::{accesskit::TextAlign, mutex::Mutex, Align2, Color32, FontId, Pos2, Stroke}; // use egui::{accesskit::TextAlign, mutex::Mutex, Align2, Color32, FontId, Pos2, Stroke};
// use egui_glow::glow; // use egui_glow::glow;
use crate::panes::PaneManager; use crate::pane_manager::PaneManager;
/// We derive Deserialize/Serialize so we can persist app state on shutdown. /// We derive Deserialize/Serialize so we can persist app state on shutdown.
// #[derive(serde::Deserialize, serde::Serialize)] // #[derive(serde::Deserialize, serde::Serialize)]
+3 -3
View File
@@ -1,8 +1,8 @@
#![warn(clippy::all, rust_2018_idioms)] #![warn(clippy::all, rust_2018_idioms)]
mod app; mod app;
mod point_cloud_renderer; mod pane_manager;
mod panes; mod panes;
mod nodes;
pub use app::App; pub use app::App;
pub use panes::PaneManager; pub use pane_manager::PaneManager;
// pub use point_cloud_renderer::PointCloudApp;
View File
+1
View File
@@ -0,0 +1 @@
pub mod constants;
+28 -64
View File
@@ -1,6 +1,7 @@
use egui::{Ui, Color32, Stroke}; use egui::{Ui, Color32, Stroke};
use eframe::egui_glow::glow; use eframe::egui_glow::glow;
use std::sync::Arc; use std::sync::Arc;
use crate::panes::*;
// use erased_serde::serialize_trait_object; // use erased_serde::serialize_trait_object;
#[derive(serde::Deserialize, serde::Serialize, PartialEq)] #[derive(serde::Deserialize, serde::Serialize, PartialEq)]
@@ -75,47 +76,6 @@ impl PaneState {
// #[derive(serde::Deserialize, serde::Serialize)] // #[derive(serde::Deserialize, serde::Serialize)]
// #[derive(serde::Deserialize, serde::Serialize)] // #[derive(serde::Deserialize, serde::Serialize)]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct BluePane {}
#[typetag::serde]
impl Pane for BluePane {
fn new() -> PaneState where Self: Sized {
let mut s = Self {};
PaneState {
id: s.name().to_string(),
mode: PaneMode::Left,
pane: Box::new(s),
}
}
fn init(&mut self, _pcc: &PsudoCreationContext){}
fn name(&mut self) -> &str {"BLUE"}
fn render(&mut self, ui: &mut Ui){
ui.painter().rect(ui.max_rect(), 0., Color32::BLUE, Stroke::NONE);
}
fn context_menu(&mut self, _ui: &mut Ui) {}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct GreenPane {}
#[typetag::serde]
impl Pane for GreenPane {
fn new() -> PaneState where Self: Sized {
let mut s = Self {};
PaneState {
id: s.name().to_string(),
mode: PaneMode::Bottom,
pane: Box::new(s),
}
}
fn init(&mut self, _cc: &PsudoCreationContext){}
fn name(&mut self) -> &str {"Green"}
fn render(&mut self, ui: &mut Ui){
ui.painter().rect(ui.max_rect(), 0., Color32::GREEN, Stroke::NONE);
}
fn context_menu(&mut self, _ui: &mut Ui) {}
}
pub struct PsudoCreationContext { pub struct PsudoCreationContext {
pub gl: Option<Arc<glow::Context>>, pub gl: Option<Arc<glow::Context>>,
} }
@@ -131,9 +91,8 @@ impl PaneManager {
// if let Some(cc) = cc { // if let Some(cc) = cc {
let mut panes = vec![ let mut panes = vec![
BluePane::new(), point_cloud_renderer::PointRendererPane::new(),
GreenPane::new(), pipeline_editor::PipelinePane::new(),
crate::point_cloud_renderer::PointRendererPane::new(),
]; ];
let pcc = PsudoCreationContext { let pcc = PsudoCreationContext {
@@ -172,21 +131,6 @@ impl PaneManager {
for i in 0..len { for i in 0..len {
ui.menu_button(self.panes[i].id.clone(), |ui| { ui.menu_button(self.panes[i].id.clone(), |ui| {
if ui.button("Hidden").clicked() {
self.panes[i].mode = PaneMode::Hidden;
}
if ui.button("Windowed").clicked() {
self.panes[i].mode = PaneMode::Windowed;
}
if ui.button("Left").clicked() {
self.panes[i].mode = PaneMode::Left;
}
if ui.button("Right").clicked() {
self.panes[i].mode = PaneMode::Right;
}
if ui.button("Bottom").clicked() {
self.panes[i].mode = PaneMode::Bottom;
}
if ui.button("Center").clicked() { if ui.button("Center").clicked() {
for a in 0..len { for a in 0..len {
let pane2: &mut PaneState = &mut self.panes[a]; let pane2: &mut PaneState = &mut self.panes[a];
@@ -195,6 +139,27 @@ impl PaneManager {
} }
} }
self.panes[i].mode = PaneMode::Center; self.panes[i].mode = PaneMode::Center;
ui.close_menu();
}
if ui.button("Windowed").clicked() {
self.panes[i].mode = PaneMode::Windowed;
ui.close_menu();
}
if ui.button("Left").clicked() {
self.panes[i].mode = PaneMode::Left;
ui.close_menu();
}
if ui.button("Right").clicked() {
self.panes[i].mode = PaneMode::Right;
ui.close_menu();
}
if ui.button("Bottom").clicked() {
self.panes[i].mode = PaneMode::Bottom;
ui.close_menu();
}
if ui.button("Hidden").clicked() {
self.panes[i].mode = PaneMode::Hidden;
ui.close_menu();
} }
}); });
} }
@@ -203,13 +168,12 @@ impl PaneManager {
ui.separator(); ui.separator();
for i in 0..len { for i in 0..len {
if self.panes[i].mode != PaneMode::Hidden {
ui.menu_button(self.panes[i].id.clone(), |ui| { ui.menu_button(self.panes[i].id.clone(), |ui| {
let pane: &mut PaneState = &mut self.panes[i]; let _ = &mut self.panes[i].pane.context_menu(ui);
if pane.mode != PaneMode::Hidden {
pane.pane.context_menu(ui);
}
}); });
} }
}
}); });
}); });
@@ -269,7 +233,7 @@ impl PaneManager {
self.panes = panes; self.panes = panes;
for (mut pane) in &mut self.panes { for pane in &mut self.panes {
pane.pane.init(&self.pcc); pane.pane.init(&self.pcc);
} }
} }
+2
View File
@@ -0,0 +1,2 @@
pub mod pipeline_editor;
pub mod point_cloud_renderer;
+257
View File
@@ -0,0 +1,257 @@
use crate::pane_manager::{Pane, PaneMode, PaneState, PsudoCreationContext};
use egui::{Color32, Id, Pos2, Ui};
use egui_snarl::{
ui::{PinInfo, SnarlStyle, SnarlViewer},
InPin, NodeId, OutPin, Snarl,
};
use egui_snarl::ui::{PinShape, WireStyle};
#[derive(serde::Serialize, serde::Deserialize)]
pub struct PipelinePane {
snarl: Option<Snarl<Box<dyn Node>>>,
style: Option<SnarlStyle>,
snarl_ui_id: Option<Id>,
}
#[typetag::serde]
impl Pane for PipelinePane {
fn new() -> PaneState
where
Self: Sized,
{
let mut s = Self {
snarl: Some(Snarl::new()),
style: Some(SnarlStyle::new()),
snarl_ui_id: None,
};
PaneState {
id: s.name().to_string(),
mode: PaneMode::Center,
pane: Box::new(s),
}
}
fn init(&mut self, _cc: &PsudoCreationContext) {}
fn name(&mut self) -> &str {
"Pipeline Pane"
}
fn render(&mut self, ui: &mut Ui) {
self.snarl_ui_id = Some(ui.id());
if let Some(snarl) = &mut self.snarl {
if let Some(style) = &self.style {
snarl.show(&mut NodeViewer, style, "snarl", ui);
}
}
}
fn context_menu(&mut self, ui: &mut Ui) {
ui.menu_button("Add Node", |ui| {
if let Some(snarl) = &mut self.snarl {
NodeViewer::add_node_menu(Pos2 { x: 0., y: 0. }, ui, snarl);
}
});
if ui.button("Run").clicked() {
ui.close_menu();
self.run();
}
// if !self.snarl.is_none() {
// self.snarl.unwrap().add_node_menu(ui, ui.clip_rect().min.clone(), )
// }
}
}
impl PipelinePane {
pub fn run(&mut self) {
// Todo:
}
}
fn format_float(v: f64) -> String {
let v = (v * 1000.0).round() / 1000.0;
format!("{}", v)
}
#[typetag::serde(tag = "type")]
trait Node {
fn new() -> Self
where
Self: Sized;
fn get_name(&self) -> &str;
fn get_description(&self) -> &str;
fn duplicate(&self) -> Box<dyn Node>;
fn inputs(&self) -> usize;
fn outputs(&self) -> usize;
fn show_input(&self, pin: &InPin, ui: &mut Ui, scale: f32) -> PinInfo;
fn show_output(&self, pin: &OutPin, ui: &mut Ui, scale: f32) -> PinInfo;
fn can_rx(&self, other: &Box<dyn Node>) -> bool;
fn can_tx(&self, other: &Box<dyn Node>) -> bool;
fn context_menu(&self, ui: &mut Ui);
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
struct Node1;
#[typetag::serde]
impl Node for Node1 {
fn new() -> Self {
Self
}
fn get_name(&self) -> &str {
"Test"
}
fn get_description(&self) -> &str {"Test Node"}
fn duplicate(&self) -> Box<dyn Node> {
Box::new(Self::new())
}
fn inputs(&self) -> usize {
1
}
fn outputs(&self) -> usize {
1
}
fn show_input(&self, _pin: &InPin, _ui: &mut Ui, _scale: f32) -> PinInfo {
PinInfo::square()
}
fn show_output(&self, _pin: &OutPin, _ui: &mut Ui, _scale: f32) -> PinInfo {
PinInfo::square().with_fill(Color32::RED).with_wire_style(WireStyle::Bezier3)
}
fn can_rx(&self, _other: &Box<dyn Node>) -> bool {
true
}
fn can_tx(&self, _other: &Box<dyn Node>) -> bool {
true
}
fn context_menu(&self, ui: &mut Ui) {
ui.label("Test!");
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
struct NodeViewer;
impl SnarlViewer<Box<dyn Node>> for NodeViewer {
fn connect(&mut self, from: &OutPin, to: &InPin, snarl: &mut Snarl<Box<dyn Node>>) {
// Validate connection
let rx = snarl.get_node(to.id.node).unwrap();
let tx = snarl.get_node(from.id.node).unwrap();
if rx.can_rx(tx) && tx.can_tx(rx) {
for &remote in &to.remotes {
snarl.disconnect(remote, to.id);
}
snarl.connect(from.id, to.id);
}
}
fn title(&mut self, node: &Box<dyn Node>) -> String {
node.get_name().to_string()
}
fn outputs(&mut self, node: &Box<dyn Node>) -> usize {
node.outputs()
}
fn inputs(&mut self, node: &Box<dyn Node>) -> usize {
node.inputs()
}
fn show_input(
&mut self,
pin: &InPin,
ui: &mut Ui,
scale: f32,
snarl: &mut Snarl<Box<dyn Node>>,
) -> PinInfo {
snarl
.get_node(pin.id.node)
.unwrap()
.show_input(pin, ui, scale)
}
fn show_output(
&mut self,
pin: &OutPin,
ui: &mut Ui,
scale: f32,
snarl: &mut Snarl<Box<dyn Node>>,
) -> PinInfo {
snarl
.get_node(pin.id.node)
.unwrap()
.show_output(pin, ui, scale)
}
fn has_graph_menu(&mut self, _pos: Pos2, _snarl: &mut Snarl<Box<dyn Node>>) -> bool {
true
}
fn show_graph_menu(
&mut self,
pos: egui::Pos2,
ui: &mut Ui,
_scale: f32,
snarl: &mut Snarl<Box<dyn Node>>,
) {
NodeViewer::add_node_menu(pos, ui, snarl);
}
fn has_on_hover_popup(&mut self, _: &Box<dyn Node>) -> bool {
true
}
fn show_on_hover_popup(
&mut self,
node: NodeId,
_inputs: &[InPin],
_outputs: &[OutPin],
ui: &mut Ui,
_scale: f32,
snarl: &mut Snarl<Box<dyn Node>>,
) {
ui.label(snarl.get_node(node).unwrap().get_description());
}
fn has_node_menu(&mut self, _node: &Box<dyn Node>) -> bool {
true
}
fn show_node_menu(
&mut self,
nodeid: NodeId,
_inputs: &[InPin],
_outputs: &[OutPin],
ui: &mut Ui,
_scale: f32,
snarl: &mut Snarl<Box<dyn Node + 'static>>,
) {
ui.label("Node menu");
if ui.button("Remove").clicked() {
snarl.remove_node(nodeid);
ui.close_menu();
} else if ui.button("Duplicate").clicked() {
snarl.insert_node(Pos2 {x:0.,y:0.}, snarl.get_node(nodeid).unwrap().duplicate());
ui.close_menu();
} else {
snarl.get_node(nodeid).unwrap().context_menu(ui);
}
}
}
impl NodeViewer {
pub fn add_node_menu(pos: Pos2, ui: &mut Ui, snarl: &mut Snarl<Box<dyn Node>>) {
ui.label("Add node");
let button = ui.button("Test1");
if button.clicked() {
snarl.insert_node(pos, Box::new(Node1::new()));
ui.close_menu();
}
}
}
@@ -6,7 +6,7 @@ use glam::{Vec3, Mat4, Quat};
use std::fs::File; use std::fs::File;
// use std::path::Path; // use std::path::Path;
use std::io::{BufReader, BufRead}; use std::io::{BufReader, BufRead};
use crate::panes::{Pane, PaneMode, PaneState}; use crate::pane_manager::{Pane, PaneMode, PaneState, PsudoCreationContext};
use std::sync::Mutex; use std::sync::Mutex;
use egui::FontId; use egui::FontId;
use egui::Align2; use egui::Align2;
@@ -14,7 +14,6 @@ use egui::Align2;
use std::time::Instant; use std::time::Instant;
use egui::Stroke; use egui::Stroke;
use egui::Ui; use egui::Ui;
use crate::panes;
// Shader sources updated for 3D rendering with fixed-point positions // Shader sources updated for 3D rendering with fixed-point positions
const VERTEX_SHADER: &str = r#" const VERTEX_SHADER: &str = r#"
@@ -532,7 +531,7 @@ pub struct PointRendererPane {
#[typetag::serde] #[typetag::serde]
impl Pane for PointRendererPane { impl Pane for PointRendererPane {
fn new() -> PaneState where Self: Sized { fn new() -> PaneState where Self: Sized {
let mut renderer = PointRenderer::default(); let renderer = PointRenderer::default();
let mut s = Self { let mut s = Self {
renderer: Arc::new(Mutex::new(renderer)), renderer: Arc::new(Mutex::new(renderer)),
points: Vec::new(), points: Vec::new(),
@@ -541,11 +540,11 @@ impl Pane for PointRendererPane {
}; };
PaneState { PaneState {
id: s.name().to_string(), id: s.name().to_string(),
mode: PaneMode::Center, mode: PaneMode::Hidden,
pane: Box::new(s), pane: Box::new(s),
} }
} }
fn init(&mut self, pcc: &panes::PsudoCreationContext){ fn init(&mut self, pcc: &PsudoCreationContext){
self.renderer.lock().expect("Renderer Not Initialized").init(pcc.gl.clone(), 1_000_000); self.renderer.lock().expect("Renderer Not Initialized").init(pcc.gl.clone(), 1_000_000);
} }
fn name(&mut self) -> &str {"Point Cloud"} fn name(&mut self) -> &str {"Point Cloud"}