Skip to content

Commit 8383a3a

Browse files
0HyperCubeKeavon
andauthored
New node: 'Extrude' (#3414)
* Add extrude node * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
1 parent 5ebf6d6 commit 8383a3a

File tree

6 files changed

+230
-8
lines changed

6 files changed

+230
-8
lines changed

editor/src/messages/portfolio/document/node_graph/node_properties.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use graphene_std::raster::{
2323
use graphene_std::table::{Table, TableRow};
2424
use graphene_std::text::{Font, TextAlign};
2525
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
26-
use graphene_std::vector::misc::{ArcType, CentroidType, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType};
26+
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType};
2727
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
2828

2929
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
@@ -219,6 +219,7 @@ pub(crate) fn property_from_type(
219219
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
220220
Some(x) if x == TypeId::of::<TextAlign>() => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
221221
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
222+
Some(x) if x == TypeId::of::<ExtrudeJoiningAlgorithm>() => enum_choice::<ExtrudeJoiningAlgorithm>().for_socket(default_info).property_row(),
222223
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
223224
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
224225
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),

node-graph/graph-craft/src/document/value.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ tagged_value! {
245245
GridType(vector::misc::GridType),
246246
ArcType(vector::misc::ArcType),
247247
MergeByDistanceAlgorithm(vector::misc::MergeByDistanceAlgorithm),
248+
ExtrudeJoiningAlgorithm(vector::misc::ExtrudeJoiningAlgorithm),
248249
PointSpacingType(vector::misc::PointSpacingType),
249250
SpiralType(vector::misc::SpiralType),
250251
#[serde(alias = "LineCap")]

node-graph/interpreted-executor/src/node_registry.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
134134
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]),
135135
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]),
136136
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]),
137+
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]),
137138
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
138139
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::FillType]),
139140
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]),
@@ -220,6 +221,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
220221
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]),
221222
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]),
222223
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]),
224+
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]),
223225
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
224226
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]),
225227
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]),

node-graph/libraries/vector-types/src/vector/misc.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@ pub enum MergeByDistanceAlgorithm {
8383
Topological,
8484
}
8585

86+
#[repr(C)]
87+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
88+
#[widget(Radio)]
89+
pub enum ExtrudeJoiningAlgorithm {
90+
All,
91+
#[default]
92+
Extrema,
93+
None,
94+
}
95+
8696
#[repr(C)]
8797
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
8898
#[widget(Radio)]

node-graph/libraries/vector-types/src/vector/vector_attributes.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@ impl SegmentDomain {
300300
self.end_point[segment_index] = new;
301301
}
302302

303+
pub fn set_handles(&mut self, segment_index: usize, new: BezierHandles) {
304+
self.handles[segment_index] = new;
305+
}
306+
303307
pub fn handles(&self) -> &[BezierHandles] {
304308
&self.handles
305309
}

node-graph/nodes/vector/src/vector_nodes.rs

Lines changed: 211 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, evaluat
2121
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
2222
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
2323
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
24-
use vector_types::vector::misc::{CentroidType, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2};
24+
use vector_types::vector::misc::{CentroidType, ExtrudeJoiningAlgorithm, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2};
2525
use vector_types::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType, is_linear};
2626
use vector_types::vector::misc::{handles_to_segment, segment_to_handles};
2727
use vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke};
@@ -580,6 +580,210 @@ pub fn merge_by_distance(
580580
}
581581
}
582582

583+
pub mod extrude_algorithms {
584+
use glam::DVec2;
585+
use kurbo::{ParamCurve, ParamCurveDeriv};
586+
use vector_types::subpath::BezierHandles;
587+
use vector_types::vector::StrokeId;
588+
use vector_types::vector::misc::ExtrudeJoiningAlgorithm;
589+
590+
/// Convert [`vector_types::subpath::Bezier`] to [`kurbo::PathSeg`].
591+
fn bezier_to_path_seg(bezier: vector_types::subpath::Bezier) -> kurbo::PathSeg {
592+
let [start, end] = [(bezier.start().x, bezier.start().y), (bezier.end().x, bezier.end().y)];
593+
match bezier.handles {
594+
BezierHandles::Linear => kurbo::Line::new(start, end).into(),
595+
BezierHandles::Quadratic { handle } => kurbo::QuadBez::new(start, (handle.x, handle.y), end).into(),
596+
BezierHandles::Cubic { handle_start, handle_end } => kurbo::CubicBez::new(start, (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), end).into(),
597+
}
598+
}
599+
600+
/// Convert [`kurbo::CubicBez`] to [`vector_types::subpath::BezierHandles`].
601+
fn cubic_to_handles(cubic_bez: kurbo::CubicBez) -> BezierHandles {
602+
BezierHandles::Cubic {
603+
handle_start: DVec2::new(cubic_bez.p1.x, cubic_bez.p1.y),
604+
handle_end: DVec2::new(cubic_bez.p2.x, cubic_bez.p2.y),
605+
}
606+
}
607+
608+
/// Find the `t` values to split (where the tangent changes to be on the other side of the direction).
609+
fn find_splits(cubic_segment: kurbo::CubicBez, direction: DVec2) -> impl Iterator<Item = f64> {
610+
let derivative = cubic_segment.deriv();
611+
let convert = |x: kurbo::Point| DVec2::new(x.x, x.y);
612+
let derivative_points = [derivative.p0, derivative.p1, derivative.p2].map(convert);
613+
614+
let t_squared = derivative_points[0] - 2. * derivative_points[1] + derivative_points[2];
615+
let t_scalar = -2. * derivative_points[0] + 2. * derivative_points[1];
616+
let constant = derivative_points[0];
617+
618+
kurbo::common::solve_quadratic(constant.perp_dot(direction), t_scalar.perp_dot(direction), t_squared.perp_dot(direction))
619+
.into_iter()
620+
.filter(|&t| t > 1e-6 && t < 1. - 1e-6)
621+
}
622+
623+
/// Split so segments no longer have tangents on both sides of the direction vector.
624+
fn split(vector: &mut graphic_types::Vector, direction: DVec2) {
625+
let segment_count = vector.segment_domain.ids().len();
626+
let mut next_point = vector.point_domain.next_id();
627+
let mut next_segment = vector.segment_domain.next_id();
628+
629+
for segment_index in 0..segment_count {
630+
let (_, _, bezier) = vector.segment_points_from_index(segment_index);
631+
let mut start_index = vector.segment_domain.start_point()[segment_index];
632+
let pathseg = bezier_to_path_seg(bezier).to_cubic();
633+
let mut start_t = 0.;
634+
635+
for split_t in find_splits(pathseg, direction) {
636+
let [first, second] = [pathseg.subsegment(start_t..split_t), pathseg.subsegment(split_t..1.)];
637+
let [first_handles, second_handles] = [first, second].map(cubic_to_handles);
638+
let middle_point = next_point.next_id();
639+
let start_segment = next_segment.next_id();
640+
641+
let middle_point_index = vector.point_domain.len();
642+
vector.point_domain.push(middle_point, DVec2::new(first.end().x, first.end().y));
643+
vector.segment_domain.push(start_segment, start_index, middle_point_index, first_handles, StrokeId::ZERO);
644+
vector.segment_domain.set_start_point(segment_index, middle_point_index);
645+
vector.segment_domain.set_handles(segment_index, second_handles);
646+
647+
start_t = split_t;
648+
start_index = middle_point_index;
649+
}
650+
}
651+
}
652+
653+
/// Copy all segments with the offset of `direction`.
654+
fn offset_copy_all_segments(vector: &mut graphic_types::Vector, direction: DVec2) {
655+
let points_count = vector.point_domain.ids().len();
656+
let mut next_point = vector.point_domain.next_id();
657+
for index in 0..points_count {
658+
vector.point_domain.push(next_point.next_id(), vector.point_domain.positions()[index] + direction);
659+
}
660+
661+
let segment_count = vector.segment_domain.ids().len();
662+
let mut next_segment = vector.segment_domain.next_id();
663+
for index in 0..segment_count {
664+
vector.segment_domain.push(
665+
next_segment.next_id(),
666+
vector.segment_domain.start_point()[index] + points_count,
667+
vector.segment_domain.end_point()[index] + points_count,
668+
vector.segment_domain.handles()[index].apply_transformation(|x| x + direction),
669+
vector.segment_domain.stroke()[index],
670+
);
671+
}
672+
}
673+
674+
/// Join points from the original to the copied that are on opposite sides of the direction.
675+
fn join_extrema_edges(vector: &mut graphic_types::Vector, direction: DVec2) {
676+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
677+
enum Found {
678+
#[default]
679+
None,
680+
Positive,
681+
Negative,
682+
Both,
683+
Invalid,
684+
}
685+
686+
impl Found {
687+
fn update(&mut self, value: f64) {
688+
*self = match (*self, value > 0.) {
689+
(Found::None, true) => Found::Positive,
690+
(Found::None, false) => Found::Negative,
691+
(Found::Positive, true) | (Found::Negative, false) => Found::Both,
692+
_ => Found::Invalid,
693+
};
694+
}
695+
}
696+
697+
let first_half_points = vector.point_domain.len() / 2;
698+
let mut points = vec![Found::None; first_half_points];
699+
let first_half_segments = vector.segment_domain.ids().len() / 2;
700+
701+
for segment_id in 0..first_half_segments {
702+
let index = [vector.segment_domain.start_point()[segment_id], vector.segment_domain.end_point()[segment_id]];
703+
let position = index.map(|index| vector.point_domain.positions()[index]);
704+
705+
if position[0].abs_diff_eq(position[1], 1e-6) {
706+
continue; // Skip zero length segments
707+
}
708+
709+
points[index[0]].update(direction.perp_dot(position[1] - position[0]));
710+
points[index[1]].update(direction.perp_dot(position[0] - position[1]));
711+
}
712+
713+
let mut next_segment = vector.segment_domain.next_id();
714+
for (index, &point) in points.iter().enumerate().take(first_half_points) {
715+
if point != Found::Both {
716+
continue;
717+
}
718+
719+
vector
720+
.segment_domain
721+
.push(next_segment.next_id(), index, index + first_half_points, BezierHandles::Linear, StrokeId::ZERO);
722+
}
723+
}
724+
725+
/// Join all points from the original to the copied.
726+
fn join_all(vector: &mut graphic_types::Vector) {
727+
let mut next_segment = vector.segment_domain.next_id();
728+
let first_half = vector.point_domain.len() / 2;
729+
for index in 0..first_half {
730+
vector.segment_domain.push(next_segment.next_id(), index, index + first_half, BezierHandles::Linear, StrokeId::ZERO);
731+
}
732+
}
733+
734+
pub fn extrude(vector: &mut graphic_types::Vector, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) {
735+
split(vector, direction);
736+
offset_copy_all_segments(vector, direction);
737+
738+
match joining_algorithm {
739+
ExtrudeJoiningAlgorithm::Extrema => join_extrema_edges(vector, direction),
740+
ExtrudeJoiningAlgorithm::All => join_all(vector),
741+
ExtrudeJoiningAlgorithm::None => {}
742+
}
743+
}
744+
745+
#[cfg(test)]
746+
mod extrude_tests {
747+
use glam::DVec2;
748+
use kurbo::{ParamCurve, ParamCurveDeriv};
749+
750+
#[test]
751+
fn split_cubic() {
752+
let l1 = kurbo::CubicBez::new((0., 0.), (100., 0.), (100., 100.), (0., 100.));
753+
assert_eq!(super::find_splits(l1, DVec2::Y).collect::<Vec<f64>>(), vec![0.5]);
754+
assert!(super::find_splits(l1, DVec2::X).collect::<Vec<f64>>().is_empty());
755+
756+
let l2 = kurbo::CubicBez::new((0., 0.), (0., 0.), (100., 0.), (100., 0.));
757+
assert!(super::find_splits(l2, DVec2::X).collect::<Vec<f64>>().is_empty());
758+
759+
let l3 = kurbo::PathSeg::Line(kurbo::Line::new((0., 0.), (100., 0.)));
760+
assert!(super::find_splits(l3.to_cubic(), DVec2::X).collect::<Vec<f64>>().is_empty());
761+
762+
let l4 = kurbo::CubicBez::new((0., 0.), (100., -10.), (100., 110.), (0., 100.));
763+
let splits = super::find_splits(l4, DVec2::X).map(|t| l4.deriv().eval(t)).collect::<Vec<_>>();
764+
assert_eq!(splits.len(), 2);
765+
assert!(splits.iter().all(|&deriv| deriv.y.abs() < 1e-8), "{splits:?}");
766+
}
767+
768+
#[test]
769+
fn split_vector() {
770+
let curve = kurbo::PathSeg::Cubic(kurbo::CubicBez::new((0., 0.), (100., -10.), (100., 110.), (0., 100.)));
771+
let mut vector = graphic_types::Vector::from_bezpath(kurbo::BezPath::from_path_segments([curve].into_iter()));
772+
super::split(&mut vector, DVec2::X);
773+
assert_eq!(vector.segment_ids().len(), 3);
774+
assert_eq!(vector.point_domain.ids().len(), 4);
775+
}
776+
}
777+
}
778+
779+
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
780+
async fn extrude(_: impl Ctx, mut source: Table<Vector>, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> Table<Vector> {
781+
for TableRowMut { element: source, .. } in source.iter_mut() {
782+
extrude_algorithms::extrude(source, direction, joining_algorithm);
783+
}
784+
source
785+
}
786+
583787
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
584788
async fn box_warp(_: impl Ctx, content: Table<Vector>, #[expose] rectangle: Table<Vector>) -> Table<Vector> {
585789
let Some((target, target_transform)) = rectangle.get(0).map(|rect| (rect.element, rect.transform)) else {
@@ -1631,7 +1835,7 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
16311835
let target_segment_len = target_bezpath.segments().count();
16321836
let source_segment_len = source_bezpath.segments().count();
16331837

1634-
// Insert new segments to align the number of segments in sorce_bezpath and target_bezpath.
1838+
// Insert new segments to align the number of segments in source_bezpath and target_bezpath.
16351839
make_new_segments(&mut source_bezpath, target_segment_len.max(source_segment_len) - source_segment_len);
16361840
make_new_segments(&mut target_bezpath, source_segment_len.max(target_segment_len) - target_segment_len);
16371841

@@ -1736,7 +1940,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
17361940
segments_connected_count[point_index] += 1;
17371941
}
17381942

1739-
// Zero out points without exactly two connectors. These are ignored
1943+
// Zero out points without exactly two connectors. These are ignored.
17401944
for count in &mut segments_connected_count {
17411945
if *count != 2 {
17421946
*count = 0;
@@ -1764,7 +1968,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
17641968
}
17651969
}
17661970

1767-
fn calculate_distance_to_spilt(bezier1: PathSeg, bezier2: PathSeg, bevel_length: f64) -> f64 {
1971+
fn calculate_distance_to_split(bezier1: PathSeg, bezier2: PathSeg, bevel_length: f64) -> f64 {
17681972
if is_linear(bezier1) && is_linear(bezier2) {
17691973
let v1 = (bezier1.end() - bezier1.start()).normalize();
17701974
let v2 = (bezier1.end() - bezier2.end()).normalize();
@@ -1901,7 +2105,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
19012105
let mut next_bezier = handles_to_segment(next_start, *next_handles, next_end);
19022106
next_bezier = Affine::new(transform.to_cols_array()) * next_bezier;
19032107

1904-
let spilt_distance = calculate_distance_to_spilt(bezier, next_bezier, distance);
2108+
let calculated_split_distance = calculate_distance_to_split(bezier, next_bezier, distance);
19052109

19062110
if is_linear(bezier) {
19072111
bezier = PathSeg::Line(Line::new(bezier.start(), bezier.end()));
@@ -1934,7 +2138,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
19342138
let valid_length = length > 1e-10;
19352139
if segments_connected[*end_point] > 0 && valid_length {
19362140
// Apply the bevel to the end
1937-
let distance = spilt_distance.min(original_length.min(next_original_length) / 2.);
2141+
let distance = calculated_split_distance.min(original_length.min(next_original_length) / 2.);
19382142
bezier = split_distance(bezier.reverse(), distance, length).reverse();
19392143

19402144
if index == 0 && next_index == 1 {
@@ -1953,7 +2157,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
19532157
let valid_length = next_length > 1e-10;
19542158
if segments_connected[*next_start_point] > 0 && valid_length {
19552159
// Apply the bevel to the start
1956-
let distance = spilt_distance.min(next_original_length.min(original_length) / 2.);
2160+
let distance = calculated_split_distance.min(next_original_length.min(original_length) / 2.);
19572161
next_bezier = split_distance(next_bezier, distance, next_length);
19582162
next_length = (next_length - distance).max(0.);
19592163

0 commit comments

Comments
 (0)