mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
New nodes: Mirror, Round Corners, Box Warp, Remove/Generate Handles, Spatial Merge by Distance (#2448)
* Add Mirror and Round Corner nodes * Removed perspective warp ride along node * Fixed naming, added back Box Warp * Add Soften, Sharpen and Subdivide * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
5f398e79e9
commit
fff0a53799
1 changed files with 588 additions and 2 deletions
|
@ -2,13 +2,14 @@ use super::misc::CentroidType;
|
|||
use super::style::{Fill, Gradient, GradientStops, Stroke};
|
||||
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
|
||||
use crate::instances::{InstanceMut, Instances};
|
||||
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, SeedValue};
|
||||
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Percentage, PixelLength, SeedValue};
|
||||
use crate::renderer::GraphicElementRendered;
|
||||
use crate::transform::{Footprint, Transform, TransformMut};
|
||||
use crate::vector::PointDomain;
|
||||
use crate::vector::style::LineJoin;
|
||||
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
|
||||
use bezier_rs::{Cap, Join, Subpath, SubpathTValue, TValue};
|
||||
use bezier_rs::{Cap, Join, ManipulatorGroup, Subpath, SubpathTValue, TValue};
|
||||
use core::f64::consts::PI;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use rand::{Rng, SeedableRng};
|
||||
|
||||
|
@ -335,6 +336,591 @@ where
|
|||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn mirror<I: 'n + Send>(
|
||||
_: impl Ctx,
|
||||
#[implementations(VectorDataTable, GraphicGroupTable)] instance: Instances<I>,
|
||||
#[default(0., 0.)] center: DVec2,
|
||||
#[range((-90., 90.))] angle: Angle,
|
||||
) -> GraphicGroupTable
|
||||
where
|
||||
Instances<I>: GraphicElementRendered,
|
||||
{
|
||||
let mut result_table = GraphicGroupTable::default();
|
||||
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table };
|
||||
// The mirror center is based on the bounding box for now
|
||||
let mirror_center = (bounding_box[0] + bounding_box[1]) / 2. + center;
|
||||
// Normalize direction vector
|
||||
let normal = DVec2::from_angle(angle.to_radians());
|
||||
// Create reflection matrix
|
||||
let reflection = DAffine2::from_mat2_translation(
|
||||
glam::DMat2::from_cols(
|
||||
DVec2::new(1. - 2. * normal.x * normal.x, -2. * normal.y * normal.x),
|
||||
DVec2::new(-2. * normal.x * normal.y, 1. - 2. * normal.y * normal.y),
|
||||
),
|
||||
DVec2::ZERO,
|
||||
);
|
||||
// Apply reflection around the center point
|
||||
let modification = DAffine2::from_translation(mirror_center) * reflection * DAffine2::from_translation(-mirror_center);
|
||||
// Add original instance to result
|
||||
let original_element = instance.to_graphic_element().clone();
|
||||
result_table.push(original_element);
|
||||
// Create and add mirrored instance
|
||||
let mut mirrored_element = instance.to_graphic_element().clone();
|
||||
mirrored_element.new_ids_from_hash(None);
|
||||
// Finally, apply the transformation to the mirrored instance
|
||||
let mirrored_instance = result_table.push(mirrored_element);
|
||||
*mirrored_instance.transform = modification;
|
||||
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn round_corners(
|
||||
_: impl Ctx,
|
||||
source: VectorDataTable,
|
||||
#[min(0.)]
|
||||
#[default(10.)]
|
||||
radius: PixelLength,
|
||||
#[range((0., 1.))]
|
||||
#[default(0.5)]
|
||||
roundness: f64,
|
||||
#[default(100.)] edge_length_limit: Percentage,
|
||||
#[range((0., 180.))]
|
||||
#[default(5.)]
|
||||
min_angle_threshold: Angle,
|
||||
) -> VectorDataTable {
|
||||
let source_transform = source.transform();
|
||||
let source_transform_inverse = source_transform.inverse();
|
||||
let source = source.one_instance().instance;
|
||||
|
||||
// Flip the roundness to help with user intuition
|
||||
let roundness = 1. - roundness;
|
||||
// Convert 0-100 to 0-0.5
|
||||
let edge_length_limit = edge_length_limit * 0.005;
|
||||
|
||||
let mut result = VectorData::empty();
|
||||
result.style = source.style.clone();
|
||||
|
||||
for mut subpath in source.stroke_bezier_paths() {
|
||||
subpath.apply_transform(source_transform);
|
||||
|
||||
if subpath.manipulator_groups().len() < 3 {
|
||||
// Not enough points for corner rounding
|
||||
result.append_subpath(subpath, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
let groups = subpath.manipulator_groups();
|
||||
let mut new_groups = Vec::new();
|
||||
let is_closed = subpath.closed();
|
||||
|
||||
for i in 0..groups.len() {
|
||||
// Skip first and last points for open paths
|
||||
if !is_closed && (i == 0 || i == groups.len() - 1) {
|
||||
new_groups.push(groups[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not the prettiest, but it makes the rest of the logic more readable
|
||||
let prev_idx = if i == 0 { if is_closed { groups.len() - 1 } else { 0 } } else { i - 1 };
|
||||
let curr_idx = i;
|
||||
let next_idx = if i == groups.len() - 1 { if is_closed { 0 } else { i } } else { i + 1 };
|
||||
|
||||
let prev = groups[prev_idx].anchor;
|
||||
let curr = groups[curr_idx].anchor;
|
||||
let next = groups[next_idx].anchor;
|
||||
|
||||
let dir1 = (curr - prev).normalize_or(DVec2::X);
|
||||
let dir2 = (next - curr).normalize_or(DVec2::X);
|
||||
|
||||
let theta = PI - dir1.angle_to(dir2).abs();
|
||||
|
||||
// Skip near-straight corners
|
||||
if theta > PI - min_angle_threshold.to_radians() {
|
||||
new_groups.push(groups[curr_idx]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate L, with limits to avoid extreme values
|
||||
let distance_along_edge = radius / (theta / 2.).sin();
|
||||
let distance_along_edge = distance_along_edge.min(edge_length_limit * (curr - prev).length().min((next - curr).length())).max(0.01);
|
||||
|
||||
// Find points on each edge at distance L from corner
|
||||
let p1 = curr - dir1 * distance_along_edge;
|
||||
let p2 = curr + dir2 * distance_along_edge;
|
||||
|
||||
// Add first point with out handle
|
||||
new_groups.push(ManipulatorGroup {
|
||||
anchor: p1,
|
||||
in_handle: None,
|
||||
out_handle: Some(curr - dir1 * distance_along_edge * roundness),
|
||||
id: PointId::generate(),
|
||||
});
|
||||
|
||||
// Add second point with in handle
|
||||
new_groups.push(ManipulatorGroup {
|
||||
anchor: p2,
|
||||
in_handle: Some(curr + dir2 * distance_along_edge * roundness),
|
||||
out_handle: None,
|
||||
id: PointId::generate(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut rounded_subpath = Subpath::new(new_groups, is_closed);
|
||||
rounded_subpath.apply_transform(source_transform_inverse);
|
||||
result.append_subpath(rounded_subpath, false);
|
||||
}
|
||||
|
||||
let mut result_table = VectorDataTable::new(result);
|
||||
*result_table.transform_mut() = source_transform;
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Spatial Merge by Distance"), category("Debug"), path(graphene_core::vector))]
|
||||
async fn spatial_merge_by_distance(
|
||||
_: impl Ctx,
|
||||
vector_data: VectorDataTable,
|
||||
#[default(0.1)]
|
||||
#[min(0.0001)]
|
||||
distance: f64,
|
||||
) -> VectorDataTable {
|
||||
let vector_data_transform = vector_data.transform();
|
||||
let vector_data = vector_data.one_instance().instance;
|
||||
let point_count = vector_data.point_domain.positions().len();
|
||||
|
||||
// Find min x and y for grid cell normalization
|
||||
let mut min_x = f64::MAX;
|
||||
let mut min_y = f64::MAX;
|
||||
|
||||
// Calculate mins without collecting all positions
|
||||
for &pos in vector_data.point_domain.positions() {
|
||||
let transformed_pos = vector_data_transform.transform_point2(pos);
|
||||
min_x = min_x.min(transformed_pos.x);
|
||||
min_y = min_y.min(transformed_pos.y);
|
||||
}
|
||||
|
||||
// Create a spatial grid with cell size of 'distance'
|
||||
use std::collections::HashMap;
|
||||
let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
|
||||
|
||||
// Add points to grid cells without collecting all positions first
|
||||
for i in 0..point_count {
|
||||
let pos = vector_data_transform.transform_point2(vector_data.point_domain.positions()[i]);
|
||||
let grid_x = ((pos.x - min_x) / distance).floor() as i32;
|
||||
let grid_y = ((pos.y - min_y) / distance).floor() as i32;
|
||||
|
||||
grid.entry((grid_x, grid_y)).or_default().push(i);
|
||||
}
|
||||
|
||||
// Create point index mapping for merged points
|
||||
let mut point_index_map = vec![None; point_count];
|
||||
let mut merged_positions = Vec::new();
|
||||
let mut merged_indices = Vec::new();
|
||||
|
||||
// Process each point
|
||||
for i in 0..point_count {
|
||||
// Skip points that have already been processed
|
||||
if point_index_map[i].is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pos_i = vector_data_transform.transform_point2(vector_data.point_domain.positions()[i]);
|
||||
let grid_x = ((pos_i.x - min_x) / distance).floor() as i32;
|
||||
let grid_y = ((pos_i.y - min_y) / distance).floor() as i32;
|
||||
|
||||
let mut group = vec![i];
|
||||
|
||||
// Check only neighboring cells (3x3 grid around current cell)
|
||||
for dx in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
let neighbor_cell = (grid_x + dx, grid_y + dy);
|
||||
|
||||
if let Some(indices) = grid.get(&neighbor_cell) {
|
||||
for &j in indices {
|
||||
if j > i && point_index_map[j].is_none() {
|
||||
let pos_j = vector_data_transform.transform_point2(vector_data.point_domain.positions()[j]);
|
||||
if pos_i.distance(pos_j) <= distance {
|
||||
group.push(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create merged point - calculate positions as needed
|
||||
let merged_position = group
|
||||
.iter()
|
||||
.map(|&idx| vector_data_transform.transform_point2(vector_data.point_domain.positions()[idx]))
|
||||
.fold(DVec2::ZERO, |sum, pos| sum + pos)
|
||||
/ group.len() as f64;
|
||||
|
||||
let merged_position = vector_data_transform.inverse().transform_point2(merged_position);
|
||||
let merged_index = merged_positions.len();
|
||||
|
||||
merged_positions.push(merged_position);
|
||||
merged_indices.push(vector_data.point_domain.ids()[group[0]]);
|
||||
|
||||
// Update mapping for all points in the group
|
||||
for &idx in &group {
|
||||
point_index_map[idx] = Some(merged_index);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new point domain with merged points
|
||||
let mut new_point_domain = PointDomain::new();
|
||||
for (idx, pos) in merged_indices.into_iter().zip(merged_positions) {
|
||||
new_point_domain.push(idx, pos);
|
||||
}
|
||||
|
||||
// Update segment domain
|
||||
let mut new_segment_domain = SegmentDomain::new();
|
||||
for segment_idx in 0..vector_data.segment_domain.ids().len() {
|
||||
let id = vector_data.segment_domain.ids()[segment_idx];
|
||||
let start = vector_data.segment_domain.start_point()[segment_idx];
|
||||
let end = vector_data.segment_domain.end_point()[segment_idx];
|
||||
let handles = vector_data.segment_domain.handles()[segment_idx];
|
||||
let stroke = vector_data.segment_domain.stroke()[segment_idx];
|
||||
|
||||
// Get new indices for start and end points
|
||||
let new_start = point_index_map[start].unwrap();
|
||||
let new_end = point_index_map[end].unwrap();
|
||||
|
||||
// Skip segments where start and end points were merged
|
||||
if new_start != new_end {
|
||||
new_segment_domain.push(id, new_start, new_end, handles, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new vector data
|
||||
let mut result = vector_data.clone();
|
||||
result.point_domain = new_point_domain;
|
||||
result.segment_domain = new_segment_domain;
|
||||
|
||||
// Create and return the result
|
||||
let mut result_table = VectorDataTable::new(result);
|
||||
*result_table.transform_mut() = vector_data_transform;
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Debug"), path(graphene_core::vector))]
|
||||
async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle: VectorDataTable) -> VectorDataTable {
|
||||
let vector_data_transform = vector_data.transform();
|
||||
let vector_data = vector_data.one_instance().instance.clone();
|
||||
|
||||
let target_transform = rectangle.transform();
|
||||
let target = rectangle.one_instance().instance;
|
||||
|
||||
// Get the bounding box of the source vector data
|
||||
let source_bbox = vector_data.bounding_box_with_transform(vector_data_transform).unwrap_or([DVec2::ZERO, DVec2::ONE]);
|
||||
|
||||
// Extract first 4 points from target shape to form the quadrilateral
|
||||
// Apply the target's transform to get points in world space
|
||||
let target_points: Vec<DVec2> = target.point_domain.positions().iter().map(|&p| target_transform.transform_point2(p)).take(4).collect();
|
||||
|
||||
// If we have fewer than 4 points, use the corners of the source bounding box
|
||||
// This handles the degenerative case
|
||||
let dst_corners = if target_points.len() >= 4 {
|
||||
[target_points[0], target_points[1], target_points[2], target_points[3]]
|
||||
} else {
|
||||
warn!("Target shape has fewer than 4 points. Using source bounding box instead.");
|
||||
[
|
||||
source_bbox[0],
|
||||
DVec2::new(source_bbox[1].x, source_bbox[0].y),
|
||||
source_bbox[1],
|
||||
DVec2::new(source_bbox[0].x, source_bbox[1].y),
|
||||
]
|
||||
};
|
||||
|
||||
// Apply the warp
|
||||
let mut result = vector_data.clone();
|
||||
|
||||
// Precompute source bounding box size for normalization
|
||||
let source_size = source_bbox[1] - source_bbox[0];
|
||||
|
||||
// Transform points
|
||||
for (_, position) in result.point_domain.positions_mut() {
|
||||
// Get the point in world space
|
||||
let world_pos = vector_data_transform.transform_point2(*position);
|
||||
|
||||
// Normalize coordinates within the source bounding box
|
||||
let t = ((world_pos - source_bbox[0]) / source_size).clamp(DVec2::ZERO, DVec2::ONE);
|
||||
|
||||
// Apply bilinear interpolation
|
||||
*position = bilinear_interpolate(t, &dst_corners);
|
||||
}
|
||||
|
||||
// Transform handles in bezier curves
|
||||
for (_, handles, _, _) in result.handles_mut() {
|
||||
*handles = handles.apply_transformation(|pos| {
|
||||
// Get the handle in world space
|
||||
let world_pos = vector_data_transform.transform_point2(pos);
|
||||
|
||||
// Normalize coordinates within the source bounding box
|
||||
let t = ((world_pos - source_bbox[0]) / source_size).clamp(DVec2::ZERO, DVec2::ONE);
|
||||
|
||||
// Apply bilinear interpolation
|
||||
bilinear_interpolate(t, &dst_corners)
|
||||
});
|
||||
}
|
||||
|
||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
// Create a new VectorDataTable with the result
|
||||
let mut result_table = VectorDataTable::new(result);
|
||||
|
||||
// Reset the transform since we've applied it directly to the points
|
||||
*result_table.transform_mut() = DAffine2::IDENTITY;
|
||||
|
||||
result_table
|
||||
}
|
||||
|
||||
// Interpolate within a quadrilateral using normalized coordinates (0-1)
|
||||
fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 {
|
||||
let tl = quad[0]; // Top-left
|
||||
let tr = quad[1]; // Top-right
|
||||
let br = quad[2]; // Bottom-right
|
||||
let bl = quad[3]; // Bottom-left
|
||||
|
||||
// Bilinear interpolation
|
||||
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.)]
|
||||
#[min(0.)]
|
||||
max_handle_distance: f64,
|
||||
) -> VectorDataTable {
|
||||
let vector_data_transform = vector_data.transform();
|
||||
let mut vector_data = vector_data.one_instance().instance.clone();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = VectorDataTable::new(vector_data);
|
||||
*result.transform_mut() = vector_data_transform;
|
||||
result
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn generate_handles(
|
||||
_: impl Ctx,
|
||||
source: VectorDataTable,
|
||||
#[default(0.4)]
|
||||
#[range((0., 1.))]
|
||||
curvature: f64,
|
||||
) -> VectorDataTable {
|
||||
let source_transform = source.transform();
|
||||
let source = source.one_instance().instance;
|
||||
|
||||
let mut result = VectorData::empty();
|
||||
result.style = source.style.clone();
|
||||
|
||||
for mut subpath in source.stroke_bezier_paths() {
|
||||
subpath.apply_transform(source_transform);
|
||||
|
||||
let groups = subpath.manipulator_groups();
|
||||
if groups.len() < 2 {
|
||||
// Not enough points for softening
|
||||
result.append_subpath(subpath, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut new_groups = Vec::with_capacity(groups.len());
|
||||
let is_closed = subpath.closed();
|
||||
|
||||
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 has_handles || (!is_closed && (i == 0 || i == groups.len() - 1)) {
|
||||
new_groups.push(*curr);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get previous and next points
|
||||
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 };
|
||||
|
||||
let prev = groups[prev_idx].anchor;
|
||||
let curr_pos = curr.anchor;
|
||||
let next = groups[next_idx].anchor;
|
||||
|
||||
// Calculate directions 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
|
||||
if dir_prev.length_squared() < 1e-5 || dir_next.length_squared() < 1e-5 {
|
||||
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 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;
|
||||
|
||||
// Create new manipulator group with handles
|
||||
new_groups.push(ManipulatorGroup {
|
||||
anchor: curr_pos,
|
||||
in_handle: Some(curr_pos + handle_dir * in_length),
|
||||
out_handle: Some(curr_pos - handle_dir * out_length),
|
||||
id: curr.id,
|
||||
});
|
||||
}
|
||||
|
||||
let mut softened_subpath = Subpath::new(new_groups, is_closed);
|
||||
softened_subpath.apply_transform(source_transform.inverse());
|
||||
result.append_subpath(softened_subpath, true);
|
||||
}
|
||||
|
||||
let mut result_table = VectorDataTable::new(result);
|
||||
*result_table.transform_mut() = source_transform;
|
||||
result_table
|
||||
}
|
||||
|
||||
// TODO: Fix issues and reenable
|
||||
// #[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
// async fn subdivide(
|
||||
// _: impl Ctx,
|
||||
// source: VectorDataTable,
|
||||
// #[default(1.)]
|
||||
// #[min(1.)]
|
||||
// #[max(8.)]
|
||||
// subdivisions: f64,
|
||||
// ) -> VectorDataTable {
|
||||
// let source_transform = source.transform();
|
||||
// let source_vector_data = source.one_instance().instance;
|
||||
// let subdivisions = subdivisions as usize;
|
||||
|
||||
// let mut result = VectorData::empty();
|
||||
// result.style = source_vector_data.style.clone();
|
||||
|
||||
// for mut subpath in source_vector_data.stroke_bezier_paths() {
|
||||
// subpath.apply_transform(source_transform);
|
||||
|
||||
// if subpath.manipulator_groups().len() < 2 {
|
||||
// // Not enough points to subdivide
|
||||
// result.append_subpath(subpath, true);
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// // Apply subdivisions recursively
|
||||
// let mut current_subpath = subpath;
|
||||
// for _ in 0..subdivisions {
|
||||
// current_subpath = subdivide_once(¤t_subpath);
|
||||
// }
|
||||
|
||||
// current_subpath.apply_transform(source_transform.inverse());
|
||||
// result.append_subpath(current_subpath, true);
|
||||
// }
|
||||
|
||||
// let mut result_table = VectorDataTable::new(result);
|
||||
// *result_table.transform_mut() = source_transform;
|
||||
// result_table
|
||||
// }
|
||||
|
||||
// fn subdivide_once(subpath: &Subpath<PointId>) -> Subpath<PointId> {
|
||||
// let original_groups = subpath.manipulator_groups();
|
||||
// let mut new_groups = Vec::new();
|
||||
// let is_closed = subpath.closed();
|
||||
// let mut last_in_handle = None;
|
||||
|
||||
// for i in 0..original_groups.len() {
|
||||
// let start_idx = i;
|
||||
// let end_idx = (i + 1) % original_groups.len();
|
||||
|
||||
// // Skip the last segment for open paths
|
||||
// if !is_closed && end_idx == 0 {
|
||||
// break;
|
||||
// }
|
||||
|
||||
// let current_bezier = original_groups[start_idx].to_bezier(&original_groups[end_idx]);
|
||||
|
||||
// // Create modified start point with original ID, but updated in_handle & out_handle
|
||||
// let mut start_point = original_groups[start_idx].clone();
|
||||
// let [first, _] = current_bezier.split(TValue::Euclidean(0.5));
|
||||
// start_point.out_handle = first.handle_start();
|
||||
// start_point.in_handle = last_in_handle;
|
||||
// if new_groups.contains(&start_point) {
|
||||
// debug!("start_point already in");
|
||||
// } else {
|
||||
// new_groups.push(start_point);
|
||||
// }
|
||||
|
||||
// // Add midpoint
|
||||
// let [first, second] = current_bezier.split(TValue::Euclidean(0.5));
|
||||
|
||||
// let new_point = ManipulatorGroup {
|
||||
// anchor: first.end,
|
||||
// in_handle: first.handle_end(),
|
||||
// out_handle: second.handle_start(),
|
||||
// id: start_point.id.generate_from_hash(u64::MAX),
|
||||
// };
|
||||
// if new_groups.contains(&new_point) {
|
||||
// debug!("new_point already in");
|
||||
// } else {
|
||||
// new_groups.push(new_point);
|
||||
// }
|
||||
|
||||
// last_in_handle = second.handle_end();
|
||||
// }
|
||||
|
||||
// // Handle the final point for open paths
|
||||
// if !is_closed && !original_groups.is_empty() {
|
||||
// let mut last_point = original_groups.last().unwrap().clone();
|
||||
// last_point.in_handle = last_in_handle;
|
||||
// if new_groups.contains(&last_point) {
|
||||
// debug!("last_point already in");
|
||||
// } else {
|
||||
// new_groups.push(last_point);
|
||||
// }
|
||||
// } else if is_closed && !new_groups.is_empty() {
|
||||
// // Update the first point's in_handle for closed paths
|
||||
// new_groups[0].in_handle = last_in_handle;
|
||||
// }
|
||||
|
||||
// Subpath::new(new_groups, is_closed)
|
||||
// }
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
|
||||
let vector_data_transform = vector_data.transform();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue