diff --git a/Cargo.lock b/Cargo.lock index af9078b73..f55a8b7fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,7 +397,7 @@ dependencies = [ "http", "http-body", "hyper", - "itoa 1.0.8", + "itoa 1.0.9", "matchit", "memchr", "mime", @@ -1191,9 +1191,9 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "dtoa" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519b83cd10f5f6e969625a409f735182bea5558cd8b64c655806ceaae36f1999" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" [[package]] name = "dtoa-short" @@ -1231,9 +1231,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" +checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" [[package]] name = "either" @@ -2320,7 +2320,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", - "itoa 1.0.8", + "itoa 1.0.9", ] [[package]] @@ -2379,7 +2379,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.8", + "itoa 1.0.9", "pin-project-lite", "socket2", "tokio", @@ -2598,9 +2598,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "javascriptcore-rs" @@ -3597,9 +3597,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pathdiff" @@ -3856,9 +3856,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +checksum = "92de25114670a878b1261c79c9f8f729fb97e95bac93f6312f583c60dd6a1dfe" dependencies = [ "unicode-ident", ] @@ -3889,9 +3889,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "5907a1b7c277254a8b15170f6e7c97cfa60ee7872a3217663bb81151e48184bb" dependencies = [ "proc-macro2", ] @@ -4281,9 +4281,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rustybuzz" @@ -4303,9 +4303,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "safe_arch" @@ -4420,9 +4420,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" dependencies = [ "serde", ] @@ -4471,22 +4471,22 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5062a995d481b2308b6064e9af76011f2921c35f97b0468811ed9f6cd91dfed" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" dependencies = [ - "itoa 1.0.8", + "itoa 1.0.9", "ryu", "serde", ] [[package]] name = "serde_path_to_error" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc4422959dd87a76cb117c191dcbffc20467f06c9100b76721dab370f24d3a" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" dependencies = [ - "itoa 1.0.8", + "itoa 1.0.9", "serde", ] @@ -4517,7 +4517,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.8", + "itoa 1.0.9", "ryu", "serde", ] @@ -5405,7 +5405,7 @@ version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" dependencies = [ - "itoa 1.0.8", + "itoa 1.0.9", "libc", "num_threads", "serde", diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index 0275baeb9..0d04f3509 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -5,7 +5,9 @@ use crate::messages::layout::utility_types::widgets::input_widgets::{CheckboxInp use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType, TextLabel}; use crate::messages::prelude::*; -use glam::UVec2; +use graphene_core::uuid::generate_uuid; + +use glam::{IVec2, UVec2}; /// A dialog to allow users to set some initial options about a new document. #[derive(Debug, Clone, Default)] @@ -27,13 +29,20 @@ impl MessageHandler for NewDocumentDialogMessageHa responses.add(PortfolioMessage::NewDocumentWithName { name: self.name.clone() }); if !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0 { + let id = generate_uuid(); responses.add(ArtboardMessage::AddArtboard { - id: None, + id: Some(id), position: (0., 0.), size: (self.dimensions.x as f64, self.dimensions.y as f64), }); + responses.add(GraphOperationMessage::NewArtboard { + id, + artboard: graphene_core::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()), + }); responses.add(DocumentMessage::ZoomCanvasToFitAll); } + responses.add(NodeGraphMessage::RunDocumentGraph); + responses.add(NodeGraphMessage::UpdateNewNodeGraph); } } diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 542fcd843..a58131cdd 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -150,6 +150,9 @@ pub enum FrontendMessage { layout_target: LayoutTarget, diff: Vec, }, + UpdateDocumentNodeRender { + svg: String, + }, UpdateDocumentOverlays { svg: String, }, @@ -163,6 +166,9 @@ pub enum FrontendMessage { size: (f64, f64), multiplier: (f64, f64), }, + UpdateDocumentTransform { + transform: String, + }, UpdateEyedropperSamplingState { #[serde(rename = "mousePosition")] mouse_position: Option<(f64, f64)>, diff --git a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs index 89d336c05..9f886220b 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -32,17 +32,7 @@ impl MessageHandler for InputP // TODO: Extend this to multiple viewports instead of setting it to the value of this last loop iteration self.viewport_bounds = bounds; - responses.add(Operation::TransformLayer { - path: vec![], - transform: glam::DAffine2::from_translation(translation).to_cols_array(), - }); - responses.add(DocumentMessage::Artboard( - Operation::TransformLayer { - path: vec![], - transform: glam::DAffine2::from_translation(translation).to_cols_array(), - } - .into(), - )); + responses.add(NavigationMessage::TranslateCanvas { delta: DVec2::ZERO }); responses.add(FrontendMessage::TriggerViewportResize); } } diff --git a/editor/src/messages/portfolio/document/artboard/artboard_message_handler.rs b/editor/src/messages/portfolio/document/artboard/artboard_message_handler.rs index 2451ef853..cba032465 100644 --- a/editor/src/messages/portfolio/document/artboard/artboard_message_handler.rs +++ b/editor/src/messages/portfolio/document/artboard/artboard_message_handler.rs @@ -61,9 +61,11 @@ impl MessageHandler for ArtboardMessageHandler responses.add(DocumentMessage::RenderDocument); } ClearArtboards => { - for &artboard in self.artboard_ids.iter() { - responses.add_front(ArtboardMessage::DeleteArtboard { artboard }); - } + // TODO: Make this remove the artboard layers from the graph (and cleanly reconnect the artwork) + responses.add(DialogMessage::RequestComingSoonDialog { issue: None }); + // for &artboard in self.artboard_ids.iter() { + // responses.add_front(ArtboardMessage::DeleteArtboard { artboard }); + // } } DeleteArtboard { artboard } => { self.artboard_ids.retain(|&id| id != artboard); @@ -78,11 +80,13 @@ impl MessageHandler for ArtboardMessageHandler responses.add(FrontendMessage::UpdateDocumentArtboards { svg: r##""##.to_string(), }) - } else { - let render_data = RenderData::new(&persistent_data.font_cache, ViewMode::Normal, None); - responses.add(FrontendMessage::UpdateDocumentArtboards { - svg: self.artboards_document.render_root(&render_data), - }); + // TODO: Delete this whole legacy code path when cleaning up/removing the old (non-node based) artboard implementation + // TODO: The below code was used to draw the non-node based artboards, but we still need the above code to draw the infinite canvas until the refactor is complete and all this code can be removed + // } else { + // let render_data = RenderData::new(&persistent_data.font_cache, ViewMode::Normal, None); + // responses.add(FrontendMessage::UpdateDocumentArtboards { + // svg: self.artboards_document.render_root(&render_data), + // }); } } ResizeArtboard { artboard, position, mut size } => { diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index a4c94ce77..7778133e2 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -124,6 +124,9 @@ pub enum DocumentMessage { mouse: Option<(f64, f64)>, }, Redo, + RenameDocument { + new_name: String, + }, RenameLayer { layer_path: Vec, new_name: String, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 7d59bcb42..cb4ff9393 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -197,6 +197,7 @@ impl MessageHandler { let properties_panel_message_handler_data = PropertiesPanelMessageHandlerData { + document_name: &self.name.as_str(), artwork_document: &self.document_legacy, artboard_document: &self.artboard_message_handler.artboards_document, selected_layers: &mut self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then_some(path.as_slice())), @@ -208,7 +209,8 @@ impl MessageHandler { - self.node_graph_handler.process_message(message, responses, (&mut self.document_legacy, executor, document_id)); + self.node_graph_handler + .process_message(message, responses, (&mut self.document_legacy, executor, document_id, self.name.as_str())); } #[remain::unsorted] GraphOperation(message) => GraphOperationMessageHandler.process_message(message, responses, (&mut self.document_legacy, &mut self.node_graph_handler)), @@ -659,6 +661,11 @@ impl MessageHandler { + self.name = new_name; + responses.add(PortfolioMessage::UpdateOpenDocumentsList); + responses.add(NodeGraphMessage::UpdateNewNodeGraph); + } RenameLayer { layer_path, new_name } => responses.add(DocumentOperation::RenameLayer { layer_path, new_name }), RenderDocument => { responses.add(FrontendMessage::UpdateDocumentArtwork { @@ -1096,6 +1103,7 @@ impl DocumentMessageHandler { let starting_root_transform = document.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.size() / 2.); document.document_legacy.root.transform = starting_root_transform; document.artboard_message_handler.artboards_document.root.transform = starting_root_transform; + document } diff --git a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs index 3e62e5a49..dce9e67ac 100644 --- a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs +++ b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs @@ -10,6 +10,7 @@ use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use document_legacy::document::Document; use document_legacy::Operation as DocumentOperation; +use graphene_core::renderer::format_transform_matrix; use glam::{DAffine2, DVec2}; use serde::{Deserialize, Serialize}; @@ -352,6 +353,9 @@ impl NavigationMessageHandler { } .into(), )); + let transform = format_transform_matrix(self.calculate_offset_transform(scaled_half_viewport)); + responses.add(FrontendMessage::UpdateDocumentTransform { transform }); + // TODO: Artboard pos } pub fn center_zoom(&self, viewport_bounds: DVec2, zoom_factor: f64, mouse: DVec2) -> Message { diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs index fd03b1302..e3f5ac9a9 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs @@ -1,11 +1,13 @@ use crate::messages::prelude::*; +use graph_craft::document::NodeId; use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::brush_stroke::BrushStroke; use graphene_core::vector::style::{Fill, Stroke}; use graphene_core::vector::ManipulatorPointId; +use graphene_core::Artboard; -use glam::{DAffine2, DVec2}; +use glam::{DAffine2, DVec2, IVec2}; pub type LayerIdentifier = Vec; @@ -51,6 +53,20 @@ pub enum GraphOperationMessage { layer: LayerIdentifier, strokes: Vec, }, + + NewArtboard { + id: NodeId, + artboard: Artboard, + }, + ResizeArtboard { + id: NodeId, + location: IVec2, + dimensions: IVec2, + }, + DeleteArtboard { + id: NodeId, + }, + ClearArtboards, } #[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs index a99d30487..76cc683d7 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs @@ -4,12 +4,13 @@ use crate::messages::prelude::*; use document_legacy::document::Document; use document_legacy::{LayerId, Operation}; use graph_craft::document::value::TaggedValue; -use graph_craft::document::{generate_uuid, NodeId, NodeInput, NodeNetwork}; +use graph_craft::document::{generate_uuid, DocumentNode, DocumentNodeMetadata, NodeId, NodeInput, NodeNetwork, NodeOutput}; use graphene_core::vector::brush_stroke::BrushStroke; use graphene_core::vector::style::{Fill, FillType, Stroke}; +use graphene_core::Artboard; use transform_utils::LayerBounds; -use glam::{DAffine2, DVec2}; +use glam::{DAffine2, DVec2, IVec2}; pub mod transform_utils; @@ -21,27 +22,152 @@ struct ModifyInputsContext<'a> { node_graph: &'a mut NodeGraphMessageHandler, responses: &'a mut VecDeque, layer: &'a [LayerId], + outwards_links: HashMap>, + layer_node: Option, } impl<'a> ModifyInputsContext<'a> { /// Get the node network from the document fn new(layer: &'a [LayerId], document: &'a mut Document, node_graph: &'a mut NodeGraphMessageHandler, responses: &'a mut VecDeque) -> Option { document.layer_mut(layer).ok().and_then(|layer| layer.as_layer_network_mut().ok()).map(|network| Self { + outwards_links: network.collect_outwards_links(), network, node_graph, responses, layer, + layer_node: None, }) } + /// Get the node network from the document + fn new_doc(document: &'a mut Document, node_graph: &'a mut NodeGraphMessageHandler, responses: &'a mut VecDeque) -> Self { + Self { + outwards_links: document.document_network.collect_outwards_links(), + network: &mut document.document_network, + node_graph, + responses, + layer: &[], + layer_node: None, + } + } + + fn locate_layer(&mut self, mut id: NodeId) -> Option { + while self.network.nodes.get(&id)?.name != "Layer" { + id = self.outwards_links.get(&id)?.first().copied()?; + } + self.layer_node = Some(id); + Some(id) + } + /// Updates the input of an existing node fn modify_existing_node_inputs(&mut self, node_id: NodeId, update_input: impl FnOnce(&mut Vec)) { let document_node = self.network.nodes.get_mut(&node_id).unwrap(); update_input(&mut document_node.inputs); } + pub fn insert_between(&mut self, pre: NodeOutput, post: NodeOutput, mut node: DocumentNode, input: usize, output: usize) -> Option { + let id = generate_uuid(); + let pre_node = self.network.nodes.get_mut(&pre.node_id)?; + node.metadata.position = pre_node.metadata.position; + + let post_node = self.network.nodes.get_mut(&post.node_id)?; + node.inputs[input] = NodeInput::node(pre.node_id, pre.node_output_index); + post_node.inputs[post.node_output_index] = NodeInput::node(id, output); + + self.network.nodes.insert(id, node); + + self.shift_upstream(id, IVec2::new(-8, 0)); + + Some(id) + } + + pub fn insert_layer_below(&mut self, node_id: NodeId, input_index: usize) -> Option { + let layer_node = resolve_document_node_type("Layer").expect("Layer node"); + + let new_id = generate_uuid(); + let post_node = self.network.nodes.get_mut(&node_id)?; + post_node.inputs[input_index] = NodeInput::node(new_id, 0); + let document_node = layer_node.to_document_node_default_inputs([], DocumentNodeMetadata::position(post_node.metadata.position + IVec2::new(0, 2))); + + self.network.nodes.insert(new_id, document_node); + + Some(new_id) + } + + pub fn insert_node_before(&mut self, new_id: NodeId, node_id: NodeId, input_index: usize, mut document_node: DocumentNode, offset: IVec2) -> Option { + let post_node = self.network.nodes.get_mut(&node_id)?; + + post_node.inputs[input_index] = NodeInput::node(new_id, 0); + document_node.metadata.position = post_node.metadata.position + offset; + self.network.nodes.insert(new_id, document_node); + + Some(new_id) + } + + pub fn create_layer(&mut self, new_id: NodeId, output_node_id: NodeId) -> Option { + let mut current_node = output_node_id; + let mut input_index = 0; + let mut current_input = &self.network.nodes.get(¤t_node)?.inputs[input_index]; + + while let NodeInput::Node { node_id, output_index, .. } = current_input { + let mut sibling_node = &self.network.nodes.get(node_id)?; + if sibling_node.name == "Layer" { + current_node = *node_id; + input_index = 7; + current_input = &self.network.nodes.get(¤t_node)?.inputs[input_index]; + } else { + // Insert a layer node between the output and the new + let layer_node = resolve_document_node_type("Layer").expect("Layer node"); + let node = layer_node.to_document_node_default_inputs([], DocumentNodeMetadata::default()); + let node_id = self.insert_between(NodeOutput::new(*node_id, *output_index), NodeOutput::new(current_node, input_index), node, 0, 0)?; + current_node = node_id; + input_index = 7; + current_input = &self.network.nodes.get(¤t_node)?.inputs[input_index]; + } + } + + let layer_node = resolve_document_node_type("Layer").expect("Node").to_document_node_default_inputs([], Default::default()); + let layer_node = self.insert_node_before(new_id, current_node, input_index, layer_node, IVec2::new(0, 3))?; + + Some(layer_node) + } + + fn insert_artboard(&mut self, artboard: Artboard, layer: NodeId) -> Option { + let artboard_node = resolve_document_node_type("Artboard").expect("Node").to_document_node_default_inputs( + [ + None, + Some(NodeInput::value(TaggedValue::IVec2(artboard.location), false)), + Some(NodeInput::value(TaggedValue::IVec2(artboard.dimensions), false)), + Some(NodeInput::value(TaggedValue::Color(artboard.background), false)), + Some(NodeInput::value(TaggedValue::Bool(artboard.clip), false)), + ], + Default::default(), + ); + self.insert_node_before(generate_uuid(), layer, 0, artboard_node, IVec2::new(-8, 0)) + } + + fn shift_upstream(&mut self, node_id: NodeId, shift: IVec2) { + let mut shift_nodes = HashSet::new(); + let mut stack = vec![node_id]; + while let Some(node) = stack.pop() { + let Some(node) = self.network.nodes.get(&node_id) else { continue }; + for input in &node.inputs { + let NodeInput::Node { node_id, .. } = input else { continue }; + if shift_nodes.insert(*node_id) { + stack.push(*node_id); + } + } + } + + for node_id in shift_nodes { + if let Some(node) = self.network.nodes.get_mut(&node_id) { + node.metadata.position += shift; + } + } + } + /// Inserts a new node and modifies the inputs fn modify_new_node(&mut self, name: &'static str, update_input: impl FnOnce(&mut Vec)) { - let output_node_id = self.network.outputs[0].node_id; + let output_node_id = self.layer_node.unwrap_or(self.network.outputs[0].node_id); let Some(output_node) = self.network.nodes.get_mut(&output_node_id) else { warn!("Output node doesn't exist"); return; @@ -65,7 +191,7 @@ impl<'a> ModifyInputsContext<'a> { /// Changes the inputs of a specific node fn modify_inputs(&mut self, name: &'static str, skip_rerender: bool, update_input: impl FnOnce(&mut Vec)) { - let existing_node_id = self.network.primary_flow().find(|(node, _)| node.name == name).map(|(_, id)| id); + let existing_node_id = self.network.primary_flow_from_opt(self.layer_node).find(|(node, _)| node.name == name).map(|(_, id)| id); if let Some(node_id) = existing_node_id { self.modify_existing_node_inputs(node_id, update_input); } else { @@ -229,6 +355,36 @@ impl<'a> ModifyInputsContext<'a> { inputs[2] = NodeInput::value(TaggedValue::BrushStrokes(strokes), false); }); } + + fn resize_artboard(&mut self, location: IVec2, dimensions: IVec2) { + self.modify_inputs("Artboard", false, |inputs| { + inputs[1] = NodeInput::value(TaggedValue::IVec2(location), false); + inputs[2] = NodeInput::value(TaggedValue::IVec2(dimensions), false); + }); + } + + fn delete_layer(&mut self, id: NodeId) { + let mut new_input = None; + let post_node = self.outwards_links.get(&id).and_then(|links| links.first().copied()); + let mut delete_nodes = vec![id]; + for (node, id) in self.network.primary_flow_from_opt(Some(id)) { + delete_nodes.push(id); + if node.name == "Artboard" { + new_input = Some(node.inputs[0].clone()); + break; + } + } + + for node_id in delete_nodes { + self.network.nodes.remove(&node_id); + } + + if let (Some(new_input), Some(post_node)) = (new_input, post_node) { + if let Some(node) = self.network.nodes.get_mut(&post_node) { + node.inputs[0] = new_input; + } + } + } } impl MessageHandler for GraphOperationMessageHandler { @@ -316,6 +472,31 @@ impl MessageHandler { + let mut modify_inputs = ModifyInputsContext::new_doc(document, node_graph, responses); + if let Some(layer) = modify_inputs.create_layer(id, modify_inputs.network.outputs[0].node_id) { + modify_inputs.insert_artboard(artboard, layer); + } + + //modify_inputs.brush_modify(strokes); + } + GraphOperationMessage::ResizeArtboard { id, location, dimensions } => { + let mut modify_inputs = ModifyInputsContext::new_doc(document, node_graph, responses); + if let Some(layer) = modify_inputs.locate_layer(id) { + modify_inputs.resize_artboard(location, dimensions); + } + } + GraphOperationMessage::DeleteArtboard { id } => { + let mut modify_inputs = ModifyInputsContext::new_doc(document, node_graph, responses); + modify_inputs.delete_layer(id); + } + GraphOperationMessage::ClearArtboards => { + let mut modify_inputs = ModifyInputsContext::new_doc(document, node_graph, responses); + let artboard_nodes = modify_inputs.network.nodes.iter().filter(|(_, node)| node.name == "Artboard").map(|(id, _)| *id).collect::>(); + for id in artboard_nodes { + modify_inputs.delete_layer(id); + } + } } } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index dcc84f4f5..87855c9a3 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -156,7 +156,7 @@ impl NodeGraphMessageHandler { self.get_root_network_mut(document).nested_network_mut(&self.nested_path) } - /// Send the cached layout for the bar at the top of the node panel to the frontend + /// Send the cached layout to the frontend for the options bar at the top of the node panel fn send_node_bar_layout(&self, responses: &mut VecDeque) { responses.add(LayoutMessage::SendLayout { layout: Layout::WidgetLayout(WidgetLayout::new(self.widgets.to_vec())), @@ -165,34 +165,48 @@ impl NodeGraphMessageHandler { } /// Collect the addresses of the currently viewed nested node e.g. Root -> MyFunFilter -> Exposure - fn collect_nested_addresses(&mut self, document: &Document, responses: &mut VecDeque) { - // // Build path list - let mut path = vec![]; - if let Some(layer) = self.layer_path.as_ref().and_then(|path| document.layer(path).ok()) { - path.push(format!("Layer: {}", layer.name.as_deref().unwrap_or("Untitled Layer"))); - } else { - path.push("Document Network".to_string()); - } + fn collect_nested_addresses(&mut self, document: &Document, document_name: &str, responses: &mut VecDeque) { + let layer_if_selected = self.layer_path.as_ref().and_then(|path| document.layer(path).ok()); + + // Build path list for the layer, or otherwise the root document + let path_root = match layer_if_selected { + Some(layer) => layer.name.as_deref().unwrap_or("Untitled Layer"), + None => document_name, + }; + let mut path = vec![path_root.to_string()]; + + let (icon, tooltip) = match layer_if_selected { + Some(_) => ("Layer", "Layer"), + None => ("File", "Document"), + }; + let mut network = Some(self.get_root_network(document)); for node_id in &self.nested_path { let node = network.and_then(|network| network.nodes.get(node_id)); + if let Some(DocumentNode { name, .. }) = node { path.push(name.clone()); } + network = node.and_then(|node| node.implementation.get_network()); } + let nesting = path.len(); // Update UI self.widgets[0] = LayoutGroup::Row { - widgets: vec![BreadcrumbTrailButtons::new(path.clone()) - .on_update(move |input: &u64| { - NodeGraphMessage::ExitNestedNetwork { - depth_of_nesting: nesting - (*input as usize) - 1, - } - .into() - }) - .widget_holder()], + widgets: vec![ + IconLabel::new(icon).tooltip(tooltip).widget_holder(), + WidgetHolder::unrelated_separator(), + BreadcrumbTrailButtons::new(path.clone()) + .on_update(move |input: &u64| { + NodeGraphMessage::ExitNestedNetwork { + depth_of_nesting: nesting - (*input as usize) - 1, + } + .into() + }) + .widget_holder(), + ], }; self.send_node_bar_layout(responses); @@ -425,9 +439,9 @@ impl NodeGraphMessageHandler { } } -impl MessageHandler for NodeGraphMessageHandler { +impl MessageHandler for NodeGraphMessageHandler { #[remain::check] - fn process_message(&mut self, message: NodeGraphMessage, responses: &mut VecDeque, (document, executor, document_id): (&mut Document, &NodeGraphExecutor, u64)) { + fn process_message(&mut self, message: NodeGraphMessage, responses: &mut VecDeque, (document, executor, document_id, document_name): (&mut Document, &NodeGraphExecutor, u64, &str)) { #[remain::sorted] match message { NodeGraphMessage::CloseNodeGraph => { @@ -564,7 +578,7 @@ impl MessageHandler if let Some(network) = self.get_active_network(document) { Self::send_graph(network, executor, &self.layer_path, responses); } - self.collect_nested_addresses(document, responses); + self.collect_nested_addresses(document, document_name, responses); self.update_selected(document, responses); } NodeGraphMessage::DuplicateSelectedNodes => { @@ -600,7 +614,7 @@ impl MessageHandler if let Some(network) = self.get_active_network(document) { Self::send_graph(network, executor, &self.layer_path, responses); } - self.collect_nested_addresses(document, responses); + self.collect_nested_addresses(document, document_name, responses); self.update_selected(document, responses); } NodeGraphMessage::ExposeInput { node_id, input_index, new_exposed } => { @@ -662,7 +676,7 @@ impl MessageHandler let node_types = document_node_types::collect_node_types(); responses.add(FrontendMessage::UpdateNodeTypes { node_types }); } - self.collect_nested_addresses(document, responses); + self.collect_nested_addresses(document, document_name, responses); self.update_selected(document, responses); } NodeGraphMessage::PasteNodes { serialized_nodes } => { @@ -895,7 +909,7 @@ impl MessageHandler let node_types = document_node_types::collect_node_types(); responses.add(FrontendMessage::UpdateNodeTypes { node_types }); } - self.collect_nested_addresses(document, responses); + self.collect_nested_addresses(document, document_name, responses); self.update_selected(document, responses); } } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index bd5adb04a..a3c7c9a54 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -204,12 +204,13 @@ fn static_nodes() -> Vec { DocumentNodeType { name: "Artboard", category: "General", - identifier: NodeImplementation::proto("graphene_core::ConstructArtboardNode<_, _, _>"), + identifier: NodeImplementation::proto("graphene_core::ConstructArtboardNode<_, _, _, _>"), inputs: vec![ DocumentInputType::value("Graphic Group", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true), DocumentInputType::value("Location", TaggedValue::IVec2(glam::IVec2::ZERO), false), DocumentInputType::value("Dimensions", TaggedValue::IVec2(glam::IVec2::new(1920, 1080)), false), DocumentInputType::value("Background", TaggedValue::Color(Color::WHITE), false), + DocumentInputType::value("Clip", TaggedValue::Bool(false), false), ], outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::Artboard)], properties: node_properties::artboard_properties, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index 673a5016d..0f92e92b8 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -1623,5 +1623,8 @@ pub fn artboard_properties(document_node: &DocumentNode, node_id: NodeId, _conte let location = vec2_widget(document_node, node_id, 1, "Location", "X", "Y", " px", add_blank_assist); let dimensions = vec2_widget(document_node, node_id, 2, "Dimensions", "W", "H", " px", add_blank_assist); let background = color_widget(document_node, node_id, 3, "Background", ColorInput::default().allow_none(false), true); - vec![location, dimensions, background] + let clip = LayoutGroup::Row { + widgets: bool_widget(document_node, node_id, 4, "Clip", true), + }; + vec![location, dimensions, background, clip] } diff --git a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs index 539822f0f..1911eb809 100644 --- a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs @@ -24,6 +24,7 @@ impl<'a> MessageHandler MessageHandler { + // This causes the Properties panel to change, so this needs to happen before the following lines clear the Properties panel + responses.add(NodeGraphMessage::CloseNodeGraph); + responses.add(LayoutMessage::SendLayout { layout: Layout::WidgetLayout(WidgetLayout::new(vec![])), layout_target: LayoutTarget::PropertiesOptions, @@ -71,7 +75,6 @@ impl<'a> MessageHandler responses.add(BroadcastMessage::UnsubscribeEvent { @@ -151,7 +154,7 @@ impl<'a> MessageHandler responses.add(PropertiesPanelMessage::SetActiveLayers { diff --git a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs index fbcec313f..4ec2d94ec 100644 --- a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs +++ b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs @@ -78,19 +78,14 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ ..Default::default() })), WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Artboard".into(), - ..TextLabel::default() - })), - WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::TextInput(TextInput { - value: layer.name.clone().unwrap_or_else(|| "Untitled".to_string()), + value: layer.name.clone().unwrap_or_else(|| "Untitled Artboard".to_string()), on_update: WidgetCallback::new(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()), ..Default::default() })), WidgetHolder::related_separator(), WidgetHolder::new(Widget::PopoverButton(PopoverButton { - header: "Options Bar".into(), + header: "Additional Options".into(), text: "Coming soon".into(), ..Default::default() })), @@ -261,22 +256,14 @@ pub fn register_artwork_layer_properties( })), }, WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: match &layer.data { - LayerDataType::Layer(_) => "Layer".into(), - other => LayerDataTypeDiscriminant::from(other).to_string(), - }, - ..TextLabel::default() - })), - WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::TextInput(TextInput { - value: layer.name.clone().unwrap_or_else(|| "Untitled".to_string()), + value: layer.name.clone().unwrap_or_else(|| "Untitled Layer".to_string()), on_update: WidgetCallback::new(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()), ..Default::default() })), WidgetHolder::related_separator(), WidgetHolder::new(Widget::PopoverButton(PopoverButton { - header: "Options Bar".into(), + header: "Additional Options".into(), text: "Coming soon".into(), ..Default::default() })), @@ -326,18 +313,18 @@ pub fn register_artwork_layer_properties( }); } -pub fn register_document_graph_properties(mut context: NodePropertiesContext, node_graph_message_handler: &NodeGraphMessageHandler) { +pub fn register_document_graph_properties(mut context: NodePropertiesContext, node_graph_message_handler: &NodeGraphMessageHandler, document_name: &str) { let mut properties_sections = Vec::new(); node_graph_message_handler.collate_properties(&mut context, &mut properties_sections); let options_bar = vec![LayoutGroup::Row { widgets: vec![ - IconLabel::new("File").widget_holder(), + IconLabel::new("File").tooltip("Document").widget_holder(), WidgetHolder::unrelated_separator(), - TextLabel::new("Document graph").widget_holder(), - WidgetHolder::unrelated_separator(), - TextInput::new("No layer selected").disabled(true).widget_holder(), + TextInput::new(document_name) + .on_update(|text_input| DocumentMessage::RenameDocument { new_name: text_input.value.clone() }.into()) + .widget_holder(), WidgetHolder::related_separator(), - PopoverButton::new("Options Bar", "Coming soon").widget_holder(), + PopoverButton::new("Additional Options", "Coming soon").widget_holder(), ], }]; diff --git a/editor/src/messages/portfolio/document/properties_panel/utility_types.rs b/editor/src/messages/portfolio/document/properties_panel/utility_types.rs index a4a639539..f6040f1c1 100644 --- a/editor/src/messages/portfolio/document/properties_panel/utility_types.rs +++ b/editor/src/messages/portfolio/document/properties_panel/utility_types.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::{messages::prelude::NodeGraphMessageHandler, node_graph_executor::NodeGraphExecutor}; pub struct PropertiesPanelMessageHandlerData<'a> { + pub document_name: &'a str, pub artwork_document: &'a DocumentLegacy, pub artboard_document: &'a DocumentLegacy, pub selected_layers: &'a mut dyn Iterator, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 7a6a15857..e4f9320db 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use super::utility_types::PersistentData; use crate::application::generate_uuid; use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION}; @@ -19,6 +17,9 @@ use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; use graphene_core::text::Font; +use glam::DAffine2; +use std::sync::Arc; + #[derive(Debug, Default)] pub struct PortfolioMessageHandler { menu_bar_message_handler: MenuBarMessageHandler, @@ -114,12 +115,13 @@ impl MessageHandler self.executor.update_imaginate_preferences(preferences.get_imaginate_preferences()), PortfolioMessage::ImaginateServerHostname => { - info!("setting imaginate persistent data"); + debug!("setting imaginate persistent data"); self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname); } PortfolioMessage::Import => { @@ -466,6 +468,7 @@ impl MessageHandler { self.active_document_id = Some(document_id); @@ -664,7 +667,8 @@ impl PortfolioMessageHandler { } pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque) { - self.executor.poll_node_graph_evaluation(responses).unwrap_or_else(|e| { + let transform = self.active_document().map(|document| document.document_legacy.root.transform).unwrap_or(DAffine2::IDENTITY); + self.executor.poll_node_graph_evaluation(transform, responses).unwrap_or_else(|e| { log::error!("Error while evaluating node graph: {}", e); }); } diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index 2415014e0..75581423d 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -13,7 +13,7 @@ use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use document_legacy::intersection::Quad; use document_legacy::LayerId; -use glam::{DVec2, Vec2Swizzles}; +use glam::{DVec2, IVec2, Vec2Swizzles}; use serde::{Deserialize, Serialize}; #[derive(Default)] @@ -133,10 +133,6 @@ impl Fsm for ArtboardToolFsmState { tool_data.bounding_box_overlays = Some(bounding_box_overlays); responses.add(OverlaysMessage::Rerender); - responses.add(PropertiesPanelMessage::SetActiveLayers { - paths: vec![vec![tool_data.selected_artboard.unwrap()]], - document: TargetDocument::Artboard, - }); } _ => {} }; @@ -196,11 +192,6 @@ impl Fsm for ArtboardToolFsmState { .start_snap(document, input, document.bounding_boxes(None, Some(intersection[0]), render_data), true, true); tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]); - responses.add(PropertiesPanelMessage::SetActiveLayers { - paths: vec![intersection.clone()], - document: TargetDocument::Artboard, - }); - ArtboardToolFsmState::Dragging } else { tool_data.selected_artboard = None; @@ -226,6 +217,11 @@ impl Fsm for ArtboardToolFsmState { position: position.round().into(), size: size.round().into(), }); + responses.add(GraphOperationMessage::ResizeArtboard { + id: tool_data.selected_artboard.unwrap(), + location: position.round().as_ivec2(), + dimensions: size.round().as_ivec2(), + }); responses.add(BroadcastEvent::DocumentIsDirty); } @@ -251,6 +247,11 @@ impl Fsm for ArtboardToolFsmState { position: position.round().into(), size: size.round().into(), }); + responses.add(GraphOperationMessage::ResizeArtboard { + id: tool_data.selected_artboard.unwrap(), + location: position.round().as_ivec2(), + dimensions: size.round().as_ivec2(), + }); responses.add(BroadcastEvent::DocumentIsDirty); @@ -285,6 +286,11 @@ impl Fsm for ArtboardToolFsmState { position: start.round().into(), size: size.round().into(), }); + responses.add(GraphOperationMessage::ResizeArtboard { + id: tool_data.selected_artboard.unwrap(), + location: start.round().as_ivec2(), + dimensions: size.round().as_ivec2(), + }); } else { let id = generate_uuid(); tool_data.selected_artboard = Some(id); @@ -297,14 +303,20 @@ impl Fsm for ArtboardToolFsmState { position: start.round().into(), size: (1., 1.), }); + responses.add(GraphOperationMessage::NewArtboard { + id, + artboard: graphene_core::Artboard { + graphic_group: graphene_core::GraphicGroup::EMPTY, + location: start.round().as_ivec2(), + dimensions: IVec2::splat(1), + background: graphene_core::Color::WHITE, + clip: false, + }, + }) } // Have to put message here instead of when Artboard is created // This might result in a few more calls but it is not reliant on the order of messages - responses.add(PropertiesPanelMessage::SetActiveLayers { - paths: vec![vec![tool_data.selected_artboard.unwrap()]], - document: TargetDocument::Artboard, - }); responses.add(BroadcastEvent::DocumentIsDirty); @@ -352,6 +364,8 @@ impl Fsm for ArtboardToolFsmState { (_, ArtboardToolMessage::DeleteSelected) => { if let Some(artboard) = tool_data.selected_artboard.take() { responses.add(ArtboardMessage::DeleteArtboard { artboard }); + responses.add(GraphOperationMessage::DeleteArtboard { id: artboard }); + responses.add(BroadcastEvent::DocumentIsDirty); } ArtboardToolFsmState::Ready @@ -363,6 +377,11 @@ impl Fsm for ArtboardToolFsmState { position: (bounds.bounds[0].x + delta_x, bounds.bounds[0].y + delta_y), size: (bounds.bounds[1] - bounds.bounds[0]).round().into(), }); + responses.add(GraphOperationMessage::ResizeArtboard { + id: tool_data.selected_artboard.unwrap(), + location: DVec2::new(bounds.bounds[0].x + delta_x, bounds.bounds[0].y + delta_y).round().as_ivec2(), + dimensions: (bounds.bounds[1] - bounds.bounds[0]).round().as_ivec2(), + }); } ArtboardToolFsmState::Ready diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index a2593ce8d..c53b12790 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -214,7 +214,7 @@ impl NodeRuntime { graphic_group.render_svg(&mut render, &render_params); let [min, max] = bounds.unwrap_or_default(); render.format_svg(min, max); - info!("SVG {}", render.svg); + debug!("SVG {}", render.svg); if let (Some(layer_id), Some(node_id)) = (layer_path.last().copied(), node_path.get(node_path.len() - 2).copied()) { let old_thumbnail = self.thumbnails.entry(layer_id).or_default().entry(node_id).or_default(); @@ -283,16 +283,16 @@ struct ExecutionContext { impl Default for NodeGraphExecutor { fn default() -> Self { - let (request_sender, request_reciever) = std::sync::mpsc::channel(); - let (response_sender, response_reciever) = std::sync::mpsc::channel(); + let (request_sender, request_receiver) = std::sync::mpsc::channel(); + let (response_sender, response_receiver) = std::sync::mpsc::channel(); NODE_RUNTIME.with(|runtime| { - runtime.borrow_mut().replace(NodeRuntime::new(request_reciever, response_sender)); + runtime.borrow_mut().replace(NodeRuntime::new(request_receiver, response_sender)); }); Self { futures: Default::default(), sender: request_sender, - receiver: response_reciever, + receiver: response_receiver, last_output_type: Default::default(), thumbnails: Default::default(), } @@ -423,7 +423,7 @@ impl NodeGraphExecutor { Ok(()) } - pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque) -> Result<(), String> { + pub fn poll_node_graph_evaluation(&mut self, transform: DAffine2, responses: &mut VecDeque) -> Result<(), String> { let results = self.receiver.try_iter().collect::>(); for response in results { match response { @@ -437,7 +437,7 @@ impl NodeGraphExecutor { let node_graph_output = result.map_err(|e| format!("Node graph evaluation failed: {:?}", e))?; let execution_context = self.futures.remove(&generation_id).ok_or_else(|| "Invalid generation ID".to_string())?; responses.extend(updates); - self.process_node_graph_output(node_graph_output, execution_context.layer_path.clone(), responses, execution_context.document_id)?; + self.process_node_graph_output(node_graph_output, execution_context.layer_path.clone(), transform, responses, execution_context.document_id)?; responses.add(DocumentMessage::LayerChanged { affected_layer_path: execution_context.layer_path, }); @@ -456,7 +456,7 @@ impl NodeGraphExecutor { Ok(()) } - fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, layer_path: Vec, responses: &mut VecDeque, document_id: u64) -> Result<(), String> { + fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, layer_path: Vec, transform: DAffine2, responses: &mut VecDeque, document_id: u64) -> Result<(), String> { self.last_output_type.insert(layer_path.clone(), Some(node_graph_output.ty())); match node_graph_output { TaggedValue::VectorData(vector_data) => { @@ -489,11 +489,30 @@ impl NodeGraphExecutor { } } TaggedValue::Artboard(artboard) => { - info!("{artboard:#?}"); + debug!("{artboard:#?}"); return Err("Artboard (see console)".to_string()); } TaggedValue::GraphicGroup(graphic_group) => { - info!("{graphic_group:#?}"); + debug!("{graphic_group:#?}"); + use graphene_core::renderer::{format_transform_matrix, GraphicElementRendered, RenderParams, SvgRender}; + + // Setup rendering + let mut render = SvgRender::new(); + let render_params = RenderParams::new(ViewMode::Normal, None, false); + + // Render svg + graphic_group.render_svg(&mut render, &render_params); + + // Conctenate the defs and the svg into one string + let mut svg = "".to_string(); + svg.push_str(&render.svg_defs); + svg.push_str(""); + use std::fmt::Write; + write!(svg, "{}", render.svg).unwrap(); + + // Send to frontend + responses.add(FrontendMessage::UpdateDocumentNodeRender { svg }); + return Err("Graphic group (see console)".to_string()); } _ => { diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 8615bf212..db66b1d07 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -18,6 +18,8 @@ UpdateDocumentScrollbars, UpdateEyedropperSamplingState, UpdateMouseCursor, + UpdateDocumentNodeRender, + UpdateDocumentTransform, } from "@graphite/wasm-communication/messages"; import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte"; @@ -58,8 +60,10 @@ // Rendered SVG viewport data let artworkSvg = ""; + let nodeRenderSvg = ""; let artboardSvg = ""; let overlaysSvg = ""; + let artworkTransform = ""; // Rasterized SVG viewport data, or none if it's not up-to-date let rasterizedCanvas: HTMLCanvasElement | undefined = undefined; @@ -140,12 +144,21 @@ export function updateDocumentOverlays(svg: string) { overlaysSvg = svg; } - + export function updateDocumentArtboards(svg: string) { artboardSvg = svg; rasterizedCanvas = undefined; } + export function updateDocumentNodeRender(svg: string) { + nodeRenderSvg = svg; + rasterizedCanvas = undefined; + } + + export function updateDocumentTransform(transform: string) { + artworkTransform = transform; + } + export async function updateEyedropperSamplingState(mousePosition: XY | undefined, colorPrimary: string, colorSecondary: string): Promise<[number, number, number] | undefined> { if (mousePosition === undefined) { cursorEyedropper = false; @@ -335,6 +348,16 @@ updateDocumentArtboards(data.svg); }); + editor.subscriptions.subscribeJsMessage(UpdateDocumentNodeRender, async (data) => { + await tick(); + + updateDocumentNodeRender(data.svg); + }); + editor.subscriptions.subscribeJsMessage(UpdateDocumentTransform, async (data) => { + await tick(); + + updateDocumentTransform(data.transform); + }); editor.subscriptions.subscribeJsMessage(UpdateEyedropperSamplingState, async (data) => { await tick(); @@ -445,6 +468,11 @@ {@html artboardSvg} + + + {@html nodeRenderSvg} + + {@html artworkSvg} diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index 3318510ee..96523a1b4 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -445,6 +445,14 @@ export class UpdateDocumentArtboards extends JsMessage { readonly svg!: string; } +export class UpdateDocumentNodeRender extends JsMessage { + readonly svg!: string; +} + +export class UpdateDocumentTransform extends JsMessage { + readonly transform!: string; +} + export class UpdateDocumentScrollbars extends JsMessage { @TupleToVec2 readonly position!: XY; @@ -1351,6 +1359,7 @@ export const messageMakers: Record = { UpdateActiveDocument, UpdateDialogDetails, UpdateDocumentArtboards, + UpdateDocumentNodeRender, UpdateDocumentArtwork, UpdateDocumentBarLayout, UpdateDocumentLayerDetails, @@ -1359,6 +1368,7 @@ export const messageMakers: Record = { UpdateDocumentOverlays, UpdateDocumentRulers, UpdateDocumentScrollbars, + UpdateDocumentTransform, UpdateEyedropperSamplingState, UpdateImageData, UpdateInputHints, diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs index 979ef7d7d..bb8578093 100644 --- a/node-graph/gcore/src/graphic_element.rs +++ b/node-graph/gcore/src/graphic_element.rs @@ -49,6 +49,19 @@ pub struct Artboard { pub location: IVec2, pub dimensions: IVec2, pub background: Color, + pub clip: bool, +} + +impl Artboard { + pub fn new(location: IVec2, dimensions: IVec2) -> Self { + Self { + graphic_group: GraphicGroup::EMPTY, + location: location.min(location + dimensions), + dimensions: dimensions.abs(), + background: Color::WHITE, + clip: false, + } + } } pub struct ConstructLayerNode { @@ -84,19 +97,21 @@ fn construct_layer>( stack } -pub struct ConstructArtboardNode { +pub struct ConstructArtboardNode { location: Location, dimensions: Dimensions, background: Background, + clip: Clip, } #[node_fn(ConstructArtboardNode)] -fn construct_artboard(graphic_group: GraphicGroup, location: IVec2, dimensions: IVec2, background: Color) -> Artboard { +fn construct_artboard(graphic_group: GraphicGroup, location: IVec2, dimensions: IVec2, background: Color, clip: bool) -> Artboard { Artboard { graphic_group, - location, - dimensions, + location: location.min(location + dimensions), + dimensions: dimensions.abs(), background, + clip, } } diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 6ea7723f2..1e09aefff 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -12,6 +12,7 @@ pub struct SvgRender { pub svg_defs: String, pub transform: DAffine2, pub image_data: Vec<(u64, Image)>, + indent: usize, } impl SvgRender { @@ -21,9 +22,15 @@ impl SvgRender { svg_defs: String::new(), transform: DAffine2::IDENTITY, image_data: Vec::new(), + indent: 0, } } + pub fn indent(&mut self) { + self.svg.push("\n"); + self.svg.push("\t".repeat(self.indent)); + } + /// Add an outer `` tag with a `viewBox` and the `` pub fn format_svg(&mut self, bounds_min: DVec2, bounds_max: DVec2) { let (x, y) = bounds_min.into(); @@ -31,7 +38,38 @@ impl SvgRender { let defs = &self.svg_defs; let svg_header = format!(r#"{defs}"#,); self.svg.insert(0, svg_header.into()); - self.svg.push("".into()); + self.svg.push(""); + } + + pub fn leaf_tag(&mut self, name: impl Into, attributes: impl FnOnce(&mut SvgRenderAttrs)) { + self.indent(); + self.svg.push("<"); + self.svg.push(name); + attributes(&mut SvgRenderAttrs(self)); + + self.svg.push("/>"); + } + + pub fn parent_tag(&mut self, name: impl Into, attributes: impl FnOnce(&mut SvgRenderAttrs), inner: impl FnOnce(&mut Self)) { + let name = name.into(); + self.indent(); + self.svg.push("<"); + self.svg.push(name.clone()); + attributes(&mut SvgRenderAttrs(self)); + self.svg.push(">"); + let length = self.svg.len(); + self.indent += 1; + inner(self); + self.indent -= 1; + if self.svg.len() != length { + self.indent(); + self.svg.push(""); + } else { + self.svg.pop(); + self.svg.push("/>"); + } } } @@ -54,8 +92,18 @@ impl RenderParams { } } -fn format_transform_matrix(transform: DAffine2) -> String { - transform.to_cols_array().iter().map(ToString::to_string).collect::>().join(",") +pub fn format_transform_matrix(transform: DAffine2) -> String { + use std::fmt::Write; + let mut result = "matrix(".to_string(); + let cols = transform.to_cols_array(); + for (index, item) in cols.iter().enumerate() { + write!(result, "{}", item).unwrap(); + if index != cols.len() - 1 { + result.push_str(", "); + } + } + result.push(')'); + result } pub trait GraphicElementRendered { @@ -77,17 +125,17 @@ impl GraphicElementRendered for VectorData { let layer_bounds = self.bounding_box().unwrap_or_default(); let transformed_bounds = self.bounding_box_with_transform(render.transform).unwrap_or_default(); - render.svg.push("".into()); + render.leaf_tag("path", |attributes| { + attributes.push("class", "vector-data"); + attributes.push("d", path); + let render = &mut attributes.0; + let style = self.style.render(render_params.view_mode, &mut render.svg_defs, render.transform, layer_bounds, transformed_bounds); + attributes.push_val(style); + }); } fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { self.bounding_box_with_transform(self.transform * transform) @@ -96,7 +144,54 @@ impl GraphicElementRendered for VectorData { impl GraphicElementRendered for Artboard { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { - self.graphic_group.render_svg(render, render_params) + // Background + render.leaf_tag("rect", |attributes| { + attributes.push("class", "artboard-bg"); + attributes.push("fill", format!("#{}", self.background.rgba_hex())); + attributes.push("x", self.location.x.min(self.location.x + self.dimensions.x).to_string()); + attributes.push("y", self.location.y.min(self.location.y + self.dimensions.y).to_string()); + attributes.push("width", self.dimensions.x.abs().to_string()); + attributes.push("height", self.dimensions.y.abs().to_string()); + }); + + // Label + render.parent_tag( + "text", + |attributes| { + attributes.push("class", "artboard-label"); + attributes.push("fill", "white"); + attributes.push("x", (self.location.x.min(self.location.x + self.dimensions.x)).to_string()); + attributes.push("y", (self.location.y.min(self.location.y + self.dimensions.y) - 4).to_string()); + attributes.push("font-size", "14px"); + }, + |render| { + render.svg.push("Artboard"); + }, + ); + + // Contents group + render.parent_tag( + "g", + |attributes| { + attributes.push("class", "artboard"); + if self.clip { + let id = format!("artboard-{}", generate_uuid()); + let selector = format!("url(#{id})"); + use std::fmt::Write; + write!( + &mut attributes.0.svg_defs, + r##""##, + self.location.x, self.location.y, self.dimensions.x, self.dimensions.y + ) + .unwrap(); + attributes.push("clip-path", selector); + } + }, + |render| { + // Contents + self.graphic_group.render_svg(render, render_params); + }, + ); } fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box(); @@ -107,12 +202,14 @@ impl GraphicElementRendered for Artboard { impl GraphicElementRendered for ImageFrame { fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) { let transform: String = format_transform_matrix(self.transform * render.transform); - render - .svg - .push(format!(r#"".into()); + render.leaf_tag("image", |attributes| { + attributes.push("width", 1.to_string()); + attributes.push("height", 1.to_string()); + attributes.push("preserveAspectRatio", "none"); + attributes.push("transform", transform); + attributes.push("href", SvgSegment::BlobUrl(uuid)) + }); render.image_data.push((uuid, self.image.clone())) } fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { @@ -193,3 +290,27 @@ impl core::fmt::Display for SvgSegmentList { Ok(()) } } + +pub struct SvgRenderAttrs<'a>(&'a mut SvgRender); + +impl<'a> SvgRenderAttrs<'a> { + pub fn push_complex(&mut self, name: impl Into, value: impl FnOnce(&mut SvgRender)) { + self.0.svg.push(" "); + self.0.svg.push(name); + self.0.svg.push("=\""); + value(self.0); + self.0.svg.push("\""); + } + pub fn push(&mut self, name: impl Into, value: impl Into) { + self.push_complex(name, move |renderer| renderer.svg.push(value)); + } + pub fn push_val(&mut self, value: impl Into) { + self.0.svg.push(value); + } +} + +impl SvgSegmentList { + pub fn push(&mut self, value: impl Into) { + self.0.push(value.into()); + } +} diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 300eeef8e..1e2c1002f 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -496,34 +496,19 @@ impl NodeNetwork { /// /// Used for the properties panel and tools. pub fn primary_flow(&self) -> impl Iterator { - struct FlowIter<'a> { - stack: Vec, - network: &'a NodeNetwork, - } - impl<'a> Iterator for FlowIter<'a> { - type Item = (&'a DocumentNode, NodeId); - fn next(&mut self) -> Option { - loop { - let node_id = self.stack.pop()?; - if let Some(document_node) = self.network.nodes.get(&node_id) { - self.stack.extend( - document_node - .inputs - .iter() - .take(1) // Only show the primary input - .filter_map(|input| if let NodeInput::Node { node_id: ref_id, .. } = input { Some(*ref_id) } else { None }), - ); - return Some((document_node, node_id)); - }; - } - } - } FlowIter { stack: self.outputs.iter().map(|output| output.node_id).collect(), network: self, } } + pub fn primary_flow_from_opt(&self, id: Option) -> impl Iterator { + FlowIter { + stack: id.map_or_else(|| self.outputs.iter().map(|output| output.node_id).collect(), |id| vec![id]), + network: self, + } + } + pub fn is_acyclic(&self) -> bool { let mut dependencies: HashMap> = HashMap::new(); for (node_id, node) in &self.nodes { @@ -549,6 +534,29 @@ impl NodeNetwork { } } +struct FlowIter<'a> { + stack: Vec, + network: &'a NodeNetwork, +} +impl<'a> Iterator for FlowIter<'a> { + type Item = (&'a DocumentNode, NodeId); + fn next(&mut self) -> Option { + loop { + let node_id = self.stack.pop()?; + if let Some(document_node) = self.network.nodes.get(&node_id) { + self.stack.extend( + document_node + .inputs + .iter() + .take(1) // Only show the primary input + .filter_map(|input| if let NodeInput::Node { node_id: ref_id, .. } = input { Some(*ref_id) } else { None }), + ); + return Some((document_node, node_id)); + }; + } + } +} + /// Functions for compiling the network impl NodeNetwork { pub fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId + Copy) { @@ -835,7 +843,7 @@ impl NodeNetwork { self.nodes.retain(|_, node| !matches!(node.implementation, DocumentNodeImplementation::Extract)); for (_, node) in &mut extraction_nodes { - log::info!("extraction network: {:#?}", &self); + log::debug!("extraction network: {:#?}", &self); if let DocumentNodeImplementation::Extract = node.implementation { assert_eq!(node.inputs.len(), 1); log::debug!("Resolving extract node {:?}", node); diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index 15a4cf3cb..5cadb611d 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -169,7 +169,7 @@ impl ApplicationIo for WasmApplicationIo { fn load_resource(&self, url: impl AsRef) -> Result { let url = url::Url::parse(url.as_ref()).map_err(|_| ApplicationError::InvalidUrl)?; - log::info!("Loading resource: {:?}", url); + log::trace!("Loading resource: {:?}", url); match url.scheme() { #[cfg(feature = "tokio")] "file" => { @@ -196,7 +196,7 @@ impl ApplicationIo for WasmApplicationIo { "graphite" => { let path = url.path(); let path = path.to_owned(); - log::info!("Loading resource: {}", path); + log::trace!("Loading local resource: {}", path); let data = self.resources.get(&path).ok_or(ApplicationError::NotFound)?.clone(); Ok(Box::pin(async move { Ok(data.clone()) }) as Pin, _>>>>) } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 104a6da90..33ad5283e 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -539,7 +539,7 @@ fn node_registry() -> HashMap, input: ImageFrame, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]), register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: graphene_core::GraphicGroup, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]), register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: graphene_core::Artboard, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]), - register_node!(graphene_core::ConstructArtboardNode<_, _, _>, input: graphene_core::GraphicGroup, params: [glam::IVec2, glam::IVec2, Color]), + register_node!(graphene_core::ConstructArtboardNode<_, _, _, _>, input: graphene_core::GraphicGroup, params: [glam::IVec2, glam::IVec2, Color, bool]), ]; let mut map: HashMap> = HashMap::new(); for (id, c, types) in node_types.into_iter().flatten() {