diff --git a/Cargo.lock b/Cargo.lock index 33df1cdfc7..9ca5753cf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4498,6 +4498,7 @@ dependencies = [ "usvg", "vector-types", "vello", + "vello_encoding", ] [[package]] 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 b767b60305..c4f358dc9a 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 @@ -11,9 +11,9 @@ use graphene_std::Context; use graphene_std::gradient::GradientStops; use graphene_std::memo::IORecord; use graphene_std::raster_types::{CPU, GPU, Raster}; -use graphene_std::table::Table; +use graphene_std::table::{CustomColumnValue, Table}; use graphene_std::vector::Vector; -use graphene_std::vector::style::{Fill, FillChoice}; +use graphene_std::vector::style::{Fill, FillChoice, GRADIENT_SPREAD_METHOD_KEY, GRADIENT_TYPE_KEY}; use graphene_std::{Artboard, Graphic}; use std::any::Any; use std::sync::Arc; @@ -204,6 +204,9 @@ trait TableRowLayout { fn element_page(&self, _data: &mut LayoutData) -> Vec { vec![] } + fn format_column_value(_key: &str, value: &CustomColumnValue) -> String { + format!("{value:?}") + } } impl TableRowLayout for Vec { @@ -258,11 +261,13 @@ impl TableRowLayout for Table { } } + let additional_keys = self.additional_column_keys(); + let mut rows = self .iter() .enumerate() .map(|(index, row)| { - vec![ + let mut data = vec![ TextLabel::new(format!("{index}")).narrow(true).widget_instance(), row.element.element_widget(index), TextLabel::new(format_transform_matrix(row.transform)).narrow(true).widget_instance(), @@ -270,11 +275,21 @@ impl TableRowLayout for Table { TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))) .narrow(true) .widget_instance(), - ] + ]; + + for key in &additional_keys { + let value = *(row.additional.get(key).unwrap_or(&&CustomColumnValue::None)); + let formatted_value = T::format_column_value(key, value); + log::debug!("formatted_value {}", formatted_value); + data.push(TextLabel::new(formatted_value).narrow(true).widget_instance()); + } + data }) .collect::>(); - rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"])); + let mut headings = vec!["", "element", "transform", "alpha_blending", "source_node_id"]; + headings.extend(additional_keys); + rows.insert(0, column_headings(headings.as_slice())); vec![LayoutGroup::table(rows, false)] } @@ -574,6 +589,16 @@ impl TableRowLayout for GradientStops { let widgets = vec![self.element_widget(0)]; vec![LayoutGroup::row(widgets)] } + fn format_column_value(key: &str, value: &CustomColumnValue) -> String { + match (key, value) { + (GRADIENT_TYPE_KEY, CustomColumnValue::U32(0)) => "Linear".to_string(), + (GRADIENT_TYPE_KEY, CustomColumnValue::U32(1)) => "Radial".to_string(), + (GRADIENT_SPREAD_METHOD_KEY, CustomColumnValue::U32(0)) => "Pad".to_string(), + (GRADIENT_SPREAD_METHOD_KEY, CustomColumnValue::U32(1)) => "Reflect".to_string(), + (GRADIENT_SPREAD_METHOD_KEY, CustomColumnValue::U32(2)) => "Repeat".to_string(), + _ => format!("{value:?}"), + } + } } impl TableRowLayout for f64 { 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 a0828ba63a..7a31c0fed1 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 @@ -10,9 +10,8 @@ use graphene_std::raster_types::{CPU, Raster}; use graphene_std::subpath::Subpath; use graphene_std::table::Table; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::PointId; -use graphene_std::vector::VectorModificationType; -use graphene_std::vector::style::{Fill, Stroke}; +use graphene_std::vector::style::{Fill, GradientSpreadMethod, Stroke}; +use graphene_std::vector::{GradientStops, GradientType, PointId, VectorModificationType}; use graphene_std::{Artboard, Color}; #[impl_message(Message, DocumentMessage, GraphOperation)] @@ -26,6 +25,13 @@ pub enum GraphOperationMessage { layer: LayerNodeIdentifier, fill: f64, }, + GradientTableSet { + layer: LayerNodeIdentifier, + stops: GradientStops, + transform: DAffine2, + gradient_type: GradientType, + spread_method: GradientSpreadMethod, + }, OpacitySet { layer: LayerNodeIdentifier, opacity: f64, 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 befb069156..ab76bfcff9 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 @@ -45,6 +45,17 @@ impl MessageHandler> for modify_inputs.blending_fill_set(fill); } } + GraphOperationMessage::GradientTableSet { + layer, + stops, + transform, + gradient_type, + spread_method, + } => { + if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { + modify_inputs.gradient_table_set(stops, transform, gradient_type, spread_method); + } + } GraphOperationMessage::OpacitySet { layer, opacity } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { modify_inputs.opacity_set(opacity); 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 e64f8ed625..d58bd63392 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -11,11 +11,10 @@ use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::raster::BlendMode; use graphene_std::raster_types::{CPU, Raster}; use graphene_std::subpath::Subpath; -use graphene_std::table::Table; +use graphene_std::table::{Table, TableRow}; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::Vector; -use graphene_std::vector::style::{Fill, Stroke}; -use graphene_std::vector::{PointId, VectorModificationType}; +use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientTableRowExt, Stroke}; +use graphene_std::vector::{GradientStops, GradientType, PointId, Vector, VectorModificationType}; use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration}; #[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] @@ -457,6 +456,16 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(fill * 100.), false), false); } + pub fn gradient_table_set(&mut self, stops: GradientStops, transform: DAffine2, gradient_type: GradientType, spread_method: GradientSpreadMethod) { + let Some(gradient_node_id) = self.existing_proto_node_id(graphene_std::math_nodes::gradient_value::IDENTIFIER, true) else { + return; + }; + + let table = Table::new_from_row(TableRow::new_gradient_row(stops, transform, gradient_type, spread_method)); + let input_connector = InputConnector::node(gradient_node_id, 1); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientTable(table), false), false); + } + pub fn clip_mode_toggle(&mut self, clip_mode: Option) { let clip = !clip_mode.unwrap_or(false); let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blending::IDENTIFIER, true) else { diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 788f6e5e8b..517b0280da 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1153,20 +1153,37 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button: .on_commit(commit_value) .widget_instance(), ), - TaggedValue::GradientTable(gradient_table) => widgets.push( - color_button - .value(match gradient_table.iter().next() { - Some(row) => FillChoice::Gradient(row.element.clone()), - None => FillChoice::Gradient(GradientStops::default()), - }) - .on_update(update_value( - |input: &ColorInput| TaggedValue::GradientTable(input.value.as_gradient().iter().map(|&gradient| TableRow::new_from_element(gradient.clone())).collect()), - node_id, - index, - )) - .on_commit(commit_value) - .widget_instance(), - ), + TaggedValue::GradientTable(gradient_table) => { + let existing_transform = gradient_table.iter().next().map(|row| *row.transform).unwrap_or_default(); + + widgets.push( + color_button + .value(match gradient_table.iter().next() { + Some(row) => FillChoice::Gradient(row.element.clone()), + None => FillChoice::Gradient(GradientStops::default()), + }) + .on_update(update_value( + move |input: &ColorInput| { + TaggedValue::GradientTable( + input + .value + .as_gradient() + .iter() + .map(|&gradient| TableRow { + element: gradient.clone(), + transform: existing_transform, + ..Default::default() + }) + .collect(), + ) + }, + node_id, + index, + )) + .on_commit(commit_value) + .widget_instance(), + ) + } x => warn!("Color {x:?}"), } diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 51ec764b0c..1184223cff 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -16,7 +16,7 @@ use graphene_std::table::Table; use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::misc::ManipulatorPointId; use graphene_std::vector::style::{Fill, Gradient}; -use graphene_std::vector::{PointId, SegmentId, VectorModificationType}; +use graphene_std::vector::{GradientStops, PointId, SegmentId, VectorModificationType}; use std::collections::VecDeque; /// Returns the ID of the first Spline node in the horizontal flow which is not followed by a `Path` node, or `None` if none exists. @@ -285,6 +285,17 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI Some(gradient.clone()) } +/// Get the gradient table of a layer. +pub fn get_gradient_table(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option> { + let gradient_table_index = 1; + + let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER))?; + let TaggedValue::GradientTable(gradient_table) = inputs.get(gradient_table_index)?.as_value()? else { + return None; + }; + Some(gradient_table.clone()) +} + /// Get the current fill of a layer from the closest "Fill" node. pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { let fill_index = 1; diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index f6b8f49be9..6954bddd8f 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -3,13 +3,16 @@ use crate::consts::{ COLOR_OVERLAY_BLUE, DRAG_THRESHOLD, GRADIENT_MIDPOINT_DIAMOND_RADIUS, GRADIENT_MIDPOINT_MAX, GRADIENT_MIDPOINT_MIN, GRADIENT_STOP_MIN_VIEWPORT_GAP, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, }; +use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; use crate::messages::portfolio::document::overlays::utility_types::{GizmoEmphasis, OverlayContext}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; -use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_gradient}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_gradient_table}; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; +use crate::messages::tool::tool_messages::gradient_tool::graph_modification_utils::is_layer_fed_by_node_of_name; use graphene_std::raster::color::Color; -use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStops, GradientType}; +use graphene_std::vector::style::{Fill, GRADIENT_TABLE_END, GRADIENT_TABLE_START, Gradient, GradientSpreadMethod, GradientStops, GradientTableRowRefExt, GradientType}; #[derive(Default, ExtractField)] pub struct GradientTool { @@ -125,8 +128,14 @@ impl<'a> MessageHandler> for Grad self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, false); let has_gradient = has_gradient_on_selected_layers(context.document); + let mut options_changed = false; + if has_gradient != self.data.has_selected_gradient { self.data.has_selected_gradient = has_gradient; + options_changed = true; + } + + if options_changed { responses.add(ToolMessage::RefreshToolOptions); } @@ -178,18 +187,6 @@ impl LayoutHolder for GradientTool { .selected_index(Some((self.options.gradient_type == GradientType::Radial) as u32)) .widget_instance(); - let reverse_stops = IconButton::new("Reverse", 24) - .tooltip_label("Reverse Stops") - .tooltip_description("Reverse the gradient color stops.") - .disabled(!self.data.has_selected_gradient) - .on_update(|_| { - GradientToolMessage::UpdateOptions { - options: GradientOptionsUpdate::ReverseStops, - } - .into() - }) - .widget_instance(); - let spread_method = RadioInput::new(vec![ RadioEntryData::new("Pad").label("Pad").tooltip_label("Pad").on_update(move |_| { GradientToolMessage::UpdateOptions { @@ -213,6 +210,18 @@ impl LayoutHolder for GradientTool { .selected_index(Some(self.options.spread_method as u32)) .widget_instance(); + let reverse_stops = IconButton::new("Reverse", 24) + .tooltip_label("Reverse Stops") + .tooltip_description("Reverse the gradient color stops.") + .disabled(!self.data.has_selected_gradient) + .on_update(|_| { + GradientToolMessage::UpdateOptions { + options: GradientOptionsUpdate::ReverseStops, + } + .into() + }) + .widget_instance(); + let mut widgets = vec![ gradient_type, Separator::new(SeparatorStyle::Unrelated).widget_instance(), @@ -273,14 +282,54 @@ impl Default for GradientToolFsmState { /// Computes the transform from gradient space to viewport space (where gradient space is 0..1) fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 { - let bounds = document.metadata().nonzero_bounding_box(layer); - let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); + let is_gradient_table = is_layer_fed_by_node_of_name( + layer, + &document.network_interface, + &DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER), + ); - let multiplied = document.metadata().transform_to_viewport(layer); + if is_gradient_table { + // Table layers use the table's row transform from gradient space to document space, + // so we cannot use transform_to_viewport here as it would apply the transform twice. + return document + .metadata() + .upstream_footprints + .get(&layer.to_node()) + .map(|footprint| footprint.transform) + .unwrap_or(document.metadata().document_to_viewport); + } + let multiplied = document.metadata().transform_to_viewport(layer); + let bounds = document.metadata().nonzero_bounding_box(layer); + let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); multiplied * bound_transform } +// TODO: This conversion is a temporary solution, this should be removed after migration to Table for all gradient use. +// TODO: We only support linear gradient with pad spread method since there is no place to store the gradient type in the table row currently. +fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + match (get_gradient_table(layer, network_interface), graph_modification_utils::get_gradient(layer, network_interface)) { + (Some(gradient_graphic), _) => { + let row = gradient_graphic.get(0)?; + let stops = row.element.clone(); + let transform = *(row.transform); + let gradient_type = row.gradient_type().unwrap_or_default(); + let spread_method = row.spread_method().unwrap_or_default(); + + let gradient = Gradient { + stops, + gradient_type, + spread_method, + start: transform.transform_point2(GRADIENT_TABLE_START), + end: transform.transform_point2(GRADIENT_TABLE_END), + }; + Some(gradient) + } + (None, Some(gradient)) => Some(gradient), + (None, None) => None, + } +} + /// Whether two adjacent stops are too closely packed in viewport space for a midpoint diamond to be shown or interacted with. fn midpoint_hidden_by_proximity(left_stop_pos: f64, right_stop_pos: f64, viewport_line_length: f64) -> bool { (right_stop_pos - left_stop_pos) * viewport_line_length < GRADIENT_STOP_MIN_VIEWPORT_GAP * 2. @@ -304,6 +353,7 @@ struct SelectedGradient { gradient: Gradient, dragging: GradientDragTarget, initial_gradient: Gradient, + is_gradient_table: bool, } fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: DVec2) -> Option { @@ -347,12 +397,14 @@ fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: D impl SelectedGradient { pub fn new(gradient: Gradient, layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Self { let transform = gradient_space_transform(layer, document); + let is_gradient_table = get_gradient_table(layer, &document.network_interface).is_some(); Self { layer: Some(layer), transform, gradient: gradient.clone(), dragging: GradientDragTarget::End, initial_gradient: gradient, + is_gradient_table, } } @@ -568,10 +620,20 @@ impl SelectedGradient { /// Update the layer fill to the current gradient pub fn render_gradient(&mut self, responses: &mut VecDeque) { if let Some(layer) = self.layer { - responses.add(GraphOperationMessage::FillSet { - layer, - fill: Fill::Gradient(self.gradient.clone()), - }); + if self.is_gradient_table { + responses.add(GraphOperationMessage::GradientTableSet { + layer, + stops: self.gradient.stops.clone(), + transform: compute_gradient_transform(&self.gradient), + gradient_type: self.gradient.gradient_type, + spread_method: self.gradient.spread_method, + }); + } else { + responses.add(GraphOperationMessage::FillSet { + layer, + fill: Fill::Gradient(self.gradient.clone()), + }); + } } } } @@ -953,7 +1015,9 @@ impl Fsm for GradientToolFsmState { // The gradient has only one point and so should become a fill if selected_gradient.gradient.stops.len() == 1 { - if let Some(layer) = selected_gradient.layer { + if selected_gradient.is_gradient_table { + selected_gradient.render_gradient(responses); + } else if let Some(layer) = selected_gradient.layer { responses.add(GraphOperationMessage::FillSet { layer, fill: Fill::Solid(selected_gradient.gradient.stops.color[0]), @@ -1047,6 +1111,7 @@ impl Fsm for GradientToolFsmState { for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) { let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue }; let transform = gradient_space_transform(layer, document); + let is_gradient_table = get_gradient_table(layer, &document.network_interface).is_some(); // Check for dragging a midpoint diamond if drag_hint.is_none() { @@ -1074,6 +1139,7 @@ impl Fsm for GradientToolFsmState { gradient: gradient.clone(), dragging: GradientDragTarget::Midpoint(i), initial_gradient: gradient.clone(), + is_gradient_table, }); break; @@ -1114,6 +1180,7 @@ impl Fsm for GradientToolFsmState { gradient: gradient.clone(), dragging: drag_target, initial_gradient: gradient.clone(), + is_gradient_table, }); } } @@ -1130,6 +1197,7 @@ impl Fsm for GradientToolFsmState { gradient: gradient.clone(), dragging: dragging_target, initial_gradient: gradient.clone(), + is_gradient_table, }) } } @@ -1510,6 +1578,12 @@ fn compute_selected_target(tool_data: &GradientToolData) -> GradientSelectedTarg } } +fn compute_gradient_transform(gradient: &Gradient) -> DAffine2 { + let delta = gradient.end - gradient.start; + let perp = DVec2::new(-delta.y, delta.x); + DAffine2::from_cols_array(&[delta.x, delta.y, perp.x, perp.y, gradient.start.x, gradient.start.y]) +} + fn apply_gradient_update( data: &mut GradientToolData, context: &mut ToolActionMessageContext, @@ -1526,6 +1600,8 @@ fn apply_gradient_update( let mut transaction_started = false; for layer in selected_layers { + let is_gradient_table = get_gradient_table(layer, &context.document.network_interface).is_some(); + if NodeGraphLayer::is_raster_layer(layer, &mut context.document.network_interface) { continue; } @@ -1538,10 +1614,21 @@ fn apply_gradient_update( transaction_started = true; } update(&mut gradient); - responses.add(GraphOperationMessage::FillSet { - layer, - fill: Fill::Gradient(gradient), - }); + + if is_gradient_table { + responses.add(GraphOperationMessage::GradientTableSet { + layer, + stops: gradient.stops.clone(), + transform: compute_gradient_transform(&gradient), + gradient_type: gradient.gradient_type, + spread_method: gradient.spread_method, + }); + } else { + responses.add(GraphOperationMessage::FillSet { + layer, + fill: Fill::Gradient(gradient), + }); + }; } } @@ -1623,11 +1710,15 @@ mod test_gradient { use crate::messages::input_mapper::utility_types::input_mouse::ScrollDelta; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; + use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector}; pub use crate::test_utils::test_prelude::*; use glam::DAffine2; - use graphene_std::vector::fill; - use graphene_std::vector::style::Fill; - use graphene_std::vector::style::Gradient; + use graph_craft::document::value::TaggedValue; + use graphene_std::table::{Table, TableRow}; + use graphene_std::vector::GradientType; + use graphene_std::vector::style::GradientSpreadMethod; + use graphene_std::vector::style::{Fill, Gradient}; + use graphene_std::vector::{GradientStop, GradientStops, fill}; use super::gradient_space_transform; @@ -1672,6 +1763,46 @@ mod test_gradient { } } + async fn create_gradient_table_layer(editor: &mut EditorTestUtils) -> LayerNodeIdentifier { + editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await; + let document = editor.active_document(); + let layer = document.metadata().all_layers().next().unwrap(); + + let gradient_node_id = editor.create_node_by_name(DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)).await; + + editor + .handle_message(NodeGraphMessage::CreateWire { + output_connector: OutputConnector::node(gradient_node_id, 0), + input_connector: InputConnector::node(layer.to_node(), 1), + }) + .await; + + editor + .handle_message(NodeGraphMessage::SetInputValue { + node_id: gradient_node_id, + input_index: 1, + value: TaggedValue::GradientTable(Table::new_from_row(TableRow { + element: GradientStops::new([ + GradientStop { + position: 0., + midpoint: 0.5, + color: Color::RED, + }, + GradientStop { + position: 1., + midpoint: 0.5, + color: Color::BLUE, + }, + ]), + transform: DAffine2::IDENTITY, + ..Default::default() + })), + }) + .await; + + layer + } + #[tokio::test] async fn ignore_artboard() { let mut editor = EditorTestUtils::create(); @@ -2037,4 +2168,153 @@ mod test_gradient { let (gradient, _) = get_gradient(&mut editor).await; assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect); } + + #[tokio::test] + async fn gradient_table_drag_endpoint() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + let layer = create_gradient_table_layer(&mut editor).await; + + // Create original transform for the control geometry and apply it + let initial_start = DVec2::new(10., 50.); + let initial_end = DVec2::new(200., 50.); + let delta = initial_end - initial_start; + let perp = DVec2::new(-delta.y, delta.x); + let initial_row_transform = DAffine2::from_cols_array(&[delta.x, delta.y, perp.x, perp.y, initial_start.x, initial_start.y]); + editor + .handle_message(GraphOperationMessage::GradientTableSet { + layer, + stops: GradientStops::new([ + GradientStop { + position: 0., + midpoint: 0.5, + color: Color::RED, + }, + GradientStop { + position: 1., + midpoint: 0.5, + color: Color::BLUE, + }, + ]), + transform: initial_row_transform, + gradient_type: GradientType::Linear, + spread_method: GradientSpreadMethod::Pad, + }) + .await; + + editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await; + + let document = editor.active_document(); + let space_transform = gradient_space_transform(layer, document); + let gradient = super::get_gradient(layer, &document.network_interface).unwrap(); + let viewport_start = space_transform.transform_point2(gradient.start); + let viewport_end = space_transform.transform_point2(gradient.end); + + // Drag target of the end point, move 80px down + let new_viewport_end = viewport_end + DVec2::new(0., 80.); + editor.select_tool(ToolType::Gradient).await; + editor.move_mouse(viewport_end.x, viewport_end.y, ModifierKeys::empty(), MouseKeys::empty()).await; + editor.left_mousedown(viewport_end.x, viewport_end.y, ModifierKeys::empty()).await; + editor.move_mouse(new_viewport_end.x, new_viewport_end.y, ModifierKeys::empty(), MouseKeys::LEFT).await; + editor + .mouseup( + EditorMouseState { + editor_position: new_viewport_end, + mouse_keys: MouseKeys::empty(), + scroll_delta: ScrollDelta::default(), + }, + ModifierKeys::empty(), + ) + .await; + + // Verify if the gradient position is updated correctly + let document = editor.active_document(); + let updated = super::get_gradient(layer, &document.network_interface).expect("Gradient should exist after drag"); + let updated_space_transform = gradient_space_transform(layer, document); + let updated_viewport_start = updated_space_transform.transform_point2(updated.start); + let updated_viewport_end = updated_space_transform.transform_point2(updated.end); + + assert!( + updated_viewport_start.abs_diff_eq(viewport_start, 1.), + "Start should not move. Expected {viewport_start:?}, got {updated_viewport_start:?}" + ); + assert!( + updated_viewport_end.abs_diff_eq(new_viewport_end, 1.), + "End should move to new position. Expected {new_viewport_end:?}, got {updated_viewport_end:?}" + ); + } + + #[tokio::test] + async fn gradient_table_preserves_stops() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + let layer = create_gradient_table_layer(&mut editor).await; + + // Set up a 3-stop gradient with distinct colors + let original_stops = GradientStops::new([ + GradientStop { + position: 0., + midpoint: 0.5, + color: Color::RED, + }, + GradientStop { + position: 0.5, + midpoint: 0.5, + color: Color::GREEN, + }, + GradientStop { + position: 1., + midpoint: 0.5, + color: Color::BLUE, + }, + ]); + let initial_start = DVec2::new(10., 50.); + let initial_end = DVec2::new(200., 50.); + let delta = initial_end - initial_start; + let perp = DVec2::new(-delta.y, delta.x); + let initial_row_transform = DAffine2::from_cols_array(&[delta.x, delta.y, perp.x, perp.y, initial_start.x, initial_start.y]); + editor + .handle_message(GraphOperationMessage::GradientTableSet { + layer, + stops: original_stops.clone(), + transform: initial_row_transform, + gradient_type: GradientType::Linear, + spread_method: GradientSpreadMethod::Pad, + }) + .await; + + editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await; + + let document = editor.active_document(); + let space_transform = gradient_space_transform(layer, document); + let gradient = super::get_gradient(layer, &document.network_interface).unwrap(); + let viewport_end = space_transform.transform_point2(gradient.end); + + // Drag the end point 80px down + let new_viewport_end = viewport_end + DVec2::new(0., 80.); + editor.select_tool(ToolType::Gradient).await; + editor.move_mouse(viewport_end.x, viewport_end.y, ModifierKeys::empty(), MouseKeys::empty()).await; + editor.left_mousedown(viewport_end.x, viewport_end.y, ModifierKeys::empty()).await; + editor.move_mouse(new_viewport_end.x, new_viewport_end.y, ModifierKeys::empty(), MouseKeys::LEFT).await; + editor + .mouseup( + EditorMouseState { + editor_position: new_viewport_end, + mouse_keys: MouseKeys::empty(), + scroll_delta: ScrollDelta::default(), + }, + ModifierKeys::empty(), + ) + .await; + + // Verify stops are preserved after dragging + let document = editor.active_document(); + let updated = super::get_gradient(layer, &document.network_interface).expect("Gradient should exist after drag"); + + assert_eq!(updated.stops.len(), 3, "Stop count should be preserved"); + assert_stops_at_positions(&updated.stops.position, &[0., 0.5, 1.], 1e-10); + assert_eq!(updated.stops.color[0].to_rgba8_srgb(), Color::RED.to_rgba8_srgb(), "First stop color should be preserved"); + assert_eq!(updated.stops.color[1].to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb(), "Middle stop color should be preserved"); + assert_eq!(updated.stops.color[2].to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb(), "Last stop color should be preserved"); + } } diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 70ceaefec0..88435b4da5 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -467,7 +467,7 @@ impl NodeRuntime { return; } - let bounds = match graphic.bounding_box(DAffine2::IDENTITY, true) { + let bounds = match graphic.thumbnail_bounding_box(DAffine2::IDENTITY, true) { RenderBoundingBox::None => None, RenderBoundingBox::Infinite => Some([DVec2::ZERO, DVec2::new(300., 200.)]), RenderBoundingBox::Rectangle(bounds) => Some(bounds), diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 511710a597..94e3c280a8 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -3,7 +3,7 @@ use crate::application_io::PlatformEditorApi; use crate::proto::{Any as DAny, FutureAny}; use brush_nodes::brush_cache::BrushCache; use brush_nodes::brush_stroke::BrushStroke; -use core_types::table::Table; +use core_types::table::{Table, TableRow}; use core_types::uuid::NodeId; use core_types::{Color, ContextFeatures, MemoHash, Node, Type}; use dyn_any::DynAny; @@ -15,17 +15,16 @@ use graphic_types::Graphic; use graphic_types::Vector; use graphic_types::raster_types::Image; use graphic_types::raster_types::{CPU, Raster}; +use graphic_types::vector_types::gradient::GradientTableRowExt; use graphic_types::vector_types::vector; use graphic_types::vector_types::vector::ReferencePoint; -use graphic_types::vector_types::vector::style::Fill; -use graphic_types::vector_types::vector::style::GradientStops; +use graphic_types::vector_types::vector::style::{Fill, GradientSpreadMethod, GradientStop, GradientStops, GradientType}; use rendering::RenderMetadata; use std::fmt::Display; use std::hash::Hash; use std::marker::PhantomData; use std::str::FromStr; pub use std::sync::Arc; -use text_nodes::vector_types::GradientStop; pub struct TaggedValueTypeError; @@ -120,7 +119,7 @@ macro_rules! tagged_value { x if x == TypeId::of::<()>() => TaggedValue::None, // Table-wrapped types need a single-row default with the element's default, not an empty table x if x == TypeId::of::>() => TaggedValue::Color(Table::new_from_element(Color::default())), - x if x == TypeId::of::>() => TaggedValue::GradientTable(Table::new_from_element(GradientStops::default())), + x if x == TypeId::of::>() => TaggedValue::GradientTable(Table::new_from_row(TableRow::new_gradient_row(GradientStops::default(), DAffine2::from_scale(DVec2::splat(100.)), GradientType::Linear, GradientSpreadMethod::Pad))), $( x if x == TypeId::of::<$ty>() => TaggedValue::$identifier(Default::default()), )* _ => return None, }) diff --git a/node-graph/libraries/core-types/src/bounds.rs b/node-graph/libraries/core-types/src/bounds.rs index d59902ff0a..5a7e4762c7 100644 --- a/node-graph/libraries/core-types/src/bounds.rs +++ b/node-graph/libraries/core-types/src/bounds.rs @@ -11,6 +11,7 @@ pub enum RenderBoundingBox { pub trait BoundingBox { fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox; + fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox; } macro_rules! none_impl { @@ -19,6 +20,10 @@ macro_rules! none_impl { fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox { RenderBoundingBox::None } + + fn thumbnail_bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox { + RenderBoundingBox::None + } } }; } @@ -32,4 +37,8 @@ impl BoundingBox for Color { fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox { RenderBoundingBox::Infinite } + + fn thumbnail_bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox { + RenderBoundingBox::Rectangle([DVec2::ZERO, DVec2::new(300., 200.)]) + } } diff --git a/node-graph/libraries/core-types/src/ops.rs b/node-graph/libraries/core-types/src/ops.rs index 9221f6dc6d..1ed6db8dcd 100644 --- a/node-graph/libraries/core-types/src/ops.rs +++ b/node-graph/libraries/core-types/src/ops.rs @@ -67,6 +67,7 @@ impl + Send> Convert, ()> for Table { transform: row.transform, alpha_blending: row.alpha_blending, source_node_id: row.source_node_id, + additional: row.additional, }) .collect(); table diff --git a/node-graph/libraries/core-types/src/table.rs b/node-graph/libraries/core-types/src/table.rs index 4a814aaf58..f887c46cad 100644 --- a/node-graph/libraries/core-types/src/table.rs +++ b/node-graph/libraries/core-types/src/table.rs @@ -4,8 +4,17 @@ use crate::uuid::NodeId; use crate::{AlphaBlending, math::quad::Quad}; use dyn_any::{StaticType, StaticTypeSized}; use glam::DAffine2; +use std::collections::HashMap; use std::hash::Hash; +// TODO: Temporal solution for storing an additional custom column in a table +#[derive(Clone, Debug, PartialEq, Hash, Default, serde::Serialize, serde::Deserialize)] +pub enum CustomColumnValue { + #[default] + None, + U32(u32), +} + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Table { #[serde(alias = "instances", alias = "instance")] @@ -13,11 +22,19 @@ pub struct Table { transform: Vec, alpha_blending: Vec, source_node_id: Vec>, + #[serde(default)] + additional: HashMap>, } impl Table { pub fn new() -> Self { - Self::default() + Self { + element: Vec::new(), + transform: Vec::new(), + alpha_blending: Vec::new(), + source_node_id: Vec::new(), + additional: HashMap::new(), + } } pub fn with_capacity(capacity: usize) -> Self { @@ -26,6 +43,7 @@ impl Table { transform: Vec::with_capacity(capacity), alpha_blending: Vec::with_capacity(capacity), source_node_id: Vec::with_capacity(capacity), + additional: HashMap::new(), } } @@ -35,6 +53,7 @@ impl Table { transform: vec![DAffine2::IDENTITY], alpha_blending: vec![AlphaBlending::default()], source_node_id: vec![None], + additional: HashMap::new(), } } @@ -44,6 +63,7 @@ impl Table { transform: vec![row.transform], alpha_blending: vec![row.alpha_blending], source_node_id: vec![row.source_node_id], + additional: row.additional.into_iter().map(|(key, value)| (key, vec![value])).collect(), } } @@ -52,13 +72,41 @@ impl Table { self.transform.push(row.transform); self.alpha_blending.push(row.alpha_blending); self.source_node_id.push(row.source_node_id); + + let target_len = self.element.len(); + + // Ensure all additional columns have the same length by padding with None + for (key, value) in row.additional { + let col = self.additional.entry(key).or_default(); + col.resize(target_len - 1, CustomColumnValue::None); + col.push(value); + } + + for values in self.additional.values_mut() { + values.resize(target_len, CustomColumnValue::None); + } } pub fn extend(&mut self, table: Table) { + let original_len = self.element.len(); + self.element.extend(table.element); self.transform.extend(table.transform); self.alpha_blending.extend(table.alpha_blending); self.source_node_id.extend(table.source_node_id); + + // Ensure all additional columns remain the same length after extending by padding with None + let target_len = self.element.len(); + + for (key, values) in table.additional { + let col = self.additional.entry(key).or_default(); + col.resize(original_len, CustomColumnValue::None); + col.extend(values); + } + + for values in self.additional.values_mut() { + values.resize(target_len, CustomColumnValue::None); + } } pub fn get(&self, index: usize) -> Option> { @@ -71,6 +119,7 @@ impl Table { transform: &self.transform[index], alpha_blending: &self.alpha_blending[index], source_node_id: &self.source_node_id[index], + additional: self.additional.iter().map(|(key, values)| (key.as_str(), &values[index])).collect(), }) } @@ -84,6 +133,7 @@ impl Table { transform: &mut self.transform[index], alpha_blending: &mut self.alpha_blending[index], source_node_id: &mut self.source_node_id[index], + additional: self.additional.iter_mut().map(|(key, value)| (key.as_str(), &mut value[index])).collect(), }) } @@ -97,33 +147,46 @@ impl Table { /// Borrows a [`Table`] and returns an iterator of [`TableRowRef`]s, each containing references to the data of the respective row from the table. pub fn iter(&self) -> impl DoubleEndedIterator> + Clone { - self.element - .iter() - .zip(self.transform.iter()) - .zip(self.alpha_blending.iter()) - .zip(self.source_node_id.iter()) - .map(|(((element, transform), alpha_blending), source_node_id)| TableRowRef { - element, - transform, - alpha_blending, - source_node_id, - }) + (0..self.element.len()).map(move |i| TableRowRef { + element: &self.element[i], + transform: &self.transform[i], + alpha_blending: &self.alpha_blending[i], + source_node_id: &self.source_node_id[i], + additional: self.additional.iter().map(|(key, values)| (key.as_str(), &values[i])).collect(), + }) } /// Mutably borrows a [`Table`] and returns an iterator of [`TableRowMut`]s, each containing mutable references to the data of the respective row from the table. pub fn iter_mut(&mut self) -> impl DoubleEndedIterator> { + let len = self.element.len(); + + let mut additional_rows: Vec> = (0..len).map(|_| HashMap::new()).collect(); + for (key, values) in self.additional.iter_mut() { + for (i, value) in values.iter_mut().enumerate() { + additional_rows[i].insert(key.as_str(), value); + } + } + self.element .iter_mut() .zip(self.transform.iter_mut()) .zip(self.alpha_blending.iter_mut()) .zip(self.source_node_id.iter_mut()) - .map(|(((element, transform), alpha_blending), source_node_id)| TableRowMut { + .zip(additional_rows) + .map(|((((element, transform), alpha_blending), source_node_id), additional)| TableRowMut { element, transform, alpha_blending, source_node_id, + additional, }) } + + pub fn additional_column_keys(&self) -> Vec<&str> { + let mut keys: Vec<_> = self.additional.keys().map(|key| key.as_str()).collect(); + keys.sort(); + keys + } } impl BoundingBox for Table { @@ -146,6 +209,26 @@ impl BoundingBox for Table { None => RenderBoundingBox::None, } } + + fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { + let mut combined_bounds = None; + + for row in self.iter() { + match row.element.thumbnail_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(bounds) => RenderBoundingBox::Rectangle(bounds), + None => RenderBoundingBox::None, + } + } } impl IntoIterator for Table { @@ -159,6 +242,7 @@ impl IntoIterator for Table { transform: self.transform.into_iter(), alpha_blending: self.alpha_blending.into_iter(), source_node_id: self.source_node_id.into_iter(), + additional: self.additional.into_iter().map(|(key, value)| (key, value.into_iter())).collect(), } } } @@ -168,6 +252,7 @@ pub struct TableRowIter { transform: std::vec::IntoIter, alpha_blending: std::vec::IntoIter, source_node_id: std::vec::IntoIter>, + additional: HashMap>, } impl Iterator for TableRowIter { type Item = TableRow; @@ -177,12 +262,18 @@ impl Iterator for TableRowIter { let transform = self.transform.next()?; let alpha_blending = self.alpha_blending.next()?; let source_node_id = self.source_node_id.next()?; + let additional = self + .additional + .iter_mut() + .map(|(key, value)| (key.clone(), value.next().expect("additional column length mismatch"))) + .collect(); Some(TableRow { element, transform, alpha_blending, source_node_id, + additional, }) } } @@ -194,6 +285,7 @@ impl Default for Table { transform: Vec::new(), alpha_blending: Vec::new(), source_node_id: Vec::new(), + additional: HashMap::new(), } } } @@ -209,12 +301,21 @@ impl Hash for Table { for alpha_blending in &self.alpha_blending { alpha_blending.hash(state); } + // Sort by key to have the same result + let mut entries: Vec<_> = self.additional.iter().collect(); + entries.sort_by_key(|(key, _)| *key); + for (key, values) in entries { + key.hash(state); + for value in values { + value.hash(state); + } + } } } impl PartialEq for Table { fn eq(&self, other: &Self) -> bool { - self.element == other.element && self.transform == other.transform && self.alpha_blending == other.alpha_blending + self.element == other.element && self.transform == other.transform && self.alpha_blending == other.alpha_blending && self.additional == other.additional } } @@ -248,13 +349,15 @@ impl FromIterator> for Table { } } -#[derive(Copy, Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct TableRow { #[serde(alias = "instance")] pub element: T, pub transform: DAffine2, pub alpha_blending: AlphaBlending, pub source_node_id: Option, + #[serde(default)] + pub additional: HashMap, } impl TableRow { @@ -264,6 +367,7 @@ impl TableRow { transform: DAffine2::IDENTITY, alpha_blending: AlphaBlending::default(), source_node_id: None, + additional: HashMap::new(), } } @@ -273,6 +377,7 @@ impl TableRow { transform: &self.transform, alpha_blending: &self.alpha_blending, source_node_id: &self.source_node_id, + additional: self.additional.iter().map(|(key, value)| (key.as_str(), value)).collect(), } } @@ -282,16 +387,18 @@ impl TableRow { transform: &mut self.transform, alpha_blending: &mut self.alpha_blending, source_node_id: &mut self.source_node_id, + additional: self.additional.iter_mut().map(|(key, value)| (key.as_str(), value)).collect(), } } } -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct TableRowRef<'a, T> { pub element: &'a T, pub transform: &'a DAffine2, pub alpha_blending: &'a AlphaBlending, pub source_node_id: &'a Option, + pub additional: HashMap<&'a str, &'a CustomColumnValue>, } impl TableRowRef<'_, T> { @@ -304,6 +411,7 @@ impl TableRowRef<'_, T> { transform: *self.transform, alpha_blending: *self.alpha_blending, source_node_id: *self.source_node_id, + additional: self.additional.into_iter().map(|(key, value)| (key.to_string(), value.clone())).collect(), } } } @@ -314,6 +422,7 @@ pub struct TableRowMut<'a, T> { pub transform: &'a mut DAffine2, pub alpha_blending: &'a mut AlphaBlending, pub source_node_id: &'a mut Option, + pub additional: HashMap<&'a str, &'a mut CustomColumnValue>, } // Conversion from Table to Option - extracts first element diff --git a/node-graph/libraries/graphic-types/src/artboard.rs b/node-graph/libraries/graphic-types/src/artboard.rs index 7595f2cd52..4f2b52f06a 100644 --- a/node-graph/libraries/graphic-types/src/artboard.rs +++ b/node-graph/libraries/graphic-types/src/artboard.rs @@ -54,6 +54,11 @@ impl BoundingBox for Artboard { other => other, } } + + fn thumbnail_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(); + RenderBoundingBox::Rectangle(artboard_bounds) + } } impl RenderComplexity for Artboard { @@ -106,6 +111,7 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re transform: DAffine2::IDENTITY, alpha_blending: AlphaBlending::default(), source_node_id, + additional: Default::default(), }); } table @@ -119,6 +125,7 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re transform, alpha_blending, source_node_id: None, + additional: Default::default(), }) .collect(), ArtboardFormat::ArtboardTable(artboard_table) => artboard_table, diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 001c0c33a2..7502963570 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -139,6 +139,11 @@ fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic transform: current_graphic_row.transform * row.transform, alpha_blending: compose_alpha_blending(current_graphic_row.alpha_blending, row.alpha_blending), source_node_id, + additional: { + let mut additional = current_graphic_row.additional.clone(); + additional.extend(row.additional); + additional + }, }); } } @@ -322,6 +327,17 @@ impl BoundingBox for Graphic { Graphic::Gradient(gradient) => gradient.bounding_box(transform, include_stroke), } } + + fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { + match self { + Graphic::Vector(vector) => vector.thumbnail_bounding_box(transform, include_stroke), + Graphic::RasterCPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke), + Graphic::RasterGPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke), + Graphic::Graphic(graphic) => graphic.thumbnail_bounding_box(transform, include_stroke), + Graphic::Color(color) => color.thumbnail_bounding_box(transform, include_stroke), + Graphic::Gradient(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke), + } + } } impl TableConvert for Vector { @@ -447,6 +463,7 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res transform: old.transform, alpha_blending: old.alpha_blending, source_node_id, + additional: Default::default(), }); } graphic_table @@ -460,6 +477,7 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res transform: element.transform, alpha_blending: element.alpha_blending, source_node_id, + additional: Default::default(), }) }) .collect(), @@ -472,6 +490,7 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res transform: element.transform, alpha_blending: element.alpha_blending, source_node_id, + additional: Default::default(), }) }) .collect(), @@ -484,6 +503,7 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res transform: Default::default(), alpha_blending: Default::default(), source_node_id, + additional: Default::default(), }) }) .collect(), @@ -498,6 +518,7 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res transform: *row.transform, alpha_blending: *row.alpha_blending, source_node_id: *source_node_id, + additional: Default::default(), }); } } diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index 54e4302bca..62cfc586b4 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -92,6 +92,7 @@ pub mod migrations { transform, alpha_blending, source_node_id: None, + additional: Default::default(), }) .collect(), VectorFormat::VectorTable(vector_table) => vector_table, diff --git a/node-graph/libraries/raster-types/src/image.rs b/node-graph/libraries/raster-types/src/image.rs index 420b2439af..048eebcf5f 100644 --- a/node-graph/libraries/raster-types/src/image.rs +++ b/node-graph/libraries/raster-types/src/image.rs @@ -321,6 +321,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> transform, alpha_blending, source_node_id: None, + additional: Default::default(), }) .collect() } @@ -334,6 +335,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> transform: DAffine2::IDENTITY, alpha_blending: AlphaBlending::default(), source_node_id: None, + additional: Default::default(), }) .collect() } @@ -452,6 +454,7 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D transform: image_frame_with_transform_and_blending.transform, alpha_blending: image_frame_with_transform_and_blending.alpha_blending, source_node_id: None, + additional: Default::default(), }, FormatVersions::ImageFrameTable(image_frame) => TableRow { element: Raster::new_cpu(image_frame.iter().next().unwrap().element.image.clone()), diff --git a/node-graph/libraries/raster-types/src/raster_types.rs b/node-graph/libraries/raster-types/src/raster_types.rs index 918df0cec8..2b306a9f43 100644 --- a/node-graph/libraries/raster-types/src/raster_types.rs +++ b/node-graph/libraries/raster-types/src/raster_types.rs @@ -210,6 +210,10 @@ where let unit_rectangle = Quad::from_box([DVec2::ZERO, DVec2::ONE]); RenderBoundingBox::Rectangle((transform * unit_rectangle).bounding_box()) } + + fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { + self.bounding_box(transform, include_stroke) + } } // RenderComplexity trait implementations diff --git a/node-graph/libraries/rendering/Cargo.toml b/node-graph/libraries/rendering/Cargo.toml index e33ce052fd..00284257a0 100644 --- a/node-graph/libraries/rendering/Cargo.toml +++ b/node-graph/libraries/rendering/Cargo.toml @@ -25,3 +25,4 @@ graphic-types = { workspace = true } # Workspace dependencies vello = { workspace = true } +vello_encoding = { workspace = true } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 6f1690ad37..5555f27f2a 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -15,7 +15,7 @@ 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::vector_types::vector::style::{Fill, GRADIENT_TABLE_END, GRADIENT_TABLE_START, GradientSpreadMethod, GradientTableRowRefExt, PaintOrder, RenderMode, Stroke, StrokeAlign}; use graphic_types::{Artboard, Graphic}; use kurbo::{Affine, Cap, Join, Shape}; use num_traits::Zero; @@ -24,7 +24,7 @@ use std::fmt::Write; use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::sync::{Arc, LazyLock}; -use vector_types::gradient::GradientSpreadMethod; +use vello::peniko::GradientKind; use vello::*; /// Cached 16x16 transparency checkerboard image data (two 8x8 cells of #ffffff and #cccccc). @@ -301,6 +301,10 @@ pub fn to_transform(transform: DAffine2) -> usvg::Transform { usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32) } +fn to_point(p: DVec2) -> kurbo::Point { + kurbo::Point::new(p.x, p.y) +} + fn get_outline_styles(render_params: &RenderParams) -> (kurbo::Stroke, peniko::Color) { use core_types::consts::LAYER_OUTLINE_STROKE_WEIGHT; @@ -935,6 +939,7 @@ impl Render for Table { alpha_blending: *row.alpha_blending, transform: *row.transform, source_node_id: None, + additional: Default::default(), }); (id, mask_type, vector_row) @@ -1077,7 +1082,6 @@ impl Render for Table { } let layer_bounds = row.element.bounding_box().unwrap_or_default(); - let to_point = |p: DVec2| kurbo::Point::new(p.x, p.y); let mut path = kurbo::BezPath::new(); for mut bezpath in row.element.stroke_bezpath_iter() { bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); @@ -1250,6 +1254,7 @@ impl Render for Table { alpha_blending: *row.alpha_blending, transform: *row.transform, source_node_id: None, + additional: Default::default(), }); let bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); @@ -1703,10 +1708,9 @@ impl Render for Table { } impl Render for Table { - // TODO: Fix infinite gradient rendering fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for row in self.iter() { - render.leaf_tag("rect", |attributes| { + render.leaf_tag("polyline", |attributes| { // Chrome doesn't like drawing centered rectangles bigger than ~20 million so we draw a polyline quad instead let max = u64::MAX; attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}")); @@ -1723,25 +1727,36 @@ impl Render for Table { stop_string.push_str(" />"); } - let gradient_transform = render_params.footprint.transform * *row.transform; + // render_thumbnail already added the footprint transform + let gradient_transform = if render_params.thumbnail { + *row.transform + } else { + render_params.footprint.transform * *row.transform + }; let gradient_transform_matrix = format_transform_matrix(gradient_transform); let gradient_transform_attribute = if gradient_transform_matrix.is_empty() { String::new() } else { format!(r#" gradientTransform="{gradient_transform_matrix}""#) }; + let spread_method = row.spread_method().unwrap_or_default(); + let spread_method_attribute = if spread_method == GradientSpreadMethod::Pad { + String::new() + } else { + format!(r#" spreadMethod="{}""#, spread_method.svg_name()) + }; let gradient_id = generate_uuid(); - let start = DVec2::ZERO; - let end = DVec2::X; + let start = GRADIENT_TABLE_START; + let end = GRADIENT_TABLE_END; - match GradientType::Radial { + match row.gradient_type().unwrap_or_default() { GradientType::Linear => { let (x1, y1) = (start.x, start.y); let (x2, y2) = (end.x, end.y); let _ = write!( &mut attributes.0.svg_defs, - r#"{stop_string}"# + r#"{stop_string}"# ); } GradientType::Radial => { @@ -1749,7 +1764,7 @@ impl Render for Table { let r = start.distance(end); let _ = write!( &mut attributes.0.svg_defs, - r#"{stop_string}"# + r#"{stop_string}"# ); } } @@ -1768,29 +1783,77 @@ impl Render for Table { } } - // TODO: Fix infinite gradient rendering - fn render_to_vello(&self, scene: &mut Scene, _parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { + fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { use vello::peniko; + if let RenderMode::Outline = render_params.render_mode { + return; + } + for row in self.iter() { + let gradient_transform = parent_transform * *row.transform; + let alpha_blending = *row.alpha_blending; let blend_mode = alpha_blending.blend_mode.to_peniko(); let opacity = alpha_blending.opacity(render_params.for_mask); - let color = row.element.color.first().copied().unwrap_or(Color::MAGENTA); - let vello_color = peniko::Color::new([color.r(), color.g(), color.b(), color.a()]); + let mut stops: peniko::ColorStops = peniko::ColorStops::new(); + for (position, color, _) in row.element.interpolated_samples() { + stops.push(peniko::ColorStop { + offset: position as f32, + color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])), + }) + } + + let kind = match row.gradient_type().unwrap_or_default() { + GradientType::Linear => GradientKind::from(peniko::LinearGradientPosition { + start: to_point(GRADIENT_TABLE_START), + end: to_point(GRADIENT_TABLE_END), + }), + GradientType::Radial => { + let center = to_point(GRADIENT_TABLE_START); + let radius = center.distance(to_point(GRADIENT_TABLE_END)); + GradientKind::from(peniko::RadialGradientPosition { + start_center: center, + start_radius: 0., + end_center: center, + end_radius: radius as f32, + }) + } + }; + + let extend = match row.spread_method().unwrap_or_default() { + GradientSpreadMethod::Pad => peniko::Extend::Pad, + GradientSpreadMethod::Reflect => peniko::Extend::Reflect, + GradientSpreadMethod::Repeat => peniko::Extend::Repeat, + }; + let fill = peniko::Brush::Gradient(peniko::Gradient { + stops, + kind, + extend, + interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied, + ..Default::default() + }); + let brush_transform = kurbo::Affine::new((gradient_transform).to_cols_array()); let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.)); let mut layer = false; if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() { let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver); - // See implemenation in `Table` for more detail + // See implementation in `Table` for more detail scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::scale(f64::INFINITY), &rect); layer = true; } - scene.fill(peniko::Fill::NonZero, kurbo::Affine::scale(f64::INFINITY), vello_color, None, &rect); + // Encode shape and brush manually instead of Scene.fill(), which would multiply brush_transform by the path transform. + scene.encoding_mut().encode_transform(vello_encoding::Transform::from_kurbo(&kurbo::Affine::scale(f64::INFINITY))); + scene.encoding_mut().encode_fill_style(peniko::Fill::NonZero); + scene.encoding_mut().encode_shape(&rect, true); + + scene.encoding_mut().encode_transform(vello_encoding::Transform::from_kurbo(&brush_transform)); + scene.encoding_mut().swap_last_path_tags(); + scene.encoding_mut().encode_brush(&fill, 1.0); if layer { scene.pop_layer(); diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index f5c241c2f0..812acc2d56 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -1,7 +1,16 @@ -use core_types::{Color, render_complexity::RenderComplexity}; +use std::collections::HashMap; + +use core_types::{ + AlphaBlending, Color, + render_complexity::RenderComplexity, + table::{CustomColumnValue, TableRow, TableRowRef}, +}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; +pub const GRADIENT_TABLE_START: DVec2 = DVec2::ZERO; +pub const GRADIENT_TABLE_END: DVec2 = DVec2::X; + #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)] #[widget(Radio)] @@ -510,4 +519,74 @@ impl core_types::bounds::BoundingBox for GradientStops { fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox { core_types::bounds::RenderBoundingBox::Infinite } + + fn thumbnail_bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox { + // We don't have the gradient type or artboard size here, + // so we use the gradient's radius as the bounding box to ensure a radial gradient fits within the thumbnail + let center = transform.transform_point2(DVec2::ZERO); + let edge = transform.transform_point2(DVec2::X); + let radius = center.distance(edge).max(300.); + let radius_vec = DVec2::splat(radius); + + core_types::bounds::RenderBoundingBox::Rectangle([center - radius_vec, center + radius_vec]) + } +} + +pub const GRADIENT_TYPE_KEY: &str = "gradient_type"; +pub const GRADIENT_SPREAD_METHOD_KEY: &str = "gradient_spread_method"; + +pub trait GradientTableRowExt { + fn new_gradient_row(element: GradientStops, transform: DAffine2, gradient_type: GradientType, spread_method: GradientSpreadMethod) -> Self; +} + +impl GradientTableRowExt for TableRow { + fn new_gradient_row(element: GradientStops, transform: DAffine2, gradient_type: GradientType, spread_method: GradientSpreadMethod) -> Self { + let gradient_type_raw = match gradient_type { + GradientType::Linear => CustomColumnValue::U32(0), + GradientType::Radial => CustomColumnValue::U32(1), + }; + + let spread_method_raw = match spread_method { + GradientSpreadMethod::Pad => CustomColumnValue::U32(0), + GradientSpreadMethod::Reflect => CustomColumnValue::U32(1), + GradientSpreadMethod::Repeat => CustomColumnValue::U32(2), + }; + + Self { + element, + // This is to ensure the gradient visible by default as identity transform will render 1px width gradient + transform, + alpha_blending: AlphaBlending::default(), + source_node_id: None, + additional: HashMap::from([(GRADIENT_TYPE_KEY.to_string(), gradient_type_raw), (GRADIENT_SPREAD_METHOD_KEY.to_string(), spread_method_raw)]), + } + } +} + +pub trait GradientTableRowRefExt { + fn gradient_type(&self) -> Option; + fn spread_method(&self) -> Option; +} + +impl GradientTableRowRefExt for TableRowRef<'_, GradientStops> { + fn gradient_type(&self) -> Option { + let gradient_type_raw = self.additional.get(GRADIENT_TYPE_KEY)?; + + match gradient_type_raw { + CustomColumnValue::U32(0) => Some(GradientType::Linear), + CustomColumnValue::U32(1) => Some(GradientType::Radial), + _ => None, + } + } + + fn spread_method(&self) -> Option { + let spread_method_raw = self.additional.get(GRADIENT_SPREAD_METHOD_KEY)?; + + match spread_method_raw { + CustomColumnValue::U32(0) => Some(GradientSpreadMethod::Pad), + CustomColumnValue::U32(1) => Some(GradientSpreadMethod::Reflect), + CustomColumnValue::U32(2) => Some(GradientSpreadMethod::Repeat), + _ => None, + } + } } diff --git a/node-graph/libraries/vector-types/src/vector/vector_types.rs b/node-graph/libraries/vector-types/src/vector/vector_types.rs index e9b8bbb670..3acc05c011 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_types.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_types.rs @@ -493,6 +493,10 @@ impl BoundingBox for Vector { None => RenderBoundingBox::None, } } + + fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { + BoundingBox::bounding_box(self, transform, include_stroke) + } } impl RenderComplexity for Vector { diff --git a/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs b/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs index d6fcf75d3b..a753e7efb2 100644 --- a/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs +++ b/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs @@ -241,6 +241,7 @@ impl PerPixelAdjustGraphicsPipeline { transform: *instance.transform, alpha_blending: *instance.alpha_blending, source_node_id: *instance.source_node_id, + additional: Default::default(), } }) .collect::>(); diff --git a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs index 4e8ed8461a..d75f2c9f78 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs @@ -160,6 +160,7 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { transform: *row.transform, alpha_blending: *row.alpha_blending, source_node_id: *row.source_node_id, + additional: Default::default(), } }) .collect(); @@ -211,6 +212,7 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { transform: row.transform, alpha_blending: row.alpha_blending, source_node_id: row.source_node_id, + additional: Default::default(), }); } @@ -234,6 +236,7 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { transform: row.transform, alpha_blending: row.alpha_blending, source_node_id: row.source_node_id, + additional: Default::default(), }) .collect() } diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index f939df8ee8..e2eb8f4dcd 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -280,6 +280,7 @@ pub async fn flatten_graphic(_: impl Ctx, content: Table, fully_flatten transform: *current_row.transform, alpha_blending: *current_row.alpha_blending, source_node_id: reference, + additional: current_row.additional.into_iter().map(|(key, value)| (key.to_string(), value.clone())).collect(), }); } } diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 3a80a44189..c141269558 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -217,6 +217,7 @@ fn flatten_vector(graphic_table: &Table) -> Table { transform: row.transform, alpha_blending: row.alpha_blending, source_node_id: row.source_node_id, + additional: row.additional, } }) .collect::>(), @@ -235,6 +236,7 @@ fn flatten_vector(graphic_table: &Table) -> Table { transform: row.transform, alpha_blending: row.alpha_blending, source_node_id: row.source_node_id, + additional: row.additional, } }) .collect::>(), diff --git a/node-graph/nodes/raster/src/std_nodes.rs b/node-graph/nodes/raster/src/std_nodes.rs index ebba6268de..35d9e59277 100644 --- a/node-graph/nodes/raster/src/std_nodes.rs +++ b/node-graph/nodes/raster/src/std_nodes.rs @@ -178,6 +178,7 @@ pub fn combine_channels( transform, alpha_blending, source_node_id, + additional: Default::default(), }) }) .collect() diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 40fb786b06..94927fdfbb 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -421,6 +421,7 @@ async fn round_corners( transform: source_transform, alpha_blending: Default::default(), source_node_id: *source_node_id, + additional: Default::default(), } }) .collect() @@ -1019,6 +1020,7 @@ async fn auto_tangents( transform, alpha_blending, source_node_id, + additional: Default::default(), } }) .collect() @@ -1210,6 +1212,7 @@ async fn solidify_stroke(_: impl Ctx, content: Table) -> Table { transform, alpha_blending, source_node_id, + additional: Default::default(), }; // If the original vector has a fill, preserve it as a separate row with the stroke cleared. @@ -1221,6 +1224,7 @@ async fn solidify_stroke(_: impl Ctx, content: Table) -> Table { transform, alpha_blending, source_node_id, + additional: Default::default(), } }); @@ -1255,6 +1259,7 @@ async fn separate_subpaths(_: impl Ctx, content: Table) -> Table transform, alpha_blending, source_node_id, + additional: Default::default(), } }) .collect::>>()