@@ -10,6 +10,7 @@ use glam::{DAffine2, DVec2};
1010use graphic_types:: Vector ;
1111use graphic_types:: raster_types:: { CPU , GPU , Raster } ;
1212use graphic_types:: { Graphic , IntoGraphicTable } ;
13+ use kurbo:: simplify:: { SimplifyOptions , simplify_bezpath} ;
1314use kurbo:: { Affine , BezPath , DEFAULT_ACCURACY , Line , ParamCurve , PathEl , PathSeg , Shape } ;
1415use rand:: { Rng , SeedableRng } ;
1516use 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