mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-02 12:32:17 +00:00
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:
parent
1462d2b662
commit
18507b78ac
33 changed files with 1018 additions and 258 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
231
editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs
Normal file
231
editor/src/messages/tool/tool_messages/node_graph_frame_tool.rs
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
7
frontend/assets/icon-16px-solid/node-nodes.svg
Normal file
7
frontend/assets/icon-16px-solid/node-nodes.svg
Normal 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 |
6
frontend/assets/icon-24px-two-tone/raster-nodes-tool.svg
Normal file
6
frontend/assets/icon-24px-two-tone/raster-nodes-tool.svg
Normal 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 |
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
152
graphene/src/layers/nodegraph_layer.rs
Normal file
152
graphene/src/layers/nodegraph_layer.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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])),
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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()),
|
||||
},
|
||||
)]
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue