mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Tweaks, crash fixes and a few new nodes
This commit is contained in:
parent
0c3cae2ba5
commit
914360551f
5 changed files with 602 additions and 24 deletions
34
frontend/package-lock.json
generated
34
frontend/package-lock.json
generated
|
|
@ -7,7 +7,7 @@
|
|||
"name": "graphite-web-frontend",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.40.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
|
|
@ -989,15 +989,13 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz",
|
||||
"integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==",
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz",
|
||||
"integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
|
|
@ -1050,16 +1048,6 @@
|
|||
"vite": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.2.0.tgz",
|
||||
"integrity": "sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
|
|
@ -4668,6 +4656,20 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.34.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz",
|
||||
"integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"wasm:watch-production": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --release --target=web -- --color=always\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.40.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ impl Bezier {
|
|||
pub(crate) fn t_value_to_parametric(&self, t: TValue) -> f64 {
|
||||
match t {
|
||||
TValue::Parametric(t) => {
|
||||
assert!((0.0..=1.).contains(&t));
|
||||
t
|
||||
// Clamp the t value to the valid range [0.0, 1.0]
|
||||
t.clamp(0.0, 1.0)
|
||||
}
|
||||
TValue::Euclidean(t) => {
|
||||
assert!((0.0..=1.).contains(&t));
|
||||
|
|
|
|||
|
|
@ -212,7 +212,8 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
|
|||
(segment_index, t)
|
||||
}
|
||||
SubpathTValue::GlobalParametric(global_t) => {
|
||||
assert!((0.0..=1.).contains(&global_t));
|
||||
// Clamp global_t to the valid range [0, 1] instead of asserting
|
||||
let global_t = global_t.clamp(0.0, 1.0);
|
||||
|
||||
if global_t == 1. {
|
||||
return (self.len_segments() - 1, 1.);
|
||||
|
|
|
|||
|
|
@ -659,7 +659,7 @@ async fn spatial_merge_by_distance(
|
|||
}
|
||||
|
||||
#[node_macro::node(category("Debug"), path(graphene_core::vector))]
|
||||
async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle: VectorDataTable) -> VectorDataTable {
|
||||
async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle: VectorDataTable, #[default(false)] preserve_aspect_ratio: bool) -> VectorDataTable {
|
||||
let vector_data_transform = vector_data.transform();
|
||||
let vector_data = vector_data.one_instance_ref().instance.clone();
|
||||
|
||||
|
|
@ -687,6 +687,42 @@ async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle
|
|||
]
|
||||
};
|
||||
|
||||
// If we're preserving aspect ratio, we need to calculate a modified destination rectangle
|
||||
let modified_corners = if preserve_aspect_ratio {
|
||||
// Calculate destination bounds from the corners
|
||||
let dst_min = dst_corners.iter().fold(DVec2::splat(f64::MAX), |min, &p| min.min(p));
|
||||
let dst_max = dst_corners.iter().fold(DVec2::splat(f64::MIN), |max, &p| max.max(p));
|
||||
let dst_size = dst_max - dst_min;
|
||||
let dst_center = dst_min + dst_size * 0.5;
|
||||
|
||||
// Calculate source aspect ratio
|
||||
let source_size = source_bbox[1] - source_bbox[0];
|
||||
let source_ratio = source_size.x / source_size.y;
|
||||
|
||||
// Calculate destination aspect ratio
|
||||
let dst_ratio = dst_size.x / dst_size.y;
|
||||
|
||||
// Determine the new size that preserves aspect ratio
|
||||
let new_dst_size = if source_ratio > dst_ratio {
|
||||
// Width constrained
|
||||
DVec2::new(dst_size.x, dst_size.x / source_ratio)
|
||||
} else {
|
||||
// Height constrained
|
||||
DVec2::new(dst_size.y * source_ratio, dst_size.y)
|
||||
};
|
||||
|
||||
// Calculate the new corners for the aspect-preserved rectangle
|
||||
let half_size = new_dst_size * 0.5;
|
||||
[
|
||||
dst_center - half_size, // Top-left
|
||||
DVec2::new(dst_center.x + half_size.x, dst_center.y - half_size.y), // Top-right
|
||||
dst_center + half_size, // Bottom-right
|
||||
DVec2::new(dst_center.x - half_size.x, dst_center.y + half_size.y), // Bottom-left
|
||||
]
|
||||
} else {
|
||||
dst_corners
|
||||
};
|
||||
|
||||
// Apply the warp
|
||||
let mut result = vector_data.clone();
|
||||
|
||||
|
|
@ -701,8 +737,8 @@ async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle
|
|||
// 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);
|
||||
// Apply bilinear interpolation with either the original or modified corners
|
||||
*position = bilinear_interpolate(t, &modified_corners);
|
||||
}
|
||||
|
||||
// Transform handles in bezier curves
|
||||
|
|
@ -714,8 +750,8 @@ async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle
|
|||
// 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)
|
||||
// Apply bilinear interpolation with either the original or modified corners
|
||||
bilinear_interpolate(t, &modified_corners)
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -730,6 +766,278 @@ async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle
|
|||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Select Subpath by Index"), category("Vector"), path(graphene_core::vector))]
|
||||
async fn select_subpath_by_index(_ctx: impl Ctx + ExtractAll + CloneVarArgs, vector_data: VectorDataTable, #[default(0)] index: u64, #[default(false)] reverse: bool) -> VectorDataTable {
|
||||
let source_transform = vector_data.transform();
|
||||
let source_instance = vector_data.one_instance_ref();
|
||||
let source_vector_data = source_instance.instance;
|
||||
|
||||
let mut result_vector_data = VectorData::empty();
|
||||
result_vector_data.style = source_vector_data.style.clone();
|
||||
result_vector_data.upstream_graphic_group = source_vector_data.upstream_graphic_group.clone();
|
||||
|
||||
// Collect all subpaths
|
||||
let subpaths: Vec<_> = source_vector_data.stroke_bezier_paths().collect();
|
||||
|
||||
// Skip if no subpaths found
|
||||
if subpaths.is_empty() {
|
||||
let mut result_table = VectorDataTable::new(result_vector_data);
|
||||
*result_table.transform_mut() = source_transform;
|
||||
return result_table;
|
||||
}
|
||||
|
||||
// Select the subpath at the specified index (with bounds checking)
|
||||
let subpath_count = subpaths.len();
|
||||
let idx = if reverse {
|
||||
subpath_count.saturating_sub(1).saturating_sub(index as usize % subpath_count.max(1))
|
||||
} else {
|
||||
index as usize % subpath_count.max(1)
|
||||
};
|
||||
|
||||
if let Some(selected_subpath) = subpaths.get(idx) {
|
||||
// Add the selected subpath to the result VectorData
|
||||
// We need to clone because append_subpath takes ownership, but we only have a reference
|
||||
result_vector_data.append_subpath(selected_subpath.clone(), true);
|
||||
}
|
||||
|
||||
// Create the result table and set the transform
|
||||
let mut result_table = VectorDataTable::new(result_vector_data);
|
||||
*result_table.transform_mut() = source_transform;
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Select Points Within Shape"), category("Vector"), path(graphene_core::vector))]
|
||||
async fn select_points_within_shape(
|
||||
_: impl Ctx,
|
||||
/// The vector data containing points to filter.
|
||||
points_data: VectorDataTable,
|
||||
/// The vector data representing the shape to filter points by.
|
||||
#[expose]
|
||||
filter_shape: VectorDataTable,
|
||||
) -> VectorDataTable {
|
||||
let points_transform = points_data.transform();
|
||||
let points_instance = points_data.one_instance_ref().instance;
|
||||
|
||||
let filter_transform = filter_shape.transform();
|
||||
let filter_instance = filter_shape.one_instance_ref().instance;
|
||||
|
||||
let mut result_vector_data = VectorData::empty();
|
||||
result_vector_data.style = points_instance.style.clone();
|
||||
result_vector_data.upstream_graphic_group = points_instance.upstream_graphic_group.clone();
|
||||
|
||||
// Pre-transform filter subpaths into world space for efficient checking
|
||||
let filter_subpaths_world: Vec<_> = filter_instance
|
||||
.stroke_bezier_paths()
|
||||
.map(|mut subpath| {
|
||||
subpath.apply_transform(filter_transform);
|
||||
subpath
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Iterate through each point in the input points_data
|
||||
for (point_id, point_pos) in points_instance.point_domain.ids().iter().zip(points_instance.point_domain.positions()) {
|
||||
// Transform the point into world space
|
||||
let point_world = points_transform.transform_point2(*point_pos);
|
||||
|
||||
// Check if the point is contained within any of the filter subpaths
|
||||
let is_contained = filter_subpaths_world.iter().any(|subpath| subpath.contains_point(point_world));
|
||||
|
||||
// If the point is contained, add it to the result
|
||||
if is_contained {
|
||||
result_vector_data.point_domain.push(*point_id, *point_pos);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the result table and set the transform
|
||||
let mut result_table = VectorDataTable::new(result_vector_data);
|
||||
*result_table.transform_mut() = points_transform;
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Select Point by Index"), category("Vector"), path(graphene_core::vector))]
|
||||
async fn select_point_by_index(_ctx: impl Ctx + ExtractAll + CloneVarArgs, vector_data: VectorDataTable, #[default(0)] index: u64, #[default(false)] reverse: bool) -> VectorDataTable {
|
||||
let source_transform = vector_data.transform();
|
||||
let source_instance = vector_data.one_instance_ref();
|
||||
let source_vector_data = source_instance.instance;
|
||||
|
||||
let mut result_vector_data = VectorData::empty();
|
||||
result_vector_data.style = source_vector_data.style.clone();
|
||||
result_vector_data.upstream_graphic_group = source_vector_data.upstream_graphic_group.clone();
|
||||
|
||||
// Get point IDs and positions
|
||||
let point_ids = source_vector_data.point_domain.ids();
|
||||
let point_positions = source_vector_data.point_domain.positions();
|
||||
|
||||
// Skip if no points found
|
||||
if point_ids.is_empty() {
|
||||
let mut result_table = VectorDataTable::new(result_vector_data);
|
||||
*result_table.transform_mut() = source_transform;
|
||||
return result_table;
|
||||
}
|
||||
|
||||
// Select the point at the specified index (with bounds checking)
|
||||
let point_count = point_ids.len();
|
||||
let idx = if reverse {
|
||||
point_count.saturating_sub(1).saturating_sub(index as usize % point_count.max(1))
|
||||
} else {
|
||||
index as usize % point_count.max(1)
|
||||
};
|
||||
|
||||
if let (Some(&selected_id), Some(&selected_position)) = (point_ids.get(idx), point_positions.get(idx)) {
|
||||
// Add the selected point to the result VectorData's PointDomain
|
||||
result_vector_data.point_domain.push(selected_id, selected_position);
|
||||
}
|
||||
|
||||
// Create the result table and set the transform
|
||||
let mut result_table = VectorDataTable::new(result_vector_data);
|
||||
*result_table.transform_mut() = source_transform;
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Select Group by Index"), category("Vector"), path(graphene_core::vector))]
|
||||
async fn select_group_by_index(_ctx: impl Ctx + ExtractAll + CloneVarArgs, shape: GraphicGroupTable, #[default(0)] index: u64, #[default(false)] reverse: bool) -> GraphicGroupTable {
|
||||
let mut result_table = GraphicGroupTable::empty();
|
||||
|
||||
// Get the instances from the input GraphicGroupTable
|
||||
let groups: Vec<_> = shape.instance_iter().collect();
|
||||
|
||||
// Skip if no groups found
|
||||
if groups.is_empty() {
|
||||
return result_table;
|
||||
}
|
||||
|
||||
// Select the group at the specified index (with bounds checking)
|
||||
let group_count = groups.len();
|
||||
let idx = if reverse {
|
||||
group_count.saturating_sub(1).saturating_sub(index as usize % group_count.max(1))
|
||||
} else {
|
||||
index as usize % group_count.max(1)
|
||||
};
|
||||
|
||||
if let Some(selected_group) = groups.get(idx) {
|
||||
// Directly push the selected group element into the result table
|
||||
result_table.push(selected_group.clone());
|
||||
}
|
||||
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn shrink_wrap(
|
||||
_: impl Ctx,
|
||||
vector_data: VectorDataTable,
|
||||
#[default(0.)]
|
||||
#[range((0., 1.))]
|
||||
concavity: f64,
|
||||
) -> VectorDataTable {
|
||||
let vector_data_transform = vector_data.transform();
|
||||
let vector_data = vector_data.one_instance_ref().instance;
|
||||
|
||||
// Collect points from all shapes (including handles for better coverage)
|
||||
let mut points = Vec::new();
|
||||
|
||||
// Add points from all subpaths
|
||||
for subpath in vector_data.stroke_bezier_paths() {
|
||||
let mut transformed_subpath = subpath.clone();
|
||||
transformed_subpath.apply_transform(vector_data_transform);
|
||||
|
||||
// Sample points more densely for better hull accuracy
|
||||
for (t_idx, t) in (0..=20).map(|i| i as f64 / 20.0).enumerate() {
|
||||
let point = transformed_subpath.evaluate(SubpathTValue::GlobalParametric(t));
|
||||
points.push(point);
|
||||
|
||||
// Add extra points for corners to ensure accuracy
|
||||
if t_idx > 0 && t_idx < 20 {
|
||||
let tangent = transformed_subpath.tangent(SubpathTValue::GlobalParametric(t));
|
||||
if tangent.length() < 1e-6 {
|
||||
// Detect corners/cusps
|
||||
for i in 1..=5 {
|
||||
let offset = i as f64 * 0.01;
|
||||
points.push(transformed_subpath.evaluate(SubpathTValue::GlobalParametric(t - offset)));
|
||||
points.push(transformed_subpath.evaluate(SubpathTValue::GlobalParametric(t + offset)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we have enough points for a hull
|
||||
if points.len() < 3 {
|
||||
return VectorDataTable::default();
|
||||
}
|
||||
|
||||
// Compute hull points
|
||||
let hull_points = compute_hull(&points, concavity);
|
||||
|
||||
// Create subpath from hull points
|
||||
let hull_subpath = Subpath::from_anchors(
|
||||
hull_points.iter().map(|&p| vector_data_transform.inverse().transform_point2(p)).collect::<Vec<_>>(),
|
||||
true, // Closed path
|
||||
);
|
||||
|
||||
// Create vector data from hull subpath
|
||||
let mut result = VectorData::from_subpath(&hull_subpath);
|
||||
result.style = vector_data.style.clone();
|
||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
// Return result with original transform
|
||||
let mut result_table = VectorDataTable::new(result);
|
||||
*result_table.transform_mut() = vector_data_transform;
|
||||
result_table
|
||||
}
|
||||
|
||||
// Compute hull points based on concavity parameter
|
||||
fn compute_hull(points: &[DVec2], concavity: f64) -> Vec<DVec2> {
|
||||
// Sort points by polar angle for convex hull computation
|
||||
compute_convex_hull(points)
|
||||
}
|
||||
|
||||
// Compute convex hull using Jarvis march (gift wrapping) algorithm
|
||||
fn compute_convex_hull(points: &[DVec2]) -> Vec<DVec2> {
|
||||
if points.len() < 3 {
|
||||
return points.to_vec();
|
||||
}
|
||||
|
||||
// Find leftmost point
|
||||
let mut leftmost = 0;
|
||||
for i in 1..points.len() {
|
||||
if points[i].x < points[leftmost].x {
|
||||
leftmost = i;
|
||||
}
|
||||
}
|
||||
|
||||
let mut hull = Vec::new();
|
||||
let mut p = leftmost;
|
||||
|
||||
// Jarvis march algorithm
|
||||
loop {
|
||||
hull.push(points[p]);
|
||||
|
||||
let mut q = (p + 1) % points.len();
|
||||
for i in 0..points.len() {
|
||||
// If i is more counter-clockwise than current q, then update q
|
||||
if orient(points[p], points[i], points[q]) > 0.0 {
|
||||
q = i;
|
||||
}
|
||||
}
|
||||
|
||||
p = q;
|
||||
|
||||
// Break if we've come back to start
|
||||
if p == leftmost {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
hull
|
||||
}
|
||||
|
||||
// Helper: Calculate orientation of triplet (p, q, r)
|
||||
// Returns positive if counter-clockwise, negative if clockwise, 0 if collinear
|
||||
fn orient(p: DVec2, q: DVec2, r: DVec2) -> f64 {
|
||||
(q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y)
|
||||
}
|
||||
|
||||
// Interpolate within a quadrilateral using normalized coordinates (0-1)
|
||||
fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 {
|
||||
let tl = quad[0]; // Top-left
|
||||
|
|
@ -741,6 +1049,133 @@ 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 relax(
|
||||
_: impl Ctx,
|
||||
source: VectorDataTable,
|
||||
#[default(0.5)]
|
||||
#[range((0., 1.))]
|
||||
smooth_amount: f64,
|
||||
#[default(0.5)]
|
||||
#[range((0., 1.))]
|
||||
match_original: f64,
|
||||
#[default(8)]
|
||||
#[min(1.0)]
|
||||
iterations: u32,
|
||||
) -> VectorDataTable {
|
||||
let source_transform = source.transform();
|
||||
let source = source.one_instance_ref().instance.clone();
|
||||
|
||||
// For stability, limit the iterations based on smooth amount
|
||||
let iterations = (iterations as f64 * smooth_amount.max(0.1)) as u32;
|
||||
let iterations = iterations.max(1);
|
||||
|
||||
// Convert to relaxation strength and shape preservation
|
||||
let relaxation_strength = smooth_amount.powf(1.5);
|
||||
let shape_preservation = match_original.powf(0.8);
|
||||
|
||||
let mut result = source.clone();
|
||||
|
||||
// Store original positions for shape preservation
|
||||
let original_positions: Vec<DVec2> = result.point_domain.positions().to_vec();
|
||||
|
||||
// Create a mapping of points to their connected neighbors
|
||||
let num_points = result.point_domain.positions().len();
|
||||
let mut connected_neighbors = vec![Vec::new(); num_points];
|
||||
|
||||
for segment_idx in 0..result.segment_domain.ids().len() {
|
||||
let start_id = result.segment_domain.start_point()[segment_idx];
|
||||
let end_id = result.segment_domain.end_point()[segment_idx];
|
||||
|
||||
// Use the indices directly after checking bounds.
|
||||
if start_id < num_points && end_id < num_points {
|
||||
connected_neighbors[start_id].push(end_id);
|
||||
connected_neighbors[end_id].push(start_id);
|
||||
} else {
|
||||
// Indices obtained from SegmentDomain should always be valid if VectorData is consistent.
|
||||
warn!("Segment references out-of-bounds point index in relax node. Start: {}, End: {}, Max: {}", start_id, end_id, num_points);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply iterative relaxation
|
||||
for _ in 0..iterations {
|
||||
// Calculate new positions based on neighbors (Laplacian smoothing)
|
||||
let mut new_positions = Vec::with_capacity(result.point_domain.positions().len());
|
||||
|
||||
for (i, pos) in result.point_domain.positions().iter().enumerate() {
|
||||
let neighbors = &connected_neighbors[i];
|
||||
|
||||
if neighbors.is_empty() {
|
||||
new_positions.push(*pos);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate average of neighboring positions
|
||||
let mut avg_pos = DVec2::ZERO;
|
||||
for &neighbor_idx in neighbors {
|
||||
avg_pos += result.point_domain.positions()[neighbor_idx];
|
||||
}
|
||||
avg_pos /= neighbors.len() as f64;
|
||||
|
||||
// Apply Laplacian smoothing with strength factor
|
||||
let relaxed_pos = pos.lerp(avg_pos, relaxation_strength);
|
||||
|
||||
// Pull back toward original position based on preservation factor
|
||||
let final_pos = relaxed_pos.lerp(original_positions[i], shape_preservation);
|
||||
|
||||
new_positions.push(final_pos);
|
||||
}
|
||||
|
||||
// Update positions
|
||||
for (i, pos) in new_positions.into_iter().enumerate() {
|
||||
result.point_domain.set_position(i, pos);
|
||||
}
|
||||
|
||||
// Apply transformation to handles
|
||||
for (_segment_id, handles, start_idx, end_idx) in result.segment_domain.handles_mut() {
|
||||
// Check if indices are valid before accessing positions and original_positions
|
||||
if start_idx < result.point_domain.positions().len() && end_idx < result.point_domain.positions().len() && start_idx < original_positions.len() && end_idx < original_positions.len() {
|
||||
match handles {
|
||||
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let start_pos = result.point_domain.positions()[start_idx];
|
||||
let end_pos = result.point_domain.positions()[end_idx];
|
||||
|
||||
// Adjust handles to maintain tangents but adapt to new anchor positions
|
||||
let orig_start_to_handle = *handle_start - original_positions[start_idx];
|
||||
let orig_end_to_handle = *handle_end - original_positions[end_idx];
|
||||
|
||||
*handle_start = start_pos + orig_start_to_handle * (1.0 - relaxation_strength * 0.5);
|
||||
*handle_end = end_pos + orig_end_to_handle * (1.0 - relaxation_strength * 0.5);
|
||||
}
|
||||
bezier_rs::BezierHandles::Quadratic { handle } => {
|
||||
let start_pos = result.point_domain.positions()[start_idx];
|
||||
let end_pos = result.point_domain.positions()[end_idx];
|
||||
|
||||
// Reposition quadratic handle based on new anchor positions
|
||||
let original_delta = original_positions[end_idx] - original_positions[start_idx];
|
||||
if original_delta.length_squared() > 1e-12 {
|
||||
// Project the original handle offset onto the original segment direction
|
||||
let orig_handle_ratio = (*handle - original_positions[start_idx]).dot(original_delta) / original_delta.length_squared();
|
||||
// Apply the same ratio to the new segment
|
||||
*handle = start_pos + orig_handle_ratio.clamp(0.0, 1.0) * (end_pos - start_pos);
|
||||
} else {
|
||||
// Handle degenerate case: start and end points were originally the same.
|
||||
// Place the handle halfway between the new positions.
|
||||
*handle = start_pos.lerp(end_pos, 0.5);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create final result
|
||||
let mut result_table = VectorDataTable::new(result);
|
||||
*result_table.transform_mut() = source_transform;
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn remove_handles(
|
||||
_: impl Ctx,
|
||||
|
|
@ -1285,6 +1720,145 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64,
|
|||
result
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn outer_path(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
|
||||
let vector_data_transform = vector_data.transform();
|
||||
let vector_data = vector_data.one_instance_ref().instance;
|
||||
|
||||
// Create a new VectorData to store our result
|
||||
let mut result = VectorData::empty();
|
||||
result.style = vector_data.style.clone();
|
||||
|
||||
// Collect all closed subpaths with their indices
|
||||
let subpaths: Vec<_> = vector_data
|
||||
.stroke_bezier_paths()
|
||||
.enumerate()
|
||||
.filter(|(_, subpath)| subpath.closed())
|
||||
.map(|(i, mut subpath)| {
|
||||
// Apply transform to work in world space
|
||||
subpath.apply_transform(vector_data_transform);
|
||||
(i, subpath)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if subpaths.is_empty() {
|
||||
// No closed paths found, return empty result
|
||||
let mut result_table = VectorDataTable::new(result);
|
||||
*result_table.transform_mut() = vector_data_transform;
|
||||
return result_table;
|
||||
}
|
||||
|
||||
// Find the outermost path:
|
||||
// 1. Start by assuming all subpaths could be the outermost
|
||||
let mut potential_outer_paths: Vec<_> = subpaths.iter().map(|(i, _)| *i).collect();
|
||||
|
||||
// 2. For each subpath, test against all others
|
||||
for (i, subpath_i) in &subpaths {
|
||||
// Skip if already eliminated
|
||||
if !potential_outer_paths.contains(i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (j, subpath_j) in &subpaths {
|
||||
// Skip comparing to self
|
||||
if i == j {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sample points from subpath_i
|
||||
let total_points = 20;
|
||||
let mut contained_points = 0;
|
||||
|
||||
// Check several points along subpath_i to see if they're contained in subpath_j
|
||||
for k in 0..total_points {
|
||||
let t = k as f64 / total_points as f64;
|
||||
let point = subpath_i.evaluate(bezier_rs::SubpathTValue::GlobalParametric(t));
|
||||
|
||||
if subpath_j.contains_point(point) {
|
||||
contained_points += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If most points from subpath_i are inside subpath_j,
|
||||
// then subpath_i is not an outer path
|
||||
if contained_points > total_points / 2 {
|
||||
potential_outer_paths.retain(|&x| x != *i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Among remaining potential outer paths, choose the one with the largest area
|
||||
if let Some(&outer_index) = potential_outer_paths.iter().max_by(|&&a, &&b| {
|
||||
let area_a = subpaths.iter().find(|(i, _)| *i == a).map(|(_, s)| s.area(None, None).abs()).unwrap_or(0.0);
|
||||
let area_b = subpaths.iter().find(|(i, _)| *i == b).map(|(_, s)| s.area(None, None).abs()).unwrap_or(0.0);
|
||||
area_a.partial_cmp(&area_b).unwrap_or(std::cmp::Ordering::Equal)
|
||||
}) {
|
||||
// Find the outer path and add it to the result
|
||||
if let Some((_, outer_path)) = subpaths.iter().find(|(i, _)| *i == outer_index) {
|
||||
let mut path_copy = outer_path.clone();
|
||||
path_copy.apply_transform(vector_data_transform.inverse());
|
||||
result.append_subpath(path_copy, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the resulting VectorDataTable
|
||||
let mut result_table = VectorDataTable::new(result);
|
||||
*result_table.transform_mut() = vector_data_transform;
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Filter by Area"), category("Vector"), path(graphene_core::vector))]
|
||||
async fn filter_by_area(
|
||||
_: impl Ctx,
|
||||
/// The vector data to filter
|
||||
vector_data: VectorDataTable,
|
||||
/// Minimum area threshold - subpaths with areas smaller than this will be removed
|
||||
#[default(1.0)]
|
||||
#[min(0.0)]
|
||||
min_area: f64,
|
||||
/// Whether to filter only closed paths (true) or all paths (false)
|
||||
#[default(true)]
|
||||
only_closed: bool,
|
||||
) -> VectorDataTable {
|
||||
let vector_data_transform = vector_data.transform();
|
||||
let vector_data = vector_data.one_instance_ref().instance;
|
||||
|
||||
// Create a new VectorData to store filtered subpaths
|
||||
let mut result = VectorData::empty();
|
||||
result.style = vector_data.style.clone();
|
||||
result.upstream_graphic_group = vector_data.upstream_graphic_group.clone();
|
||||
|
||||
// Calculate the scale to apply to area calculations
|
||||
let scale = vector_data_transform.decompose_scale();
|
||||
let scale_factor = scale[0] * scale[1];
|
||||
|
||||
for subpath in vector_data.stroke_bezier_paths() {
|
||||
// Skip open paths if only_closed is true
|
||||
if only_closed && !subpath.closed() {
|
||||
result.append_subpath(subpath, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get a copy with the transform applied for area calculation
|
||||
let mut transformed_subpath = subpath.clone();
|
||||
transformed_subpath.apply_transform(vector_data_transform);
|
||||
|
||||
// Calculate the area of this subpath with proper scaling
|
||||
let area = transformed_subpath.area(Some(1e-3), Some(1e-3)).abs() * scale_factor.abs();
|
||||
|
||||
// If area is above threshold, keep it
|
||||
if area >= min_area {
|
||||
result.append_subpath(subpath, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new VectorDataTable with the filtered result
|
||||
let mut result_table = VectorDataTable::new(result);
|
||||
*result_table.transform_mut() = vector_data_transform;
|
||||
result_table
|
||||
}
|
||||
|
||||
/// Determines the position of a point on the path, given by its progress from 0 to 1 along the path.
|
||||
/// If multiple subpaths make up the path, the whole number part of the progress value selects the subpath and the decimal part determines the position along it.
|
||||
#[node_macro::node(name("Position on Path"), category("Vector"), path(graphene_core::vector))]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue