mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 06:18:11 +08:00
Add path-bool library (#1952)
* Add path-bool library * Cleanup code * Cargo format * Integrate boolean ops into graphite * Add test for editor crash * Fix edge sort floating point instability * Add unit test for red-dress failure * Backport tests and aux functions * Use curvature based sorting * Convert linear cubic splines to line segments * Deduplicate reversed path segments * Fix epsilon for empty segments * Remove parameter based intersection pruning * Add support for reversed paths * Add benchmark infrastructure * Add intersection benchmark * Add recursion bound * Implement support for overlapping path segments * Remove rouge prinln * Fix sorting for bezier segments with one control point at the start of the segment * Cleanup log statements * Directly translate graphite paths to Path segments * Round data before passing it to path_bool * Fix flag_faces traversal order * Add test for white dots in bottom right of painted dreams * Make rounding configurable * Update demo artwork to remove manual path modifications * Convert from path segments to manipulator groups directly * Remove dead code * Fix clippy lints * Replace functions in path segment with methods and add documentation * Add more documentation * Close subpaths * Reorganize files and add README.md * Add license information * Code review * Fix license info * Adopt new node macro and fix demo artwork * Close subpaths with Z --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
2febbfd698
commit
3eb98c6d6d
@@ -0,0 +1,261 @@
|
||||
#![expect(clippy::needless_doctest_main)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
mod path_boolean;
|
||||
#[cfg(feature = "parsing")]
|
||||
mod parsing {
|
||||
pub(crate) mod path_command;
|
||||
pub(crate) mod path_data;
|
||||
}
|
||||
|
||||
mod util {
|
||||
pub(crate) mod aabb;
|
||||
pub(crate) mod epsilons;
|
||||
pub(crate) mod math;
|
||||
pub(crate) mod quad_tree;
|
||||
}
|
||||
mod path;
|
||||
#[cfg(test)]
|
||||
mod visual_tests;
|
||||
|
||||
#[cfg(feature = "parsing")]
|
||||
pub(crate) use parsing::*;
|
||||
pub(crate) use path::*;
|
||||
pub(crate) use util::*;
|
||||
|
||||
pub use intersection_path_segment::path_segment_intersection;
|
||||
#[cfg(feature = "parsing")]
|
||||
pub use parsing::path_data::{path_from_path_data, path_to_path_data};
|
||||
pub use path_boolean::{path_boolean, BooleanError, FillRule, PathBooleanOperation, EPS};
|
||||
pub use path_segment::PathSegment;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
path_boolean::{self, FillRule, PathBooleanOperation},
|
||||
path_data::{path_from_path_data, path_to_path_data},
|
||||
};
|
||||
use path_boolean::path_boolean;
|
||||
|
||||
#[test]
|
||||
fn square() {
|
||||
let a = path_from_path_data("M 10 10 L 50 10 L 30 40 Z");
|
||||
let b = path_from_path_data("M 20 30 L 60 30 L 60 50 L 20 50 Z");
|
||||
let union = path_boolean(
|
||||
&a,
|
||||
path_boolean::FillRule::NonZero,
|
||||
&b,
|
||||
path_boolean::FillRule::NonZero,
|
||||
path_boolean::PathBooleanOperation::Intersection,
|
||||
)
|
||||
.unwrap();
|
||||
dbg!(path_to_path_data(&union[0], 0.001));
|
||||
assert!(!union[0].is_empty());
|
||||
// panic!();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nesting_01() {
|
||||
let a = path_from_path_data("M 47,24 A 23,23 0 0 1 24,47 23,23 0 0 1 1,24 23,23 0 0 1 24,1 23,23 0 0 1 47,24 Z");
|
||||
let b = path_from_path_data(
|
||||
"M 37.909023,24 A 13.909023,13.909023 0 0 1 24,37.909023 13.909023,13.909023 0 0 1 10.090978,24 13.909023,13.909023 0 0 1 24,10.090978 13.909023,13.909023 0 0 1 37.909023,24 Z",
|
||||
);
|
||||
let union = path_boolean(&a, path_boolean::FillRule::NonZero, &b, path_boolean::FillRule::NonZero, path_boolean::PathBooleanOperation::Union).unwrap();
|
||||
dbg!(path_to_path_data(&union[0], 0.001));
|
||||
assert!(!union[0].is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn nesting_02() {
|
||||
let a = path_from_path_data("M 0.99999994,31.334457 C 122.61195,71.81859 -79.025816,-5.5803326 47,32.253367 V 46.999996 H 0.99999994 Z");
|
||||
let b = path_from_path_data("m 25.797222,29.08718 c 0,1.292706 -1.047946,2.340652 -2.340652,2.340652 -1.292707,0 -2.340652,-1.047946 -2.340652,-2.340652 0,-1.292707 1.047945,-2.340652 2.340652,-2.340652 1.292706,0 2.340652,1.047945 2.340652,2.340652 z M 7.5851073,28.332212 c 1e-7,1.292706 -1.0479456,2.340652 -2.3406521,2.340652 -1.2927063,-1e-6 -2.3406518,-1.047946 -2.3406517,-2.340652 -10e-8,-1.292707 1.0479454,-2.340652 2.3406517,-2.340652 1.2927065,-1e-6 2.3406522,1.047945 2.3406521,2.340652 z");
|
||||
|
||||
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();
|
||||
|
||||
// Add assertions here based on expected results
|
||||
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
|
||||
// Add more specific assertions about the resulting path if needed
|
||||
assert!(!result[0].is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn nesting_03() {
|
||||
let a = path_from_path_data("m 21.829117,3.5444345 h 4.341766 V 16.502158 H 21.829117 Z M 47,24 A 23,23 0 0 1 24,47 23,23 0 0 1 1,24 23,23 0 0 1 24,1 23,23 0 0 1 47,24 Z");
|
||||
let b = path_from_path_data("M 24 6.4960938 A 17.504802 17.504802 0 0 0 6.4960938 24 A 17.504802 17.504802 0 0 0 24 41.503906 A 17.504802 17.504802 0 0 0 41.503906 24 A 17.504802 17.504802 0 0 0 24 6.4960938 z M 24 12.193359 A 11.805881 11.805881 0 0 1 35.806641 24 A 11.805881 11.805881 0 0 1 24 35.806641 A 11.805881 11.805881 0 0 1 12.193359 24 A 11.805881 11.805881 0 0 1 24 12.193359 z ");
|
||||
|
||||
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();
|
||||
|
||||
// Add assertions here based on expected results
|
||||
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
|
||||
// Add more specific assertions about the resulting path if needed
|
||||
let path_string = dbg!(path_to_path_data(&result[0], 0.001));
|
||||
assert_eq!(path_string.chars().filter(|c| c == &'M').count(), 1, "More than one path returned");
|
||||
assert!(!result[0].is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn simple_07() {
|
||||
let a = path_from_path_data("M 37.671452,24 C 52.46888,31.142429 42.887716,37.358779 24,37.671452 16.4505,37.796429 10.328548,31.550534 10.328548,24 c 0,-7.550534 6.120918,-13.671452 13.671452,-13.671452 7.550534,0 6.871598,10.389295 13.671452,13.671452 z",
|
||||
);
|
||||
let b = path_from_path_data("M 37.671452,24 C 33.698699,53.634887 29.50935,49.018306 24,37.671452 20.7021,30.879219 10.328548,31.550534 10.328548,24 c 0,-7.550534 6.120918,-13.671452 13.671452,-13.671452 7.550534,0 14.674677,6.187863 13.671452,13.671452 z");
|
||||
|
||||
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();
|
||||
|
||||
// Add assertions here based on expected results
|
||||
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
|
||||
// Add more specific assertions about the resulting path if needed
|
||||
dbg!(path_to_path_data(&result[0], 0.001));
|
||||
assert!(!result[0].is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn rect_ellipse() {
|
||||
let a = path_from_path_data("M0,0C0,0 100,0 100,0 C100,0 100,100 100,100 C100,100 0,100 0,100 C0,100 0,0 0,0 Z");
|
||||
let b = path_from_path_data("M50,0C77.589239,0 100,22.410761 100,50 C100,77.589239 77.589239,100 50,100 C22.410761,100 0,77.589239 0,50 C0,22.410761 22.410761,0 50,0 Z");
|
||||
|
||||
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();
|
||||
|
||||
// Add assertions here based on expected results
|
||||
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
|
||||
dbg!(path_to_path_data(&result[0], 0.001));
|
||||
assert!(!result[0].is_empty());
|
||||
// Add more specific assertions about the resulting path if needed
|
||||
}
|
||||
#[test]
|
||||
fn red_dress_loop() {
|
||||
let a = path_from_path_data("M969.000000,0.000000C969.000000,0.000000 1110.066898,76.934393 1085.000000,181.000000 C1052.000000,318.000000 1199.180581,334.301571 1277.000000,319.000000 C1455.000000,284.000000 1586.999985,81.000000 1418.000000,0.000000 C1418.000000,0.000000 969.000000,0.000000 969.000000,0.000000");
|
||||
let b = path_from_path_data(
|
||||
"M1211.000000,0.000000C1211.000000,0.000000 1255.000000,78.000000 1536.000000,95.000000 C1536.000000,95.000000 1536.000000,0.000000 1536.000000,0.000000 C1536.000000,0.000000 1211.000000,0.000000 1211.000000,0.000000 Z",
|
||||
);
|
||||
|
||||
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Intersection).unwrap();
|
||||
|
||||
// Add assertions here based on expected results
|
||||
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
|
||||
dbg!(path_to_path_data(&result[0], 0.001));
|
||||
// Add more specific assertions about the resulting path if needed
|
||||
assert!(!result[0].is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn painted_dreams_1() {
|
||||
let a = path_from_path_data("M969.000000,0.000000C969.000000,0.000000 1110.066898,76.934393 1085.000000,181.000000 C1052.000000,318.000000 1199.180581,334.301571 1277.000000,319.000000 C1455.000000,284.000000 1586.999985,81.000000 1418.000000,0.000000 C1418.000000,0.000000 969.000000,0.000000 969.000000,0.000000 Z");
|
||||
let b = path_from_path_data(
|
||||
"M763.000000,0.000000C763.000000,0.000000 1536.000000,0.000000 1536.000000,0.000000 C1536.000000,0.000000 1536.000000,254.000000 1536.000000,254.000000 C1536.000000,254.000000 1462.000000,93.000000 1271.000000,199.000000 C1149.163056,266.616314 976.413656,188.510842 908.000000,134.000000 C839.586344,79.489158 763.000000,0.000000 763.000000,0.000000 Z",
|
||||
);
|
||||
|
||||
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Intersection).unwrap();
|
||||
|
||||
// Add assertions here based on expected results
|
||||
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
|
||||
dbg!(path_to_path_data(&result[0], 0.001));
|
||||
// Add more specific assertions about the resulting path if needed
|
||||
assert!(!result[0].is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn painted_dreams_2() {
|
||||
let a = path_from_path_data("M0,340C161.737914,383.575765 107.564182,490.730587 273,476 C419,463 481.741198,514.692273 481.333333,768 C481.333333,768 -0,768 -0,768 C-0,768 0,340 0,340 Z ");
|
||||
let b = path_from_path_data(
|
||||
"M458.370270,572.165771C428.525848,486.720093 368.618805,467.485992 273,476 C107.564178,490.730591 161.737915,383.575775 0,340 C0,340 0,689 0,689 C56,700 106.513901,779.342590 188,694.666687 C306.607422,571.416260 372.033966,552.205139 458.370270,572.165771 Z",
|
||||
);
|
||||
|
||||
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();
|
||||
|
||||
// Add assertions here based on expected results
|
||||
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
|
||||
dbg!(path_to_path_data(&result[0], 0.001));
|
||||
// Add more specific assertions about the resulting path if needed
|
||||
assert!(!result[0].is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn painted_dreams_3() {
|
||||
let a = path_from_path_data("M889,0C889,0 889,21 898,46 C909.595887,78.210796 872.365858,104.085306 869,147 C865,198 915,237 933,273 C951,309 951.703704,335.407407 923,349 C898.996281,360.366922 881,367 902,394 C923,421 928.592593,431.407407 898,468 C912.888889,472.888889 929.333333,513.333333 896,523 C896,523 876,533.333333 886,572 C896.458810,612.440732 873.333333,657.777778 802.666667,656.444444 C738.670245,655.236965 689,643 655,636 C621,629 604,623 585,666 C566,709 564,768 564,768 C564,768 0,768 0,768 C0,768 0,0 0,0 C0,0 889,0 889,0 Z ");
|
||||
let b = path_from_path_data(
|
||||
"M552,768C552,768 993,768 993,768 C993,768 1068.918039,682.462471 1093,600 C1126,487 1007.352460,357.386071 957,324 C906.647540,290.613929 842,253 740,298 C638,343 491.342038,421.999263 491.342038,506.753005 C491.342038,641.999411 552,768 552,768 Z ",
|
||||
);
|
||||
|
||||
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Difference).unwrap();
|
||||
|
||||
// Add assertions here based on expected results
|
||||
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
|
||||
dbg!(path_to_path_data(&result[0], 0.001));
|
||||
// Add more specific assertions about the resulting path if needed
|
||||
assert!(!result[0].is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn painted_dreams_4() {
|
||||
let a = path_from_path_data("M458.370270,572.165771C372.033966,552.205139 306.607422,571.416260 188.000000,694.666687 C106.513901,779.342590 56.000000,700.000000 0.000000,689.000000 C0.000000,689.000000 0.000000,768.000000 0.000000,768.000000 C0.000000,768.000000 481.333344,768.000000 481.333344,768.000000 C481.474091,680.589417 474.095154,617.186768 458.370270,572.165771 Z ");
|
||||
let b = path_from_path_data(
|
||||
"M364.000000,768.000000C272.000000,686.000000 294.333333,468.666667 173.333333,506.666667 C110.156241,526.507407 0.000000,608.000000 0.000000,608.000000 L -0.000000,768.000000 L 364.000000,768.000000 Z",
|
||||
);
|
||||
|
||||
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Difference).unwrap();
|
||||
|
||||
// Add assertions here based on expected results
|
||||
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
|
||||
dbg!(path_to_path_data(&result[0], 0.001));
|
||||
// Add more specific assertions about the resulting path if needed
|
||||
assert!(!result[0].is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn painted_dreams_5() {
|
||||
let a = path_from_path_data("M889.000000,0.000000C889.000000,0.000000 889.000000,21.000000 898.000000,46.000000 C909.595887,78.210796 872.365858,104.085306 869.000000,147.000000 C865.000000,198.000000 915.000000,237.000000 933.000000,273.000000 C951.000000,309.000000 951.703704,335.407407 923.000000,349.000000 C898.996281,360.366922 881.000000,367.000000 902.000000,394.000000 C923.000000,421.000000 928.592593,431.407407 898.000000,468.000000 C912.888889,472.888889 929.333333,513.333333 896.000000,523.000000 C896.000000,523.000000 876.000000,533.333333 886.000000,572.000000 C896.458810,612.440732 873.333333,657.777778 802.666667,656.444444 C738.670245,655.236965 689.000000,643.000000 655.000000,636.000000 C621.000000,629.000000 604.000000,623.000000 585.000000,666.000000 C566.000000,709.000000 564.000000,768.000000 564.000000,768.000000 C564.000000,768.000000 0.000000,768.000000 0.000000,768.000000 C0.000000,768.000000 0.000000,0.000000 0.000000,0.000000 C0.000000,0.000000 889.000000,0.000000 889.000000,0.000000 Z"
|
||||
);
|
||||
let b = path_from_path_data(
|
||||
"M891.555556,569.382716C891.555556,569.382716 883.555556,577.777778 879.111111,595.851852 C874.666667,613.925926 857.185185,631.407407 830.814815,633.777778 C804.444444,636.148148 765.629630,637.925926 708.148148,616.296296 C650.666667,594.666667 560.666667,568.000000 468.000000,487.333333 C375.333333,406.666667 283.333333,354.666667 283.333333,354.666667 C332.000000,330.666667 373.407788,298.323579 468.479950,219.785706 C495.739209,197.267187 505.084065,165.580817 514.452332,146.721008 C525.711584,124.054345 577.519713,94.951389 589.958848,64.658436 C601.152263,37.399177 601.175694,0.000010 601.175694,0.000000 C601.175694,0.000000 0.000000,0.000000 0.000000,0.000000 C0.000000,0.000000 0.000000,768.000000 0.000000,768.000000 C0.000000,768.000000 891.555556,768.000000 891.555556,768.000000 C891.555556,768.000000 891.555556,569.382716 891.555556,569.382716 Z",
|
||||
);
|
||||
|
||||
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Intersection).unwrap();
|
||||
|
||||
// Add assertions here based on expected results
|
||||
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
|
||||
dbg!(path_to_path_data(&result[0], 0.001));
|
||||
// Add more specific assertions about the resulting path if needed
|
||||
assert!(!result[0].is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn painted_dreams_6() {
|
||||
let a = path_from_path_data(
|
||||
"M 969.000000000000,0.000000000000 C 969.000000000000,0.000000000000 1110.066900000000,76.934400000000 1085.000000000000,181.000000000000 C 1052.000000000000,318.000000000000 1199.180600000000,334.301600000000 1277.000000000000,319.000000000000 C 1455.000000000000,284.000000000000 1587.000000000000,81.000000000000 1418.000000000000,0.000000000000 C 1418.000000000000,0.000000000000 969.000000000000,0.000000000000 969.000000000000,0.000000000000 L 969.000000000000,0.000000000000"
|
||||
);
|
||||
let b = path_from_path_data(
|
||||
"M 763.000000000000,0.000000000000 C 763.000000000000,0.000000000000 1536.000000000000,0.000000000000 1536.000000000000,0.000000000000 C 1536.000000000000,0.000000000000 1536.000000000000,254.000000000000 1536.000000000000,254.000000000000 C 1536.000000000000,254.000000000000 1462.000000000000,93.000000000000 1271.000000000000,199.000000000000 C 1149.163100000000,266.616300000000 976.413700000000,188.510800000000 908.000000000000,134.000000000000 C 839.586300000000,79.489200000000 763.000000000000,0.000000000000 763.000000000000,0.000000000000 L 763.000000000000,0.000000000000",
|
||||
);
|
||||
|
||||
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Intersection).unwrap();
|
||||
|
||||
// Add assertions here based on expected results
|
||||
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
|
||||
dbg!(path_to_path_data(&result[0], 0.001));
|
||||
// Add more specific assertions about the resulting path if needed
|
||||
assert!(!result[0].is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn painted_dreams_7() {
|
||||
let a = path_from_path_data(
|
||||
"M 989.666700000000,768.000000000000 C 989.666700000000,768.000000000000 1011.111100000000,786.399400000000 1011.111100000000,786.399400000000 C 1011.111100000000,786.399400000000 1299.306500000000,786.399400000000 1299.306500000000,786.399400000000 C 1299.306500000000,786.399400000000 1318.000000000000,768.000000000000 1318.000000000000,768.000000000000 C 1293.666700000000,681.000000000000 1173.363200000000,625.103600000000 1094.162400000000,594.296600000000 C 1094.162400000000,594.296600000000 1058.747200000000,687.805800000000 989.666700000000,768.000000000000"
|
||||
);
|
||||
let b = path_from_path_data(
|
||||
"M 983.155000000000,775.589300000000 L 1004.599400000000,793.988700000000 L 1007.409000000000,796.399400000000 L 1011.111100000000,796.399400000000 L 1299.306500000000,796.399400000000 L 1303.402200000000,796.399400000000 L 1306.321200000000,793.526300000000 L 1325.014800000000,775.126900000000 L 1329.236900000000,770.971200000000 L 1327.630400000000,765.306400000000 C 1302.280700000000,675.920800000000 1179.503900000000,617.211200000000 1097.787500000000,584.976800000000 L 1088.418100000000,581.280900000000 L 1084.806400000000,590.765700000000 C 1084.117400000000,592.575300000000 1049.449700000000,683.516200000000 982.090100000000,761.473400000000 L 975.539200000000,769.055000000000 L 983.155000000000,775.589300000000 M 1003.696800000000,766.861600000000 C 1068.901100000000,687.878900000000 1102.806400000000,599.696700000000 1103.497000000000,597.883400000000 L 1090.537200000000,603.616300000000 C 1165.521500000000,632.344400000000 1279.846400000000,683.736400000000 1306.585700000000,765.203400000000 L 1295.210700000000,776.399400000000 L 1014.813100000000,776.399400000000 L 1003.696800000000,766.861600000000",
|
||||
);
|
||||
|
||||
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Difference).unwrap();
|
||||
|
||||
// Add assertions here based on expected results
|
||||
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
|
||||
dbg!(path_to_path_data(&result[0], 0.001));
|
||||
// Add more specific assertions about the resulting path if needed
|
||||
assert!(!result[0].is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn blobs() {
|
||||
let a = path_from_path_data(
|
||||
"m658.03348 118.4966c7.85928 4.83645 114.84582 7.8304 127.89652 6.52531 20.97932-2.09799 43.06722-24.79623 43.06722-24.79623 0 0-96.43723-26.02101-108.97311-28.54836-20.22849-4.07832-78.95651 36.37872-61.99063 46.81928z
|
||||
m658.03348 115.88649c40.45718-30.01653 82.213-45.24662 103.10032-31.32163 7.83037 5.2203-3.58567 22.51547 13.05064 39.152 3.91519 3.9152-129.49099 2.06705-116.15096-7.83037z
|
||||
m680.87214 56.0165c2.20775-9.60391 62.6449-29.65403 101.79518-30.01652 17.61846-0.16312 119.39605 40.30737 130.50668 54.8128 5.8045 7.57806-76.88558 29.08762-91.35464 31.32162-15.28899 2.36056-144.20983-41.92525-140.94722-56.1179z"
|
||||
);
|
||||
let b = path_from_path_data("");
|
||||
|
||||
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();
|
||||
|
||||
// Add assertions here based on expected results
|
||||
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
|
||||
dbg!(path_to_path_data(&result[0], 0.001));
|
||||
// Add more specific assertions about the resulting path if needed
|
||||
assert!(!result[0].is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use glam::DVec2;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AbsolutePathCommand {
|
||||
H(f64),
|
||||
V(f64),
|
||||
M(DVec2),
|
||||
L(DVec2),
|
||||
C(DVec2, DVec2, DVec2),
|
||||
S(DVec2, DVec2),
|
||||
Q(DVec2, DVec2),
|
||||
T(DVec2),
|
||||
A(f64, f64, f64, bool, bool, DVec2),
|
||||
Z,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RelativePathCommand {
|
||||
H(f64),
|
||||
V(f64),
|
||||
M(f64, f64),
|
||||
L(f64, f64),
|
||||
C(f64, f64, f64, f64, f64, f64),
|
||||
S(f64, f64, f64, f64),
|
||||
Q(f64, f64, f64, f64),
|
||||
T(f64, f64),
|
||||
A(f64, f64, f64, bool, bool, f64, f64),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PathCommand {
|
||||
Absolute(AbsolutePathCommand),
|
||||
Relative(RelativePathCommand),
|
||||
}
|
||||
|
||||
pub fn to_absolute_commands<I>(commands: I) -> impl Iterator<Item = AbsolutePathCommand>
|
||||
where
|
||||
I: IntoIterator<Item = PathCommand>,
|
||||
{
|
||||
let mut last_point = DVec2::ZERO;
|
||||
let mut first_point = last_point;
|
||||
|
||||
commands.into_iter().flat_map(move |cmd| match cmd {
|
||||
PathCommand::Absolute(abs_cmd) => {
|
||||
match abs_cmd {
|
||||
AbsolutePathCommand::H(x) => {
|
||||
last_point.x = x;
|
||||
}
|
||||
AbsolutePathCommand::V(y) => {
|
||||
last_point.y = y;
|
||||
}
|
||||
AbsolutePathCommand::M(point) => {
|
||||
last_point = point;
|
||||
first_point = point;
|
||||
}
|
||||
AbsolutePathCommand::L(point) => {
|
||||
last_point = point;
|
||||
}
|
||||
AbsolutePathCommand::C(_, _, end) => {
|
||||
last_point = end;
|
||||
}
|
||||
AbsolutePathCommand::S(_, end) => {
|
||||
last_point = end;
|
||||
}
|
||||
AbsolutePathCommand::Q(_, end) => {
|
||||
last_point = end;
|
||||
}
|
||||
AbsolutePathCommand::T(end) => {
|
||||
last_point = end;
|
||||
}
|
||||
AbsolutePathCommand::A(_, _, _, _, _, end) => {
|
||||
last_point = end;
|
||||
}
|
||||
AbsolutePathCommand::Z => {
|
||||
last_point = first_point;
|
||||
}
|
||||
}
|
||||
vec![abs_cmd]
|
||||
}
|
||||
PathCommand::Relative(rel_cmd) => match rel_cmd {
|
||||
RelativePathCommand::H(dx) => {
|
||||
last_point.x += dx;
|
||||
vec![AbsolutePathCommand::L(last_point)]
|
||||
}
|
||||
RelativePathCommand::V(dy) => {
|
||||
last_point.y += dy;
|
||||
vec![AbsolutePathCommand::L(last_point)]
|
||||
}
|
||||
RelativePathCommand::M(dx, dy) => {
|
||||
last_point += DVec2::new(dx, dy);
|
||||
first_point = last_point;
|
||||
vec![AbsolutePathCommand::M(last_point)]
|
||||
}
|
||||
RelativePathCommand::L(dx, dy) => {
|
||||
last_point += DVec2::new(dx, dy);
|
||||
vec![AbsolutePathCommand::L(last_point)]
|
||||
}
|
||||
RelativePathCommand::C(dx1, dy1, dx2, dy2, dx, dy) => {
|
||||
let c1 = last_point + DVec2::new(dx1, dy1);
|
||||
let c2 = last_point + DVec2::new(dx2, dy2);
|
||||
last_point += DVec2::new(dx, dy);
|
||||
vec![AbsolutePathCommand::C(c1, c2, last_point)]
|
||||
}
|
||||
RelativePathCommand::S(dx2, dy2, dx, dy) => {
|
||||
let c2 = last_point + DVec2::new(dx2, dy2);
|
||||
last_point += DVec2::new(dx, dy);
|
||||
vec![AbsolutePathCommand::S(c2, last_point)]
|
||||
}
|
||||
RelativePathCommand::Q(dx1, dy1, dx, dy) => {
|
||||
let control = last_point + DVec2::new(dx1, dy1);
|
||||
last_point += DVec2::new(dx, dy);
|
||||
vec![AbsolutePathCommand::Q(control, last_point)]
|
||||
}
|
||||
RelativePathCommand::T(dx, dy) => {
|
||||
last_point += DVec2::new(dx, dy);
|
||||
vec![AbsolutePathCommand::T(last_point)]
|
||||
}
|
||||
RelativePathCommand::A(rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, dx, dy) => {
|
||||
last_point += DVec2::new(dx, dy);
|
||||
vec![AbsolutePathCommand::A(rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, last_point)]
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
use crate::path::{path_from_commands, path_to_commands, Path};
|
||||
use crate::path_command::{AbsolutePathCommand, PathCommand, RelativePathCommand};
|
||||
use glam::DVec2;
|
||||
use regex::Regex;
|
||||
|
||||
pub fn commands_from_path_data(d: &str) -> Vec<PathCommand> {
|
||||
let re_float = Regex::new(r"^\s*,?\s*(-?\d*(?:\d\.|\.\d|\d)\d*(?:[eE][+\-]?\d+)?)").unwrap();
|
||||
let re_cmd = Regex::new(r"^\s*([MLCSQTAZHVmlhvcsqtaz])").unwrap();
|
||||
let re_bool = Regex::new(r"^\s*,?\s*([01])").unwrap();
|
||||
|
||||
let mut i = 0;
|
||||
let mut last_cmd = 'M';
|
||||
let mut commands = Vec::new();
|
||||
|
||||
let get_cmd = |i: &mut usize, last_cmd: char| -> Option<char> {
|
||||
if *i >= d.len() - 1.min(d.len()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(cap) = re_cmd.captures(&d[*i..]) {
|
||||
*i += cap[0].len();
|
||||
Some(cap[1].chars().next().unwrap())
|
||||
} else {
|
||||
match last_cmd {
|
||||
'M' => Some('L'),
|
||||
'm' => Some('l'),
|
||||
_ => Some(last_cmd),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let get_float = |i: &mut usize| -> f64 {
|
||||
if let Some(cap) = re_float.captures(&d[*i..]) {
|
||||
*i += cap[0].len();
|
||||
cap[1].parse().unwrap()
|
||||
} else {
|
||||
panic!("Invalid path data. Expected a number at index {}, got {}", i, &d[*i..]);
|
||||
}
|
||||
};
|
||||
|
||||
let get_bool = |i: &mut usize| -> bool {
|
||||
if let Some(cap) = re_bool.captures(&d[*i..]) {
|
||||
*i += cap[0].len();
|
||||
&cap[1] == "1"
|
||||
} else {
|
||||
panic!("Invalid path data. Expected a flag at index {}", i);
|
||||
}
|
||||
};
|
||||
|
||||
while let Some(cmd) = get_cmd(&mut i, last_cmd) {
|
||||
last_cmd = cmd;
|
||||
match cmd {
|
||||
'M' => commands.push(PathCommand::Absolute(AbsolutePathCommand::M(DVec2::new(get_float(&mut i), get_float(&mut i))))),
|
||||
'L' => commands.push(PathCommand::Absolute(AbsolutePathCommand::L(DVec2::new(get_float(&mut i), get_float(&mut i))))),
|
||||
'C' => commands.push(PathCommand::Absolute(AbsolutePathCommand::C(
|
||||
DVec2::new(get_float(&mut i), get_float(&mut i)),
|
||||
DVec2::new(get_float(&mut i), get_float(&mut i)),
|
||||
DVec2::new(get_float(&mut i), get_float(&mut i)),
|
||||
))),
|
||||
'S' => commands.push(PathCommand::Absolute(AbsolutePathCommand::S(
|
||||
DVec2::new(get_float(&mut i), get_float(&mut i)),
|
||||
DVec2::new(get_float(&mut i), get_float(&mut i)),
|
||||
))),
|
||||
'Q' => commands.push(PathCommand::Absolute(AbsolutePathCommand::Q(
|
||||
DVec2::new(get_float(&mut i), get_float(&mut i)),
|
||||
DVec2::new(get_float(&mut i), get_float(&mut i)),
|
||||
))),
|
||||
'T' => commands.push(PathCommand::Absolute(AbsolutePathCommand::T(DVec2::new(get_float(&mut i), get_float(&mut i))))),
|
||||
'A' => commands.push(PathCommand::Absolute(AbsolutePathCommand::A(
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
get_bool(&mut i),
|
||||
get_bool(&mut i),
|
||||
DVec2::new(get_float(&mut i), get_float(&mut i)),
|
||||
))),
|
||||
'Z' | 'z' => commands.push(PathCommand::Absolute(AbsolutePathCommand::Z)),
|
||||
'H' => commands.push(PathCommand::Absolute(AbsolutePathCommand::H(get_float(&mut i)))),
|
||||
'V' => commands.push(PathCommand::Absolute(AbsolutePathCommand::V(get_float(&mut i)))),
|
||||
'm' => commands.push(PathCommand::Relative(RelativePathCommand::M(get_float(&mut i), get_float(&mut i)))),
|
||||
'l' => commands.push(PathCommand::Relative(RelativePathCommand::L(get_float(&mut i), get_float(&mut i)))),
|
||||
'h' => commands.push(PathCommand::Relative(RelativePathCommand::H(get_float(&mut i)))),
|
||||
'v' => commands.push(PathCommand::Relative(RelativePathCommand::V(get_float(&mut i)))),
|
||||
'c' => commands.push(PathCommand::Relative(RelativePathCommand::C(
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
))),
|
||||
's' => commands.push(PathCommand::Relative(RelativePathCommand::S(
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
))),
|
||||
'q' => commands.push(PathCommand::Relative(RelativePathCommand::Q(
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
))),
|
||||
't' => commands.push(PathCommand::Relative(RelativePathCommand::T(get_float(&mut i), get_float(&mut i)))),
|
||||
'a' => commands.push(PathCommand::Relative(RelativePathCommand::A(
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
get_bool(&mut i),
|
||||
get_bool(&mut i),
|
||||
get_float(&mut i),
|
||||
get_float(&mut i),
|
||||
))),
|
||||
_ => panic!("Invalid command: {}", cmd),
|
||||
}
|
||||
}
|
||||
|
||||
commands
|
||||
}
|
||||
|
||||
pub fn path_from_path_data(d: &str) -> Path {
|
||||
path_from_commands(commands_from_path_data(d)).collect()
|
||||
}
|
||||
|
||||
pub fn path_to_path_data(path: &Path, eps: f64) -> String {
|
||||
path_to_commands(path.iter(), eps)
|
||||
.map(|cmd| match cmd {
|
||||
PathCommand::Absolute(abs_cmd) => match abs_cmd {
|
||||
AbsolutePathCommand::H(dx) => format!("H {:.12}", dx),
|
||||
AbsolutePathCommand::V(dy) => format!("V {:.12}", dy),
|
||||
AbsolutePathCommand::M(p) => format!("M {:.12},{:.12}", p.x, p.y),
|
||||
AbsolutePathCommand::L(p) => format!("L {:.12},{:.12}", p.x, p.y),
|
||||
AbsolutePathCommand::C(p1, p2, p3) => format!("C {:.12},{:.12} {:.12},{:.12} {:.12},{:.12}", p1.x, p1.y, p2.x, p2.y, p3.x, p3.y),
|
||||
AbsolutePathCommand::S(p1, p2) => {
|
||||
format!("S {:.12},{:.12} {:.12},{:.12}", p1.x, p1.y, p2.x, p2.y)
|
||||
}
|
||||
AbsolutePathCommand::Q(p1, p2) => {
|
||||
format!("Q {:.12},{:.12} {:.12},{:.12}", p1.x, p1.y, p2.x, p2.y)
|
||||
}
|
||||
AbsolutePathCommand::T(p) => format!("T {:.12},{:.12}", p.x, p.y),
|
||||
AbsolutePathCommand::A(rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, p) => {
|
||||
format!("A {:.12} {:.12} {:.12} {} {} {:.12},{:.12}", rx, ry, x_axis_rotation, large_arc_flag as u8, sweep_flag as u8, p.x, p.y)
|
||||
}
|
||||
AbsolutePathCommand::Z => "Z".to_string(),
|
||||
},
|
||||
PathCommand::Relative(rel_cmd) => match rel_cmd {
|
||||
RelativePathCommand::M(dx, dy) => format!("m {:.12},{:.12}", dx, dy),
|
||||
RelativePathCommand::L(dx, dy) => format!("l {:.12},{:.12}", dx, dy),
|
||||
RelativePathCommand::H(dx) => format!("h {:.12}", dx),
|
||||
RelativePathCommand::V(dy) => format!("v {:.12}", dy),
|
||||
RelativePathCommand::C(dx1, dy1, dx2, dy2, dx, dy) => format!("c{:.12},{:.12} {:.12},{:.12} {:.12},{:.12}", dx1, dy1, dx2, dy2, dx, dy),
|
||||
RelativePathCommand::S(dx2, dy2, dx, dy) => {
|
||||
format!("s {:.12},{:.12} {:.12},{:.12}", dx2, dy2, dx, dy)
|
||||
}
|
||||
RelativePathCommand::Q(dx1, dy1, dx, dy) => {
|
||||
format!("q {:.12},{:.12} {:.12},{:.12}", dx1, dy1, dx, dy)
|
||||
}
|
||||
RelativePathCommand::T(dx, dy) => format!("t{:.12},{:.12}", dx, dy),
|
||||
RelativePathCommand::A(rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, dx, dy) => {
|
||||
format!("a {:.12} {:.12} {:.12} {} {} {:.12},{:.12}", rx, ry, x_axis_rotation, large_arc_flag as u8, sweep_flag as u8, dx, dy)
|
||||
}
|
||||
},
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
pub(crate) mod intersection_path_segment;
|
||||
pub(crate) mod line_segment;
|
||||
pub(crate) mod line_segment_aabb;
|
||||
pub(crate) mod path_cubic_segment_self_intersection;
|
||||
pub(crate) mod path_segment;
|
||||
|
||||
use glam::DVec2;
|
||||
|
||||
#[cfg(feature = "parsing")]
|
||||
use crate::path_command::{to_absolute_commands, AbsolutePathCommand, PathCommand};
|
||||
use crate::path_segment::PathSegment;
|
||||
|
||||
pub type Path = Vec<PathSegment>;
|
||||
|
||||
fn reflect_control_point(point: DVec2, control_point: DVec2) -> DVec2 {
|
||||
point * 2.0 - control_point
|
||||
}
|
||||
|
||||
pub fn path_from_commands<I>(commands: I) -> impl Iterator<Item = PathSegment>
|
||||
where
|
||||
I: IntoIterator<Item = PathCommand>,
|
||||
{
|
||||
let mut first_point: Option<DVec2> = None;
|
||||
let mut last_point: Option<DVec2> = None;
|
||||
let mut last_control_point: Option<DVec2> = None;
|
||||
|
||||
to_absolute_commands(commands).filter_map(move |cmd| match cmd {
|
||||
AbsolutePathCommand::M(point) => {
|
||||
last_point = Some(point);
|
||||
first_point = Some(point);
|
||||
last_control_point = None;
|
||||
None
|
||||
}
|
||||
AbsolutePathCommand::L(point) => {
|
||||
let start = last_point.unwrap();
|
||||
last_point = Some(point);
|
||||
last_control_point = None;
|
||||
Some(PathSegment::Line(start, point))
|
||||
}
|
||||
AbsolutePathCommand::H(x) => {
|
||||
let start = last_point.unwrap();
|
||||
let point = DVec2::new(x, start.y);
|
||||
last_point = Some(point);
|
||||
last_control_point = None;
|
||||
Some(PathSegment::Line(start, point))
|
||||
}
|
||||
AbsolutePathCommand::V(y) => {
|
||||
let start = last_point.unwrap();
|
||||
let point = DVec2::new(start.x, y);
|
||||
last_point = Some(point);
|
||||
last_control_point = None;
|
||||
Some(PathSegment::Line(start, point))
|
||||
}
|
||||
AbsolutePathCommand::C(c1, c2, end) => {
|
||||
let start = last_point.unwrap();
|
||||
last_point = Some(end);
|
||||
last_control_point = Some(c2);
|
||||
Some(PathSegment::Cubic(start, c1, c2, end))
|
||||
}
|
||||
AbsolutePathCommand::S(c2, end) => {
|
||||
let start = last_point.unwrap();
|
||||
let c1 = reflect_control_point(start, last_control_point.unwrap_or(start));
|
||||
last_point = Some(end);
|
||||
last_control_point = Some(c2);
|
||||
Some(PathSegment::Cubic(start, c1, c2, end))
|
||||
}
|
||||
AbsolutePathCommand::Q(c, end) => {
|
||||
let start = last_point.unwrap();
|
||||
last_point = Some(end);
|
||||
last_control_point = Some(c);
|
||||
Some(PathSegment::Quadratic(start, c, end))
|
||||
}
|
||||
AbsolutePathCommand::T(end) => {
|
||||
let start = last_point.unwrap();
|
||||
let c = reflect_control_point(start, last_control_point.unwrap_or(start));
|
||||
last_point = Some(end);
|
||||
last_control_point = Some(c);
|
||||
Some(PathSegment::Quadratic(start, c, end))
|
||||
}
|
||||
AbsolutePathCommand::A(rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, end) => {
|
||||
let start = last_point.unwrap();
|
||||
last_point = Some(end);
|
||||
last_control_point = None;
|
||||
Some(PathSegment::Arc(start, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, end))
|
||||
}
|
||||
AbsolutePathCommand::Z => {
|
||||
let start = last_point.unwrap();
|
||||
let end = first_point.unwrap();
|
||||
last_point = Some(end);
|
||||
last_control_point = None;
|
||||
Some(PathSegment::Line(start, end))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn path_to_commands<'a, I>(segments: I, eps: f64) -> impl Iterator<Item = PathCommand> + 'a
|
||||
where
|
||||
I: IntoIterator<Item = &'a PathSegment> + 'a,
|
||||
{
|
||||
let mut last_point: Option<DVec2> = None;
|
||||
|
||||
segments
|
||||
.into_iter()
|
||||
.flat_map(move |seg| {
|
||||
let start = seg.start();
|
||||
let mut commands = Vec::new();
|
||||
|
||||
if last_point.map_or(true, |lp| !start.abs_diff_eq(lp, eps)) {
|
||||
if last_point.is_some() {
|
||||
commands.push(PathCommand::Absolute(AbsolutePathCommand::Z));
|
||||
}
|
||||
|
||||
commands.push(PathCommand::Absolute(AbsolutePathCommand::M(start)));
|
||||
}
|
||||
|
||||
match seg {
|
||||
PathSegment::Line(_, end) => {
|
||||
commands.push(PathCommand::Absolute(AbsolutePathCommand::L(*end)));
|
||||
last_point = Some(*end);
|
||||
}
|
||||
PathSegment::Cubic(_, c1, c2, end) => {
|
||||
commands.push(PathCommand::Absolute(AbsolutePathCommand::C(*c1, *c2, *end)));
|
||||
last_point = Some(*end);
|
||||
}
|
||||
PathSegment::Quadratic(_, c, end) => {
|
||||
commands.push(PathCommand::Absolute(AbsolutePathCommand::Q(*c, *end)));
|
||||
last_point = Some(*end);
|
||||
}
|
||||
PathSegment::Arc(_, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, end) => {
|
||||
commands.push(PathCommand::Absolute(AbsolutePathCommand::A(*rx, *ry, *x_axis_rotation, *large_arc_flag, *sweep_flag, *end)));
|
||||
last_point = Some(*end);
|
||||
}
|
||||
}
|
||||
|
||||
commands
|
||||
})
|
||||
.chain(std::iter::once(PathCommand::Absolute(AbsolutePathCommand::Z)))
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
use glam::DVec2;
|
||||
|
||||
use crate::aabb::{bounding_box_max_extent, bounding_boxes_overlap, Aabb};
|
||||
use crate::epsilons::Epsilons;
|
||||
use crate::line_segment::{line_segment_intersection, line_segments_intersect};
|
||||
use crate::line_segment_aabb::line_segment_aabb_intersect;
|
||||
use crate::math::lerp;
|
||||
use crate::path_segment::PathSegment;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct IntersectionSegment {
|
||||
seg: PathSegment,
|
||||
start_param: f64,
|
||||
end_param: f64,
|
||||
bounding_box: Aabb,
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn subdivide_intersection_segment(int_seg: &IntersectionSegment) -> [IntersectionSegment; 2] {
|
||||
let (seg0, seg1) = int_seg.seg.split_at(0.5);
|
||||
let mid_param = (int_seg.start_param + int_seg.end_param) / 2.0;
|
||||
[
|
||||
IntersectionSegment {
|
||||
seg: seg0,
|
||||
start_param: int_seg.start_param,
|
||||
end_param: mid_param,
|
||||
bounding_box: seg0.bounding_box(),
|
||||
},
|
||||
IntersectionSegment {
|
||||
seg: seg1,
|
||||
start_param: mid_param,
|
||||
end_param: int_seg.end_param,
|
||||
bounding_box: seg1.bounding_box(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn path_segment_to_line_segment(seg: &PathSegment) -> [DVec2; 2] {
|
||||
match seg {
|
||||
PathSegment::Line(start, end) => [*start, *end],
|
||||
PathSegment::Cubic(start, _, _, end) => [*start, *end],
|
||||
PathSegment::Quadratic(start, _, end) => [*start, *end],
|
||||
PathSegment::Arc(start, _, _, _, _, _, end) => [*start, *end],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn intersection_segments_overlap(seg0: &IntersectionSegment, seg1: &IntersectionSegment) -> bool {
|
||||
match (&seg0.seg, &seg1.seg) {
|
||||
(PathSegment::Line(start0, end0), PathSegment::Line(start1, end1)) => {
|
||||
line_segments_intersect([*start0, *end0], [*start1, *end1], 1e-6) // TODO: configurable
|
||||
}
|
||||
(PathSegment::Line(start, end), _) => line_segment_aabb_intersect([*start, *end], &seg1.bounding_box),
|
||||
(_, PathSegment::Line(start, end)) => line_segment_aabb_intersect([*start, *end], &seg0.bounding_box),
|
||||
_ => bounding_boxes_overlap(&seg0.bounding_box, &seg1.bounding_box),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
pub fn segments_equal(seg0: &PathSegment, seg1: &PathSegment, point_epsilon: f64) -> bool {
|
||||
match (*seg0, *seg1) {
|
||||
(PathSegment::Line(start0, end0), PathSegment::Line(start1, end1)) => start0.abs_diff_eq(start1, point_epsilon) && end0.abs_diff_eq(end1, point_epsilon),
|
||||
(PathSegment::Cubic(p00, p01, p02, p03), PathSegment::Cubic(p10, p11, p12, p13)) => {
|
||||
let start_and_end_equal = p00.abs_diff_eq(p10, point_epsilon) && p03.abs_diff_eq(p13, point_epsilon);
|
||||
|
||||
let parameter_equal = p01.abs_diff_eq(p11, point_epsilon) && p02.abs_diff_eq(p12, point_epsilon);
|
||||
let direction1 = seg0.sample_at(0.1);
|
||||
let direction2 = seg1.sample_at(0.1);
|
||||
let angles_equal = (direction1 - p00).angle_to(direction2 - p00).abs() < point_epsilon * 4.;
|
||||
|
||||
start_and_end_equal && (parameter_equal || angles_equal)
|
||||
}
|
||||
(PathSegment::Quadratic(p00, p01, p02), PathSegment::Quadratic(p10, p11, p12)) => {
|
||||
p00.abs_diff_eq(p10, point_epsilon) && p01.abs_diff_eq(p11, point_epsilon) && p02.abs_diff_eq(p12, point_epsilon)
|
||||
}
|
||||
(PathSegment::Arc(p00, rx0, ry0, angle0, large_arc0, sweep0, p01), PathSegment::Arc(p10, rx1, ry1, angle1, large_arc1, sweep1, p11)) => {
|
||||
p00.abs_diff_eq(p10, point_epsilon) &&
|
||||
(rx0 - rx1).abs() < point_epsilon &&
|
||||
(ry0 - ry1).abs() < point_epsilon &&
|
||||
(angle0 - angle1).abs() < point_epsilon && // TODO: Phi can be anything if rx = ry. Also, handle rotations by Pi/2.
|
||||
large_arc0 == large_arc1 &&
|
||||
sweep0 == sweep1 &&
|
||||
p01.abs_diff_eq(p11, point_epsilon)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoints: bool, eps: &Epsilons) -> Vec<[f64; 2]> {
|
||||
// dbg!(&seg0, &seg1, endpoints);
|
||||
if let (PathSegment::Line(start0, end0), PathSegment::Line(start1, end1)) = (seg0, seg1) {
|
||||
if let Some(st) = line_segment_intersection([*start0, *end0], [*start1, *end1], eps.param) {
|
||||
if !endpoints && (st.0 < eps.param || st.0 > 1.0 - eps.param) && (st.1 < eps.param || st.1 > 1.0 - eps.param) {
|
||||
return vec![];
|
||||
}
|
||||
return vec![st.into()];
|
||||
}
|
||||
}
|
||||
|
||||
// https://math.stackexchange.com/questions/20321/how-can-i-tell-when-two-cubic-b%C3%A9zier-curves-intersect
|
||||
|
||||
let mut pairs = vec![(
|
||||
IntersectionSegment {
|
||||
seg: *seg0,
|
||||
start_param: 0.0,
|
||||
end_param: 1.0,
|
||||
bounding_box: seg0.bounding_box(),
|
||||
},
|
||||
IntersectionSegment {
|
||||
seg: *seg1,
|
||||
start_param: 0.0,
|
||||
end_param: 1.0,
|
||||
bounding_box: seg1.bounding_box(),
|
||||
},
|
||||
)];
|
||||
let mut next_pairs = Vec::new();
|
||||
|
||||
let mut params = Vec::new();
|
||||
let mut subdivided0 = Vec::new();
|
||||
let mut subdivided1 = Vec::new();
|
||||
|
||||
// check if start and end points are on the other bezier curves. If so, add as intersection.
|
||||
|
||||
while !pairs.is_empty() {
|
||||
next_pairs.clear();
|
||||
|
||||
if pairs.len() > 1000 {
|
||||
// TODO: check for intersections of the start/end points. If the two lines overlap, return split points for the start/end points. Use a binary search to check where the points are on the line.
|
||||
return calculate_overlap_intersections(seg0, seg1, eps);
|
||||
}
|
||||
|
||||
for (seg0, seg1) in pairs.iter() {
|
||||
if segments_equal(&seg0.seg, &seg1.seg, eps.point) {
|
||||
// TODO: move this outside of this loop?
|
||||
continue; // TODO: what to do?
|
||||
}
|
||||
|
||||
let is_linear0 = bounding_box_max_extent(&seg0.bounding_box) <= eps.linear || (seg0.end_param - seg0.start_param).abs() < eps.param;
|
||||
let is_linear1 = bounding_box_max_extent(&seg1.bounding_box) <= eps.linear || (seg1.end_param - seg1.start_param).abs() < eps.param;
|
||||
|
||||
if is_linear0 && is_linear1 {
|
||||
let line_segment0 = path_segment_to_line_segment(&seg0.seg);
|
||||
let line_segment1 = path_segment_to_line_segment(&seg1.seg);
|
||||
if let Some(st) = line_segment_intersection(line_segment0, line_segment1, eps.param) {
|
||||
// dbg!("pushing param");
|
||||
params.push([lerp(seg0.start_param, seg0.end_param, st.0), lerp(seg1.start_param, seg1.end_param, st.1)]);
|
||||
}
|
||||
} else {
|
||||
subdivided0.clear();
|
||||
subdivided1.clear();
|
||||
if is_linear0 {
|
||||
subdivided0.push(seg0.clone())
|
||||
} else {
|
||||
subdivided0.extend_from_slice(&subdivide_intersection_segment(seg0))
|
||||
};
|
||||
if is_linear1 {
|
||||
subdivided1.push(seg1.clone())
|
||||
} else {
|
||||
subdivided1.extend_from_slice(&subdivide_intersection_segment(seg1))
|
||||
};
|
||||
|
||||
for seg0 in &subdivided0 {
|
||||
for seg1 in &subdivided1 {
|
||||
if intersection_segments_overlap(seg0, seg1) {
|
||||
next_pairs.push((seg0.clone(), seg1.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::mem::swap(&mut pairs, &mut next_pairs);
|
||||
}
|
||||
|
||||
if !endpoints {
|
||||
params.retain(|[s, t]| (s > &eps.param && s < &(1.0 - eps.param)) || (t > &eps.param && t < &(1.0 - eps.param)));
|
||||
}
|
||||
|
||||
params
|
||||
}
|
||||
|
||||
fn calculate_overlap_intersections(seg0: &PathSegment, seg1: &PathSegment, eps: &Epsilons) -> Vec<[f64; 2]> {
|
||||
let start0 = seg0.start();
|
||||
let end0 = seg0.end();
|
||||
let start1 = seg1.start();
|
||||
let end1 = seg1.end();
|
||||
|
||||
let mut intersections = Vec::new();
|
||||
|
||||
// Check start0 against seg1
|
||||
if let Some(t1) = find_point_on_segment(seg1, start0, eps) {
|
||||
intersections.push([0.0, t1]);
|
||||
}
|
||||
|
||||
// Check end0 against seg1
|
||||
if let Some(t1) = find_point_on_segment(seg1, end0, eps) {
|
||||
intersections.push([1.0, t1]);
|
||||
}
|
||||
|
||||
// Check start1 against seg0
|
||||
if let Some(t0) = find_point_on_segment(seg0, start1, eps) {
|
||||
intersections.push([t0, 0.0]);
|
||||
}
|
||||
|
||||
// Check end1 against seg0
|
||||
if let Some(t0) = find_point_on_segment(seg0, end1, eps) {
|
||||
intersections.push([t0, 1.0]);
|
||||
}
|
||||
|
||||
// Remove duplicates and sort intersections
|
||||
intersections.sort_unstable_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
|
||||
intersections.dedup_by(|a, b| DVec2::from(*a).abs_diff_eq(DVec2::from(*b), eps.param));
|
||||
|
||||
// Handle special cases
|
||||
if intersections.is_empty() {
|
||||
// Check if segments are identical
|
||||
if (start0.abs_diff_eq(start1, eps.point)) && end0.abs_diff_eq(end1, eps.point) {
|
||||
return vec![[0.0, 0.0], [1.0, 1.0]];
|
||||
}
|
||||
} else if intersections.len() > 2 {
|
||||
// Keep only the first and last intersection points
|
||||
intersections = vec![intersections[0], intersections[intersections.len() - 1]];
|
||||
}
|
||||
|
||||
intersections
|
||||
}
|
||||
|
||||
fn find_point_on_segment(seg: &PathSegment, point: DVec2, eps: &Epsilons) -> Option<f64> {
|
||||
let start = 0.0;
|
||||
let end = 1.0;
|
||||
let mut t = 0.5;
|
||||
|
||||
for _ in 0..32 {
|
||||
// Limit iterations to prevent infinite loops
|
||||
let current_point = seg.sample_at(t);
|
||||
|
||||
if current_point.abs_diff_eq(point, eps.point) {
|
||||
return Some(t);
|
||||
}
|
||||
|
||||
let start_point = seg.sample_at(start);
|
||||
let end_point = seg.sample_at(end);
|
||||
|
||||
let dist_start = (point - start_point).length_squared();
|
||||
let dist_end = (point - end_point).length_squared();
|
||||
let dist_current = (point - current_point).length_squared();
|
||||
|
||||
if dist_current < dist_start && dist_current < dist_end {
|
||||
return Some(t);
|
||||
}
|
||||
|
||||
if dist_start < dist_end {
|
||||
t = (start + t) / 2.0;
|
||||
} else {
|
||||
t = (t + end) / 2.0;
|
||||
}
|
||||
|
||||
if (end - start) < eps.param {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use glam::DVec2;
|
||||
|
||||
#[test]
|
||||
fn intersect_cubic_slow_first() {
|
||||
path_segment_intersection(&a(), &b(), true, &crate::EPS);
|
||||
}
|
||||
#[test]
|
||||
fn intersect_cubic_slow_second() {
|
||||
path_segment_intersection(&c(), &d(), true, &crate::EPS);
|
||||
}
|
||||
|
||||
fn a() -> PathSegment {
|
||||
PathSegment::Cubic(
|
||||
DVec2::new(458.37027, 572.165771),
|
||||
DVec2::new(428.525848, 486.720093),
|
||||
DVec2::new(368.618805, 467.485992),
|
||||
DVec2::new(273.0, 476.0),
|
||||
)
|
||||
}
|
||||
fn b() -> PathSegment {
|
||||
PathSegment::Cubic(DVec2::new(273.0, 476.0), DVec2::new(419.0, 463.0), DVec2::new(481.741198, 514.692273), DVec2::new(481.333333, 768.0))
|
||||
}
|
||||
fn c() -> PathSegment {
|
||||
PathSegment::Cubic(DVec2::new(273.0, 476.0), DVec2::new(107.564178, 490.730591), DVec2::new(161.737915, 383.575775), DVec2::new(0.0, 340.0))
|
||||
}
|
||||
fn d() -> PathSegment {
|
||||
PathSegment::Cubic(DVec2::new(0.0, 340.0), DVec2::new(161.737914, 383.575765), DVec2::new(107.564182, 490.730587), DVec2::new(273.0, 476.0))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use glam::DVec2;
|
||||
|
||||
pub type LineSegment = [DVec2; 2];
|
||||
|
||||
const COLLINEAR_EPS: f64 = f64::EPSILON * 64.0;
|
||||
|
||||
#[inline(never)]
|
||||
pub fn line_segment_intersection([p1, p2]: LineSegment, [p3, p4]: LineSegment, eps: f64) -> Option<(f64, f64)> {
|
||||
// https://en.wikipedia.org/wiki/Intersection_(geometry)#Two_line_segments
|
||||
|
||||
let a = p2 - p1;
|
||||
let b = p3 - p4;
|
||||
let c = p3 - p1;
|
||||
|
||||
let denom = a.x * b.y - a.y * b.x;
|
||||
|
||||
if denom.abs() < COLLINEAR_EPS {
|
||||
return None;
|
||||
}
|
||||
|
||||
let s = (c.x * b.y - c.y * b.x) / denom;
|
||||
let t = (a.x * c.y - a.y * c.x) / denom;
|
||||
|
||||
if (-eps..=1.0 + eps).contains(&s) && (-eps..=1.0 + eps).contains(&t) {
|
||||
Some((s, t))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn line_segments_intersect(seg1: LineSegment, seg2: LineSegment, eps: f64) -> bool {
|
||||
line_segment_intersection(seg1, seg2, eps).is_some()
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use crate::aabb::Aabb;
|
||||
use crate::line_segment::LineSegment;
|
||||
|
||||
const INSIDE: u8 = 0;
|
||||
const LEFT: u8 = 1;
|
||||
const RIGHT: u8 = 1 << 1;
|
||||
const BOTTOM: u8 = 1 << 2;
|
||||
const TOP: u8 = 1 << 3;
|
||||
|
||||
fn out_code(x: f64, y: f64, bounding_box: &Aabb) -> u8 {
|
||||
let mut code = INSIDE;
|
||||
|
||||
if x < bounding_box.left {
|
||||
code |= LEFT;
|
||||
} else if x > bounding_box.right {
|
||||
code |= RIGHT;
|
||||
}
|
||||
|
||||
if y < bounding_box.top {
|
||||
code |= BOTTOM;
|
||||
} else if y > bounding_box.bottom {
|
||||
code |= TOP;
|
||||
}
|
||||
|
||||
code
|
||||
}
|
||||
|
||||
pub(crate) fn line_segment_aabb_intersect(seg: LineSegment, bounding_box: &Aabb) -> bool {
|
||||
let [mut p0, mut p1] = seg;
|
||||
|
||||
let mut outcode0 = out_code(p0.x, p0.y, bounding_box);
|
||||
let mut outcode1 = out_code(p1.x, p1.y, bounding_box);
|
||||
|
||||
loop {
|
||||
if (outcode0 | outcode1) == 0 {
|
||||
// bitwise OR is 0: both points inside window; trivially accept and exit loop
|
||||
return true;
|
||||
} else if (outcode0 & outcode1) != 0 {
|
||||
// bitwise AND is not 0: both points share an outside zone (LEFT, RIGHT, TOP,
|
||||
// or BOTTOM), so both must be outside window; exit loop (accept is false)
|
||||
return false;
|
||||
} else {
|
||||
// failed both tests, so calculate the line segment to clip
|
||||
// from an outside point to an intersection with clip edge
|
||||
let mut x = 0.0;
|
||||
let mut y = 0.0;
|
||||
|
||||
// At least one endpoint is outside the clip rectangle; pick it.
|
||||
let outcode_out = if outcode1 > outcode0 { outcode1 } else { outcode0 };
|
||||
|
||||
// Now find the intersection point;
|
||||
// use formulas:
|
||||
// slope = (y1 - y0) / (x1 - x0)
|
||||
// x = x0 + (1 / slope) * (ym - y0), where ym is ymin or ymax
|
||||
// y = y0 + slope * (xm - x0), where xm is xmin or xmax
|
||||
// No need to worry about divide-by-zero because, in each case, the
|
||||
// outcode bit being tested guarantees the denominator is non-zero
|
||||
if (outcode_out & TOP) != 0 {
|
||||
// point is above the clip window
|
||||
x = p0.x + (p1.x - p0.x) * (bounding_box.bottom - p0.y) / (p1.y - p0.y);
|
||||
y = bounding_box.bottom;
|
||||
} else if (outcode_out & BOTTOM) != 0 {
|
||||
// point is below the clip window
|
||||
x = p0.x + (p1.x - p0.x) * (bounding_box.top - p0.y) / (p1.y - p0.y);
|
||||
y = bounding_box.top;
|
||||
} else if (outcode_out & RIGHT) != 0 {
|
||||
// point is to the right of clip window
|
||||
y = p0.y + (p1.y - p0.y) * (bounding_box.right - p0.x) / (p1.x - p0.x);
|
||||
x = bounding_box.right;
|
||||
} else if (outcode_out & LEFT) != 0 {
|
||||
// point is to the left of clip window
|
||||
y = p0.y + (p1.y - p0.y) * (bounding_box.left - p0.x) / (p1.x - p0.x);
|
||||
x = bounding_box.left;
|
||||
}
|
||||
|
||||
// Now we move outside point to intersection point to clip
|
||||
// and get ready for next pass.
|
||||
if outcode_out == outcode0 {
|
||||
p0.x = x;
|
||||
p0.y = y;
|
||||
outcode0 = out_code(p0.x, p0.y, bounding_box);
|
||||
} else {
|
||||
p1.x = x;
|
||||
p1.y = y;
|
||||
outcode1 = out_code(p1.x, p1.y, bounding_box);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use crate::path_segment::PathSegment;
|
||||
|
||||
const EPS: f64 = 1e-12;
|
||||
|
||||
pub fn path_cubic_segment_self_intersection(seg: &PathSegment) -> Option<[f64; 2]> {
|
||||
// https://math.stackexchange.com/questions/3931865/self-intersection-of-a-cubic-bezier-interpretation-of-the-solution
|
||||
|
||||
if let PathSegment::Cubic(p1, p2, p3, p4) = seg {
|
||||
let ax = -p1.x + 3.0 * p2.x - 3.0 * p3.x + p4.x;
|
||||
let ay = -p1.y + 3.0 * p2.y - 3.0 * p3.y + p4.y;
|
||||
let bx = 3.0 * p1.x - 6.0 * p2.x + 3.0 * p3.x;
|
||||
let by = 3.0 * p1.y - 6.0 * p2.y + 3.0 * p3.y;
|
||||
let cx = -3.0 * p1.x + 3.0 * p2.x;
|
||||
let cy = -3.0 * p1.y + 3.0 * p2.y;
|
||||
|
||||
let m = ay * bx - ax * by;
|
||||
let n = ax * cy - ay * cx;
|
||||
|
||||
let k = (-3.0 * ax * ax * cy * cy + 6.0 * ax * ay * cx * cy + 4.0 * ax * bx * by * cy - 4.0 * ax * by * by * cx - 3.0 * ay * ay * cx * cx - 4.0 * ay * bx * bx * cy + 4.0 * ay * bx * by * cx)
|
||||
/ (ax * ax * by * by - 2.0 * ax * ay * bx * by + ay * ay * bx * bx);
|
||||
|
||||
if k < 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let t1 = (n / m + k.sqrt()) / 2.0;
|
||||
let t2 = (n / m - k.sqrt()) / 2.0;
|
||||
|
||||
if (EPS..=1.0 - EPS).contains(&t1) && (EPS..=1.0 - EPS).contains(&t2) {
|
||||
let mut result = [t1, t2];
|
||||
result.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,720 @@
|
||||
//! Defines the `PathSegment` enum and related functionality for representing and
|
||||
//! manipulating path segments in 2D space.
|
||||
//!
|
||||
//! This module provides implementations for various types of path segments including
|
||||
//! lines, cubic and quadratic Bézier curves, and elliptical arcs. It also includes
|
||||
//! utility functions for operations such as bounding box calculation, segment splitting,
|
||||
//! and arc-to-cubic conversion.
|
||||
//!
|
||||
//! The implementations in this module closely follow the SVG path specification,
|
||||
//! making it suitable for use in vector graphics applications.
|
||||
|
||||
use glam::{DMat2, DMat3, DVec2};
|
||||
use std::f64::consts::{PI, TAU};
|
||||
|
||||
use crate::aabb::{bounding_box_around_point, expand_bounding_box, extend_bounding_box, merge_bounding_boxes, Aabb};
|
||||
use crate::math::{lerp, vector_angle};
|
||||
use crate::EPS;
|
||||
|
||||
/// Represents a segment of a path in a 2D space, based on the SVG path specification.
|
||||
///
|
||||
/// This enum closely follows the path segment types defined in the SVG 2 specification.
|
||||
/// For more details, see: <https://www.w3.org/TR/SVG2/paths.html>
|
||||
///
|
||||
/// Each variant of this enum corresponds to a different type of path segment:
|
||||
/// - Line: A straight line between two points.
|
||||
/// - Cubic: A cubic Bézier curve.
|
||||
/// - Quadratic: A quadratic Bézier curve.
|
||||
/// - Arc: An elliptical arc.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Creating a line segment:
|
||||
/// ```
|
||||
/// use path_bool::PathSegment;
|
||||
/// use glam::DVec2;
|
||||
///
|
||||
/// let line = PathSegment::Line(DVec2::new(0.0, 0.0), DVec2::new(1.0, 1.0));
|
||||
/// ```
|
||||
///
|
||||
/// Creating a cubic Bézier curve:
|
||||
/// ```
|
||||
/// use path_bool::PathSegment;
|
||||
/// use glam::DVec2;
|
||||
///
|
||||
/// let cubic = PathSegment::Cubic(
|
||||
/// DVec2::new(0.0, 0.0),
|
||||
/// DVec2::new(1.0, 0.0),
|
||||
/// DVec2::new(1.0, 1.0),
|
||||
/// DVec2::new(2.0, 1.0)
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum PathSegment {
|
||||
/// A line segment from the first point to the second.
|
||||
/// Corresponds to the SVG "L" command.
|
||||
Line(DVec2, DVec2),
|
||||
|
||||
/// A cubic Bézier curve with start point, two control points, and end point.
|
||||
/// Corresponds to the SVG "C" command.
|
||||
Cubic(DVec2, DVec2, DVec2, DVec2),
|
||||
|
||||
/// A quadratic Bézier curve with start point, control point, and end point.
|
||||
/// Corresponds to the SVG "Q" command.
|
||||
Quadratic(DVec2, DVec2, DVec2),
|
||||
|
||||
/// An elliptical arc.
|
||||
/// Corresponds to the SVG "A" command.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - Start point
|
||||
/// - X-axis radius
|
||||
/// - Y-axis radius
|
||||
/// - X-axis rotation (in radians)
|
||||
/// - Large arc flag (true if the arc should be greater than or equal to 180 degrees)
|
||||
/// - Sweep flag (true if the arc should be drawn in a "positive-angle" direction)
|
||||
/// - End point
|
||||
Arc(DVec2, f64, f64, f64, bool, bool, DVec2),
|
||||
}
|
||||
|
||||
impl PathSegment {
|
||||
/// Calculates the angle of the tangent at the start point of the segment.
|
||||
///
|
||||
/// This method computes the angle (in radians) of the tangent vector at the
|
||||
/// beginning of the path segment. The angle is measured clockwise
|
||||
/// from the positive x-axis.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A float representing the angle in radians, normalized to the range [0, 2π).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use path_bool::PathSegment;
|
||||
/// use glam::DVec2;
|
||||
/// use std::f64::consts::{TAU, FRAC_PI_4};
|
||||
///
|
||||
/// let line = PathSegment::Line(DVec2::new(0.0, 0.0), DVec2::new(1.0, 1.0));
|
||||
/// assert_eq!(line.start_angle(), TAU - (FRAC_PI_4));
|
||||
/// ```
|
||||
pub fn start_angle(&self) -> f64 {
|
||||
let angle = match *self {
|
||||
PathSegment::Line(start, end) => (end - start).angle_to(DVec2::X),
|
||||
PathSegment::Cubic(start, control1, control2, _) => {
|
||||
let diff = control1 - start;
|
||||
if diff.abs_diff_eq(DVec2::ZERO, EPS.point) {
|
||||
// if this diff were empty too, the segments would have been convertet to a line
|
||||
(control2 - start).angle_to(DVec2::X)
|
||||
} else {
|
||||
diff.angle_to(DVec2::X)
|
||||
}
|
||||
}
|
||||
// Apply same logic as for cubic bezier
|
||||
PathSegment::Quadratic(start, control, _) => (control - start).to_angle(),
|
||||
PathSegment::Arc(..) => self.arc_segment_to_cubics(0.001)[0].start_angle(),
|
||||
};
|
||||
use std::f64::consts::TAU;
|
||||
(angle + TAU) % TAU
|
||||
}
|
||||
|
||||
/// Computes the curvature at the start point of the segment.
|
||||
///
|
||||
/// The curvature is a measure of how sharply a curve bends. A straight line
|
||||
/// has a curvature of 0, while a tight curve has a higher curvature value.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A float representing the curvature. Positive values indicate a left
|
||||
/// curve, while negative values indicate a right curve.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use path_bool::PathSegment;
|
||||
/// use glam::DVec2;
|
||||
///
|
||||
/// let line = PathSegment::Line(DVec2::new(0.0, 0.0), DVec2::new(1.0, 1.0));
|
||||
/// assert_eq!(line.start_curvature(), 0.0);
|
||||
///
|
||||
/// let curve = PathSegment::Cubic(
|
||||
/// DVec2::new(0.0, 0.0),
|
||||
/// DVec2::new(0.0, 1.0),
|
||||
/// DVec2::new(1.0, 1.0),
|
||||
/// DVec2::new(1.0, 0.0)
|
||||
/// );
|
||||
/// assert!(curve.start_curvature() < 0.0);
|
||||
/// ```
|
||||
pub fn start_curvature(&self) -> f64 {
|
||||
match *self {
|
||||
PathSegment::Line(_, _) => 0.0,
|
||||
PathSegment::Cubic(start, control1, control2, _) => {
|
||||
let a = control1 - start;
|
||||
let a = 3. * a;
|
||||
let b = start - 2.0 * control1 + control2;
|
||||
let b = 6. * b;
|
||||
let numerator = a.x * b.y - a.y * b.x;
|
||||
let denominator = a.length_squared() * a.length();
|
||||
// dbg!(a, b, numerator, denominator);
|
||||
if denominator == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
numerator / denominator
|
||||
}
|
||||
}
|
||||
PathSegment::Quadratic(start, control, end) => {
|
||||
// first derivatiave
|
||||
let a = 2. * (control - start);
|
||||
// second derivatiave
|
||||
let b = 2. * (start - 2.0 * control + end);
|
||||
let numerator = a.x * b.y - a.y * b.x;
|
||||
let denominator = a.length_squared() * a.length();
|
||||
if denominator == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
numerator / denominator
|
||||
}
|
||||
}
|
||||
PathSegment::Arc(..) => self.arc_segment_to_cubics(0.001)[0].start_curvature(),
|
||||
}
|
||||
}
|
||||
/// Converts the segment to a cubic Bézier curve representation.
|
||||
///
|
||||
/// This method provides a uniform representation of all segment types as
|
||||
/// cubic Bézier curves. For segments that are not naturally cubic Bézier
|
||||
/// curves (like lines or quadratic Bézier curves), an equivalent cubic
|
||||
/// Bézier representation is computed.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// An array of four `DVec2` points representing the cubic Bézier curve:
|
||||
/// [start point, first control point, second control point, end point]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use path_bool::PathSegment;
|
||||
/// use glam::DVec2;
|
||||
///
|
||||
/// let line = PathSegment::Line(DVec2::new(0.0, 0.0), DVec2::new(1.0, 1.0));
|
||||
/// let cubic = line.to_cubic();
|
||||
/// assert_eq!(cubic[0], DVec2::new(0.0, 0.0));
|
||||
/// assert_eq!(cubic[3], DVec2::new(1.0, 1.0));
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method is not implemented for `PathSegment::Arc`. Attempting to call
|
||||
/// `to_cubic()` on an `Arc` segment will result in a panic.
|
||||
pub fn to_cubic(&self) -> [DVec2; 4] {
|
||||
match *self {
|
||||
PathSegment::Line(start, end) => [start, start, end, end],
|
||||
PathSegment::Cubic(s, c1, c2, e) => [s, c1, c2, e],
|
||||
PathSegment::Quadratic(start, control, end) => {
|
||||
// C0 = Q0
|
||||
// C1 = Q0 + (2/3) (Q1 - Q0)
|
||||
// C2 = Q2 + (2/3) (Q1 - Q2)
|
||||
// C3 = Q2
|
||||
let d1 = control - start;
|
||||
let d2 = control - end;
|
||||
[start, start + (2. / 3.) * d1, end + (2. / 3.) * d2, end]
|
||||
}
|
||||
PathSegment::Arc(..) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Retrieves the start point of a path segment.
|
||||
pub fn start(&self) -> DVec2 {
|
||||
match self {
|
||||
PathSegment::Line(start, _) => *start,
|
||||
PathSegment::Cubic(start, _, _, _) => *start,
|
||||
PathSegment::Quadratic(start, _, _) => *start,
|
||||
PathSegment::Arc(start, _, _, _, _, _, _) => *start,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Retrieves the end point of a path segment.
|
||||
pub fn end(&self) -> DVec2 {
|
||||
match self {
|
||||
PathSegment::Line(_, end) => *end,
|
||||
PathSegment::Cubic(_, _, _, end) => *end,
|
||||
PathSegment::Quadratic(_, _, end) => *end,
|
||||
PathSegment::Arc(_, _, _, _, _, _, end) => *end,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Reverses the direction of the path segment.
|
||||
///
|
||||
/// This method creates a new `PathSegment` that represents the same geometric shape
|
||||
/// but in the opposite direction.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use path_bool::PathSegment;
|
||||
/// use glam::DVec2;
|
||||
///
|
||||
/// let line = PathSegment::Line(DVec2::new(0.0, 0.0), DVec2::new(1.0, 1.0));
|
||||
/// let reversed = line.reverse();
|
||||
/// assert_eq!(reversed.start(), DVec2::new(1.0, 1.0));
|
||||
/// assert_eq!(reversed.end(), DVec2::new(0.0, 0.0));
|
||||
/// ```
|
||||
pub fn reverse(&self) -> PathSegment {
|
||||
match *self {
|
||||
PathSegment::Line(start, end) => PathSegment::Line(end, start),
|
||||
PathSegment::Cubic(p1, p2, p3, p4) => PathSegment::Cubic(p4, p3, p2, p1),
|
||||
PathSegment::Quadratic(p1, p2, p3) => PathSegment::Quadratic(p3, p2, p1),
|
||||
PathSegment::Arc(start, rx, ry, phi, fa, fs, end) => PathSegment::Arc(end, rx, ry, phi, fa, !fs, start),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Converts an arc segment to its center parameterization.
|
||||
///
|
||||
/// This method is only meaningful for `Arc` segments. For other segment types,
|
||||
/// it returns `None`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// An `Option` containing `PathArcSegmentCenterParametrization` if the segment
|
||||
/// is an `Arc`, or `None` otherwise.
|
||||
pub fn arc_segment_to_center(&self) -> Option<PathArcSegmentCenterParametrization> {
|
||||
if let PathSegment::Arc(xy1, rx, ry, phi, fa, fs, xy2) = *self {
|
||||
if rx == 0.0 || ry == 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let rotation_matrix = DMat2::from_angle(-phi.to_radians());
|
||||
let xy1_prime = rotation_matrix * (xy1 - xy2) * 0.5;
|
||||
|
||||
let mut rx2 = rx * rx;
|
||||
let mut ry2 = ry * ry;
|
||||
let x1_prime2 = xy1_prime.x * xy1_prime.x;
|
||||
let y1_prime2 = xy1_prime.y * xy1_prime.y;
|
||||
|
||||
let mut rx = rx.abs();
|
||||
let mut ry = ry.abs();
|
||||
let lambda = x1_prime2 / rx2 + y1_prime2 / ry2 + 1e-12;
|
||||
if lambda > 1.0 {
|
||||
let lambda_sqrt = lambda.sqrt();
|
||||
rx *= lambda_sqrt;
|
||||
ry *= lambda_sqrt;
|
||||
let lambda_abs = lambda.abs();
|
||||
rx2 *= lambda_abs;
|
||||
ry2 *= lambda_abs;
|
||||
}
|
||||
|
||||
let sign = if fa == fs { -1.0 } else { 1.0 };
|
||||
let multiplier = ((rx2 * ry2 - rx2 * y1_prime2 - ry2 * x1_prime2) / (rx2 * y1_prime2 + ry2 * x1_prime2)).sqrt();
|
||||
let cx_prime = sign * multiplier * ((rx * xy1_prime.y) / ry);
|
||||
let cy_prime = sign * multiplier * ((-ry * xy1_prime.x) / rx);
|
||||
|
||||
let cxy = rotation_matrix.transpose() * DVec2::new(cx_prime, cy_prime) + (xy1 + xy2) * 0.5;
|
||||
|
||||
let vec1 = DVec2::new((xy1_prime.x - cx_prime) / rx, (xy1_prime.y - cy_prime) / ry);
|
||||
let theta1 = vector_angle(DVec2::new(1.0, 0.0), vec1);
|
||||
let mut delta_theta = vector_angle(vec1, DVec2::new((-xy1_prime.x - cx_prime) / rx, (-xy1_prime.y - cy_prime) / ry));
|
||||
|
||||
if !fs && delta_theta > 0.0 {
|
||||
delta_theta -= TAU;
|
||||
} else if fs && delta_theta < 0.0 {
|
||||
delta_theta += TAU;
|
||||
}
|
||||
|
||||
Some(PathArcSegmentCenterParametrization {
|
||||
center: cxy,
|
||||
theta1,
|
||||
delta_theta,
|
||||
rx,
|
||||
ry,
|
||||
phi,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Samples a point on the path segment at a given parameter value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `t` - A value between 0.0 and 1.0 representing the position along the segment.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use path_bool::PathSegment;
|
||||
/// use glam::DVec2;
|
||||
///
|
||||
/// let line = PathSegment::Line(DVec2::new(0.0, 0.0), DVec2::new(2.0, 2.0));
|
||||
/// assert_eq!(line.sample_at(0.5), DVec2::new(1.0, 1.0));
|
||||
/// ```
|
||||
pub fn sample_at(&self, t: f64) -> DVec2 {
|
||||
match *self {
|
||||
PathSegment::Line(start, end) => start.lerp(end, t),
|
||||
PathSegment::Cubic(p1, p2, p3, p4) => {
|
||||
let p01 = p1.lerp(p2, t);
|
||||
let p12 = p2.lerp(p3, t);
|
||||
let p23 = p3.lerp(p4, t);
|
||||
let p012 = p01.lerp(p12, t);
|
||||
let p123 = p12.lerp(p23, t);
|
||||
p012.lerp(p123, t)
|
||||
}
|
||||
PathSegment::Quadratic(p1, p2, p3) => {
|
||||
let p01 = p1.lerp(p2, t);
|
||||
let p12 = p2.lerp(p3, t);
|
||||
p01.lerp(p12, t)
|
||||
}
|
||||
PathSegment::Arc(start, rx, ry, phi, _, _, end) => {
|
||||
if let Some(center_param) = self.arc_segment_to_center() {
|
||||
let theta = center_param.theta1 + t * center_param.delta_theta;
|
||||
let p = DVec2::new(rx * theta.cos(), ry * theta.sin());
|
||||
let rotation_matrix = DMat2::from_angle(phi);
|
||||
rotation_matrix * p + center_param.center
|
||||
} else {
|
||||
start.lerp(end, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Approximates an arc segment with a series of cubic Bézier curves.
|
||||
///
|
||||
/// This method is primarily used for `Arc` segments, converting them into
|
||||
/// a series of cubic Bézier curves for easier rendering or manipulation.
|
||||
/// For non-`Arc` segments, it returns a vector containing only the original segment.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `max_delta_theta` - The maximum angle (in radians) that each cubic Bézier
|
||||
/// curve approximation should span.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of `PathSegment::Cubic` approximating the original segment.
|
||||
pub fn arc_segment_to_cubics(&self, max_delta_theta: f64) -> Vec<PathSegment> {
|
||||
if let PathSegment::Arc(start, rx, ry, phi, _, _, end) = *self {
|
||||
if let Some(center_param) = self.arc_segment_to_center() {
|
||||
let count = ((center_param.delta_theta.abs() / max_delta_theta).ceil() as usize).max(1);
|
||||
|
||||
let from_unit = DMat3::from_translation(center_param.center) * DMat3::from_angle(phi.to_radians()) * DMat3::from_scale(DVec2::new(rx, ry));
|
||||
|
||||
let theta = center_param.delta_theta / count as f64;
|
||||
let k = (4.0 / 3.0) * (theta / 4.0).tan();
|
||||
let sin_theta = theta.sin();
|
||||
let cos_theta = theta.cos();
|
||||
|
||||
(0..count)
|
||||
.map(|i| {
|
||||
let start = DVec2::new(1.0, 0.0);
|
||||
let control1 = DVec2::new(1.0, k);
|
||||
let control2 = DVec2::new(cos_theta + k * sin_theta, sin_theta - k * cos_theta);
|
||||
let end = DVec2::new(cos_theta, sin_theta);
|
||||
|
||||
let matrix = DMat3::from_angle(center_param.theta1 + i as f64 * theta) * from_unit;
|
||||
let start = (matrix * start.extend(1.0)).truncate();
|
||||
let control1 = (matrix * control1.extend(1.0)).truncate();
|
||||
let control2 = (matrix * control2.extend(1.0)).truncate();
|
||||
let end = (matrix * end.extend(1.0)).truncate();
|
||||
|
||||
PathSegment::Cubic(start, control1, control2, end)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![PathSegment::Line(start, end)]
|
||||
}
|
||||
} else {
|
||||
vec![*self]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the center parameterization of an elliptical arc.
|
||||
///
|
||||
/// This struct is used internally to perform calculations on arc segments.
|
||||
pub struct PathArcSegmentCenterParametrization {
|
||||
center: DVec2,
|
||||
theta1: f64,
|
||||
delta_theta: f64,
|
||||
rx: f64,
|
||||
ry: f64,
|
||||
phi: f64,
|
||||
}
|
||||
|
||||
/// Converts the center parameterization back to an arc segment.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `start` - Optional start point of the arc. If `None`, the start point is calculated.
|
||||
/// * `end` - Optional end point of the arc. If `None`, the end point is calculated.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `PathSegment::Arc` representing the arc described by this parameterization.
|
||||
impl PathArcSegmentCenterParametrization {
|
||||
#[must_use]
|
||||
pub fn arc_segment_from_center(&self, start: Option<DVec2>, end: Option<DVec2>) -> PathSegment {
|
||||
let rotation_matrix = DMat2::from_angle(self.phi);
|
||||
|
||||
let mut xy1 = rotation_matrix * DVec2::new(self.rx * self.theta1.cos(), self.ry * self.theta1.sin()) + self.center;
|
||||
|
||||
let mut xy2 = rotation_matrix * DVec2::new(self.rx * (self.theta1 + self.delta_theta).cos(), self.ry * (self.theta1 + self.delta_theta).sin()) + self.center;
|
||||
|
||||
let fa = self.delta_theta.abs() > PI;
|
||||
let fs = self.delta_theta > 0.0;
|
||||
xy1 = start.unwrap_or(xy1);
|
||||
xy2 = end.unwrap_or(xy2);
|
||||
|
||||
PathSegment::Arc(xy1, self.rx, self.ry, self.phi, fa, fs, xy2)
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates a 1D cubic Bézier curve at a given parameter value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `p0`, `p1`, `p2`, `p3` - Control points of the cubic Bézier curve.
|
||||
/// * `t` - Parameter value between 0 and 1.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The value of the Bézier curve at parameter `t`.
|
||||
fn eval_cubic_1d(p0: f64, p1: f64, p2: f64, p3: f64, t: f64) -> f64 {
|
||||
let p01 = lerp(p0, p1, t);
|
||||
let p12 = lerp(p1, p2, t);
|
||||
let p23 = lerp(p2, p3, t);
|
||||
let p012 = lerp(p01, p12, t);
|
||||
let p123 = lerp(p12, p23, t);
|
||||
lerp(p012, p123, t)
|
||||
}
|
||||
|
||||
/// Computes the bounding interval of a 1D cubic Bézier curve.
|
||||
///
|
||||
/// This function finds the minimum and maximum values of a cubic Bézier curve
|
||||
/// over the interval [0, 1].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `p0`, `p1`, `p2`, `p3` - Control points of the cubic Bézier curve.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A tuple `(min, max)` representing the bounding interval.
|
||||
fn cubic_bounding_interval(p0: f64, p1: f64, p2: f64, p3: f64) -> (f64, f64) {
|
||||
let mut min = p0.min(p3);
|
||||
let mut max = p0.max(p3);
|
||||
|
||||
let a = 3.0 * (-p0 + 3.0 * p1 - 3.0 * p2 + p3);
|
||||
let b = 6.0 * (p0 - 2.0 * p1 + p2);
|
||||
let c = 3.0 * (p1 - p0);
|
||||
let d = b * b - 4.0 * a * c;
|
||||
|
||||
if d < 0.0 || a == 0.0 {
|
||||
// TODO: if a=0, solve linear
|
||||
return (min, max);
|
||||
}
|
||||
|
||||
let sqrt_d = d.sqrt();
|
||||
|
||||
let t0 = (-b - sqrt_d) / (2.0 * a);
|
||||
if 0.0 < t0 && t0 < 1.0 {
|
||||
let x0 = eval_cubic_1d(p0, p1, p2, p3, t0);
|
||||
min = min.min(x0);
|
||||
max = max.max(x0);
|
||||
}
|
||||
|
||||
let t1 = (-b + sqrt_d) / (2.0 * a);
|
||||
if 0.0 < t1 && t1 < 1.0 {
|
||||
let x1 = eval_cubic_1d(p0, p1, p2, p3, t1);
|
||||
min = min.min(x1);
|
||||
max = max.max(x1);
|
||||
}
|
||||
|
||||
(min, max)
|
||||
}
|
||||
|
||||
/// Evaluates a 1D quadratic Bézier curve at a given parameter value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `p0`, `p1`, `p2` - Control points of the quadratic Bézier curve.
|
||||
/// * `t` - Parameter value between 0 and 1.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The value of the Bézier curve at parameter `t`.
|
||||
fn eval_quadratic_1d(p0: f64, p1: f64, p2: f64, t: f64) -> f64 {
|
||||
let p01 = lerp(p0, p1, t);
|
||||
let p12 = lerp(p1, p2, t);
|
||||
lerp(p01, p12, t)
|
||||
}
|
||||
|
||||
/// Computes the bounding interval of a 1D quadratic Bézier curve.
|
||||
///
|
||||
/// This function finds the minimum and maximum values of a quadratic Bézier curve
|
||||
/// over the interval [0, 1].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `p0`, `p1`, `p2` - Control points of the quadratic Bézier curve.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A tuple `(min, max)` representing the bounding interval.
|
||||
fn quadratic_bounding_interval(p0: f64, p1: f64, p2: f64) -> (f64, f64) {
|
||||
let mut min = p0.min(p2);
|
||||
let mut max = p0.max(p2);
|
||||
|
||||
let denominator = p0 - 2.0 * p1 + p2;
|
||||
|
||||
if denominator == 0.0 {
|
||||
return (min, max);
|
||||
}
|
||||
|
||||
let t = (p0 - p1) / denominator;
|
||||
if (0.0..=1.0).contains(&t) {
|
||||
let x = eval_quadratic_1d(p0, p1, p2, t);
|
||||
min = min.min(x);
|
||||
max = max.max(x);
|
||||
}
|
||||
|
||||
(min, max)
|
||||
}
|
||||
|
||||
fn in_interval(x: f64, x0: f64, x1: f64) -> bool {
|
||||
(x0..=x1).contains(&x)
|
||||
}
|
||||
|
||||
impl PathSegment {
|
||||
/// Computes the bounding box of the path segment.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// An `AaBb` representing the axis-aligned bounding box of the segment.
|
||||
pub(crate) fn bounding_box(&self) -> Aabb {
|
||||
match *self {
|
||||
PathSegment::Line(start, end) => Aabb {
|
||||
top: start.y.min(end.y),
|
||||
right: start.x.max(end.x),
|
||||
bottom: start.y.max(end.y),
|
||||
left: start.x.min(end.x),
|
||||
},
|
||||
PathSegment::Cubic(p1, p2, p3, p4) => {
|
||||
let (left, right) = cubic_bounding_interval(p1.x, p2.x, p3.x, p4.x);
|
||||
let (top, bottom) = cubic_bounding_interval(p1.y, p2.y, p3.y, p4.y);
|
||||
Aabb { top, right, bottom, left }
|
||||
}
|
||||
PathSegment::Quadratic(p1, p2, p3) => {
|
||||
let (left, right) = quadratic_bounding_interval(p1.x, p2.x, p3.x);
|
||||
let (top, bottom) = quadratic_bounding_interval(p1.y, p2.y, p3.y);
|
||||
Aabb { top, right, bottom, left }
|
||||
}
|
||||
PathSegment::Arc(start, rx, ry, phi, _, _, end) => {
|
||||
if let Some(center_param) = self.arc_segment_to_center() {
|
||||
let theta2 = center_param.theta1 + center_param.delta_theta;
|
||||
let mut bounding_box = extend_bounding_box(Some(bounding_box_around_point(start, 0.0)), end);
|
||||
|
||||
if phi == 0.0 || rx == ry {
|
||||
// FIXME: the following gives false positives, resulting in larger boxes
|
||||
if in_interval(-PI, center_param.theta1, theta2) || in_interval(PI, center_param.theta1, theta2) {
|
||||
bounding_box = extend_bounding_box(Some(bounding_box), DVec2::new(center_param.center.x - rx, center_param.center.y));
|
||||
}
|
||||
if in_interval(-PI / 2.0, center_param.theta1, theta2) || in_interval(3.0 * PI / 2.0, center_param.theta1, theta2) {
|
||||
bounding_box = extend_bounding_box(Some(bounding_box), DVec2::new(center_param.center.x, center_param.center.y - ry));
|
||||
}
|
||||
if in_interval(0.0, center_param.theta1, theta2) || in_interval(2.0 * PI, center_param.theta1, theta2) {
|
||||
bounding_box = extend_bounding_box(Some(bounding_box), DVec2::new(center_param.center.x + rx, center_param.center.y));
|
||||
}
|
||||
if in_interval(PI / 2.0, center_param.theta1, theta2) || in_interval(5.0 * PI / 2.0, center_param.theta1, theta2) {
|
||||
bounding_box = extend_bounding_box(Some(bounding_box), DVec2::new(center_param.center.x, center_param.center.y + ry));
|
||||
}
|
||||
expand_bounding_box(&bounding_box, 1e-11) // TODO: get rid of expansion
|
||||
} else {
|
||||
// TODO: don't convert to cubics
|
||||
let cubics = self.arc_segment_to_cubics(PI / 16.0);
|
||||
let mut bounding_box = None;
|
||||
for cubic_seg in cubics {
|
||||
bounding_box = Some(merge_bounding_boxes(bounding_box, &cubic_seg.bounding_box()));
|
||||
}
|
||||
bounding_box.unwrap_or_else(|| bounding_box_around_point(start, 0.0))
|
||||
}
|
||||
} else {
|
||||
extend_bounding_box(Some(bounding_box_around_point(start, 0.0)), end)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits the path segment at a given parameter value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `t` - A value between 0.0 and 1.0 representing the split point along the segment.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A tuple of two `PathSegment`s representing the parts before and after the split point.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use path_bool::PathSegment;
|
||||
/// use glam::DVec2;
|
||||
///
|
||||
/// let line = PathSegment::Line(DVec2::new(0.0, 0.0), DVec2::new(2.0, 2.0));
|
||||
/// let (first_half, second_half) = line.split_at(0.5);
|
||||
/// assert_eq!(first_half.end(), DVec2::new(1.0, 1.0));
|
||||
/// assert_eq!(second_half.start(), DVec2::new(1.0, 1.0));
|
||||
/// ```
|
||||
pub fn split_at(&self, t: f64) -> (PathSegment, PathSegment) {
|
||||
match *self {
|
||||
PathSegment::Line(start, end) => {
|
||||
let p = start.lerp(end, t);
|
||||
(PathSegment::Line(start, p), PathSegment::Line(p, end))
|
||||
}
|
||||
PathSegment::Cubic(p0, p1, p2, p3) => {
|
||||
let p01 = p0.lerp(p1, t);
|
||||
let p12 = p1.lerp(p2, t);
|
||||
let p23 = p2.lerp(p3, t);
|
||||
let p012 = p01.lerp(p12, t);
|
||||
let p123 = p12.lerp(p23, t);
|
||||
let p = p012.lerp(p123, t);
|
||||
|
||||
(PathSegment::Cubic(p0, p01, p012, p), PathSegment::Cubic(p, p123, p23, p3))
|
||||
}
|
||||
PathSegment::Quadratic(p0, p1, p2) => {
|
||||
let p01 = p0.lerp(p1, t);
|
||||
let p12 = p1.lerp(p2, t);
|
||||
let p = p01.lerp(p12, t);
|
||||
|
||||
(PathSegment::Quadratic(p0, p01, p), PathSegment::Quadratic(p, p12, p2))
|
||||
}
|
||||
PathSegment::Arc(start, _, _, _, _, _, end) => {
|
||||
if let Some(center_param) = self.arc_segment_to_center() {
|
||||
let mid_delta_theta = center_param.delta_theta * t;
|
||||
let seg1 = PathArcSegmentCenterParametrization {
|
||||
delta_theta: mid_delta_theta,
|
||||
..center_param
|
||||
}
|
||||
.arc_segment_from_center(Some(start), None);
|
||||
let seg2 = PathArcSegmentCenterParametrization {
|
||||
theta1: center_param.theta1 + mid_delta_theta,
|
||||
delta_theta: center_param.delta_theta - mid_delta_theta,
|
||||
..center_param
|
||||
}
|
||||
.arc_segment_from_center(None, Some(end));
|
||||
(seg1, seg2)
|
||||
} else {
|
||||
// https://svgwg.org/svg2-draft/implnote.html#ArcCorrectionOutOfRangeRadii
|
||||
let p = start.lerp(end, t);
|
||||
(PathSegment::Line(start, p), PathSegment::Line(p, end))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
use glam::DVec2;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct Aabb {
|
||||
pub top: f64,
|
||||
pub right: f64,
|
||||
pub bottom: f64,
|
||||
pub left: f64,
|
||||
}
|
||||
|
||||
pub(crate) fn bounding_boxes_overlap(a: &Aabb, b: &Aabb) -> bool {
|
||||
a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom
|
||||
}
|
||||
|
||||
pub(crate) fn merge_bounding_boxes(a: Option<Aabb>, b: &Aabb) -> Aabb {
|
||||
match a {
|
||||
Some(a) => Aabb {
|
||||
top: a.top.min(b.top),
|
||||
right: a.right.max(b.right),
|
||||
bottom: a.bottom.max(b.bottom),
|
||||
left: a.left.min(b.left),
|
||||
},
|
||||
None => *b,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn extend_bounding_box(bounding_box: Option<Aabb>, point: DVec2) -> Aabb {
|
||||
match bounding_box {
|
||||
Some(bb) => Aabb {
|
||||
top: bb.top.min(point.y),
|
||||
right: bb.right.max(point.x),
|
||||
bottom: bb.bottom.max(point.y),
|
||||
left: bb.left.min(point.x),
|
||||
},
|
||||
None => Aabb {
|
||||
top: point.y,
|
||||
right: point.x,
|
||||
bottom: point.y,
|
||||
left: point.x,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bounding_box_max_extent(bounding_box: &Aabb) -> f64 {
|
||||
(bounding_box.right - bounding_box.left).max(bounding_box.bottom - bounding_box.top)
|
||||
}
|
||||
|
||||
pub(crate) fn bounding_box_around_point(point: DVec2, padding: f64) -> Aabb {
|
||||
Aabb {
|
||||
top: point.y - padding,
|
||||
right: point.x + padding,
|
||||
bottom: point.y + padding,
|
||||
left: point.x - padding,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expand_bounding_box(bounding_box: &Aabb, padding: f64) -> Aabb {
|
||||
Aabb {
|
||||
top: bounding_box.top - padding,
|
||||
right: bounding_box.right + padding,
|
||||
bottom: bounding_box.bottom + padding,
|
||||
left: bounding_box.left - padding,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Epsilons {
|
||||
pub point: f64,
|
||||
pub linear: f64,
|
||||
pub param: f64,
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use glam::{DVec2, FloatExt};
|
||||
pub use std::f64::consts::PI;
|
||||
|
||||
pub fn lin_map(value: f64, in_min: f64, in_max: f64, out_min: f64, out_max: f64) -> f64 {
|
||||
((value - in_min) / (in_max - in_min)) * (out_max - out_min) + out_min
|
||||
}
|
||||
|
||||
pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
|
||||
a.lerp(b, t)
|
||||
}
|
||||
|
||||
pub fn vector_angle(u: DVec2, v: DVec2) -> f64 {
|
||||
const EPS: f64 = 1e-12;
|
||||
|
||||
let sign = u.x * v.y - u.y * v.x;
|
||||
|
||||
if sign.abs() < EPS && (u + v).length_squared() < EPS * EPS {
|
||||
// TODO: u can be scaled
|
||||
return PI;
|
||||
}
|
||||
|
||||
sign.signum() * (u.dot(v) / (u.length() * v.length())).acos()
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use crate::aabb::Aabb;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub struct QuadTree<T> {
|
||||
bounding_box: Aabb,
|
||||
depth: usize,
|
||||
inner_node_capacity: usize,
|
||||
subtrees: Option<Box<[QuadTree<T>; 4]>>,
|
||||
pairs: Vec<(Aabb, T)>,
|
||||
}
|
||||
|
||||
impl<T: Clone> QuadTree<T> {
|
||||
pub fn new(bounding_box: Aabb, depth: usize, inner_node_capacity: usize) -> Self {
|
||||
QuadTree {
|
||||
bounding_box,
|
||||
depth,
|
||||
inner_node_capacity,
|
||||
subtrees: None,
|
||||
pairs: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, bounding_box: Aabb, value: T) -> bool {
|
||||
if !crate::aabb::bounding_boxes_overlap(&bounding_box, &self.bounding_box) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.depth > 0 && self.pairs.len() >= self.inner_node_capacity {
|
||||
self.ensure_subtrees();
|
||||
for tree in self.subtrees.as_mut().unwrap().iter_mut() {
|
||||
tree.insert(bounding_box, value.clone());
|
||||
}
|
||||
} else {
|
||||
self.pairs.push((bounding_box, value));
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn find(&self, bounding_box: &Aabb) -> HashSet<T>
|
||||
where
|
||||
T: Eq + std::hash::Hash + Clone,
|
||||
{
|
||||
let mut set = HashSet::new();
|
||||
self.find_internal(bounding_box, &mut set);
|
||||
set
|
||||
}
|
||||
|
||||
fn find_internal(&self, bounding_box: &Aabb, set: &mut HashSet<T>)
|
||||
where
|
||||
T: Eq + std::hash::Hash + Clone,
|
||||
{
|
||||
if !crate::aabb::bounding_boxes_overlap(bounding_box, &self.bounding_box) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (key, value) in &self.pairs {
|
||||
if crate::aabb::bounding_boxes_overlap(bounding_box, key) {
|
||||
set.insert(value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(subtrees) = &self.subtrees {
|
||||
for tree in subtrees.iter() {
|
||||
tree.find_internal(bounding_box, set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_subtrees(&mut self) {
|
||||
if self.subtrees.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let midx = (self.bounding_box.left + self.bounding_box.right) / 2.0;
|
||||
let midy = (self.bounding_box.top + self.bounding_box.bottom) / 2.0;
|
||||
|
||||
self.subtrees = Some(Box::new([
|
||||
QuadTree::new(
|
||||
Aabb {
|
||||
top: self.bounding_box.top,
|
||||
right: midx,
|
||||
bottom: midy,
|
||||
left: self.bounding_box.left,
|
||||
},
|
||||
self.depth - 1,
|
||||
self.inner_node_capacity,
|
||||
),
|
||||
QuadTree::new(
|
||||
Aabb {
|
||||
top: self.bounding_box.top,
|
||||
right: self.bounding_box.right,
|
||||
bottom: midy,
|
||||
left: midx,
|
||||
},
|
||||
self.depth - 1,
|
||||
self.inner_node_capacity,
|
||||
),
|
||||
QuadTree::new(
|
||||
Aabb {
|
||||
top: midy,
|
||||
right: midx,
|
||||
bottom: self.bounding_box.bottom,
|
||||
left: self.bounding_box.left,
|
||||
},
|
||||
self.depth - 1,
|
||||
self.inner_node_capacity,
|
||||
),
|
||||
QuadTree::new(
|
||||
Aabb {
|
||||
top: midy,
|
||||
right: self.bounding_box.right,
|
||||
bottom: self.bounding_box.bottom,
|
||||
left: midx,
|
||||
},
|
||||
self.depth - 1,
|
||||
self.inner_node_capacity,
|
||||
),
|
||||
]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
use core::panic;
|
||||
use glob::glob;
|
||||
use image::{DynamicImage, GenericImageView, RgbaImage};
|
||||
use resvg::render;
|
||||
use resvg::tiny_skia::Transform;
|
||||
use resvg::usvg::{Options, Tree};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use svg::parser::Event;
|
||||
|
||||
use crate::path_boolean::{self, FillRule, PathBooleanOperation};
|
||||
use crate::path_data::{path_from_path_data, path_to_path_data};
|
||||
|
||||
const TOLERANCE: u8 = 84;
|
||||
|
||||
fn get_fill_rule(fill_rule: &str) -> FillRule {
|
||||
match fill_rule {
|
||||
"evenodd" => FillRule::EvenOdd,
|
||||
_ => FillRule::NonZero,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visual_tests() {
|
||||
let ops = [
|
||||
("union", PathBooleanOperation::Union),
|
||||
("difference", PathBooleanOperation::Difference),
|
||||
("intersection", PathBooleanOperation::Intersection),
|
||||
("exclusion", PathBooleanOperation::Exclusion),
|
||||
("division", PathBooleanOperation::Division),
|
||||
("fracture", PathBooleanOperation::Fracture),
|
||||
];
|
||||
|
||||
let folders: Vec<(String, PathBuf, &str, PathBooleanOperation)> = glob("__fixtures__/visual-tests/*/")
|
||||
.expect("Failed to read glob pattern")
|
||||
.flat_map(|entry| {
|
||||
let dir = entry.expect("Failed to get directory entry");
|
||||
ops.iter()
|
||||
.map(move |(op_name, op)| (dir.file_name().unwrap().to_string_lossy().into_owned(), dir.clone(), *op_name, *op))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut failure = false;
|
||||
|
||||
for (name, dir, op_name, op) in folders {
|
||||
let test_name = format!("{} {}", name, op_name);
|
||||
println!("Running test: {}", test_name);
|
||||
|
||||
fs::create_dir_all(dir.join("test-results")).expect("Failed to create test-results directory");
|
||||
|
||||
let original_path = dir.join("original.svg");
|
||||
|
||||
let mut content = String::new();
|
||||
let svg_tree = svg::open(&original_path, &mut content).expect("Failed to parse SVG");
|
||||
|
||||
let mut paths = Vec::new();
|
||||
let mut first_path_attributes = String::new();
|
||||
let mut width = String::new();
|
||||
let mut height = String::new();
|
||||
let mut view_box = String::new();
|
||||
for event in svg_tree {
|
||||
match event {
|
||||
Event::Tag("svg", svg::node::element::tag::Type::Start, attributes) => {
|
||||
width = attributes.get("width").map(|s| s.to_string()).unwrap_or_default();
|
||||
height = attributes.get("height").map(|s| s.to_string()).unwrap_or_default();
|
||||
view_box = attributes.get("viewBox").map(|s| s.to_string()).unwrap_or_default();
|
||||
}
|
||||
Event::Tag("path", svg::node::element::tag::Type::Empty, attributes) => {
|
||||
let data = attributes.get("d").map(|s| s.to_string()).expect("Path data not found");
|
||||
let fill_rule = attributes.get("fill-rule").map(|v| v.to_string()).unwrap_or_else(|| "nonzero".to_string());
|
||||
paths.push((data, fill_rule));
|
||||
|
||||
// Store attributes of the first path
|
||||
if first_path_attributes.is_empty() {
|
||||
for (key, value) in attributes.iter() {
|
||||
if key != "d" && key != "id" {
|
||||
first_path_attributes.push_str(&format!("{}=\"{}\" ", key, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if (width.is_empty() || height.is_empty()) && !view_box.is_empty() {
|
||||
let vb: Vec<&str> = view_box.split_whitespace().collect();
|
||||
if vb.len() == 4 {
|
||||
width = vb[2].to_string();
|
||||
height = vb[3].to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if width.is_empty() || height.is_empty() {
|
||||
panic!("Failed to extract width and height from SVG");
|
||||
}
|
||||
|
||||
let a_node = paths[0].clone();
|
||||
let b_node = paths[1].clone();
|
||||
|
||||
let a = path_from_path_data(&a_node.0);
|
||||
let b = path_from_path_data(&b_node.0);
|
||||
|
||||
let a_fill_rule = get_fill_rule(&a_node.1);
|
||||
let b_fill_rule = get_fill_rule(&b_node.1);
|
||||
|
||||
let result = path_boolean::path_boolean(&a, a_fill_rule, &b, b_fill_rule, op).unwrap();
|
||||
|
||||
// Create the result SVG with correct dimensions
|
||||
let mut result_svg = format!("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" viewBox=\"{}\">", width, height, view_box);
|
||||
for path in &result {
|
||||
result_svg.push_str(&format!("<path d=\"{}\" {}/>", path_to_path_data(path, 1e-4), first_path_attributes));
|
||||
}
|
||||
result_svg.push_str("</svg>");
|
||||
|
||||
// Save the result SVG
|
||||
let destination_path = dir.join("test-results").join(format!("{}-ours.svg", op_name));
|
||||
fs::write(&destination_path, &result_svg).expect("Failed to write result SVG");
|
||||
|
||||
// Render and compare images
|
||||
let ground_truth_path = dir.join(format!("{}.svg", op_name));
|
||||
let ground_truth_svg = fs::read_to_string(&ground_truth_path).expect("Failed to read ground truth SVG");
|
||||
|
||||
let ours_image = render_svg(&result_svg);
|
||||
let ground_truth_image = render_svg(&ground_truth_svg);
|
||||
|
||||
let ours_png_path = dir.join("test-results").join(format!("{}-ours.png", op_name));
|
||||
ours_image.save(&ours_png_path).expect("Failed to save our PNG");
|
||||
|
||||
let ground_truth_png_path = dir.join("test-results").join(format!("{}.png", op_name));
|
||||
ground_truth_image.save(&ground_truth_png_path).expect("Failed to save ground truth PNG");
|
||||
|
||||
failure |= compare_images(&ours_image, &ground_truth_image, TOLERANCE);
|
||||
|
||||
// Check the number of paths
|
||||
let result_path_count = result.len();
|
||||
let ground_truth_path_count = ground_truth_svg.matches("<path").count();
|
||||
if result_path_count != ground_truth_path_count {
|
||||
failure = true;
|
||||
eprintln!("Number of paths doesn't match for test: {}", test_name);
|
||||
}
|
||||
}
|
||||
if failure {
|
||||
panic!("Some tests have failed");
|
||||
}
|
||||
}
|
||||
|
||||
fn render_svg(svg_code: &str) -> DynamicImage {
|
||||
let opts = Options::default();
|
||||
let tree = Tree::from_str(svg_code, &opts).unwrap();
|
||||
let pixmap_size = tree.size();
|
||||
let (width, height) = (pixmap_size.width() as u32, pixmap_size.height() as u32);
|
||||
let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height).unwrap();
|
||||
let mut pixmap_mut = pixmap.as_mut();
|
||||
render(&tree, Transform::default(), &mut pixmap_mut);
|
||||
DynamicImage::ImageRgba8(RgbaImage::from_raw(width, height, pixmap.data().to_vec()).unwrap())
|
||||
}
|
||||
|
||||
fn compare_images(img1: &DynamicImage, img2: &DynamicImage, tolerance: u8) -> bool {
|
||||
assert_eq!(img1.dimensions(), img2.dimensions(), "Image dimensions do not match");
|
||||
|
||||
for (x, y, pixel1) in img1.pixels() {
|
||||
let pixel2 = img2.get_pixel(x, y);
|
||||
for i in 0..4 {
|
||||
let difference = (pixel1[i] as i32 - pixel2[i] as i32).unsigned_abs() as u8;
|
||||
if difference > tolerance {
|
||||
println!("Difference {} larger than tolerance {} at [{}, {}], channel {}.", difference, tolerance, x, y, i);
|
||||
return true;
|
||||
}
|
||||
|
||||
assert!(
|
||||
difference <= tolerance,
|
||||
"Difference {} larger than tolerance {} at [{}, {}], channel {}.",
|
||||
difference,
|
||||
tolerance,
|
||||
x,
|
||||
y,
|
||||
i
|
||||
);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
Reference in New Issue
Block a user