diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 69b77180..2018d626 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -102,7 +102,9 @@ impl Dispatcher { let font = Font::new(DEFAULT_FONT_FAMILY.into(), DEFAULT_FONT_STYLE.into()); queue.add(FrontendMessage::TriggerFontLoad { font, is_default: true }); } - + Message::Batched(messages) => { + messages.iter().for_each(|message| self.handle_message(message.to_owned())); + } Message::Broadcast(message) => self.message_handlers.broadcast_message_handler.process_message(message, &mut queue, ()), Message::Debug(message) => { self.message_handlers.debug_message_handler.process_message(message, &mut queue, ()); diff --git a/editor/src/messages/message.rs b/editor/src/messages/message.rs index 5aba1021..cb6de732 100644 --- a/editor/src/messages/message.rs +++ b/editor/src/messages/message.rs @@ -7,6 +7,7 @@ use graphite_proc_macros::*; pub enum Message { NoOp, Init, + Batched(Box<[Message]>), #[child] Broadcast(BroadcastMessage), diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/document_node_types.rs index 8dd3bfe1..93dcd47c 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_types.rs @@ -2226,13 +2226,20 @@ fn static_nodes() -> Vec { name: "Rectangle", category: "Vector", implementation: DocumentNodeImplementation::Network(NodeNetwork { - imports: vec![NodeId(0), NodeId(0), NodeId(0)], + imports: vec![NodeId(0), NodeId(0), NodeId(0), NodeId(0), NodeId(0), NodeId(0)], exports: vec![NodeOutput::new(NodeId(1), 0)], nodes: vec![ DocumentNode { name: "Rectangle Generator".to_string(), - inputs: vec![NodeInput::Network(concrete!(())), NodeInput::Network(concrete!(f64)), NodeInput::Network(concrete!(f64))], - implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::generator_nodes::RectangleGenerator<_, _>")), + inputs: vec![ + NodeInput::Network(concrete!(())), + NodeInput::Network(concrete!(f64)), + NodeInput::Network(concrete!(f64)), + NodeInput::Network(concrete!(bool)), + NodeInput::Network(generic!(T)), + NodeInput::Network(concrete!(bool)), + ], + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::generator_nodes::RectangleGenerator<_, _, _, _, _>")), ..Default::default() }, DocumentNode { @@ -2253,6 +2260,9 @@ fn static_nodes() -> Vec { DocumentInputType::none(), DocumentInputType::value("Size X", TaggedValue::F64(100.), false), DocumentInputType::value("Size Y", TaggedValue::F64(100.), false), + DocumentInputType::value("Individual Corner Radii", TaggedValue::Bool(false), false), + DocumentInputType::value("Corner Radius", TaggedValue::F64(0.), false), + DocumentInputType::value("Clamped", TaggedValue::Bool(true), false), ], outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], properties: node_properties::rectangle_properties, 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 206fc71a..fdcdf3a4 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1503,12 +1503,136 @@ pub fn ellipse_properties(document_node: &DocumentNode, node_id: NodeId, _contex } pub fn rectangle_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let operand = |name: &str, index| { - let widgets = number_widget(document_node, node_id, index, name, NumberInput::default(), true); + let size_x_index = 1; + let size_y_index = 2; + let corner_rounding_type_index = 3; + let corner_radius_index = 4; + let clamped_index = 5; - LayoutGroup::Row { widgets } - }; - vec![operand("Size X", 1), operand("Size Y", 2)] + // Size X + let size_x = number_widget(document_node, node_id, size_x_index, "Size X", NumberInput::default(), true); + + // Size Y + let size_y = number_widget(document_node, node_id, size_y_index, "Size Y", NumberInput::default(), true); + + // Corner Radius + let mut corner_radius_row_1 = start_widgets(document_node, node_id, corner_radius_index, "Corner Radius", FrontendGraphDataType::Number, true); + corner_radius_row_1.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + + let mut corner_radius_row_2 = vec![Separator::new(SeparatorType::Unrelated).widget_holder()]; + corner_radius_row_2.push(TextLabel::new("").widget_holder()); + add_blank_assist(&mut corner_radius_row_2); + + if let &NodeInput::Value { + tagged_value: TaggedValue::Bool(is_individual), + exposed: false, + } = &document_node.inputs[corner_rounding_type_index] + { + // Values + let uniform_val = match document_node.inputs[corner_radius_index] { + NodeInput::Value { + tagged_value: TaggedValue::F64(x), + exposed: false, + } => x, + NodeInput::Value { + tagged_value: TaggedValue::F64Array4(x), + exposed: false, + } => x[0], + _ => 0., + }; + let individual_val = match document_node.inputs[corner_radius_index] { + NodeInput::Value { + tagged_value: TaggedValue::F64Array4(x), + exposed: false, + } => x, + NodeInput::Value { + tagged_value: TaggedValue::F64(x), + exposed: false, + } => [x; 4], + _ => [0.; 4], + }; + + // Uniform/individual radio input widget + let uniform = RadioEntryData::new("Uniform") + .label("Uniform") + .on_update(move |_| { + Message::Batched(Box::new([ + NodeGraphMessage::SetInputValue { + node_id, + input_index: corner_rounding_type_index, + value: TaggedValue::Bool(false), + } + .into(), + NodeGraphMessage::SetInputValue { + node_id, + input_index: corner_radius_index, + value: TaggedValue::F64(uniform_val), + } + .into(), + ])) + }) + .on_commit(commit_value); + let individual = RadioEntryData::new("Individual") + .label("Individual") + .on_update(move |_| { + Message::Batched(Box::new([ + NodeGraphMessage::SetInputValue { + node_id, + input_index: corner_rounding_type_index, + value: TaggedValue::Bool(true), + } + .into(), + NodeGraphMessage::SetInputValue { + node_id, + input_index: corner_radius_index, + value: TaggedValue::F64Array4(individual_val), + } + .into(), + ])) + }) + .on_commit(commit_value); + let radio_input = RadioInput::new(vec![uniform, individual]).selected_index(Some(is_individual as u32)).widget_holder(); + corner_radius_row_1.push(radio_input); + + // Radius value input widget + let input_widget = if is_individual { + let from_string = |string: &str| { + string + .split(&[',', ' ']) + .filter(|x| !x.is_empty()) + .map(str::parse::) + .collect::, _>>() + .ok() + .map(|v| { + let arr: Box<[f64; 4]> = v.into_boxed_slice().try_into().unwrap_or_default(); + *arr + }) + .map(TaggedValue::F64Array4) + }; + TextInput::default() + .value(individual_val.iter().map(|v| v.to_string()).collect::>().join(", ")) + .on_update(optionally_update_value(move |x: &TextInput| from_string(&x.value), node_id, corner_radius_index)) + .widget_holder() + } else { + NumberInput::default() + .value(Some(uniform_val)) + .on_update(update_value(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, corner_radius_index)) + .on_commit(commit_value) + .widget_holder() + }; + corner_radius_row_2.push(input_widget); + } + + // Clamped + let clamped = bool_widget(document_node, node_id, clamped_index, "Clamped", true); + + vec![ + LayoutGroup::Row { widgets: size_x }, + LayoutGroup::Row { widgets: size_y }, + LayoutGroup::Row { widgets: corner_radius_row_1 }, + LayoutGroup::Row { widgets: corner_radius_row_2 }, + LayoutGroup::Row { widgets: clamped }, + ] } pub fn regular_polygon_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index 31f0ade3..bd59d5a9 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -217,6 +217,39 @@ impl Subpath { Self::from_anchors([corner1, DVec2::new(corner2.x, corner1.y), corner2, DVec2::new(corner1.x, corner2.y)], true) } + /// Constructs a rounded rectangle with `corner1` and `corner2` as the two corners and `corner_radii` as the radii of the corners: `[top_left, top_right, bottom_right, bottom_left]`. + pub fn new_rounded_rect(corner1: DVec2, corner2: DVec2, corner_radii: [f64; 4]) -> Self { + use std::f64::consts::{FRAC_1_SQRT_2, PI}; + + let new_arc = |center: DVec2, corner: DVec2, radius: f64| -> Vec> { + let point1 = center + DVec2::from_angle(-PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2; + let point2 = center + DVec2::from_angle(PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2; + if radius == 0. { + return vec![ManipulatorGroup::new_anchor(point1), ManipulatorGroup::new_anchor(point2)]; + } + + // Based on https://pomax.github.io/bezierinfo/#circles_cubic + const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; + let handle_offset = radius * HANDLE_OFFSET_FACTOR; + vec![ + ManipulatorGroup::new_anchor(point1), + ManipulatorGroup::new(point1, None, Some(point1 + handle_offset * (corner - point1).normalize())), + ManipulatorGroup::new(point2, Some(point2 + handle_offset * (corner - point2).normalize()), None), + ManipulatorGroup::new_anchor(point2), + ] + }; + Self::new( + [ + new_arc(DVec2::new(corner1.x + corner_radii[0], corner1.y + corner_radii[0]), DVec2::new(corner1.x, corner1.y), corner_radii[0]), + new_arc(DVec2::new(corner2.x - corner_radii[1], corner1.y + corner_radii[1]), DVec2::new(corner2.x, corner1.y), corner_radii[1]), + new_arc(DVec2::new(corner2.x - corner_radii[2], corner2.y - corner_radii[2]), DVec2::new(corner2.x, corner2.y), corner_radii[2]), + new_arc(DVec2::new(corner1.x + corner_radii[3], corner2.y - corner_radii[3]), DVec2::new(corner1.x, corner2.y), corner_radii[3]), + ] + .concat(), + true, + ) + } + /// Constructs an ellipse with `corner1` and `corner2` as the two corners of the bounding box. pub fn new_ellipse(corner1: DVec2, corner2: DVec2) -> Self { let size = (corner1 - corner2).abs(); diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index c74cade2..a3496fb4 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -32,18 +32,47 @@ fn ellipse_generator(_input: (), radius_x: f64, radius_y: f64) -> VectorData { } #[derive(Debug, Clone, Copy)] -pub struct RectangleGenerator { +pub struct RectangleGenerator { size_x: SizeX, size_y: SizeY, + is_individual: IsIndividual, + corner_radius: CornerRadius, + clamped: Clamped, +} + +trait CornerRadius { + fn generate(self, size: DVec2, clamped: bool) -> super::VectorData; +} +impl CornerRadius for f64 { + fn generate(self, size: DVec2, clamped: bool) -> super::VectorData { + let clamped_radius = if clamped { self.clamp(0., size.x.min(size.y).max(0.) / 2.) } else { self }; + super::VectorData::from_subpaths(vec![Subpath::new_rounded_rect(size / -2., size / 2., [clamped_radius; 4])]) + } +} +impl CornerRadius for [f64; 4] { + fn generate(self, size: DVec2, clamped: bool) -> super::VectorData { + let clamped_radius = if clamped { + // Algorithm follows the CSS spec: + + let mut scale_factor: f64 = 1.; + for i in 0..4 { + let side_length = if i % 2 == 0 { size.x } else { size.y }; + let adjacent_corner_radius_sum = self[i] + self[(i + 1) % 4]; + if side_length < adjacent_corner_radius_sum { + scale_factor = scale_factor.min(side_length / adjacent_corner_radius_sum); + } + } + self.map(|x| x * scale_factor) + } else { + self + }; + super::VectorData::from_subpaths(vec![Subpath::new_rounded_rect(size / -2., size / 2., clamped_radius)]) + } } #[node_macro::node_fn(RectangleGenerator)] -fn square_generator(_input: (), size_x: f64, size_y: f64) -> VectorData { - let size = DVec2::new(size_x, size_y); - let corner1 = -size / 2.; - let corner2 = size / 2.; - - super::VectorData::from_subpath(Subpath::new_rect(corner1, corner2)) +fn square_generator(_input: (), size_x: f64, size_y: f64, is_individual: bool, corner_radius: T, clamped: bool) -> VectorData { + corner_radius.generate(DVec2::new(size_x, size_y), clamped) } #[derive(Debug, Clone, Copy)] diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 78c9f9d8..3133ccc6 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -42,6 +42,7 @@ pub enum TaggedValue { VectorData(graphene_core::vector::VectorData), Fill(graphene_core::vector::style::Fill), Stroke(graphene_core::vector::style::Stroke), + F64Array4([f64; 4]), VecF64(Vec), VecDVec2(Vec), RedGreenBlue(graphene_core::raster::RedGreenBlue), @@ -109,6 +110,7 @@ impl Hash for TaggedValue { Self::VectorData(x) => x.hash(state), Self::Fill(x) => x.hash(state), Self::Stroke(x) => x.hash(state), + Self::F64Array4(x) => x.iter().for_each(|x| x.to_bits().hash(state)), Self::VecF64(x) => x.iter().for_each(|val| val.to_bits().hash(state)), Self::VecDVec2(x) => x.iter().for_each(|val| val.to_array().iter().for_each(|x| x.to_bits().hash(state))), Self::RedGreenBlue(x) => x.hash(state), @@ -183,6 +185,7 @@ impl<'a> TaggedValue { TaggedValue::VectorData(x) => Box::new(x), TaggedValue::Fill(x) => Box::new(x), TaggedValue::Stroke(x) => Box::new(x), + TaggedValue::F64Array4(x) => Box::new(x), TaggedValue::VecF64(x) => Box::new(x), TaggedValue::VecDVec2(x) => Box::new(x), TaggedValue::RedGreenBlue(x) => Box::new(x), @@ -259,6 +262,7 @@ impl<'a> TaggedValue { TaggedValue::VectorData(_) => concrete!(graphene_core::vector::VectorData), TaggedValue::Fill(_) => concrete!(graphene_core::vector::style::Fill), TaggedValue::Stroke(_) => concrete!(graphene_core::vector::style::Stroke), + TaggedValue::F64Array4(_) => concrete!([f64; 4]), TaggedValue::VecF64(_) => concrete!(Vec), TaggedValue::VecDVec2(_) => concrete!(Vec), TaggedValue::RedGreenBlue(_) => concrete!(graphene_core::raster::RedGreenBlue), diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 94993af5..fccdcf83 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -757,7 +757,8 @@ fn node_registry() -> HashMap, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, Footprint => VectorData, () => u32, () => f64]), register_node!(graphene_core::vector::generator_nodes::CircleGenerator<_>, input: (), params: [f64]), register_node!(graphene_core::vector::generator_nodes::EllipseGenerator<_, _>, input: (), params: [f64, f64]), - register_node!(graphene_core::vector::generator_nodes::RectangleGenerator<_, _>, input: (), params: [f64, f64]), + register_node!(graphene_core::vector::generator_nodes::RectangleGenerator<_, _, _, _, _>, input: (), params: [f64, f64, bool, f64, bool]), + register_node!(graphene_core::vector::generator_nodes::RectangleGenerator<_, _, _, _, _>, input: (), params: [f64, f64, bool, [f64; 4], bool]), register_node!(graphene_core::vector::generator_nodes::RegularPolygonGenerator<_, _>, input: (), params: [u32, f64]), register_node!(graphene_core::vector::generator_nodes::StarGenerator<_, _, _>, input: (), params: [u32, f64, f64]), register_node!(graphene_core::vector::generator_nodes::LineGenerator<_, _>, input: (), params: [DVec2, DVec2]),