Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use graphene_std::raster::{
use graphene_std::table::{Table, TableRow};
use graphene_std::text::{Font, TextAlign};
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
use graphene_std::vector::misc::{ArcType, CentroidType, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType};
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};

pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
Expand Down Expand Up @@ -219,6 +219,7 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<TextAlign>() => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<ExtrudeJoiningAlgorithm>() => enum_choice::<ExtrudeJoiningAlgorithm>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
Expand Down
1 change: 1 addition & 0 deletions node-graph/graph-craft/src/document/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ tagged_value! {
GridType(vector::misc::GridType),
ArcType(vector::misc::ArcType),
MergeByDistanceAlgorithm(vector::misc::MergeByDistanceAlgorithm),
ExtrudeJoiningAlgorithm(vector::misc::ExtrudeJoiningAlgorithm),
PointSpacingType(vector::misc::PointSpacingType),
SpiralType(vector::misc::SpiralType),
#[serde(alias = "LineCap")]
Expand Down
2 changes: 2 additions & 0 deletions node-graph/interpreted-executor/src/node_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::FillType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]),
Expand Down Expand Up @@ -220,6 +221,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]),
Expand Down
10 changes: 10 additions & 0 deletions node-graph/libraries/vector-types/src/vector/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ pub enum MergeByDistanceAlgorithm {
Topological,
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum ExtrudeJoiningAlgorithm {
All,
#[default]
Extrema,
None,
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,10 @@ impl SegmentDomain {
self.end_point[segment_index] = new;
}

pub fn set_handles(&mut self, segment_index: usize, new: BezierHandles) {
self.handles[segment_index] = new;
}

pub fn handles(&self) -> &[BezierHandles] {
&self.handles
}
Expand Down
218 changes: 211 additions & 7 deletions node-graph/nodes/vector/src/vector_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, evaluat
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
use vector_types::vector::misc::{CentroidType, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2};
use vector_types::vector::misc::{CentroidType, ExtrudeJoiningAlgorithm, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2};
use vector_types::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType, is_linear};
use vector_types::vector::misc::{handles_to_segment, segment_to_handles};
use vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke};
Expand Down Expand Up @@ -580,6 +580,210 @@ pub fn merge_by_distance(
}
}

pub mod extrude_algorithms {
use glam::DVec2;
use kurbo::{ParamCurve, ParamCurveDeriv};
use vector_types::subpath::BezierHandles;
use vector_types::vector::StrokeId;
use vector_types::vector::misc::ExtrudeJoiningAlgorithm;

/// Convert [`vector_types::subpath::Bezier`] to [`kurbo::PathSeg`].
fn bezier_to_path_seg(bezier: vector_types::subpath::Bezier) -> kurbo::PathSeg {
let [start, end] = [(bezier.start().x, bezier.start().y), (bezier.end().x, bezier.end().y)];
match bezier.handles {
BezierHandles::Linear => kurbo::Line::new(start, end).into(),
BezierHandles::Quadratic { handle } => kurbo::QuadBez::new(start, (handle.x, handle.y), end).into(),
BezierHandles::Cubic { handle_start, handle_end } => kurbo::CubicBez::new(start, (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), end).into(),
}
}

/// Convert [`kurbo::CubicBez`] to [`vector_types::subpath::BezierHandles`].
fn cubic_to_handles(cubic_bez: kurbo::CubicBez) -> BezierHandles {
BezierHandles::Cubic {
handle_start: DVec2::new(cubic_bez.p1.x, cubic_bez.p1.y),
handle_end: DVec2::new(cubic_bez.p2.x, cubic_bez.p2.y),
}
}

/// Find the `t` values to split (where the tangent changes to be on the other side of the direction).
fn find_splits(cubic_segment: kurbo::CubicBez, direction: DVec2) -> impl Iterator<Item = f64> {
let derivative = cubic_segment.deriv();
let convert = |x: kurbo::Point| DVec2::new(x.x, x.y);
let derivative_points = [derivative.p0, derivative.p1, derivative.p2].map(convert);

let t_squared = derivative_points[0] - 2. * derivative_points[1] + derivative_points[2];
let t_scalar = -2. * derivative_points[0] + 2. * derivative_points[1];
let constant = derivative_points[0];

kurbo::common::solve_quadratic(constant.perp_dot(direction), t_scalar.perp_dot(direction), t_squared.perp_dot(direction))
.into_iter()
.filter(|&t| t > 1e-6 && t < 1. - 1e-6)
}

/// Split so segments no longer have tangents on both sides of the direction vector.
fn split(vector: &mut graphic_types::Vector, direction: DVec2) {
let segment_count = vector.segment_domain.ids().len();
let mut next_point = vector.point_domain.next_id();
let mut next_segment = vector.segment_domain.next_id();

for segment_index in 0..segment_count {
let (_, _, bezier) = vector.segment_points_from_index(segment_index);
let mut start_index = vector.segment_domain.start_point()[segment_index];
let pathseg = bezier_to_path_seg(bezier).to_cubic();
let mut start_t = 0.;

for split_t in find_splits(pathseg, direction) {
let [first, second] = [pathseg.subsegment(start_t..split_t), pathseg.subsegment(split_t..1.)];
let [first_handles, second_handles] = [first, second].map(cubic_to_handles);
let middle_point = next_point.next_id();
let start_segment = next_segment.next_id();

let middle_point_index = vector.point_domain.len();
vector.point_domain.push(middle_point, DVec2::new(first.end().x, first.end().y));
vector.segment_domain.push(start_segment, start_index, middle_point_index, first_handles, StrokeId::ZERO);
vector.segment_domain.set_start_point(segment_index, middle_point_index);
vector.segment_domain.set_handles(segment_index, second_handles);

start_t = split_t;
start_index = middle_point_index;
}
}
}

/// Copy all segments with the offset of `direction`.
fn offset_copy_all_segments(vector: &mut graphic_types::Vector, direction: DVec2) {
let points_count = vector.point_domain.ids().len();
let mut next_point = vector.point_domain.next_id();
for index in 0..points_count {
vector.point_domain.push(next_point.next_id(), vector.point_domain.positions()[index] + direction);
}

let segment_count = vector.segment_domain.ids().len();
let mut next_segment = vector.segment_domain.next_id();
for index in 0..segment_count {
vector.segment_domain.push(
next_segment.next_id(),
vector.segment_domain.start_point()[index] + points_count,
vector.segment_domain.end_point()[index] + points_count,
vector.segment_domain.handles()[index].apply_transformation(|x| x + direction),
vector.segment_domain.stroke()[index],
);
}
}

/// Join points from the original to the copied that are on opposite sides of the direction.
fn join_extrema_edges(vector: &mut graphic_types::Vector, direction: DVec2) {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum Found {
#[default]
None,
Positive,
Negative,
Both,
Invalid,
}

impl Found {
fn update(&mut self, value: f64) {
*self = match (*self, value > 0.) {
(Found::None, true) => Found::Positive,
(Found::None, false) => Found::Negative,
(Found::Positive, true) | (Found::Negative, false) => Found::Both,
_ => Found::Invalid,
};
}
}

let first_half_points = vector.point_domain.len() / 2;
let mut points = vec![Found::None; first_half_points];
let first_half_segments = vector.segment_domain.ids().len() / 2;

for segment_id in 0..first_half_segments {
let index = [vector.segment_domain.start_point()[segment_id], vector.segment_domain.end_point()[segment_id]];
let position = index.map(|index| vector.point_domain.positions()[index]);

if position[0].abs_diff_eq(position[1], 1e-6) {
continue; // Skip zero length segments
}

points[index[0]].update(direction.perp_dot(position[1] - position[0]));
points[index[1]].update(direction.perp_dot(position[0] - position[1]));
}

let mut next_segment = vector.segment_domain.next_id();
for (index, &point) in points.iter().enumerate().take(first_half_points) {
if point != Found::Both {
continue;
}

vector
.segment_domain
.push(next_segment.next_id(), index, index + first_half_points, BezierHandles::Linear, StrokeId::ZERO);
}
}

/// Join all points from the original to the copied.
fn join_all(vector: &mut graphic_types::Vector) {
let mut next_segment = vector.segment_domain.next_id();
let first_half = vector.point_domain.len() / 2;
for index in 0..first_half {
vector.segment_domain.push(next_segment.next_id(), index, index + first_half, BezierHandles::Linear, StrokeId::ZERO);
}
}

pub fn extrude(vector: &mut graphic_types::Vector, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) {
split(vector, direction);
offset_copy_all_segments(vector, direction);

match joining_algorithm {
ExtrudeJoiningAlgorithm::Extrema => join_extrema_edges(vector, direction),
ExtrudeJoiningAlgorithm::All => join_all(vector),
ExtrudeJoiningAlgorithm::None => {}
}
}

#[cfg(test)]
mod extrude_tests {
use glam::DVec2;
use kurbo::{ParamCurve, ParamCurveDeriv};

#[test]
fn split_cubic() {
let l1 = kurbo::CubicBez::new((0., 0.), (100., 0.), (100., 100.), (0., 100.));
assert_eq!(super::find_splits(l1, DVec2::Y).collect::<Vec<f64>>(), vec![0.5]);
assert!(super::find_splits(l1, DVec2::X).collect::<Vec<f64>>().is_empty());

let l2 = kurbo::CubicBez::new((0., 0.), (0., 0.), (100., 0.), (100., 0.));
assert!(super::find_splits(l2, DVec2::X).collect::<Vec<f64>>().is_empty());

let l3 = kurbo::PathSeg::Line(kurbo::Line::new((0., 0.), (100., 0.)));
assert!(super::find_splits(l3.to_cubic(), DVec2::X).collect::<Vec<f64>>().is_empty());

let l4 = kurbo::CubicBez::new((0., 0.), (100., -10.), (100., 110.), (0., 100.));
let splits = super::find_splits(l4, DVec2::X).map(|t| l4.deriv().eval(t)).collect::<Vec<_>>();
assert_eq!(splits.len(), 2);
assert!(splits.iter().all(|&deriv| deriv.y.abs() < 1e-8), "{splits:?}");
}

#[test]
fn split_vector() {
let curve = kurbo::PathSeg::Cubic(kurbo::CubicBez::new((0., 0.), (100., -10.), (100., 110.), (0., 100.)));
let mut vector = graphic_types::Vector::from_bezpath(kurbo::BezPath::from_path_segments([curve].into_iter()));
super::split(&mut vector, DVec2::X);
assert_eq!(vector.segment_ids().len(), 3);
assert_eq!(vector.point_domain.ids().len(), 4);
}
}
}

#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn extrude(_: impl Ctx, mut source: Table<Vector>, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> Table<Vector> {
for TableRowMut { element: source, .. } in source.iter_mut() {
extrude_algorithms::extrude(source, direction, joining_algorithm);
}
source
}

#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn box_warp(_: impl Ctx, content: Table<Vector>, #[expose] rectangle: Table<Vector>) -> Table<Vector> {
let Some((target, target_transform)) = rectangle.get(0).map(|rect| (rect.element, rect.transform)) else {
Expand Down Expand Up @@ -1631,7 +1835,7 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
let target_segment_len = target_bezpath.segments().count();
let source_segment_len = source_bezpath.segments().count();

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

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

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

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

let spilt_distance = calculate_distance_to_spilt(bezier, next_bezier, distance);
let calculated_split_distance = calculate_distance_to_split(bezier, next_bezier, distance);

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

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

Expand Down
Loading