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 ee7094ce..52c8f59f 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 @@ -34,6 +34,10 @@ pub enum FrontendGraphDataType { Boolean, #[serde(rename = "vec2")] Vector, + #[serde(rename = "graphic")] + GraphicGroup, + #[serde(rename = "artboard")] + Artboard, } impl FrontendGraphDataType { pub const fn with_tagged_value(value: &TaggedValue) -> Self { @@ -46,6 +50,8 @@ impl FrontendGraphDataType { TaggedValue::ImageFrame(_) => Self::Raster, TaggedValue::Color(_) => Self::Color, TaggedValue::RcSubpath(_) | TaggedValue::Subpaths(_) | TaggedValue::VectorData(_) => Self::Subpath, + TaggedValue::GraphicGroup(_) => Self::GraphicGroup, + TaggedValue::Artboard(_) => Self::Artboard, _ => Self::General, } } @@ -750,6 +756,7 @@ impl MessageHandler Vec { outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::General)], properties: |_document_node, _node_id, _context| node_properties::string_properties("The Monitor node stores the value of its last evaluation"), }, + DocumentNodeType { + name: "Layer", + category: "General", + identifier: NodeImplementation::proto("graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>"), + inputs: vec![ + DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true), + DocumentInputType::value("Name", TaggedValue::String(String::new()), false), + DocumentInputType::value("Blend Mode", TaggedValue::BlendMode(BlendMode::Normal), false), + DocumentInputType::value("Opacity", TaggedValue::F32(100.), false), + DocumentInputType::value("Visible", TaggedValue::Bool(true), false), + DocumentInputType::value("Locked", TaggedValue::Bool(false), false), + DocumentInputType::value("Collapsed", TaggedValue::Bool(false), false), + DocumentInputType::value("Stack", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true), + ], + outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::GraphicGroup)], + properties: node_properties::layer_properties, + }, + DocumentNodeType { + name: "Artboard", + category: "General", + identifier: NodeImplementation::proto("graphene_core::ConstructArtboardNode<_>"), + inputs: vec![ + DocumentInputType::value("Graphic Group", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true), + DocumentInputType::value("Bounds", TaggedValue::Optional2IVec2(None), false), + ], + outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::Artboard)], + properties: node_properties::artboard_properties, + }, DocumentNodeType { name: "Downres", category: "Ignore", 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 aa1705d3..3f6fb9e4 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 @@ -75,7 +75,6 @@ fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, na widgets } -#[cfg(feature = "gpu")] fn text_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Text, blank_assist); @@ -203,7 +202,7 @@ fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na WidgetHolder::unrelated_separator(), number_props .value(Some(x)) - .on_update(update_value(|x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, index)) + .on_update(update_value(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, index)) .widget_holder(), ]) } else if let NodeInput::Value { @@ -215,7 +214,19 @@ fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na WidgetHolder::unrelated_separator(), number_props .value(Some(x as f64)) - .on_update(update_value(|x: &NumberInput| TaggedValue::U32(x.value.unwrap() as u32), node_id, index)) + .on_update(update_value(move |x: &NumberInput| TaggedValue::U32((x.value.unwrap()) as u32), node_id, index)) + .widget_holder(), + ]) + } else if let NodeInput::Value { + tagged_value: TaggedValue::F32(x), + exposed: false, + } = document_node.inputs[index] + { + widgets.extend_from_slice(&[ + WidgetHolder::unrelated_separator(), + number_props + .value(Some(x as f64)) + .on_update(update_value(move |x: &NumberInput| TaggedValue::F32((x.value.unwrap()) as f32), node_id, index)) .widget_holder(), ]) } @@ -788,14 +799,8 @@ pub fn quantize_properties(document_node: &DocumentNode, node_id: NodeId, _conte pub fn exposure_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let exposure = number_widget(document_node, node_id, 1, "Exposure", NumberInput::default().min(-20.).max(20.), true); let offset = number_widget(document_node, node_id, 2, "Offset", NumberInput::default().min(-0.5).max(0.5), true); - let gamma_correction = number_widget( - document_node, - node_id, - 3, - "Gamma Correction", - NumberInput::default().min(0.01).max(9.99).mode_increment().increment_step(0.1), - true, - ); + let gamma_input = NumberInput::default().min(0.01).max(9.99).mode_increment().increment_step(0.1); + let gamma_correction = number_widget(document_node, node_id, 3, "Gamma Correction", gamma_input, true); vec![ LayoutGroup::Row { widgets: exposure }, @@ -1434,7 +1439,8 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte }; let blur_radius = { - let widgets = number_widget(document_node, node_id, mask_blur_index, "Mask Blur", NumberInput::default().unit(" px").min(0.).max(25.).int(), true); + let number_props = NumberInput::default().unit(" px").min(0.).max(25.).int(); + let widgets = number_widget(document_node, node_id, mask_blur_index, "Mask Blur", number_props, true); LayoutGroup::Row { widgets }.with_tooltip("Blur radius for the mask. Useful for softening sharp edges to blend the masked area with the rest of the image.") }; @@ -1590,3 +1596,25 @@ pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context: widgets } + +pub fn layer_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let name = text_widget(document_node, node_id, 1, "Name", true); + let blend_mode = blend_mode(document_node, node_id, 2, "Blend Mode", true); + let opacity = number_widget(document_node, node_id, 3, "Opacity", NumberInput::default().min(0.).max(100.).unit("%"), true); + let visible = bool_widget(document_node, node_id, 4, "Visible", true); + let locked = bool_widget(document_node, node_id, 5, "Locked", true); + let collapsed = bool_widget(document_node, node_id, 6, "Collapsed", true); + + vec![ + LayoutGroup::Row { widgets: name }, + blend_mode, + LayoutGroup::Row { widgets: opacity }, + LayoutGroup::Row { widgets: visible }, + LayoutGroup::Row { widgets: locked }, + LayoutGroup::Row { widgets: collapsed }, + ] +} +pub fn artboard_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let label = text_widget(document_node, node_id, 1, "Label", true); + vec![LayoutGroup::Row { widgets: label }] +} diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 4f6dc9b6..0a9669cb 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -286,7 +286,7 @@ impl NodeGraphExecutor { self.last_output_type.insert(layer_path.clone(), Some(concrete!(VectorData))); responses.add(Operation::SetLayerTransform { path: layer_path.clone(), transform }); responses.add(Operation::SetVectorData { path: layer_path, vector_data }); - } else { + } else if core::any::TypeId::of::>() == DynAny::type_id(boxed_node_graph_output.as_ref()) { // Attempt to downcast to an image frame let ImageFrame { image, transform } = dyn_any::downcast(boxed_node_graph_output).map(|image_frame| *image_frame)?; self.last_output_type.insert(layer_path.clone(), Some(concrete!(ImageFrame))); @@ -316,6 +316,14 @@ impl NodeGraphExecutor { }]; responses.add(FrontendMessage::UpdateImageData { document_id, image_data }); } + } else if core::any::TypeId::of::() == DynAny::type_id(boxed_node_graph_output.as_ref()) { + let artboard: graphene_core::Artboard = dyn_any::downcast(boxed_node_graph_output).map(|artboard| *artboard)?; + info!("{artboard:#?}"); + return Err(format!("Artboard (see console)")); + } else if core::any::TypeId::of::() == DynAny::type_id(boxed_node_graph_output.as_ref()) { + let graphic_group: graphene_core::GraphicGroup = dyn_any::downcast(boxed_node_graph_output).map(|graphic| *graphic)?; + info!("{graphic_group:#?}"); + return Err(format!("Graphic group (see console)")); } Ok(()) diff --git a/frontend/src/components/Editor.svelte b/frontend/src/components/Editor.svelte index 19da1b30..cd9248df 100644 --- a/frontend/src/components/Editor.svelte +++ b/frontend/src/components/Editor.svelte @@ -118,6 +118,10 @@ --color-data-vec2-dim: #71008d; --color-data-color: #70a898; --color-data-color-dim: #43645b; + --color-data-graphic: #e4bb72; + --color-data-graphic-dim: #8b7752; + --color-data-artboard: #70a898; + --color-data-artboard-dim: #3a6156; --color-none: white; --color-none-repeat: no-repeat; diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index 8c7b9efb..85447717 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -74,7 +74,7 @@ export class FrontendDocumentDetails extends DocumentDetails { readonly id!: bigint; } -export type FrontendGraphDataType = "general" | "raster" | "color" | "vector" | "number"; +export type FrontendGraphDataType = "general" | "raster" | "color" | "vector" | "vec2" | "graphic" | "artboard"; export class NodeGraphInput { readonly dataType!: FrontendGraphDataType; diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs new file mode 100644 index 00000000..e994b3a8 --- /dev/null +++ b/node-graph/gcore/src/graphic_element.rs @@ -0,0 +1,140 @@ +use crate::raster::{BlendMode, ImageFrame}; +use crate::vector::VectorData; +use crate::{Color, Node}; + +use dyn_any::{DynAny, StaticType}; + +use core::ops::{Deref, DerefMut}; +use glam::IVec2; +use node_macro::node_fn; + +/// A list of [`GraphicElement`]s +#[derive(Clone, Debug, Hash, PartialEq, DynAny, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct GraphicGroup(Vec); + +/// Internal data for a [`GraphicElement`]. Can be [`VectorData`], [`ImageFrame`], text, or a nested [`GraphicGroup`] +#[derive(Clone, Debug, Hash, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum GraphicElementData { + VectorShape(Box), + ImageFrame(ImageFrame), + Text(String), + GraphicGroup(GraphicGroup), + Artboard(Artboard), +} + +/// A named [`GraphicElementData`] with a blend mode, opacity, as well as visibility, locked, and collapsed states. +#[derive(Clone, Debug, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct GraphicElement { + pub name: String, + pub blend_mode: BlendMode, + /// In range 0..=1 + pub opacity: f32, + pub visible: bool, + pub locked: bool, + pub collapsed: bool, + pub graphic_element_data: GraphicElementData, +} + +/// Some [`ArtboardData`] with some optional clipping bounds that can be exported. +/// Similar to an Inkscape page: https://media.inkscape.org/media/doc/release_notes/1.2/Inkscape_1.2.html#Page_tool +#[derive(Clone, Debug, Hash, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Artboard { + pub graphic_group: GraphicGroup, + pub bounds: Option<[IVec2; 2]>, +} + +pub struct ConstructLayerNode { + name: Name, + blend_mode: BlendMode, + opacity: Opacity, + visible: Visible, + locked: Locked, + collapsed: Collapsed, + stack: Stack, +} + +#[node_fn(ConstructLayerNode)] +fn construct_layer>( + graphic_element_data: Data, + name: String, + blend_mode: BlendMode, + opacity: f32, + visible: bool, + locked: bool, + collapsed: bool, + mut stack: GraphicGroup, +) -> GraphicGroup { + stack.push(GraphicElement { + name, + blend_mode, + opacity: opacity / 100., + visible, + locked, + collapsed, + graphic_element_data: graphic_element_data.into(), + }); + stack +} + +pub struct ConstructArtboardNode { + bounds: Bounds, +} + +#[node_fn(ConstructArtboardNode)] +fn construct_artboard(graphic_group: GraphicGroup, bounds: Option<[IVec2; 2]>) -> Artboard { + Artboard { graphic_group, bounds } +} + +impl From> for GraphicElementData { + fn from(image_frame: ImageFrame) -> Self { + GraphicElementData::ImageFrame(image_frame) + } +} +impl From for GraphicElementData { + fn from(vector_data: VectorData) -> Self { + GraphicElementData::VectorShape(Box::new(vector_data)) + } +} +impl From for GraphicElementData { + fn from(graphic_group: GraphicGroup) -> Self { + GraphicElementData::GraphicGroup(graphic_group) + } +} + +impl From for GraphicElementData { + fn from(artboard: Artboard) -> Self { + GraphicElementData::Artboard(artboard) + } +} + +impl Deref for GraphicGroup { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for GraphicGroup { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl GraphicGroup { + pub const EMPTY: Self = Self(Vec::new()); +} + +impl core::hash::Hash for GraphicElement { + fn hash(&self, state: &mut H) { + self.name.hash(state); + self.blend_mode.hash(state); + self.opacity.to_bits().hash(state); + self.visible.hash(state); + self.locked.hash(state); + self.collapsed.hash(state); + self.graphic_element_data.hash(state); + } +} diff --git a/node-graph/gcore/src/lib.rs b/node-graph/gcore/src/lib.rs index 405150ad..b3e62a68 100644 --- a/node-graph/gcore/src/lib.rs +++ b/node-graph/gcore/src/lib.rs @@ -26,6 +26,10 @@ pub mod raster; #[cfg(feature = "alloc")] pub mod transform; +#[cfg(feature = "alloc")] +mod graphic_element; +#[cfg(feature = "alloc")] +pub use graphic_element::*; #[cfg(feature = "alloc")] pub mod vector; diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index 734986d3..ce1c7849 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -17,6 +17,15 @@ pub struct VectorData { pub mirror_angle: Vec, } +impl core::hash::Hash for VectorData { + fn hash(&self, state: &mut H) { + self.subpaths.hash(state); + self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)); + self.style.hash(state); + self.mirror_angle.hash(state); + } +} + impl VectorData { /// An empty subpath with no data, an identity transform, and a black fill. pub const fn empty() -> Self { diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 62a8b349..edd4aa94 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -505,6 +505,30 @@ impl NodeNetwork { network: self, } } + + pub fn is_acyclic(&self) -> bool { + let mut dependencies: HashMap> = HashMap::new(); + for (node_id, node) in &self.nodes { + dependencies.insert( + *node_id, + node.inputs + .iter() + .filter_map(|input| if let NodeInput::Node { node_id: ref_id, .. } = input { Some(*ref_id) } else { None }) + .collect(), + ); + } + while !dependencies.is_empty() { + let Some((&disconnected, _)) = dependencies.iter().find(|(_, l)| l.is_empty()) else { + error!("Dependencies {dependencies:?}"); + return false + }; + dependencies.remove(&disconnected); + for connections in dependencies.values_mut() { + connections.retain(|&id| id != disconnected); + } + } + true + } } /// Functions for compiling the network @@ -762,19 +786,16 @@ impl NodeNetwork { self.nodes.retain(|_, node| !matches!(node.implementation, DocumentNodeImplementation::Extract)); for (_, node) in &mut extraction_nodes { - match node.implementation { - DocumentNodeImplementation::Extract => { - assert_eq!(node.inputs.len(), 1); - let NodeInput::Node { node_id, output_index, lambda } = node.inputs.pop().unwrap() else { - panic!("Extract node has no input"); - }; - assert_eq!(output_index, 0); - assert!(lambda); - let input_node = self.nodes.get_mut(&node_id).unwrap(); - node.implementation = DocumentNodeImplementation::Unresolved("graphene_core::value::ValueNode".into()); - node.inputs = vec![NodeInput::value(TaggedValue::DocumentNode(input_node.clone()), false)]; - } - _ => (), + if let DocumentNodeImplementation::Extract = node.implementation { + assert_eq!(node.inputs.len(), 1); + let NodeInput::Node { node_id, output_index, lambda } = node.inputs.pop().unwrap() else { + panic!("Extract node has no input"); + }; + assert_eq!(output_index, 0); + assert!(lambda); + let input_node = self.nodes.get_mut(&node_id).unwrap(); + node.implementation = DocumentNodeImplementation::Unresolved("graphene_core::value::ValueNode".into()); + node.inputs = vec![NodeInput::value(TaggedValue::DocumentNode(input_node.clone()), false)]; } } self.nodes.extend(extraction_nodes); diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index baac1edf..bb85abc8 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -57,6 +57,9 @@ pub enum TaggedValue { Segments(Vec>), EditorApi(graphene_core::EditorApi<'static>), DocumentNode(DocumentNode), + GraphicGroup(graphene_core::GraphicGroup), + Artboard(graphene_core::Artboard), + Optional2IVec2(Option<[glam::IVec2; 2]>), } #[allow(clippy::derived_hash_with_manual_eq)] @@ -88,15 +91,8 @@ impl Hash for TaggedValue { Self::ImaginateMaskStartingFill(f) => f.hash(state), Self::ImaginateStatus(s) => s.hash(state), Self::LayerPath(p) => p.hash(state), - Self::ImageFrame(i) => { - i.image.hash(state); - i.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)) - } - Self::VectorData(vector_data) => { - vector_data.subpaths.hash(state); - vector_data.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)); - vector_data.style.hash(state); - } + Self::ImageFrame(i) => i.hash(state), + Self::VectorData(vector_data) => vector_data.hash(state), Self::Fill(fill) => fill.hash(state), Self::Stroke(stroke) => stroke.hash(state), Self::VecF32(vec_f32) => vec_f32.iter().for_each(|val| val.to_bits().hash(state)), @@ -131,6 +127,9 @@ impl Hash for TaggedValue { } Self::EditorApi(editor_api) => editor_api.hash(state), Self::DocumentNode(document_node) => document_node.hash(state), + Self::GraphicGroup(graphic_group) => graphic_group.hash(state), + Self::Artboard(artboard) => artboard.hash(state), + Self::Optional2IVec2(v) => v.hash(state), } } } @@ -180,6 +179,9 @@ impl<'a> TaggedValue { TaggedValue::Segments(x) => Box::new(x), TaggedValue::EditorApi(x) => Box::new(x), TaggedValue::DocumentNode(x) => Box::new(x), + TaggedValue::GraphicGroup(x) => Box::new(x), + TaggedValue::Artboard(x) => Box::new(x), + TaggedValue::Optional2IVec2(x) => Box::new(x), } } @@ -240,6 +242,9 @@ impl<'a> TaggedValue { TaggedValue::Segments(_) => concrete!(graphene_core::raster::IndexNode>>), TaggedValue::EditorApi(_) => concrete!(graphene_core::EditorApi), TaggedValue::DocumentNode(_) => concrete!(crate::document::DocumentNode), + TaggedValue::GraphicGroup(_) => concrete!(graphene_core::GraphicGroup), + TaggedValue::Artboard(_) => concrete!(graphene_core::Artboard), + TaggedValue::Optional2IVec2(_) => concrete!(Option<[glam::IVec2; 2]>), } } } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index a9f9c2c3..5a9e0164 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -532,6 +532,11 @@ fn node_registry() -> HashMap, input: graphene_core::EditorApi, params: [String, graphene_core::text::Font, f64]), register_node!(graphene_std::brush::VectorPointsNode, input: VectorData, params: []), register_node!(graphene_core::ExtractImageFrame, input: graphene_core::EditorApi, params: []), + register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: graphene_core::vector::VectorData, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]), + register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, 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: [Option<[glam::IVec2; 2]>]), ]; let mut map: HashMap> = HashMap::new(); for (id, c, types) in node_types.into_iter().flatten() {