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:
parent
d09e7eaf86
commit
438c45eb80
|
|
@ -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, ());
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use graphite_proc_macros::*;
|
|||
pub enum Message {
|
||||
NoOp,
|
||||
Init,
|
||||
Batched(Box<[Message]>),
|
||||
|
||||
#[child]
|
||||
Broadcast(BroadcastMessage),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
|
|
|
|||
Loading…
Reference in New Issue