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 e508cbd5bc..ea321efd4c 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 @@ -1,7 +1,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::prelude::*; -use glam::{IVec2, UVec2}; +use glam::UVec2; use graph_craft::document::NodeId; use graphene_std::Color; @@ -47,7 +47,10 @@ impl MessageHandler for NewDocumentDialogMessageHa // Finite canvas: create an artboard with the specified dimensions responses.add(GraphOperationMessage::NewArtboard { id: NodeId::new(), - artboard: graphene_std::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()), + location: glam::DVec2::ZERO, + dimensions: self.dimensions.as_dvec2(), + background: Color::WHITE, + clip: true, }); responses.add(NavigationMessage::CanvasPan { delta: self.dimensions.as_dvec2() }); } diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index 1b8f76217a..ec3e251add 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -7,6 +7,7 @@ use crate::messages::tool::tool_messages::tool_prelude::*; use glam::{Affine2, DAffine2, Vec2}; use graph_craft::document::NodeId; use graphene_std::Context; +use graphene_std::Graphic; use graphene_std::gradient::GradientStops; use graphene_std::memo::IORecord; use graphene_std::raster_types::{CPU, GPU, Raster}; @@ -14,7 +15,6 @@ use graphene_std::table::Table; use graphene_std::vector::Vector; use graphene_std::vector::style::{Fill, FillChoice}; use graphene_std::{AlphaBlending, Color}; -use graphene_std::{Artboard, Graphic}; use std::any::Any; use std::sync::Arc; @@ -182,7 +182,7 @@ fn generate_layout(introspected_data: &Arc, + Table>, Table, Table, Table>, @@ -300,18 +300,6 @@ impl TableRowLayout for Table { } } -impl TableRowLayout for Artboard { - fn type_name() -> &'static str { - "Artboard" - } - fn identifier(&self) -> String { - self.label.clone() - } - fn value_page(&self, data: &mut LayoutData) -> Vec { - self.content.value_page(data) - } -} - impl TableRowLayout for Graphic { fn type_name() -> &'static str { "Graphic" @@ -652,7 +640,7 @@ impl TableRowLayout for bool { "Bool".to_string() } fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { - TextLabel::new(self.to_string()).narrow(true).widget_instance() + CheckboxInput::new(*self).disabled(true).widget_instance() } fn value_page(&self, _data: &mut LayoutData) -> Vec { vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])] @@ -897,7 +885,7 @@ impl TableRowLayout for NodeId { macro_rules! known_table_row_types { ($apply:ident) => { $apply!( - Table, + Table>, Table, Table, Table>, @@ -926,7 +914,6 @@ macro_rules! known_table_row_types { Vector, Raster, Raster, - Artboard, Graphic, ); }; diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 72c48cdfc5..229fbf1d4e 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -28,7 +28,7 @@ use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys; use crate::messages::tool::tool_messages::tool_prelude::Key; use crate::messages::tool::utility_types::ToolType; use crate::node_graph_executor::NodeGraphExecutor; -use glam::{DAffine2, DVec2, IVec2}; +use glam::{DAffine2, DVec2}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork}; use graphene_std::math::quad::Quad; @@ -1396,11 +1396,11 @@ impl MessageHandler> for DocumentMes // When artboard_canvas is provided (SVG file-open flow), use the declared canvas origin and dimensions; // no content-shift Transform node needed since the SVG was already placed at its natural coordinates. let (artboard_location, artboard_dimensions, content_shift) = if let Some((origin, dimensions)) = artboard_canvas { - (origin, dimensions, DVec2::ZERO) + (origin.as_dvec2(), dimensions.as_dvec2(), DVec2::ZERO) } else { // No declared canvas (image or clipboard paste): derive location and dimensions from the content bounding box. - let location = if place_artboard_at_origin { IVec2::ZERO } else { bounds[0].round().as_ivec2() }; - (location, (bounds[1] - bounds[0]).round().as_ivec2(), -bounds[0].round()) + let location = if place_artboard_at_origin { DVec2::ZERO } else { bounds[0].round() }; + (location, (bounds[1] - bounds[0]).round(), -bounds[0].round()) }; // Create an artboard and set its dimensions to the bounding box size and location diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index 7c0d8bb8e1..f572c4c1e9 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -2,9 +2,8 @@ use super::utility_types::TransformIn; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::NodeTemplate; use crate::messages::prelude::*; -use glam::{DAffine2, IVec2}; +use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; -use graphene_std::Artboard; use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::color::Color; use graphene_std::raster::BlendMode; @@ -66,7 +65,10 @@ pub enum GraphOperationMessage { }, NewArtboard { id: NodeId, - artboard: Artboard, + location: DVec2, + dimensions: DVec2, + background: Color, + clip: bool, }, NewBitmapLayer { id: NodeId, @@ -119,8 +121,8 @@ pub enum GraphOperationMessage { }, ResizeArtboard { layer: LayerNodeIdentifier, - location: IVec2, - dimensions: IVec2, + location: DVec2, + dimensions: DVec2, }, RemoveArtboards, NewSvg { diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 489d5cce2c..701ca2b50e 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -108,11 +108,16 @@ impl MessageHandler> for network_interface.force_set_upstream_to_chain(&first_chain_node, &[]); } - GraphOperationMessage::NewArtboard { id, artboard } => { + GraphOperationMessage::NewArtboard { + id, + location, + dimensions, + background, + clip, + } => { let mut modify_inputs = ModifyInputsContext::new(network_interface, responses); - let artboard_location = artboard.location; - let artboard_layer = modify_inputs.create_artboard(id, artboard); + let artboard_layer = modify_inputs.create_artboard(id, location, dimensions, background, clip); network_interface.move_layer_to_stack(artboard_layer, LayerNodeIdentifier::ROOT_PARENT, 0, &[]); // If there is a non artboard feeding into the primary input of the artboard, move it to the secondary input @@ -138,7 +143,7 @@ impl MessageHandler> for // Apply a translation to prevent the content from shifting responses.add(GraphOperationMessage::TransformChange { layer, - transform: DAffine2::from_translation(-artboard_location.as_dvec2()), + transform: DAffine2::from_translation(-location), transform_in: TransformIn::Local, skip_rerender: true, }); diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 72d0cf3748..c6c2e4ea46 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -3,7 +3,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions: use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{self, InputConnector, NodeNetworkInterface, OutputConnector}; use crate::messages::prelude::*; -use glam::{DAffine2, IVec2}; +use glam::{DAffine2, DVec2}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; use graph_craft::{ProtoNodeIdentifier, concrete}; @@ -16,7 +16,7 @@ use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::Vector; use graphene_std::vector::style::{Fill, Stroke}; use graphene_std::vector::{PointId, VectorModification, VectorModificationType}; -use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration}; +use graphene_std::{Color, Graphic, NodeInputDecleration}; #[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] pub enum TransformIn { @@ -130,15 +130,15 @@ impl<'a> ModifyInputsContext<'a> { LayerNodeIdentifier::new(new_id, self.network_interface) } - /// Creates an artboard as the primary export for the document network - pub fn create_artboard(&mut self, new_id: NodeId, artboard: Artboard) -> LayerNodeIdentifier { + /// Creates an artboard as the primary export for the document network. + pub fn create_artboard(&mut self, new_id: NodeId, location: DVec2, dimensions: DVec2, background: Color, clip: bool) -> LayerNodeIdentifier { let artboard_node_template = resolve_network_node_type("Artboard").expect("Node").node_template_input_override([ Some(NodeInput::value(TaggedValue::Artboard(Default::default()), true)), Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)), - Some(NodeInput::value(TaggedValue::DVec2(artboard.location.into()), false)), - Some(NodeInput::value(TaggedValue::DVec2(artboard.dimensions.into()), false)), - Some(NodeInput::value(TaggedValue::Color(Table::new_from_element(artboard.background)), false)), - Some(NodeInput::value(TaggedValue::Bool(artboard.clip), false)), + Some(NodeInput::value(TaggedValue::DVec2(location), false)), + Some(NodeInput::value(TaggedValue::DVec2(dimensions), false)), + Some(NodeInput::value(TaggedValue::Color(Table::new_from_element(background)), false)), + Some(NodeInput::value(TaggedValue::Bool(clip), false)), ]); self.network_interface.insert_node(new_id, artboard_node_template, &[]); LayerNodeIdentifier::new(new_id, self.network_interface) @@ -584,7 +584,7 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(InputConnector::node(brush_node_id, 1), NodeInput::value(TaggedValue::BrushStrokeTable(strokes_table), false), false); } - pub fn resize_artboard(&mut self, location: IVec2, dimensions: IVec2) { + pub fn resize_artboard(&mut self, location: DVec2, dimensions: DVec2) { let Some(artboard_node_id) = self.existing_network_node_id("Artboard", true) else { return; }; @@ -592,16 +592,16 @@ impl<'a> ModifyInputsContext<'a> { let mut dimensions = dimensions; let mut location = location; - if dimensions.x < 0 { - dimensions.x *= -1; + if dimensions.x < 0. { + dimensions.x = -dimensions.x; location.x -= dimensions.x; } - if dimensions.y < 0 { - dimensions.y *= -1; + if dimensions.y < 0. { + dimensions.y = -dimensions.y; location.y -= dimensions.y; } - self.set_input_with_refresh(InputConnector::node(artboard_node_id, 2), NodeInput::value(TaggedValue::DVec2(location.into()), false), false); - self.set_input_with_refresh(InputConnector::node(artboard_node_id, 3), NodeInput::value(TaggedValue::DVec2(dimensions.into()), false), false); + self.set_input_with_refresh(InputConnector::node(artboard_node_id, 2), NodeInput::value(TaggedValue::DVec2(location), false), false); + self.set_input_with_refresh(InputConnector::node(artboard_node_id, 3), NodeInput::value(TaggedValue::DVec2(dimensions), false), false); } /// Set the input, refresh the Properties panel, and run the document graph if skip_rerender is false diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 6115091837..a607e32b23 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -354,13 +354,11 @@ fn document_node_definitions() -> HashMap HashMap))), 0), + NodeInput::import(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(Table>))), 0), NodeInput::node(NodeId(3), 0), ], implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER), diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 81142d9b8b..0ba6ceb542 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -4571,32 +4571,7 @@ impl NodeNetworkInterface { return; } - node_metadata.persistent_metadata.display_name.clone_from(&display_name); - - // Keep the alias in sync with the `ToArtboard` name input - if self - .reference(node_id, network_path) - .is_some_and(|reference| reference == DefinitionIdentifier::Network("Artboard".into())) - { - let Some(nested_network) = self.network_mut(network_path) else { - return; - }; - let Some(artboard_node) = nested_network.nodes.get_mut(node_id) else { - return; - }; - let DocumentNodeImplementation::Network(network) = &mut artboard_node.implementation else { - return; - }; - // Keep this in sync with the definition - let Some(to_artboard) = network.nodes.get_mut(&NodeId(0)) else { - return; - }; - - let label_index = 1; - let label = if !display_name.is_empty() { display_name } else { "Artboard".to_string() }; - let label_input = NodeInput::value(TaggedValue::String(label), false); - to_artboard.inputs[label_index] = label_input; - } + node_metadata.persistent_metadata.display_name = display_name; self.transaction_modified(); self.try_unload_layer_width(node_id, network_path); diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 10de491e9d..d31a2c21f5 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1313,14 +1313,6 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[3].clone(), network_path); } - // Upgrade artboard name being passed as hidden value input to "Create Artboard" - if reference == DefinitionIdentifier::Network("Artboard".into()) && reset_node_definitions_on_open { - let label = document.network_interface.display_name(node_id, network_path); - document - .network_interface - .set_input(&InputConnector::node(NodeId(0), 1), NodeInput::value(TaggedValue::String(label), false), &[*node_id]); - } - if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER) && inputs_count == 1 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index dc404a1a22..b8e3c847a1 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1125,8 +1125,8 @@ impl MessageHandler> for Portfolio responses.add(GraphOperationMessage::ResizeArtboard { layer: item_id, - location: new_artboard_origin_doc.round().as_ivec2(), - dimensions: dimensions_doc.round().as_ivec2(), + location: new_artboard_origin_doc.round(), + dimensions: dimensions_doc.round(), }); } } else { diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index 6f83f1d075..6898a5ebee 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -11,9 +11,7 @@ use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint; use crate::messages::tool::common_functionality::snapping::SnapData; use crate::messages::tool::common_functionality::transformation_cage::*; use graph_craft::document::NodeId; -use graphene_std::Artboard; use graphene_std::renderer::{Quad, Rect}; -use graphene_std::table::Table; #[derive(Default, ExtractField)] pub struct ArtboardTool { @@ -113,7 +111,7 @@ struct ArtboardToolData { drag_current: DVec2, auto_panning: AutoPanning, snap_candidates: Vec, - dragging_current_artboard_location: glam::IVec2, + dragging_current_artboard_location: glam::DVec2, draw: Resize, } @@ -142,7 +140,7 @@ impl ArtboardToolData { fn start_resizing(&mut self, _selected_edges: (bool, bool, bool, bool), _document: &DocumentMessageHandler, _input: &InputPreprocessorMessageHandler) { if let Some(bounds) = &mut self.bounding_box_manager { bounds.center_of_transformation = bounds.transform.transform_point2((bounds.bounds[0] + bounds.bounds[1]) / 2.); - self.dragging_current_artboard_location = bounds.bounds[0].round().as_ivec2(); + self.dragging_current_artboard_location = bounds.bounds[0].round(); } } @@ -211,14 +209,14 @@ impl ArtboardToolData { responses.add(GraphOperationMessage::ResizeArtboard { layer: selected_artboard, - location: position.round().as_ivec2(), - dimensions: size.round().as_ivec2(), + location: position.round(), + dimensions: size.round(), }); - let translation = position.round().as_ivec2() - self.dragging_current_artboard_location; - self.dragging_current_artboard_location = position.round().as_ivec2(); + let translation = position.round() - self.dragging_current_artboard_location; + self.dragging_current_artboard_location = position.round(); for child in selected_artboard.children(document.metadata()) { - let local_translation = document.metadata().downstream_transform_to_document(child).inverse().transform_vector2(-translation.as_dvec2()); + let local_translation = document.metadata().downstream_transform_to_document(child).inverse().transform_vector2(-translation); responses.add(GraphOperationMessage::TransformChange { layer: child, transform: DAffine2::from_translation(local_translation), @@ -348,8 +346,8 @@ impl Fsm for ArtboardToolFsmState { } responses.add(GraphOperationMessage::ResizeArtboard { layer: tool_data.selected_artboard.unwrap(), - location: position.round().as_ivec2(), - dimensions: size.round().as_ivec2(), + location: position.round(), + dimensions: size.round(), }); // The second term is added to prevent the slow change in position due to rounding errors. @@ -379,8 +377,8 @@ impl Fsm for ArtboardToolFsmState { responses.add(GraphOperationMessage::ResizeArtboard { layer: artboard, - location: start.min(end).round().as_ivec2(), - dimensions: (start.round() - end.round()).abs().as_ivec2(), + location: start.min(end).round(), + dimensions: (start.round() - end.round()).abs(), }); } else { let id = NodeId::new(); @@ -389,14 +387,10 @@ impl Fsm for ArtboardToolFsmState { responses.add(GraphOperationMessage::NewArtboard { id, - artboard: Artboard { - content: Table::new(), - label: String::from("Artboard"), - location: start.min(end).round().as_ivec2(), - dimensions: (start.round() - end.round()).abs().as_ivec2(), - background: graphene_std::Color::WHITE, - clip: true, - }, + location: start.min(end).round(), + dimensions: (start.round() - end.round()).abs(), + background: graphene_std::Color::WHITE, + clip: true, }) } @@ -516,8 +510,8 @@ impl Fsm for ArtboardToolFsmState { responses.add(GraphOperationMessage::ResizeArtboard { layer: selected_artboard, - location: DVec2::new(existing_top_left.x + delta.x, existing_top_left.y + delta.y).round().as_ivec2(), - dimensions: (existing_bottom_right - existing_top_left).round().as_ivec2(), + location: DVec2::new(existing_top_left.x + delta.x, existing_top_left.y + delta.y).round(), + dimensions: (existing_bottom_right - existing_top_left).round(), }); return ArtboardToolFsmState::Ready { hovered }; @@ -563,8 +557,8 @@ impl Fsm for ArtboardToolFsmState { responses.add(GraphOperationMessage::ResizeArtboard { layer: selected_artboard, - location: position.round().as_ivec2(), - dimensions: new.transform_vector2(existing_bottom_right - existing_top_left).round().as_ivec2(), + location: position.round(), + dimensions: new.transform_vector2(existing_bottom_right - existing_top_left).round(), }); ArtboardToolFsmState::Ready { hovered } @@ -617,26 +611,27 @@ impl Fsm for ArtboardToolFsmState { #[cfg(test)] mod test_artboard { pub use crate::test_utils::test_prelude::*; + use graphene_std::Graphic; use graphene_std::table::Table; - async fn get_artboards(editor: &mut EditorTestUtils) -> Table { + async fn get_artboards(editor: &mut EditorTestUtils) -> Table> { let instrumented = match editor.eval_graph().await { Ok(instrumented) => instrumented, Err(e) => panic!("Failed to evaluate graph: {e}"), }; instrumented - .grab_all_input::>(&editor.runtime) + .grab_all_input::>>(&editor.runtime) .flatten() .collect() } #[derive(Debug, PartialEq)] struct ArtboardLayoutDocument { - position: IVec2, - dimensions: IVec2, + position: DVec2, + dimensions: DVec2, } impl ArtboardLayoutDocument { - pub fn new(position: impl Into, dimensions: impl Into) -> Self { + pub fn new(position: impl Into, dimensions: impl Into) -> Self { Self { position: position.into(), dimensions: dimensions.into(), @@ -649,8 +644,9 @@ mod test_artboard { let artboards = get_artboards(editor).await; let artboards = (0..artboards.len()) .map(|index| { - let element = artboards.element(index).unwrap(); - ArtboardLayoutDocument::new(element.location, element.dimensions) + let location: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_LOCATION, index); + let dimensions: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_DIMENSIONS, index); + ArtboardLayoutDocument::new(location, dimensions) }) .collect::>(); assert_eq!(artboards.len(), expected.len(), "incorrect len: actual {:?}, expected {:?}", artboards, expected); @@ -668,7 +664,7 @@ mod test_artboard { let mut editor = EditorTestUtils::create(); editor.new_document().await; editor.drag_tool(ToolType::Artboard, 10.1, 10.8, 19.9, 0.2, ModifierKeys::empty()).await; - has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((10, 0), (10, 11))]).await; + has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((10., 0.), (10., 11.))]).await; } #[tokio::test] @@ -678,7 +674,11 @@ mod test_artboard { editor.set_viewport_size(DVec2::splat(-1000.), DVec2::splat(1000.)).await; // Necessary for doing snapping since snaps outside of the viewport are discarded editor.drag_tool(ToolType::Artboard, 10., 10., 20., 20., ModifierKeys::empty()).await; editor.drag_tool(ToolType::Artboard, 11., 50., 19., 60., ModifierKeys::empty()).await; - has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((10, 10), (10, 10)), ArtboardLayoutDocument::new((10, 50), (10, 10))]).await; + has_artboards( + &mut editor, + vec![ArtboardLayoutDocument::new((10., 10.), (10., 10.)), ArtboardLayoutDocument::new((10., 50.), (10., 10.))], + ) + .await; } #[tokio::test] @@ -686,7 +686,7 @@ mod test_artboard { let mut editor = EditorTestUtils::create(); editor.new_document().await; editor.drag_tool(ToolType::Artboard, 10., 10., -10., 11., ModifierKeys::SHIFT).await; - has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((-10, 10), (20, 20))]).await; + has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((-10., 10.), (20., 20.))]).await; } #[tokio::test] @@ -703,7 +703,7 @@ mod test_artboard { editor.drag_tool(ToolType::Artboard, 0., 0., 0., 10., ModifierKeys::SHIFT).await; let desired_size = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * 10.); - has_artboards(&mut editor, vec![ArtboardLayoutDocument::new(IVec2::new(0, 0), desired_size.round().as_ivec2())]).await; + has_artboards(&mut editor, vec![ArtboardLayoutDocument::new(DVec2::new(0., 0.), desired_size.round())]).await; } #[tokio::test] @@ -719,8 +719,8 @@ mod test_artboard { .await; // Viewport coordinates editor.drag_tool(ToolType::Artboard, 0., 0., 0., 10., ModifierKeys::SHIFT | ModifierKeys::ALT).await; - let desired_location = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * -10.).as_ivec2(); - let desired_size = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * 20.).as_ivec2(); + let desired_location = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * -10.).round(); + let desired_size = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * 20.).round(); has_artboards(&mut editor, vec![ArtboardLayoutDocument::new(desired_location, desired_size)]).await; } @@ -752,7 +752,7 @@ mod test_artboard { editor.drag_tool(ToolType::Artboard, 10., 10., 20., 22., ModifierKeys::empty()).await; // Artboard to drag editor.drag_tool(ToolType::Artboard, 15., 15., 65., 65., ModifierKeys::empty()).await; // Drag from the middle by (50,50) - has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((60, 60), (10, 12))]).await; + has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((60., 60.), (10., 12.))]).await; } #[tokio::test] async fn artboard_move_snapping() { @@ -763,7 +763,11 @@ mod test_artboard { editor.drag_tool(ToolType::Artboard, 70., 0., 80., 100., ModifierKeys::empty()).await; // Artboard to snap to editor.drag_tool(ToolType::Artboard, 15., 15., 15. + 49., 15., ModifierKeys::empty()).await; // Drag the artboard so it should snap to the edge - has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((60, 10), (10, 12)), ArtboardLayoutDocument::new((70, 0), (10, 100))]).await; + has_artboards( + &mut editor, + vec![ArtboardLayoutDocument::new((60., 10.), (10., 12.)), ArtboardLayoutDocument::new((70., 0.), (10., 100.))], + ) + .await; } #[tokio::test] @@ -777,7 +781,7 @@ mod test_artboard { // Put the artboard in editor.drag_tool(ToolType::Artboard, 5., 5., 30., 10., ModifierKeys::empty()).await; - has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((5, 5), (25, 5))]).await; + has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((5., 5.), (25., 5.))]).await; let document = editor.active_document(); // artboard diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 5a08a22b79..0766ffadce 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -20,7 +20,7 @@ use graphene_std::text::FontCache; use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; use graphene_std::vector::style::RenderMode; -use graphene_std::{Artboard, Context, Graphic}; +use graphene_std::{Context, Graphic}; use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::util::wrap_network_in_scope; use spin::Mutex; @@ -439,7 +439,7 @@ impl NodeRuntime { } } // Artboard table: thumbnail - else if let Some(io) = introspected_data.downcast_ref::>>() { + else if let Some(io) = introspected_data.downcast_ref::>>>() { if update_thumbnails { Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses) } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index c72a62358f..503591010e 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -14,7 +14,7 @@ pub use glam::{DAffine2, DVec2, IVec2, UVec2}; use graphic_types::raster_types::{CPU, Image, Raster}; use graphic_types::vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke}; use graphic_types::vector_types::vector::{self, ReferencePoint}; -use graphic_types::{Artboard, Graphic, Vector}; +use graphic_types::{Graphic, Vector}; use raster_nodes::curve::Curve; use rendering::RenderMetadata; use std::fmt::Display; @@ -184,7 +184,7 @@ tagged_value! { Graphic(Table), #[serde(deserialize_with = "graphic_types::artboard::migrate_artboard")] // TODO: Eventually remove this migration document upgrade code #[serde(alias = "ArtboardGroup")] - Artboard(Table), + Artboard(Table>), #[serde(deserialize_with = "core_types::misc::migrate_color")] // TODO: Eventually remove this migration document upgrade code #[serde(alias = "ColorTable", alias = "OptionalColor", alias = "ColorNotInTable")] Color(Table), diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 9af0bc21e3..501521c20c 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -21,7 +21,7 @@ use graphene_std::table::Table; use graphene_std::transform::Footprint; use graphene_std::uuid::NodeId; use graphene_std::vector::Vector; -use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future}; +use graphene_std::{Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future}; use node_registry_macros::{async_node, convert_node, into_node}; use std::collections::HashMap; #[cfg(feature = "gpu")] @@ -63,7 +63,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => ()]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table>]), @@ -145,7 +145,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => ()]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => bool]), - async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table>]), diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 15fdea3d9b..11b31f32fc 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -33,7 +33,9 @@ pub use num_traits; use std::any::TypeId; use std::future::Future; use std::pin::Pin; -pub use table::{ATTR_ALPHA_BLENDING, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_END, ATTR_NAME, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE}; +pub use table::{ + ATTR_ALPHA_BLENDING, ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_END, ATTR_LOCATION, ATTR_NAME, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE, +}; #[cfg(feature = "wasm")] pub use tsify; pub use types::Cow; diff --git a/node-graph/libraries/core-types/src/table.rs b/node-graph/libraries/core-types/src/table.rs index 8041cda0ea..8bca5d5689 100644 --- a/node-graph/libraries/core-types/src/table.rs +++ b/node-graph/libraries/core-types/src/table.rs @@ -44,6 +44,18 @@ pub const ATTR_NAME: &str = "name"; /// `json_query_all` text node alongside each extracted value. pub const ATTR_TYPE: &str = "type"; +/// Attribute key for an artboard row's `DVec2` top-left corner location in document coordinates. +pub const ATTR_LOCATION: &str = "location"; + +/// Attribute key for an artboard row's `DVec2` width and height. +pub const ATTR_DIMENSIONS: &str = "dimensions"; + +/// Attribute key for an artboard row's `Color` background fill. +pub const ATTR_BACKGROUND: &str = "background"; + +/// Attribute key for an artboard row's `bool` flag indicating whether content is clipped to the artboard bounds. +pub const ATTR_CLIP: &str = "clip"; + // ===================== // TRAIT: AttributeValue // ===================== diff --git a/node-graph/libraries/core-types/src/types.rs b/node-graph/libraries/core-types/src/types.rs index 1230ce0ded..14852cf063 100644 --- a/node-graph/libraries/core-types/src/types.rs +++ b/node-graph/libraries/core-types/src/types.rs @@ -153,41 +153,12 @@ impl Display for ProtoNodeIdentifier { } } -fn migrate_type_descriptor_names<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result, D::Error> { - use serde::Deserialize; - - let name = String::deserialize(deserializer)?; - let name = match name.as_str() { - "f32" => "f64".to_string(), - "grahpene_core::transform::Footprint" => "std::option::Option>".to_string(), - "grahpene_core::graphic_element::GraphicGroup" => "grahpene_core::table::Table".to_string(), - "grahpene_core::raster::image::ImageFrame" - | "grahpene_core::raster::image::ImageFrame" - | "grahpene_core::instances::Instances>" - | "grahpene_core::instances::Instances>" - | "grahpene_core::instances::Instances>" => { - "grahpene_core::table::Table>".to_string() - } - "grahpene_core::vector::vector_data::VectorData" - | "grahpene_core::instances::Instances" - | "grahpene_core::table::Table" - | "grahpene_core::table::Table" => "grahpene_core::table::Table".to_string(), - "grahpene_core::instances::Instances" => "grahpene_core::table::Table".to_string(), - "grahpene_core::vector::vector_data::modification::VectorModification" => "grahpene_core::vector::vector_modification::VectorModification".to_string(), - "grahpene_core::table::Table" => "grahpene_core::table::Table".to_string(), - _ => name, - }; - - Ok(Cow::Owned(name)) -} - #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(Clone, Debug, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TypeDescriptor { #[cfg_attr(feature = "serde", serde(skip))] pub id: Option, - #[cfg_attr(feature = "serde", serde(deserialize_with = "migrate_type_descriptor_names"))] pub name: Cow<'static, str>, #[cfg_attr(feature = "serde", serde(default))] pub alias: Option>, diff --git a/node-graph/libraries/graphic-types/src/artboard.rs b/node-graph/libraries/graphic-types/src/artboard.rs index ab2ad81c05..b8dc271731 100644 --- a/node-graph/libraries/graphic-types/src/artboard.rs +++ b/node-graph/libraries/graphic-types/src/artboard.rs @@ -1,107 +1,45 @@ use crate::graphic::Graphic; use core_types::blending::AlphaBlending; -use core_types::bounds::{BoundingBox, RenderBoundingBox}; -use core_types::math::quad::Quad; -use core_types::render_complexity::RenderComplexity; use core_types::table::{Table, TableRow}; -use core_types::transform::Transform; use core_types::uuid::NodeId; -use core_types::{ATTR_TRANSFORM, Color}; +use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, Color}; use dyn_any::DynAny; -use glam::{DAffine2, DVec2, IVec2}; -use graphene_hash::CacheHash; - -/// Some [`ArtboardData`] with some optional clipping bounds that can be exported. -#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Artboard { - pub content: Table, - pub label: String, - pub location: IVec2, - pub dimensions: IVec2, - pub background: Color, - pub clip: bool, -} - -impl Default for Artboard { - fn default() -> Self { - Self::new(IVec2::ZERO, IVec2::new(1920, 1080)) - } -} - -impl Artboard { - pub fn new(location: IVec2, dimensions: IVec2) -> Self { - Self { - content: Table::new(), - label: "Artboard".to_string(), - location: location.min(location + dimensions), - dimensions: dimensions.abs(), - background: Color::WHITE, - clip: true, - } - } -} - -impl BoundingBox for Artboard { - fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { - let artboard_bounds = || (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box(); - - if self.clip { - return RenderBoundingBox::Rectangle(artboard_bounds()); - } - - let mut combined_bounds = None; - - for (element, row_transform) in self.content.iter_element_values().zip(self.content.iter_attribute_values_or_default::(ATTR_TRANSFORM)) { - match element.bounding_box(transform * row_transform, include_stroke) { - RenderBoundingBox::None => continue, - RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite, - RenderBoundingBox::Rectangle(bounds) => match combined_bounds { - Some(existing) => combined_bounds = Some(Quad::combine_bounds(existing, bounds)), - None => combined_bounds = Some(bounds), - }, - } - } - - match combined_bounds { - Some(content_bounds) => RenderBoundingBox::Rectangle(Quad::combine_bounds(content_bounds, artboard_bounds())), - None => RenderBoundingBox::Rectangle(artboard_bounds()), - } - } -} - -impl RenderComplexity for Artboard { - fn render_complexity(&self) -> usize { - self.content.render_complexity() - } -} - -// Implementations for Artboard -impl Transform for Artboard { - fn transform(&self) -> DAffine2 { - DAffine2::from_translation(self.location.as_dvec2()) - } - fn local_pivot(&self, pivot: DVec2) -> DVec2 { - self.location.as_dvec2() + self.dimensions.as_dvec2() * pivot - } -} +use glam::{DAffine2, IVec2}; + +// An artboard table is `Table>`: each row's element is the artboard's content +// (a `Table`), with the artboard's metadata stored alongside on the row as attributes +// (see `ATTR_LOCATION`, `ATTR_DIMENSIONS`, `ATTR_BACKGROUND`, `ATTR_CLIP`). +// +// The artboard's user-visible name is the parent layer's display name (resolved live from the +// network interface via the row's `ATTR_EDITOR_LAYER_PATH` attribute) — not stored here, so it +// can never go stale. +// +// These metadata attributes are populated at runtime by the `Artboard` proto node from its +// inputs and therefore aren't persisted in document files; the proto node's input values are +// what get serialized. // TODO: Eventually remove this migration document upgrade code -pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result, D::Error> { +pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result>, D::Error> { use serde::Deserialize; - #[derive(Clone, Default, Debug, PartialEq, DynAny)] + /// Pre-migration shape of the artboard's stored data: the struct that used to live as the element + /// of `Table`. Kept as a private type so we can deserialize legacy documents into the new + /// `Table>` (element = `content`, other fields → row attributes). + #[derive(Clone, Debug, DynAny)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct ArtboardGroup { - pub artboards: Vec<(Artboard, Option)>, + pub struct LegacyArtboard { + pub content: Table, + pub label: String, + pub location: IVec2, + pub dimensions: IVec2, + pub background: Color, + pub clip: bool, } + #[derive(Clone, Default, Debug, DynAny)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - #[cfg_attr(feature = "serde", serde(untagged))] - enum ArtboardFormat { - ArtboardGroup(ArtboardGroup), - OldArtboardTable(OldTable), - ArtboardTable(Table), + pub struct LegacyArtboardGroup { + pub artboards: Vec<(LegacyArtboard, Option)>, } #[derive(Clone, Debug)] @@ -113,13 +51,30 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re alpha_blending: Vec, } - // Attributes (transform, alpha_blending, editor:layer_path) are not serialized, so migration only needs - // to recover the elements. Per-item attribute values are populated at runtime by the node graph. + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serde", serde(untagged))] + enum ArtboardFormat { + ArtboardGroup(LegacyArtboardGroup), + OldArtboardTable(OldTable), + LegacyArtboardTable(Table), + // Note: this variant must come last so older formats above are tried first; an empty + // `Table>` would otherwise match (since `Table` has the same shell across `T`). + ArtboardTable(Table>), + } + + fn legacy_to_row(legacy: LegacyArtboard) -> TableRow> { + // Legacy `label` field is dropped — the artboard's name now comes from its parent layer's display name. + TableRow::new_from_element(legacy.content) + .with_attribute(ATTR_LOCATION, legacy.location.as_dvec2()) + .with_attribute(ATTR_DIMENSIONS, legacy.dimensions.as_dvec2()) + .with_attribute(ATTR_BACKGROUND, legacy.background) + .with_attribute(ATTR_CLIP, legacy.clip) + } + Ok(match ArtboardFormat::deserialize(deserializer)? { - ArtboardFormat::ArtboardGroup(artboard_group) => artboard_group.artboards.into_iter().map(|(artboard, _)| TableRow::new_from_element(artboard)).collect(), - ArtboardFormat::OldArtboardTable(old_table) => old_table.element.into_iter().map(TableRow::new_from_element).collect(), + ArtboardFormat::ArtboardGroup(group) => group.artboards.into_iter().map(|(artboard, _)| legacy_to_row(artboard)).collect(), + ArtboardFormat::OldArtboardTable(old_table) => old_table.element.into_iter().map(legacy_to_row).collect(), + ArtboardFormat::LegacyArtboardTable(legacy_table) => legacy_table.into_iter().map(|row| legacy_to_row(row.into_element())).collect(), ArtboardFormat::ArtboardTable(artboard_table) => artboard_table, }) } - -// Node definitions moved to graphic-nodes crate diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index 9335d41d2c..adeabd4b48 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -7,7 +7,6 @@ pub use raster_types; pub use vector_types; // Re-export commonly used types at the crate root -pub use artboard::Artboard; pub use graphic::{Graphic, IntoGraphicTable, TryFromGraphic, Vector}; pub mod migrations { diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 076f2e7e0f..db9372743e 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -8,19 +8,19 @@ use core_types::color::{Alpha, Color}; use core_types::math::quad::Quad; use core_types::render_complexity::RenderComplexity; use core_types::table::{Table, TableRow}; -use core_types::transform::{Footprint, Transform}; +use core_types::transform::Footprint; use core_types::uuid::{NodeId, generate_uuid}; -use core_types::{ATTR_ALPHA_BLENDING, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM}; +use core_types::{ATTR_ALPHA_BLENDING, ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_LOCATION, ATTR_TRANSFORM}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use graphene_hash::CacheHashWrapper; +use graphic_types::Graphic; use graphic_types::Vector; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster}; use graphic_types::vector_types::gradient::{GradientStops, GradientType}; use graphic_types::vector_types::subpath::Subpath; use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, Stroke, StrokeAlign}; -use graphic_types::{Artboard, Graphic}; use kurbo::{Affine, Cap, Join, Shape}; use num_traits::Zero; use std::collections::{HashMap, HashSet}; @@ -500,172 +500,172 @@ impl Render for Graphic { } } -impl Render for Artboard { +/// Reads the artboard metadata for the row at `index` from a `Table>` of artboards. +fn read_artboard_attributes(table: &Table>, index: usize) -> (DVec2, DVec2, Color, bool) { + let location: DVec2 = table.attribute_cloned_or_default(ATTR_LOCATION, index); + let dimensions: DVec2 = table.attribute_cloned_or_default(ATTR_DIMENSIONS, index); + let background: Color = table.attribute_cloned_or_default(ATTR_BACKGROUND, index); + let clip: bool = table.attribute_cloned_or_default(ATTR_CLIP, index); + (location, dimensions, background, clip) +} + +impl Render for Table> { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { - let x = self.location.x.min(self.location.x + self.dimensions.x); - let y = self.location.y.min(self.location.y + self.dimensions.y); - let width = self.dimensions.x.abs(); - let height = self.dimensions.y.abs(); - - // Rectangle for the artboard - if !render_params.hide_artboards { - // Transparency checkerboard behind the artboard background (viewport only) - let show_checkerboard = self.background.alpha() < 1. && render_params.to_canvas(); - if show_checkerboard && render_params.viewport_zoom > 0. { - let checker_id = format!("checkered-artboard-{}", generate_uuid()); - let cell_size = 8. / render_params.viewport_zoom; - let pattern_size = cell_size * 2.; - - // Anchor pattern at this artboard's top-left corner (x, y), not the document origin - let _ = write!( - &mut render.svg_defs, - r##""## - ); + for index in 0..self.len() { + let Some(content) = self.element(index) else { continue }; + let (location, dimensions, background, clip) = read_artboard_attributes(self, index); + + let x = location.x.min(location.x + dimensions.x); + let y = location.y.min(location.y + dimensions.y); + let width = dimensions.x.abs(); + let height = dimensions.y.abs(); + + // Rectangle for the artboard + if !render_params.hide_artboards { + // Transparency checkerboard behind the artboard background (viewport only) + let show_checkerboard = background.alpha() < 1. && render_params.to_canvas(); + if show_checkerboard && render_params.viewport_zoom > 0. { + let checker_id = format!("checkered-artboard-{}", generate_uuid()); + let cell_size = 8. / render_params.viewport_zoom; + let pattern_size = cell_size * 2.; + + // Anchor pattern at this artboard's top-left corner (x, y), not the document origin + let _ = write!( + &mut render.svg_defs, + r##""## + ); + render.leaf_tag("rect", |attributes| { + attributes.push("x", x.to_string()); + attributes.push("y", y.to_string()); + attributes.push("width", width.to_string()); + attributes.push("height", height.to_string()); + attributes.push("fill", format!("url(#{checker_id})")); + }); + } + + // Background render.leaf_tag("rect", |attributes| { + attributes.push("fill", format!("#{}", background.to_rgb_hex_srgb_from_gamma())); + if background.a() < 1. { + attributes.push("fill-opacity", ((background.a() * 1000.).round() / 1000.).to_string()); + } attributes.push("x", x.to_string()); attributes.push("y", y.to_string()); attributes.push("width", width.to_string()); attributes.push("height", height.to_string()); - attributes.push("fill", format!("url(#{checker_id})")); }); } - // Background - render.leaf_tag("rect", |attributes| { - attributes.push("fill", format!("#{}", self.background.to_rgb_hex_srgb_from_gamma())); - if self.background.a() < 1. { - attributes.push("fill-opacity", ((self.background.a() * 1000.).round() / 1000.).to_string()); - } - attributes.push("x", x.to_string()); - attributes.push("y", y.to_string()); - attributes.push("width", width.to_string()); - attributes.push("height", height.to_string()); - }); - } - - // Artwork - render.parent_tag( - // SVG group tag - "g", - // Group tag attributes - |attributes| { - let matrix = format_transform_matrix(self.transform()); - if !matrix.is_empty() { - attributes.push(ATTR_TRANSFORM, matrix); - } + // Artwork + render.parent_tag( + // SVG group tag + "g", + // Group tag attributes + |attributes| { + let matrix = format_transform_matrix(DAffine2::from_translation(location)); + if !matrix.is_empty() { + attributes.push(ATTR_TRANSFORM, matrix); + } - if self.clip { - let id = format!("artboard-{}", generate_uuid()); - let selector = format!("url(#{id})"); + if clip { + let id = format!("artboard-{}", generate_uuid()); + let selector = format!("url(#{id})"); - write!( - &mut attributes.0.svg_defs, - r##""##, - self.dimensions.x, self.dimensions.y, - ) - .unwrap(); - attributes.push("clip-path", selector); - } - }, - // Artwork content - |render| { - let mut render_params = render_params.clone(); - render_params.artboard_background = Some(self.background); - self.content.render_svg(render, &render_params); - }, - ); + write!( + &mut attributes.0.svg_defs, + r##""##, + dimensions.x, dimensions.y, + ) + .unwrap(); + attributes.push("clip-path", selector); + } + }, + // Artwork content + |render| { + let mut render_params = render_params.clone(); + render_params.artboard_background = Some(background); + content.render_svg(render, &render_params); + }, + ); + } } fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { use vello::peniko; - let [a, b] = [self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()]; - let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)); - - // Render background - if !render_params.hide_artboards { - let artboard_transform = kurbo::Affine::new(transform.to_cols_array()); - - // Transparency checkerboard behind the artboard background (viewport only) - let show_checkerboard = self.background.alpha() < 1. && render_params.to_canvas(); - if show_checkerboard && render_params.viewport_zoom > 0. { - // Anchor pattern at THIS artboard's top-left corner - // brush_transform is an image placement transform: it maps brush pixel coords → shape coords - // scale(1/zoom) sets each brush pixel to 1/zoom document units (constant CSS size after viewport transform) - // then_translate places the brush origin at the artboard corner - let brush_transform = kurbo::Affine::scale(1. / render_params.viewport_zoom).then_translate(kurbo::Vec2::new(rect.x0, rect.y0)); - scene.fill(peniko::Fill::NonZero, artboard_transform, &checkerboard_brush(), Some(brush_transform), &rect); - } - - let color = peniko::Color::new([self.background.r(), self.background.g(), self.background.b(), self.background.a()]); - scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., artboard_transform, &rect); - scene.fill(peniko::Fill::NonZero, artboard_transform, color, None, &rect); - scene.pop_layer(); - } - - if self.clip { - scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), &rect); - } - - // Since the content's transform is right multiplied in when rendering the content, we just need to right multiply by the artboard offset here. - let child_transform = transform * DAffine2::from_translation(self.location.as_dvec2()); - let mut render_params = render_params.clone(); - render_params.artboard_background = Some(self.background); - self.content.render_to_vello(scene, child_transform, context, &render_params); - if self.clip { - scene.pop_layer(); - } - } + for index in 0..self.len() { + let Some(content) = self.element(index) else { continue }; + let (location, dimensions, background, clip) = read_artboard_attributes(self, index); + + let [a, b] = [location, location + dimensions]; + let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)); + + // Render background + if !render_params.hide_artboards { + let artboard_transform = kurbo::Affine::new(transform.to_cols_array()); + + // Transparency checkerboard behind the artboard background (viewport only) + let show_checkerboard = background.alpha() < 1. && render_params.to_canvas(); + if show_checkerboard && render_params.viewport_zoom > 0. { + // Anchor pattern at THIS artboard's top-left corner + // brush_transform is an image placement transform: it maps brush pixel coords → shape coords + // scale(1/zoom) sets each brush pixel to 1/zoom document units (constant CSS size after viewport transform) + // then_translate places the brush origin at the artboard corner + let brush_transform = kurbo::Affine::scale(1. / render_params.viewport_zoom).then_translate(kurbo::Vec2::new(rect.x0, rect.y0)); + scene.fill(peniko::Fill::NonZero, artboard_transform, &checkerboard_brush(), Some(brush_transform), &rect); + } - fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option) { - if let Some(element_id) = element_id { - let subpath = Subpath::new_rectangle(DVec2::ZERO, self.dimensions.as_dvec2()); - metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]); - metadata.upstream_footprints.insert(element_id, footprint); - metadata.local_transforms.insert(element_id, DAffine2::from_translation(self.location.as_dvec2())); - if self.clip { - metadata.clip_targets.insert(element_id); + let color = peniko::Color::new([background.r(), background.g(), background.b(), background.a()]); + scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., artboard_transform, &rect); + scene.fill(peniko::Fill::NonZero, artboard_transform, color, None, &rect); + scene.pop_layer(); } - } - footprint.transform *= self.transform(); - self.content.collect_metadata(metadata, footprint, None); - } - - fn add_upstream_click_targets(&self, click_targets: &mut Vec) { - let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, self.dimensions.as_dvec2()); - click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.)); - } - fn contains_artboard(&self) -> bool { - true - } -} - -impl Render for Table { - fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { - for element in self.iter_element_values() { - element.render_svg(render, render_params); - } - } + if clip { + scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), &rect); + } - fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { - for element in self.iter_element_values() { - element.render_to_vello(scene, transform, context, render_params); + // Since the content's transform is right multiplied in when rendering the content, we just need to right multiply by the artboard offset here. + let child_transform = transform * DAffine2::from_translation(location); + let mut render_params = render_params.clone(); + render_params.artboard_background = Some(background); + content.render_to_vello(scene, child_transform, context, &render_params); + if clip { + scene.pop_layer(); + } } } fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option) { for index in 0..self.len() { + let Some(content) = self.element(index) else { continue }; + let (location, dimensions, _background, clip) = read_artboard_attributes(self, index); + let layer_path: Table = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); - let layer = layer_path.iter_element_values().next_back().copied(); - self.element(index).unwrap().collect_metadata(metadata, footprint, layer); + let element_id = layer_path.iter_element_values().next_back().copied(); + + if let Some(element_id) = element_id { + let subpath = Subpath::new_rectangle(DVec2::ZERO, dimensions); + metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]); + metadata.upstream_footprints.insert(element_id, footprint); + metadata.local_transforms.insert(element_id, DAffine2::from_translation(location)); + if clip { + metadata.clip_targets.insert(element_id); + } + } + + let mut child_footprint = footprint; + child_footprint.transform *= DAffine2::from_translation(location); + content.collect_metadata(metadata, child_footprint, None); } } fn add_upstream_click_targets(&self, click_targets: &mut Vec) { - for element in self.iter_element_values() { - element.add_upstream_click_targets(click_targets); + for index in 0..self.len() { + let dimensions: DVec2 = self.attribute_cloned_or_default(ATTR_DIMENSIONS, index); + let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions); + click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.)); } } diff --git a/node-graph/nodes/gcore/src/animation.rs b/node-graph/nodes/gcore/src/animation.rs index a5011e7ca7..19d7d81e26 100644 --- a/node-graph/nodes/gcore/src/animation.rs +++ b/node-graph/nodes/gcore/src/animation.rs @@ -3,7 +3,7 @@ use core_types::transform::Footprint; use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::vector_types::GradientStops; -use graphic_types::{Artboard, Graphic, Vector}; +use graphic_types::{Graphic, Vector}; use raster_types::{CPU, GPU, Raster}; const DAY: f64 = 1000. * 3600. * 24.; @@ -78,7 +78,7 @@ async fn quantize_real_time( Context -> Table>, Context -> Table>, Context -> Table, - Context -> Table, + Context -> Table>, Context -> Table, Context -> Table, Context -> Table, @@ -118,7 +118,7 @@ async fn quantize_animation_time( Context -> Table>, Context -> Table>, Context -> Table, - Context -> Table, + Context -> Table>, Context -> Table, Context -> Table, Context -> Table, diff --git a/node-graph/nodes/gcore/src/context_modification.rs b/node-graph/nodes/gcore/src/context_modification.rs index 627525ea5e..dbe3f8ad2a 100644 --- a/node-graph/nodes/gcore/src/context_modification.rs +++ b/node-graph/nodes/gcore/src/context_modification.rs @@ -6,7 +6,7 @@ use core_types::uuid::NodeId; use core_types::{Color, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::vector_types::GradientStops; -use graphic_types::{Artboard, Graphic, Vector}; +use graphic_types::{Graphic, Vector}; use raster_types::{CPU, GPU, Raster}; /// Filters out what should be unused components of the context based on the specified requirements. @@ -35,7 +35,7 @@ async fn context_modification( Context -> Table>, Context -> Table>, Context -> Table, - Context -> Table, + Context -> Table>, Context -> Table, )] value: impl Node, Output = T>, diff --git a/node-graph/nodes/graphic/src/artboard.rs b/node-graph/nodes/graphic/src/artboard.rs index 2630ee8e6a..ef0d67deae 100644 --- a/node-graph/nodes/graphic/src/artboard.rs +++ b/node-graph/nodes/graphic/src/artboard.rs @@ -1,13 +1,19 @@ -use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl, table::Table, transform::TransformMut}; -use glam::{DAffine2, DVec2, IVec2}; +use core_types::{ + ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl, + table::{Table, TableRow}, + transform::TransformMut, +}; +use glam::{DAffine2, DVec2}; use graphic_types::{ - Artboard, Vector, + Vector, graphic::{Graphic, IntoGraphicTable}, }; use raster_types::{CPU, GPU, Raster}; use vector_types::GradientStops; -/// Constructs a new single-item `Table` with the chosen properties. +/// Constructs a new single-item `Table>` (an artboard table) where the row's element is +/// the artboard's content and the metadata (label, location, dimensions, background, clip) is stored as +/// per-row attributes. #[node_macro::node(category(""))] pub async fn create_artboard( ctx: impl ExtractAll + CloneVarArgs + Ctx, @@ -22,42 +28,40 @@ pub async fn create_artboard( Context -> DAffine2, )] content: impl Node, Output = T>, - /// Name of the artboard, shown in parts of the editor. - label: String, /// Coordinate of the top-left corner of the artboard within the document. location: DVec2, - /// Width and height of the artboard within the document. Only integers are valid. + /// Width and height of the artboard within the document. dimensions: DVec2, - /// Color of the artboard background. Only positive integers are valid. + /// Color of the artboard background. background: Table, /// Whether to cut off the contained content that extends outside the artboard, or keep it visible. #[default(true)] clip: bool, -) -> Table { - let location = location.as_ivec2(); - +) -> Table> { let footprint = ctx.try_footprint().copied(); let mut new_ctx = OwnedContextImpl::from(ctx); if let Some(mut footprint) = footprint { - footprint.translate(location.as_dvec2()); + footprint.translate(location); new_ctx = new_ctx.with_footprint(footprint); } let content = content.eval(new_ctx.into_context()).await.into_graphic_table(); - let dimensions = dimensions.as_ivec2().max(IVec2::ONE); - - let location = location.min(location + dimensions); - - let dimensions = dimensions.abs(); + // Normalize so `location` is the top-left corner and `dimensions` are positive (allowing negative input + // dimensions to represent dragging from the opposite corner). Compute the corner using the raw signed + // dimensions before clamping, otherwise negative inputs collapse to the original corner instead of inverting. + let normalized_location = location.min(location + dimensions); + let normalized_dimensions = dimensions.abs().max(DVec2::ONE); let background = background.element(0).copied().unwrap_or(Color::WHITE); - Table::new_from_element(Artboard { - content, - label, - location, - dimensions, - background, - clip, - }) + // The artboard's user-visible name is its parent layer's display name; not stored as an attribute here so + // it can't go stale. The data panel resolves it live from the row's `editor:layer_path` NodeId via the network + // interface, so renaming the layer reflects everywhere on the next refresh. + Table::new_from_row( + TableRow::new_from_element(content) + .with_attribute(ATTR_LOCATION, normalized_location) + .with_attribute(ATTR_DIMENSIONS, normalized_dimensions) + .with_attribute(ATTR_BACKGROUND, background) + .with_attribute(ATTR_CLIP, clip), + ) } diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 0830971fb5..ad4d2fa493 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -4,8 +4,8 @@ use core_types::table::{Table, TableRow}; use core_types::uuid::NodeId; use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, AnyHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; use glam::{DAffine2, DVec2}; +use graphic_types::Vector; use graphic_types::graphic::{Graphic, IntoGraphicTable}; -use graphic_types::{Artboard, Vector}; use raster_types::{CPU, GPU, Raster}; use vector_types::{GradientStop, GradientStops, ReferencePoint}; @@ -16,7 +16,7 @@ pub fn index_elements( _: impl Ctx, /// The list of data. #[implementations( - Table, + Table>, Table, Table, Table>, @@ -48,7 +48,7 @@ pub fn omit_element( /// The list of data. #[implementations( Table, - Table, + Table>, Table, Table, Table>, @@ -86,7 +86,7 @@ pub fn extract_element( Table, Table>, Table, - Table, + Table>, )] table: Table, /// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item. @@ -225,7 +225,7 @@ async fn write_attribute, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, @@ -262,11 +262,11 @@ async fn write_attribute( _: impl Ctx, /// The `Table` whose items will appear at the start of the extended `Table`. - #[implementations(Table, Table, Table, Table>, Table>, Table, Table)] + #[implementations(Table>, Table, Table, Table>, Table>, Table, Table)] base: Table, /// The `Table` whose items will appear at the end of the extended `Table`. #[expose] - #[implementations(Table, Table, Table, Table>, Table>, Table, Table)] + #[implementations(Table>, Table, Table, Table>, Table>, Table, Table)] new: Table, ) -> Table { let mut base = base; @@ -281,9 +281,9 @@ pub async fn extend( #[node_macro::node(category(""))] pub async fn legacy_layer_extend( _: impl Ctx, - #[implementations(Table, Table, Table, Table>, Table>, Table, Table)] base: Table, + #[implementations(Table>, Table, Table, Table>, Table>, Table, Table)] base: Table, #[expose] - #[implementations(Table, Table, Table, Table>, Table>, Table, Table)] + #[implementations(Table>, Table, Table, Table>, Table>, Table, Table)] new: Table, nested_node_path: Table, ) -> Table { diff --git a/node-graph/nodes/gstd/src/lib.rs b/node-graph/nodes/gstd/src/lib.rs index 7f1f059117..539a34359a 100644 --- a/node-graph/nodes/gstd/src/lib.rs +++ b/node-graph/nodes/gstd/src/lib.rs @@ -11,7 +11,7 @@ pub use graphene_application_io as application_io; pub use graphene_core; pub use graphene_core::debug; pub use graphic_nodes; -pub use graphic_types::{Artboard, Graphic, Vector}; +pub use graphic_types::{Graphic, Vector}; pub use math_nodes; pub use path_bool_nodes; pub use raster_nodes; @@ -44,7 +44,6 @@ pub mod vector { pub mod graphic { pub use graphic_nodes::graphic::*; - pub use graphic_types::Artboard; pub use graphic_types::graphic::*; } diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 7d8bcd9387..13ea0113c0 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -9,7 +9,7 @@ pub use graph_craft::document::value::RenderOutputType; use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig}; use graphic_types::raster_types::Image; use graphic_types::raster_types::{CPU, Raster}; -use graphic_types::{Artboard, Graphic, Vector}; +use graphic_types::{Graphic, Vector}; use rendering::{Render, RenderOutputType as RenderOutputTypeRequest, RenderParams, RenderSvgSegmentList, SvgRender, checkerboard_brush}; use rendering::{RenderMetadata, SvgSegment}; use std::collections::HashMap; @@ -39,7 +39,7 @@ pub struct RenderIntermediate { async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Sync>( ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs, #[implementations( - Context -> Table, + Context -> Table>, Context -> Table, Context -> Table, Context -> Table>, diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index fe1c4c7c3a..eea4e976c3 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -5,7 +5,7 @@ use core_types::transform::Footprint; use core_types::{Color, Ctx, num_traits}; use glam::{DAffine2, DVec2}; use graphic_types::raster_types::{CPU, GPU, Raster}; -use graphic_types::{Artboard, Graphic, Vector}; +use graphic_types::{Graphic, Vector}; use log::warn; use math_parser::ast; use math_parser::context::{EvalContext, NothingMap, ValueProvider}; @@ -753,7 +753,7 @@ async fn switch( Context -> u64, Context -> DVec2, Context -> DAffine2, - Context -> Table, + Context -> Table>, Context -> Table, Context -> Table, Context -> Table>, @@ -772,7 +772,7 @@ async fn switch( Context -> u64, Context -> DVec2, Context -> DAffine2, - Context -> Table, + Context -> Table>, Context -> Table, Context -> Table, Context -> Table>,