Integrate the node graph as a Node Graph Frame layer type (#812)

* Add node graph frame tool

* Add a brighten

* Use the node graph

* Fix topological_sort

* Update UI

* Add icons for the tool and layer type

* Avoid serde & use bitmaps to improve performance

* Allow serialising a node graph

* Fix missing ..Default::default()

* Fix incorrect comments

* Cache node graph output image

* Suppress no-cycle import warning

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2022-11-05 14:38:14 -07:00
committed by GitHub
co-authored by Keavon Chambers
parent 596b9f4531
commit 9cdebfb1f0
33 changed files with 1018 additions and 258 deletions
Generated
+9
View File
@@ -360,6 +360,7 @@ dependencies = [
"graphene-std", "graphene-std",
"num-traits", "num-traits",
"rand_chacha", "rand_chacha",
"serde",
] ]
[[package]] [[package]]
@@ -405,11 +406,17 @@ version = "0.0.0"
dependencies = [ dependencies = [
"bezier-rs", "bezier-rs",
"bitflags", "bitflags",
"borrow_stack",
"derivative", "derivative",
"dyn-any",
"env_logger", "env_logger",
"glam", "glam",
"graph-craft",
"graphene-core",
"graphene-std",
"graphite-graphene", "graphite-graphene",
"graphite-proc-macros", "graphite-proc-macros",
"image",
"kurbo", "kurbo",
"log", "log",
"once_cell", "once_cell",
@@ -429,6 +436,8 @@ dependencies = [
"base64", "base64",
"bezier-rs", "bezier-rs",
"glam", "glam",
"graph-craft",
"image",
"kurbo", "kurbo",
"log", "log",
"rustybuzz", "rustybuzz",
+8
View File
@@ -28,6 +28,14 @@ remain = "0.2.2"
derivative = "2.2.0" derivative = "2.2.0"
once_cell = "1.13.0" # Remove when `core::cell::OnceCell` is stabilized (<https://doc.rust-lang.org/core/cell/struct.OnceCell.html>) once_cell = "1.13.0" # Remove when `core::cell::OnceCell` is stabilized (<https://doc.rust-lang.org/core/cell/struct.OnceCell.html>)
# Node graph
image = { version = "0.24", default-features = false, features = ["bmp"] }
graph-craft = { path = "../node-graph/graph-craft" }
borrow_stack = { path = "../node-graph/borrow_stack" }
dyn-any = { path = "../libraries/dyn-any" }
graphene-core = { path = "../node-graph/gcore" }
graphene-std = { path = "../node-graph/gstd" }
[dependencies.graphene] [dependencies.graphene]
path = "../graphene" path = "../graphene"
package = "graphite-graphene" package = "graphite-graphene"
@@ -86,6 +86,14 @@ pub enum FrontendMessage {
}, },
TriggerLoadAutoSaveDocuments, TriggerLoadAutoSaveDocuments,
TriggerLoadPreferences, TriggerLoadPreferences,
TriggerNodeGraphFrameGenerate {
#[serde(rename = "documentId")]
document_id: u64,
#[serde(rename = "layerPath")]
layer_path: Vec<LayerId>,
svg: String,
size: glam::DVec2,
},
TriggerOpenDocument, TriggerOpenDocument,
TriggerPaste, TriggerPaste,
TriggerRasterDownload { TriggerRasterDownload {
@@ -123,6 +123,13 @@ pub fn default_mapping() -> Mapping {
entry!(KeyDown(Escape); action_dispatch=ImaginateToolMessage::Abort), entry!(KeyDown(Escape); action_dispatch=ImaginateToolMessage::Abort),
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=ImaginateToolMessage::Resize { center: Alt, lock_ratio: Shift }), entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=ImaginateToolMessage::Resize { center: Alt, lock_ratio: Shift }),
// //
// NodeGraphFrameToolMessage
entry!(KeyDown(Lmb); action_dispatch=NodeGraphFrameToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=NodeGraphFrameToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=NodeGraphFrameToolMessage::Abort),
entry!(KeyDown(Escape); action_dispatch=NodeGraphFrameToolMessage::Abort),
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=NodeGraphFrameToolMessage::Resize { center: Alt, lock_ratio: Shift }),
//
// EllipseToolMessage // EllipseToolMessage
entry!(KeyDown(Lmb); action_dispatch=EllipseToolMessage::DragStart), entry!(KeyDown(Lmb); action_dispatch=EllipseToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=EllipseToolMessage::DragStop), entry!(KeyUp(Lmb); action_dispatch=EllipseToolMessage::DragStop),
@@ -73,8 +73,8 @@ pub enum DocumentMessage {
FolderChanged { FolderChanged {
affected_folder_path: Vec<LayerId>, affected_folder_path: Vec<LayerId>,
}, },
FrameClear,
GroupSelectedLayers, GroupSelectedLayers,
ImaginateClear,
ImaginateGenerate, ImaginateGenerate,
ImaginateTerminate, ImaginateTerminate,
LayerChanged { LayerChanged {
@@ -89,6 +89,7 @@ pub enum DocumentMessage {
layer_path: Vec<LayerId>, layer_path: Vec<LayerId>,
delta: (f64, f64), delta: (f64, f64),
}, },
NodeGraphFrameGenerate,
NudgeSelectedLayers { NudgeSelectedLayers {
delta_x: f64, delta_x: f64,
delta_y: f64, delta_y: f64,
@@ -377,6 +377,30 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
let affected_layer_path = affected_folder_path; let affected_layer_path = affected_folder_path;
responses.extend([LayerChanged { affected_layer_path }.into(), DocumentStructureChanged.into()]); responses.extend([LayerChanged { affected_layer_path }.into(), DocumentStructureChanged.into()]);
} }
FrameClear => {
let mut selected_frame_layers = self
.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate)
.chain(self.selected_layers_with_type(LayerDataTypeDiscriminant::NodeGraphFrame));
// Get what is hopefully the only selected Imaginate/NodeGraphFrame layer
let layer_path = selected_frame_layers.next();
// Abort if we didn't have any Imaginate/NodeGraphFrame layer, or if there are additional ones also selected
if layer_path.is_none() || selected_frame_layers.next().is_some() {
return;
}
let layer_path = layer_path.unwrap();
let layer = self.graphene_document.layer(layer_path).expect("Clearing Imaginate/NodeGraphFrame image for invalid layer");
let previous_blob_url = match &layer.data {
LayerDataType::Imaginate(imaginate) => &imaginate.blob_url,
LayerDataType::NodeGraphFrame(node_graph_frame) => &node_graph_frame.blob_url,
x => panic!("Cannot find blob url for layer type {}", LayerDataTypeDiscriminant::from(x)),
};
if let Some(url) = previous_blob_url {
responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into());
}
responses.push_back(DocumentOperation::ClearBlobURL { path: layer_path.into() }.into());
}
GroupSelectedLayers => { GroupSelectedLayers => {
let mut new_folder_path = self.graphene_document.shallowest_common_folder(self.selected_layers()).unwrap_or(&[]).to_vec(); let mut new_folder_path = self.graphene_document.shallowest_common_folder(self.selected_layers()).unwrap_or(&[]).to_vec();
@@ -406,24 +430,6 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
.into(), .into(),
); );
} }
ImaginateClear => {
let mut selected_imaginate_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate);
// Get what is hopefully the only selected Imaginate layer
let layer_path = selected_imaginate_layers.next();
// Abort if we didn't have any Imaginate layer, or if there are additional ones also selected
if layer_path.is_none() || selected_imaginate_layers.next().is_some() {
return;
}
let layer_path = layer_path.unwrap();
let layer = self.graphene_document.layer(layer_path).expect("Clearing Imaginate image for invalid layer");
let previous_blob_url = &layer.as_imaginate().unwrap().blob_url;
if let Some(url) = previous_blob_url {
responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into());
}
responses.push_back(DocumentOperation::ImaginateClear { path: layer_path.into() }.into());
}
ImaginateGenerate => { ImaginateGenerate => {
if let Some(message) = self.call_imaginate(document_id, preferences, persistent_data) { if let Some(message) = self.call_imaginate(document_id, preferences, persistent_data) {
// TODO: Eventually remove this after a message system ordering architectural change // TODO: Eventually remove this after a message system ordering architectural change
@@ -505,6 +511,11 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
responses.push_back(DocumentOperation::MoveSelectedManipulatorPoints { layer_path, delta }.into()); responses.push_back(DocumentOperation::MoveSelectedManipulatorPoints { layer_path, delta }.into());
} }
} }
NodeGraphFrameGenerate => {
if let Some(message) = self.call_node_graph_frame(document_id, preferences, persistent_data) {
responses.push_back(message);
}
}
NudgeSelectedLayers { delta_x, delta_y } => { NudgeSelectedLayers { delta_x, delta_y } => {
self.backup(responses); self.backup(responses);
for path in self.selected_layers().map(|path| path.to_vec()) { for path in self.selected_layers().map(|path| path.to_vec()) {
@@ -705,8 +716,16 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into()); responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into());
} }
} }
LayerDataType::NodeGraphFrame(node_graph_frame) => {
if let Some(url) = &node_graph_frame.blob_url {
responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into());
}
}
LayerDataType::Image(_) => {} LayerDataType::Image(_) => {}
other => panic!("Setting blob URL for invalid layer type, which must be an `Imaginate` or `Image`. Found: `{:?}`", other), other => panic!(
"Setting blob URL for invalid layer type, which must be an `Imaginate`, `NodeGraphFrame` or `Image`. Found: `{:?}`",
other
),
} }
responses.push_back( responses.push_back(
@@ -936,7 +955,7 @@ impl DocumentMessageHandler {
}; };
let base_image = if imaginate_layer.use_img2img { let base_image = if imaginate_layer.use_img2img {
// Calculate the size of the region to be exported // Calculate the size of the region to be exported
let size = transform.transform_point2(DVec2::ONE) - transform.transform_point2(DVec2::ZERO); let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length());
let old_transforms = self.remove_document_transform(); let old_transforms = self.remove_document_transform();
let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path)); let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path));
@@ -960,6 +979,32 @@ impl DocumentMessageHandler {
) )
} }
pub fn call_node_graph_frame(&mut self, document_id: u64, _preferences: &PreferencesMessageHandler, persistent_data: &PersistentData) -> Option<Message> {
let layer_path = {
let mut selected_nodegraph_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::NodeGraphFrame);
// Get what is hopefully the only selected nodegraph layer
match selected_nodegraph_layers.next() {
// Continue only if there are no additional nodegraph layers also selected
Some(layer_path) if selected_nodegraph_layers.next().is_none() => layer_path.to_owned(),
_ => return None,
}
};
// Prepare the node graph base image base image
// Calculate the size of the region to be exported
let old_transforms = self.remove_document_transform();
let transform = self.graphene_document.multiply_transforms(&layer_path).unwrap();
let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length());
let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path));
self.restore_document_transform(old_transforms);
Some(FrontendMessage::TriggerNodeGraphFrameGenerate { document_id, layer_path, svg, size }.into())
}
/// Remove the artwork and artboard pan/tilt/zoom to render it without the user's viewport navigation, and save it to be restored at the end /// Remove the artwork and artboard pan/tilt/zoom to render it without the user's viewport navigation, and save it to be restored at the end
fn remove_document_transform(&mut self) -> [DAffine2; 2] { fn remove_document_transform(&mut self) -> [DAffine2; 2] {
let old_artwork_transform = self.graphene_document.root.transform; let old_artwork_transform = self.graphene_document.root.transform;
@@ -1474,6 +1519,15 @@ impl DocumentMessageHandler {
}); });
} }
} }
LayerDataType::NodeGraphFrame(node_graph_frame) => {
if let Some(data) = &node_graph_frame.image_data {
image_data.push(FrontendImageData {
path: path.clone(),
image_data: data.image_data.clone(),
mime: node_graph_frame.mime.clone(),
});
}
}
_ => {} _ => {}
} }
} }
@@ -14,7 +14,8 @@ use crate::messages::prelude::*;
use graphene::color::Color; use graphene::color::Color;
use graphene::document::pick_layer_safe_imaginate_resolution; use graphene::document::pick_layer_safe_imaginate_resolution;
use graphene::layers::imaginate_layer::{ImaginateLayer, ImaginateSamplingMethod, ImaginateStatus}; use graphene::layers::imaginate_layer::{ImaginateLayer, ImaginateSamplingMethod, ImaginateStatus};
use graphene::layers::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDiscriminant}; use graphene::layers::layer_info::{Layer, LayerDataType, LayerDataTypeDiscriminant};
use graphene::layers::nodegraph_layer::NodeGraphFrameLayer;
use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke}; use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke};
use graphene::layers::text_layer::{FontCache, TextLayer}; use graphene::layers::text_layer::{FontCache, TextLayer};
@@ -251,6 +252,11 @@ pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque
tooltip: "Imaginate".into(), tooltip: "Imaginate".into(),
..Default::default() ..Default::default()
})), })),
LayerDataType::NodeGraphFrame(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
icon: "NodeNodes".into(),
tooltip: "Node Graph Frame".into(),
..Default::default()
})),
}, },
WidgetHolder::new(Widget::Separator(Separator { WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Related, separator_type: SeparatorType::Related,
@@ -307,6 +313,9 @@ pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque
LayerDataType::Imaginate(imaginate) => { LayerDataType::Imaginate(imaginate) => {
vec![node_section_transform(layer, persistent_data), node_section_imaginate(imaginate, layer, persistent_data, responses)] vec![node_section_transform(layer, persistent_data), node_section_imaginate(imaginate, layer, persistent_data, responses)]
} }
LayerDataType::NodeGraphFrame(node_graph_frame) => {
vec![node_section_transform(layer, persistent_data), node_section_node_graph_frame(node_graph_frame)]
}
LayerDataType::Folder(_) => { LayerDataType::Folder(_) => {
vec![node_section_transform(layer, persistent_data)] vec![node_section_transform(layer, persistent_data)]
} }
@@ -659,8 +668,8 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi
WidgetHolder::new(Widget::TextButton(TextButton { WidgetHolder::new(Widget::TextButton(TextButton {
label: "Clear".into(), label: "Clear".into(),
tooltip: "Remove generated image from the layer frame".into(), tooltip: "Remove generated image from the layer frame".into(),
disabled: imaginate_layer.blob_url == None, disabled: imaginate_layer.blob_url.is_none(),
on_update: WidgetCallback::new(|_| DocumentMessage::ImaginateClear.into()), on_update: WidgetCallback::new(|_| DocumentMessage::FrameClear.into()),
..Default::default() ..Default::default()
})), })),
], ],
@@ -1009,6 +1018,55 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi
} }
} }
fn node_section_node_graph_frame(node_graph_frame: &NodeGraphFrameLayer) -> LayoutGroup {
LayoutGroup::Section {
name: "Node Graph Frame".into(),
layout: vec![
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Temporary layer that applies a greyscale to the layers below it.".into(),
..TextLabel::default()
}))],
},
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Powered by the node graph :)".into(),
..TextLabel::default()
}))],
},
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextButton(TextButton {
label: "Open Node Graph UI (todo)".into(),
tooltip: "Open the node graph associated with this layer".into(),
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(800) }.into()),
..Default::default()
}))],
},
LayoutGroup::Row {
widgets: vec![
WidgetHolder::new(Widget::TextButton(TextButton {
label: "Generate".into(),
tooltip: "Fill layer frame by generating a new image".into(),
on_update: WidgetCallback::new(|_| DocumentMessage::NodeGraphFrameGenerate.into()),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Related,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextButton(TextButton {
label: "Clear".into(),
tooltip: "Remove generated image from the layer frame".into(),
disabled: node_graph_frame.blob_url.is_none(),
on_update: WidgetCallback::new(|_| DocumentMessage::FrameClear.into()),
..Default::default()
})),
],
},
],
}
}
fn node_section_font(layer: &TextLayer) -> LayoutGroup { fn node_section_font(layer: &TextLayer) -> LayoutGroup {
let font = layer.font.clone(); let font = layer.font.clone();
let size = layer.size; let size = layer.size;
@@ -1425,7 +1483,7 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup {
direction: SeparatorDirection::Horizontal, direction: SeparatorDirection::Horizontal,
})), })),
WidgetHolder::new(Widget::NumberInput(NumberInput { WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(stroke.weight() as f64), value: Some(stroke.weight()),
is_integer: false, is_integer: false,
min: Some(0.), min: Some(0.),
unit: " px".into(), unit: " px".into(),
@@ -1473,7 +1531,7 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup {
direction: SeparatorDirection::Horizontal, direction: SeparatorDirection::Horizontal,
})), })),
WidgetHolder::new(Widget::NumberInput(NumberInput { WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(stroke.dash_offset() as f64), value: Some(stroke.dash_offset()),
is_integer: true, is_integer: true,
min: Some(0.), min: Some(0.),
unit: " px".into(), unit: " px".into(),
@@ -109,6 +109,12 @@ pub enum PortfolioMessage {
data: String, data: String,
}, },
PrevDocument, PrevDocument,
ProcessNodeGraphFrame {
document_id: u64,
layer_path: Vec<LayerId>,
image_data: Vec<u8>,
size: (u32, u32),
},
SelectDocument { SelectDocument {
document_id: u64, document_id: u64,
}, },
@@ -2,16 +2,16 @@ use super::utility_types::PersistentData;
use crate::application::generate_uuid; use crate::application::generate_uuid;
use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION}; use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION};
use crate::messages::dialog::simple_dialogs; use crate::messages::dialog::simple_dialogs;
use crate::messages::frontend::utility_types::FrontendDocumentDetails; use crate::messages::frontend::utility_types::{FrontendDocumentDetails, FrontendImageData};
use crate::messages::layout::utility_types::layout_widget::PropertyHolder; use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT}; use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use crate::messages::portfolio::utility_types::ImaginateServerStatus; use crate::messages::portfolio::utility_types::ImaginateServerStatus;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene::layers::layer_info::LayerDataTypeDiscriminant; use graphene::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant};
use graphene::layers::text_layer::Font; use graphene::layers::text_layer::Font;
use graphene::Operation as DocumentOperation; use graphene::{LayerId, Operation as DocumentOperation};
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct PortfolioMessageHandler { pub struct PortfolioMessageHandler {
@@ -413,6 +413,86 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
responses.push_back(PortfolioMessage::SelectDocument { document_id: prev_id }.into()); responses.push_back(PortfolioMessage::SelectDocument { document_id: prev_id }.into());
} }
} }
PortfolioMessage::ProcessNodeGraphFrame {
document_id,
layer_path,
image_data,
size,
} => {
fn read_image(document: Option<&DocumentMessageHandler>, layer_path: &[LayerId], image_data: Vec<u8>, (width, height): (u32, u32)) -> Result<Vec<u8>, String> {
use graphene_std::raster::Image;
use image::{ImageBuffer, Rgba};
use std::io::Cursor;
let data = image_data.chunks_exact(4).map(|v| graphene_core::raster::color::Color::from_rgba8(v[0], v[1], v[2], v[3])).collect();
let image = graphene_std::raster::Image { width, height, data };
let document = document.ok_or_else(|| "Invalid document".to_string())?;
let layer = document.graphene_document.layer(layer_path).map_err(|e| format!("No layer: {e:?}"))?;
let node_graph_frame = match &layer.data {
LayerDataType::NodeGraphFrame(frame) => Ok(frame),
_ => Err("Invalid layer type".to_string()),
}?;
// Execute the node graph
let mut network = node_graph_frame.network.clone();
let stack = borrow_stack::FixedSizeStack::new(256);
network.flatten(0);
let mut proto_network = network.into_proto_network();
proto_network.reorder_ids();
for (_id, node) in proto_network.nodes {
graph_craft::node_registry::push_node(node, &stack);
}
use borrow_stack::BorrowStack;
use dyn_any::IntoDynAny;
use graphene_core::Node;
let result = unsafe { stack.get().last().unwrap().eval(image.into_dyn()) };
let result = *dyn_any::downcast::<Image>(result).unwrap();
let mut bytes: Vec<u8> = Vec::new();
let [result_width, result_height] = [result.width, result.height];
let size_estimate = (result_width * result_height * 4) as usize;
let mut result_bytes = Vec::with_capacity(size_estimate);
result_bytes.extend(result.data.into_iter().flat_map(|colour| colour.to_rgba8()));
let output: ImageBuffer<Rgba<u8>, _> = image::ImageBuffer::from_raw(result_width, result_height, result_bytes).ok_or_else(|| "Invalid image size".to_string())?;
output.write_to(&mut Cursor::new(&mut bytes), image::ImageOutputFormat::Bmp).map_err(|e| e.to_string())?;
Ok(bytes)
}
match read_image(self.documents.get(&document_id), &layer_path, image_data, size) {
Ok(image_data) => {
responses.push_back(
DocumentOperation::SetNodeGraphFrameImageData {
layer_path: layer_path.clone(),
image_data: image_data.clone(),
}
.into(),
);
let mime = "image/bmp".to_string();
responses.push_back(
FrontendMessage::UpdateImageData {
document_id,
image_data: vec![FrontendImageData { path: layer_path, image_data, mime }],
}
.into(),
);
}
Err(description) => responses.push_back(
DialogMessage::DisplayDialogError {
title: "Failed to update image".to_string(),
description,
}
.into(),
),
}
}
PortfolioMessage::SelectDocument { document_id } => { PortfolioMessage::SelectDocument { document_id } => {
if let Some(document) = self.active_document() { if let Some(document) = self.active_document() {
if !document.is_auto_saved() { if !document.is_auto_saved() {
+1
View File
@@ -37,6 +37,7 @@ pub use crate::messages::tool::tool_messages::gradient_tool::{GradientToolMessag
pub use crate::messages::tool::tool_messages::imaginate_tool::{ImaginateToolMessage, ImaginateToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::imaginate_tool::{ImaginateToolMessage, ImaginateToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::line_tool::{LineToolMessage, LineToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::line_tool::{LineToolMessage, LineToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::navigate_tool::{NavigateToolMessage, NavigateToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::navigate_tool::{NavigateToolMessage, NavigateToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::node_graph_frame_tool::{NodeGraphFrameToolMessage, NodeGraphFrameToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::path_tool::{PathToolMessage, PathToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::path_tool::{PathToolMessage, PathToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::pen_tool::{PenToolMessage, PenToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::pen_tool::{PenToolMessage, PenToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::rectangle_tool::{RectangleToolMessage, RectangleToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::rectangle_tool::{RectangleToolMessage, RectangleToolMessageDiscriminant};
+5
View File
@@ -78,6 +78,9 @@ pub enum ToolMessage {
#[remain::unsorted] #[remain::unsorted]
#[child] #[child]
Imaginate(ImaginateToolMessage), Imaginate(ImaginateToolMessage),
#[remain::unsorted]
#[child]
NodeGraphFrame(NodeGraphFrameToolMessage),
// Messages // Messages
#[remain::unsorted] #[remain::unsorted]
@@ -114,6 +117,8 @@ pub enum ToolMessage {
#[remain::unsorted] #[remain::unsorted]
ActivateToolImaginate, ActivateToolImaginate,
#[remain::unsorted]
ActivateToolNodeGraphFrame,
ActivateTool { ActivateTool {
tool_type: ToolType, tool_type: ToolType,
@@ -7,6 +7,7 @@ pub mod gradient_tool;
pub mod imaginate_tool; pub mod imaginate_tool;
pub mod line_tool; pub mod line_tool;
pub mod navigate_tool; pub mod navigate_tool;
pub mod node_graph_frame_tool;
pub mod path_tool; pub mod path_tool;
pub mod pen_tool; pub mod pen_tool;
pub mod rectangle_tool; pub mod rectangle_tool;
@@ -0,0 +1,231 @@
use crate::consts::DRAG_THRESHOLD;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::Operation;
use glam::DAffine2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct NodeGraphFrameTool {
fsm_state: NodeGraphToolFsmState,
tool_data: NodeGraphToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, NodeGraphFrame)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum NodeGraphFrameToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
// Tool-specific messages
DragStart,
DragStop,
Resize {
center: Key,
lock_ratio: Key,
},
}
impl PropertyHolder for NodeGraphFrameTool {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for NodeGraphFrameTool {
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &(), responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
fn actions(&self) -> ActionList {
use NodeGraphToolFsmState::*;
match self.fsm_state {
Ready => actions!(NodeGraphFrameToolMessageDiscriminant;
DragStart,
),
Drawing => actions!(NodeGraphFrameToolMessageDiscriminant;
DragStop,
Abort,
Resize,
),
}
}
}
impl ToolMetadata for NodeGraphFrameTool {
fn icon_name(&self) -> String {
"RasterNodesTool".into()
}
fn tooltip(&self) -> String {
"Node Graph Frame Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::NodeGraphFrame
}
}
impl ToolTransition for NodeGraphFrameTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(NodeGraphFrameToolMessage::Abort.into()),
selection_changed: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum NodeGraphToolFsmState {
Ready,
Drawing,
}
impl Default for NodeGraphToolFsmState {
fn default() -> Self {
NodeGraphToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct NodeGraphToolData {
data: Resize,
}
impl Fsm for NodeGraphToolFsmState {
type ToolData = NodeGraphToolData;
type ToolOptions = ();
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, _document_id, _global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use NodeGraphFrameToolMessage::*;
use NodeGraphToolFsmState::*;
let mut shape_data = &mut tool_data.data;
if let ToolMessage::NodeGraphFrame(event) = event {
match (self, event) {
(Ready, DragStart) => {
shape_data.start(responses, document, input.mouse.position, font_cache);
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(document.get_path_for_new_layer());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(
Operation::AddNodeGraphFrame {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
}
.into(),
);
Drawing
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(responses, document, center, lock_ratio, input) {
responses.push_back(message);
}
state
}
(Drawing, DragStop) => {
match shape_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
shape_data.cleanup(responses);
Ready
}
(Drawing, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.cleanup(responses);
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
NodeGraphToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Repaint Frame"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::Shift])],
key_groups_mac: None,
mouse: None,
label: String::from("Constrain Square"),
plus: true,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::Alt])],
key_groups_mac: None,
mouse: None,
label: String::from("From Center"),
plus: true,
},
])]),
NodeGraphToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::Shift])],
key_groups_mac: None,
mouse: None,
label: String::from("Constrain Square"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::Alt])],
key_groups_mac: None,
mouse: None,
label: String::from("From Center"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into());
}
}
+20 -16
View File
@@ -281,6 +281,7 @@ pub enum ToolType {
Detail, Detail,
Relight, Relight,
Imaginate, Imaginate,
NodeGraphFrame,
} }
enum ToolAvailability { enum ToolAvailability {
@@ -293,28 +294,29 @@ fn list_tools_in_groups() -> Vec<Vec<ToolAvailability>> {
vec![ vec![
vec![ vec![
// General tool group // General tool group
ToolAvailability::Available(Box::new(select_tool::SelectTool::default())), ToolAvailability::Available(Box::<select_tool::SelectTool>::default()),
ToolAvailability::Available(Box::new(artboard_tool::ArtboardTool::default())), ToolAvailability::Available(Box::<artboard_tool::ArtboardTool>::default()),
ToolAvailability::Available(Box::new(navigate_tool::NavigateTool::default())), ToolAvailability::Available(Box::<navigate_tool::NavigateTool>::default()),
ToolAvailability::Available(Box::new(eyedropper_tool::EyedropperTool::default())), ToolAvailability::Available(Box::<eyedropper_tool::EyedropperTool>::default()),
ToolAvailability::Available(Box::new(fill_tool::FillTool::default())), ToolAvailability::Available(Box::<fill_tool::FillTool>::default()),
ToolAvailability::Available(Box::new(gradient_tool::GradientTool::default())), ToolAvailability::Available(Box::<gradient_tool::GradientTool>::default()),
], ],
vec![ vec![
// Vector tool group // Vector tool group
ToolAvailability::Available(Box::new(path_tool::PathTool::default())), ToolAvailability::Available(Box::<path_tool::PathTool>::default()),
ToolAvailability::Available(Box::new(pen_tool::PenTool::default())), ToolAvailability::Available(Box::<pen_tool::PenTool>::default()),
ToolAvailability::Available(Box::new(freehand_tool::FreehandTool::default())), ToolAvailability::Available(Box::<freehand_tool::FreehandTool>::default()),
ToolAvailability::Available(Box::new(spline_tool::SplineTool::default())), ToolAvailability::Available(Box::<spline_tool::SplineTool>::default()),
ToolAvailability::Available(Box::new(line_tool::LineTool::default())), ToolAvailability::Available(Box::<line_tool::LineTool>::default()),
ToolAvailability::Available(Box::new(rectangle_tool::RectangleTool::default())), ToolAvailability::Available(Box::<rectangle_tool::RectangleTool>::default()),
ToolAvailability::Available(Box::new(ellipse_tool::EllipseTool::default())), ToolAvailability::Available(Box::<ellipse_tool::EllipseTool>::default()),
ToolAvailability::Available(Box::new(shape_tool::ShapeTool::default())), ToolAvailability::Available(Box::<shape_tool::ShapeTool>::default()),
ToolAvailability::Available(Box::new(text_tool::TextTool::default())), ToolAvailability::Available(Box::<text_tool::TextTool>::default()),
], ],
vec![ vec![
// Raster tool group // Raster tool group
ToolAvailability::Available(Box::new(imaginate_tool::ImaginateTool::default())), ToolAvailability::Available(Box::<imaginate_tool::ImaginateTool>::default()),
ToolAvailability::Available(Box::<node_graph_frame_tool::NodeGraphFrameTool>::default()),
ToolAvailability::ComingSoon(ToolEntry { ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Brush, tool_type: ToolType::Brush,
icon_name: "RasterBrushTool".into(), icon_name: "RasterBrushTool".into(),
@@ -384,6 +386,7 @@ pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType {
// ToolMessage::Detail(_) => ToolType::Detail, // ToolMessage::Detail(_) => ToolType::Detail,
// ToolMessage::Relight(_) => ToolType::Relight, // ToolMessage::Relight(_) => ToolType::Relight,
ToolMessage::Imaginate(_) => ToolType::Imaginate, ToolMessage::Imaginate(_) => ToolType::Imaginate,
ToolMessage::NodeGraphFrame(_) => ToolType::NodeGraphFrame,
_ => panic!( _ => panic!(
"Conversion from ToolMessage to ToolType impossible because the given ToolMessage does not have a matching ToolType. Got: {:?}", "Conversion from ToolMessage to ToolType impossible because the given ToolMessage does not have a matching ToolType. Got: {:?}",
tool_message tool_message
@@ -420,6 +423,7 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis
// ToolType::Detail => ToolMessageDiscriminant::ActivateToolDetail, // ToolType::Detail => ToolMessageDiscriminant::ActivateToolDetail,
// ToolType::Relight => ToolMessageDiscriminant::ActivateToolRelight, // ToolType::Relight => ToolMessageDiscriminant::ActivateToolRelight,
ToolType::Imaginate => ToolMessageDiscriminant::ActivateToolImaginate, ToolType::Imaginate => ToolMessageDiscriminant::ActivateToolImaginate,
ToolType::NodeGraphFrame => ToolMessageDiscriminant::ActivateToolNodeGraphFrame,
_ => panic!( _ => panic!(
"Conversion from ToolType to ToolMessage impossible because the given ToolType does not have a matching ToolMessage. Got: {:?}", "Conversion from ToolType to ToolMessage impossible because the given ToolType does not have a matching ToolMessage. Got: {:?}",
tool_type tool_type
@@ -0,0 +1,7 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<polygon points="3,1 1,1 0,1 0,2 0,4 1,4 1,2 3,2" />
<polygon points="15,1 13,1 13,2 15,2 15,4 16,4 16,2 16,1" />
<polygon points="1,14 1,12 0,12 0,14 0,15 1,15 3,15 3,14" />
<polygon points="15,12 15,14 13,14 13,15 15,15 16,15 16,14 16,12" />
<path d="M11,7h2c0.5,0,1-0.5,1-1V4c0-0.5-0.5-1-1-1h-2c-0.5,0-1,0.5-1,1v0.5L9.8,4.5C9.1,4.8,8.6,5.3,7.9,5.8C7.1,6.5,6.3,7.2,5,7.5V7c0-0.5-0.5-1-1-1H2C1.5,6,1,6.5,1,7v2c0,0.5,0.5,1,1,1h2c0.5,0,1-0.5,1-1V8.5c1.3,0.3,2.1,1,2.9,1.7c0.6,0.5,1.2,1,1.9,1.3l0.2,0.1V12c0,0.5,0.5,1,1,1h2c0.5,0,1-0.5,1-1v-2c0-0.5-0.5-1-1-1h-2c-0.5,0-1,0.5-1,1v0.5c-0.5-0.2-0.9-0.6-1.4-1C8,9,7.4,8.4,6.5,8C7.4,7.6,8,7,8.6,6.5c0.5-0.4,0.9-0.8,1.4-1V6C10,6.5,10.5,7,11,7z" />
</svg>

After

Width:  |  Height:  |  Size: 763 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path class="color-raster" d="M23,18c0,1.1-0.9,2-2,2h-4c-1.1,0-2-0.9-2-2v-2c0-1.1,0.9-2,2-2h4c1.1,0,2,0.9,2,2V18z" />
<path class="color-raster" d="M23,8c0,1.1-0.9,2-2,2h-4c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h4c1.1,0,2,0.9,2,2V8z" />
<path class="color-raster" d="M9,13c0,1.1-0.9,2-2,2H3c-1.1,0-2-0.9-2-2v-2c0-1.1,0.9-2,2-2h4c1.1,0,2,0.9,2,2V13z" />
<path class="color-solid" d="M12.9,9.3c0.2-1,0.4-1.6,1.1-1.8v-1l-0.1,0c-1.5,0.3-1.7,1.6-2,2.6c-0.3,1.2-0.5,2.1-1.9,2.4v1c1.5,0.2,1.7,1.2,1.9,2.4c0.2,1,0.5,2.3,2,2.6l0.1,0v-1c-0.7-0.2-0.9-0.8-1.1-1.8c-0.2-0.9-0.4-2-1.4-2.7C12.5,11.3,12.7,10.2,12.9,9.3z" />
</svg>

After

Width:  |  Height:  |  Size: 677 B

+10 -1
View File
@@ -3,7 +3,7 @@ import { reactive, readonly } from "vue";
import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files"; import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files";
import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate, preloadAndSetImaginateBlobURL } from "@/utility-functions/imaginate"; import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate, preloadAndSetImaginateBlobURL } from "@/utility-functions/imaginate";
import { rasterizeSVG } from "@/utility-functions/rasterization"; import { rasterizeSVG, rasterizeSVGCanvas } from "@/utility-functions/rasterization";
import { type Editor } from "@/wasm-communication/editor"; import { type Editor } from "@/wasm-communication/editor";
import { import {
type FrontendDocumentDetails, type FrontendDocumentDetails,
@@ -14,6 +14,7 @@ import {
TriggerImaginateGenerate, TriggerImaginateGenerate,
TriggerImaginateTerminate, TriggerImaginateTerminate,
TriggerImaginateCheckServerStatus, TriggerImaginateCheckServerStatus,
TriggerNodeGraphFrameGenerate,
UpdateActiveDocument, UpdateActiveDocument,
UpdateOpenDocumentsList, UpdateOpenDocumentsList,
UpdateImageData, UpdateImageData,
@@ -100,6 +101,14 @@ export function createPortfolioState(editor: Editor) {
editor.instance.setImageBlobURL(updateImageData.documentId, element.path, blobURL, image.naturalWidth, image.naturalHeight); editor.instance.setImageBlobURL(updateImageData.documentId, element.path, blobURL, image.naturalWidth, image.naturalHeight);
}); });
}); });
editor.subscriptions.subscribeJsMessage(TriggerNodeGraphFrameGenerate, async (triggerNodeGraphFrameGenerate) => {
const { documentId, layerPath, svg, size } = triggerNodeGraphFrameGenerate;
// Rasterize the SVG to an image file
const imageData = (await rasterizeSVGCanvas(svg, size[0], size[1])).getContext("2d")?.getImageData(0, 0, size[0], size[1]);
if (imageData) editor.instance.processNodeGraphFrame(documentId, layerPath, new Uint8Array(imageData.data), imageData.width, imageData.height);
});
editor.subscriptions.subscribeJsMessage(TriggerRevokeBlobUrl, async (triggerRevokeBlobUrl) => { editor.subscriptions.subscribeJsMessage(TriggerRevokeBlobUrl, async (triggerRevokeBlobUrl) => {
URL.revokeObjectURL(triggerRevokeBlobUrl.url); URL.revokeObjectURL(triggerRevokeBlobUrl.url);
}); });
+4
View File
@@ -112,6 +112,7 @@ import NodeImaginate from "@/../assets/icon-16px-solid/node-imaginate.svg";
import NodeMagicWand from "@/../assets/icon-16px-solid/node-magic-wand.svg"; import NodeMagicWand from "@/../assets/icon-16px-solid/node-magic-wand.svg";
import NodeMask from "@/../assets/icon-16px-solid/node-mask.svg"; import NodeMask from "@/../assets/icon-16px-solid/node-mask.svg";
import NodeMotionBlur from "@/../assets/icon-16px-solid/node-motion-blur.svg"; import NodeMotionBlur from "@/../assets/icon-16px-solid/node-motion-blur.svg";
import NodeNodes from "@/../assets/icon-16px-solid/node-nodes.svg";
import NodeOutput from "@/../assets/icon-16px-solid/node-output.svg"; import NodeOutput from "@/../assets/icon-16px-solid/node-output.svg";
import NodeShape from "@/../assets/icon-16px-solid/node-shape.svg"; import NodeShape from "@/../assets/icon-16px-solid/node-shape.svg";
import NodeText from "@/../assets/icon-16px-solid/node-text.svg"; import NodeText from "@/../assets/icon-16px-solid/node-text.svg";
@@ -168,6 +169,7 @@ const SOLID_16PX = {
NodeMagicWand: { component: NodeMagicWand, size: 16 }, NodeMagicWand: { component: NodeMagicWand, size: 16 },
NodeMask: { component: NodeMask, size: 16 }, NodeMask: { component: NodeMask, size: 16 },
NodeMotionBlur: { component: NodeMotionBlur, size: 16 }, NodeMotionBlur: { component: NodeMotionBlur, size: 16 },
NodeNodes: { component: NodeNodes, size: 16 },
NodeOutput: { component: NodeOutput, size: 16 }, NodeOutput: { component: NodeOutput, size: 16 },
NodeShape: { component: NodeShape, size: 16 }, NodeShape: { component: NodeShape, size: 16 },
NodeText: { component: NodeText, size: 16 }, NodeText: { component: NodeText, size: 16 },
@@ -228,6 +230,7 @@ import RasterCloneTool from "@/../assets/icon-24px-two-tone/raster-clone-tool.sv
import RasterDetailTool from "@/../assets/icon-24px-two-tone/raster-detail-tool.svg"; import RasterDetailTool from "@/../assets/icon-24px-two-tone/raster-detail-tool.svg";
import RasterHealTool from "@/../assets/icon-24px-two-tone/raster-heal-tool.svg"; import RasterHealTool from "@/../assets/icon-24px-two-tone/raster-heal-tool.svg";
import RasterImaginateTool from "@/../assets/icon-24px-two-tone/raster-imaginate-tool.svg"; import RasterImaginateTool from "@/../assets/icon-24px-two-tone/raster-imaginate-tool.svg";
import RasterNodesTool from "@/../assets/icon-24px-two-tone/raster-nodes-tool.svg";
import RasterPatchTool from "@/../assets/icon-24px-two-tone/raster-patch-tool.svg"; import RasterPatchTool from "@/../assets/icon-24px-two-tone/raster-patch-tool.svg";
import RasterRelightTool from "@/../assets/icon-24px-two-tone/raster-relight-tool.svg"; import RasterRelightTool from "@/../assets/icon-24px-two-tone/raster-relight-tool.svg";
import VectorEllipseTool from "@/../assets/icon-24px-two-tone/vector-ellipse-tool.svg"; import VectorEllipseTool from "@/../assets/icon-24px-two-tone/vector-ellipse-tool.svg";
@@ -248,6 +251,7 @@ const TWO_TONE_24PX = {
GeneralNavigateTool: { component: GeneralNavigateTool, size: 24 }, GeneralNavigateTool: { component: GeneralNavigateTool, size: 24 },
GeneralSelectTool: { component: GeneralSelectTool, size: 24 }, GeneralSelectTool: { component: GeneralSelectTool, size: 24 },
RasterImaginateTool: { component: RasterImaginateTool, size: 24 }, RasterImaginateTool: { component: RasterImaginateTool, size: 24 },
RasterNodesTool: { component: RasterNodesTool, size: 24 },
RasterBrushTool: { component: RasterBrushTool, size: 24 }, RasterBrushTool: { component: RasterBrushTool, size: 24 },
RasterCloneTool: { component: RasterCloneTool, size: 24 }, RasterCloneTool: { component: RasterCloneTool, size: 24 },
RasterDetailTool: { component: RasterDetailTool, size: 24 }, RasterDetailTool: { component: RasterDetailTool, size: 24 },
+16
View File
@@ -9,6 +9,20 @@ export type Editor = Readonly<ReturnType<typeof createEditor>>;
// `wasmImport` starts uninitialized because its initialization needs to occur asynchronously, and thus needs to occur by manually calling and awaiting `initWasm()` // `wasmImport` starts uninitialized because its initialization needs to occur asynchronously, and thus needs to occur by manually calling and awaiting `initWasm()`
let wasmImport: WasmRawInstance | undefined; let wasmImport: WasmRawInstance | undefined;
let editorInstance: WasmEditorInstance | undefined;
export async function updateImage(path: BigUint64Array, mime: string, imageData: Uint8Array, documentId: bigint): Promise<void> {
const blob = new Blob([imageData], { type: mime });
const blobURL = URL.createObjectURL(blob);
// Pre-decode the image so it is ready to be drawn instantly once it's placed into the viewport SVG
const image = new Image();
image.src = blobURL;
await image.decode();
editorInstance?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight);
}
// Should be called asynchronously before `createEditor()` // Should be called asynchronously before `createEditor()`
export async function initWasm(): Promise<void> { export async function initWasm(): Promise<void> {
@@ -16,6 +30,7 @@ export async function initWasm(): Promise<void> {
if (wasmImport !== undefined) return; if (wasmImport !== undefined) return;
// Import the WASM module JS bindings and wrap them in the panic proxy // Import the WASM module JS bindings and wrap them in the panic proxy
// eslint-disable-next-line import/no-cycle
wasmImport = await import("@/../wasm/pkg").then(panicProxy); wasmImport = await import("@/../wasm/pkg").then(panicProxy);
// Provide a random starter seed which must occur after initializing the WASM module, since WASM can't generate its own random numbers // Provide a random starter seed which must occur after initializing the WASM module, since WASM can't generate its own random numbers
@@ -36,6 +51,7 @@ export function createEditor() {
// We pass along the first two arguments then add our own `raw` and `instance` context for the last two arguments // We pass along the first two arguments then add our own `raw` and `instance` context for the last two arguments
subscriptions.handleJsMessage(messageType, messageData, raw, instance); subscriptions.handleJsMessage(messageType, messageData, raw, instance);
}); });
editorInstance = instance;
// Subscriptions: Allows subscribing to messages in JS that are sent from the WASM backend // Subscriptions: Allows subscribing to messages in JS that are sent from the WASM backend
const subscriptions: SubscriptionRouter = createSubscriptionRouter(); const subscriptions: SubscriptionRouter = createSubscriptionRouter();
+13 -1
View File
@@ -514,6 +514,16 @@ export class TriggerImaginateTerminate extends JsMessage {
readonly hostname!: string; readonly hostname!: string;
} }
export class TriggerNodeGraphFrameGenerate extends JsMessage {
readonly documentId!: bigint;
readonly layerPath!: BigUint64Array;
readonly svg!: string;
readonly size!: [number, number];
}
export class TriggerRefreshBoundsOfViewports extends JsMessage {} export class TriggerRefreshBoundsOfViewports extends JsMessage {}
export class TriggerRevokeBlobUrl extends JsMessage { export class TriggerRevokeBlobUrl extends JsMessage {
@@ -641,7 +651,7 @@ export class LayerMetadata {
selected!: boolean; selected!: boolean;
} }
export type LayerType = "Imaginate" | "Folder" | "Image" | "Shape" | "Text"; export type LayerType = "Imaginate" | "NodeGraphFrame" | "Folder" | "Image" | "Shape" | "Text";
export type LayerTypeData = { export type LayerTypeData = {
name: string; name: string;
@@ -651,6 +661,7 @@ export type LayerTypeData = {
export function layerTypeData(layerType: LayerType): LayerTypeData | undefined { export function layerTypeData(layerType: LayerType): LayerTypeData | undefined {
const entries: Record<string, LayerTypeData> = { const entries: Record<string, LayerTypeData> = {
Imaginate: { name: "Imaginate", icon: "NodeImaginate" }, Imaginate: { name: "Imaginate", icon: "NodeImaginate" },
NodeGraphFrame: { name: "Node Graph Frame", icon: "NodeNodes" },
Folder: { name: "Folder", icon: "NodeFolder" }, Folder: { name: "Folder", icon: "NodeFolder" },
Image: { name: "Image", icon: "NodeImage" }, Image: { name: "Image", icon: "NodeImage" },
Shape: { name: "Shape", icon: "NodeShape" }, Shape: { name: "Shape", icon: "NodeShape" },
@@ -1218,6 +1229,7 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerImaginateCheckServerStatus, TriggerImaginateCheckServerStatus,
TriggerImaginateGenerate, TriggerImaginateGenerate,
TriggerImaginateTerminate, TriggerImaginateTerminate,
TriggerNodeGraphFrameGenerate,
TriggerFileDownload, TriggerFileDownload,
TriggerFontLoad, TriggerFontLoad,
TriggerImport, TriggerImport,
+27
View File
@@ -28,6 +28,13 @@ pub fn set_random_seed(seed: u64) {
editor::application::set_uuid_seed(seed); editor::application::set_uuid_seed(seed);
} }
/// We directly interface with the updateImage JS function for massively increased performance over serializing and deserializing.
/// This avoids creating a json with a list millions of numbers long.
#[wasm_bindgen(module = "@/wasm-communication/editor")]
extern "C" {
fn updateImage(path: Vec<u64>, mime: String, imageData: Vec<u8>, document_id: u64);
}
/// Provides a handle to access the raw WASM memory /// Provides a handle to access the raw WASM memory
#[wasm_bindgen(js_name = wasmMemory)] #[wasm_bindgen(js_name = wasmMemory)]
pub fn wasm_memory() -> JsValue { pub fn wasm_memory() -> JsValue {
@@ -90,6 +97,14 @@ impl JsEditorHandle {
// Sends a FrontendMessage to JavaScript // Sends a FrontendMessage to JavaScript
fn send_frontend_message_to_js(&self, message: FrontendMessage) { fn send_frontend_message_to_js(&self, message: FrontendMessage) {
// Special case for update image data to avoid serialization times.
if let FrontendMessage::UpdateImageData { document_id, image_data } = message {
for image in image_data {
updateImage(image.path, image.mime, image.image_data, document_id);
}
return;
}
let message_type = message.to_discriminant().local_name(); let message_type = message.to_discriminant().local_name();
let serializer = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true); let serializer = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true);
@@ -512,6 +527,18 @@ impl JsEditorHandle {
self.dispatch(message); self.dispatch(message);
} }
/// Sends the blob URL generated by JS to the Imaginate layer in the respective document
#[wasm_bindgen(js_name = processNodeGraphFrame)]
pub fn process_node_graph_frame(&self, document_id: u64, layer_path: Vec<LayerId>, image_data: Vec<u8>, width: u32, height: u32) {
let message = PortfolioMessage::ProcessNodeGraphFrame {
document_id,
layer_path,
image_data,
size: (width, height),
};
self.dispatch(message);
}
/// Pastes an image /// Pastes an image
#[wasm_bindgen(js_name = pasteImage)] #[wasm_bindgen(js_name = pasteImage)]
pub fn paste_image(&self, mime: String, image_data: Vec<u8>, mouse_x: Option<f64>, mouse_y: Option<f64>) { pub fn paste_image(&self, mime: String, image_data: Vec<u8>, mouse_x: Option<f64>, mouse_y: Option<f64>) {
+3
View File
@@ -11,6 +11,9 @@ repository = "https://github.com/GraphiteEditor/Graphite"
license = "Apache-2.0" license = "Apache-2.0"
[dependencies] [dependencies]
graph-craft = { path = "../node-graph/graph-craft", features = ["serde"] }
image = { version = "0.24", default-features = false }
log = "0.4" log = "0.4"
bezier-rs = { path = "../libraries/bezier-rs" } bezier-rs = { path = "../libraries/bezier-rs" }
+35 -9
View File
@@ -4,6 +4,7 @@ use crate::layers::folder_layer::FolderLayer;
use crate::layers::image_layer::ImageLayer; use crate::layers::image_layer::ImageLayer;
use crate::layers::imaginate_layer::{ImaginateImageData, ImaginateLayer, ImaginateStatus}; use crate::layers::imaginate_layer::{ImaginateImageData, ImaginateLayer, ImaginateStatus};
use crate::layers::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDiscriminant}; use crate::layers::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDiscriminant};
use crate::layers::nodegraph_layer::NodeGraphFrameLayer;
use crate::layers::shape_layer::ShapeLayer; use crate::layers::shape_layer::ShapeLayer;
use crate::layers::style::RenderData; use crate::layers::style::RenderData;
use crate::layers::text_layer::{Font, FontCache, TextLayer}; use crate::layers::text_layer::{Font, FontCache, TextLayer};
@@ -595,6 +596,22 @@ impl Document {
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
} }
Operation::AddNodeGraphFrame { path, insert_index, transform } => {
let layer = Layer::new(LayerDataType::NodeGraphFrame(NodeGraphFrameLayer::default()), transform);
self.set_layer(&path, layer, insert_index)?;
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
}
Operation::SetNodeGraphFrameImageData { layer_path, image_data } => {
let layer = self.layer_mut(&layer_path).expect("Setting NodeGraphFrame image data for invalid layer");
if let LayerDataType::NodeGraphFrame(node_graph_frame) = &mut layer.data {
node_graph_frame.image_data = Some(crate::layers::nodegraph_layer::ImageData { image_data });
} else {
panic!("Incorrectly trying to set image data for a layer that is not an NodeGraphFrame layer type");
}
Some(vec![LayerChanged { path: layer_path.clone() }])
}
Operation::SetTextEditability { path, editable } => { Operation::SetTextEditability { path, editable } => {
self.layer_mut(&path)?.as_text_mut()?.editable = editable; self.layer_mut(&path)?.as_text_mut()?.editable = editable;
self.mark_as_dirty(&path)?; self.mark_as_dirty(&path)?;
@@ -797,11 +814,15 @@ impl Document {
image.blob_url = Some(blob_url); image.blob_url = Some(blob_url);
image.dimensions = resolution.into(); image.dimensions = resolution.into();
} }
LayerDataType::NodeGraphFrame(node_graph_frame) => {
node_graph_frame.blob_url = Some(blob_url);
node_graph_frame.dimensions = resolution.into();
}
LayerDataType::Imaginate(imaginate) => { LayerDataType::Imaginate(imaginate) => {
imaginate.blob_url = Some(blob_url); imaginate.blob_url = Some(blob_url);
imaginate.dimensions = resolution.into(); imaginate.dimensions = resolution.into();
} }
_ => panic!("Incorrectly trying to set the image blob URL for a layer that is not an Image or Imaginate layer type"), _ => panic!("Incorrectly trying to set the image blob URL for a layer that is not an Image, NodeGraphFrame or Imaginate layer type"),
} }
self.mark_as_dirty(&layer_path)?; self.mark_as_dirty(&layer_path)?;
@@ -833,15 +854,20 @@ impl Document {
} }
Some(vec![LayerChanged { path: path.clone() }]) Some(vec![LayerChanged { path: path.clone() }])
} }
Operation::ImaginateClear { path } => { Operation::ClearBlobURL { path } => {
let layer = self.layer_mut(&path).expect("Clearing Imaginate image for invalid layer"); let layer = self.layer_mut(&path).expect("Clearing Imaginate image for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data { match &mut layer.data {
imaginate.image_data = None; LayerDataType::Imaginate(imaginate) => {
imaginate.blob_url = None; imaginate.image_data = None;
imaginate.status = ImaginateStatus::Idle; imaginate.blob_url = None;
imaginate.percent_complete = 0.; imaginate.status = ImaginateStatus::Idle;
} else { imaginate.percent_complete = 0.;
panic!("Incorrectly trying to clear the blob URL for a layer that is not an Imaginate layer type"); }
LayerDataType::NodeGraphFrame(node_graph) => {
node_graph.image_data = None;
node_graph.blob_url = None;
}
e => panic!("Incorrectly trying to clear the blob URL for layer of type {}", LayerDataTypeDiscriminant::from(&*e)),
} }
self.mark_as_dirty(&path)?; self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
+9 -1
View File
@@ -2,6 +2,7 @@ use super::blend_mode::BlendMode;
use super::folder_layer::FolderLayer; use super::folder_layer::FolderLayer;
use super::image_layer::ImageLayer; use super::image_layer::ImageLayer;
use super::imaginate_layer::ImaginateLayer; use super::imaginate_layer::ImaginateLayer;
use super::nodegraph_layer::NodeGraphFrameLayer;
use super::shape_layer::ShapeLayer; use super::shape_layer::ShapeLayer;
use super::style::{PathStyle, RenderData}; use super::style::{PathStyle, RenderData};
use super::text_layer::TextLayer; use super::text_layer::TextLayer;
@@ -27,8 +28,10 @@ pub enum LayerDataType {
Text(TextLayer), Text(TextLayer),
/// A layer that wraps an [ImageLayer] struct. /// A layer that wraps an [ImageLayer] struct.
Image(ImageLayer), Image(ImageLayer),
/// A layer that wraps an [ImageLayer] struct. /// A layer that wraps an [ImaginateLayer] struct.
Imaginate(ImaginateLayer), Imaginate(ImaginateLayer),
/// A layer that wraps an [NodeGraphFrameLayer] struct.
NodeGraphFrame(NodeGraphFrameLayer),
} }
impl LayerDataType { impl LayerDataType {
@@ -39,6 +42,7 @@ impl LayerDataType {
LayerDataType::Text(t) => t, LayerDataType::Text(t) => t,
LayerDataType::Image(i) => i, LayerDataType::Image(i) => i,
LayerDataType::Imaginate(a) => a, LayerDataType::Imaginate(a) => a,
LayerDataType::NodeGraphFrame(n) => n,
} }
} }
@@ -49,6 +53,7 @@ impl LayerDataType {
LayerDataType::Text(t) => t, LayerDataType::Text(t) => t,
LayerDataType::Image(i) => i, LayerDataType::Image(i) => i,
LayerDataType::Imaginate(a) => a, LayerDataType::Imaginate(a) => a,
LayerDataType::NodeGraphFrame(n) => n,
} }
} }
} }
@@ -60,6 +65,7 @@ pub enum LayerDataTypeDiscriminant {
Text, Text,
Image, Image,
Imaginate, Imaginate,
NodeGraphFrame,
} }
impl fmt::Display for LayerDataTypeDiscriminant { impl fmt::Display for LayerDataTypeDiscriminant {
@@ -70,6 +76,7 @@ impl fmt::Display for LayerDataTypeDiscriminant {
LayerDataTypeDiscriminant::Text => write!(f, "Text"), LayerDataTypeDiscriminant::Text => write!(f, "Text"),
LayerDataTypeDiscriminant::Image => write!(f, "Image"), LayerDataTypeDiscriminant::Image => write!(f, "Image"),
LayerDataTypeDiscriminant::Imaginate => write!(f, "Imaginate"), LayerDataTypeDiscriminant::Imaginate => write!(f, "Imaginate"),
LayerDataTypeDiscriminant::NodeGraphFrame => write!(f, "NodeGraphFrame"),
} }
} }
} }
@@ -84,6 +91,7 @@ impl From<&LayerDataType> for LayerDataTypeDiscriminant {
Text(_) => LayerDataTypeDiscriminant::Text, Text(_) => LayerDataTypeDiscriminant::Text,
Image(_) => LayerDataTypeDiscriminant::Image, Image(_) => LayerDataTypeDiscriminant::Image,
Imaginate(_) => LayerDataTypeDiscriminant::Imaginate, Imaginate(_) => LayerDataTypeDiscriminant::Imaginate,
NodeGraphFrame(_) => LayerDataTypeDiscriminant::NodeGraphFrame,
} }
} }
} }
+2
View File
@@ -27,6 +27,8 @@ pub mod image_layer;
pub mod imaginate_layer; pub mod imaginate_layer;
/// Contains the base [Layer](layer_info::Layer) type, an abstraction over the different types of layers. /// Contains the base [Layer](layer_info::Layer) type, an abstraction over the different types of layers.
pub mod layer_info; pub mod layer_info;
/// Contains the [NodegraphLayer](nodegraph_layer::NodegraphLayer) type that contains a node graph.
pub mod nodegraph_layer;
/// Contains the [ShapeLayer](shape_layer::ShapeLayer) type, a generic SVG element defined using Bezier paths. /// Contains the [ShapeLayer](shape_layer::ShapeLayer) type, a generic SVG element defined using Bezier paths.
pub mod shape_layer; pub mod shape_layer;
pub mod style; pub mod style;
+152
View File
@@ -0,0 +1,152 @@
use super::base64_serde;
use super::layer_info::LayerData;
use super::style::{RenderData, ViewMode};
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::layers::text_layer::FontCache;
use crate::LayerId;
use glam::{DAffine2, DMat2, DVec2};
use kurbo::{Affine, BezPath, Shape as KurboShape};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct NodeGraphFrameLayer {
// Image stored in layer after generation completes
pub mime: String,
/// The document node network that this layer contains
pub network: graph_craft::document::NodeNetwork,
// TODO: Have the browser dispose of this blob URL when this is dropped (like when the layer is deleted)
#[serde(skip)]
pub blob_url: Option<String>,
#[serde(skip)]
pub dimensions: DVec2,
pub image_data: Option<ImageData>,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub struct ImageData {
#[serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64")]
pub image_data: Vec<u8>,
}
impl LayerData for NodeGraphFrameLayer {
fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
let transform = self.transform(transforms, render_data.view_mode);
let inverse = transform.inverse();
let (width, height) = (transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length());
if !inverse.is_finite() {
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
return;
}
let _ = writeln!(svg, r#"<g transform="matrix("#);
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
let _ = svg.write_str(&(entry.to_string() + if i == 5 { "" } else { "," }));
});
let _ = svg.write_str(r#")">"#);
let matrix = (transform * DAffine2::from_scale((width, height).into()).inverse())
.to_cols_array()
.iter()
.enumerate()
.fold(String::new(), |val, (i, entry)| val + &(entry.to_string() + if i == 5 { "" } else { "," }));
if let Some(blob_url) = &self.blob_url {
let _ = write!(
svg,
r#"<image width="{}" height="{}" preserveAspectRatio="none" href="{}" transform="matrix({})" />"#,
width.abs(),
height.abs(),
blob_url,
matrix
);
}
let _ = write!(
svg,
r#"<rect width="{}" height="{}" fill="none" stroke="var(--color-data-vector)" stroke-width="3" stroke-dasharray="8" transform="matrix({})" />"#,
width.abs(),
height.abs(),
matrix,
);
let _ = svg.write_str(r#"</g>"#);
}
fn bounding_box(&self, transform: glam::DAffine2, _font_cache: &FontCache) -> Option<[DVec2; 2]> {
let mut path = self.bounds();
if transform.matrix2 == DMat2::ZERO {
return None;
}
path.apply_affine(glam_to_kurbo(transform));
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
Some([(x0, y0).into(), (x1, y1).into()])
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _font_cache: &FontCache) {
if intersect_quad_bez_path(quad, &self.bounds(), true) {
intersections.push(path.clone());
}
}
}
impl NodeGraphFrameLayer {
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
let start = match mode {
ViewMode::Outline => 0,
_ => (transforms.len() as i32 - 1).max(0) as usize,
};
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
}
fn bounds(&self) -> BezPath {
kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.)).to_path(0.)
}
}
fn glam_to_kurbo(transform: DAffine2) -> Affine {
Affine::new(transform.to_cols_array())
}
impl Default for NodeGraphFrameLayer {
fn default() -> Self {
use graph_craft::document::*;
use graph_craft::proto::NodeIdentifier;
Self {
mime: String::new(),
network: NodeNetwork {
inputs: vec![1],
output: 1,
nodes: [
(
0,
DocumentNode {
name: "grayscale".into(),
inputs: vec![NodeInput::Network],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::raster::GrayscaleNode", &[])),
},
),
(
1,
DocumentNode {
name: "map image".into(),
inputs: vec![NodeInput::Network, NodeInput::Node(0)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_std::raster::MapImageNode", &[])),
},
),
]
.into_iter()
.collect(),
},
blob_url: None,
dimensions: DVec2::ZERO,
image_data: None,
}
}
}
+11 -2
View File
@@ -57,6 +57,15 @@ pub enum Operation {
insert_index: isize, insert_index: isize,
transform: [f64; 6], transform: [f64; 6],
}, },
AddNodeGraphFrame {
path: Vec<LayerId>,
insert_index: isize,
transform: [f64; 6],
},
SetNodeGraphFrameImageData {
layer_path: Vec<LayerId>,
image_data: Vec<u8>,
},
/// Sets a blob URL as the image source for an Image or Imaginate layer type. /// Sets a blob URL as the image source for an Image or Imaginate layer type.
/// **Be sure to call `FrontendMessage::TriggerRevokeBlobUrl` together with this.** /// **Be sure to call `FrontendMessage::TriggerRevokeBlobUrl` together with this.**
SetLayerBlobUrl { SetLayerBlobUrl {
@@ -64,9 +73,9 @@ pub enum Operation {
blob_url: String, blob_url: String,
resolution: (f64, f64), resolution: (f64, f64),
}, },
/// Clears the image to leave the Imaginate layer un-rendered. /// Clears the image to leave the layer un-rendered.
/// **Be sure to call `FrontendMessage::TriggerRevokeBlobUrl` together with this.** /// **Be sure to call `FrontendMessage::TriggerRevokeBlobUrl` together with this.**
ImaginateClear { ClearBlobURL {
path: Vec<LayerId>, path: Vec<LayerId>,
}, },
ImaginateSetGeneratingStatus { ImaginateSetGeneratingStatus {
+5
View File
@@ -14,3 +14,8 @@ num-traits = "0.2"
borrow_stack = { path = "../borrow_stack" } borrow_stack = { path = "../borrow_stack" }
dyn-clone = "1.0" dyn-clone = "1.0"
rand_chacha = "0.3.1" rand_chacha = "0.3.1"
[dependencies.serde]
version = "1.0"
optional = true
features = ["derive"]
+15 -13
View File
@@ -28,9 +28,8 @@ fn merge_ids(a: u64, b: u64) -> u64 {
hasher.finish() hasher.finish()
} }
type Fqn = NodeIdentifier<'static>; #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, PartialEq)]
pub struct DocumentNode { pub struct DocumentNode {
pub name: String, pub name: String,
pub inputs: Vec<NodeInput>, pub inputs: Vec<NodeInput>,
@@ -55,9 +54,9 @@ impl DocumentNode {
let first = self.inputs.remove(0); let first = self.inputs.remove(0);
if let DocumentNodeImplementation::Unresolved(fqn) = self.implementation { if let DocumentNodeImplementation::Unresolved(fqn) = self.implementation {
let (input, mut args) = match first { let (input, mut args) = match first {
NodeInput::Value(value) => { NodeInput::Value(tagged_value) => {
assert_eq!(self.inputs.len(), 0); assert_eq!(self.inputs.len(), 0);
(ProtoNodeInput::None, ConstructionArgs::Value(value)) (ProtoNodeInput::None, ConstructionArgs::Value(tagged_value.to_value()))
} }
NodeInput::Node(id) => (ProtoNodeInput::Node(id), ConstructionArgs::Nodes(vec![])), NodeInput::Node(id) => (ProtoNodeInput::Node(id), ConstructionArgs::Nodes(vec![])),
NodeInput::Network => (ProtoNodeInput::Network, ConstructionArgs::Nodes(vec![])), NodeInput::Network => (ProtoNodeInput::Network, ConstructionArgs::Nodes(vec![])),
@@ -82,10 +81,11 @@ impl DocumentNode {
} }
} }
#[derive(Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum NodeInput { pub enum NodeInput {
Node(NodeId), Node(NodeId),
Value(value::Value), Value(value::TaggedValue),
Network, Network,
} }
@@ -107,13 +107,15 @@ impl PartialEq for NodeInput {
} }
} }
#[derive(Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DocumentNodeImplementation { pub enum DocumentNodeImplementation {
Network(NodeNetwork), Network(NodeNetwork),
Unresolved(Fqn), Unresolved(NodeIdentifier),
} }
#[derive(Debug, Default, PartialEq)] #[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NodeNetwork { pub struct NodeNetwork {
pub inputs: Vec<NodeId>, pub inputs: Vec<NodeId>,
pub output: NodeId, pub output: NodeId,
@@ -160,7 +162,7 @@ impl NodeNetwork {
network_input.populate_first_network_input(node, *offset); network_input.populate_first_network_input(node, *offset);
} }
NodeInput::Value(value) => { NodeInput::Value(value) => {
let name = format!("Value: {:?}", value); let name = format!("Value: {:?}", value.clone().to_value());
let new_id = map_ids(id, gen_id()); let new_id = map_ids(id, gen_id());
let value_node = DocumentNode { let value_node = DocumentNode {
name: name.clone(), name: name.clone(),
@@ -284,7 +286,7 @@ mod test {
1, 1,
DocumentNode { DocumentNode {
name: "Inc".into(), name: "Inc".into(),
inputs: vec![NodeInput::Network, NodeInput::Value(2_u32.into_any())], inputs: vec![NodeInput::Network, NodeInput::Value(value::TaggedValue::U32(2))],
implementation: DocumentNodeImplementation::Network(add_network()), implementation: DocumentNodeImplementation::Network(add_network()),
}, },
)] )]
@@ -384,7 +386,7 @@ mod test {
14, 14,
DocumentNode { DocumentNode {
name: "Value: 2".into(), name: "Value: 2".into(),
inputs: vec![NodeInput::Value(2_u32.into_any())], inputs: vec![NodeInput::Value(value::TaggedValue::U32(2))],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::value::ValueNode", &[Type::Generic])), implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::value::ValueNode", &[Type::Generic])),
}, },
), ),
@@ -4,6 +4,26 @@ use dyn_clone::DynClone;
use dyn_any::{DynAny, Upcast}; use dyn_any::{DynAny, Upcast};
/// A type that is known, allowing serialization (serde::Deserialize is not object safe)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TaggedValue {
String(String),
U32(u32),
//Image(graphene_std::raster::Image),
Color(graphene_core::raster::color::Color),
}
impl TaggedValue {
pub fn to_value(self) -> Value {
match self {
TaggedValue::String(x) => Box::new(x),
TaggedValue::U32(x) => Box::new(x),
TaggedValue::Color(x) => Box::new(x),
}
}
}
pub type Value = Box<dyn ValueTrait>; pub type Value = Box<dyn ValueTrait>;
pub trait ValueTrait: DynAny<'static> + Upcast<dyn DynAny<'static>> + std::fmt::Debug + DynClone {} pub trait ValueTrait: DynAny<'static> + Upcast<dyn DynAny<'static>> + std::fmt::Debug + DynClone {}
+9 -4
View File
@@ -11,7 +11,6 @@ mod tests {
use graphene_core::value::ValueNode; use graphene_core::value::ValueNode;
use graphene_core::{structural::*, RefNode}; use graphene_core::{structural::*, RefNode};
use crate::document::value::IntoValue;
use borrow_stack::BorrowStack; use borrow_stack::BorrowStack;
use borrow_stack::FixedSizeStack; use borrow_stack::FixedSizeStack;
use dyn_any::{downcast, IntoDynAny}; use dyn_any::{downcast, IntoDynAny};
@@ -68,7 +67,10 @@ mod tests {
DocumentNode { DocumentNode {
name: "cons".into(), name: "cons".into(),
inputs: vec![NodeInput::Network, NodeInput::Network], inputs: vec![NodeInput::Network, NodeInput::Network],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::structural::ConsNode", &[Type::Concrete("u32"), Type::Concrete("u32")])), implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new(
"graphene_core::structural::ConsNode",
&[Type::Concrete(std::borrow::Cow::Borrowed("u32")), Type::Concrete(std::borrow::Cow::Borrowed("u32"))],
)),
}, },
), ),
( (
@@ -76,7 +78,10 @@ mod tests {
DocumentNode { DocumentNode {
name: "add".into(), name: "add".into(),
inputs: vec![NodeInput::Node(0)], inputs: vec![NodeInput::Node(0)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Concrete("u32"), Type::Concrete("u32")])), implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new(
"graphene_core::ops::AddNode",
&[Type::Concrete(std::borrow::Cow::Borrowed("u32")), Type::Concrete(std::borrow::Cow::Borrowed("u32"))],
)),
}, },
), ),
] ]
@@ -92,7 +97,7 @@ mod tests {
0, 0,
DocumentNode { DocumentNode {
name: "Inc".into(), name: "Inc".into(),
inputs: vec![NodeInput::Network, NodeInput::Value(1_u32.into_any())], inputs: vec![NodeInput::Network, NodeInput::Value(value::TaggedValue::U32(1))],
implementation: DocumentNodeImplementation::Network(add_network()), implementation: DocumentNodeImplementation::Network(add_network()),
}, },
)] )]
+91 -133
View File
@@ -1,11 +1,9 @@
use std::marker::PhantomData;
use borrow_stack::FixedSizeStack; use borrow_stack::FixedSizeStack;
use graphene_core::generic::FnNode; use graphene_core::generic::FnNode;
use graphene_core::ops::{AddNode, IdNode}; use graphene_core::ops::AddNode;
use graphene_core::raster::color::Color; use graphene_core::raster::color::Color;
use graphene_core::structural::{ConsNode, Then}; use graphene_core::structural::{ConsNode, Then};
use graphene_core::{AsRefNode, Node}; use graphene_core::Node;
use graphene_std::any::DowncastBothNode; use graphene_std::any::DowncastBothNode;
use graphene_std::any::{Any, DowncastNode, DynAnyNode, IntoTypeErasedNode, TypeErasedNode}; use graphene_std::any::{Any, DowncastNode, DynAnyNode, IntoTypeErasedNode, TypeErasedNode};
use graphene_std::raster::Image; use graphene_std::raster::Image;
@@ -13,17 +11,12 @@ use graphene_std::raster::Image;
use crate::proto::Type; use crate::proto::Type;
use crate::proto::{ConstructionArgs, NodeIdentifier, ProtoNode, ProtoNodeInput, Type::Concrete}; use crate::proto::{ConstructionArgs, NodeIdentifier, ProtoNode, ProtoNodeInput, Type::Concrete};
use dyn_any::Upcast;
type NodeConstructor = fn(ProtoNode, &FixedSizeStack<TypeErasedNode<'static>>); type NodeConstructor = fn(ProtoNode, &FixedSizeStack<TypeErasedNode<'static>>);
//TODO: turn into hasmap //TODO: turn into hasmap
static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
( (
NodeIdentifier { NodeIdentifier::new("graphene_core::ops::IdNode", &[Concrete(std::borrow::Cow::Borrowed("Any<'_>"))]),
name: "graphene_core::ops::IdNode",
types: &[Concrete("Any<'_>")],
},
|proto_node, stack| { |proto_node, stack| {
stack.push_fn(|nodes| { stack.push_fn(|nodes| {
let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap();
@@ -32,24 +25,18 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
}) })
}, },
), ),
(NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Generic]), |proto_node, stack| {
stack.push_fn(|nodes| {
let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap();
let node = pre_node.then(graphene_core::ops::IdNode);
node.into_type_erased()
})
}),
( (
NodeIdentifier { NodeIdentifier::new(
name: "graphene_core::ops::IdNode", "graphene_core::ops::AddNode",
types: &[Type::Generic], &[Concrete(std::borrow::Cow::Borrowed("u32")), Concrete(std::borrow::Cow::Borrowed("u32"))],
}, ),
|proto_node, stack| {
stack.push_fn(|nodes| {
let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap();
let node = pre_node.then(graphene_core::ops::IdNode);
node.into_type_erased()
})
},
),
(
NodeIdentifier {
name: "graphene_core::ops::AddNode",
types: &[Concrete("u32"), Concrete("u32")],
},
|proto_node, stack| { |proto_node, stack| {
stack.push_fn(|nodes| { stack.push_fn(|nodes| {
let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap();
@@ -61,10 +48,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
}, },
), ),
( (
NodeIdentifier { NodeIdentifier::new(
name: "graphene_core::ops::AddNode", "graphene_core::ops::AddNode",
types: &[Concrete("&u32"), Concrete("&u32")], &[Concrete(std::borrow::Cow::Borrowed("&u32")), Concrete(std::borrow::Cow::Borrowed("&u32"))],
}, ),
|proto_node, stack| { |proto_node, stack| {
stack.push_fn(|nodes| { stack.push_fn(|nodes| {
let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap();
@@ -76,10 +63,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
}, },
), ),
( (
NodeIdentifier { NodeIdentifier::new(
name: "graphene_core::ops::AddNode", "graphene_core::ops::AddNode",
types: &[Concrete("&u32"), Concrete("u32")], &[Concrete(std::borrow::Cow::Borrowed("&u32")), Concrete(std::borrow::Cow::Borrowed("u32"))],
}, ),
|proto_node, stack| { |proto_node, stack| {
stack.push_fn(|nodes| { stack.push_fn(|nodes| {
let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap();
@@ -91,10 +78,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
}, },
), ),
( (
NodeIdentifier { NodeIdentifier::new(
name: "graphene_core::structural::ConsNode", "graphene_core::structural::ConsNode",
types: &[Concrete("&u32"), Concrete("u32")], &[Concrete(std::borrow::Cow::Borrowed("&u32")), Concrete(std::borrow::Cow::Borrowed("u32"))],
}, ),
|proto_node, stack| { |proto_node, stack| {
if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args { if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args {
stack.push_fn(move |nodes| { stack.push_fn(move |nodes| {
@@ -118,10 +105,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
}, },
), ),
( (
NodeIdentifier { NodeIdentifier::new(
name: "graphene_core::structural::ConsNode", "graphene_core::structural::ConsNode",
types: &[Concrete("u32"), Concrete("u32")], &[Concrete(std::borrow::Cow::Borrowed("u32")), Concrete(std::borrow::Cow::Borrowed("u32"))],
}, ),
|proto_node, stack| { |proto_node, stack| {
if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args { if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args {
stack.push_fn(move |nodes| { stack.push_fn(move |nodes| {
@@ -146,10 +133,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
), ),
// TODO: create macro to impl for all types // TODO: create macro to impl for all types
( (
NodeIdentifier { NodeIdentifier::new(
name: "graphene_core::structural::ConsNode", "graphene_core::structural::ConsNode",
types: &[Concrete("&u32"), Concrete("&u32")], &[Concrete(std::borrow::Cow::Borrowed("&u32")), Concrete(std::borrow::Cow::Borrowed("&u32"))],
}, ),
|proto_node, stack| { |proto_node, stack| {
let node_id = proto_node.input.unwrap_node() as usize; let node_id = proto_node.input.unwrap_node() as usize;
if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args { if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args {
@@ -168,10 +155,7 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
}, },
), ),
( (
NodeIdentifier { NodeIdentifier::new("graphene_core::any::DowncastNode", &[Concrete(std::borrow::Cow::Borrowed("&u32"))]),
name: "graphene_core::any::DowncastNode",
types: &[Concrete("&u32")],
},
|proto_node, stack| { |proto_node, stack| {
stack.push_fn(|nodes| { stack.push_fn(|nodes| {
let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap();
@@ -181,10 +165,7 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
}, },
), ),
( (
NodeIdentifier { NodeIdentifier::new("graphene_core::value::ValueNode", &[Concrete(std::borrow::Cow::Borrowed("Any<'_>"))]),
name: "graphene_core::value::ValueNode",
types: &[Concrete("Any<'_>")],
},
|proto_node, stack| { |proto_node, stack| {
stack.push_fn(|_nodes| { stack.push_fn(|_nodes| {
if let ConstructionArgs::Value(value) = proto_node.construction_args { if let ConstructionArgs::Value(value) = proto_node.construction_args {
@@ -197,69 +178,48 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
}) })
}, },
), ),
( (NodeIdentifier::new("graphene_core::value::ValueNode", &[Type::Generic]), |proto_node, stack| {
NodeIdentifier { stack.push_fn(|_nodes| {
name: "graphene_core::value::ValueNode", if let ConstructionArgs::Value(value) = proto_node.construction_args {
types: &[Type::Generic], let node = FnNode::new(move |_| value.clone().up_box() as Any<'static>);
}, node.into_type_erased()
|proto_node, stack| {
stack.push_fn(|_nodes| {
if let ConstructionArgs::Value(value) = proto_node.construction_args {
let node = FnNode::new(move |_| value.clone().up_box() as Any<'static>);
node.into_type_erased()
} else {
unreachable!()
}
})
},
),
(
NodeIdentifier {
name: "graphene_core::raster::GrayscaleNode",
types: &[],
},
|proto_node, stack| {
stack.push_fn(|nodes| {
let node = DynAnyNode::new(graphene_core::raster::GrayscaleNode);
if let ProtoNodeInput::Node(pre_id) = proto_node.input {
let pre_node = nodes.get(pre_id as usize).unwrap();
(pre_node).then(node).into_type_erased()
} else {
node.into_type_erased()
}
})
},
),
(
NodeIdentifier {
name: "graphene_std::raster::MapImageNode",
types: &[],
},
|proto_node, stack| {
let node_id = proto_node.input.unwrap_node() as usize;
if let ConstructionArgs::Nodes(operation_node_id) = proto_node.construction_args {
stack.push_fn(move |nodes| {
let pre_node = nodes.get(node_id).unwrap();
let operation_node = nodes.get(operation_node_id[0] as usize).unwrap();
let operation_node: DowncastBothNode<_, Color, Color> = DowncastBothNode::new(operation_node);
let map_node = DynAnyNode::new(graphene_std::raster::MapImageNode::new(operation_node));
let node = (pre_node).then(map_node);
node.into_type_erased()
})
} else { } else {
unimplemented!() unreachable!()
} }
}, })
), }),
(NodeIdentifier::new("graphene_core::raster::GrayscaleNode", &[]), |proto_node, stack| {
stack.push_fn(|nodes| {
let node = DynAnyNode::new(graphene_core::raster::GrayscaleNode);
if let ProtoNodeInput::Node(pre_id) = proto_node.input {
let pre_node = nodes.get(pre_id as usize).unwrap();
(pre_node).then(node).into_type_erased()
} else {
node.into_type_erased()
}
})
}),
(NodeIdentifier::new("graphene_std::raster::MapImageNode", &[]), |proto_node, stack| {
if let ConstructionArgs::Nodes(operation_node_id) = proto_node.construction_args {
stack.push_fn(move |nodes| {
let operation_node = nodes.get(operation_node_id[0] as usize).unwrap();
let operation_node: DowncastBothNode<_, Color, Color> = DowncastBothNode::new(operation_node);
let map_node = DynAnyNode::new(graphene_std::raster::MapImageNode::new(operation_node));
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
(pre_node).then(map_node).into_type_erased()
} else {
map_node.into_type_erased()
}
})
} else {
unimplemented!()
}
}),
( (
NodeIdentifier { NodeIdentifier::new("graphene_std::raster::ImageNode", &[Concrete(std::borrow::Cow::Borrowed("&str"))]),
name: "graphene_std::raster::ImageNode",
types: &[Concrete("&str")],
},
|_proto_node, stack| { |_proto_node, stack| {
stack.push_fn(|_nodes| { stack.push_fn(|_nodes| {
let image = FnNode::new(|s: &str| graphene_std::raster::image_node::<&str>().eval(s).unwrap()); let image = FnNode::new(|s: &str| graphene_std::raster::image_node::<&str>().eval(s).unwrap());
@@ -269,10 +229,7 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
}, },
), ),
( (
NodeIdentifier { NodeIdentifier::new("graphene_std::raster::ExportImageNode", &[Concrete(std::borrow::Cow::Borrowed("&str"))]),
name: "graphene_std::raster::ExportImageNode",
types: &[Concrete("&str")],
},
|proto_node, stack| { |proto_node, stack| {
stack.push_fn(|nodes| { stack.push_fn(|nodes| {
let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap(); let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap();
@@ -285,10 +242,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
}, },
), ),
( (
NodeIdentifier { NodeIdentifier::new(
name: "graphene_core::structural::ConsNode", "graphene_core::structural::ConsNode",
types: &[Concrete("Image"), Concrete("&str")], &[Concrete(std::borrow::Cow::Borrowed("Image")), Concrete(std::borrow::Cow::Borrowed("&str"))],
}, ),
|proto_node, stack| { |proto_node, stack| {
let node_id = proto_node.input.unwrap_node() as usize; let node_id = proto_node.input.unwrap_node() as usize;
if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args { if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args {
@@ -322,11 +279,6 @@ mod protograph_testing {
use super::*; use super::*;
/// Lookup a node by th suffix of the name (for testing only)
fn simple_lookup(suffix: &str) -> &(NodeIdentifier, fn(ProtoNode, &FixedSizeStack<TypeErasedNode<'static>>)) {
NODE_REGISTRY.iter().find(|node| node.0.name.ends_with(suffix)).unwrap()
}
#[test] #[test]
fn add_values() { fn add_values() {
let stack = FixedSizeStack::new(256); let stack = FixedSizeStack::new(256);
@@ -339,14 +291,20 @@ mod protograph_testing {
let cons_protonode = ProtoNode { let cons_protonode = ProtoNode {
construction_args: ConstructionArgs::Nodes(vec![1]), construction_args: ConstructionArgs::Nodes(vec![1]),
input: ProtoNodeInput::Node(0), input: ProtoNodeInput::Node(0),
identifier: NodeIdentifier::new("graphene_core::structural::ConsNode", &[Concrete("u32"), Concrete("u32")]), identifier: NodeIdentifier::new(
"graphene_core::structural::ConsNode",
&[Concrete(std::borrow::Cow::Borrowed("u32")), Concrete(std::borrow::Cow::Borrowed("u32"))],
),
}; };
push_node(cons_protonode, &stack); push_node(cons_protonode, &stack);
let add_protonode = ProtoNode { let add_protonode = ProtoNode {
construction_args: ConstructionArgs::Nodes(vec![]), construction_args: ConstructionArgs::Nodes(vec![]),
input: ProtoNodeInput::Node(2), input: ProtoNodeInput::Node(2),
identifier: NodeIdentifier::new("graphene_core::ops::AddNode", &[Concrete("u32"), Concrete("u32")]), identifier: NodeIdentifier::new(
"graphene_core::ops::AddNode",
&[Concrete(std::borrow::Cow::Borrowed("u32")), Concrete(std::borrow::Cow::Borrowed("u32"))],
),
}; };
push_node(add_protonode, &stack); push_node(add_protonode, &stack);
@@ -379,7 +337,7 @@ mod protograph_testing {
let image_protonode = ProtoNode { let image_protonode = ProtoNode {
construction_args: ConstructionArgs::Nodes(vec![]), construction_args: ConstructionArgs::Nodes(vec![]),
input: ProtoNodeInput::None, input: ProtoNodeInput::None,
identifier: NodeIdentifier::new("graphene_std::raster::ImageNode", &[Concrete("&str")]), identifier: NodeIdentifier::new("graphene_std::raster::ImageNode", &[Concrete(std::borrow::Cow::Borrowed("&str"))]),
}; };
push_node(image_protonode, &stack); push_node(image_protonode, &stack);
@@ -394,7 +352,7 @@ mod protograph_testing {
let image_protonode = ProtoNode { let image_protonode = ProtoNode {
construction_args: ConstructionArgs::Nodes(vec![]), construction_args: ConstructionArgs::Nodes(vec![]),
input: ProtoNodeInput::None, input: ProtoNodeInput::None,
identifier: NodeIdentifier::new("graphene_std::raster::ImageNode", &[Concrete("&str")]), identifier: NodeIdentifier::new("graphene_std::raster::ImageNode", &[Concrete(std::borrow::Cow::Borrowed("&str"))]),
}; };
push_node(image_protonode, &stack); push_node(image_protonode, &stack);
+65 -49
View File
@@ -1,35 +1,49 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::HashSet;
use crate::document::value; use crate::document::value;
use crate::document::NodeId; use crate::document::NodeId;
#[derive(Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct NodeIdentifier<'a> { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub name: &'a str, pub struct NodeIdentifier {
pub types: &'a [Type<'a>], pub name: std::borrow::Cow<'static, str>,
pub types: std::borrow::Cow<'static, [Type]>,
} }
#[derive(Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Type<'a> { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Type {
Generic, Generic,
Concrete(&'a str), Concrete(std::borrow::Cow<'static, str>),
} }
impl<'a> From<&'a str> for Type<'a> { impl From<&'static str> for Type {
fn from(s: &'a str) -> Self { fn from(s: &'static str) -> Self {
Type::Concrete(s) Type::Concrete(std::borrow::Cow::Borrowed(s))
}
}
impl<'a> From<&'a str> for NodeIdentifier<'a> {
fn from(s: &'a str) -> Self {
NodeIdentifier { name: s, types: &[] }
} }
} }
impl<'a> NodeIdentifier<'a> { impl Type {
pub fn new(name: &'a str, types: &'a [Type<'a>]) -> Self { pub const fn from_str(concrete: &'static str) -> Self {
NodeIdentifier { name, types } Type::Concrete(std::borrow::Cow::Borrowed(concrete))
}
}
impl From<&'static str> for NodeIdentifier {
fn from(s: &'static str) -> Self {
NodeIdentifier {
name: std::borrow::Cow::Borrowed(s),
types: std::borrow::Cow::Borrowed(&[]),
}
}
}
impl NodeIdentifier {
pub const fn new(name: &'static str, types: &'static [Type]) -> Self {
NodeIdentifier {
name: std::borrow::Cow::Borrowed(name),
types: std::borrow::Cow::Borrowed(types),
}
} }
} }
@@ -60,7 +74,7 @@ impl PartialEq for ConstructionArgs {
pub struct ProtoNode { pub struct ProtoNode {
pub construction_args: ConstructionArgs, pub construction_args: ConstructionArgs,
pub input: ProtoNodeInput, pub input: ProtoNodeInput,
pub identifier: NodeIdentifier<'static>, pub identifier: NodeIdentifier,
} }
#[derive(Debug, Default, PartialEq, Eq)] #[derive(Debug, Default, PartialEq, Eq)]
@@ -83,10 +97,7 @@ impl ProtoNodeInput {
impl ProtoNode { impl ProtoNode {
pub fn value(value: ConstructionArgs) -> Self { pub fn value(value: ConstructionArgs) -> Self {
Self { Self {
identifier: NodeIdentifier { identifier: NodeIdentifier::new("graphene_core::value::ValueNode", &[Type::Generic]),
name: "graphene_core::value::ValueNode",
types: &[Type::Generic],
},
construction_args: value, construction_args: value,
input: ProtoNodeInput::None, input: ProtoNodeInput::None,
} }
@@ -110,7 +121,22 @@ impl ProtoNode {
} }
impl ProtoNetwork { impl ProtoNetwork {
fn reverse_edges(&self) -> HashMap<NodeId, Vec<NodeId>> { pub fn collect_outwards_edges(&self) -> HashMap<NodeId, Vec<NodeId>> {
let mut edges: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for (id, node) in &self.nodes {
if let ProtoNodeInput::Node(ref_id) = &node.input {
edges.entry(*ref_id).or_default().push(*id)
}
if let ConstructionArgs::Nodes(ref_nodes) = &node.construction_args {
for ref_id in ref_nodes {
edges.entry(*ref_id).or_default().push(*id)
}
}
}
edges
}
pub fn collect_inwards_edges(&self) -> HashMap<NodeId, Vec<NodeId>> {
let mut edges: HashMap<NodeId, Vec<NodeId>> = HashMap::new(); let mut edges: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for (id, node) in &self.nodes { for (id, node) in &self.nodes {
if let ProtoNodeInput::Node(ref_id) = &node.input { if let ProtoNodeInput::Node(ref_id) = &node.input {
@@ -125,38 +151,28 @@ impl ProtoNetwork {
edges edges
} }
// Based on https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
pub fn topological_sort(&self) -> Vec<NodeId> { pub fn topological_sort(&self) -> Vec<NodeId> {
let mut visited = HashSet::new();
let mut stack = Vec::new();
let mut sorted = Vec::new(); let mut sorted = Vec::new();
let graph = self.reverse_edges(); let outwards_edges = self.collect_outwards_edges();
// TODO: remove let mut inwards_edges = self.collect_inwards_edges();
println!("{:#?}", graph); let mut no_incoming_edges: Vec<_> = self.nodes.iter().map(|entry| entry.0).filter(|id| !inwards_edges.contains_key(id)).collect();
for (id, _) in &self.nodes { assert_ne!(no_incoming_edges.len(), 0, "Acyclic graphs must have at least one node with no incoming edge");
if !visited.contains(id) {
stack.push(*id);
while let Some(id) = stack.pop() { while let Some(node_id) = no_incoming_edges.pop() {
//TODO remove sorted.push(node_id);
println!("{:?}", stack);
if !visited.contains(&id) { if let Some(outwards_edges) = outwards_edges.get(&node_id) {
visited.insert(id); for &ref_id in outwards_edges {
if let Some(refs) = graph.get(&id) { let dependencies = inwards_edges.get_mut(&ref_id).unwrap();
for ref_id in refs { dependencies.retain(|&id| id != node_id);
if !visited.contains(ref_id) { if dependencies.is_empty() {
stack.push(id); no_incoming_edges.push(ref_id)
stack.push(*ref_id);
break;
}
}
}
sorted.push(id);
} }
} }
} }
} }
sorted.reverse();
sorted sorted
} }