mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
Replace 'Generate Handles' and 'Remove Handles' nodes with 'Auto-Tangents' node; rename vector2 data type to coordinate
This commit is contained in:
parent
f72263f4f8
commit
96a0dada92
7 changed files with 171 additions and 101 deletions
|
@ -230,7 +230,7 @@ fn cosine_inverse<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f6
|
|||
|
||||
/// The inverse tangent trigonometric function (atan or atan2, depending on input type) calculates:
|
||||
/// atan: the angle whose tangent is the specified scalar number.
|
||||
/// atan2: the angle of a ray from the origin to the specified vector2 point.
|
||||
/// atan2: the angle of a ray from the origin to the specified coordinate.
|
||||
#[node_macro::node(category("Math: Trig"))]
|
||||
fn tangent_inverse<U: TangentInverse>(_: impl Ctx, #[implementations(f64, f32, DVec2)] value: U, radians: bool) -> U::Output {
|
||||
value.atan(radians)
|
||||
|
@ -434,8 +434,8 @@ fn percentage_value(_: impl Ctx, _primary: (), percentage: Percentage) -> f64 {
|
|||
}
|
||||
|
||||
/// Constructs a two-dimensional vector value which may be set to any XY coordinate.
|
||||
#[node_macro::node(name("Vector2 Value"), category("Value"))]
|
||||
fn vector2_value(_: impl Ctx, _primary: (), x: f64, y: f64) -> DVec2 {
|
||||
#[node_macro::node(category("Value"))]
|
||||
fn coordinate_value(_: impl Ctx, _primary: (), x: f64, y: f64) -> DVec2 {
|
||||
DVec2::new(x, y)
|
||||
}
|
||||
|
||||
|
@ -524,7 +524,7 @@ fn dot_product(_: impl Ctx, vector_a: DVec2, vector_b: DVec2) -> f64 {
|
|||
vector_a.dot(vector_b)
|
||||
}
|
||||
|
||||
/// Obtain the X or Y component of a vector2.
|
||||
/// Obtain the X or Y component of a coordinate.
|
||||
#[node_macro::node(name("Extract XY"), category("Math: Vector"))]
|
||||
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T, axis: XY) -> f64 {
|
||||
match axis {
|
||||
|
@ -533,7 +533,7 @@ fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2
|
|||
}
|
||||
}
|
||||
|
||||
/// The X or Y component of a vector2.
|
||||
/// The X or Y component of a coordinate.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(specta::Type))]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
|
||||
|
|
|
@ -26,7 +26,7 @@ pub mod types {
|
|||
pub type IntegerCount = u32;
|
||||
/// Unsigned integer to be used for random seeds
|
||||
pub type SeedValue = u32;
|
||||
/// Non-negative integer vector2 with px unit
|
||||
/// Non-negative integer coordinate with px unit
|
||||
pub type Resolution = glam::UVec2;
|
||||
/// DVec2 with px unit
|
||||
pub type PixelSize = glam::DVec2;
|
||||
|
|
|
@ -9,9 +9,9 @@ use crate::raster_types::{CPU, RasterDataTable};
|
|||
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue};
|
||||
use crate::renderer::GraphicElementRendered;
|
||||
use crate::transform::{Footprint, ReferencePoint, Transform};
|
||||
use crate::vector::PointDomain;
|
||||
use crate::vector::misc::dvec2_to_point;
|
||||
use crate::vector::style::{LineCap, LineJoin};
|
||||
use crate::vector::{FillId, PointDomain, RegionId};
|
||||
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
|
||||
use bezier_rs::{Join, ManipulatorGroup, Subpath};
|
||||
use core::f64::consts::PI;
|
||||
|
@ -763,69 +763,25 @@ fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 {
|
|||
tl * (1. - t.x) * (1. - t.y) + tr * t.x * (1. - t.y) + br * t.x * t.y + bl * (1. - t.x) * t.y
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn remove_handles(
|
||||
_: impl Ctx,
|
||||
vector_data: VectorDataTable,
|
||||
#[default(10.)]
|
||||
#[soft_min(0.)]
|
||||
max_handle_distance: f64,
|
||||
) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
let mut vector_data = vector_data_instance.instance;
|
||||
|
||||
for (_, handles, start, end) in vector_data.segment_domain.handles_mut() {
|
||||
// Only convert to linear if handles are within the threshold distance
|
||||
match *handles {
|
||||
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let start_pos = vector_data.point_domain.positions()[start];
|
||||
let end_pos = vector_data.point_domain.positions()[end];
|
||||
|
||||
let start_handle_distance = (handle_start - start_pos).length();
|
||||
let end_handle_distance = (handle_end - end_pos).length();
|
||||
|
||||
// If handles are close enough to their anchor points, make the segment linear
|
||||
if start_handle_distance <= max_handle_distance && end_handle_distance <= max_handle_distance {
|
||||
*handles = bezier_rs::BezierHandles::Linear;
|
||||
}
|
||||
}
|
||||
bezier_rs::BezierHandles::Quadratic { handle } => {
|
||||
let start_pos = vector_data.point_domain.positions()[start];
|
||||
let end_pos = vector_data.point_domain.positions()[end];
|
||||
|
||||
// Use average distance from handle to both points
|
||||
let avg_distance = ((handle - start_pos).length() + (handle - end_pos).length()) / 2.;
|
||||
|
||||
if avg_distance <= max_handle_distance {
|
||||
*handles = bezier_rs::BezierHandles::Linear;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
vector_data_instance.instance = vector_data;
|
||||
vector_data_instance.source_node_id = None;
|
||||
result_table.push(vector_data_instance);
|
||||
}
|
||||
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn generate_handles(
|
||||
/// Automatically constructs tangents (Bézier handles) for anchor points in a vector path.
|
||||
#[node_macro::node(category("Vector"), name("Auto-Tangents"), path(graphene_core::vector))]
|
||||
async fn auto_tangents(
|
||||
_: impl Ctx,
|
||||
source: VectorDataTable,
|
||||
#[default(0.4)]
|
||||
/// The amount of spread for the auto-tangents, from 0 (sharp corner) to 1 (full spread).
|
||||
#[default(0.5)]
|
||||
#[range((0., 1.))]
|
||||
curvature: f64,
|
||||
spread: f64,
|
||||
/// If active, existing non-zero handles won't be affected.
|
||||
#[default(true)]
|
||||
preserve_existing: bool,
|
||||
) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
for source in source.instance_ref_iter() {
|
||||
let source_transform = *source.transform;
|
||||
let transform = *source.transform;
|
||||
let alpha_blending = *source.alpha_blending;
|
||||
let source_node_id = *source.source_node_id;
|
||||
let source = source.instance;
|
||||
|
||||
let mut result = VectorData {
|
||||
|
@ -834,11 +790,11 @@ async fn generate_handles(
|
|||
};
|
||||
|
||||
for mut subpath in source.stroke_bezier_paths() {
|
||||
subpath.apply_transform(source_transform);
|
||||
subpath.apply_transform(transform);
|
||||
|
||||
let groups = subpath.manipulator_groups();
|
||||
if groups.len() < 2 {
|
||||
// Not enough points for softening
|
||||
// Not enough points for softening or handle removal
|
||||
result.append_subpath(subpath, true);
|
||||
continue;
|
||||
}
|
||||
|
@ -849,16 +805,30 @@ async fn generate_handles(
|
|||
for i in 0..groups.len() {
|
||||
let curr = &groups[i];
|
||||
|
||||
// Check if this point has handles
|
||||
let has_handles =
|
||||
(curr.in_handle.is_some() && !curr.in_handle.unwrap().abs_diff_eq(curr.anchor, 1e-5)) || (curr.out_handle.is_some() && !curr.out_handle.unwrap().abs_diff_eq(curr.anchor, 1e-5));
|
||||
if preserve_existing {
|
||||
// Check if this point has handles that are meaningfully different from the anchor
|
||||
let has_handles = (curr.in_handle.is_some() && !curr.in_handle.unwrap().abs_diff_eq(curr.anchor, 1e-5))
|
||||
|| (curr.out_handle.is_some() && !curr.out_handle.unwrap().abs_diff_eq(curr.anchor, 1e-5));
|
||||
|
||||
if has_handles || (!is_closed && (i == 0 || i == groups.len() - 1)) {
|
||||
new_groups.push(*curr);
|
||||
// If the point already has handles, or if it's an endpoint of an open path, keep it as is.
|
||||
if has_handles || (!is_closed && (i == 0 || i == groups.len() - 1)) {
|
||||
new_groups.push(*curr);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If spread is 0, remove handles for this point, making it a sharp corner.
|
||||
if spread == 0. {
|
||||
new_groups.push(ManipulatorGroup {
|
||||
anchor: curr.anchor,
|
||||
in_handle: None,
|
||||
out_handle: None,
|
||||
id: curr.id,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get previous and next points
|
||||
// Get previous and next points for auto-tangent calculation
|
||||
let prev_idx = if i == 0 { if is_closed { groups.len() - 1 } else { i } } else { i - 1 };
|
||||
let next_idx = if i == groups.len() - 1 { if is_closed { 0 } else { i } } else { i + 1 };
|
||||
|
||||
|
@ -866,25 +836,33 @@ async fn generate_handles(
|
|||
let curr_pos = curr.anchor;
|
||||
let next = groups[next_idx].anchor;
|
||||
|
||||
// Calculate directions to adjacent points
|
||||
// Calculate directions from current point to adjacent points
|
||||
let dir_prev = (prev - curr_pos).normalize_or_zero();
|
||||
let dir_next = (next - curr_pos).normalize_or_zero();
|
||||
|
||||
// Check if we have valid directions
|
||||
// Check if we have valid directions (e.g., points are not coincident)
|
||||
if dir_prev.length_squared() < 1e-5 || dir_next.length_squared() < 1e-5 {
|
||||
// Fallback: keep the original manipulator group (which has no active handles here)
|
||||
new_groups.push(*curr);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate handle direction (perpendicular to the angle bisector)
|
||||
let handle_dir = (dir_prev - dir_next).try_normalize().unwrap_or(dir_prev.perp());
|
||||
let handle_dir = if dir_prev.dot(handle_dir) < 0. { -handle_dir } else { handle_dir };
|
||||
// Calculate handle direction (colinear, pointing along the line from prev to next)
|
||||
// Original logic: (dir_prev - dir_next) is equivalent to (prev - curr) - (next - curr) = prev - next
|
||||
// The handle_dir will be along the line connecting prev and next, or perpendicular if they are coincident.
|
||||
let mut handle_dir = (dir_prev - dir_next).try_normalize().unwrap_or_else(|| dir_prev.perp());
|
||||
|
||||
// Calculate handle lengths - 1/3 of distance to adjacent points, scaled by curvature
|
||||
let in_length = (curr_pos - prev).length() / 3. * curvature;
|
||||
let out_length = (next - curr_pos).length() / 3. * curvature;
|
||||
// Ensure consistent orientation of the handle_dir
|
||||
// This makes the `+ handle_dir` for in_handle and `- handle_dir` for out_handle consistent
|
||||
if dir_prev.dot(handle_dir) < 0. {
|
||||
handle_dir = -handle_dir;
|
||||
}
|
||||
|
||||
// Create new manipulator group with handles
|
||||
// Calculate handle lengths: 1/3 of distance to adjacent points, scaled by spread
|
||||
let in_length = (curr_pos - prev).length() / 3. * spread;
|
||||
let out_length = (next - curr_pos).length() / 3. * spread;
|
||||
|
||||
// Create new manipulator group with calculated auto-tangents
|
||||
new_groups.push(ManipulatorGroup {
|
||||
anchor: curr_pos,
|
||||
in_handle: Some(curr_pos + handle_dir * in_length),
|
||||
|
@ -894,15 +872,15 @@ async fn generate_handles(
|
|||
}
|
||||
|
||||
let mut softened_subpath = Subpath::new(new_groups, is_closed);
|
||||
softened_subpath.apply_transform(source_transform.inverse());
|
||||
softened_subpath.apply_transform(transform.inverse());
|
||||
result.append_subpath(softened_subpath, true);
|
||||
}
|
||||
|
||||
result_table.push(Instance {
|
||||
instance: result,
|
||||
transform: source_transform,
|
||||
alpha_blending: Default::default(),
|
||||
source_node_id: None,
|
||||
transform,
|
||||
alpha_blending,
|
||||
source_node_id,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1058,6 +1036,53 @@ async fn dimensions(_: impl Ctx, vector_data: VectorDataTable) -> DVec2 {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Converts a coordinate value into a vector anchor point.
|
||||
///
|
||||
/// This is useful in conjunction with nodes that repeat it, followed by the "Points to Polyline" node to string together a path of the points.
|
||||
#[node_macro::node(category("Vector"), name("Coordinate to Point"), path(graphene_core::vector))]
|
||||
async fn position_to_point(_: impl Ctx, coordinate: DVec2) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
let mut point_domain = PointDomain::new();
|
||||
point_domain.push(PointId::generate(), coordinate);
|
||||
|
||||
result_table.push(Instance {
|
||||
instance: VectorData { point_domain, ..Default::default() },
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
result_table
|
||||
}
|
||||
|
||||
/// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist.
|
||||
#[node_macro::node(category("Vector"), name("Points to Polyline"), path(graphene_core::vector))]
|
||||
async fn points_to_polyline(_: impl Ctx, mut points: VectorDataTable, #[default(true)] closed: bool) -> VectorDataTable {
|
||||
for instance in points.instance_mut_iter() {
|
||||
let mut segment_domain = SegmentDomain::new();
|
||||
|
||||
let points_count = instance.instance.point_domain.ids().len();
|
||||
|
||||
if points_count > 2 {
|
||||
(0..points_count - 1).for_each(|i| {
|
||||
segment_domain.push(SegmentId::generate(), i, i + 1, bezier_rs::BezierHandles::Linear, StrokeId::generate());
|
||||
});
|
||||
|
||||
if closed {
|
||||
segment_domain.push(SegmentId::generate(), points_count - 1, 0, bezier_rs::BezierHandles::Linear, StrokeId::generate());
|
||||
|
||||
instance
|
||||
.instance
|
||||
.region_domain
|
||||
.push(RegionId::generate(), segment_domain.ids()[0]..=*segment_domain.ids().last().unwrap(), FillId::generate());
|
||||
}
|
||||
}
|
||||
|
||||
instance.instance.segment_domain = segment_domain;
|
||||
}
|
||||
|
||||
points
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector), properties("offset_path_properties"))]
|
||||
async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, line_join: LineJoin, #[default(4.)] miter_limit: f64) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue