Skip to content

Commit 8b375f3

Browse files
committed
Add extrude node
1 parent 3138cf7 commit 8b375f3

File tree

6 files changed

+226
-2
lines changed

6 files changed

+226
-2
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> {
@@ -217,6 +217,7 @@ pub(crate) fn property_from_type(
217217
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
218218
Some(x) if x == TypeId::of::<TextAlign>() => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
219219
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
220+
Some(x) if x == TypeId::of::<ExtrudeJoiningAlgorithm>() => enum_choice::<ExtrudeJoiningAlgorithm>().for_socket(default_info).property_row(),
220221
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
221222
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
222223
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
@@ -136,6 +136,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
136136
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]),
137137
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]),
138138
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]),
139+
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]),
139140
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
140141
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::FillType]),
141142
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]),
@@ -222,6 +223,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
222223
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]),
223224
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]),
224225
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]),
226+
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]),
225227
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
226228
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]),
227229
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: 207 additions & 1 deletion
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,212 @@ pub fn merge_by_distance(
580580
}
581581
}
582582

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

0 commit comments

Comments
 (0)