mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Replace the image layer type with an Image node (#948)
* Use builder pattern for widgets * Arguments to new function * Add node graph when dragging in image * Fix duplicate import * Skip processing under node graph frame if unused * Reduce node graph rerenders * DUPLICATE ALL frontend changes into other frontend * DUPLICATE more changes to another frontend * Code review * Allow importing SVG files as bitmaps Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
parent
66e8325362
commit
64e62699fc
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1744,6 +1744,7 @@ version = "0.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bezier-rs",
|
"bezier-rs",
|
||||||
"graph-craft",
|
"graph-craft",
|
||||||
|
"graphene-core",
|
||||||
"graphite-document-legacy",
|
"graphite-document-legacy",
|
||||||
"graphite-editor",
|
"graphite-editor",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -1762,6 +1763,7 @@ version = "0.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bezier-rs",
|
"bezier-rs",
|
||||||
"graph-craft",
|
"graph-craft",
|
||||||
|
"graphene-core",
|
||||||
"graphite-document-legacy",
|
"graphite-document-legacy",
|
||||||
"graphite-editor",
|
"graphite-editor",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|||||||
@@ -1045,7 +1045,7 @@ impl Document {
|
|||||||
match &mut layer.data {
|
match &mut layer.data {
|
||||||
LayerDataType::Shape(s) => s.style = style,
|
LayerDataType::Shape(s) => s.style = style,
|
||||||
LayerDataType::Text(text) => text.path_style = style,
|
LayerDataType::Text(text) => text.path_style = style,
|
||||||
_ => return Err(DocumentError::NotAShape),
|
_ => return Err(DocumentError::NotShape),
|
||||||
}
|
}
|
||||||
self.mark_as_dirty(&path)?;
|
self.mark_as_dirty(&path)?;
|
||||||
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ pub enum DocumentError {
|
|||||||
IndexOutOfBounds,
|
IndexOutOfBounds,
|
||||||
NotAFolder,
|
NotAFolder,
|
||||||
NonReorderableSelection,
|
NonReorderableSelection,
|
||||||
NotAShape,
|
NotShape,
|
||||||
NotText,
|
NotText,
|
||||||
NotAnImage,
|
NotNodeGraph,
|
||||||
NotAnImaginate,
|
|
||||||
InvalidFile(String),
|
InvalidFile(String),
|
||||||
BooleanOperationError(BooleanOperationError),
|
BooleanOperationError(BooleanOperationError),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -488,21 +488,21 @@ impl Layer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a mutable reference to the Image element wrapped by the layer.
|
/// Get a mutable reference to the NodeNetwork
|
||||||
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Image`.
|
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::NodeGraphFrame`.
|
||||||
pub fn as_image_mut(&mut self) -> Result<&mut ImageLayer, DocumentError> {
|
pub fn as_node_graph_mut(&mut self) -> Result<&mut graph_craft::document::NodeNetwork, DocumentError> {
|
||||||
match &mut self.data {
|
match &mut self.data {
|
||||||
LayerDataType::Image(img) => Ok(img),
|
LayerDataType::NodeGraphFrame(frame) => Ok(&mut frame.network),
|
||||||
_ => Err(DocumentError::NotAnImage),
|
_ => Err(DocumentError::NotNodeGraph),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to the Image element wrapped by the layer.
|
/// Get a reference to the NodeNetwork
|
||||||
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Image`.
|
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::NodeGraphFrame`.
|
||||||
pub fn as_image(&self) -> Result<&ImageLayer, DocumentError> {
|
pub fn as_node_graph(&self) -> Result<&graph_craft::document::NodeNetwork, DocumentError> {
|
||||||
match &self.data {
|
match &self.data {
|
||||||
LayerDataType::Image(img) => Ok(img),
|
LayerDataType::NodeGraphFrame(frame) => Ok(&frame.network),
|
||||||
_ => Err(DocumentError::NotAnImage),
|
_ => Err(DocumentError::NotNodeGraph),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,7 +510,7 @@ impl Layer {
|
|||||||
match &self.data {
|
match &self.data {
|
||||||
LayerDataType::Shape(s) => Ok(&s.style),
|
LayerDataType::Shape(s) => Ok(&s.style),
|
||||||
LayerDataType::Text(t) => Ok(&t.path_style),
|
LayerDataType::Text(t) => Ok(&t.path_style),
|
||||||
_ => Err(DocumentError::NotAShape),
|
_ => Err(DocumentError::NotShape),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,7 +518,7 @@ impl Layer {
|
|||||||
match &mut self.data {
|
match &mut self.data {
|
||||||
LayerDataType::Shape(s) => Ok(&mut s.style),
|
LayerDataType::Shape(s) => Ok(&mut s.style),
|
||||||
LayerDataType::Text(t) => Ok(&mut t.path_style),
|
LayerDataType::Text(t) => Ok(&mut t.path_style),
|
||||||
_ => Err(DocumentError::NotAShape),
|
_ => Err(DocumentError::NotShape),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use document_legacy::layers::style::ViewMode;
|
|||||||
use document_legacy::LayerId;
|
use document_legacy::LayerId;
|
||||||
use document_legacy::Operation as DocumentOperation;
|
use document_legacy::Operation as DocumentOperation;
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
|
use graphene_core::raster::Image;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[remain::sorted]
|
#[remain::sorted]
|
||||||
@@ -115,8 +116,7 @@ pub enum DocumentMessage {
|
|||||||
delta_y: f64,
|
delta_y: f64,
|
||||||
},
|
},
|
||||||
PasteImage {
|
PasteImage {
|
||||||
mime: String,
|
image: Image,
|
||||||
image_data: Vec<u8>,
|
|
||||||
mouse: Option<(f64, f64)>,
|
mouse: Option<(f64, f64)>,
|
||||||
},
|
},
|
||||||
Redo,
|
Redo,
|
||||||
|
|||||||
@@ -546,25 +546,36 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
|
|||||||
}
|
}
|
||||||
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
|
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
|
||||||
}
|
}
|
||||||
PasteImage { mime, image_data, mouse } => {
|
PasteImage { image, mouse } => {
|
||||||
|
let image_size = DVec2::new(image.width as f64, image.height as f64);
|
||||||
|
|
||||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
|
|
||||||
let path = vec![generate_uuid()];
|
let path = vec![generate_uuid()];
|
||||||
responses.push_back(
|
let image_node_id = 2;
|
||||||
DocumentOperation::AddImage {
|
let mut network = graph_craft::document::NodeNetwork::new_network(32, image_node_id);
|
||||||
path: path.clone(),
|
|
||||||
transform: DAffine2::ZERO.to_cols_array(),
|
let Some(image_node_type) = crate::messages::portfolio::document::node_graph::resolve_document_node_type("Image") else {
|
||||||
insert_index: -1,
|
warn!("Image node should be in registry");
|
||||||
image_data: image_data.clone(),
|
return;
|
||||||
mime: mime.clone(),
|
};
|
||||||
}
|
|
||||||
.into(),
|
network.nodes.insert(
|
||||||
|
image_node_id,
|
||||||
|
graph_craft::document::DocumentNode {
|
||||||
|
name: image_node_type.name.to_string(),
|
||||||
|
inputs: vec![graph_craft::document::NodeInput::value(graph_craft::document::value::TaggedValue::Image(image), false)],
|
||||||
|
implementation: image_node_type.generate_implementation(),
|
||||||
|
metadata: graph_craft::document::DocumentNodeMetadata { position: (20, 4).into() },
|
||||||
|
},
|
||||||
);
|
);
|
||||||
let image_data = std::sync::Arc::new(image_data);
|
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
FrontendMessage::UpdateImageData {
|
DocumentOperation::AddNodeGraphFrame {
|
||||||
document_id,
|
path: path.clone(),
|
||||||
image_data: vec![FrontendImageData { path: path.clone(), image_data, mime }],
|
insert_index: -1,
|
||||||
|
transform: DAffine2::ZERO.to_cols_array(),
|
||||||
|
network,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
@@ -575,9 +586,21 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
|
|||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mouse = mouse.map_or(ipp.viewport_bounds.center(), |pos| pos.into());
|
// Transform of parent folder
|
||||||
let transform = DAffine2::from_translation(mouse - ipp.viewport_bounds.top_left).to_cols_array();
|
let to_parent_folder = self.document_legacy.generate_transform_across_scope(&path[..path.len() - 1], None).unwrap_or_default();
|
||||||
responses.push_back(DocumentOperation::SetLayerTransformInViewport { path, transform }.into());
|
|
||||||
|
// Align the layer with the mouse or center of viewport
|
||||||
|
let viewport_location = mouse.map_or(ipp.viewport_bounds.center(), |pos| pos.into());
|
||||||
|
let center_in_viewport = DAffine2::from_translation(viewport_location - ipp.viewport_bounds.top_left);
|
||||||
|
let center_in_viewport_layerspace = to_parent_folder.inverse() * center_in_viewport;
|
||||||
|
|
||||||
|
// Make layer the size of the image
|
||||||
|
let fit_image_size = DAffine2::from_scale_angle_translation(image_size, 0., image_size / -2.);
|
||||||
|
|
||||||
|
let transform = (center_in_viewport_layerspace * fit_image_size).to_cols_array();
|
||||||
|
responses.push_back(DocumentOperation::SetLayerTransform { path, transform }.into());
|
||||||
|
|
||||||
|
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
|
||||||
}
|
}
|
||||||
Redo => {
|
Redo => {
|
||||||
responses.push_back(SelectToolMessage::Abort.into());
|
responses.push_back(SelectToolMessage::Abort.into());
|
||||||
@@ -949,6 +972,24 @@ impl DocumentMessageHandler {
|
|||||||
|
|
||||||
// Prepare the node graph input image
|
// Prepare the node graph input image
|
||||||
|
|
||||||
|
let Some(node_network) = self.document_legacy.layer(&layer_path).ok().and_then(|layer|layer.as_node_graph().ok()) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skip processing under node graph frame input if not connected
|
||||||
|
if !node_network.connected_to_output(node_network.inputs[0]) {
|
||||||
|
return Some(
|
||||||
|
PortfolioMessage::ProcessNodeGraphFrame {
|
||||||
|
document_id,
|
||||||
|
layer_path,
|
||||||
|
image_data: Default::default(),
|
||||||
|
size: (0, 0),
|
||||||
|
imaginate_node,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate the size of the region to be exported
|
// Calculate the size of the region to be exported
|
||||||
|
|
||||||
let old_transforms = self.remove_document_transform();
|
let old_transforms = self.remove_document_transform();
|
||||||
@@ -1366,7 +1407,7 @@ impl DocumentMessageHandler {
|
|||||||
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into())
|
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
responses.push_back(NodeGraphMessage::SendGraph.into());
|
responses.push_back(NodeGraphMessage::SendGraph { should_rerender: true }.into());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1400,7 +1441,7 @@ impl DocumentMessageHandler {
|
|||||||
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into())
|
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
responses.push_back(NodeGraphMessage::SendGraph.into());
|
responses.push_back(NodeGraphMessage::SendGraph { should_rerender: true }.into());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ pub enum NodeGraphMessage {
|
|||||||
SelectNodes {
|
SelectNodes {
|
||||||
nodes: Vec<NodeId>,
|
nodes: Vec<NodeId>,
|
||||||
},
|
},
|
||||||
SendGraph,
|
SendGraph {
|
||||||
|
should_rerender: bool,
|
||||||
|
},
|
||||||
SetDrawing {
|
SetDrawing {
|
||||||
new_drawing: bool,
|
new_drawing: bool,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
|
pub use self::document_node_types::*;
|
||||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||||
use crate::messages::layout::utility_types::widgets::button_widgets::{BreadcrumbTrailButtons, TextButton};
|
use crate::messages::layout::utility_types::widgets::button_widgets::{BreadcrumbTrailButtons, TextButton};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
use document_legacy::document::Document;
|
use document_legacy::document::Document;
|
||||||
use document_legacy::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant};
|
use document_legacy::layers::layer_info::LayerDataTypeDiscriminant;
|
||||||
use document_legacy::layers::nodegraph_layer::NodeGraphFrameLayer;
|
use document_legacy::layers::nodegraph_layer::NodeGraphFrameLayer;
|
||||||
use document_legacy::LayerId;
|
use document_legacy::LayerId;
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, DocumentNodeMetadata, NodeId, NodeInput, NodeNetwork};
|
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork};
|
||||||
|
|
||||||
mod document_node_types;
|
mod document_node_types;
|
||||||
mod node_properties;
|
mod node_properties;
|
||||||
pub use self::document_node_types::*;
|
|
||||||
|
|
||||||
use glam::IVec2;
|
use glam::IVec2;
|
||||||
|
|
||||||
@@ -112,37 +111,21 @@ pub struct NodeGraphMessageHandler {
|
|||||||
|
|
||||||
impl NodeGraphMessageHandler {
|
impl NodeGraphMessageHandler {
|
||||||
fn get_root_network<'a>(&self, document: &'a Document) -> Option<&'a graph_craft::document::NodeNetwork> {
|
fn get_root_network<'a>(&self, document: &'a Document) -> Option<&'a graph_craft::document::NodeNetwork> {
|
||||||
self.layer_path.as_ref().and_then(|path| document.layer(path).ok()).and_then(|layer| match &layer.data {
|
self.layer_path.as_ref().and_then(|path| document.layer(path).ok()).and_then(|layer| layer.as_node_graph().ok())
|
||||||
LayerDataType::NodeGraphFrame(n) => Some(&n.network),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_root_network_mut<'a>(&self, document: &'a mut Document) -> Option<&'a mut graph_craft::document::NodeNetwork> {
|
fn get_root_network_mut<'a>(&self, document: &'a mut Document) -> Option<&'a mut graph_craft::document::NodeNetwork> {
|
||||||
self.layer_path.as_ref().and_then(|path| document.layer_mut(path).ok()).and_then(|layer| match &mut layer.data {
|
self.layer_path.as_ref().and_then(|path| document.layer_mut(path).ok()).and_then(|layer| layer.as_node_graph_mut().ok())
|
||||||
LayerDataType::NodeGraphFrame(n) => Some(&mut n.network),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the active graph_craft NodeNetwork struct
|
/// Get the active graph_craft NodeNetwork struct
|
||||||
fn get_active_network<'a>(&self, document: &'a Document) -> Option<&'a graph_craft::document::NodeNetwork> {
|
fn get_active_network<'a>(&self, document: &'a Document) -> Option<&'a graph_craft::document::NodeNetwork> {
|
||||||
let mut network = self.get_root_network(document);
|
self.get_root_network(document).and_then(|network| network.nested_network(&self.nested_path))
|
||||||
|
|
||||||
for segement in &self.nested_path {
|
|
||||||
network = network.and_then(|network| network.nodes.get(segement)).and_then(|node| node.implementation.get_network());
|
|
||||||
}
|
|
||||||
network
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the active graph_craft NodeNetwork struct
|
/// Get the active graph_craft NodeNetwork struct
|
||||||
fn get_active_network_mut<'a>(&self, document: &'a mut Document) -> Option<&'a mut graph_craft::document::NodeNetwork> {
|
fn get_active_network_mut<'a>(&self, document: &'a mut Document) -> Option<&'a mut graph_craft::document::NodeNetwork> {
|
||||||
let mut network = self.get_root_network_mut(document);
|
self.get_root_network_mut(document).and_then(|network| network.nested_network_mut(&self.nested_path))
|
||||||
|
|
||||||
for segement in &self.nested_path {
|
|
||||||
network = network.and_then(|network| network.nodes.get_mut(segement)).and_then(|node| node.implementation.get_network_mut());
|
|
||||||
}
|
|
||||||
network
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send the cached layout for the bar at the top of the node panel to the frontend
|
/// Send the cached layout for the bar at the top of the node panel to the frontend
|
||||||
@@ -239,8 +222,8 @@ impl NodeGraphMessageHandler {
|
|||||||
|
|
||||||
pub fn collate_properties(&self, node_graph_frame: &NodeGraphFrameLayer, context: &mut NodePropertiesContext, sections: &mut Vec<LayoutGroup>) {
|
pub fn collate_properties(&self, node_graph_frame: &NodeGraphFrameLayer, context: &mut NodePropertiesContext, sections: &mut Vec<LayoutGroup>) {
|
||||||
let mut network = &node_graph_frame.network;
|
let mut network = &node_graph_frame.network;
|
||||||
for segement in &self.nested_path {
|
for segment in &self.nested_path {
|
||||||
network = network.nodes.get(segement).and_then(|node| node.implementation.get_network()).unwrap();
|
network = network.nodes.get(segment).and_then(|node| node.implementation.get_network()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If empty, show all nodes in the network starting with the output
|
// If empty, show all nodes in the network starting with the output
|
||||||
@@ -293,7 +276,7 @@ impl NodeGraphMessageHandler {
|
|||||||
for (id, node) in &network.nodes {
|
for (id, node) in &network.nodes {
|
||||||
let Some(node_type) = document_node_types::resolve_document_node_type(&node.name) else {
|
let Some(node_type) = document_node_types::resolve_document_node_type(&node.name) else {
|
||||||
warn!("Node '{}' does not exist in library", node.name);
|
warn!("Node '{}' does not exist in library", node.name);
|
||||||
continue
|
continue;
|
||||||
};
|
};
|
||||||
nodes.push(FrontendNode {
|
nodes.push(FrontendNode {
|
||||||
id: *id,
|
id: *id,
|
||||||
@@ -442,7 +425,8 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
|
|||||||
let input = NodeInput::Node(output_node);
|
let input = NodeInput::Node(output_node);
|
||||||
responses.push_back(NodeGraphMessage::SetNodeInput { node_id, input_index, input }.into());
|
responses.push_back(NodeGraphMessage::SetNodeInput { node_id, input_index, input }.into());
|
||||||
|
|
||||||
responses.push_back(NodeGraphMessage::SendGraph.into());
|
let should_rerender = network.connected_to_output(node_id);
|
||||||
|
responses.push_back(NodeGraphMessage::SendGraph { should_rerender }.into());
|
||||||
}
|
}
|
||||||
NodeGraphMessage::Copy => {
|
NodeGraphMessage::Copy => {
|
||||||
let Some(network) = self.get_active_network(document) else {
|
let Some(network) = self.get_active_network(document) else {
|
||||||
@@ -468,38 +452,17 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let num_inputs = document_node_type.inputs.len();
|
|
||||||
|
|
||||||
let inner_network = NodeNetwork {
|
|
||||||
inputs: (0..num_inputs).map(|_| 0).collect(),
|
|
||||||
output: 0,
|
|
||||||
nodes: [(
|
|
||||||
0,
|
|
||||||
DocumentNode {
|
|
||||||
name: format!("{}_impl", document_node_type.name),
|
|
||||||
// TODO: Allow inserting nodes that contain other nodes.
|
|
||||||
implementation: DocumentNodeImplementation::Unresolved(document_node_type.identifier.clone()),
|
|
||||||
inputs: (0..num_inputs).map(|_| NodeInput::Network).collect(),
|
|
||||||
metadata: DocumentNodeMetadata::default(),
|
|
||||||
},
|
|
||||||
)]
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
|
|
||||||
let document_node = DocumentNode {
|
let document_node = DocumentNode {
|
||||||
name: node_type.clone(),
|
name: node_type.clone(),
|
||||||
inputs: document_node_type.inputs.iter().map(|input| input.default.clone()).collect(),
|
inputs: document_node_type.inputs.iter().map(|input| input.default.clone()).collect(),
|
||||||
// TODO: Allow inserting nodes that contain other nodes.
|
implementation: document_node_type.generate_implementation(),
|
||||||
implementation: DocumentNodeImplementation::Network(inner_network),
|
|
||||||
metadata: graph_craft::document::DocumentNodeMetadata { position: (x, y).into() },
|
metadata: graph_craft::document::DocumentNodeMetadata { position: (x, y).into() },
|
||||||
};
|
};
|
||||||
responses.push_back(NodeGraphMessage::InsertNode { node_id, document_node }.into());
|
responses.push_back(NodeGraphMessage::InsertNode { node_id, document_node }.into());
|
||||||
|
|
||||||
responses.push_back(NodeGraphMessage::SendGraph.into());
|
responses.push_back(NodeGraphMessage::SendGraph { should_rerender: false }.into());
|
||||||
}
|
}
|
||||||
NodeGraphMessage::Cut => {
|
NodeGraphMessage::Cut => {
|
||||||
responses.push_back(NodeGraphMessage::Copy.into());
|
responses.push_back(NodeGraphMessage::Copy.into());
|
||||||
@@ -518,8 +481,14 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
|
|||||||
responses.push_back(NodeGraphMessage::DeleteNode { node_id }.into());
|
responses.push_back(NodeGraphMessage::DeleteNode { node_id }.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
responses.push_back(NodeGraphMessage::SendGraph.into());
|
responses.push_back(NodeGraphMessage::SendGraph { should_rerender: false }.into());
|
||||||
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
|
|
||||||
|
if let Some(network) = self.get_active_network(document) {
|
||||||
|
// Only generate node graph if one of the selected nodes is connected to the output
|
||||||
|
if self.selected_nodes.iter().any(|&node_id| network.connected_to_output(node_id)) {
|
||||||
|
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NodeGraphMessage::DisconnectNodes { node_id, input_index } => {
|
NodeGraphMessage::DisconnectNodes { node_id, input_index } => {
|
||||||
let Some(network) = self.get_active_network(document) else {
|
let Some(network) = self.get_active_network(document) else {
|
||||||
@@ -540,7 +509,8 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
|
|||||||
let input = node_type.inputs[input_index].default.clone();
|
let input = node_type.inputs[input_index].default.clone();
|
||||||
responses.push_back(NodeGraphMessage::SetNodeInput { node_id, input_index, input }.into());
|
responses.push_back(NodeGraphMessage::SetNodeInput { node_id, input_index, input }.into());
|
||||||
|
|
||||||
responses.push_back(NodeGraphMessage::SendGraph.into());
|
let should_rerender = network.connected_to_output(node_id);
|
||||||
|
responses.push_back(NodeGraphMessage::SendGraph { should_rerender }.into());
|
||||||
}
|
}
|
||||||
NodeGraphMessage::DoubleClickNode { node } => {
|
NodeGraphMessage::DoubleClickNode { node } => {
|
||||||
if let Some(network) = self.get_active_network(document) {
|
if let Some(network) = self.get_active_network(document) {
|
||||||
@@ -615,7 +585,8 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
|
|||||||
}
|
}
|
||||||
responses.push_back(NodeGraphMessage::SetNodeInput { node_id, input_index, input }.into());
|
responses.push_back(NodeGraphMessage::SetNodeInput { node_id, input_index, input }.into());
|
||||||
|
|
||||||
responses.push_back(NodeGraphMessage::SendGraph.into());
|
let should_rerender = network.connected_to_output(node_id);
|
||||||
|
responses.push_back(NodeGraphMessage::SendGraph { should_rerender }.into());
|
||||||
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into());
|
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into());
|
||||||
}
|
}
|
||||||
NodeGraphMessage::InsertNode { node_id, document_node } => {
|
NodeGraphMessage::InsertNode { node_id, document_node } => {
|
||||||
@@ -697,7 +668,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
|
|||||||
let nodes = new_ids.values().copied().collect();
|
let nodes = new_ids.values().copied().collect();
|
||||||
responses.push_back(NodeGraphMessage::SelectNodes { nodes }.into());
|
responses.push_back(NodeGraphMessage::SelectNodes { nodes }.into());
|
||||||
|
|
||||||
responses.push_back(NodeGraphMessage::SendGraph.into());
|
responses.push_back(NodeGraphMessage::SendGraph { should_rerender: false }.into());
|
||||||
}
|
}
|
||||||
NodeGraphMessage::SelectNodes { nodes } => {
|
NodeGraphMessage::SelectNodes { nodes } => {
|
||||||
self.selected_nodes = nodes;
|
self.selected_nodes = nodes;
|
||||||
@@ -705,10 +676,12 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
|
|||||||
self.update_selected(document, responses);
|
self.update_selected(document, responses);
|
||||||
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into());
|
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into());
|
||||||
}
|
}
|
||||||
NodeGraphMessage::SendGraph => {
|
NodeGraphMessage::SendGraph { should_rerender } => {
|
||||||
if let Some(network) = self.get_active_network(document) {
|
if let Some(network) = self.get_active_network(document) {
|
||||||
Self::send_graph(network, responses);
|
Self::send_graph(network, responses);
|
||||||
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
|
if should_rerender {
|
||||||
|
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NodeGraphMessage::SetDrawing { new_drawing } => {
|
NodeGraphMessage::SetDrawing { new_drawing } => {
|
||||||
@@ -738,7 +711,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
|
|||||||
let input = NodeInput::Value { tagged_value: value, exposed: false };
|
let input = NodeInput::Value { tagged_value: value, exposed: false };
|
||||||
responses.push_back(NodeGraphMessage::SetNodeInput { node_id, input_index, input }.into());
|
responses.push_back(NodeGraphMessage::SetNodeInput { node_id, input_index, input }.into());
|
||||||
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into());
|
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into());
|
||||||
if node.name != "Imaginate" || input_index == 0 {
|
if (node.name != "Imaginate" || input_index == 0) && network.connected_to_output(node_id) {
|
||||||
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
|
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -757,18 +730,16 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
|
|||||||
input_index,
|
input_index,
|
||||||
value,
|
value,
|
||||||
} => {
|
} => {
|
||||||
let mut network = document.layer_mut(&layer_path).ok().and_then(|layer| match &mut layer.data {
|
|
||||||
LayerDataType::NodeGraphFrame(n) => Some(&mut n.network),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
|
|
||||||
let Some((node_id, node_path)) = node_path.split_last() else {
|
let Some((node_id, node_path)) = node_path.split_last() else {
|
||||||
error!("Node path is empty");
|
error!("Node path is empty");
|
||||||
return
|
return;
|
||||||
};
|
};
|
||||||
for segement in node_path {
|
|
||||||
network = network.and_then(|network| network.nodes.get_mut(segement)).and_then(|node| node.implementation.get_network_mut());
|
let network = document
|
||||||
}
|
.layer_mut(&layer_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|layer| layer.as_node_graph_mut().ok())
|
||||||
|
.and_then(|network| network.nested_network_mut(node_path));
|
||||||
|
|
||||||
if let Some(network) = network {
|
if let Some(network) = network {
|
||||||
if let Some(node) = network.nodes.get_mut(node_id) {
|
if let Some(node) = network.nodes.get_mut(node_id) {
|
||||||
@@ -777,7 +748,9 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
|
|||||||
node.inputs.extend(((node.inputs.len() - 1)..input_index).map(|_| NodeInput::Network));
|
node.inputs.extend(((node.inputs.len() - 1)..input_index).map(|_| NodeInput::Network));
|
||||||
}
|
}
|
||||||
node.inputs[input_index] = NodeInput::Value { tagged_value: value, exposed: false };
|
node.inputs[input_index] = NodeInput::Value { tagged_value: value, exposed: false };
|
||||||
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
|
if network.connected_to_output(*node_id) {
|
||||||
|
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -826,7 +799,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
|
|||||||
stack.extend(outwards_links.get(&id).unwrap_or(&Vec::new()).iter().copied())
|
stack.extend(outwards_links.get(&id).unwrap_or(&Vec::new()).iter().copied())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
responses.push_back(NodeGraphMessage::SendGraph.into());
|
responses.push_back(NodeGraphMessage::SendGraph { should_rerender: false }.into());
|
||||||
}
|
}
|
||||||
NodeGraphMessage::ToggleHidden => {
|
NodeGraphMessage::ToggleHidden => {
|
||||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
@@ -844,9 +817,13 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
|
|||||||
network.disabled.extend(self.selected_nodes.iter().filter(|&id| !network.inputs.contains(id) && original_output != *id));
|
network.disabled.extend(self.selected_nodes.iter().filter(|&id| !network.inputs.contains(id) && original_output != *id));
|
||||||
}
|
}
|
||||||
Self::send_graph(network, responses);
|
Self::send_graph(network, responses);
|
||||||
|
|
||||||
|
// Only generate node graph if one of the selected nodes is connected to the output
|
||||||
|
if self.selected_nodes.iter().any(|&node_id| network.connected_to_output(node_id)) {
|
||||||
|
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.update_selection_action_buttons(document, responses);
|
self.update_selection_action_buttons(document, responses);
|
||||||
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
|
|
||||||
}
|
}
|
||||||
NodeGraphMessage::TogglePreview { node_id } => {
|
NodeGraphMessage::TogglePreview { node_id } => {
|
||||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
@@ -860,6 +837,8 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
|
|||||||
network.output = node_id;
|
network.output = node_id;
|
||||||
} else if let Some(output) = network.previous_output.take() {
|
} else if let Some(output) = network.previous_output.take() {
|
||||||
network.output = output
|
network.output = output
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
Self::send_graph(network, responses);
|
Self::send_graph(network, responses);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::messages::layout::utility_types::layout_widget::LayoutGroup;
|
|||||||
|
|
||||||
use graph_craft::concrete;
|
use graph_craft::concrete;
|
||||||
use graph_craft::document::value::*;
|
use graph_craft::document::value::*;
|
||||||
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
|
use graph_craft::document::*;
|
||||||
use graph_craft::imaginate_input::ImaginateSamplingMethod;
|
use graph_craft::imaginate_input::ImaginateSamplingMethod;
|
||||||
use graph_craft::proto::{NodeIdentifier, Type};
|
use graph_craft::proto::{NodeIdentifier, Type};
|
||||||
use graphene_core::raster::Image;
|
use graphene_core::raster::Image;
|
||||||
@@ -61,11 +61,19 @@ static DOCUMENT_NODE_TYPES: &[DocumentNodeType] = &[
|
|||||||
default: NodeInput::Node(0),
|
default: NodeInput::Node(0),
|
||||||
}],
|
}],
|
||||||
outputs: &[FrontendGraphDataType::General],
|
outputs: &[FrontendGraphDataType::General],
|
||||||
properties: |_document_node, _node_id, _context| node_properties::string_properties("The identity node simply returns the input".to_string()),
|
properties: |_document_node, _node_id, _context| node_properties::string_properties("The identity node simply returns the input"),
|
||||||
|
},
|
||||||
|
DocumentNodeType {
|
||||||
|
name: "Image",
|
||||||
|
category: "Ignore",
|
||||||
|
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[concrete!("Any<'_>")]),
|
||||||
|
inputs: &[DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), false)],
|
||||||
|
outputs: &[FrontendGraphDataType::Raster],
|
||||||
|
properties: |_document_node, _node_id, _context| node_properties::string_properties("A bitmap image embedded in this node"),
|
||||||
},
|
},
|
||||||
DocumentNodeType {
|
DocumentNodeType {
|
||||||
name: "Input",
|
name: "Input",
|
||||||
category: "Meta",
|
category: "Ignore",
|
||||||
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[concrete!("Any<'_>")]),
|
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[concrete!("Any<'_>")]),
|
||||||
inputs: &[DocumentInputType {
|
inputs: &[DocumentInputType {
|
||||||
name: "In",
|
name: "In",
|
||||||
@@ -77,7 +85,7 @@ static DOCUMENT_NODE_TYPES: &[DocumentNodeType] = &[
|
|||||||
},
|
},
|
||||||
DocumentNodeType {
|
DocumentNodeType {
|
||||||
name: "Output",
|
name: "Output",
|
||||||
category: "Meta",
|
category: "Ignore",
|
||||||
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[concrete!("Any<'_>")]),
|
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[concrete!("Any<'_>")]),
|
||||||
inputs: &[DocumentInputType {
|
inputs: &[DocumentInputType {
|
||||||
name: "In",
|
name: "In",
|
||||||
@@ -337,7 +345,32 @@ pub fn resolve_document_node_type(name: &str) -> Option<&DocumentNodeType> {
|
|||||||
pub fn collect_node_types() -> Vec<FrontendNodeType> {
|
pub fn collect_node_types() -> Vec<FrontendNodeType> {
|
||||||
DOCUMENT_NODE_TYPES
|
DOCUMENT_NODE_TYPES
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|node_type| !matches!(node_type.name, "Input" | "Output"))
|
.filter(|node_type| !node_type.category.eq_ignore_ascii_case("ignore"))
|
||||||
.map(|node_type| FrontendNodeType::new(node_type.name, node_type.category))
|
.map(|node_type| FrontendNodeType::new(node_type.name, node_type.category))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DocumentNodeType {
|
||||||
|
/// Generate a [`DocumentNodeImplementation`] from this node type, using a nested network.
|
||||||
|
pub fn generate_implementation(&self) -> DocumentNodeImplementation {
|
||||||
|
let number_of_inputs = self.inputs.len();
|
||||||
|
let network = NodeNetwork {
|
||||||
|
inputs: (0..number_of_inputs).map(|_| 0).collect(),
|
||||||
|
output: 0,
|
||||||
|
nodes: [(
|
||||||
|
0,
|
||||||
|
DocumentNode {
|
||||||
|
name: format!("{}_impl", self.name),
|
||||||
|
// TODO: Allow inserting nodes that contain other nodes.
|
||||||
|
implementation: DocumentNodeImplementation::Unresolved(self.identifier.clone()),
|
||||||
|
inputs: (0..number_of_inputs).map(|_| NodeInput::Network).collect(),
|
||||||
|
metadata: DocumentNodeMetadata::default(),
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
DocumentNodeImplementation::Network(network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -633,7 +633,7 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte
|
|||||||
LayoutGroup::Row { widgets }.with_tooltip(
|
LayoutGroup::Row { widgets }.with_tooltip(
|
||||||
"Amplification of the text prompt's influence over the outcome. At 0, the prompt is entirely ignored.\n\
|
"Amplification of the text prompt's influence over the outcome. At 0, the prompt is entirely ignored.\n\
|
||||||
\n\
|
\n\
|
||||||
Lower values are more creative and exploratory. Higher values are more literal and uninspired, but may be lower quality.\n\
|
Lower values are more creative and exploratory. Higher values are more literal and uninspired.\n\
|
||||||
\n\
|
\n\
|
||||||
This parameter is otherwise known as CFG (classifier-free guidance).",
|
This parameter is otherwise known as CFG (classifier-free guidance).",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -276,14 +276,13 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
|||||||
} => {
|
} => {
|
||||||
let get = |name: &str| IMAGINATE_NODE.inputs.iter().position(|input| input.name == name).unwrap_or_else(|| panic!("Input {name} not found"));
|
let get = |name: &str| IMAGINATE_NODE.inputs.iter().position(|input| input.name == name).unwrap_or_else(|| panic!("Input {name} not found"));
|
||||||
|
|
||||||
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 = Image::from_image_data(&image_data, width, height);
|
||||||
let image = Image { width, height, data };
|
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
PortfolioMessage::DocumentPassMessage {
|
PortfolioMessage::DocumentPassMessage {
|
||||||
document_id,
|
document_id,
|
||||||
message: NodeGraphMessage::SetQualifiedInputValue {
|
message: NodeGraphMessage::SetQualifiedInputValue {
|
||||||
layer_path: layer_path.clone(),
|
layer_path,
|
||||||
node_path: node_path.clone(),
|
node_path,
|
||||||
input_index: get("Cached Data"),
|
input_index: get("Cached Data"),
|
||||||
value: TaggedValue::RcImage(Some(std::sync::Arc::new(image))),
|
value: TaggedValue::RcImage(Some(std::sync::Arc::new(image))),
|
||||||
}
|
}
|
||||||
@@ -769,15 +768,13 @@ impl PortfolioMessageHandler {
|
|||||||
&mut self,
|
&mut self,
|
||||||
document_id: u64,
|
document_id: u64,
|
||||||
layer_path: Vec<LayerId>,
|
layer_path: Vec<LayerId>,
|
||||||
(image_data, size): (Vec<u8>, (u32, u32)),
|
(image_data, (width, height)): (Vec<u8>, (u32, u32)),
|
||||||
imaginate_node: Option<Vec<NodeId>>,
|
imaginate_node: Option<Vec<NodeId>>,
|
||||||
preferences: &PreferencesMessageHandler,
|
preferences: &PreferencesMessageHandler,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Reformat the input image data into an f32 image
|
// Reformat the input image data into an f32 image
|
||||||
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_core::raster::Image::from_image_data(&image_data, width, height);
|
||||||
let (width, height) = size;
|
|
||||||
let image = graphene_core::raster::Image { width, height, data };
|
|
||||||
|
|
||||||
// Get the node graph layer
|
// Get the node graph layer
|
||||||
let document = self.documents.get_mut(&document_id).ok_or_else(|| "Invalid document".to_string())?;
|
let document = self.documents.get_mut(&document_id).ok_or_else(|| "Invalid document".to_string())?;
|
||||||
@@ -885,8 +882,7 @@ impl PortfolioMessageHandler {
|
|||||||
|
|
||||||
// If no image was generated, use the input image
|
// If no image was generated, use the input image
|
||||||
if image.width == 0 || image.height == 0 {
|
if image.width == 0 || image.height == 0 {
|
||||||
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();
|
image = graphene_core::raster::Image::from_image_data(&image_data, width, height);
|
||||||
image = graphene_core::raster::Image { width, height, data };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let (image_data, _size) = Self::encode_img(image, None, image::ImageOutputFormat::Bmp)?;
|
let (image_data, _size) = Self::encode_img(image, None, image::ImageOutputFormat::Bmp)?;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ impl OverlayRenderer {
|
|||||||
|
|
||||||
// Only view in and out handles if they are not on top of the anchor
|
// Only view in and out handles if they are not on top of the anchor
|
||||||
let [in_handle, out_handle] = {
|
let [in_handle, out_handle] = {
|
||||||
let Some(anchor) = manipulator_group.points[ManipulatorType::Anchor].as_ref() else{
|
let Some(anchor) = manipulator_group.points[ManipulatorType::Anchor].as_ref() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ impl SelectedEdges {
|
|||||||
|
|
||||||
let mut pivot = self.pivot_from_bounds(min, max);
|
let mut pivot = self.pivot_from_bounds(min, max);
|
||||||
if center {
|
if center {
|
||||||
// The below ratio is: `dragging edge / being centred`.
|
// The below ratio is: `dragging edge / being centered`.
|
||||||
// The `is_finite()` checks are in case the user is dragging the edge where the pivot is located (in which case the centering mode is ignored).
|
// The `is_finite()` checks are in case the user is dragging the edge where the pivot is located (in which case the centering mode is ignored).
|
||||||
if self.top {
|
if self.top {
|
||||||
let ratio = (center_around.y - min.y) / (center_around.y - self.bounds[0].y);
|
let ratio = (center_around.y - min.y) / (center_around.y - self.bounds[0].y);
|
||||||
|
|||||||
@@ -139,68 +139,24 @@ impl Fsm for ImaginateToolFsmState {
|
|||||||
shape_data.path = Some(document.get_path_for_new_layer());
|
shape_data.path = Some(document.get_path_for_new_layer());
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
|
|
||||||
use graph_craft::{document::*, generic, proto::*};
|
use graph_craft::document::*;
|
||||||
|
|
||||||
let imaginate_node_type = IMAGINATE_NODE;
|
let imaginate_node_type = IMAGINATE_NODE;
|
||||||
let num_inputs = imaginate_node_type.inputs.len();
|
|
||||||
|
|
||||||
let imaginate_inner_network = NodeNetwork {
|
|
||||||
inputs: (0..num_inputs).map(|_| 0).collect(),
|
|
||||||
output: 0,
|
|
||||||
nodes: [(
|
|
||||||
0,
|
|
||||||
DocumentNode {
|
|
||||||
name: format!("{}_impl", imaginate_node_type.name),
|
|
||||||
// TODO: Allow inserting nodes that contain other nodes.
|
|
||||||
implementation: DocumentNodeImplementation::Unresolved(imaginate_node_type.identifier.clone()),
|
|
||||||
inputs: (0..num_inputs).map(|_| NodeInput::Network).collect(),
|
|
||||||
metadata: DocumentNodeMetadata::default(),
|
|
||||||
},
|
|
||||||
)]
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let mut imaginate_inputs: Vec<NodeInput> = imaginate_node_type.inputs.iter().map(|input| input.default.clone()).collect();
|
let mut imaginate_inputs: Vec<NodeInput> = imaginate_node_type.inputs.iter().map(|input| input.default.clone()).collect();
|
||||||
imaginate_inputs[0] = NodeInput::Node(0);
|
imaginate_inputs[0] = NodeInput::Node(0);
|
||||||
|
|
||||||
let network = NodeNetwork {
|
let imaginate_node_id = 2;
|
||||||
inputs: vec![0],
|
let mut network = NodeNetwork::new_network(32, imaginate_node_id);
|
||||||
output: 1,
|
network.nodes.insert(
|
||||||
nodes: [
|
imaginate_node_id,
|
||||||
(
|
DocumentNode {
|
||||||
0,
|
name: imaginate_node_type.name.to_string(),
|
||||||
DocumentNode {
|
inputs: imaginate_inputs,
|
||||||
name: "Input".into(),
|
implementation: imaginate_node_type.generate_implementation(),
|
||||||
inputs: vec![NodeInput::Network],
|
metadata: graph_craft::document::DocumentNodeMetadata { position: (20, 4).into() },
|
||||||
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])),
|
},
|
||||||
metadata: DocumentNodeMetadata { position: (8, 4).into() },
|
);
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
1,
|
|
||||||
DocumentNode {
|
|
||||||
name: "Output".into(),
|
|
||||||
inputs: vec![NodeInput::Node(2)],
|
|
||||||
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])),
|
|
||||||
metadata: DocumentNodeMetadata { position: (32, 4).into() },
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
2,
|
|
||||||
DocumentNode {
|
|
||||||
name: imaginate_node_type.name.to_string(),
|
|
||||||
inputs: imaginate_inputs,
|
|
||||||
// TODO: Allow inserting nodes that contain other nodes.
|
|
||||||
implementation: DocumentNodeImplementation::Network(imaginate_inner_network),
|
|
||||||
metadata: graph_craft::document::DocumentNodeMetadata { position: (20, 4).into() },
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
Operation::AddNodeGraphFrame {
|
Operation::AddNodeGraphFrame {
|
||||||
|
|||||||
@@ -138,35 +138,7 @@ impl Fsm for NodeGraphToolFsmState {
|
|||||||
shape_data.path = Some(document.get_path_for_new_layer());
|
shape_data.path = Some(document.get_path_for_new_layer());
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
|
|
||||||
use graph_craft::{document::*, generic, proto::*};
|
let network = graph_craft::document::NodeNetwork::new_network(20, 0);
|
||||||
|
|
||||||
let network = NodeNetwork {
|
|
||||||
inputs: vec![0],
|
|
||||||
output: 1,
|
|
||||||
nodes: [
|
|
||||||
(
|
|
||||||
0,
|
|
||||||
DocumentNode {
|
|
||||||
name: "Input".into(),
|
|
||||||
inputs: vec![NodeInput::Network],
|
|
||||||
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])),
|
|
||||||
metadata: DocumentNodeMetadata { position: (8, 4).into() },
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
1,
|
|
||||||
DocumentNode {
|
|
||||||
name: "Output".into(),
|
|
||||||
inputs: vec![NodeInput::Node(0)],
|
|
||||||
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])),
|
|
||||||
metadata: DocumentNodeMetadata { position: (20, 4).into() },
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
Operation::AddNodeGraphFrame {
|
Operation::AddNodeGraphFrame {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { getContext, onMount, tick } from "svelte";
|
import { getContext, onMount, tick } from "svelte";
|
||||||
|
|
||||||
import { textInputCleanup } from "@/utility-functions/keyboard-entry";
|
import { textInputCleanup } from "@/utility-functions/keyboard-entry";
|
||||||
import { rasterizeSVGCanvas } from "@/utility-functions/rasterization";
|
import { extractPixelData, rasterizeSVGCanvas } from "@/utility-functions/rasterization";
|
||||||
import {
|
import {
|
||||||
type MouseCursorIcon,
|
type MouseCursorIcon,
|
||||||
type XY,
|
type XY,
|
||||||
@@ -82,10 +82,9 @@
|
|||||||
Array.from(dataTransfer.items).forEach(async (item) => {
|
Array.from(dataTransfer.items).forEach(async (item) => {
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (file?.type.startsWith("image")) {
|
if (file?.type.startsWith("image")) {
|
||||||
const buffer = await file.arrayBuffer();
|
const imageData = await extractPixelData(file);
|
||||||
const u8Array = new Uint8Array(buffer);
|
|
||||||
|
|
||||||
editor.instance.pasteImage(file.type, u8Array, e.clientX, e.clientY);
|
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height, e.clientX, e.clientY);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { type FullscreenState } from "@/state-providers/fullscreen";
|
|||||||
import { type PortfolioState } from "@/state-providers/portfolio";
|
import { type PortfolioState } from "@/state-providers/portfolio";
|
||||||
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@/utility-functions/keyboard-entry";
|
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@/utility-functions/keyboard-entry";
|
||||||
import { platformIsMac } from "@/utility-functions/platform";
|
import { platformIsMac } from "@/utility-functions/platform";
|
||||||
|
import { extractPixelData } from "@/utility-functions/rasterization";
|
||||||
import { stripIndents } from "@/utility-functions/strip-indents";
|
import { stripIndents } from "@/utility-functions/strip-indents";
|
||||||
import { type Editor } from "@/wasm-communication/editor";
|
import { type Editor } from "@/wasm-communication/editor";
|
||||||
import { TriggerPaste } from "@/wasm-communication/messages";
|
import { TriggerPaste } from "@/wasm-communication/messages";
|
||||||
@@ -271,10 +272,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, document
|
|||||||
|
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (file?.type.startsWith("image")) {
|
if (file?.type.startsWith("image")) {
|
||||||
file.arrayBuffer().then((buffer): void => {
|
extractPixelData(file).then((imageData): void => {
|
||||||
const u8Array = new Uint8Array(buffer);
|
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
|
||||||
|
|
||||||
editor.instance.pasteImage(file.type, u8Array);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -318,10 +317,11 @@ export function createInputManager(editor: Editor, dialog: DialogState, document
|
|||||||
if (imageType) {
|
if (imageType) {
|
||||||
const blob = await item.getType(imageType);
|
const blob = await item.getType(imageType);
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (): void => {
|
reader.onload = async (): Promise<void> => {
|
||||||
const u8Array = new Uint8Array(reader.result as ArrayBuffer);
|
if (reader.result instanceof ArrayBuffer) {
|
||||||
|
const imageData = await extractPixelData(new Blob([reader.result], { type: imageType }));
|
||||||
editor.instance.pasteImage(imageType, u8Array);
|
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
reader.readAsArrayBuffer(blob);
|
reader.readAsArrayBuffer(blob);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {writable} from "svelte/store";
|
|||||||
|
|
||||||
import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files";
|
import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files";
|
||||||
import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate, updateBackendImage } from "@/utility-functions/imaginate";
|
import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate, updateBackendImage } from "@/utility-functions/imaginate";
|
||||||
import { rasterizeSVG, rasterizeSVGCanvas } from "@/utility-functions/rasterization";
|
import { extractPixelData, rasterizeSVG, rasterizeSVGCanvas } from "@/utility-functions/rasterization";
|
||||||
import { type Editor } from "@/wasm-communication/editor";
|
import { type Editor } from "@/wasm-communication/editor";
|
||||||
import {
|
import {
|
||||||
type FrontendDocumentDetails,
|
type FrontendDocumentDetails,
|
||||||
@@ -52,7 +52,8 @@ export function createPortfolioState(editor: Editor) {
|
|||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
|
editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
|
||||||
const data = await upload("image/*", "data");
|
const data = await upload("image/*", "data");
|
||||||
editor.instance.pasteImage(data.type, Uint8Array.from(data.content));
|
const imageData = await extractPixelData(new Blob([data.content], { type: data.type }));
|
||||||
|
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
|
||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerFileDownload, (triggerFileDownload) => {
|
editor.subscriptions.subscribeJsMessage(TriggerFileDownload, (triggerFileDownload) => {
|
||||||
downloadFileText(triggerFileDownload.name, triggerFileDownload.document);
|
downloadFileText(triggerFileDownload.name, triggerFileDownload.document);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { replaceBlobURLsWithBase64 } from "@/utility-functions/files";
|
import { replaceBlobURLsWithBase64 } from "@/utility-functions/files";
|
||||||
|
|
||||||
// Rasterize the string of an SVG document at a given width and height and turn it into the blob data of an image file matching the given MIME type
|
// Rasterize the string of an SVG document at a given width and height and return the canvas it was drawn onto during the rasterization process
|
||||||
export async function rasterizeSVGCanvas(svg: string, width: number, height: number, backgroundColor?: string): Promise<HTMLCanvasElement> {
|
export async function rasterizeSVGCanvas(svg: string, width: number, height: number, backgroundColor?: string): Promise<HTMLCanvasElement> {
|
||||||
// A canvas to render our SVG to in order to get a raster image
|
// A canvas to render our SVG to in order to get a raster image
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
@@ -22,6 +22,7 @@ export async function rasterizeSVGCanvas(svg: string, width: number, height: num
|
|||||||
const svgBlob = new Blob([svgWithBase64Images], { type: "image/svg+xml;charset=utf-8" });
|
const svgBlob = new Blob([svgWithBase64Images], { type: "image/svg+xml;charset=utf-8" });
|
||||||
const url = URL.createObjectURL(svgBlob);
|
const url = URL.createObjectURL(svgBlob);
|
||||||
|
|
||||||
|
// Load the Image from the URL and wait until it's done
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
image.src = url;
|
image.src = url;
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
@@ -37,6 +38,7 @@ export async function rasterizeSVGCanvas(svg: string, width: number, height: num
|
|||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rasterize the string of an SVG document at a given width and height and turn it into the blob data of an image file matching the given MIME type
|
||||||
export async function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise<Blob> {
|
export async function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise<Blob> {
|
||||||
const canvas = await rasterizeSVGCanvas(svg, width, height, backgroundColor);
|
const canvas = await rasterizeSVGCanvas(svg, width, height, backgroundColor);
|
||||||
|
|
||||||
@@ -51,3 +53,49 @@ export async function rasterizeSVG(svg: string, width: number, height: number, m
|
|||||||
|
|
||||||
return blob;
|
return blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert an image source (e.g. PNG document) into pixel data, a width and a height
|
||||||
|
export async function extractPixelData(imageData: ImageBitmapSource): Promise<ImageData> {
|
||||||
|
// Special handling to rasterize an SVG file
|
||||||
|
let svgImageData;
|
||||||
|
if (imageData instanceof File && imageData.type === "image/svg+xml") {
|
||||||
|
const svgSource = await imageData.text();
|
||||||
|
const svgElement = new DOMParser().parseFromString(svgSource, "image/svg+xml").querySelector("svg");
|
||||||
|
if (!svgElement) throw new Error("Error reading SVG file");
|
||||||
|
|
||||||
|
let bounds = svgElement.viewBox.baseVal;
|
||||||
|
|
||||||
|
// If the bounds are zero (which will happen if the `viewBox` is not provided), set bounds to the artwork's bounding box
|
||||||
|
if (bounds.width === 0 || bounds.height === 0) {
|
||||||
|
// It's necessary to measure while the element is in the DOM, otherwise the dimensions are zero
|
||||||
|
const toRemove = document.body.insertAdjacentElement("beforeend", svgElement);
|
||||||
|
bounds = svgElement.getBBox();
|
||||||
|
toRemove?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
svgImageData = await rasterizeSVGCanvas(svgSource, bounds.width, bounds.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the image file binary data
|
||||||
|
const image = await createImageBitmap(svgImageData || imageData);
|
||||||
|
|
||||||
|
// Halve the image size until the editor lag is somewhat usable
|
||||||
|
// TODO: Fix lag so this can be removed
|
||||||
|
const MAX_IMAGE_SIZE = 512;
|
||||||
|
let { width, height } = image;
|
||||||
|
while (width > MAX_IMAGE_SIZE || height > MAX_IMAGE_SIZE) {
|
||||||
|
width /= 2;
|
||||||
|
height /= 2;
|
||||||
|
}
|
||||||
|
width = Math.floor(width);
|
||||||
|
height = Math.floor(height);
|
||||||
|
|
||||||
|
// Render image to canvas
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) throw new Error("Could not create canvas context");
|
||||||
|
context.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
|
||||||
|
return context.getImageData(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ editor = { path = "../../editor", package = "graphite-editor" }
|
|||||||
document-legacy = { path = "../../document-legacy", package = "graphite-document-legacy" }
|
document-legacy = { path = "../../document-legacy", package = "graphite-document-legacy" }
|
||||||
graph-craft = { path = "../../node-graph/graph-craft" }
|
graph-craft = { path = "../../node-graph/graph-craft" }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
graphene-core = { path = "../../node-graph/gcore", features = ["async", "std", "alloc"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
wasm-bindgen = { version = "0.2.73" }
|
wasm-bindgen = { version = "0.2.73" }
|
||||||
serde-wasm-bindgen = "0.4.1"
|
serde-wasm-bindgen = "0.4.1"
|
||||||
|
|||||||
@@ -660,9 +660,10 @@ impl JsEditorHandle {
|
|||||||
|
|
||||||
/// Pastes an image
|
/// Pastes an image
|
||||||
#[wasm_bindgen(js_name = pasteImage)]
|
#[wasm_bindgen(js_name = pasteImage)]
|
||||||
pub fn paste_image(&self, mime: String, image_data: Vec<u8>, mouse_x: Option<f64>, mouse_y: Option<f64>) {
|
pub fn paste_image(&self, image_data: Vec<u8>, width: u32, height: u32, mouse_x: Option<f64>, mouse_y: Option<f64>) {
|
||||||
let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y)));
|
let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y)));
|
||||||
let message = DocumentMessage::PasteImage { mime, image_data, mouse };
|
let image = graphene_core::raster::Image::from_image_data(&image_data, width, height);
|
||||||
|
let message = DocumentMessage::PasteImage { image, mouse };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -228,7 +228,7 @@
|
|||||||
import { defineComponent, nextTick } from "vue";
|
import { defineComponent, nextTick } from "vue";
|
||||||
|
|
||||||
import { textInputCleanup } from "@/utility-functions/keyboard-entry";
|
import { textInputCleanup } from "@/utility-functions/keyboard-entry";
|
||||||
import { rasterizeSVGCanvas } from "@/utility-functions/rasterization";
|
import { extractPixelData, rasterizeSVGCanvas } from "@/utility-functions/rasterization";
|
||||||
import { type DisplayEditableTextbox, type MouseCursorIcon, type XY } from "@/wasm-communication/messages";
|
import { type DisplayEditableTextbox, type MouseCursorIcon, type XY } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@/components/floating-menus/EyedropperPreview.vue";
|
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@/components/floating-menus/EyedropperPreview.vue";
|
||||||
@@ -300,10 +300,9 @@ export default defineComponent({
|
|||||||
Array.from(dataTransfer.items).forEach(async (item) => {
|
Array.from(dataTransfer.items).forEach(async (item) => {
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (file?.type.startsWith("image")) {
|
if (file?.type.startsWith("image")) {
|
||||||
const buffer = await file.arrayBuffer();
|
const imageData = await extractPixelData(file);
|
||||||
const u8Array = new Uint8Array(buffer);
|
|
||||||
|
|
||||||
this.editor.instance.pasteImage(file.type, u8Array, e.clientX, e.clientY);
|
this.editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height, e.clientX, e.clientY);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { type FullscreenState } from "@/state-providers/fullscreen";
|
|||||||
import { type PortfolioState } from "@/state-providers/portfolio";
|
import { type PortfolioState } from "@/state-providers/portfolio";
|
||||||
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@/utility-functions/keyboard-entry";
|
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@/utility-functions/keyboard-entry";
|
||||||
import { platformIsMac } from "@/utility-functions/platform";
|
import { platformIsMac } from "@/utility-functions/platform";
|
||||||
|
import { extractPixelData } from "@/utility-functions/rasterization";
|
||||||
import { stripIndents } from "@/utility-functions/strip-indents";
|
import { stripIndents } from "@/utility-functions/strip-indents";
|
||||||
import { type Editor } from "@/wasm-communication/editor";
|
import { type Editor } from "@/wasm-communication/editor";
|
||||||
import { TriggerPaste } from "@/wasm-communication/messages";
|
import { TriggerPaste } from "@/wasm-communication/messages";
|
||||||
@@ -270,10 +271,8 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
|
|||||||
|
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (file?.type.startsWith("image")) {
|
if (file?.type.startsWith("image")) {
|
||||||
file.arrayBuffer().then((buffer): void => {
|
extractPixelData(file).then((imageData): void => {
|
||||||
const u8Array = new Uint8Array(buffer);
|
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
|
||||||
|
|
||||||
editor.instance.pasteImage(file.type, u8Array);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -317,10 +316,11 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
|
|||||||
if (imageType) {
|
if (imageType) {
|
||||||
const blob = await item.getType(imageType);
|
const blob = await item.getType(imageType);
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (): void => {
|
reader.onload = async (): Promise<void> => {
|
||||||
const u8Array = new Uint8Array(reader.result as ArrayBuffer);
|
if (reader.result instanceof ArrayBuffer) {
|
||||||
|
const imageData = await extractPixelData(new Blob([reader.result], { type: imageType }));
|
||||||
editor.instance.pasteImage(imageType, u8Array);
|
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
reader.readAsArrayBuffer(blob);
|
reader.readAsArrayBuffer(blob);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { reactive, readonly } from "vue";
|
|||||||
|
|
||||||
import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files";
|
import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files";
|
||||||
import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate, updateBackendImage } from "@/utility-functions/imaginate";
|
import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate, updateBackendImage } from "@/utility-functions/imaginate";
|
||||||
import { rasterizeSVG, rasterizeSVGCanvas } from "@/utility-functions/rasterization";
|
import { extractPixelData, rasterizeSVG, rasterizeSVGCanvas } from "@/utility-functions/rasterization";
|
||||||
import { type Editor } from "@/wasm-communication/editor";
|
import { type Editor } from "@/wasm-communication/editor";
|
||||||
import {
|
import {
|
||||||
type FrontendDocumentDetails,
|
type FrontendDocumentDetails,
|
||||||
@@ -45,7 +45,8 @@ export function createPortfolioState(editor: Editor) {
|
|||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
|
editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
|
||||||
const data = await upload("image/*", "data");
|
const data = await upload("image/*", "data");
|
||||||
editor.instance.pasteImage(data.type, Uint8Array.from(data.content));
|
const imageData = await extractPixelData(new Blob([data.content], { type: data.type }));
|
||||||
|
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
|
||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerFileDownload, (triggerFileDownload) => {
|
editor.subscriptions.subscribeJsMessage(TriggerFileDownload, (triggerFileDownload) => {
|
||||||
downloadFileText(triggerFileDownload.name, triggerFileDownload.document);
|
downloadFileText(triggerFileDownload.name, triggerFileDownload.document);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { replaceBlobURLsWithBase64 } from "@/utility-functions/files";
|
import { replaceBlobURLsWithBase64 } from "@/utility-functions/files";
|
||||||
|
|
||||||
// Rasterize the string of an SVG document at a given width and height and turn it into the blob data of an image file matching the given MIME type
|
// Rasterize the string of an SVG document at a given width and height and return the canvas it was drawn onto during the rasterization process
|
||||||
export async function rasterizeSVGCanvas(svg: string, width: number, height: number, backgroundColor?: string): Promise<HTMLCanvasElement> {
|
export async function rasterizeSVGCanvas(svg: string, width: number, height: number, backgroundColor?: string): Promise<HTMLCanvasElement> {
|
||||||
// A canvas to render our SVG to in order to get a raster image
|
// A canvas to render our SVG to in order to get a raster image
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
@@ -22,6 +22,7 @@ export async function rasterizeSVGCanvas(svg: string, width: number, height: num
|
|||||||
const svgBlob = new Blob([svgWithBase64Images], { type: "image/svg+xml;charset=utf-8" });
|
const svgBlob = new Blob([svgWithBase64Images], { type: "image/svg+xml;charset=utf-8" });
|
||||||
const url = URL.createObjectURL(svgBlob);
|
const url = URL.createObjectURL(svgBlob);
|
||||||
|
|
||||||
|
// Load the Image from the URL and wait until it's done
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
image.src = url;
|
image.src = url;
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
@@ -37,6 +38,7 @@ export async function rasterizeSVGCanvas(svg: string, width: number, height: num
|
|||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rasterize the string of an SVG document at a given width and height and turn it into the blob data of an image file matching the given MIME type
|
||||||
export async function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise<Blob> {
|
export async function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise<Blob> {
|
||||||
const canvas = await rasterizeSVGCanvas(svg, width, height, backgroundColor);
|
const canvas = await rasterizeSVGCanvas(svg, width, height, backgroundColor);
|
||||||
|
|
||||||
@@ -51,3 +53,49 @@ export async function rasterizeSVG(svg: string, width: number, height: number, m
|
|||||||
|
|
||||||
return blob;
|
return blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert an image source (e.g. PNG document) into pixel data, a width and a height
|
||||||
|
export async function extractPixelData(imageData: ImageBitmapSource): Promise<ImageData> {
|
||||||
|
// Special handling to rasterize an SVG file
|
||||||
|
let svgImageData;
|
||||||
|
if (imageData instanceof File && imageData.type === "image/svg+xml") {
|
||||||
|
const svgSource = await imageData.text();
|
||||||
|
const svgElement = new DOMParser().parseFromString(svgSource, "image/svg+xml").querySelector("svg");
|
||||||
|
if (!svgElement) throw new Error("Error reading SVG file");
|
||||||
|
|
||||||
|
let bounds = svgElement.viewBox.baseVal;
|
||||||
|
|
||||||
|
// If the bounds are zero (which will happen if the `viewBox` is not provided), set bounds to the artwork's bounding box
|
||||||
|
if (bounds.width === 0 || bounds.height === 0) {
|
||||||
|
// It's necessary to measure while the element is in the DOM, otherwise the dimensions are zero
|
||||||
|
const toRemove = document.body.insertAdjacentElement("beforeend", svgElement);
|
||||||
|
bounds = svgElement.getBBox();
|
||||||
|
toRemove?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
svgImageData = await rasterizeSVGCanvas(svgSource, bounds.width, bounds.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the image file binary data
|
||||||
|
const image = await createImageBitmap(svgImageData || imageData);
|
||||||
|
|
||||||
|
// Halve the image size until the editor lag is somewhat usable
|
||||||
|
// TODO: Fix lag so this can be removed
|
||||||
|
const MAX_IMAGE_SIZE = 512;
|
||||||
|
let { width, height } = image;
|
||||||
|
while (width > MAX_IMAGE_SIZE || height > MAX_IMAGE_SIZE) {
|
||||||
|
width /= 2;
|
||||||
|
height /= 2;
|
||||||
|
}
|
||||||
|
width = Math.floor(width);
|
||||||
|
height = Math.floor(height);
|
||||||
|
|
||||||
|
// Render image to canvas
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) throw new Error("Could not create canvas context");
|
||||||
|
context.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
|
||||||
|
return context.getImageData(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ crate-type = ["cdylib", "rlib"]
|
|||||||
editor = { path = "../../editor", package = "graphite-editor" }
|
editor = { path = "../../editor", package = "graphite-editor" }
|
||||||
document-legacy = { path = "../../document-legacy", package = "graphite-document-legacy" }
|
document-legacy = { path = "../../document-legacy", package = "graphite-document-legacy" }
|
||||||
graph-craft = { path = "../../node-graph/graph-craft" }
|
graph-craft = { path = "../../node-graph/graph-craft" }
|
||||||
|
graphene-core = { path = "../../node-graph/gcore", features = ["async", "std", "alloc"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
wasm-bindgen = { version = "0.2.73" }
|
wasm-bindgen = { version = "0.2.73" }
|
||||||
|
|||||||
@@ -660,9 +660,10 @@ impl JsEditorHandle {
|
|||||||
|
|
||||||
/// Pastes an image
|
/// Pastes an image
|
||||||
#[wasm_bindgen(js_name = pasteImage)]
|
#[wasm_bindgen(js_name = pasteImage)]
|
||||||
pub fn paste_image(&self, mime: String, image_data: Vec<u8>, mouse_x: Option<f64>, mouse_y: Option<f64>) {
|
pub fn paste_image(&self, image_data: Vec<u8>, width: u32, height: u32, mouse_x: Option<f64>, mouse_y: Option<f64>) {
|
||||||
let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y)));
|
let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y)));
|
||||||
let message = DocumentMessage::PasteImage { mime, image_data, mouse };
|
let image = graphene_core::raster::Image::from_image_data(&image_data, width, height);
|
||||||
|
let message = DocumentMessage::PasteImage { image, mouse };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -459,6 +459,11 @@ mod image {
|
|||||||
data: self.data.as_slice(),
|
data: self.data.as_slice(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// Generate Image from some frontend image data (the canvas pixels as u8s in a flat array)
|
||||||
|
pub fn from_image_data(image_data: &[u8], width: u32, height: u32) -> Self {
|
||||||
|
let data = image_data.chunks_exact(4).map(|v| Color::from_rgba8(v[0], v[1], v[2], v[3])).collect();
|
||||||
|
Image { width, height, data }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoIterator for Image {
|
impl IntoIterator for Image {
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ fn bilt_subpath(base_image: Image, path_data: Subpath) -> Image {
|
|||||||
let composition = Composition::new();
|
let composition = Composition::new();
|
||||||
let mut renderer = cpu::Renderer::new();
|
let mut renderer = cpu::Renderer::new();
|
||||||
let mut path_builder = PathBuilder::new();
|
let mut path_builder = PathBuilder::new();
|
||||||
for path_segement in path_data.bezier_iter() {
|
for path_segment in path_data.bezier_iter() {
|
||||||
let points = path_segement.internal.get_points().collect::<Vec<_>>();
|
let points = path_segment.internal.get_points().collect::<Vec<_>>();
|
||||||
match points.len() {
|
match points.len() {
|
||||||
2 => path_builder.line_to(points[1].into()),
|
2 => path_builder.line_to(points[1].into()),
|
||||||
3 => path_builder.quad_to(points[1].into(), points[2].into()),
|
3 => path_builder.quad_to(points[1].into(), points[2].into()),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ serde = ["dep:serde", "graphene-core/serde", "glam/serde"]
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
graphene-core = { path = "../gcore", features = ["async", "std", "alloc"] }
|
graphene-core = { path = "../gcore", features = ["alloc"] }
|
||||||
dyn-any = { path = "../../libraries/dyn-any", features = ["log-bad-types", "rc", "glam"] }
|
dyn-any = { path = "../../libraries/dyn-any", features = ["log-bad-types", "rc", "glam"] }
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
dyn-clone = "1.0"
|
dyn-clone = "1.0"
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
|
use crate::document::value::TaggedValue;
|
||||||
use crate::generic;
|
use crate::generic;
|
||||||
use crate::proto::{ConstructionArgs, NodeIdentifier, ProtoNetwork, ProtoNode, ProtoNodeInput, Type};
|
use crate::proto::{ConstructionArgs, NodeIdentifier, ProtoNetwork, ProtoNode, ProtoNodeInput, Type};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
pub mod value;
|
|
||||||
|
|
||||||
use dyn_any::{DynAny, StaticType};
|
use dyn_any::{DynAny, StaticType};
|
||||||
use glam::IVec2;
|
use glam::IVec2;
|
||||||
@@ -11,6 +8,10 @@ use rand_chacha::{
|
|||||||
rand_core::{RngCore, SeedableRng},
|
rand_core::{RngCore, SeedableRng},
|
||||||
ChaCha20Rng,
|
ChaCha20Rng,
|
||||||
};
|
};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
pub mod value;
|
||||||
|
|
||||||
pub type NodeId = u64;
|
pub type NodeId = u64;
|
||||||
static RNG: Mutex<Option<ChaCha20Rng>> = Mutex::new(None);
|
static RNG: Mutex<Option<ChaCha20Rng>> = Mutex::new(None);
|
||||||
@@ -226,7 +227,7 @@ impl NodeNetwork {
|
|||||||
self.flatten_with_fns(node, merge_ids, generate_uuid)
|
self.flatten_with_fns(node, merge_ids, generate_uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recursively dissolve non primitive document nodes and return a single flattened network of nodes.
|
/// Recursively dissolve non-primitive document nodes and return a single flattened network of nodes.
|
||||||
pub fn flatten_with_fns(&mut self, node: NodeId, map_ids: impl Fn(NodeId, NodeId) -> NodeId + Copy, gen_id: impl Fn() -> NodeId + Copy) {
|
pub fn flatten_with_fns(&mut self, node: NodeId, map_ids: impl Fn(NodeId, NodeId) -> NodeId + Copy, gen_id: impl Fn() -> NodeId + Copy) {
|
||||||
let (id, mut node) = self
|
let (id, mut node) = self
|
||||||
.nodes
|
.nodes
|
||||||
@@ -258,10 +259,18 @@ impl NodeNetwork {
|
|||||||
network_input.populate_first_network_input(node, *offset);
|
network_input.populate_first_network_input(node, *offset);
|
||||||
}
|
}
|
||||||
NodeInput::Value { tagged_value, exposed } => {
|
NodeInput::Value { tagged_value, exposed } => {
|
||||||
let name = format!("Value: {:?}", tagged_value.clone().to_value());
|
// Skip formatting very large values for seconds in performance speedup
|
||||||
|
let name = if matches!(
|
||||||
|
tagged_value,
|
||||||
|
TaggedValue::Image(_) | TaggedValue::RcImage(_) | TaggedValue::Color(_) | TaggedValue::Subpath(_) | TaggedValue::RcSubpath(_)
|
||||||
|
) {
|
||||||
|
"Value".to_string()
|
||||||
|
} else {
|
||||||
|
format!("Value: {:?}", tagged_value.clone().to_value())
|
||||||
|
};
|
||||||
let new_id = map_ids(id, gen_id());
|
let new_id = map_ids(id, gen_id());
|
||||||
let value_node = DocumentNode {
|
let value_node = DocumentNode {
|
||||||
name: name.clone(),
|
name,
|
||||||
inputs: vec![NodeInput::Value { tagged_value, exposed }],
|
inputs: vec![NodeInput::Value { tagged_value, exposed }],
|
||||||
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::value::ValueNode", &[generic!("T")])),
|
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::value::ValueNode", &[generic!("T")])),
|
||||||
metadata: DocumentNodeMetadata::default(),
|
metadata: DocumentNodeMetadata::default(),
|
||||||
@@ -305,6 +314,95 @@ impl NodeNetwork {
|
|||||||
pub fn original_output(&self) -> NodeId {
|
pub fn original_output(&self) -> NodeId {
|
||||||
self.previous_output.unwrap_or(self.output)
|
self.previous_output.unwrap_or(self.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A graph with just an input and output node
|
||||||
|
pub fn new_network(output_offset: i32, output_node_id: NodeId) -> Self {
|
||||||
|
Self {
|
||||||
|
inputs: vec![0],
|
||||||
|
output: 1,
|
||||||
|
nodes: [
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
DocumentNode {
|
||||||
|
name: "Input".into(),
|
||||||
|
inputs: vec![NodeInput::Network],
|
||||||
|
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])),
|
||||||
|
metadata: DocumentNodeMetadata { position: (8, 4).into() },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
DocumentNode {
|
||||||
|
name: "Output".into(),
|
||||||
|
inputs: vec![NodeInput::Node(output_node_id)],
|
||||||
|
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("T")])),
|
||||||
|
metadata: DocumentNodeMetadata { position: (output_offset, 4).into() },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the nested network given by the path of node ids
|
||||||
|
pub fn nested_network(&self, nested_path: &[NodeId]) -> Option<&Self> {
|
||||||
|
let mut network = Some(self);
|
||||||
|
|
||||||
|
for segment in nested_path {
|
||||||
|
network = network.and_then(|network| network.nodes.get(segment)).and_then(|node| node.implementation.get_network());
|
||||||
|
}
|
||||||
|
network
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the mutable nested network given by the path of node ids
|
||||||
|
pub fn nested_network_mut(&mut self, nested_path: &[NodeId]) -> Option<&mut Self> {
|
||||||
|
let mut network = Some(self);
|
||||||
|
|
||||||
|
for segment in nested_path {
|
||||||
|
network = network.and_then(|network| network.nodes.get_mut(segment)).and_then(|node| node.implementation.get_network_mut());
|
||||||
|
}
|
||||||
|
network
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the specified node id is connected to the output
|
||||||
|
pub fn connected_to_output(&self, node_id: NodeId) -> bool {
|
||||||
|
// If the node is the output then return true
|
||||||
|
if self.output == node_id {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Get the output
|
||||||
|
let Some(output_node) = self.nodes.get(&self.output) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut stack = vec![output_node];
|
||||||
|
let mut already_visited = HashSet::new();
|
||||||
|
already_visited.insert(self.output);
|
||||||
|
|
||||||
|
while let Some(node) = stack.pop() {
|
||||||
|
for input in &node.inputs {
|
||||||
|
if let &NodeInput::Node(ref_id) = input {
|
||||||
|
// Skip if already viewed
|
||||||
|
if already_visited.contains(&ref_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// If the target node is used as input then return true
|
||||||
|
if ref_id == node_id {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Add the referenced node to the stack
|
||||||
|
let Some(ref_node) = self.nodes.get(&ref_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
already_visited.insert(ref_id);
|
||||||
|
stack.push(ref_node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ fn has_attribute(attrs: &[Attribute], target: &str) -> bool {
|
|||||||
/// Returns the new input type and a conversion to the origional.
|
/// Returns the new input type and a conversion to the origional.
|
||||||
fn easier_string_assignment(field_ty: &Type, field_ident: &Ident) -> (TokenStream2, TokenStream2) {
|
fn easier_string_assignment(field_ty: &Type, field_ident: &Ident) -> (TokenStream2, TokenStream2) {
|
||||||
if let Type::Path(type_path) = field_ty {
|
if let Type::Path(type_path) = field_ty {
|
||||||
if let Some(last_segement) = type_path.path.segments.last() {
|
if let Some(last_segment) = type_path.path.segments.last() {
|
||||||
// Check if this type is a `String`
|
// Check if this type is a `String`
|
||||||
// Based on https://stackoverflow.com/questions/66906261/rust-proc-macro-derive-how-do-i-check-if-a-field-is-of-a-primitive-type-like-b
|
// Based on https://stackoverflow.com/questions/66906261/rust-proc-macro-derive-how-do-i-check-if-a-field-is-of-a-primitive-type-like-b
|
||||||
if last_segement.ident == Ident::new("String", last_segement.ident.span()) {
|
if last_segment.ident == Ident::new("String", last_segment.ident.span()) {
|
||||||
return (
|
return (
|
||||||
quote::quote_spanned!(type_path.span() => impl Into<String>),
|
quote::quote_spanned!(type_path.span() => impl Into<String>),
|
||||||
quote::quote_spanned!(field_ident.span() => #field_ident.into()),
|
quote::quote_spanned!(field_ident.span() => #field_ident.into()),
|
||||||
@@ -49,10 +49,10 @@ fn find_type_and_assignment(field: &Field) -> syn::Result<(TokenStream2, TokenSt
|
|||||||
|
|
||||||
// Check if type is `WidgetCallback`
|
// Check if type is `WidgetCallback`
|
||||||
if let Type::Path(type_path) = field_ty {
|
if let Type::Path(type_path) = field_ty {
|
||||||
if let Some(last_segement) = type_path.path.segments.last() {
|
if let Some(last_segment) = type_path.path.segments.last() {
|
||||||
if let PathArguments::AngleBracketed(generic_args) = &last_segement.arguments {
|
if let PathArguments::AngleBracketed(generic_args) = &last_segment.arguments {
|
||||||
if let Some(first_generic) = generic_args.args.first() {
|
if let Some(first_generic) = generic_args.args.first() {
|
||||||
if last_segement.ident == Ident::new("WidgetCallback", last_segement.ident.span()) {
|
if last_segment.ident == Ident::new("WidgetCallback", last_segment.ident.span()) {
|
||||||
// Assign builder pattern to assign the closure directly
|
// Assign builder pattern to assign the closure directly
|
||||||
function_input_ty = quote::quote_spanned!(field_ty.span() => impl Fn(&#first_generic) -> crate::messages::message::Message + 'static + Send + Sync);
|
function_input_ty = quote::quote_spanned!(field_ty.span() => impl Fn(&#first_generic) -> crate::messages::message::Message + 'static + Send + Sync);
|
||||||
assignment = quote::quote_spanned!(field_ident.span() => crate::messages::layout::utility_types::layout_widget::WidgetCallback::new(#field_ident));
|
assignment = quote::quote_spanned!(field_ident.span() => crate::messages::layout::utility_types::layout_widget::WidgetCallback::new(#field_ident));
|
||||||
|
|||||||
Reference in New Issue
Block a user