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
2
demo-artwork/parametric-dunescape.graphite
generated
2
demo-artwork/parametric-dunescape.graphite
generated
File diff suppressed because one or more lines are too long
|
@ -951,7 +951,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
properties: None,
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Split Vector2",
|
||||
identifier: "Split Coordinate",
|
||||
category: "Math: Vector",
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
|
@ -982,7 +982,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
input_properties: vec![("Vector2", "TODO").into()],
|
||||
input_properties: vec![("Coordinate", "TODO").into()],
|
||||
output_names: vec!["X".to_string(), "Y".to_string()],
|
||||
has_primary_output: false,
|
||||
network_metadata: Some(NodeNetworkMetadata {
|
||||
|
@ -2913,7 +2913,7 @@ fn static_input_properties() -> InputProperties {
|
|||
.input_metadata(&node_id, index, "min", context.selection_network_path)
|
||||
.and_then(|value| value.as_f64());
|
||||
|
||||
Ok(vec![node_properties::vector2_widget(
|
||||
Ok(vec![node_properties::coordinate_widget(
|
||||
ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true),
|
||||
x,
|
||||
y,
|
||||
|
@ -3190,7 +3190,7 @@ fn static_input_properties() -> InputProperties {
|
|||
Box::new(|node_id, index, context| {
|
||||
let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?;
|
||||
Ok(vec![LayoutGroup::Row {
|
||||
widgets: node_properties::array_of_vector2_widget(
|
||||
widgets: node_properties::array_of_coordinates_widget(
|
||||
ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true),
|
||||
TextInput::default().centered(true),
|
||||
),
|
||||
|
|
|
@ -162,8 +162,8 @@ pub(crate) fn property_from_type(
|
|||
Some("Fraction") => number_widget(default_info, number_input.mode_range().min(min(0.)).max(max(1.))).into(),
|
||||
Some("IntegerCount") => number_widget(default_info, number_input.int().min(min(1.))).into(),
|
||||
Some("SeedValue") => number_widget(default_info, number_input.int().min(min(0.))).into(),
|
||||
Some("Resolution") => vector2_widget(default_info, "W", "H", " px", Some(64.)),
|
||||
Some("PixelSize") => vector2_widget(default_info, "X", "Y", " px", None),
|
||||
Some("Resolution") => coordinate_widget(default_info, "W", "H", " px", Some(64.)),
|
||||
Some("PixelSize") => coordinate_widget(default_info, "X", "Y", " px", None),
|
||||
|
||||
// For all other types, use TypeId-based matching
|
||||
_ => {
|
||||
|
@ -177,14 +177,14 @@ pub(crate) fn property_from_type(
|
|||
Some(x) if x == TypeId::of::<u64>() => number_widget(default_info, number_input.int().min(min(0.))).into(),
|
||||
Some(x) if x == TypeId::of::<bool>() => bool_widget(default_info, CheckboxInput::default()).into(),
|
||||
Some(x) if x == TypeId::of::<String>() => text_widget(default_info).into(),
|
||||
Some(x) if x == TypeId::of::<DVec2>() => vector2_widget(default_info, "X", "Y", "", None),
|
||||
Some(x) if x == TypeId::of::<UVec2>() => vector2_widget(default_info, "X", "Y", "", Some(0.)),
|
||||
Some(x) if x == TypeId::of::<IVec2>() => vector2_widget(default_info, "X", "Y", "", None),
|
||||
Some(x) if x == TypeId::of::<DVec2>() => coordinate_widget(default_info, "X", "Y", "", None),
|
||||
Some(x) if x == TypeId::of::<UVec2>() => coordinate_widget(default_info, "X", "Y", "", Some(0.)),
|
||||
Some(x) if x == TypeId::of::<IVec2>() => coordinate_widget(default_info, "X", "Y", "", None),
|
||||
// ==========================
|
||||
// PRIMITIVE COLLECTION TYPES
|
||||
// ==========================
|
||||
Some(x) if x == TypeId::of::<Vec<f64>>() => array_of_number_widget(default_info, TextInput::default()).into(),
|
||||
Some(x) if x == TypeId::of::<Vec<DVec2>>() => array_of_vector2_widget(default_info, TextInput::default()).into(),
|
||||
Some(x) if x == TypeId::of::<Vec<DVec2>>() => array_of_coordinates_widget(default_info, TextInput::default()).into(),
|
||||
// ====================
|
||||
// GRAPHICAL DATA TYPES
|
||||
// ====================
|
||||
|
@ -498,7 +498,7 @@ pub fn footprint_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
|
|||
last.clone()
|
||||
}
|
||||
|
||||
pub fn vector2_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &str, unit: &str, min: Option<f64>) -> LayoutGroup {
|
||||
pub fn coordinate_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &str, unit: &str, min: Option<f64>) -> LayoutGroup {
|
||||
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
|
||||
|
||||
let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::Number);
|
||||
|
@ -641,7 +641,7 @@ pub fn array_of_number_widget(parameter_widgets_info: ParameterWidgetsInfo, text
|
|||
widgets
|
||||
}
|
||||
|
||||
pub fn array_of_vector2_widget(parameter_widgets_info: ParameterWidgetsInfo, text_props: TextInput) -> Vec<WidgetHolder> {
|
||||
pub fn array_of_coordinates_widget(parameter_widgets_info: ParameterWidgetsInfo, text_props: TextInput) -> Vec<WidgetHolder> {
|
||||
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
|
||||
|
||||
let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::Number);
|
||||
|
@ -1181,7 +1181,7 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||
if let Some(&TaggedValue::GridType(grid_type)) = grid_type_input.as_non_exposed_value() {
|
||||
match grid_type {
|
||||
GridType::Rectangular => {
|
||||
let spacing = vector2_widget(ParameterWidgetsInfo::from_index(document_node, node_id, spacing_index, true, context), "W", "H", " px", Some(0.));
|
||||
let spacing = coordinate_widget(ParameterWidgetsInfo::from_index(document_node, node_id, spacing_index, true, context), "W", "H", " px", Some(0.));
|
||||
widgets.push(spacing);
|
||||
}
|
||||
GridType::Isometric => {
|
||||
|
@ -1191,7 +1191,7 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||
NumberInput::default().label("H").min(0.).unit(" px"),
|
||||
),
|
||||
};
|
||||
let angles = vector2_widget(ParameterWidgetsInfo::from_index(document_node, node_id, angles_index, true, context), "", "", "°", None);
|
||||
let angles = coordinate_widget(ParameterWidgetsInfo::from_index(document_node, node_id, angles_index, true, context), "", "", "°", None);
|
||||
widgets.extend([spacing, angles]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -467,7 +467,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
}
|
||||
};
|
||||
|
||||
const REPLACEMENTS: [(&str, &str); 37] = [
|
||||
const REPLACEMENTS: [(&str, &str); 40] = [
|
||||
("graphene_core::AddArtboardNode", "graphene_core::graphic_element::AppendArtboardNode"),
|
||||
("graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"),
|
||||
("graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"),
|
||||
|
@ -475,7 +475,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
("graphene_core::logic::LogicAndNode", "graphene_core::ops::LogicAndNode"),
|
||||
("graphene_core::logic::LogicNotNode", "graphene_core::ops::LogicNotNode"),
|
||||
("graphene_core::logic::LogicOrNode", "graphene_core::ops::LogicOrNode"),
|
||||
("graphene_core::ops::ConstructVector2", "graphene_core::ops::Vector2ValueNode"),
|
||||
("graphene_core::ops::ConstructVector2", "graphene_core::ops::CoordinateValueNode"),
|
||||
("graphene_core::ops::Vector2ValueNode", "graphene_core::ops::CoordinateValueNode"),
|
||||
("graphene_core::raster::BlackAndWhiteNode", "graphene_core::raster::adjustments::BlackAndWhiteNode"),
|
||||
("graphene_core::raster::BlendNode", "graphene_core::raster::adjustments::BlendNode"),
|
||||
("graphene_core::raster::ChannelMixerNode", "graphene_core::raster::adjustments::ChannelMixerNode"),
|
||||
|
@ -484,6 +485,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
("graphene_core::raster::ExtractChannelNode", "graphene_core::raster::adjustments::ExtractChannelNode"),
|
||||
("graphene_core::raster::GradientMapNode", "graphene_core::raster::adjustments::GradientMapNode"),
|
||||
("graphene_core::raster::HueSaturationNode", "graphene_core::raster::adjustments::HueSaturationNode"),
|
||||
("graphene_core::vector::GenerateHandlesNode", "graphene_core::vector::AutoTangentsNode"),
|
||||
("graphene_core::vector::RemoveHandlesNode", "graphene_core::vector::AutoTangentsNode"),
|
||||
("graphene_core::raster::InvertNode", "graphene_core::raster::adjustments::InvertNode"),
|
||||
("graphene_core::raster::InvertRGBNode", "graphene_core::raster::adjustments::InvertNode"),
|
||||
("graphene_core::raster::LevelsNode", "graphene_core::raster::adjustments::LevelsNode"),
|
||||
|
@ -963,6 +966,48 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
|
||||
document.network_interface.replace_reference_name(node_id, network_path, "Flatten Path".to_string());
|
||||
}
|
||||
|
||||
if reference == "Remove Handles" {
|
||||
let node_definition = resolve_document_node_type("Auto-Tangents").unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::F64(0.), false), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path);
|
||||
|
||||
document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string());
|
||||
}
|
||||
|
||||
if reference == "Generate Handles" {
|
||||
let node_definition = resolve_document_node_type("Auto-Tangents").unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||
|
||||
document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
|
|
|
@ -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