diff --git a/editor/src/messages/clipboard/clipboard_message_handler.rs b/editor/src/messages/clipboard/clipboard_message_handler.rs index 3e3481e7ec..408e760478 100644 --- a/editor/src/messages/clipboard/clipboard_message_handler.rs +++ b/editor/src/messages/clipboard/clipboard_message_handler.rs @@ -402,11 +402,11 @@ impl MessageHandler> for Clipboard }); // Add default fill and stroke to the layer - let fill = graphene_std::vector::style::Fill::solid(Color::WHITE); - responses.add(GraphOperationMessage::FillSet { layer, fill }); + responses.add(GraphOperationMessage::FillColorSet { layer, color: Some(Color::WHITE) }); - let stroke = graphene_std::vector::style::Stroke::new(Some(Color::BLACK), DEFAULT_STROKE_WIDTH); - responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); + let color = Some(Color::BLACK); + let stroke = graphene_std::vector::style::Stroke::new(DEFAULT_STROKE_WIDTH); + responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke }); // Create new point ids and add those into the existing Vector path let mut points_map = HashMap::new(); 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 6b2a8dea05..c66d3f9810 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 @@ -13,7 +13,7 @@ use graphene_std::list::List; use graphene_std::memo::IORecord; use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::vector::Vector; -use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType}; +use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType}; use graphene_std::{Artboard, Color, Context, Graphic}; use std::any::Any; use std::sync::Arc; @@ -385,61 +385,7 @@ impl TableItemLayout for Vector { VectorTableTab::Properties => { table_rows.push(column_headings(&["property", "value"])); - match self.style.fill.clone() { - Fill::None => table_rows.push(vec![ - TextLabel::new("Fill").narrow(true).widget_instance(), - ColorInput::new(FillChoiceUI::None) - .disabled(true) - .menu_direction(Some(MenuDirection::Top)) - .narrow(true) - .widget_instance(), - ]), - Fill::Solid(color) => table_rows.push(vec![ - TextLabel::new("Fill").narrow(true).widget_instance(), - ColorInput::new(FillChoiceUI::from(&FillChoice::Solid(color))) - .disabled(true) - .menu_direction(Some(MenuDirection::Top)) - .narrow(true) - .widget_instance(), - ]), - Fill::Gradient(gradient) => { - table_rows.push(vec![ - TextLabel::new("Fill").narrow(true).widget_instance(), - ColorInput::new(FillChoiceUI::from(&FillChoice::Gradient(gradient.stops))) - .disabled(true) - .menu_direction(Some(MenuDirection::Top)) - .narrow(true) - .widget_instance(), - ]); - table_rows.push(vec![ - TextLabel::new("Fill Gradient Type").narrow(true).widget_instance(), - TextLabel::new(gradient.gradient_type.to_string()).narrow(true).widget_instance(), - ]); - table_rows.push(vec![ - TextLabel::new("Fill Gradient Start").narrow(true).widget_instance(), - TextLabel::new(format_dvec2(gradient.start)).narrow(true).widget_instance(), - ]); - table_rows.push(vec![ - TextLabel::new("Fill Gradient End").narrow(true).widget_instance(), - TextLabel::new(format_dvec2(gradient.end)).narrow(true).widget_instance(), - ]); - table_rows.push(vec![ - TextLabel::new("Fill Gradient Transform").narrow(true).widget_instance(), - TextLabel::new(format_transform_matrix(gradient.transform)).narrow(true).widget_instance(), - ]); - } - } - - if let Some(stroke) = self.style.stroke.clone() { - let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None }; - table_rows.push(vec![ - TextLabel::new("Stroke").narrow(true).widget_instance(), - ColorInput::new(FillChoiceUI::from(&color)) - .disabled(true) - .menu_direction(Some(MenuDirection::Top)) - .narrow(true) - .widget_instance(), - ]); + if let Some(stroke) = self.stroke.as_ref() { table_rows.push(vec![ TextLabel::new("Stroke Weight").narrow(true).widget_instance(), TextLabel::new(format!("{} px", stroke.weight)).narrow(true).widget_instance(), diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 990220fea9..94705a7801 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -42,10 +42,10 @@ use graphene_std::math::quad::Quad; use graphene_std::path_bool_nodes::boolean_intersect; use graphene_std::raster::BlendMode; use graphene_std::subpath::Subpath; -use graphene_std::vector::PointId; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::misc::dvec2_to_point; -use graphene_std::vector::style::{Fill, Gradient, RenderMode}; +use graphene_std::vector::style::RenderMode; +use graphene_std::vector::{PointId, graphic_types}; use kurbo::{Affine, BezPath, Line, PathSeg}; use std::collections::HashSet; use std::path::PathBuf; @@ -125,7 +125,7 @@ pub struct DocumentMessageHandler { /// network path, the node itself, and its original relative gradient. The deferred migration removes each entry as its bake lands. /// Transient migration state, but persisted in the saved document so unfinished bakes retry on the next open instead of losing placement. #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub(crate) pending_gradient_bbox_bake: Vec<(Vec, NodeId, Gradient)>, + pub(crate) pending_gradient_bbox_bake: Vec<(Vec, NodeId, graphic_types::migrations::legacy::Gradient)>, // ============================================= // Fields omitted from the saved document format @@ -2763,30 +2763,20 @@ impl DocumentMessageHandler { let mut resulting_layers: Vec = Vec::new(); for layer in selected_layers { - let style = self.network_interface.document_metadata().layer_vector_data.get(&layer).map(|arc| arc.style.clone()); - let Some(style) = style else { + let Some(vector_data) = self.network_interface.document_metadata().layer_vector_data.get(&layer) else { resulting_layers.push(layer.to_node()); continue; }; + let stroke = vector_data.stroke.as_ref(); let fill_graphic_list = self.network_interface.document_metadata().layer_fill_attributes.get(&layer); let stroke_graphic_list = self.network_interface.document_metadata().layer_stroke_attributes.get(&layer); - // `ATTR_FILL` is the source of truth when set; fall back to the legacy `style.fill` only when no attribute is present - let has_fill = if let Some(list) = fill_graphic_list { - is_paint_present(list) - } else { - !matches!(style.fill, Fill::None) - }; - // `style.stroke` is `Some` whenever a `Stroke` node is in the chain, even with weight 0 or a transparent color. - // So `is_some()` would treat invisibly-stroked fill-only layers as having a stroke. - // `ATTR_STROKE` is the source of truth when set; fall back to `style.stroke.color` only when no attribute is present. - let stroke_visible = if let Some(list) = stroke_graphic_list { - list.element(0).is_some_and(|g| !g.is_fully_transparent()) - } else { - style.stroke.as_ref().and_then(|s| s.color()).is_some_and(|c| c.a() != 0.) - }; - let has_stroke = style.stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible; + let has_fill = fill_graphic_list.is_some_and(|list| is_paint_present(list)); + // `Vector.stroke` captures stroke geometry, even with weight 0 or transparent paint. + // So stroke visibility must be checked from `ATTR_STROKE`, the paint source of truth. + let stroke_visible = stroke_graphic_list.is_some_and(|list| list.element(0).is_some_and(|g| !g.is_fully_transparent())); + let has_stroke = stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible; // No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip. if !has_stroke { @@ -4017,7 +4007,7 @@ mod document_message_handler_tests { #[test] fn pending_gradient_bakes_round_trip_through_serialization() { let document = DocumentMessageHandler { - pending_gradient_bbox_bake: vec![(vec![NodeId(7)], NodeId(42), Gradient::default())], + pending_gradient_bbox_bake: vec![(vec![NodeId(7)], NodeId(42), graphic_types::migrations::legacy::Gradient::default())], ..Default::default() }; 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 3ce54d16b1..7d986bce1b 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,15 +10,22 @@ use graphene_std::raster::BlendMode; use graphene_std::raster_types::Image; use graphene_std::subpath::Subpath; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke}; +use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke}; use graphene_std::vector::{GradientStops, PointId, VectorModificationType}; #[impl_message(Message, DocumentMessage, GraphOperation)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum GraphOperationMessage { - FillSet { + FillColorSet { layer: LayerNodeIdentifier, - fill: Fill, + color: Option, + }, + FillGradientSet { + layer: LayerNodeIdentifier, + gradient: GradientStops, + gradient_type: GradientType, + spread_method: GradientSpreadMethod, + transform: DAffine2, }, BlendingFillSet { layer: LayerNodeIdentifier, @@ -28,10 +35,9 @@ pub enum GraphOperationMessage { layer: LayerNodeIdentifier, stops: GradientStops, }, - GradientLineSet { + GradientTransformSet { layer: LayerNodeIdentifier, - start: DVec2, - end: DVec2, + transform: DAffine2, }, GradientTypeSet { layer: LayerNodeIdentifier, @@ -54,6 +60,7 @@ pub enum GraphOperationMessage { }, StrokeSet { layer: LayerNodeIdentifier, + color: Option, stroke: Stroke, }, TransformChange { 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 9c35da3ec7..4e64599e79 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 @@ -13,7 +13,7 @@ use graph_craft::document::{NodeId, NodeInput}; use graphene_std::list::List; use graphene_std::renderer::convert_usvg_path::convert_usvg_path; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; +use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::{Artboard, Color}; #[derive(ExtractField)] @@ -34,9 +34,20 @@ impl MessageHandler> for let GraphOperationMessageContext { network_interface, .. } = context; match message { - GraphOperationMessage::FillSet { layer, fill } => { + GraphOperationMessage::FillColorSet { layer, color } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { - modify_inputs.fill_set(fill); + modify_inputs.fill_color_set(color); + } + } + GraphOperationMessage::FillGradientSet { + layer, + gradient, + gradient_type, + spread_method, + transform, + } => { + if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { + modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform); } } GraphOperationMessage::BlendingFillSet { layer, fill } => { @@ -49,9 +60,9 @@ impl MessageHandler> for modify_inputs.gradient_stops_set(stops); } } - GraphOperationMessage::GradientLineSet { layer, start, end } => { + GraphOperationMessage::GradientTransformSet { layer, transform } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { - modify_inputs.gradient_line_set(start, end); + modify_inputs.gradient_transform_set(transform); } } GraphOperationMessage::GradientTypeSet { layer, gradient_type } => { @@ -80,9 +91,9 @@ impl MessageHandler> for modify_inputs.clip_mode_toggle(clip_mode); } } - GraphOperationMessage::StrokeSet { layer, stroke } => { + GraphOperationMessage::StrokeSet { layer, color, stroke } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { - modify_inputs.stroke_set(stroke); + modify_inputs.stroke_set(color, stroke); } } GraphOperationMessage::TransformChange { @@ -622,7 +633,7 @@ fn import_usvg_node( usvg::Node::Text(text) => { let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string()); modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer); - modify_inputs.fill_set(Fill::Solid(Color::BLACK)); + modify_inputs.fill_color_set(Some(Color::BLACK)); } } } @@ -674,7 +685,7 @@ fn import_usvg_node_inner( usvg::Node::Text(text) => { let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string()); modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer); - modify_inputs.fill_set(Fill::Solid(Color::BLACK)); + modify_inputs.fill_color_set(Some(Color::BLACK)); 0 } } @@ -762,27 +773,29 @@ fn set_import_child_positions( fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsContext, transform: DAffine2) { if let usvg::Paint::Color(color) = &stroke.paint() { - modify_inputs.stroke_set(Stroke { - color: Some(usvg_color(*color, stroke.opacity().get())), - weight: stroke.width().get() as f64, - dash_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(), - dash_offset: stroke.dashoffset() as f64, - cap: match stroke.linecap() { - usvg::LineCap::Butt => StrokeCap::Butt, - usvg::LineCap::Round => StrokeCap::Round, - usvg::LineCap::Square => StrokeCap::Square, - }, - join: match stroke.linejoin() { - usvg::LineJoin::Miter => StrokeJoin::Miter, - usvg::LineJoin::MiterClip => StrokeJoin::Miter, - usvg::LineJoin::Round => StrokeJoin::Round, - usvg::LineJoin::Bevel => StrokeJoin::Bevel, + modify_inputs.stroke_set( + Some(usvg_color(*color, stroke.opacity().get())), + Stroke { + weight: stroke.width().get() as f64, + dash_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(), + dash_offset: stroke.dashoffset() as f64, + cap: match stroke.linecap() { + usvg::LineCap::Butt => StrokeCap::Butt, + usvg::LineCap::Round => StrokeCap::Round, + usvg::LineCap::Square => StrokeCap::Square, + }, + join: match stroke.linejoin() { + usvg::LineJoin::Miter => StrokeJoin::Miter, + usvg::LineJoin::MiterClip => StrokeJoin::Miter, + usvg::LineJoin::Round => StrokeJoin::Round, + usvg::LineJoin::Bevel => StrokeJoin::Bevel, + }, + join_miter_limit: stroke.miterlimit().get() as f64, + align: StrokeAlign::Center, + paint_order: PaintOrder::StrokeAbove, + transform, }, - join_miter_limit: stroke.miterlimit().get() as f64, - align: StrokeAlign::Center, - paint_order: PaintOrder::StrokeAbove, - transform, - }) + ) } } @@ -795,16 +808,18 @@ fn convert_spread_method(spread_method: usvg::SpreadMethod) -> GradientSpreadMet } fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, graphite_gradient_stops: &HashMap) { - modify_inputs.fill_set(match &fill.paint() { - usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity().get())), + match &fill.paint() { + usvg::Paint::Color(color) => modify_inputs.fill_color_set(Some(usvg_color(*color, fill.opacity().get()))), usvg::Paint::LinearGradient(linear) => { let gradient_transform = usvg_transform(linear.transform()); let (start, end) = (DVec2::new(linear.x1() as f64, linear.y1() as f64), DVec2::new(linear.x2() as f64, linear.y2() as f64)); let (start, end) = (gradient_transform.transform_point2(start), gradient_transform.transform_point2(end)); + let direction = end - start; + let transform = DAffine2::from_cols(direction, direction.perp(), start); let gradient_type = GradientType::Linear; - let stops = match graphite_gradient_stops.get(linear.id()) { + let gradient = match graphite_gradient_stops.get(linear.id()) { Some(graphite_stops) => graphite_stops.clone(), None => { let stops = linear.stops().iter().map(|stop| GradientStop { @@ -816,27 +831,19 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g } }; let spread_method = convert_spread_method(linear.spread_method()); - - Fill::Gradient(Gradient { - start, - end, - gradient_type, - stops, - spread_method, - // TODO: Eventually remove this document upgrade code - absolute: true, - transform: DAffine2::IDENTITY, - }) + modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform); } usvg::Paint::RadialGradient(radial) => { let gradient_transform = usvg_transform(radial.transform()); let center = DVec2::new(radial.cx() as f64, radial.cy() as f64); let edge = center + DVec2::X * radial.r().get() as f64; let (start, end) = (gradient_transform.transform_point2(center), gradient_transform.transform_point2(edge)); + let direction = end - start; + let transform = DAffine2::from_cols(direction, direction.perp(), start); let gradient_type = GradientType::Radial; - let stops = match graphite_gradient_stops.get(radial.id()) { + let gradient = match graphite_gradient_stops.get(radial.id()) { Some(graphite_stops) => graphite_stops.clone(), None => { let stops = radial.stops().iter().map(|stop| GradientStop { @@ -849,20 +856,8 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g }; let spread_method = convert_spread_method(radial.spread_method()); - Fill::Gradient(Gradient { - start, - end, - gradient_type, - stops, - spread_method, - // TODO: Eventually remove this document upgrade code - absolute: true, - transform: DAffine2::IDENTITY, - }) + modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform); } - usvg::Paint::Pattern(_) => { - warn!("SVG patterns are not currently supported"); - return; - } - }); + usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"), + }; } 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 77f8a3ddfb..4920f0aeb5 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -15,7 +15,7 @@ use graphene_std::raster::BlendMode; use graphene_std::raster_types::Image; use graphene_std::subpath::Subpath; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke, build_transform_with_y_preservation}; +use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke}; use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType}; use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration}; @@ -453,74 +453,59 @@ impl<'a> ModifyInputsContext<'a> { Some(node_id) } - pub fn fill_set(&mut self, fill: Fill) { + pub fn fill_color_set(&mut self, color: Option) { let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { return; }; let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::>::INDEX); + let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX); - match &fill { - Fill::None => { - let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX); - self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(None), false), true); + self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(color), false), true); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), false); + } - self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(None), false), false); - } - Fill::Solid(color) => { - let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX); - self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), true); + pub fn fill_gradient_set(&mut self, gradient: GradientStops, gradient_type: GradientType, spread_method: GradientSpreadMethod, transform: DAffine2) { + let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { + return; + }; + let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX); - self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), false); - } - Fill::Gradient(gradient) => { - let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX); - self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Gradient(gradient.stops.clone()), false), true); - - // Skip the rerender on all but the last input so the whole update triggers a single graph run - self.set_input_with_refresh( - InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::>::INDEX), - NodeInput::value(TaggedValue::Gradient(gradient.stops.clone()), false), - true, - ); - - // Reposition the gradient only when the transform is a plain value, leaving a wired transform source connected - let old_transform: Option = self - .network_interface - .document_network() - .nodes - .get(&fill_node_id) - .and_then(|node| node.inputs.get(graphene_std::vector::fill::TransformInput::INDEX)) - .and_then(|input| input.as_value()) - .map(|value| { - if let TaggedValue::OptionalDAffine2(transform) = value { - transform.unwrap_or(DAffine2::IDENTITY) - } else { - DAffine2::IDENTITY - } - }); - - if let Some(old_transform) = old_transform { - let new_transform = build_transform_with_y_preservation(old_transform, gradient.start, gradient.end); - self.set_input_with_refresh( - InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX), - NodeInput::value(TaggedValue::OptionalDAffine2(Some(new_transform)), false), - true, - ); - } + self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Gradient(gradient.clone()), false), true); - self.set_input_with_refresh( - InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientTypeInput::INDEX), - NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false), - true, - ); - - self.set_input_with_refresh( - InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX), - NodeInput::value(TaggedValue::GradientSpreadMethod(gradient.spread_method), false), - false, - ); - } + // Skip the rerender on all but the last input so the whole update triggers a single graph run + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::>::INDEX), + NodeInput::value(TaggedValue::Gradient(gradient), false), + true, + ); + + // Reposition the gradient only when the transform is a plain value, leaving a wired transform source connected + let transform_is_value = self + .network_interface + .document_network() + .nodes + .get(&fill_node_id) + .and_then(|node| node.inputs.get(graphene_std::vector::fill::TransformInput::INDEX)) + .is_some_and(|input| input.as_value().is_some()); + if transform_is_value { + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX), + NodeInput::value(TaggedValue::OptionalDAffine2(Some(transform)), false), + true, + ); } + + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientTypeInput::INDEX), + NodeInput::value(TaggedValue::GradientType(gradient_type), false), + true, + ); + + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX), + NodeInput::value(TaggedValue::GradientSpreadMethod(spread_method), false), + false, + ); } pub fn blend_mode_set(&mut self, blend_mode: BlendMode) { @@ -622,10 +607,10 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false); } - /// Update the gradient line so its endpoints are at `new_start` and `new_end`. + /// Update the transform to map the unit gradient ((0,0), (1, 0)) to the geometry's local space. /// With multiple `Transform` nodes the last one (closest to the layer) is modified so the chain still composes to the target. /// With none, one is inserted unless the target is the identity. - pub fn gradient_line_set(&mut self, new_start: DVec2, new_end: DVec2) { + pub fn gradient_transform_set(&mut self, transform: DAffine2) { let Some(output_layer) = self.get_output_layer() else { return }; let walk_from = if let Some(fill_input_node_id) = get_fill_input_node_id(output_layer, self.network_interface) { @@ -661,14 +646,9 @@ impl<'a> ModifyInputsContext<'a> { .map_or(acc, |document_node| acc * transform_utils::get_current_transform(&document_node.inputs)) }) }; - let composed_old = compose(&upstream_transforms); let prior_combined = compose(prior_transforms); - // Rebuild the y-axis from the new x-axis using the old (parallel, perpendicular) decomposition and length ratio, - // so the gradient's aspect ratio and skew survive an endpoint drag (so an ellipse stays the same ellipse) instead of - // the old y-axis vector remaining fixed while x changes - let new_composed = build_transform_with_y_preservation(composed_old, new_start, new_end); - let last_transform_value = new_composed * prior_combined.inverse(); + let last_transform_value = transform * prior_combined.inverse(); let target_input = gradient_chain_target_input(output_layer, self.network_interface); let transform_node_id = if let Some(id) = last_transform_node_id { @@ -730,13 +710,13 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false); } - pub fn stroke_set(&mut self, stroke: Stroke) { + pub fn stroke_set(&mut self, color: Option, stroke: Stroke) { let Some(stroke_node_id) = self.existing_proto_node_id(graphene_std::vector::stroke::IDENTIFIER, true) else { return; }; let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintInput::>::INDEX); - self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke.color), false), true); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), true); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX); 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 5afaf59c60..36eb417ca3 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -2428,10 +2428,10 @@ fn root_layer_for_chain_node(node_id: NodeId, context: &mut NodePropertiesContex /// Resolve the viewport-space orientation of a Fill node's gradient by walking downstream to its owning layer /// and reusing the same helper the Gradient tool uses, so canvas tilt and layer transforms behave identically. -fn gradient_orientation_in_fill_node(node_id: NodeId, start: DVec2, end: DVec2, context: &mut NodePropertiesContext) -> Option { +fn gradient_orientation_in_fill_node(node_id: NodeId, gradient_transform: DAffine2, context: &mut NodePropertiesContext) -> Option { let layer = root_layer_for_chain_node(node_id, context)?; let transform = graph_modification_utils::gradient_space_transform(layer, context.network_interface); - Some(graph_modification_utils::gradient_orientation_rightward(start, end, transform)) + Some(graph_modification_utils::gradient_orientation_rightward(transform * gradient_transform)) } /// Fill Node Widgets LayoutGroup @@ -2659,7 +2659,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte let start = transform.transform_point2(DVec2::ZERO); let end = transform.transform_point2(DVec2::X); let new_transform = build_transform_with_y_preservation(transform, end, start); - let orientation_rightward = gradient_orientation_in_fill_node(node_id, start, end, context).unwrap_or(true); + let orientation_rightward = gradient_orientation_in_fill_node(node_id, transform, context).unwrap_or(true); let reverse_direction_button = IconButton::new(if orientation_rightward { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24) .tooltip_label("Reverse Direction") diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index 6f58d551da..375b522cb3 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -27,7 +27,7 @@ impl FrontendGraphDataType { match TaggedValue::from_type_or_none(input) { TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::DVec2(_) | TaggedValue::F64Array(_) | TaggedValue::DAffine2(_) => Self::Number, TaggedValue::Color(_) => Self::Color, - TaggedValue::FillGradient(_) | TaggedValue::Gradient(_) => Self::Gradient, + TaggedValue::LegacyGradient(_) | TaggedValue::Gradient(_) => Self::Gradient, TaggedValue::String(_) => Self::Typography, // Types whose `TaggedValue` variant has been removed are routed through `TypeDefault` and identified by the descriptor's type name. TaggedValue::TypeDefault(td) => match td.name.as_ref() { diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs index 685d32e59b..a821ebab79 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs @@ -61,7 +61,7 @@ impl TypeSource { FrontendGraphDataType::Number } TaggedValue::Color(_) => FrontendGraphDataType::Color, - TaggedValue::FillGradient(_) | TaggedValue::Gradient(_) => FrontendGraphDataType::Gradient, + TaggedValue::LegacyGradient(_) | TaggedValue::Gradient(_) => FrontendGraphDataType::Gradient, TaggedValue::String(_) => FrontendGraphDataType::Typography, // Types whose `TaggedValue` variant has been removed are routed through `TypeDefault` and identified by the descriptor's type name. TaggedValue::TypeDefault(td) => match td.name.as_ref() { diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 6e7de65999..ec9483d4a2 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -14,7 +14,8 @@ use graphene_std::ProtoNodeIdentifier; use graphene_std::text::{TextAlign, TypesettingConfig}; use graphene_std::transform::ScaleType; use graphene_std::uuid::NodeId; -use graphene_std::vector::style::{Fill, PaintOrder, StrokeAlign}; +use graphene_std::vector::graphic_types; +use graphene_std::vector::style::{PaintOrder, StrokeAlign}; use std::collections::HashMap; use std::f64::consts::PI; use std::ops::Range; @@ -1583,19 +1584,19 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], // Fill: a literal Fill value is decomposed, and a wired input (`List / List`) is kept as-is match old_inputs[1].as_value() { - Some(TaggedValue::Fill(old_fill)) => { + Some(TaggedValue::LegacyFill(old_fill)) => { let exposed = old_inputs[1].is_exposed(); let fill_value = match old_fill { - Fill::None => TaggedValue::Color(None), - Fill::Solid(color) => TaggedValue::Color(Some(*color)), - Fill::Gradient(gradient) => TaggedValue::Gradient(gradient.stops.clone()), + graphic_types::migrations::legacy::Fill::None => TaggedValue::Color(None), + graphic_types::migrations::legacy::Fill::Solid(color) => TaggedValue::Color(Some(*color)), + graphic_types::migrations::legacy::Fill::Gradient(gradient) => TaggedValue::Gradient(gradient.stops.clone()), }; document .network_interface .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(fill_value, exposed), network_path); // Gradient metadata (4, 5, 6): applies only to a literal gradient, solids/none keep the template defaults - if let Fill::Gradient(gradient) = old_fill { + if let graphic_types::migrations::legacy::Fill::Gradient(gradient) = old_fill { document.network_interface.set_input( &InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false), @@ -1620,7 +1621,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], } } // Wired/exposed fill keeps the connection. - // The generic paint connector accepts `List`/`List` sources directly, and there were no other nodes which can generate output type that implements `From` for `Fill`. + // The generic paint connector accepts the existing `List`/`List` paint sources directly. _ => { document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); } @@ -1630,13 +1631,18 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path); // Gradient backup: extract stops - if let Some(TaggedValue::FillGradient(g)) = old_inputs[3].as_value() { + if let Some(TaggedValue::LegacyGradient(g)) = old_inputs[3].as_value() { document .network_interface .set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Gradient(g.stops.clone()), false), network_path); // A solid/no-fill node leaves the gradient metadata inputs unused, so seed them from the backup gradient for a later Solid -> Gradient toggle to restore - if matches!(old_inputs[1].as_value(), Some(TaggedValue::Fill(Fill::None | Fill::Solid(_)))) { + if matches!( + old_inputs[1].as_value(), + Some(TaggedValue::LegacyFill( + graphic_types::migrations::legacy::Fill::None | graphic_types::migrations::legacy::Fill::Solid(_) + )) + ) { document .network_interface .set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::GradientType(g.gradient_type), false), network_path); diff --git a/editor/src/messages/tool/common_functionality/color_selector.rs b/editor/src/messages/tool/common_functionality/color_selector.rs index 7fc3f02dd5..0bd902e005 100644 --- a/editor/src/messages/tool/common_functionality/color_selector.rs +++ b/editor/src/messages/tool/common_functionality/color_selector.rs @@ -60,8 +60,7 @@ impl ToolColorOptions { return; } if let Some(FillChoice::Solid(color)) = &self.fill_choice { - let fill = graphene_std::vector::style::Fill::Solid(*color); - responses.add(GraphOperationMessage::FillSet { layer, fill }); + responses.add(GraphOperationMessage::FillColorSet { layer, color: Some(*color) }); } } @@ -70,8 +69,9 @@ impl ToolColorOptions { return; } if let Some(FillChoice::Solid(color)) = &self.fill_choice { - let stroke = graphene_std::vector::style::Stroke::new(Some(*color), weight); - responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); + let color = Some(*color); + let stroke = graphene_std::vector::style::Stroke::new(weight); + responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke }); } } @@ -175,8 +175,8 @@ impl DrawingToolState { return; } let Some(FillChoice::Solid(color)) = &self.stroke.fill_choice else { return }; + let color = Some(*color); let stroke = graphene_std::vector::style::Stroke { - color: Some(*color), weight: self.effective_line_weight(), align: self.stroke_align.unwrap_or_default(), cap: self.stroke_cap.unwrap_or_default(), @@ -187,7 +187,7 @@ impl DrawingToolState { dash_offset: self.dash_offset.unwrap_or(0.), transform: glam::DAffine2::IDENTITY, }; - responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); + responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke }); } } 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 0ff9c40736..c68bc5ac56 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -14,7 +14,7 @@ use graphene_std::raster_types::{CPU, GPU, Image, Raster}; use graphene_std::subpath::Subpath; use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::misc::ManipulatorPointId; -use graphene_std::vector::style::{Fill, FillChoice, Gradient, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box}; +use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box}; use graphene_std::vector::{GradientSpreadMethod, GradientStops, GradientType, PointId, SegmentId, VectorModificationType}; use graphene_std::{Color, Graphic}; use std::collections::VecDeque; @@ -330,8 +330,8 @@ pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNe } /// Compute the transform from a gradient's local space to viewport space for the given layer. For a `List` -/// layer this is the layer's incoming footprint transform; for the legacy `Fill::Gradient` path it composes the layer's -/// viewport transform with the [0,1]² → bounding-box mapping. +/// layer this is the layer's incoming footprint transform; for a Fill-owned gradient value it composes the layer's viewport +/// transform with the [0,1]² → bounding-box mapping. pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> glam::DAffine2 { use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; @@ -350,9 +350,9 @@ pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: & /// True when start→end (mapped through `transform` into viewport space) points predominantly rightward. For purely /// vertical lines we fall back to a stable tiebreaker on (x + y) so the choice doesn't flicker between equal alternatives. -pub fn gradient_orientation_rightward(start: glam::DVec2, end: glam::DVec2, transform: glam::DAffine2) -> bool { - let viewport_start = transform.transform_point2(start); - let viewport_end = transform.transform_point2(end); +pub fn gradient_orientation_rightward(transform: glam::DAffine2) -> bool { + let viewport_start = transform.transform_point2(DVec2::ZERO); + let viewport_end = transform.transform_point2(DVec2::X); if (viewport_end.x - viewport_start.x).abs() > f64::EPSILON * 1e6 { viewport_end.x > viewport_start.x } else { @@ -617,8 +617,9 @@ pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMes let value = TaggedValue::F64(weight); responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value }); } else if weight > 0. { + let color = Some(Color::BLACK); let stroke = graphene_std::vector::style::Stroke::default().with_weight(weight); - responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); + responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke }); } } } @@ -662,32 +663,6 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn transform_is_value: transform_input.is_some(), }) } - -// TODO: Update this to return Graphic once the legacy `Fill` enum has been eliminated -/// Returns the `Fill` value from a layer's upstream Fill node. -pub fn get_fill_value(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { - let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?; - let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?; - - match fill_node.inputs.get(graphene_std::vector::fill::FillInput::>::INDEX)?.as_value()? { - &TaggedValue::Color(color) => Some(color.map_or(Fill::None, Fill::Solid)), - TaggedValue::Gradient(_) => { - let gradient = read_fill_node_gradient(fill_node, || network_interface.document_metadata().nonzero_bounding_box(layer))?; - Some(Fill::Gradient(Gradient { - stops: gradient.stops, - gradient_type: gradient.gradient_type, - spread_method: gradient.spread_method, - start: gradient.transform.transform_point2(DVec2::ZERO), - end: gradient.transform.transform_point2(DVec2::X), - // TODO: Eventually remove this document upgrade code - absolute: true, - transform: DAffine2::IDENTITY, - })) - } - _ => None, - } -} - /// Returns the stroke color from a layer's upstream Stroke node. pub fn get_stroke_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option> { let color_index = graphene_std::vector::stroke::PaintInput::>::INDEX; @@ -717,10 +692,21 @@ pub struct SelectedStrokeState { pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option { let selected_nodes = document.network_interface.selected_nodes(); let mut per_layer = selected_nodes.selected_layers_except_artboards(&document.network_interface).map(|layer| { - if get_fill_id(layer, &document.network_interface).is_none() { + let Some(fill_node_id) = get_fill_id(layer, &document.network_interface) else { return (false, FillChoice::None); - } - let fill_choice = get_fill_value(layer, &document.network_interface).map_or(FillChoice::None, FillChoice::from); + }; + + let fill_choice = (|| { + let fill_node = document.network_interface.document_network().nodes.get(&fill_node_id)?; + + match fill_node.inputs.get(graphene_std::vector::fill::FillInput::>::INDEX)?.as_value()? { + &TaggedValue::Color(color) => Some(color.map_or(FillChoice::None, FillChoice::Solid)), + TaggedValue::Gradient(stops) => Some(FillChoice::Gradient(stops.clone())), + _ => None, + } + })() + .unwrap_or(FillChoice::None); + (true, fill_choice) }); @@ -795,12 +781,40 @@ pub fn selected_stroke_state(document: &DocumentMessageHandler) -> Option