Initial commit

This commit is contained in:
hodasemi 2022-01-30 10:53:40 +01:00
commit 6e5565d45c
16 changed files with 1548 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
Cargo.lock

26
.vscode/launch.json vendored Normal file
View 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
View file

@ -0,0 +1,7 @@
{
"workbench.colorCustomizations": {
"activityBar.background": "#063147",
"titleBar.activeBackground": "#094563",
"titleBar.activeForeground": "#F6FBFE"
}
}

19
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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(&param_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(&param_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
View 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
View 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
View 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
View 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
View 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
View 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
View 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");
}
});
}
}
}
}