Initial commit
This commit is contained in:
commit
6e5565d45c
16 changed files with 1548 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
|
||||
Cargo.lock
|
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'virtualdevicecreator'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=virtualdevicecreator",
|
||||
"--package=virtualdevicecreator"
|
||||
],
|
||||
"filter": {
|
||||
"name": "virtualdevicecreator",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.background": "#063147",
|
||||
"titleBar.activeBackground": "#094563",
|
||||
"titleBar.activeForeground": "#F6FBFE"
|
||||
}
|
||||
}
|
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "virtualdevicecreator"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
egui = { version = "*", features = ["persistence"] }
|
||||
eframe = "*"
|
||||
epi = "*"
|
||||
slotmap = { version = "*", features = ["serde"] }
|
||||
smallvec = { version = "*", features = ["serde"] }
|
||||
strum = "*"
|
||||
strum_macros = "*"
|
||||
serde = { version = "*", features = ["derive"] }
|
||||
anyhow = "*"
|
||||
nalgebra = { version = "*", features = ["serde-serialize"] }
|
||||
rfd = "*"
|
93
src/color_hex_utils.rs
Normal file
93
src/color_hex_utils.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
use egui::Color32;
|
||||
|
||||
/// Converts a hex string with a leading '#' into a egui::Color32.
|
||||
/// - The first three channels are interpreted as R, G, B.
|
||||
/// - The fourth channel, if present, is used as the alpha value.
|
||||
/// - Both upper and lowercase characters can be used for the hex values.
|
||||
///
|
||||
/// *Adapted from: https://docs.rs/raster/0.1.0/src/raster/lib.rs.html#425-725.
|
||||
/// Credit goes to original authors.*
|
||||
pub fn color_from_hex(hex: &str) -> Result<Color32, String> {
|
||||
// Convert a hex string to decimal. Eg. "00" -> 0. "FF" -> 255.
|
||||
fn _hex_dec(hex_string: &str) -> Result<u8, String> {
|
||||
match u8::from_str_radix(hex_string, 16) {
|
||||
Ok(o) => Ok(o),
|
||||
Err(e) => Err(format!("Error parsing hex: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
if hex.len() == 9 && hex.starts_with('#') {
|
||||
// #FFFFFFFF (Red Green Blue Alpha)
|
||||
return Ok(Color32::from_rgba_premultiplied(
|
||||
_hex_dec(&hex[1..3])?,
|
||||
_hex_dec(&hex[3..5])?,
|
||||
_hex_dec(&hex[5..7])?,
|
||||
_hex_dec(&hex[7..9])?,
|
||||
));
|
||||
} else if hex.len() == 7 && hex.starts_with('#') {
|
||||
// #FFFFFF (Red Green Blue)
|
||||
return Ok(Color32::from_rgb(
|
||||
_hex_dec(&hex[1..3])?,
|
||||
_hex_dec(&hex[3..5])?,
|
||||
_hex_dec(&hex[5..7])?,
|
||||
));
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"Error parsing hex: {}. Example of valid formats: #FFFFFF or #ffffffff",
|
||||
hex
|
||||
))
|
||||
}
|
||||
|
||||
/// Converts a Color32 into its canonical hexadecimal representation.
|
||||
/// - The color string will be preceded by '#'.
|
||||
/// - If the alpha channel is completely opaque, it will be ommitted.
|
||||
/// - Characters from 'a' to 'f' will be written in lowercase.
|
||||
pub fn color_to_hex(color: Color32) -> String {
|
||||
if color.a() < 255 {
|
||||
format!(
|
||||
"#{:02x?}{:02x?}{:02x?}{:02x?}",
|
||||
color.r(),
|
||||
color.g(),
|
||||
color.b(),
|
||||
color.a()
|
||||
)
|
||||
} else {
|
||||
format!("#{:02x?}{:02x?}{:02x?}", color.r(), color.g(), color.b())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn test_color_from_and_to_hex() {
|
||||
assert_eq!(
|
||||
color_from_hex("#00ff00").unwrap(),
|
||||
Color32::from_rgb(0, 255, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
color_from_hex("#5577AA").unwrap(),
|
||||
Color32::from_rgb(85, 119, 170)
|
||||
);
|
||||
assert_eq!(
|
||||
color_from_hex("#E2e2e277").unwrap(),
|
||||
Color32::from_rgba_premultiplied(226, 226, 226, 119)
|
||||
);
|
||||
assert!(color_from_hex("abcdefgh").is_err());
|
||||
|
||||
assert_eq!(
|
||||
color_to_hex(Color32::from_rgb(0, 255, 0)),
|
||||
"#00ff00".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
color_to_hex(Color32::from_rgb(85, 119, 170)),
|
||||
"#5577aa".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
color_to_hex(Color32::from_rgba_premultiplied(226, 226, 226, 119)),
|
||||
"e2e2e277".to_string()
|
||||
);
|
||||
}
|
||||
}
|
69
src/editor_state.rs
Normal file
69
src/editor_state.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use crate::graph_types::*;
|
||||
use crate::id_types::*;
|
||||
use crate::node_finder::NodeFinder;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PanZoom {
|
||||
pub pan: egui::Vec2,
|
||||
pub zoom: f32,
|
||||
}
|
||||
|
||||
pub struct GraphEditorState {
|
||||
pub graph: Graph,
|
||||
/// An ongoing connection interaction: The mouse has dragged away from a
|
||||
/// port and the user is holding the click
|
||||
pub connection_in_progress: Option<(NodeId, AnyParameterId)>,
|
||||
/// The currently active node. A program will be compiled to compute the
|
||||
/// result of this node and constantly updated in real-time.
|
||||
pub active_node: Option<NodeId>,
|
||||
/// The position of each node.
|
||||
pub node_positions: HashMap<NodeId, egui::Pos2>,
|
||||
/// The node finder is used to create new nodes.
|
||||
pub node_finder: Option<NodeFinder>,
|
||||
/// When this option is set by the UI, the side effect encoded by the node
|
||||
/// will be executed at the start of the next frame.
|
||||
pub run_side_effect: Option<NodeId>,
|
||||
/// The panning of the graph viewport.
|
||||
pub pan_zoom: PanZoom,
|
||||
}
|
||||
|
||||
impl GraphEditorState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
graph: Graph::new(),
|
||||
connection_in_progress: None,
|
||||
active_node: None,
|
||||
run_side_effect: None,
|
||||
node_positions: HashMap::new(),
|
||||
node_finder: None,
|
||||
pan_zoom: PanZoom {
|
||||
pan: egui::Vec2::ZERO,
|
||||
zoom: 1.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GraphEditorState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PanZoom {
|
||||
pub fn adjust_zoom(
|
||||
&mut self,
|
||||
zoom_delta: f32,
|
||||
point: egui::Vec2,
|
||||
zoom_min: f32,
|
||||
zoom_max: f32,
|
||||
) {
|
||||
let zoom_clamped = (self.zoom + zoom_delta).clamp(zoom_min, zoom_max);
|
||||
let zoom_delta = zoom_clamped - self.zoom;
|
||||
|
||||
self.zoom += zoom_delta;
|
||||
self.pan += point * zoom_delta;
|
||||
}
|
||||
}
|
160
src/graph_editor_egui.rs
Normal file
160
src/graph_editor_egui.rs
Normal file
|
@ -0,0 +1,160 @@
|
|||
use egui::*;
|
||||
|
||||
use crate::color_hex_utils::*;
|
||||
use crate::editor_state::GraphEditorState;
|
||||
use crate::graph_node_ui::*;
|
||||
use crate::id_types::*;
|
||||
use crate::node_finder::NodeFinder;
|
||||
|
||||
pub fn draw_graph_editor(ctx: &CtxRef, state: &mut GraphEditorState) {
|
||||
let mouse = &ctx.input().pointer;
|
||||
let cursor_pos = mouse.hover_pos().unwrap_or(Pos2::ZERO);
|
||||
|
||||
// Gets filled with the port locations as nodes are drawn
|
||||
let mut port_locations = PortLocations::new();
|
||||
|
||||
// The responses returned from node drawing have side effects that are best
|
||||
// executed at the end of this function.
|
||||
let mut delayed_responses: Vec<DrawGraphNodeResponse> = vec![];
|
||||
|
||||
CentralPanel::default().show(ctx, |ui| {
|
||||
/* Draw nodes */
|
||||
let nodes = state.graph.iter_nodes().collect::<Vec<_>>(); // avoid borrow checker
|
||||
for node_id in nodes {
|
||||
let response = GraphNodeWidget {
|
||||
position: state.node_positions.get_mut(&node_id).unwrap(),
|
||||
graph: &mut state.graph,
|
||||
port_locations: &mut port_locations,
|
||||
node_id,
|
||||
ongoing_drag: state.connection_in_progress,
|
||||
active: state
|
||||
.active_node
|
||||
.map(|active| active == node_id)
|
||||
.unwrap_or(false),
|
||||
pan: state.pan_zoom.pan,
|
||||
}
|
||||
.show(ui);
|
||||
|
||||
if let Some(response) = response {
|
||||
delayed_responses.push(response);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* Draw the node finder, if open */
|
||||
let mut should_close_node_finder = false;
|
||||
if let Some(ref mut node_finder) = state.node_finder {
|
||||
let mut node_finder_area = Area::new("node_finder");
|
||||
if let Some(pos) = node_finder.position {
|
||||
node_finder_area = node_finder_area.current_pos(pos);
|
||||
}
|
||||
node_finder_area.show(ctx, |ui| {
|
||||
if let Some(node_archetype) = node_finder.show(ui) {
|
||||
let new_node = state.graph.add_node(node_archetype.to_descriptor());
|
||||
state
|
||||
.node_positions
|
||||
.insert(new_node, cursor_pos - state.pan_zoom.pan);
|
||||
should_close_node_finder = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if should_close_node_finder {
|
||||
state.node_finder = None;
|
||||
}
|
||||
|
||||
/* Draw connections */
|
||||
let connection_stroke = egui::Stroke {
|
||||
width: 5.0,
|
||||
color: color_from_hex("#efefef").unwrap(),
|
||||
};
|
||||
|
||||
if let Some((_, ref locator)) = state.connection_in_progress {
|
||||
let painter = ctx.layer_painter(LayerId::background());
|
||||
let start_pos = port_locations[locator];
|
||||
painter.line_segment([start_pos, cursor_pos], connection_stroke)
|
||||
}
|
||||
|
||||
for (input, output) in state.graph.iter_connections() {
|
||||
let painter = ctx.layer_painter(LayerId::background());
|
||||
let src_pos = port_locations[&AnyParameterId::Output(output)];
|
||||
let dst_pos = port_locations[&AnyParameterId::Input(input)];
|
||||
painter.line_segment([src_pos, dst_pos], connection_stroke);
|
||||
}
|
||||
|
||||
/* Handle responses from drawing nodes */
|
||||
|
||||
for response in delayed_responses {
|
||||
match response {
|
||||
DrawGraphNodeResponse::ConnectEventStarted(node_id, port) => {
|
||||
state.connection_in_progress = Some((node_id, port));
|
||||
}
|
||||
DrawGraphNodeResponse::ConnectEventEnded(locator) => {
|
||||
let in_out = match (
|
||||
state
|
||||
.connection_in_progress
|
||||
.map(|(_node, param)| param)
|
||||
.take()
|
||||
.expect("Cannot end drag without in-progress connection."),
|
||||
locator,
|
||||
) {
|
||||
(AnyParameterId::Input(input), AnyParameterId::Output(output))
|
||||
| (AnyParameterId::Output(output), AnyParameterId::Input(input)) => {
|
||||
Some((input, output))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some((input, output)) = in_out {
|
||||
state.graph.add_connection(output, input)
|
||||
}
|
||||
}
|
||||
DrawGraphNodeResponse::SetActiveNode(node_id) => {
|
||||
state.active_node = Some(node_id);
|
||||
}
|
||||
DrawGraphNodeResponse::ClearActiveNode => {
|
||||
state.active_node = None;
|
||||
}
|
||||
DrawGraphNodeResponse::RunNodeSideEffect(node_id) => {
|
||||
state.run_side_effect = Some(node_id);
|
||||
}
|
||||
DrawGraphNodeResponse::DeleteNode(node_id) => {
|
||||
state.graph.remove_node(node_id);
|
||||
state.node_positions.remove(&node_id);
|
||||
// Make sure to not leave references to old nodes hanging
|
||||
if state.active_node.map(|x| x == node_id).unwrap_or(false) {
|
||||
state.active_node = None;
|
||||
}
|
||||
if state.run_side_effect.map(|x| x == node_id).unwrap_or(false) {
|
||||
state.run_side_effect = None;
|
||||
}
|
||||
}
|
||||
DrawGraphNodeResponse::DisconnectEvent(input_id) => {
|
||||
let corresp_output = state
|
||||
.graph
|
||||
.connection(input_id)
|
||||
.expect("Connection data should be valid");
|
||||
let other_node = state.graph.get_input(input_id).node();
|
||||
state.graph.remove_connection(input_id);
|
||||
state.connection_in_progress =
|
||||
Some((other_node, AnyParameterId::Output(corresp_output)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Mouse input handling */
|
||||
|
||||
if mouse.any_released() && state.connection_in_progress.is_some() {
|
||||
state.connection_in_progress = None;
|
||||
}
|
||||
|
||||
if mouse.button_down(PointerButton::Secondary) {
|
||||
state.node_finder = Some(NodeFinder::new_at(cursor_pos));
|
||||
}
|
||||
if ctx.input().key_pressed(Key::Escape) {
|
||||
state.node_finder = None;
|
||||
}
|
||||
|
||||
if ctx.input().pointer.middle_down() {
|
||||
state.pan_zoom.pan += ctx.input().pointer.delta();
|
||||
}
|
||||
}
|
203
src/graph_impls.rs
Normal file
203
src/graph_impls.rs
Normal file
|
@ -0,0 +1,203 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use smallvec::smallvec;
|
||||
|
||||
use crate::graph_types::*;
|
||||
use crate::id_types::*;
|
||||
use crate::SVec;
|
||||
|
||||
impl Graph {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn add_node(&mut self, d: NodeDescriptor) -> NodeId {
|
||||
let node_id = self.nodes.insert_with_key(|node_id| {
|
||||
Node {
|
||||
id: node_id,
|
||||
label: d.label,
|
||||
op_name: d.op_name,
|
||||
// These will get filled in later
|
||||
inputs: Vec::default(),
|
||||
outputs: Vec::default(),
|
||||
is_executable: d.is_executable,
|
||||
}
|
||||
});
|
||||
|
||||
use InputParamKind::*;
|
||||
let inputs: Vec<(String, InputId)> = d
|
||||
.inputs
|
||||
.into_iter()
|
||||
.map(|(input_name, input)| {
|
||||
let input_id = self.inputs.insert_with_key(|id| match input {
|
||||
InputDescriptor::Vector { default } => InputParam {
|
||||
id,
|
||||
typ: DataType::Vector,
|
||||
value: InputParamValue::Vector(default),
|
||||
metadata: smallvec![],
|
||||
kind: ConnectionOrConstant,
|
||||
node: node_id,
|
||||
},
|
||||
InputDescriptor::Mesh => InputParam {
|
||||
id,
|
||||
typ: DataType::Mesh,
|
||||
value: InputParamValue::None,
|
||||
metadata: smallvec![],
|
||||
kind: ConnectionOnly,
|
||||
node: node_id,
|
||||
},
|
||||
InputDescriptor::Selection => InputParam {
|
||||
id,
|
||||
typ: DataType::Selection,
|
||||
value: InputParamValue::Selection {
|
||||
text: "".into(),
|
||||
selection: Some(vec![]),
|
||||
},
|
||||
metadata: smallvec![],
|
||||
kind: ConnectionOrConstant,
|
||||
node: node_id,
|
||||
},
|
||||
InputDescriptor::Scalar { default, min, max } => InputParam {
|
||||
id,
|
||||
typ: DataType::Scalar,
|
||||
value: InputParamValue::Scalar(default),
|
||||
metadata: smallvec![InputParamMetadata::MinMaxScalar { min, max }],
|
||||
kind: ConnectionOrConstant,
|
||||
node: node_id,
|
||||
},
|
||||
InputDescriptor::Enum { values } => InputParam {
|
||||
id,
|
||||
typ: DataType::Enum,
|
||||
value: InputParamValue::Enum {
|
||||
values,
|
||||
selection: None,
|
||||
},
|
||||
metadata: smallvec![],
|
||||
kind: ConstantOnly,
|
||||
node: node_id,
|
||||
},
|
||||
InputDescriptor::NewFile => InputParam {
|
||||
id,
|
||||
typ: DataType::NewFile,
|
||||
value: InputParamValue::NewFile { path: None },
|
||||
metadata: smallvec![],
|
||||
kind: ConstantOnly,
|
||||
node: node_id,
|
||||
},
|
||||
});
|
||||
(input_name, input_id)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let outputs: Vec<(String, OutputId)> = d
|
||||
.outputs
|
||||
.into_iter()
|
||||
.map(|(output_name, output)| {
|
||||
let output_id = self.outputs.insert_with_key(|id| OutputParam {
|
||||
node: node_id,
|
||||
id,
|
||||
typ: output.0,
|
||||
});
|
||||
(output_name, output_id)
|
||||
})
|
||||
.collect();
|
||||
|
||||
self[node_id].inputs = inputs;
|
||||
self[node_id].outputs = outputs;
|
||||
node_id
|
||||
}
|
||||
|
||||
pub fn remove_node(&mut self, node_id: NodeId) {
|
||||
self.connections
|
||||
.retain(|i, o| !(self.outputs[*o].node == node_id || self.inputs[*i].node == node_id));
|
||||
let inputs: SVec<_> = self[node_id].input_ids().collect();
|
||||
for input in inputs {
|
||||
self.inputs.remove(input);
|
||||
}
|
||||
let outputs: SVec<_> = self[node_id].output_ids().collect();
|
||||
for output in outputs {
|
||||
self.outputs.remove(output);
|
||||
}
|
||||
self.nodes.remove(node_id);
|
||||
}
|
||||
|
||||
pub fn remove_connection(&mut self, input_id: InputId) -> Option<OutputId> {
|
||||
self.connections.remove(&input_id)
|
||||
}
|
||||
|
||||
pub fn iter_nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
|
||||
self.nodes.iter().map(|(id, _)| id)
|
||||
}
|
||||
|
||||
pub fn add_connection(&mut self, output: OutputId, input: InputId) {
|
||||
self.connections.insert(input, output);
|
||||
}
|
||||
|
||||
pub fn iter_connections(&self) -> impl Iterator<Item = (InputId, OutputId)> + '_ {
|
||||
self.connections.iter().map(|(o, i)| (*o, *i))
|
||||
}
|
||||
|
||||
pub fn connection(&self, input: InputId) -> Option<OutputId> {
|
||||
self.connections.get(&input).copied()
|
||||
}
|
||||
|
||||
pub fn any_param_type(&self, param: AnyParameterId) -> Result<DataType> {
|
||||
match param {
|
||||
AnyParameterId::Input(input) => self.inputs.get(input).map(|x| x.typ),
|
||||
AnyParameterId::Output(output) => self.outputs.get(output).map(|x| x.typ),
|
||||
}
|
||||
.ok_or_else(|| anyhow!("Invalid parameter id: {:?}", param))
|
||||
}
|
||||
|
||||
pub fn get_input(&self, input: InputId) -> &InputParam {
|
||||
&self.inputs[input]
|
||||
}
|
||||
|
||||
pub fn get_output(&self, output: OutputId) -> &OutputParam {
|
||||
&self.outputs[output]
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn inputs<'a>(&'a self, graph: &'a Graph) -> impl Iterator<Item = &InputParam> + 'a {
|
||||
self.input_ids().map(|id| graph.get_input(id))
|
||||
}
|
||||
|
||||
pub fn outputs<'a>(&'a self, graph: &'a Graph) -> impl Iterator<Item = &OutputParam> + 'a {
|
||||
self.output_ids().map(|id| graph.get_output(id))
|
||||
}
|
||||
|
||||
pub fn input_ids(&self) -> impl Iterator<Item = InputId> + '_ {
|
||||
self.inputs.iter().map(|(_name, id)| *id)
|
||||
}
|
||||
|
||||
pub fn output_ids(&self) -> impl Iterator<Item = OutputId> + '_ {
|
||||
self.outputs.iter().map(|(_name, id)| *id)
|
||||
}
|
||||
|
||||
pub fn get_input(&self, name: &str) -> Result<InputId> {
|
||||
self.inputs
|
||||
.iter()
|
||||
.find(|(param_name, _id)| param_name == name)
|
||||
.map(|x| x.1)
|
||||
.ok_or_else(|| anyhow!("Node {:?} has no parameter named {}", self.id, name))
|
||||
}
|
||||
|
||||
pub fn get_output(&self, name: &str) -> Result<OutputId> {
|
||||
self.outputs
|
||||
.iter()
|
||||
.find(|(param_name, _id)| param_name == name)
|
||||
.map(|x| x.1)
|
||||
.ok_or_else(|| anyhow!("Node {:?} has no parameter named {}", self.id, name))
|
||||
}
|
||||
|
||||
/// Can this node be enabled on the UI? I.e. does it output a mesh?
|
||||
pub fn can_be_enabled(&self, graph: &Graph) -> bool {
|
||||
self.outputs(graph)
|
||||
.any(|output| output.typ == DataType::Mesh)
|
||||
}
|
||||
|
||||
/// Executable nodes are used to produce side effects, like exporting files.
|
||||
pub fn is_executable(&self) -> bool {
|
||||
self.is_executable
|
||||
}
|
||||
}
|
359
src/graph_node_ui.rs
Normal file
359
src/graph_node_ui.rs
Normal file
|
@ -0,0 +1,359 @@
|
|||
use egui::*;
|
||||
use epaint::*;
|
||||
|
||||
use crate::color_hex_utils::*;
|
||||
use crate::graph_types::*;
|
||||
use crate::id_types::*;
|
||||
|
||||
pub type PortLocations = std::collections::HashMap<AnyParameterId, Pos2>;
|
||||
|
||||
pub enum DrawGraphNodeResponse {
|
||||
ConnectEventStarted(NodeId, AnyParameterId),
|
||||
ConnectEventEnded(AnyParameterId),
|
||||
SetActiveNode(NodeId),
|
||||
RunNodeSideEffect(NodeId),
|
||||
ClearActiveNode,
|
||||
DeleteNode(NodeId),
|
||||
DisconnectEvent(InputId),
|
||||
}
|
||||
|
||||
pub struct GraphNodeWidget<'a> {
|
||||
pub position: &'a mut Pos2,
|
||||
pub graph: &'a mut Graph,
|
||||
pub port_locations: &'a mut PortLocations,
|
||||
pub node_id: NodeId,
|
||||
pub ongoing_drag: Option<(NodeId, AnyParameterId)>,
|
||||
pub active: bool,
|
||||
pub pan: egui::Vec2,
|
||||
}
|
||||
|
||||
impl<'a> GraphNodeWidget<'a> {
|
||||
pub const MAX_NODE_SIZE: [f32; 2] = [200.0, 200.0];
|
||||
|
||||
pub fn show(self, ui: &mut Ui) -> Option<DrawGraphNodeResponse> {
|
||||
let mut child_ui = ui.child_ui(
|
||||
Rect::from_min_size(*self.position + self.pan, Self::MAX_NODE_SIZE.into()),
|
||||
Layout::default(),
|
||||
);
|
||||
|
||||
let node_resp = Self::show_graph_node(
|
||||
self.graph,
|
||||
self.node_id,
|
||||
&mut child_ui,
|
||||
self.port_locations,
|
||||
self.ongoing_drag,
|
||||
self.active,
|
||||
);
|
||||
|
||||
let resp = ui.allocate_rect(child_ui.min_rect(), Sense::drag());
|
||||
*self.position += resp.drag_delta();
|
||||
|
||||
node_resp
|
||||
}
|
||||
|
||||
/// Draws this node. Also fills in the list of port locations with all of its ports.
|
||||
/// Returns a response showing whether a drag event was started.
|
||||
/// Parameters:
|
||||
/// - **ongoing_drag**: Is there a port drag event currently going on?
|
||||
fn show_graph_node(
|
||||
graph: &mut Graph,
|
||||
node_id: NodeId,
|
||||
ui: &mut Ui,
|
||||
port_locations: &mut PortLocations,
|
||||
ongoing_drag: Option<(NodeId, AnyParameterId)>,
|
||||
active: bool,
|
||||
) -> Option<DrawGraphNodeResponse> {
|
||||
let margin = egui::vec2(15.0, 5.0);
|
||||
let _field_separation = 5.0;
|
||||
let mut response: Option<DrawGraphNodeResponse> = None;
|
||||
|
||||
let background_color = color_from_hex("#3f3f3f").unwrap();
|
||||
let titlebar_color = background_color.linear_multiply(0.8);
|
||||
let text_color = color_from_hex("#fefefe").unwrap();
|
||||
|
||||
ui.visuals_mut().widgets.noninteractive.fg_stroke = Stroke::new(2.0, text_color);
|
||||
|
||||
// Preallocate shapes to paint below contents
|
||||
let background_shape = ui.painter().add(Shape::Noop);
|
||||
|
||||
let outer_rect_bounds = ui.available_rect_before_wrap();
|
||||
let mut inner_rect = outer_rect_bounds.shrink2(margin);
|
||||
|
||||
// Make sure we don't shrink to the negative:
|
||||
inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x);
|
||||
inner_rect.max.y = inner_rect.max.y.max(inner_rect.min.y);
|
||||
|
||||
let mut child_ui = ui.child_ui(inner_rect, *ui.layout());
|
||||
let mut title_height = 0.0;
|
||||
|
||||
let mut input_port_heights = vec![];
|
||||
let mut output_port_heights = vec![];
|
||||
|
||||
child_ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(Label::new(
|
||||
RichText::new(&graph[node_id].label)
|
||||
.text_style(TextStyle::Button)
|
||||
.color(color_from_hex("#fefefe").unwrap()),
|
||||
));
|
||||
});
|
||||
ui.add_space(margin.y);
|
||||
title_height = ui.min_size().y;
|
||||
|
||||
// First pass: Draw the inner fields. Compute port heights
|
||||
let inputs = graph[node_id].inputs.clone();
|
||||
for (param_name, param) in inputs {
|
||||
let height_before = ui.min_rect().bottom();
|
||||
if graph.connection(param).is_some() {
|
||||
ui.label(param_name);
|
||||
} else {
|
||||
graph[param].value_widget(¶m_name, ui);
|
||||
}
|
||||
let height_after = ui.min_rect().bottom();
|
||||
input_port_heights.push((height_before + height_after) / 2.0);
|
||||
}
|
||||
|
||||
let outputs = graph[node_id].outputs.clone();
|
||||
for (param_name, _param) in outputs {
|
||||
let height_before = ui.min_rect().bottom();
|
||||
ui.label(¶m_name);
|
||||
let height_after = ui.min_rect().bottom();
|
||||
output_port_heights.push((height_before + height_after) / 2.0);
|
||||
}
|
||||
|
||||
// Button row
|
||||
ui.horizontal(|ui| {
|
||||
// Show 'Enable' button for nodes that output a mesh
|
||||
if graph[node_id].can_be_enabled(graph) {
|
||||
ui.horizontal(|ui| {
|
||||
if !active {
|
||||
if ui.button("👁 Set active").clicked() {
|
||||
response = Some(DrawGraphNodeResponse::SetActiveNode(node_id));
|
||||
}
|
||||
} else {
|
||||
let button = egui::Button::new(
|
||||
RichText::new("👁 Active").color(egui::Color32::BLACK),
|
||||
)
|
||||
.fill(egui::Color32::GOLD);
|
||||
if ui.add(button).clicked() {
|
||||
response = Some(DrawGraphNodeResponse::ClearActiveNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Show 'Run' button for executable nodes
|
||||
if graph[node_id].is_executable() && ui.button("⛭ Run").clicked() {
|
||||
response = Some(DrawGraphNodeResponse::RunNodeSideEffect(node_id));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Second pass, iterate again to draw the ports. This happens outside
|
||||
// the child_ui because we want ports to overflow the node background.
|
||||
|
||||
let outer_rect = child_ui.min_rect().expand2(margin);
|
||||
let port_left = outer_rect.left();
|
||||
let port_right = outer_rect.right();
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_port(
|
||||
ui: &mut Ui,
|
||||
graph: &Graph,
|
||||
node_id: NodeId,
|
||||
port_pos: Pos2,
|
||||
response: &mut Option<DrawGraphNodeResponse>,
|
||||
param_id: AnyParameterId,
|
||||
port_locations: &mut PortLocations,
|
||||
ongoing_drag: Option<(NodeId, AnyParameterId)>,
|
||||
is_connected_input: bool,
|
||||
) {
|
||||
let port_type = graph.any_param_type(param_id).unwrap();
|
||||
|
||||
let port_rect = Rect::from_center_size(port_pos, egui::vec2(10.0, 10.0));
|
||||
|
||||
let sense = if ongoing_drag.is_some() {
|
||||
Sense::hover()
|
||||
} else {
|
||||
Sense::click_and_drag()
|
||||
};
|
||||
|
||||
let resp = ui.allocate_rect(port_rect, sense);
|
||||
let port_color = if resp.hovered() {
|
||||
Color32::WHITE
|
||||
} else {
|
||||
GraphNodeWidget::data_type_color(port_type)
|
||||
};
|
||||
ui.painter()
|
||||
.circle(port_rect.center(), 5.0, port_color, Stroke::none());
|
||||
|
||||
if resp.drag_started() {
|
||||
if is_connected_input {
|
||||
*response = Some(DrawGraphNodeResponse::DisconnectEvent(
|
||||
param_id.assume_input(),
|
||||
));
|
||||
} else {
|
||||
*response = Some(DrawGraphNodeResponse::ConnectEventStarted(
|
||||
node_id, param_id,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((origin_node, origin_param)) = ongoing_drag {
|
||||
if origin_node != node_id {
|
||||
// Don't allow self-loops
|
||||
if graph.any_param_type(origin_param).unwrap() == port_type
|
||||
&& resp.hovered()
|
||||
&& ui.input().pointer.any_released()
|
||||
{
|
||||
*response = Some(DrawGraphNodeResponse::ConnectEventEnded(param_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
port_locations.insert(param_id, port_rect.center());
|
||||
}
|
||||
|
||||
// Input ports
|
||||
for ((_, param), port_height) in graph[node_id]
|
||||
.inputs
|
||||
.iter()
|
||||
.zip(input_port_heights.into_iter())
|
||||
{
|
||||
let should_draw = match graph[*param].kind() {
|
||||
InputParamKind::ConnectionOnly => true,
|
||||
InputParamKind::ConstantOnly => false,
|
||||
InputParamKind::ConnectionOrConstant => true,
|
||||
};
|
||||
|
||||
if should_draw {
|
||||
let pos_left = pos2(port_left, port_height);
|
||||
draw_port(
|
||||
ui,
|
||||
graph,
|
||||
node_id,
|
||||
pos_left,
|
||||
&mut response,
|
||||
AnyParameterId::Input(*param),
|
||||
port_locations,
|
||||
ongoing_drag,
|
||||
graph.connection(*param).is_some(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Output ports
|
||||
for ((_, param), port_height) in graph[node_id]
|
||||
.outputs
|
||||
.iter()
|
||||
.zip(output_port_heights.into_iter())
|
||||
{
|
||||
let pos_right = pos2(port_right, port_height);
|
||||
draw_port(
|
||||
ui,
|
||||
graph,
|
||||
node_id,
|
||||
pos_right,
|
||||
&mut response,
|
||||
AnyParameterId::Output(*param),
|
||||
port_locations,
|
||||
ongoing_drag,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw the background shape.
|
||||
// NOTE: This code is a bit more involve than it needs to be because egui
|
||||
// does not support drawing rectangles with asymmetrical round corners.
|
||||
|
||||
let shape = {
|
||||
let corner_radius = 4.0;
|
||||
|
||||
let titlebar_height = title_height + margin.y;
|
||||
let titlebar_rect =
|
||||
Rect::from_min_size(outer_rect.min, vec2(outer_rect.width(), titlebar_height));
|
||||
let titlebar = Shape::Rect(RectShape {
|
||||
rect: titlebar_rect,
|
||||
corner_radius,
|
||||
fill: titlebar_color,
|
||||
stroke: Stroke::none(),
|
||||
});
|
||||
|
||||
let body_rect = Rect::from_min_size(
|
||||
outer_rect.min + vec2(0.0, titlebar_height - corner_radius),
|
||||
vec2(outer_rect.width(), outer_rect.height() - titlebar_height),
|
||||
);
|
||||
let body = Shape::Rect(RectShape {
|
||||
rect: body_rect,
|
||||
corner_radius: 0.0,
|
||||
fill: background_color,
|
||||
stroke: Stroke::none(),
|
||||
});
|
||||
|
||||
let bottom_body_rect = Rect::from_min_size(
|
||||
body_rect.min + vec2(0.0, body_rect.height() - titlebar_height * 0.5),
|
||||
vec2(outer_rect.width(), titlebar_height),
|
||||
);
|
||||
let bottom_body = Shape::Rect(RectShape {
|
||||
rect: bottom_body_rect,
|
||||
corner_radius,
|
||||
fill: background_color,
|
||||
stroke: Stroke::none(),
|
||||
});
|
||||
|
||||
Shape::Vec(vec![titlebar, body, bottom_body])
|
||||
};
|
||||
|
||||
ui.painter().set(background_shape, shape);
|
||||
ui.allocate_rect(outer_rect, Sense::hover());
|
||||
|
||||
// Titlebar buttons
|
||||
if Self::close_button(ui, outer_rect).clicked() {
|
||||
response = Some(DrawGraphNodeResponse::DeleteNode(node_id));
|
||||
};
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
fn close_button(ui: &mut Ui, node_rect: Rect) -> Response {
|
||||
// Measurements
|
||||
let margin = 8.0;
|
||||
let size = 10.0;
|
||||
let stroke_width = 2.0;
|
||||
let offs = margin + size / 2.0;
|
||||
|
||||
let position = pos2(node_rect.right() - offs, node_rect.top() + offs);
|
||||
let rect = Rect::from_center_size(position, vec2(size, size));
|
||||
let resp = ui.allocate_rect(rect, Sense::click());
|
||||
|
||||
let color = if resp.clicked() {
|
||||
color_from_hex("#ffffff").unwrap()
|
||||
} else if resp.hovered() {
|
||||
color_from_hex("#dddddd").unwrap()
|
||||
} else {
|
||||
color_from_hex("#aaaaaa").unwrap()
|
||||
};
|
||||
let stroke = Stroke {
|
||||
width: stroke_width,
|
||||
color,
|
||||
};
|
||||
|
||||
ui.painter()
|
||||
.line_segment([rect.left_top(), rect.right_bottom()], stroke);
|
||||
ui.painter()
|
||||
.line_segment([rect.right_top(), rect.left_bottom()], stroke);
|
||||
|
||||
resp
|
||||
}
|
||||
|
||||
/// The port colors for all the data types
|
||||
fn data_type_color(data_type: DataType) -> egui::Color32 {
|
||||
match data_type {
|
||||
DataType::Mesh => color_from_hex("#266dd3").unwrap(),
|
||||
DataType::Vector => color_from_hex("#eecf6d").unwrap(),
|
||||
DataType::Scalar => color_from_hex("#eb9fef").unwrap(),
|
||||
DataType::Selection => color_from_hex("#4b7f52").unwrap(),
|
||||
DataType::Enum => color_from_hex("#ff0000").unwrap(), // Should never be in a port, so highlight in red
|
||||
DataType::NewFile => color_from_hex("#ff0000").unwrap(), // Should never be in a port, so highlight in red
|
||||
}
|
||||
}
|
||||
}
|
141
src/graph_types.rs
Normal file
141
src/graph_types.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
use nalgebra::Vector3 as Vec3;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use slotmap::SlotMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::id_types::*;
|
||||
use crate::SVec;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DataType {
|
||||
Vector,
|
||||
Scalar,
|
||||
Selection,
|
||||
Mesh,
|
||||
Enum,
|
||||
// The path to a (possibly new) file where export contents will be saved to
|
||||
NewFile,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum InputParamMetadata {
|
||||
MinMaxScalar { min: f32, max: f32 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum InputParamValue {
|
||||
Vector(Vec3<f32>),
|
||||
Scalar(f32),
|
||||
Selection {
|
||||
text: String,
|
||||
selection: Option<Vec<u32>>,
|
||||
},
|
||||
/// Used for parameters that can't have a value because they only accept
|
||||
/// connections.
|
||||
None,
|
||||
Enum {
|
||||
values: Vec<String>,
|
||||
selection: Option<u32>,
|
||||
},
|
||||
NewFile {
|
||||
path: Option<std::path::PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
/// There are three kinds of input params
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum InputParamKind {
|
||||
/// No constant value can be set. Only incoming connections can produce it
|
||||
ConnectionOnly,
|
||||
/// Only a constant value can be set. No incoming connections accepted.
|
||||
ConstantOnly,
|
||||
/// Both incoming connections and constants are accepted. Connections take
|
||||
/// precedence over the constant values.
|
||||
ConnectionOrConstant,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InputParam {
|
||||
pub id: InputId,
|
||||
/// The data type of this node. Used to determine incoming connections. This
|
||||
/// should always match the type of the InputParamValue, but the property is
|
||||
/// not actually enforced.
|
||||
pub typ: DataType,
|
||||
/// The constant value stored in this parameter.
|
||||
pub value: InputParamValue,
|
||||
/// A list of metadata fields, specifying things like bounds or limits.
|
||||
/// Metadata values that don't make sense for a type are ignored.
|
||||
pub metadata: SVec<InputParamMetadata>,
|
||||
/// The input kind. See [InputParamKind]
|
||||
pub kind: InputParamKind,
|
||||
/// Back-reference to the node containing this parameter.
|
||||
pub node: NodeId,
|
||||
}
|
||||
|
||||
impl InputParam {
|
||||
pub fn value(&self) -> InputParamValue {
|
||||
self.value.clone()
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> InputParamKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
pub fn node(&self) -> NodeId {
|
||||
self.node
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OutputParam {
|
||||
pub id: OutputId,
|
||||
/// Back-reference to the node containing this parameter.
|
||||
pub node: NodeId,
|
||||
pub typ: DataType,
|
||||
}
|
||||
|
||||
impl OutputParam {
|
||||
pub fn node(&self) -> NodeId {
|
||||
self.node
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Node {
|
||||
pub id: NodeId,
|
||||
pub label: String,
|
||||
pub op_name: String,
|
||||
pub inputs: Vec<(String, InputId)>,
|
||||
pub outputs: Vec<(String, OutputId)>,
|
||||
/// Executable nodes will run some code when their "Run" button is clicked
|
||||
pub is_executable: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Graph {
|
||||
pub nodes: SlotMap<NodeId, Node>,
|
||||
pub inputs: SlotMap<InputId, InputParam>,
|
||||
pub outputs: SlotMap<OutputId, OutputParam>,
|
||||
// Connects the input of a node, to the output of its predecessor that
|
||||
// produces it
|
||||
pub connections: HashMap<InputId, OutputId>,
|
||||
}
|
||||
|
||||
pub enum InputDescriptor {
|
||||
Vector { default: Vec3<f32> },
|
||||
Mesh,
|
||||
Selection,
|
||||
Scalar { default: f32, min: f32, max: f32 },
|
||||
Enum { values: Vec<String> },
|
||||
NewFile,
|
||||
}
|
||||
|
||||
pub struct OutputDescriptor(pub DataType);
|
||||
|
||||
pub struct NodeDescriptor {
|
||||
pub op_name: String,
|
||||
pub label: String,
|
||||
pub inputs: Vec<(String, InputDescriptor)>,
|
||||
pub outputs: Vec<(String, OutputDescriptor)>,
|
||||
pub is_executable: bool,
|
||||
}
|
24
src/id_types.rs
Normal file
24
src/id_types.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
slotmap::new_key_type! { pub struct NodeId; }
|
||||
slotmap::new_key_type! { pub struct InputId; }
|
||||
slotmap::new_key_type! { pub struct OutputId; }
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum AnyParameterId {
|
||||
Input(InputId),
|
||||
Output(OutputId),
|
||||
}
|
||||
|
||||
impl AnyParameterId {
|
||||
pub fn assume_input(&self) -> InputId {
|
||||
match self {
|
||||
AnyParameterId::Input(input) => *input,
|
||||
AnyParameterId::Output(output) => panic!("{:?} is not an InputId", output),
|
||||
}
|
||||
}
|
||||
pub fn assume_output(&self) -> OutputId {
|
||||
match self {
|
||||
AnyParameterId::Output(output) => *output,
|
||||
AnyParameterId::Input(input) => panic!("{:?} is not an OutputId", input),
|
||||
}
|
||||
}
|
||||
}
|
36
src/index_impls.rs
Normal file
36
src/index_impls.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use crate::graph_types::*;
|
||||
use crate::id_types::*;
|
||||
|
||||
macro_rules! impl_index_traits {
|
||||
($id_type:ty, $output_type:ty, $arena:ident) => {
|
||||
impl std::ops::Index<$id_type> for Graph {
|
||||
type Output = $output_type;
|
||||
|
||||
fn index(&self, index: $id_type) -> &Self::Output {
|
||||
self.$arena.get(index).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"{} index error for {:?}. Has the value been deleted?",
|
||||
stringify!($id_type),
|
||||
index
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::IndexMut<$id_type> for Graph {
|
||||
fn index_mut(&mut self, index: $id_type) -> &mut Self::Output {
|
||||
self.$arena.get_mut(index).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"{} index error for {:?}. Has the value been deleted?",
|
||||
stringify!($id_type),
|
||||
index
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_index_traits!(NodeId, Node, nodes);
|
||||
impl_index_traits!(InputId, InputParam, inputs);
|
||||
impl_index_traits!(OutputId, OutputParam, outputs);
|
41
src/main.rs
Normal file
41
src/main.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use editor_state::GraphEditorState;
|
||||
|
||||
mod graph_node_ui;
|
||||
|
||||
mod color_hex_utils;
|
||||
mod editor_state;
|
||||
mod graph_editor_egui;
|
||||
mod graph_impls;
|
||||
mod graph_types;
|
||||
mod id_types;
|
||||
mod index_impls;
|
||||
mod node_finder;
|
||||
mod node_types;
|
||||
mod param_ui;
|
||||
|
||||
pub type SVec<T> = smallvec::SmallVec<[T; 4]>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
pub state: GraphEditorState,
|
||||
}
|
||||
|
||||
impl epi::App for App {
|
||||
fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
graph_editor_egui::draw_graph_editor(&ctx, &mut self.state);
|
||||
});
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"Device Node Mapper"
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let options = eframe::NativeOptions {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(Box::new(App::default()), options);
|
||||
}
|
68
src/node_finder.rs
Normal file
68
src/node_finder.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use egui::*;
|
||||
|
||||
use crate::color_hex_utils::*;
|
||||
use crate::node_types::GraphNodeType;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NodeFinder {
|
||||
query: String,
|
||||
/// Reset every frame. When set, the node finder will be moved at that position
|
||||
pub position: Option<Pos2>,
|
||||
pub just_spawned: bool,
|
||||
}
|
||||
|
||||
impl NodeFinder {
|
||||
pub fn new_at(pos: Pos2) -> Self {
|
||||
NodeFinder {
|
||||
position: Some(pos),
|
||||
just_spawned: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows the node selector panel with a search bar. Returns whether a node
|
||||
/// archetype was selected and, in that case, the finder should be hidden on
|
||||
/// the next frame.
|
||||
pub fn show(&mut self, ui: &mut Ui) -> Option<GraphNodeType> {
|
||||
let background_color = color_from_hex("#3f3f3f").unwrap();
|
||||
let _titlebar_color = background_color.linear_multiply(0.8);
|
||||
let text_color = color_from_hex("#fefefe").unwrap();
|
||||
|
||||
ui.visuals_mut().widgets.noninteractive.fg_stroke = Stroke::new(2.0, text_color);
|
||||
|
||||
let frame = Frame::dark_canvas(ui.style())
|
||||
.fill(background_color)
|
||||
.margin(vec2(5.0, 5.0));
|
||||
|
||||
// The archetype that will be returned.
|
||||
let mut submitted_archetype = None;
|
||||
frame.show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
let resp = ui.text_edit_singleline(&mut self.query);
|
||||
if self.just_spawned {
|
||||
resp.request_focus();
|
||||
self.just_spawned = false;
|
||||
}
|
||||
|
||||
let mut query_submit = resp.lost_focus() && ui.input().key_down(Key::Enter);
|
||||
|
||||
Frame::default().margin(vec2(10.0, 10.0)).show(ui, |ui| {
|
||||
for archetype in GraphNodeType::all_types() {
|
||||
let archetype_name = archetype.type_label();
|
||||
if archetype_name.contains(self.query.as_str()) {
|
||||
if query_submit {
|
||||
submitted_archetype = Some(archetype);
|
||||
query_submit = false;
|
||||
}
|
||||
if ui.selectable_label(false, archetype_name).clicked() {
|
||||
submitted_archetype = Some(archetype);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
submitted_archetype
|
||||
}
|
||||
}
|
210
src/node_types.rs
Normal file
210
src/node_types.rs
Normal file
|
@ -0,0 +1,210 @@
|
|||
use nalgebra::Vector3 as Vec3;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::graph_types::*;
|
||||
|
||||
const ONE: Vec3<f32> = Vec3::new(1.0, 1.0, 1.0);
|
||||
const ZERO: Vec3<f32> = Vec3::new(0.0, 0.0, 0.0);
|
||||
const X: Vec3<f32> = Vec3::new(1.0, 0.0, 0.0);
|
||||
const Y: Vec3<f32> = Vec3::new(0.0, 1.0, 0.0);
|
||||
|
||||
#[derive(Clone, Copy, strum_macros::EnumIter)]
|
||||
pub enum GraphNodeType {
|
||||
MakeBox,
|
||||
MakeQuad,
|
||||
BevelEdges,
|
||||
ExtrudeFaces,
|
||||
ChamferVertices,
|
||||
MakeVector,
|
||||
VectorMath,
|
||||
MergeMeshes,
|
||||
ExportObj,
|
||||
}
|
||||
|
||||
macro_rules! in_vector {
|
||||
($name:expr, $default:expr) => {
|
||||
(
|
||||
$name.to_owned(),
|
||||
InputDescriptor::Vector { default: $default },
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! in_scalar {
|
||||
($name:expr, $default:expr, $min:expr, $max:expr) => {
|
||||
(
|
||||
$name.to_owned(),
|
||||
InputDescriptor::Scalar {
|
||||
default: $default,
|
||||
max: $max,
|
||||
min: $min,
|
||||
},
|
||||
)
|
||||
};
|
||||
($name:expr) => {
|
||||
in_scalar!($name, 0.0, -1.0, 2.0)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! in_mesh {
|
||||
($name:expr) => {
|
||||
($name.to_owned(), InputDescriptor::Mesh)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! out_mesh {
|
||||
($name:expr) => {
|
||||
($name.to_owned(), OutputDescriptor(DataType::Mesh))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! out_vector {
|
||||
($name:expr) => {
|
||||
($name.to_owned(), OutputDescriptor(DataType::Vector))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! in_selection {
|
||||
($name:expr) => {
|
||||
($name.to_owned(), InputDescriptor::Selection)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! in_file {
|
||||
($name:expr) => {
|
||||
($name.to_owned(), InputDescriptor::NewFile)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! in_enum {
|
||||
($name:expr, $( $values:expr ),+) => {
|
||||
($name.to_owned(), InputDescriptor::Enum { values: vec![$( $values.to_owned() ),+] })
|
||||
};
|
||||
}
|
||||
|
||||
impl GraphNodeType {
|
||||
pub fn to_descriptor(&self) -> NodeDescriptor {
|
||||
let label = self.type_label().into();
|
||||
let op_name = self.op_name().into();
|
||||
match self {
|
||||
GraphNodeType::MakeBox => NodeDescriptor {
|
||||
op_name,
|
||||
label,
|
||||
inputs: vec![in_vector!("origin", ZERO), in_vector!("size", ONE)],
|
||||
outputs: vec![out_mesh!("out_mesh")],
|
||||
is_executable: false,
|
||||
},
|
||||
GraphNodeType::MakeQuad => NodeDescriptor {
|
||||
op_name,
|
||||
label,
|
||||
inputs: vec![
|
||||
in_vector!("center", ZERO),
|
||||
in_vector!("normal", Y),
|
||||
in_vector!("right", X),
|
||||
in_vector!("size", ONE),
|
||||
],
|
||||
outputs: vec![out_mesh!("out_mesh")],
|
||||
is_executable: false,
|
||||
},
|
||||
GraphNodeType::BevelEdges => NodeDescriptor {
|
||||
op_name,
|
||||
label,
|
||||
inputs: vec![
|
||||
in_mesh!("in_mesh"),
|
||||
in_selection!("edges"),
|
||||
in_scalar!("amount", 0.0, 0.0, 1.0),
|
||||
],
|
||||
outputs: vec![out_mesh!("out_mesh")],
|
||||
is_executable: false,
|
||||
},
|
||||
GraphNodeType::ExtrudeFaces => NodeDescriptor {
|
||||
op_name,
|
||||
label,
|
||||
inputs: vec![
|
||||
in_mesh!("in_mesh"),
|
||||
in_selection!("faces"),
|
||||
in_scalar!("amount", 0.0, 0.0, 1.0),
|
||||
],
|
||||
outputs: vec![out_mesh!("out_mesh")],
|
||||
is_executable: false,
|
||||
},
|
||||
GraphNodeType::ChamferVertices => NodeDescriptor {
|
||||
op_name,
|
||||
label,
|
||||
inputs: vec![
|
||||
in_mesh!("in_mesh"),
|
||||
in_selection!("vertices"),
|
||||
in_scalar!("amount", 0.0, 0.0, 1.0),
|
||||
],
|
||||
outputs: vec![out_mesh!("out_mesh")],
|
||||
is_executable: false,
|
||||
},
|
||||
GraphNodeType::MakeVector => NodeDescriptor {
|
||||
op_name,
|
||||
label,
|
||||
inputs: vec![in_scalar!("x"), in_scalar!("y"), in_scalar!("z")],
|
||||
outputs: vec![out_vector!("out_vec")],
|
||||
is_executable: false,
|
||||
},
|
||||
GraphNodeType::VectorMath => NodeDescriptor {
|
||||
op_name,
|
||||
label,
|
||||
inputs: vec![
|
||||
in_enum!("vec_op", "ADD", "SUB"),
|
||||
in_vector!("A", ZERO),
|
||||
in_vector!("B", ZERO),
|
||||
],
|
||||
outputs: vec![out_vector!("out_vec")],
|
||||
is_executable: false,
|
||||
},
|
||||
GraphNodeType::MergeMeshes => NodeDescriptor {
|
||||
op_name,
|
||||
label,
|
||||
inputs: vec![in_mesh!("A"), in_mesh!("B")],
|
||||
outputs: vec![out_mesh!("out_mesh")],
|
||||
is_executable: false,
|
||||
},
|
||||
GraphNodeType::ExportObj => NodeDescriptor {
|
||||
op_name,
|
||||
label,
|
||||
inputs: vec![in_mesh!("mesh"), in_file!("export_path")],
|
||||
outputs: vec![],
|
||||
is_executable: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_types() -> impl Iterator<Item = GraphNodeType> {
|
||||
GraphNodeType::iter()
|
||||
}
|
||||
|
||||
pub fn type_label(&self) -> &'static str {
|
||||
match self {
|
||||
GraphNodeType::MakeBox => "Box",
|
||||
GraphNodeType::MakeQuad => "Quad",
|
||||
GraphNodeType::BevelEdges => "Bevel edges",
|
||||
GraphNodeType::ExtrudeFaces => "Extrude faces",
|
||||
GraphNodeType::ChamferVertices => "Chamfer vertices",
|
||||
GraphNodeType::MakeVector => "Vector",
|
||||
GraphNodeType::VectorMath => "Vector math",
|
||||
GraphNodeType::MergeMeshes => "Merge meshes",
|
||||
GraphNodeType::ExportObj => "OBJ Export",
|
||||
}
|
||||
}
|
||||
|
||||
/// The op_name is used by the graph compiler in graph_compiler.rs to select
|
||||
/// which PolyASM instructions to emit.
|
||||
pub fn op_name(&self) -> &'static str {
|
||||
match self {
|
||||
GraphNodeType::MakeBox => "MakeBox",
|
||||
GraphNodeType::MakeQuad => "MakeQuad",
|
||||
GraphNodeType::BevelEdges => "BevelEdges",
|
||||
GraphNodeType::ExtrudeFaces => "ExtrudeFaces",
|
||||
GraphNodeType::ChamferVertices => "ChamferVertices",
|
||||
GraphNodeType::MakeVector => "MakeVector",
|
||||
GraphNodeType::VectorMath => "VectorMath",
|
||||
GraphNodeType::MergeMeshes => "MergeMeshes",
|
||||
GraphNodeType::ExportObj => "ExportObj",
|
||||
}
|
||||
}
|
||||
}
|
89
src/param_ui.rs
Normal file
89
src/param_ui.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
use anyhow::Result;
|
||||
use egui::*;
|
||||
|
||||
use crate::graph_types::*;
|
||||
|
||||
impl InputParam {
|
||||
pub fn value_widget(&mut self, name: &str, ui: &mut Ui) {
|
||||
match &mut self.value {
|
||||
InputParamValue::Vector(vector) => {
|
||||
ui.label(name);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("x");
|
||||
ui.add(egui::DragValue::new(&mut vector.x).speed(0.1));
|
||||
ui.label("y");
|
||||
ui.add(egui::DragValue::new(&mut vector.y).speed(0.1));
|
||||
ui.label("z");
|
||||
ui.add(egui::DragValue::new(&mut vector.z).speed(0.1));
|
||||
});
|
||||
}
|
||||
InputParamValue::Scalar(scalar) => {
|
||||
let mut min = f32::NEG_INFINITY;
|
||||
let mut max = f32::INFINITY;
|
||||
for metadata in &self.metadata {
|
||||
match metadata {
|
||||
InputParamMetadata::MinMaxScalar {
|
||||
min: min_val,
|
||||
max: max_val,
|
||||
} => {
|
||||
min = *min_val;
|
||||
max = *max_val;
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(name);
|
||||
ui.add(Slider::new(scalar, min..=max));
|
||||
});
|
||||
}
|
||||
InputParamValue::Selection { text, selection } => {
|
||||
if ui.text_edit_singleline(text).changed() {
|
||||
*selection = text
|
||||
.split(',')
|
||||
.map(|x| {
|
||||
x.parse::<u32>()
|
||||
.map_err(|_| anyhow::anyhow!("Cannot parse number"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
InputParamValue::None => {
|
||||
ui.label(name);
|
||||
}
|
||||
InputParamValue::Enum { values, selection } => {
|
||||
let selected = if let Some(selection) = selection {
|
||||
values[*selection as usize].clone()
|
||||
} else {
|
||||
"".to_owned()
|
||||
};
|
||||
ComboBox::from_label(name)
|
||||
.selected_text(selected)
|
||||
.show_ui(ui, |ui| {
|
||||
for (idx, value) in values.iter().enumerate() {
|
||||
ui.selectable_value(selection, Some(idx as u32), value);
|
||||
}
|
||||
});
|
||||
}
|
||||
InputParamValue::NewFile { path } => {
|
||||
ui.label(name);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Select").clicked() {
|
||||
*path = rfd::FileDialog::new().save_file();
|
||||
}
|
||||
if let Some(ref path) = path {
|
||||
ui.label(
|
||||
path.clone()
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.unwrap_or_else(|_| "<Invalid string>".to_owned()),
|
||||
);
|
||||
} else {
|
||||
ui.label("No file selected");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue