Skip to content

Commit 17da9ec

Browse files
authored
New nodes: 'Decimate' and 'Simplify' (#3851)
* New node: Decimate * Use preallocated stack to reduce number of allocations and fix double endpoints on closed paths * Use Kurbo implementation of path-to-polyline sampling * Add the 'Simplify' node
1 parent d5d10fe commit 17da9ec

File tree

1 file changed

+171
-0
lines changed

1 file changed

+171
-0
lines changed

node-graph/nodes/vector/src/vector_nodes.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use glam::{DAffine2, DVec2};
1010
use graphic_types::Vector;
1111
use graphic_types::raster_types::{CPU, GPU, Raster};
1212
use graphic_types::{Graphic, IntoGraphicTable};
13+
use kurbo::simplify::{SimplifyOptions, simplify_bezpath};
1314
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, PathEl, PathSeg, Shape};
1415
use rand::{Rng, SeedableRng};
1516
use std::collections::hash_map::DefaultHasher;
@@ -1306,6 +1307,176 @@ async fn sample_polyline(
13061307
.collect()
13071308
}
13081309

1310+
/// Simplifies vector paths by reducing the number of curve segments while preserving the overall shape within the given tolerance.
1311+
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
1312+
async fn simplify(
1313+
_: impl Ctx,
1314+
/// The vector paths to simplify.
1315+
content: Table<Vector>,
1316+
/// The maximum distance the simplified path may deviate from the original.
1317+
#[default(5.)]
1318+
#[unit(" px")]
1319+
tolerance: Length,
1320+
) -> Table<Vector> {
1321+
if tolerance <= 0. {
1322+
return content;
1323+
}
1324+
1325+
let options = SimplifyOptions::default();
1326+
1327+
content
1328+
.into_iter()
1329+
.map(|mut row| {
1330+
let transform = Affine::new(row.transform.to_cols_array());
1331+
let inverse_transform = transform.inverse();
1332+
1333+
let mut result = Vector {
1334+
style: std::mem::take(&mut row.element.style),
1335+
upstream_data: std::mem::take(&mut row.element.upstream_data),
1336+
..Default::default()
1337+
};
1338+
1339+
for mut bezpath in row.element.stroke_bezpath_iter() {
1340+
bezpath.apply_affine(transform);
1341+
1342+
let mut simplified = simplify_bezpath(bezpath, tolerance, &options);
1343+
1344+
simplified.apply_affine(inverse_transform);
1345+
result.append_bezpath(simplified);
1346+
}
1347+
1348+
row.element = result;
1349+
row
1350+
})
1351+
.collect()
1352+
}
1353+
1354+
/// Decimates vector paths into polylines by sampling any curves into line segments, then removing points that don't significantly contribute to the shape using the Ramer-Douglas-Peucker algorithm.
1355+
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
1356+
async fn decimate(
1357+
_: impl Ctx,
1358+
/// The vector paths to decimate.
1359+
content: Table<Vector>,
1360+
/// The maximum distance a point can deviate from the simplified path before it is kept.
1361+
#[default(5.)]
1362+
#[unit(" px")]
1363+
tolerance: Length,
1364+
) -> Table<Vector> {
1365+
// Tolerance of 0 means no simplification is possible, so return immediately
1366+
if tolerance <= 0. {
1367+
return content;
1368+
}
1369+
1370+
// Below this squared length, a line segment is treated as a degenerate point and the distance
1371+
// falls back to a simple point-to-point measurement to avoid division by near-zero.
1372+
const NEAR_ZERO_LENGTH_SQUARED: f64 = 1e-20;
1373+
1374+
fn perpendicular_distance(point: DVec2, line_start: DVec2, line_end: DVec2) -> f64 {
1375+
let line_vector = line_end - line_start;
1376+
let line_length_squared = line_vector.length_squared();
1377+
if line_length_squared < NEAR_ZERO_LENGTH_SQUARED {
1378+
return point.distance(line_start);
1379+
}
1380+
(point - line_start).perp_dot(line_vector).abs() / line_length_squared.sqrt()
1381+
}
1382+
1383+
fn rdp_simplify(points: &[DVec2], tolerance: f64) -> Vec<DVec2> {
1384+
if points.len() < 3 {
1385+
return points.to_vec();
1386+
}
1387+
1388+
let mut keep = vec![false; points.len()];
1389+
keep[0] = true;
1390+
keep[points.len() - 1] = true;
1391+
1392+
let mut stack = vec![(0, points.len() - 1)];
1393+
1394+
while let Some((start_index, end_index)) = stack.pop() {
1395+
let start = points[start_index];
1396+
let end = points[end_index];
1397+
1398+
let mut max_distance = 0.;
1399+
let mut max_index = 0;
1400+
1401+
for (i, &point) in points.iter().enumerate().take(end_index).skip(start_index + 1) {
1402+
let distance = perpendicular_distance(point, start, end);
1403+
if distance > max_distance {
1404+
max_distance = distance;
1405+
max_index = i;
1406+
}
1407+
}
1408+
1409+
if max_distance > tolerance {
1410+
keep[max_index] = true;
1411+
if max_index - start_index > 1 {
1412+
stack.push((start_index, max_index));
1413+
}
1414+
if end_index - max_index > 1 {
1415+
stack.push((max_index, end_index));
1416+
}
1417+
}
1418+
}
1419+
1420+
points.iter().enumerate().filter(|(i, _)| keep[*i]).map(|(_, p)| *p).collect()
1421+
}
1422+
1423+
content
1424+
.into_iter()
1425+
.map(|mut row| {
1426+
let transform = Affine::new(row.transform.to_cols_array());
1427+
let inverse_transform = transform.inverse();
1428+
1429+
let mut result = Vector {
1430+
style: std::mem::take(&mut row.element.style),
1431+
upstream_data: std::mem::take(&mut row.element.upstream_data),
1432+
..Default::default()
1433+
};
1434+
1435+
for mut bezpath in row.element.stroke_bezpath_iter() {
1436+
bezpath.apply_affine(transform);
1437+
1438+
let is_closed = matches!(bezpath.elements().last(), Some(PathEl::ClosePath));
1439+
1440+
// Flatten the bezpath into line segments, then collect the points
1441+
let mut points = Vec::new();
1442+
kurbo::flatten(bezpath, tolerance * 0.5, |el| match el {
1443+
PathEl::MoveTo(p) | PathEl::LineTo(p) => {
1444+
points.push(DVec2::new(p.x, p.y));
1445+
}
1446+
_ => {}
1447+
});
1448+
1449+
// For closed paths, the last point duplicates the first, so remove it
1450+
if is_closed && points.len() > 1 && points.last() == points.first() {
1451+
points.pop();
1452+
}
1453+
1454+
// Apply RDP simplification
1455+
let simplified = rdp_simplify(&points, tolerance);
1456+
if simplified.is_empty() {
1457+
continue;
1458+
}
1459+
1460+
// Reconstruct as a polyline
1461+
let mut new_bezpath = BezPath::new();
1462+
new_bezpath.move_to((simplified[0].x, simplified[0].y));
1463+
for &point in &simplified[1..] {
1464+
new_bezpath.line_to((point.x, point.y));
1465+
}
1466+
if is_closed {
1467+
new_bezpath.close_path();
1468+
}
1469+
1470+
new_bezpath.apply_affine(inverse_transform);
1471+
result.append_bezpath(new_bezpath);
1472+
}
1473+
1474+
row.element = result;
1475+
row
1476+
})
1477+
.collect()
1478+
}
1479+
13091480
/// Cuts a path at a given progression from 0 to 1 along the path, creating two new subpaths from the original one (if the path is initially open) or one open subpath (if the path is initially closed).
13101481
///
13111482
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.

0 commit comments

Comments
 (0)