Add corner rounding to the Rectangle node (#1648)

* add skeleton implementation

* add corner rounding

* fix crash when `border_radius` is zero

* rename `Border Radius` to `Corner Radius`

* add clamped property

* add `TaggedValue::F64Array4`

* add frontend support for individual corner rounding

* added individual corner rounding

* fix rebase

* change default values when switching rounding type

* fix crash caused by negative scale

* remove `Any` trait

* add `Message::Batched`

* fix stale property bug

* add smarter clamping for individual rounding

* Rearrange widgets in properties panel

* update individual clamping algorithm

* add better variable names

* make variable names clearer

* Final code cleanup

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Karthik Prakash 2024-04-06 10:39:55 +05:30 committed by GitHub
parent d09e7eaf86
commit 438c45eb80
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 221 additions and 17 deletions

View File

@ -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, ());

View File

@ -7,6 +7,7 @@ use graphite_proc_macros::*;
pub enum Message {
NoOp,
Init,
Batched(Box<[Message]>),
#[child]
Broadcast(BroadcastMessage),

View File

@ -2226,13 +2226,20 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
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<DocumentNodeDefinition> {
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,

View File

@ -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<LayoutGroup> {
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::<f64>)
.collect::<Result<Vec<f64>, _>>()
.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::<Vec<_>>().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<LayoutGroup> {

View File

@ -217,6 +217,39 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
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<ManipulatorGroup<ManipulatorGroupId>> {
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();

View File

@ -32,18 +32,47 @@ fn ellipse_generator(_input: (), radius_x: f64, radius_y: f64) -> VectorData {
}
#[derive(Debug, Clone, Copy)]
pub struct RectangleGenerator<SizeX, SizeY> {
pub struct RectangleGenerator<SizeX, SizeY, IsIndividual, CornerRadius, Clamped> {
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: <https://drafts.csswg.org/css-backgrounds/#corner-overlap>
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<T: CornerRadius>(_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)]

View File

@ -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<f64>),
VecDVec2(Vec<DVec2>),
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<f64>),
TaggedValue::VecDVec2(_) => concrete!(Vec<DVec2>),
TaggedValue::RedGreenBlue(_) => concrete!(graphene_core::raster::RedGreenBlue),

View File

@ -757,7 +757,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::vector::MorphNode<_, _, _, _>, 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]),