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 21:38:14 +00:00 committed by Keavon Chambers
parent 1462d2b662
commit 18507b78ac
33 changed files with 1018 additions and 258 deletions

9
Cargo.lock generated
View file

@ -360,6 +360,7 @@ dependencies = [
"graphene-std",
"num-traits",
"rand_chacha",
"serde",
]
[[package]]
@ -405,11 +406,17 @@ version = "0.0.0"
dependencies = [
"bezier-rs",
"bitflags",
"borrow_stack",
"derivative",
"dyn-any",
"env_logger",
"glam",
"graph-craft",
"graphene-core",
"graphene-std",
"graphite-graphene",
"graphite-proc-macros",
"image",
"kurbo",
"log",
"once_cell",
@ -429,6 +436,8 @@ dependencies = [
"base64",
"bezier-rs",
"glam",
"graph-craft",
"image",
"kurbo",
"log",
"rustybuzz",

View file

@ -28,6 +28,14 @@ remain = "0.2.2"
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>)
# 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]
path = "../graphene"
package = "graphite-graphene"

View file

@ -86,6 +86,14 @@ pub enum FrontendMessage {
},
TriggerLoadAutoSaveDocuments,
TriggerLoadPreferences,
TriggerNodeGraphFrameGenerate {
#[serde(rename = "documentId")]
document_id: u64,
#[serde(rename = "layerPath")]
layer_path: Vec<LayerId>,
svg: String,
size: glam::DVec2,
},
TriggerOpenDocument,
TriggerPaste,
TriggerRasterDownload {

View file

@ -123,6 +123,13 @@ pub fn default_mapping() -> Mapping {
entry!(KeyDown(Escape); action_dispatch=ImaginateToolMessage::Abort),
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
entry!(KeyDown(Lmb); action_dispatch=EllipseToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=EllipseToolMessage::DragStop),

View file

@ -73,8 +73,8 @@ pub enum DocumentMessage {
FolderChanged {
affected_folder_path: Vec<LayerId>,
},
FrameClear,
GroupSelectedLayers,
ImaginateClear,
ImaginateGenerate,
ImaginateTerminate,
LayerChanged {
@ -89,6 +89,7 @@ pub enum DocumentMessage {
layer_path: Vec<LayerId>,
delta: (f64, f64),
},
NodeGraphFrameGenerate,
NudgeSelectedLayers {
delta_x: f64,
delta_y: f64,

View file

@ -377,6 +377,30 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
let affected_layer_path = affected_folder_path;
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 => {
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(),
);
}
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 => {
if let Some(message) = self.call_imaginate(document_id, preferences, persistent_data) {
// 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());
}
}
NodeGraphFrameGenerate => {
if let Some(message) = self.call_node_graph_frame(document_id, preferences, persistent_data) {
responses.push_back(message);
}
}
NudgeSelectedLayers { delta_x, delta_y } => {
self.backup(responses);
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());
}
}
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(_) => {}
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(
@ -936,7 +955,7 @@ impl DocumentMessageHandler {
};
let base_image = if imaginate_layer.use_img2img {
// 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 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
fn remove_document_transform(&mut self) -> [DAffine2; 2] {
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(),
});
}
}
_ => {}
}
}

View file

@ -14,7 +14,8 @@ use crate::messages::prelude::*;
use graphene::color::Color;
use graphene::document::pick_layer_safe_imaginate_resolution;
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::text_layer::{FontCache, TextLayer};
@ -251,6 +252,11 @@ pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque
tooltip: "Imaginate".into(),
..Default::default()
})),
LayerDataType::NodeGraphFrame(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
icon: "NodeNodes".into(),
tooltip: "Node Graph Frame".into(),
..Default::default()
})),
},
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Related,
@ -307,6 +313,9 @@ pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque
LayerDataType::Imaginate(imaginate) => {
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(_) => {
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 {
label: "Clear".into(),
tooltip: "Remove generated image from the layer frame".into(),
disabled: imaginate_layer.blob_url == None,
on_update: WidgetCallback::new(|_| DocumentMessage::ImaginateClear.into()),
disabled: imaginate_layer.blob_url.is_none(),
on_update: WidgetCallback::new(|_| DocumentMessage::FrameClear.into()),
..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 {
let font = layer.font.clone();
let size = layer.size;
@ -1425,7 +1483,7 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup {
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(stroke.weight() as f64),
value: Some(stroke.weight()),
is_integer: false,
min: Some(0.),
unit: " px".into(),
@ -1473,7 +1531,7 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup {
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(stroke.dash_offset() as f64),
value: Some(stroke.dash_offset()),
is_integer: true,
min: Some(0.),
unit: " px".into(),

View file

@ -109,6 +109,12 @@ pub enum PortfolioMessage {
data: String,
},
PrevDocument,
ProcessNodeGraphFrame {
document_id: u64,
layer_path: Vec<LayerId>,
image_data: Vec<u8>,
size: (u32, u32),
},
SelectDocument {
document_id: u64,
},

View file

@ -2,16 +2,16 @@ use super::utility_types::PersistentData;
use crate::application::generate_uuid;
use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION};
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::misc::LayoutTarget;
use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use crate::messages::portfolio::utility_types::ImaginateServerStatus;
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::Operation as DocumentOperation;
use graphene::{LayerId, Operation as DocumentOperation};
#[derive(Debug, Clone, Default)]
pub struct PortfolioMessageHandler {
@ -413,6 +413,86 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
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 } => {
if let Some(document) = self.active_document() {
if !document.is_auto_saved() {

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::line_tool::{LineToolMessage, LineToolMessageDiscriminant};
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::pen_tool::{PenToolMessage, PenToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::rectangle_tool::{RectangleToolMessage, RectangleToolMessageDiscriminant};

View file

@ -78,6 +78,9 @@ pub enum ToolMessage {
#[remain::unsorted]
#[child]
Imaginate(ImaginateToolMessage),
#[remain::unsorted]
#[child]
NodeGraphFrame(NodeGraphFrameToolMessage),
// Messages
#[remain::unsorted]
@ -114,6 +117,8 @@ pub enum ToolMessage {
#[remain::unsorted]
ActivateToolImaginate,
#[remain::unsorted]
ActivateToolNodeGraphFrame,
ActivateTool {
tool_type: ToolType,

View file

@ -7,6 +7,7 @@ pub mod gradient_tool;
pub mod imaginate_tool;
pub mod line_tool;
pub mod navigate_tool;
pub mod node_graph_frame_tool;
pub mod path_tool;
pub mod pen_tool;
pub mod rectangle_tool;

View file

@ -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());
}
}

View file

@ -281,6 +281,7 @@ pub enum ToolType {
Detail,
Relight,
Imaginate,
NodeGraphFrame,
}
enum ToolAvailability {
@ -293,28 +294,29 @@ fn list_tools_in_groups() -> Vec<Vec<ToolAvailability>> {
vec![
vec![
// General tool group
ToolAvailability::Available(Box::new(select_tool::SelectTool::default())),
ToolAvailability::Available(Box::new(artboard_tool::ArtboardTool::default())),
ToolAvailability::Available(Box::new(navigate_tool::NavigateTool::default())),
ToolAvailability::Available(Box::new(eyedropper_tool::EyedropperTool::default())),
ToolAvailability::Available(Box::new(fill_tool::FillTool::default())),
ToolAvailability::Available(Box::new(gradient_tool::GradientTool::default())),
ToolAvailability::Available(Box::<select_tool::SelectTool>::default()),
ToolAvailability::Available(Box::<artboard_tool::ArtboardTool>::default()),
ToolAvailability::Available(Box::<navigate_tool::NavigateTool>::default()),
ToolAvailability::Available(Box::<eyedropper_tool::EyedropperTool>::default()),
ToolAvailability::Available(Box::<fill_tool::FillTool>::default()),
ToolAvailability::Available(Box::<gradient_tool::GradientTool>::default()),
],
vec![
// Vector tool group
ToolAvailability::Available(Box::new(path_tool::PathTool::default())),
ToolAvailability::Available(Box::new(pen_tool::PenTool::default())),
ToolAvailability::Available(Box::new(freehand_tool::FreehandTool::default())),
ToolAvailability::Available(Box::new(spline_tool::SplineTool::default())),
ToolAvailability::Available(Box::new(line_tool::LineTool::default())),
ToolAvailability::Available(Box::new(rectangle_tool::RectangleTool::default())),
ToolAvailability::Available(Box::new(ellipse_tool::EllipseTool::default())),
ToolAvailability::Available(Box::new(shape_tool::ShapeTool::default())),
ToolAvailability::Available(Box::new(text_tool::TextTool::default())),
ToolAvailability::Available(Box::<path_tool::PathTool>::default()),
ToolAvailability::Available(Box::<pen_tool::PenTool>::default()),
ToolAvailability::Available(Box::<freehand_tool::FreehandTool>::default()),
ToolAvailability::Available(Box::<spline_tool::SplineTool>::default()),
ToolAvailability::Available(Box::<line_tool::LineTool>::default()),
ToolAvailability::Available(Box::<rectangle_tool::RectangleTool>::default()),
ToolAvailability::Available(Box::<ellipse_tool::EllipseTool>::default()),
ToolAvailability::Available(Box::<shape_tool::ShapeTool>::default()),
ToolAvailability::Available(Box::<text_tool::TextTool>::default()),
],
vec![
// 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 {
tool_type: ToolType::Brush,
icon_name: "RasterBrushTool".into(),
@ -384,6 +386,7 @@ pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType {
// ToolMessage::Detail(_) => ToolType::Detail,
// ToolMessage::Relight(_) => ToolType::Relight,
ToolMessage::Imaginate(_) => ToolType::Imaginate,
ToolMessage::NodeGraphFrame(_) => ToolType::NodeGraphFrame,
_ => panic!(
"Conversion from ToolMessage to ToolType impossible because the given ToolMessage does not have a matching ToolType. Got: {:?}",
tool_message
@ -420,6 +423,7 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis
// ToolType::Detail => ToolMessageDiscriminant::ActivateToolDetail,
// ToolType::Relight => ToolMessageDiscriminant::ActivateToolRelight,
ToolType::Imaginate => ToolMessageDiscriminant::ActivateToolImaginate,
ToolType::NodeGraphFrame => ToolMessageDiscriminant::ActivateToolNodeGraphFrame,
_ => panic!(
"Conversion from ToolType to ToolMessage impossible because the given ToolType does not have a matching ToolMessage. Got: {:?}",
tool_type

View file

@ -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

View file

@ -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

View file

@ -3,7 +3,7 @@ import { reactive, readonly } from "vue";
import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files";
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 FrontendDocumentDetails,
@ -14,6 +14,7 @@ import {
TriggerImaginateGenerate,
TriggerImaginateTerminate,
TriggerImaginateCheckServerStatus,
TriggerNodeGraphFrameGenerate,
UpdateActiveDocument,
UpdateOpenDocumentsList,
UpdateImageData,
@ -100,6 +101,14 @@ export function createPortfolioState(editor: Editor) {
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) => {
URL.revokeObjectURL(triggerRevokeBlobUrl.url);
});

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 NodeMask from "@/../assets/icon-16px-solid/node-mask.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 NodeShape from "@/../assets/icon-16px-solid/node-shape.svg";
import NodeText from "@/../assets/icon-16px-solid/node-text.svg";
@ -168,6 +169,7 @@ const SOLID_16PX = {
NodeMagicWand: { component: NodeMagicWand, size: 16 },
NodeMask: { component: NodeMask, size: 16 },
NodeMotionBlur: { component: NodeMotionBlur, size: 16 },
NodeNodes: { component: NodeNodes, size: 16 },
NodeOutput: { component: NodeOutput, size: 16 },
NodeShape: { component: NodeShape, 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 RasterHealTool from "@/../assets/icon-24px-two-tone/raster-heal-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 RasterRelightTool from "@/../assets/icon-24px-two-tone/raster-relight-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 },
GeneralSelectTool: { component: GeneralSelectTool, size: 24 },
RasterImaginateTool: { component: RasterImaginateTool, size: 24 },
RasterNodesTool: { component: RasterNodesTool, size: 24 },
RasterBrushTool: { component: RasterBrushTool, size: 24 },
RasterCloneTool: { component: RasterCloneTool, size: 24 },
RasterDetailTool: { component: RasterDetailTool, size: 24 },

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()`
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()`
export async function initWasm(): Promise<void> {
@ -16,6 +30,7 @@ export async function initWasm(): Promise<void> {
if (wasmImport !== undefined) return;
// 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);
// 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
subscriptions.handleJsMessage(messageType, messageData, raw, instance);
});
editorInstance = instance;
// Subscriptions: Allows subscribing to messages in JS that are sent from the WASM backend
const subscriptions: SubscriptionRouter = createSubscriptionRouter();

View file

@ -514,6 +514,16 @@ export class TriggerImaginateTerminate extends JsMessage {
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 TriggerRevokeBlobUrl extends JsMessage {
@ -641,7 +651,7 @@ export class LayerMetadata {
selected!: boolean;
}
export type LayerType = "Imaginate" | "Folder" | "Image" | "Shape" | "Text";
export type LayerType = "Imaginate" | "NodeGraphFrame" | "Folder" | "Image" | "Shape" | "Text";
export type LayerTypeData = {
name: string;
@ -651,6 +661,7 @@ export type LayerTypeData = {
export function layerTypeData(layerType: LayerType): LayerTypeData | undefined {
const entries: Record<string, LayerTypeData> = {
Imaginate: { name: "Imaginate", icon: "NodeImaginate" },
NodeGraphFrame: { name: "Node Graph Frame", icon: "NodeNodes" },
Folder: { name: "Folder", icon: "NodeFolder" },
Image: { name: "Image", icon: "NodeImage" },
Shape: { name: "Shape", icon: "NodeShape" },
@ -1218,6 +1229,7 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerImaginateCheckServerStatus,
TriggerImaginateGenerate,
TriggerImaginateTerminate,
TriggerNodeGraphFrameGenerate,
TriggerFileDownload,
TriggerFontLoad,
TriggerImport,

View file

@ -28,6 +28,13 @@ pub fn set_random_seed(seed: u64) {
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
#[wasm_bindgen(js_name = wasmMemory)]
pub fn wasm_memory() -> JsValue {
@ -90,6 +97,14 @@ impl JsEditorHandle {
// Sends a FrontendMessage to JavaScript
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 serializer = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true);
@ -512,6 +527,18 @@ impl JsEditorHandle {
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
#[wasm_bindgen(js_name = pasteImage)]
pub fn paste_image(&self, mime: String, image_data: Vec<u8>, mouse_x: Option<f64>, mouse_y: Option<f64>) {

View file

@ -11,6 +11,9 @@ repository = "https://github.com/GraphiteEditor/Graphite"
license = "Apache-2.0"
[dependencies]
graph-craft = { path = "../node-graph/graph-craft", features = ["serde"] }
image = { version = "0.24", default-features = false }
log = "0.4"
bezier-rs = { path = "../libraries/bezier-rs" }

View file

@ -4,6 +4,7 @@ use crate::layers::folder_layer::FolderLayer;
use crate::layers::image_layer::ImageLayer;
use crate::layers::imaginate_layer::{ImaginateImageData, ImaginateLayer, ImaginateStatus};
use crate::layers::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDiscriminant};
use crate::layers::nodegraph_layer::NodeGraphFrameLayer;
use crate::layers::shape_layer::ShapeLayer;
use crate::layers::style::RenderData;
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())
}
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 } => {
self.layer_mut(&path)?.as_text_mut()?.editable = editable;
self.mark_as_dirty(&path)?;
@ -797,11 +814,15 @@ impl Document {
image.blob_url = Some(blob_url);
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) => {
imaginate.blob_url = Some(blob_url);
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)?;
@ -833,15 +854,20 @@ impl Document {
}
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");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
match &mut layer.data {
LayerDataType::Imaginate(imaginate) => {
imaginate.image_data = None;
imaginate.blob_url = None;
imaginate.status = ImaginateStatus::Idle;
imaginate.percent_complete = 0.;
} else {
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)?;
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())

View file

@ -2,6 +2,7 @@ use super::blend_mode::BlendMode;
use super::folder_layer::FolderLayer;
use super::image_layer::ImageLayer;
use super::imaginate_layer::ImaginateLayer;
use super::nodegraph_layer::NodeGraphFrameLayer;
use super::shape_layer::ShapeLayer;
use super::style::{PathStyle, RenderData};
use super::text_layer::TextLayer;
@ -27,8 +28,10 @@ pub enum LayerDataType {
Text(TextLayer),
/// A layer that wraps an [ImageLayer] struct.
Image(ImageLayer),
/// A layer that wraps an [ImageLayer] struct.
/// A layer that wraps an [ImaginateLayer] struct.
Imaginate(ImaginateLayer),
/// A layer that wraps an [NodeGraphFrameLayer] struct.
NodeGraphFrame(NodeGraphFrameLayer),
}
impl LayerDataType {
@ -39,6 +42,7 @@ impl LayerDataType {
LayerDataType::Text(t) => t,
LayerDataType::Image(i) => i,
LayerDataType::Imaginate(a) => a,
LayerDataType::NodeGraphFrame(n) => n,
}
}
@ -49,6 +53,7 @@ impl LayerDataType {
LayerDataType::Text(t) => t,
LayerDataType::Image(i) => i,
LayerDataType::Imaginate(a) => a,
LayerDataType::NodeGraphFrame(n) => n,
}
}
}
@ -60,6 +65,7 @@ pub enum LayerDataTypeDiscriminant {
Text,
Image,
Imaginate,
NodeGraphFrame,
}
impl fmt::Display for LayerDataTypeDiscriminant {
@ -70,6 +76,7 @@ impl fmt::Display for LayerDataTypeDiscriminant {
LayerDataTypeDiscriminant::Text => write!(f, "Text"),
LayerDataTypeDiscriminant::Image => write!(f, "Image"),
LayerDataTypeDiscriminant::Imaginate => write!(f, "Imaginate"),
LayerDataTypeDiscriminant::NodeGraphFrame => write!(f, "NodeGraphFrame"),
}
}
}
@ -84,6 +91,7 @@ impl From<&LayerDataType> for LayerDataTypeDiscriminant {
Text(_) => LayerDataTypeDiscriminant::Text,
Image(_) => LayerDataTypeDiscriminant::Image,
Imaginate(_) => LayerDataTypeDiscriminant::Imaginate,
NodeGraphFrame(_) => LayerDataTypeDiscriminant::NodeGraphFrame,
}
}
}

View file

@ -27,6 +27,8 @@ pub mod image_layer;
pub mod imaginate_layer;
/// Contains the base [Layer](layer_info::Layer) type, an abstraction over the different types of layers.
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.
pub mod shape_layer;
pub mod style;

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,
}
}
}

View file

@ -57,6 +57,15 @@ pub enum Operation {
insert_index: isize,
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.
/// **Be sure to call `FrontendMessage::TriggerRevokeBlobUrl` together with this.**
SetLayerBlobUrl {
@ -64,9 +73,9 @@ pub enum Operation {
blob_url: String,
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.**
ImaginateClear {
ClearBlobURL {
path: Vec<LayerId>,
},
ImaginateSetGeneratingStatus {

View file

@ -14,3 +14,8 @@ num-traits = "0.2"
borrow_stack = { path = "../borrow_stack" }
dyn-clone = "1.0"
rand_chacha = "0.3.1"
[dependencies.serde]
version = "1.0"
optional = true
features = ["derive"]

View file

@ -28,9 +28,8 @@ fn merge_ids(a: u64, b: u64) -> u64 {
hasher.finish()
}
type Fqn = NodeIdentifier<'static>;
#[derive(Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DocumentNode {
pub name: String,
pub inputs: Vec<NodeInput>,
@ -55,9 +54,9 @@ impl DocumentNode {
let first = self.inputs.remove(0);
if let DocumentNodeImplementation::Unresolved(fqn) = self.implementation {
let (input, mut args) = match first {
NodeInput::Value(value) => {
NodeInput::Value(tagged_value) => {
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::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 {
Node(NodeId),
Value(value::Value),
Value(value::TaggedValue),
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 {
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 inputs: Vec<NodeId>,
pub output: NodeId,
@ -160,7 +162,7 @@ impl NodeNetwork {
network_input.populate_first_network_input(node, *offset);
}
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 value_node = DocumentNode {
name: name.clone(),
@ -284,7 +286,7 @@ mod test {
1,
DocumentNode {
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()),
},
)]
@ -384,7 +386,7 @@ mod test {
14,
DocumentNode {
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])),
},
),

View file

@ -4,6 +4,26 @@ use dyn_clone::DynClone;
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 trait ValueTrait: DynAny<'static> + Upcast<dyn DynAny<'static>> + std::fmt::Debug + DynClone {}

View file

@ -11,7 +11,6 @@ mod tests {
use graphene_core::value::ValueNode;
use graphene_core::{structural::*, RefNode};
use crate::document::value::IntoValue;
use borrow_stack::BorrowStack;
use borrow_stack::FixedSizeStack;
use dyn_any::{downcast, IntoDynAny};
@ -68,7 +67,10 @@ mod tests {
DocumentNode {
name: "cons".into(),
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 {
name: "add".into(),
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,
DocumentNode {
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()),
},
)]

View file

@ -1,11 +1,9 @@
use std::marker::PhantomData;
use borrow_stack::FixedSizeStack;
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::structural::{ConsNode, Then};
use graphene_core::{AsRefNode, Node};
use graphene_core::Node;
use graphene_std::any::DowncastBothNode;
use graphene_std::any::{Any, DowncastNode, DynAnyNode, IntoTypeErasedNode, TypeErasedNode};
use graphene_std::raster::Image;
@ -13,17 +11,12 @@ use graphene_std::raster::Image;
use crate::proto::Type;
use crate::proto::{ConstructionArgs, NodeIdentifier, ProtoNode, ProtoNodeInput, Type::Concrete};
use dyn_any::Upcast;
type NodeConstructor = fn(ProtoNode, &FixedSizeStack<TypeErasedNode<'static>>);
//TODO: turn into hasmap
static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
(
NodeIdentifier {
name: "graphene_core::ops::IdNode",
types: &[Concrete("Any<'_>")],
},
NodeIdentifier::new("graphene_core::ops::IdNode", &[Concrete(std::borrow::Cow::Borrowed("Any<'_>"))]),
|proto_node, stack| {
stack.push_fn(|nodes| {
let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap();
@ -32,24 +25,18 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
})
},
),
(
NodeIdentifier {
name: "graphene_core::ops::IdNode",
types: &[Type::Generic],
},
|proto_node, stack| {
(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 {
name: "graphene_core::ops::AddNode",
types: &[Concrete("u32"), Concrete("u32")],
},
NodeIdentifier::new(
"graphene_core::ops::AddNode",
&[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();
@ -61,10 +48,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
},
),
(
NodeIdentifier {
name: "graphene_core::ops::AddNode",
types: &[Concrete("&u32"), Concrete("&u32")],
},
NodeIdentifier::new(
"graphene_core::ops::AddNode",
&[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();
@ -76,10 +63,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
},
),
(
NodeIdentifier {
name: "graphene_core::ops::AddNode",
types: &[Concrete("&u32"), Concrete("u32")],
},
NodeIdentifier::new(
"graphene_core::ops::AddNode",
&[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();
@ -91,10 +78,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
},
),
(
NodeIdentifier {
name: "graphene_core::structural::ConsNode",
types: &[Concrete("&u32"), Concrete("u32")],
},
NodeIdentifier::new(
"graphene_core::structural::ConsNode",
&[Concrete(std::borrow::Cow::Borrowed("&u32")), Concrete(std::borrow::Cow::Borrowed("u32"))],
),
|proto_node, stack| {
if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args {
stack.push_fn(move |nodes| {
@ -118,10 +105,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
},
),
(
NodeIdentifier {
name: "graphene_core::structural::ConsNode",
types: &[Concrete("u32"), Concrete("u32")],
},
NodeIdentifier::new(
"graphene_core::structural::ConsNode",
&[Concrete(std::borrow::Cow::Borrowed("u32")), Concrete(std::borrow::Cow::Borrowed("u32"))],
),
|proto_node, stack| {
if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args {
stack.push_fn(move |nodes| {
@ -146,10 +133,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
),
// TODO: create macro to impl for all types
(
NodeIdentifier {
name: "graphene_core::structural::ConsNode",
types: &[Concrete("&u32"), Concrete("&u32")],
},
NodeIdentifier::new(
"graphene_core::structural::ConsNode",
&[Concrete(std::borrow::Cow::Borrowed("&u32")), Concrete(std::borrow::Cow::Borrowed("&u32"))],
),
|proto_node, stack| {
let node_id = proto_node.input.unwrap_node() as usize;
if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args {
@ -168,10 +155,7 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
},
),
(
NodeIdentifier {
name: "graphene_core::any::DowncastNode",
types: &[Concrete("&u32")],
},
NodeIdentifier::new("graphene_core::any::DowncastNode", &[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();
@ -181,10 +165,7 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
},
),
(
NodeIdentifier {
name: "graphene_core::value::ValueNode",
types: &[Concrete("Any<'_>")],
},
NodeIdentifier::new("graphene_core::value::ValueNode", &[Concrete(std::borrow::Cow::Borrowed("Any<'_>"))]),
|proto_node, stack| {
stack.push_fn(|_nodes| {
if let ConstructionArgs::Value(value) = proto_node.construction_args {
@ -197,12 +178,7 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
})
},
),
(
NodeIdentifier {
name: "graphene_core::value::ValueNode",
types: &[Type::Generic],
},
|proto_node, stack| {
(NodeIdentifier::new("graphene_core::value::ValueNode", &[Type::Generic]), |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>);
@ -211,14 +187,8 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
unreachable!()
}
})
},
),
(
NodeIdentifier {
name: "graphene_core::raster::GrayscaleNode",
types: &[],
},
|proto_node, stack| {
}),
(NodeIdentifier::new("graphene_core::raster::GrayscaleNode", &[]), |proto_node, stack| {
stack.push_fn(|nodes| {
let node = DynAnyNode::new(graphene_core::raster::GrayscaleNode);
@ -229,37 +199,27 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
node.into_type_erased()
}
})
},
),
(
NodeIdentifier {
name: "graphene_std::raster::MapImageNode",
types: &[],
},
|proto_node, stack| {
let node_id = proto_node.input.unwrap_node() as usize;
}),
(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 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()
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 {
name: "graphene_std::raster::ImageNode",
types: &[Concrete("&str")],
},
NodeIdentifier::new("graphene_std::raster::ImageNode", &[Concrete(std::borrow::Cow::Borrowed("&str"))]),
|_proto_node, stack| {
stack.push_fn(|_nodes| {
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 {
name: "graphene_std::raster::ExportImageNode",
types: &[Concrete("&str")],
},
NodeIdentifier::new("graphene_std::raster::ExportImageNode", &[Concrete(std::borrow::Cow::Borrowed("&str"))]),
|proto_node, stack| {
stack.push_fn(|nodes| {
let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap();
@ -285,10 +242,10 @@ static NODE_REGISTRY: &[(NodeIdentifier<'static>, NodeConstructor)] = &[
},
),
(
NodeIdentifier {
name: "graphene_core::structural::ConsNode",
types: &[Concrete("Image"), Concrete("&str")],
},
NodeIdentifier::new(
"graphene_core::structural::ConsNode",
&[Concrete(std::borrow::Cow::Borrowed("Image")), Concrete(std::borrow::Cow::Borrowed("&str"))],
),
|proto_node, stack| {
let node_id = proto_node.input.unwrap_node() as usize;
if let ConstructionArgs::Nodes(cons_node_arg) = proto_node.construction_args {
@ -322,11 +279,6 @@ mod protograph_testing {
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]
fn add_values() {
let stack = FixedSizeStack::new(256);
@ -339,14 +291,20 @@ mod protograph_testing {
let cons_protonode = ProtoNode {
construction_args: ConstructionArgs::Nodes(vec![1]),
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);
let add_protonode = ProtoNode {
construction_args: ConstructionArgs::Nodes(vec![]),
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);
@ -379,7 +337,7 @@ mod protograph_testing {
let image_protonode = ProtoNode {
construction_args: ConstructionArgs::Nodes(vec![]),
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);
@ -394,7 +352,7 @@ mod protograph_testing {
let image_protonode = ProtoNode {
construction_args: ConstructionArgs::Nodes(vec![]),
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);

View file

@ -1,35 +1,49 @@
use std::collections::HashMap;
use std::collections::HashSet;
use crate::document::value;
use crate::document::NodeId;
#[derive(Debug, PartialEq)]
pub struct NodeIdentifier<'a> {
pub name: &'a str,
pub types: &'a [Type<'a>],
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NodeIdentifier {
pub name: std::borrow::Cow<'static, str>,
pub types: std::borrow::Cow<'static, [Type]>,
}
#[derive(Debug, PartialEq)]
pub enum Type<'a> {
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Type {
Generic,
Concrete(&'a str),
Concrete(std::borrow::Cow<'static, str>),
}
impl<'a> From<&'a str> for Type<'a> {
fn from(s: &'a str) -> Self {
Type::Concrete(s)
}
}
impl<'a> From<&'a str> for NodeIdentifier<'a> {
fn from(s: &'a str) -> Self {
NodeIdentifier { name: s, types: &[] }
impl From<&'static str> for Type {
fn from(s: &'static str) -> Self {
Type::Concrete(std::borrow::Cow::Borrowed(s))
}
}
impl<'a> NodeIdentifier<'a> {
pub fn new(name: &'a str, types: &'a [Type<'a>]) -> Self {
NodeIdentifier { name, types }
impl Type {
pub const fn from_str(concrete: &'static str) -> Self {
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 construction_args: ConstructionArgs,
pub input: ProtoNodeInput,
pub identifier: NodeIdentifier<'static>,
pub identifier: NodeIdentifier,
}
#[derive(Debug, Default, PartialEq, Eq)]
@ -83,10 +97,7 @@ impl ProtoNodeInput {
impl ProtoNode {
pub fn value(value: ConstructionArgs) -> Self {
Self {
identifier: NodeIdentifier {
name: "graphene_core::value::ValueNode",
types: &[Type::Generic],
},
identifier: NodeIdentifier::new("graphene_core::value::ValueNode", &[Type::Generic]),
construction_args: value,
input: ProtoNodeInput::None,
}
@ -110,7 +121,22 @@ impl ProtoNode {
}
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();
for (id, node) in &self.nodes {
if let ProtoNodeInput::Node(ref_id) = &node.input {
@ -125,38 +151,28 @@ impl ProtoNetwork {
edges
}
// Based on https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
pub fn topological_sort(&self) -> Vec<NodeId> {
let mut visited = HashSet::new();
let mut stack = Vec::new();
let mut sorted = Vec::new();
let graph = self.reverse_edges();
// TODO: remove
println!("{:#?}", graph);
let outwards_edges = self.collect_outwards_edges();
let mut inwards_edges = self.collect_inwards_edges();
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 {
if !visited.contains(id) {
stack.push(*id);
assert_ne!(no_incoming_edges.len(), 0, "Acyclic graphs must have at least one node with no incoming edge");
while let Some(id) = stack.pop() {
//TODO remove
println!("{:?}", stack);
if !visited.contains(&id) {
visited.insert(id);
if let Some(refs) = graph.get(&id) {
for ref_id in refs {
if !visited.contains(ref_id) {
stack.push(id);
stack.push(*ref_id);
break;
}
}
}
sorted.push(id);
while let Some(node_id) = no_incoming_edges.pop() {
sorted.push(node_id);
if let Some(outwards_edges) = outwards_edges.get(&node_id) {
for &ref_id in outwards_edges {
let dependencies = inwards_edges.get_mut(&ref_id).unwrap();
dependencies.retain(|&id| id != node_id);
if dependencies.is_empty() {
no_incoming_edges.push(ref_id)
}
}
}
}
sorted.reverse();
sorted
}