diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs index ab46077ee8..f26a863a03 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs +++ b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs @@ -187,7 +187,8 @@ impl PreferencesDialogMessageHandler { ]; let mut checkbox_id = CheckboxId::default(); - let vector_mesh_tooltip = "Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle line joins and fills."; + let vector_mesh_tooltip = + "Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle stroke joins and fills."; let vector_meshes = vec![ Separator::new(SeparatorType::Unrelated).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index f6d2f1470a..06e9900dcd 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -121,6 +121,9 @@ pub enum DocumentMessage { SelectedLayersReorder { relative_index_offset: isize, }, + ClipLayer { + id: NodeId, + }, SelectLayer { id: NodeId, ctrl: bool, @@ -142,6 +145,9 @@ pub enum DocumentMessage { SetOpacityForSelectedLayers { opacity: f64, }, + SetFillForSelectedLayers { + fill: f64, + }, SetOverlaysVisibility { visible: bool, overlays_type: Option, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index d8a3791515..67b720ba61 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -20,7 +20,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Flo use crate::messages::portfolio::document::utility_types::nodes::RawBuffer; use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::*; -use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_opacity}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity}; use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys; use crate::messages::tool::tool_messages::tool_prelude::Key; use crate::messages::tool::utility_types::ToolType; @@ -1082,6 +1082,12 @@ impl MessageHandler> for DocumentMessag DocumentMessage::SelectedLayersReorder { relative_index_offset } => { self.selected_layers_reorder(relative_index_offset, responses); } + DocumentMessage::ClipLayer { id } => { + let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]); + + responses.add(DocumentMessage::AddTransaction); + responses.add(GraphOperationMessage::ClipModeToggle { layer }); + } DocumentMessage::SelectLayer { id, ctrl, shift } => { let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]); @@ -1176,6 +1182,12 @@ impl MessageHandler> for DocumentMessag responses.add(GraphOperationMessage::OpacitySet { layer, opacity }); } } + DocumentMessage::SetFillForSelectedLayers { fill } => { + let fill = fill.clamp(0., 1.); + for layer in self.network_interface.selected_nodes().selected_layers_except_artboards(&self.network_interface) { + responses.add(GraphOperationMessage::BlendingFillSet { layer, fill }); + } + } DocumentMessage::SetOverlaysVisibility { visible, overlays_type } => { let visibility_settings = &mut self.overlays_visibility_settings; let overlays_type = match overlays_type { @@ -2532,38 +2544,47 @@ impl DocumentMessageHandler { let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface); // Look up the current opacity and blend mode of the selected layers (if any), and split the iterator into the first tuple and the rest. - let mut opacity_and_blend_mode = selected_layers_except_artboards.map(|layer| { + let mut blending_options = selected_layers_except_artboards.map(|layer| { ( get_opacity(layer, &self.network_interface).unwrap_or(100.), + get_fill(layer, &self.network_interface).unwrap_or(100.), get_blend_mode(layer, &self.network_interface).unwrap_or_default(), ) }); - let first_opacity_and_blend_mode = opacity_and_blend_mode.next(); - let result_opacity_and_blend_mode = opacity_and_blend_mode; + let first_blending_options = blending_options.next(); + let result_blending_options = blending_options; // If there are no selected layers, disable the opacity and blend mode widgets. - let disabled = first_opacity_and_blend_mode.is_none(); + let disabled = first_blending_options.is_none(); // Amongst the selected layers, check if the opacities and blend modes are identical across all layers. // The result is setting `option` and `blend_mode` to Some value if all their values are identical, or None if they are not. // If identical, we display the value in the widget. If not, we display a dash indicating dissimilarity. - let (opacity, blend_mode) = first_opacity_and_blend_mode - .map(|(first_opacity, first_blend_mode)| { + let (opacity, fill, blend_mode) = first_blending_options + .map(|(first_opacity, first_fill, first_blend_mode)| { let mut opacity_identical = true; + let mut fill_identical = true; let mut blend_mode_identical = true; - for (opacity, blend_mode) in result_opacity_and_blend_mode { + for (opacity, fill, blend_mode) in result_blending_options { if (opacity - first_opacity).abs() > (f64::EPSILON * 100.) { opacity_identical = false; } + if (fill - first_fill).abs() > (f64::EPSILON * 100.) { + fill_identical = false; + } if blend_mode != first_blend_mode { blend_mode_identical = false; } } - (opacity_identical.then_some(first_opacity), blend_mode_identical.then_some(first_blend_mode)) + ( + opacity_identical.then_some(first_opacity), + fill_identical.then_some(first_fill), + blend_mode_identical.then_some(first_blend_mode), + ) }) - .unwrap_or((None, None)); + .unwrap_or((None, None, None)); let blend_mode_menu_entries = BlendMode::list_svg_subset() .iter() @@ -2622,6 +2643,28 @@ impl DocumentMessageHandler { .max_width(100) .tooltip("Opacity") .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + NumberInput::new(fill) + .label("Fill") + .unit("%") + .display_decimal_places(0) + .disabled(disabled) + .min(0.) + .max(100.) + .range_min(Some(0.)) + .range_max(Some(100.)) + .mode_range() + .on_update(|number_input: &NumberInput| { + if let Some(value) = number_input.value { + DocumentMessage::SetFillForSelectedLayers { fill: value / 100. }.into() + } else { + Message::NoOp + } + }) + .on_commit(|_| DocumentMessage::AddTransaction.into()) + .max_width(100) + .tooltip("Fill") + .widget_holder(), ]; let layers_panel_control_bar_left = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]); 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 e95c7e9d98..18f4616015 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 @@ -21,6 +21,10 @@ pub enum GraphOperationMessage { layer: LayerNodeIdentifier, fill: Fill, }, + BlendingFillSet { + layer: LayerNodeIdentifier, + fill: f64, + }, OpacitySet { layer: LayerNodeIdentifier, opacity: f64, @@ -29,6 +33,9 @@ pub enum GraphOperationMessage { layer: LayerNodeIdentifier, blend_mode: BlendMode, }, + ClipModeToggle { + layer: LayerNodeIdentifier, + }, StrokeSet { layer: LayerNodeIdentifier, stroke: Stroke, 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 9f8b8586f1..3ee7622cd1 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 @@ -5,12 +5,13 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector}; use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::{NodeId, NodeInput}; use graphene_core::Color; use graphene_core::renderer::Quad; use graphene_core::text::{Font, TypesettingConfig}; -use graphene_core::vector::style::{Fill, Gradient, GradientStops, GradientType, LineCap, LineJoin, Stroke}; +use graphene_core::vector::style::{Fill, Gradient, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::convert_usvg_path; #[derive(Debug, Clone)] @@ -41,6 +42,11 @@ impl MessageHandler> for Gr modify_inputs.fill_set(fill); } } + GraphOperationMessage::BlendingFillSet { layer, fill } => { + if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { + modify_inputs.blending_fill_set(fill); + } + } GraphOperationMessage::OpacitySet { layer, opacity } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { modify_inputs.opacity_set(opacity); @@ -51,6 +57,12 @@ impl MessageHandler> for Gr modify_inputs.blend_mode_set(blend_mode); } } + GraphOperationMessage::ClipModeToggle { layer } => { + let clip_mode = get_clip_mode(layer, network_interface); + if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { + modify_inputs.clip_mode_toggle(clip_mode); + } + } GraphOperationMessage::StrokeSet { layer, stroke } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { modify_inputs.stroke_set(stroke); @@ -376,18 +388,20 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont 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, - line_cap: match stroke.linecap() { - usvg::LineCap::Butt => LineCap::Butt, - usvg::LineCap::Round => LineCap::Round, - usvg::LineCap::Square => LineCap::Square, + cap: match stroke.linecap() { + usvg::LineCap::Butt => StrokeCap::Butt, + usvg::LineCap::Round => StrokeCap::Round, + usvg::LineCap::Square => StrokeCap::Square, }, - line_join: match stroke.linejoin() { - usvg::LineJoin::Miter => LineJoin::Miter, - usvg::LineJoin::MiterClip => LineJoin::Miter, - usvg::LineJoin::Round => LineJoin::Round, - usvg::LineJoin::Bevel => LineJoin::Bevel, + join: match stroke.linejoin() { + usvg::LineJoin::Miter => StrokeJoin::Miter, + usvg::LineJoin::MiterClip => StrokeJoin::Miter, + usvg::LineJoin::Round => StrokeJoin::Round, + usvg::LineJoin::Bevel => StrokeJoin::Bevel, }, - line_join_miter_limit: stroke.miterlimit().get() as f64, + join_miter_limit: stroke.miterlimit().get() as f64, + align: StrokeAlign::Center, + paint_order: PaintOrder::StrokeAbove, transform, non_scaling: false, }) 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 f6648c72f8..4aa46c190d 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -15,8 +15,8 @@ use graphene_core::vector::brush_stroke::BrushStroke; use graphene_core::vector::style::{Fill, Stroke}; use graphene_core::vector::{PointId, VectorModificationType}; use graphene_core::{Artboard, Color}; -use graphene_std::GraphicGroupTable; use graphene_std::vector::{VectorData, VectorDataTable}; +use graphene_std::{GraphicGroupTable, NodeInputDecleration}; #[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] pub enum TransformIn { @@ -58,13 +58,13 @@ impl<'a> ModifyInputsContext<'a> { /// Non layer nodes directly upstream of a layer are treated as part of that layer. See insert_index == 2 in the diagram /// -----> Post node /// | if insert_index == 0, return (Post node, Some(Layer1)) - /// -> Layer1 + /// -> Layer1 /// ↑ if insert_index == 1, return (Layer1, Some(Layer2)) - /// -> Layer2 + /// -> Layer2 /// ↑ /// -> NonLayerNode /// ↑ if insert_index == 2, return (NonLayerNode, Some(Layer3)) - /// -> Layer3 + /// -> Layer3 /// if insert_index == 3, return (Layer3, None) pub fn get_post_node_with_index(network_interface: &NodeNetworkInterface, parent: LayerNodeIdentifier, insert_index: usize) -> InputConnector { let mut post_node_input_connector = if parent == LayerNodeIdentifier::ROOT_PARENT { @@ -333,37 +333,52 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Fill(fill), false), false); } + pub fn blend_mode_set(&mut self, blend_mode: BlendMode) { + let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return }; + let input_connector = InputConnector::node(blend_node_id, 1); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false); + } + pub fn opacity_set(&mut self, opacity: f64) { - let Some(opacity_node_id) = self.existing_node_id("Opacity", true) else { return }; - let input_connector = InputConnector::node(opacity_node_id, 1); + let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return }; + let input_connector = InputConnector::node(blend_node_id, 2); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(opacity * 100.), false), false); } - pub fn blend_mode_set(&mut self, blend_mode: BlendMode) { - let Some(blend_mode_node_id) = self.existing_node_id("Blend Mode", true) else { - return; - }; - let input_connector = InputConnector::node(blend_mode_node_id, 1); - self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false); + pub fn blending_fill_set(&mut self, fill: f64) { + let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return }; + let input_connector = InputConnector::node(blend_node_id, 3); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(fill * 100.), 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_node_id("Blending", true) else { return }; + let input_connector = InputConnector::node(clip_node_id, 4); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false); } pub fn stroke_set(&mut self, stroke: Stroke) { let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return }; - let input_connector = InputConnector::node(stroke_node_id, 1); + let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::>::INDEX); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(stroke.color), false), true); - let input_connector = InputConnector::node(stroke_node_id, 2); + 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, 3); + let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeAlign(stroke.align), false), false); + let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::CapInput::INDEX); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeCap(stroke.cap), false), true); + let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::JoinInput::INDEX); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeJoin(stroke.join), false), true); + let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::MiterLimitInput::INDEX); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false); + let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput::INDEX); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false); + let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::INDEX); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::VecF64(stroke.dash_lengths), false), true); - let input_connector = InputConnector::node(stroke_node_id, 4); + let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput::INDEX); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true); - let input_connector = InputConnector::node(stroke_node_id, 5); - self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::LineCap(stroke.line_cap), false), true); - let input_connector = InputConnector::node(stroke_node_id, 6); - self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::LineJoin(stroke.line_join), false), true); - let input_connector = InputConnector::node(stroke_node_id, 7); - self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.line_join_miter_limit), false), false); } /// Update the transform value of the upstream Transform node based a change to its existing value and the given parent transform. diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index e53956dd65..ff7d4e7c52 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -15,6 +15,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{ use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry}; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; +use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode; use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use glam::{DAffine2, DVec2, IVec2}; @@ -2442,6 +2443,7 @@ impl NodeGraphMessageHandler { } }); + let clippable = layer.can_be_clipped(network_interface.document_metadata()); let data = LayerPanelEntry { id: node_id, alias: network_interface.display_name(&node_id, &[]), @@ -2461,6 +2463,8 @@ impl NodeGraphMessageHandler { selected: selected_layers.contains(&node_id), ancestor_of_selected: ancestors_of_selected.contains(&node_id), descendant_of_selected: descendants_of_selected.contains(&node_id), + clipped: get_clip_mode(layer, network_interface).unwrap_or(false) && clippable, + clippable, }; responses.add(FrontendMessage::UpdateDocumentLayerDetails { data }); } 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 f2c4700dbc..aa38fb148e 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -20,7 +20,7 @@ use graphene_core::raster::{ use graphene_core::text::Font; use graphene_core::vector::generator_nodes::grid; use graphene_core::vector::misc::CentroidType; -use graphene_core::vector::style::{GradientType, LineCap, LineJoin}; +use graphene_core::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::animation::RealTimeMode; use graphene_std::ops::XY; use graphene_std::transform::{Footprint, ReferencePoint}; @@ -233,8 +233,10 @@ pub(crate) fn property_from_type( Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).disabled(false).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).disabled(false).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), - Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), - Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), @@ -1679,20 +1681,43 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) - return Vec::new(); } }; - let color_index = 1; - let weight_index = 2; - let dash_lengths_index = 3; - let dash_offset_index = 4; - let line_cap_index = 5; - let line_join_index = 6; - let miter_limit_index = 7; + let color_index = graphene_std::vector::stroke::ColorInput::>::INDEX; + let weight_index = graphene_std::vector::stroke::WeightInput::INDEX; + let align_index = graphene_std::vector::stroke::AlignInput::INDEX; + let cap_index = graphene_std::vector::stroke::CapInput::INDEX; + let join_index = graphene_std::vector::stroke::JoinInput::INDEX; + let miter_limit_index = graphene_std::vector::stroke::MiterLimitInput::INDEX; + let paint_order_index = graphene_std::vector::stroke::PaintOrderInput::INDEX; + let dash_lengths_index = graphene_std::vector::stroke::DashLengthsInput::INDEX; + let dash_offset_index = graphene_std::vector::stroke::DashOffsetInput::INDEX; let color = color_widget(ParameterWidgetsInfo::from_index(document_node, node_id, color_index, true, context), ColorInput::default()); let weight = number_widget( ParameterWidgetsInfo::from_index(document_node, node_id, weight_index, true, context), NumberInput::default().unit(" px").min(0.), ); - + let align = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, align_index, true, context)) + .property_row(); + let cap = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, cap_index, true, context)) + .property_row(); + let join = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, join_index, true, context)) + .property_row(); + let miter_limit = number_widget( + ParameterWidgetsInfo::from_index(document_node, node_id, miter_limit_index, true, context), + NumberInput::default().min(0.).disabled({ + let join_value = match &document_node.inputs[join_index].as_value() { + Some(TaggedValue::StrokeJoin(x)) => x, + _ => &StrokeJoin::Miter, + }; + join_value != &StrokeJoin::Miter + }), + ); + let paint_order = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, paint_order_index, true, context)) + .property_row(); let dash_lengths_val = match &document_node.inputs[dash_lengths_index].as_value() { Some(TaggedValue::VecF64(x)) => x, _ => &vec![], @@ -1703,29 +1728,17 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) - ); let number_input = NumberInput::default().unit(" px").disabled(dash_lengths_val.is_empty()); let dash_offset = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, dash_offset_index, true, context), number_input); - let line_cap = enum_choice::() - .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_cap_index, true, context)) - .property_row(); - let line_join = enum_choice::() - .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)) - .property_row(); - let line_join_val = match &document_node.inputs[line_join_index].as_value() { - Some(TaggedValue::LineJoin(x)) => x, - _ => &LineJoin::Miter, - }; - let miter_limit = number_widget( - ParameterWidgetsInfo::from_index(document_node, node_id, miter_limit_index, true, context), - NumberInput::default().min(0.).disabled(line_join_val != &LineJoin::Miter), - ); vec![ color, LayoutGroup::Row { widgets: weight }, + align, + cap, + join, + LayoutGroup::Row { widgets: miter_limit }, + paint_order, LayoutGroup::Row { widgets: dash_lengths }, LayoutGroup::Row { widgets: dash_offset }, - line_cap, - line_join, - LayoutGroup::Row { widgets: miter_limit }, ] } @@ -1737,25 +1750,27 @@ pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesConte return Vec::new(); } }; - let distance_index = 1; - let line_join_index = 2; - let miter_limit_index = 3; + let distance_index = graphene_std::vector::offset_path::DistanceInput::INDEX; + let join_index = graphene_std::vector::offset_path::JoinInput::INDEX; + let miter_limit_index = graphene_std::vector::offset_path::MiterLimitInput::INDEX; let number_input = NumberInput::default().unit(" px"); let distance = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, distance_index, true, context), number_input); - let line_join = enum_choice::() - .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)) + let join = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, join_index, true, context)) .property_row(); - let line_join_val = match &document_node.inputs[line_join_index].as_value() { - Some(TaggedValue::LineJoin(x)) => x, - _ => &LineJoin::Miter, - }; - let number_input = NumberInput::default().min(0.).disabled(line_join_val != &LineJoin::Miter); + let number_input = NumberInput::default().min(0.).disabled({ + let join_val = match &document_node.inputs[join_index].as_value() { + Some(TaggedValue::StrokeJoin(x)) => x, + _ => &StrokeJoin::Miter, + }; + join_val != &StrokeJoin::Miter + }); let miter_limit = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, miter_limit_index, true, context), number_input); - vec![LayoutGroup::Row { widgets: distance }, line_join, LayoutGroup::Row { widgets: miter_limit }] + vec![LayoutGroup::Row { widgets: distance }, join, LayoutGroup::Row { widgets: miter_limit }] } pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index f312ba59ae..f640fb38c2 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -287,6 +287,12 @@ impl LayerNodeIdentifier { child.ancestors(metadata).any(|ancestor| ancestor == self) } + /// Is the layer last child of parent group? Used for clipping + pub fn can_be_clipped(self, metadata: &DocumentMetadata) -> bool { + self.parent(metadata) + .map_or(false, |layer| layer.last_child(metadata).expect("Parent accessed via child should have children") != self) + } + /// Iterator over all direct children (excluding self and recursive children) pub fn children(self, metadata: &DocumentMetadata) -> AxisIter { AxisIter { diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 90f3304acd..f5c6d5c876 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -1160,6 +1160,13 @@ impl NodeNetworkInterface { .and_then(|node_metadata| node_metadata.persistent_metadata.input_properties.get(index)) } + pub fn insert_input_properties_row(&mut self, node_id: &NodeId, index: usize, network_path: &[NodeId]) { + let row = ("", "TODO").into(); + let _ = self + .node_metadata_mut(node_id, network_path) + .map(|node_metadata| node_metadata.persistent_metadata.input_properties.insert(index - 1, row)); + } + pub fn input_metadata(&self, node_id: &NodeId, index: usize, field: &str, network_path: &[NodeId]) -> Option<&Value> { let Some(input_row) = self.input_properties_row(node_id, index, network_path) else { log::error!("Could not get input_row in get_input_metadata"); diff --git a/editor/src/messages/portfolio/document/utility_types/nodes.rs b/editor/src/messages/portfolio/document/utility_types/nodes.rs index 2cac92257c..d81f1693d7 100644 --- a/editor/src/messages/portfolio/document/utility_types/nodes.rs +++ b/editor/src/messages/portfolio/document/utility_types/nodes.rs @@ -55,6 +55,8 @@ pub struct LayerPanelEntry { pub ancestor_of_selected: bool, #[serde(rename = "descendantOfSelected")] pub descendant_of_selected: bool, + pub clipped: bool, + pub clippable: bool, } #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)] diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index ca80099238..6444ea5fec 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -24,7 +24,7 @@ use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput}; use graphene_core::renderer::Quad; use graphene_core::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{Fill, FillType, Gradient}; +use graphene_std::vector::style::{Fill, FillType, Gradient, PaintOrder, StrokeAlign}; use graphene_std::vector::{VectorData, VectorDataTable}; use std::vec; @@ -678,6 +678,30 @@ impl MessageHandler> for PortfolioMes } } + // Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644) + if reference == "Stroke" && inputs_count == 8 { + let node_definition = resolve_document_node_type(reference).unwrap(); + let document_node = node_definition.default_node_template().document_node; + document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone()); + document.network_interface.insert_input_properties_row(node_id, 8, network_path); + document.network_interface.insert_input_properties_row(node_id, 9, network_path); + + let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path); + let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false); + let paint_order_input = NodeInput::value(TaggedValue::PaintOrder(PaintOrder::StrokeAbove), false); + + document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 3), align_input, network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[6].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[7].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 7), paint_order_input, network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 8), old_inputs[3].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 9), old_inputs[4].clone(), network_path); + } + // Rename the old "Splines from Points" node to "Spline" and upgrade it to the new "Spline" node if reference == "Splines from Points" { document.network_interface.set_reference(node_id, network_path, Some("Spline".to_string())); 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 a6e6f00a21..dea5319164 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -13,6 +13,7 @@ use graphene_core::raster::BlendMode; use graphene_core::raster::image::RasterDataTable; use graphene_core::text::{Font, TypesettingConfig}; use graphene_core::vector::style::Gradient; +use graphene_std::NodeInputDecleration; use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorModificationType}; use std::collections::VecDeque; @@ -258,7 +259,7 @@ pub fn get_viewport_pivot(layer: LayerNodeIdentifier, network_interface: &NodeNe network_interface.document_metadata().transform_to_viewport(layer).transform_point2(min + (max - min) * pivot) } -/// Get the current gradient of a layer from the closest Fill node +/// Get the current gradient of a layer from the closest "Fill" node. pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { let fill_index = 1; @@ -269,7 +270,7 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI Some(gradient.clone()) } -/// Get the current fill of a layer from the closest Fill node +/// 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; @@ -280,16 +281,16 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor Some(color.to_linear_srgb()) } -/// Get the current blend mode of a layer from the closest Blend Mode node +/// Get the current blend mode of a layer from the closest "Blending" node. pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { - let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blend Mode")?; + let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?; let TaggedValue::BlendMode(blend_mode) = inputs.get(1)?.as_value()? else { return None; }; Some(*blend_mode) } -/// Get the current opacity of a layer from the closest Opacity node. +/// Get the current opacity of a layer from the closest "Blending" node. /// This may differ from the actual opacity contained within the data type reaching this layer, because that actual opacity may be: /// - Multiplied with additional opacity nodes earlier in the chain /// - Set by an Opacity node with an exposed input value driven by another node @@ -298,13 +299,29 @@ pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetwor /// /// With those limitations in mind, the intention of this function is to show just the value already present in an upstream Opacity node so that value can be directly edited. pub fn get_opacity(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { - let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Opacity")?; - let TaggedValue::F64(opacity) = inputs.get(1)?.as_value()? else { + let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?; + let TaggedValue::F64(opacity) = inputs.get(2)?.as_value()? else { return None; }; Some(*opacity) } +pub fn get_clip_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?; + let TaggedValue::Bool(clip) = inputs.get(4)?.as_value()? else { + return None; + }; + Some(*clip) +} + +pub fn get_fill(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?; + let TaggedValue::F64(fill) = inputs.get(3)?.as_value()? else { + return None; + }; + Some(*fill) +} + pub fn get_fill_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Fill") } @@ -356,7 +373,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter } pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { - let weight_node_input_index = 2; + let weight_node_input_index = graphene_std::vector::stroke::WeightInput::INDEX; if let TaggedValue::F64(width) = NodeGraphLayer::new(layer, network_interface).find_input("Stroke", weight_node_input_index)? { Some(*width) } else { diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index da4b143f74..47256ccfe6 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -338,7 +338,15 @@ impl NodeGraphExecutor { fn debug_render(render_object: impl GraphicElementRendered, transform: DAffine2, responses: &mut VecDeque) { // Setup rendering let mut render = SvgRender::new(); - let render_params = RenderParams::new(ViewMode::Normal, None, false, false, false); + let render_params = RenderParams { + view_mode: ViewMode::Normal, + culling_bounds: None, + thumbnail: false, + hide_artboards: false, + for_export: false, + for_mask: false, + alignment_parent_transform: None, + }; // Render SVG render_object.render_svg(&mut render, &render_params); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 5510e1dad7..2bc11bca48 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -323,7 +323,15 @@ impl NodeRuntime { let bounds = graphic_element.bounding_box(DAffine2::IDENTITY, true); // Render the thumbnail from a `GraphicElement` into an SVG string - let render_params = RenderParams::new(ViewMode::Normal, bounds, true, false, false); + let render_params = RenderParams { + view_mode: ViewMode::Normal, + culling_bounds: bounds, + thumbnail: true, + hide_artboards: false, + for_export: false, + for_mask: false, + alignment_parent_transform: None, + }; let mut render = SvgRender::new(); graphic_element.render_svg(&mut render, &render_params); diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index cc9ea37c49..092a83fbac 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -1,5 +1,5 @@