Path Bool library code cleanup (#2000)

* Remove log statements

* Add feature gates to functions in path.rs

* Fix infinite parsing loop and add new test

* License tweaks

* Remove trailing zero in whole number floats

* Flatten visual-tests directory

* Code review

* Clean up printlines

* Add error handling to path parsing

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert 2024-09-23 12:16:31 +02:00 committed by GitHub
parent 3ddc052538
commit 8a1089938e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
175 changed files with 442 additions and 346 deletions

View file

@ -33,7 +33,7 @@ graphene-core = { path = "node-graph/gcore" }
graph-craft = { path = "node-graph/graph-craft", features = ["serde"] }
wgpu-executor = { path = "node-graph/wgpu-executor" }
bezier-rs = { path = "libraries/bezier-rs", features = ["dyn-any"] }
path-bool = { path = "libraries/path-bool", features = ["parsing"] }
path-bool = { path = "libraries/path-bool", default-features = false }
node-macro = { path = "node-graph/node-macro" }
# Workspace dependencies

View file

@ -1926,10 +1926,10 @@ impl NodeGraphMessageHandler {
// }
let curve_length = 24.;
let curve_falloff_rate = curve_length * std::f64::consts::PI * 2.0;
let curve_falloff_rate = curve_length * std::f64::consts::PI * 2.;
let horizontal_curve_amount = -(2.0f64.powf((-10. * horizontal_gap) / curve_falloff_rate)) + 1.;
let vertical_curve_amount = -(2.0f64.powf((-10. * vertical_gap) / curve_falloff_rate)) + 1.;
let horizontal_curve_amount = -(2_f64.powf((-10. * horizontal_gap) / curve_falloff_rate)) + 1.;
let vertical_curve_amount = -(2_f64.powf((-10. * vertical_gap) / curve_falloff_rate)) + 1.;
let horizontal_curve = horizontal_curve_amount * curve_length;
let vertical_curve = vertical_curve_amount * curve_length;

View file

@ -160,7 +160,7 @@ fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context
let max_x = bounds.0.iter().map(|&corner| corner.x).max_by(cmp).unwrap_or_default();
let spacing_x = isometric_spacing.x;
let tan = tan_a;
let multiply = -1.0;
let multiply = -1.;
let project = |corner: &DVec2| corner.y + multiply * tan * (corner.x - origin.x);
let inverse_project = |corner: &DVec2| corner.y - tan * multiply * (corner.x - origin.x);
let min_y = bounds.0.into_iter().min_by(|a, b| inverse_project(a).partial_cmp(&inverse_project(b)).unwrap()).unwrap_or_default();

View file

@ -2285,7 +2285,7 @@ impl NodeNetworkInterface {
let chain_width_grid_spaces = self.chain_width(node_id, network_path);
let node_bottom_right = node_top_left + DVec2::new(width as f64, height as f64);
let chain_top_left = node_top_left - DVec2::new((chain_width_grid_spaces * crate::consts::GRID_SIZE) as f64, 0.0);
let chain_top_left = node_top_left - DVec2::new((chain_width_grid_spaces * crate::consts::GRID_SIZE) as f64, 0.);
let radius = 10.;
let subpath = bezier_rs::Subpath::new_rounded_rect(chain_top_left, node_bottom_right, [radius; 4]);
let node_click_target = ClickTarget::new(subpath, 0.);
@ -5083,7 +5083,7 @@ impl Ports {
fn insert_layer_output(&mut self, node_top_left: DVec2) {
// The center of the click target is always 24 px down from the top left corner of the node
let center = node_top_left + DVec2::new(2. * 24., -8.0);
let center = node_top_left + DVec2::new(2. * 24., -8.);
self.insert_output_port_at_center(0, center);
}

View file

@ -357,7 +357,7 @@ mod tests {
let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.);
assert_eq!(bezier2.project(DVec2::new(100., 0.)), 0.);
let bezier3 = Bezier::from_cubic_coordinates(-50.0, -50.0, -50.0, -50.0, 50.0, -50.0, 50.0, -50.0);
let bezier3 = Bezier::from_cubic_coordinates(-50., -50., -50., -50., 50., -50., 50., -50.);
assert_eq!(DVec2::new(0., -50.), bezier3.evaluate(TValue::Parametric(bezier3.project(DVec2::new(0., -50.)))));
}
}

View file

@ -513,7 +513,7 @@ mod tests {
let subpath: Subpath<EmptyId> = Subpath::from_bezier(&Bezier::from_quadratic_dvec2(start, handle, end));
assert_eq!(subpath.evaluate(SubpathTValue::GlobalEuclidean(0.0)), start);
assert_eq!(subpath.evaluate(SubpathTValue::GlobalEuclidean(1.0)), end);
assert_eq!(subpath.evaluate(SubpathTValue::GlobalEuclidean(0.)), start);
assert_eq!(subpath.evaluate(SubpathTValue::GlobalEuclidean(1.)), end);
}
}

View file

@ -607,7 +607,7 @@ mod tests {
)
.all());
let t4 = 1.0;
let t4 = 1.;
assert!(utils::dvec2_compare(
subpath.evaluate(SubpathTValue::GlobalParametric(t4)),
quadratic_bezier.evaluate(TValue::Parametric(1.)),

View file

@ -357,7 +357,7 @@ impl Bezier1d {
fn value_at(&self, t: f64) -> f64 {
let bz = self;
let order = bz.len() - 1;
let u = 1.0 - t;
let u = 1. - t;
let mut bc = 1.;
let mut tn = 1.;
let mut tmp = bz[0] * u;
@ -611,7 +611,7 @@ mod tests {
#[test]
fn find_bernstein_roots() {
let bz = Bezier1d(vec![50.0, -100.0, 170.0]);
let bz = Bezier1d(vec![50., -100., 170.]);
let mut solutions = Vec::new();
bz.find_bernstein_roots(&mut solutions, 0, 0., 1.);

View file

@ -1,2 +1,2 @@
/target
test-results
/target/
test-results/

View file

@ -2,13 +2,22 @@
name = "path-bool"
version = "0.1.0"
rust-version = "1.81"
authors = ["Graphite Authors <contact@graphite.rs>"]
authors = ["Graphite Authors <contact@graphite.rs>", "Adam Platkevič"]
edition = "2021"
keywords = ["bezier", "boolean", "path", "ops", "operations", "2d"]
keywords = [
"bezier",
"curve",
"boolean",
"path",
"geometry",
"computational geometry",
"vector graphics",
"2d",
"graphics",
]
categories = ["graphics", "mathematics"]
license = "MIT OR Apache-2.0"
[features]
logging = ["parsing"]
parsing = []
@ -26,13 +35,12 @@ resvg = "0.42"
image = "0.24"
# Required dependencies
criterion = { version = "0.5", features = ["html_reports"]}
criterion = { version = "0.5", features = ["html_reports"] }
# Benchmarks
[[bench]]
name = "painted_dreams"
harness = false
[[bench]]
name = "path_segment_intersection"
harness = false

View file

@ -1,6 +1,15 @@
NOTICE
This project includes software originally developed by Adam Platkevič.
The original project is licensed under the MIT License.
Rust port and modifications are (c) 2024 Graphite Authors.
This library is derived from software originally developed by Adam Platkevič which is licensed under the MIT License reproduced below:
MIT License
Copyright (c) 2024 Adam Platkevič
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -28,8 +28,8 @@ Here's a basic example of performing an intersection operation on two paths:
use path_bool::{path_boolean, FillRule, PathBooleanOperation, path_from_path_data, path_to_path_data};
fn main() {
let path_a = path_from_path_data("M 10 10 L 50 10 L 30 40 Z");
let path_b = path_from_path_data("M 20 30 L 60 30 L 60 50 L 20 50 Z");
let path_a = path_from_path_data("M 10 10 L 50 10 L 30 40 Z").unwrap();
let path_b = path_from_path_data("M 20 30 L 60 30 L 60 50 L 20 50 Z").unwrap();
let result = path_boolean(
&path_a,
@ -50,13 +50,14 @@ The boolean operations are implemented using a graph-based approach. After the p
## Development status
This project is a port of PathBool.js and is still in early stages of development. Contributions, bug reports, and feedback are welcome.
This project is a port of PathBool.js which is still in early stages of development. Contributions, bug reports, and feedback are welcome.
Future work includes:
- Comprehensive test suite
- Performance optimizations
- Additional examples and documentation
- Support for path builder tool features
## License and acknowledgements

View file

@ -1,5 +0,0 @@
for dir in */; do
for fn in difference division exclusion fracture intersection union; do
cp "${dir}test-results/$fn-ours.svg" "$dir$fn.svg"
done
done

View file

@ -1,10 +0,0 @@
INKSCAPE_CMD=inkscape
OPS=(union difference intersection exclusion division fracture)
for dir in */; do
for op in "${OPS[@]}"; do
if [ ! -e "$dir/$op.svg" ]; then
$INKSCAPE_CMD --actions="select-all; path-$op; export-filename:$dir/$op.svg; export-plain-svg; export-do; file-close" "$dir/original.svg"
fi
done
done

View file

@ -2,10 +2,11 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion};
use path_bool::*;
pub fn criterion_benchmark(c: &mut Criterion) {
let path_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 path_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").unwrap();
let path_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",
);
"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",
).unwrap();
c.bench_function("painted_dreams_diff", |b| {
b.iter(|| path_boolean(black_box(&path_a), FillRule::NonZero, black_box(&path_b), FillRule::NonZero, PathBooleanOperation::Difference))
});

View file

@ -15,15 +15,15 @@ fn a() -> PathSegment {
DVec2::new(458.37027, 572.165771),
DVec2::new(428.525848, 486.720093),
DVec2::new(368.618805, 467.485992),
DVec2::new(273.0, 476.0),
DVec2::new(273., 476.),
)
}
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))
PathSegment::Cubic(DVec2::new(273., 476.), DVec2::new(419., 463.), DVec2::new(481.741198, 514.692273), DVec2::new(481.333333, 768.))
}
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))
PathSegment::Cubic(DVec2::new(273., 476.), DVec2::new(107.564178, 490.730591), DVec2::new(161.737915, 383.575775), DVec2::new(0., 340.))
}
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))
PathSegment::Cubic(DVec2::new(0., 340.), DVec2::new(161.737914, 383.575765), DVec2::new(107.564182, 490.730587), DVec2::new(273., 476.))
}

View file

@ -23,7 +23,7 @@
};
buildInputs = with pkgs; [
llvm
];
];
in {
devShells.default = pkgs.mkShell {
stdenv = pkgs.clangStdenv;
@ -38,8 +38,8 @@
toolchain
llvm
cargo
];
inherit buildInputs;
];
inherit buildInputs;
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
};

View file

@ -1,12 +1,11 @@
#![expect(clippy::needless_doctest_main)]
#![doc = include_str!("../README.md")]
mod path_boolean;
#[cfg(feature = "parsing")]
// #[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;
@ -30,16 +29,14 @@ 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 crate::path_boolean::{self, FillRule, PathBooleanOperation};
use crate::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 a = path_from_path_data("M 10 10 L 50 10 L 30 40 Z").unwrap();
let b = path_from_path_data("M 20 30 L 60 30 L 60 50 L 20 50 Z").unwrap();
let union = path_boolean(
&a,
path_boolean::FillRule::NonZero,
@ -50,23 +47,23 @@ mod test {
.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 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").unwrap();
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",
);
)
.unwrap();
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 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").unwrap();
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").unwrap();
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();
@ -77,8 +74,8 @@ mod test {
}
#[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 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").unwrap();
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 ").unwrap();
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();
@ -92,8 +89,8 @@ mod test {
#[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");
).unwrap();
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").unwrap();
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();
@ -105,8 +102,8 @@ mod test {
}
#[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 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").unwrap();
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").unwrap();
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();
@ -118,10 +115,10 @@ mod test {
}
#[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 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").unwrap();
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",
);
).unwrap();
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Intersection).unwrap();
@ -133,10 +130,10 @@ mod test {
}
#[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 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").unwrap();
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",
);
).unwrap();
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Intersection).unwrap();
@ -148,10 +145,11 @@ mod test {
}
#[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 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 ")
.unwrap();
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",
);
).unwrap();
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();
@ -163,10 +161,10 @@ mod test {
}
#[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 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 ").unwrap();
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 ",
);
).unwrap();
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Difference).unwrap();
@ -178,10 +176,10 @@ mod test {
}
#[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 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 ").unwrap();
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",
);
).unwrap();
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Difference).unwrap();
@ -194,10 +192,10 @@ mod test {
#[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"
);
).unwrap();
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",
);
).unwrap();
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Intersection).unwrap();
@ -211,10 +209,10 @@ mod 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"
);
).unwrap();
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",
);
).unwrap();
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Intersection).unwrap();
@ -228,10 +226,10 @@ mod 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"
);
).unwrap();
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",
);
).unwrap();
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Difference).unwrap();
@ -247,8 +245,26 @@ mod test {
"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("");
).unwrap();
let b = path_from_path_data("").unwrap();
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 shared_line() {
let a = path_from_path_data(
"m 658.03348,118.4966 c 7.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.81928 Z"
).unwrap();
let b = path_from_path_data(
"m 658.03348,115.88649 c 40.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.83037 z",
)
.unwrap();
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();

View file

@ -1,9 +1,10 @@
use crate::path::{path_from_commands, path_to_commands, Path};
use crate::path_command::{AbsolutePathCommand, PathCommand, RelativePathCommand};
use crate::BooleanError;
use glam::DVec2;
use regex::Regex;
pub fn commands_from_path_data(d: &str) -> Vec<PathCommand> {
pub fn commands_from_path_data(d: &str) -> Result<Vec<PathCommand>, BooleanError> {
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();
@ -24,6 +25,7 @@ pub fn commands_from_path_data(d: &str) -> Vec<PathCommand> {
match last_cmd {
'M' => Some('L'),
'm' => Some('l'),
'z' | 'Z' => None,
_ => Some(last_cmd),
}
}
@ -111,15 +113,15 @@ pub fn commands_from_path_data(d: &str) -> Vec<PathCommand> {
get_float(&mut i),
get_float(&mut i),
))),
_ => panic!("Invalid command: {}", cmd),
_ => return Err(BooleanError::InvalidPathCommand(cmd)),
}
}
commands
Ok(commands)
}
pub fn path_from_path_data(d: &str) -> Path {
path_from_commands(commands_from_path_data(d)).collect()
pub fn path_from_path_data(d: &str) -> Result<Path, BooleanError> {
Ok(path_from_commands(commands_from_path_data(d)?).collect())
}
pub fn path_to_path_data(path: &Path, eps: f64) -> String {

View file

@ -13,9 +13,10 @@ 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
point * 2. - control_point
}
#[cfg(feature = "parsing")]
pub fn path_from_commands<I>(commands: I) -> impl Iterator<Item = PathSegment>
where
I: IntoIterator<Item = PathCommand>,
@ -93,6 +94,7 @@ where
})
}
#[cfg(feature = "parsing")]
pub fn path_to_commands<'a, I>(segments: I, eps: f64) -> impl Iterator<Item = PathCommand> + 'a
where
I: IntoIterator<Item = &'a PathSegment> + 'a,

View file

@ -1,5 +1,3 @@
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};
@ -7,6 +5,8 @@ use crate::line_segment_aabb::line_segment_aabb_intersect;
use crate::math::lerp;
use crate::path_segment::PathSegment;
use glam::DVec2;
#[derive(Clone)]
struct IntersectionSegment {
seg: PathSegment,
@ -18,7 +18,7 @@ struct IntersectionSegment {
#[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;
let mid_param = (int_seg.start_param + int_seg.end_param) / 2.;
[
IntersectionSegment {
seg: seg0,
@ -88,10 +88,9 @@ pub fn segments_equal(seg0: &PathSegment, seg1: &PathSegment, point_epsilon: f64
}
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) {
if !endpoints && (st.0 < eps.param || st.0 > 1. - eps.param) && (st.1 < eps.param || st.1 > 1. - eps.param) {
return vec![];
}
return vec![st.into()];
@ -103,14 +102,14 @@ pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoin
let mut pairs = vec![(
IntersectionSegment {
seg: *seg0,
start_param: 0.0,
end_param: 1.0,
start_param: 0.,
end_param: 1.,
bounding_box: seg0.bounding_box(),
},
IntersectionSegment {
seg: *seg1,
start_param: 0.0,
end_param: 1.0,
start_param: 0.,
end_param: 1.,
bounding_box: seg1.bounding_box(),
},
)];
@ -120,13 +119,12 @@ pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoin
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.
// Check if start and end points are on the other bezier curves. If so, add an 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);
}
@ -143,7 +141,6 @@ pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoin
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 {
@ -174,7 +171,7 @@ pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoin
}
if !endpoints {
params.retain(|[s, t]| (s > &eps.param && s < &(1.0 - eps.param)) || (t > &eps.param && t < &(1.0 - eps.param)));
params.retain(|[s, t]| (s > &eps.param && s < &(1. - eps.param)) || (t > &eps.param && t < &(1. - eps.param)));
}
params
@ -190,22 +187,22 @@ fn calculate_overlap_intersections(seg0: &PathSegment, seg1: &PathSegment, eps:
// Check start0 against seg1
if let Some(t1) = find_point_on_segment(seg1, start0, eps) {
intersections.push([0.0, t1]);
intersections.push([0., t1]);
}
// Check end0 against seg1
if let Some(t1) = find_point_on_segment(seg1, end0, eps) {
intersections.push([1.0, t1]);
intersections.push([1., t1]);
}
// Check start1 against seg0
if let Some(t0) = find_point_on_segment(seg0, start1, eps) {
intersections.push([t0, 0.0]);
intersections.push([t0, 0.]);
}
// Check end1 against seg0
if let Some(t0) = find_point_on_segment(seg0, end1, eps) {
intersections.push([t0, 1.0]);
intersections.push([t0, 1.]);
}
// Remove duplicates and sort intersections
@ -216,7 +213,7 @@ fn calculate_overlap_intersections(seg0: &PathSegment, seg1: &PathSegment, eps:
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]];
return vec![[0., 0.], [1., 1.]];
}
} else if intersections.len() > 2 {
// Keep only the first and last intersection points
@ -227,8 +224,8 @@ fn calculate_overlap_intersections(seg0: &PathSegment, seg1: &PathSegment, eps:
}
fn find_point_on_segment(seg: &PathSegment, point: DVec2, eps: &Epsilons) -> Option<f64> {
let start = 0.0;
let end = 1.0;
let start = 0.;
let end = 1.;
let mut t = 0.5;
for _ in 0..32 {
@ -251,9 +248,9 @@ fn find_point_on_segment(seg: &PathSegment, point: DVec2, eps: &Epsilons) -> Opt
}
if dist_start < dist_end {
t = (start + t) / 2.0;
t = (start + t) / 2.;
} else {
t = (t + end) / 2.0;
t = (t + end) / 2.;
}
if (end - start) < eps.param {
@ -283,16 +280,16 @@ mod test {
DVec2::new(458.37027, 572.165771),
DVec2::new(428.525848, 486.720093),
DVec2::new(368.618805, 467.485992),
DVec2::new(273.0, 476.0),
DVec2::new(273., 476.),
)
}
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))
PathSegment::Cubic(DVec2::new(273., 476.), DVec2::new(419., 463.), DVec2::new(481.741198, 514.692273), DVec2::new(481.333333, 768.))
}
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))
PathSegment::Cubic(DVec2::new(273., 476.), DVec2::new(107.564178, 490.730591), DVec2::new(161.737915, 383.575775), DVec2::new(0., 340.))
}
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))
PathSegment::Cubic(DVec2::new(0., 340.), DVec2::new(161.737914, 383.575765), DVec2::new(107.564182, 490.730587), DVec2::new(273., 476.))
}
}

View file

@ -2,7 +2,7 @@ use glam::DVec2;
pub type LineSegment = [DVec2; 2];
const COLLINEAR_EPS: f64 = f64::EPSILON * 64.0;
const COLLINEAR_EPS: f64 = f64::EPSILON * 64.;
#[inline(never)]
pub fn line_segment_intersection([p1, p2]: LineSegment, [p3, p4]: LineSegment, eps: f64) -> Option<(f64, f64)> {
@ -21,7 +21,7 @@ pub fn line_segment_intersection([p1, p2]: LineSegment, [p3, p4]: LineSegment, e
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) {
if (-eps..=1. + eps).contains(&s) && (-eps..=1. + eps).contains(&t) {
Some((s, t))
} else {
None

View file

@ -42,8 +42,8 @@ pub(crate) fn line_segment_aabb_intersect(seg: LineSegment, bounding_box: &Aabb)
} 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;
let mut x = 0.;
let mut y = 0.;
// At least one endpoint is outside the clip rectangle; pick it.
let outcode_out = if outcode1 > outcode0 { outcode1 } else { outcode0 };

View file

@ -6,27 +6,27 @@ 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 ax = -p1.x + 3. * p2.x - 3. * p3.x + p4.x;
let ay = -p1.y + 3. * p2.y - 3. * p3.y + p4.y;
let bx = 3. * p1.x - 6. * p2.x + 3. * p3.x;
let by = 3. * p1.y - 6. * p2.y + 3. * p3.y;
let cx = -3. * p1.x + 3. * p2.x;
let cy = -3. * p1.y + 3. * 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);
let k = (-3. * ax * ax * cy * cy + 6. * ax * ay * cx * cy + 4. * ax * bx * by * cy - 4. * ax * by * by * cx - 3. * ay * ay * cx * cx - 4. * ay * bx * bx * cy + 4. * ay * bx * by * cx)
/ (ax * ax * by * by - 2. * ax * ay * bx * by + ay * ay * bx * bx);
if k < 0.0 {
if k < 0. {
return None;
}
let t1 = (n / m + k.sqrt()) / 2.0;
let t2 = (n / m - k.sqrt()) / 2.0;
let t1 = (n / m + k.sqrt()) / 2.;
let t2 = (n / m - k.sqrt()) / 2.;
if (EPS..=1.0 - EPS).contains(&t1) && (EPS..=1.0 - EPS).contains(&t2) {
if (EPS..=1. - EPS).contains(&t1) && (EPS..=1. - EPS).contains(&t2) {
let mut result = [t1, t2];
result.sort_by(|a, b| a.partial_cmp(b).unwrap());
Some(result)

View file

@ -34,7 +34,7 @@ use crate::EPS;
/// use path_bool::PathSegment;
/// use glam::DVec2;
///
/// let line = PathSegment::Line(DVec2::new(0.0, 0.0), DVec2::new(1.0, 1.0));
/// let line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(1., 1.));
/// ```
///
/// Creating a cubic Bézier curve:
@ -43,10 +43,10 @@ use crate::EPS;
/// 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)
/// DVec2::new(0., 0.),
/// DVec2::new(1., 0.),
/// DVec2::new(1., 1.),
/// DVec2::new(2., 1.)
/// );
/// ```
#[derive(Clone, Copy, Debug, PartialEq)]
@ -95,7 +95,7 @@ impl 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));
/// let line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(1., 1.));
/// assert_eq!(line.start_angle(), TAU - (FRAC_PI_4));
/// ```
pub fn start_angle(&self) -> f64 {
@ -104,7 +104,7 @@ impl PathSegment {
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
// if this diff were empty too, the segments would have been converted to a line
(control2 - start).angle_to(DVec2::X)
} else {
diff.angle_to(DVec2::X)
@ -134,43 +134,42 @@ impl PathSegment {
/// 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 line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(1., 1.));
/// assert_eq!(line.start_curvature(), 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)
/// DVec2::new(0., 0.),
/// DVec2::new(0., 1.),
/// DVec2::new(1., 1.),
/// DVec2::new(1., 0.)
/// );
/// assert!(curve.start_curvature() < 0.0);
/// assert!(curve.start_curvature() < 0.);
/// ```
pub fn start_curvature(&self) -> f64 {
match *self {
PathSegment::Line(_, _) => 0.0,
PathSegment::Line(_, _) => 0.,
PathSegment::Cubic(start, control1, control2, _) => {
let a = control1 - start;
let a = 3. * a;
let b = start - 2.0 * control1 + control2;
let b = start - 2. * 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
if denominator == 0. {
0.
} else {
numerator / denominator
}
}
PathSegment::Quadratic(start, control, end) => {
// first derivatiave
// First derivative
let a = 2. * (control - start);
// second derivatiave
let b = 2. * (start - 2.0 * control + end);
// Second derivative
let b = 2. * (start - 2. * 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
if denominator == 0. {
0.
} else {
numerator / denominator
}
@ -196,10 +195,10 @@ impl PathSegment {
/// use path_bool::PathSegment;
/// use glam::DVec2;
///
/// let line = PathSegment::Line(DVec2::new(0.0, 0.0), DVec2::new(1.0, 1.0));
/// let line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(1., 1.));
/// 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));
/// assert_eq!(cubic[0], DVec2::new(0., 0.));
/// assert_eq!(cubic[3], DVec2::new(1., 1.));
/// ```
///
/// # Panics
@ -257,10 +256,10 @@ impl PathSegment {
/// use path_bool::PathSegment;
/// use glam::DVec2;
///
/// let line = PathSegment::Line(DVec2::new(0.0, 0.0), DVec2::new(1.0, 1.0));
/// let line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(1., 1.));
/// let reversed = line.reverse();
/// assert_eq!(reversed.start(), DVec2::new(1.0, 1.0));
/// assert_eq!(reversed.end(), DVec2::new(0.0, 0.0));
/// assert_eq!(reversed.start(), DVec2::new(1., 1.));
/// assert_eq!(reversed.end(), DVec2::new(0., 0.));
/// ```
pub fn reverse(&self) -> PathSegment {
match *self {
@ -283,7 +282,7 @@ impl PathSegment {
/// 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 {
if rx == 0. || ry == 0. {
return None;
}
@ -298,7 +297,7 @@ impl PathSegment {
let mut rx = rx.abs();
let mut ry = ry.abs();
let lambda = x1_prime2 / rx2 + y1_prime2 / ry2 + 1e-12;
if lambda > 1.0 {
if lambda > 1. {
let lambda_sqrt = lambda.sqrt();
rx *= lambda_sqrt;
ry *= lambda_sqrt;
@ -307,7 +306,7 @@ impl PathSegment {
ry2 *= lambda_abs;
}
let sign = if fa == fs { -1.0 } else { 1.0 };
let sign = if fa == fs { -1. } else { 1. };
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);
@ -315,12 +314,12 @@ impl PathSegment {
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 theta1 = vector_angle(DVec2::new(1., 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 {
if !fs && delta_theta > 0. {
delta_theta -= TAU;
} else if fs && delta_theta < 0.0 {
} else if fs && delta_theta < 0. {
delta_theta += TAU;
}
@ -342,7 +341,7 @@ impl PathSegment {
///
/// # Arguments
///
/// * `t` - A value between 0.0 and 1.0 representing the position along the segment.
/// * `t` - A value between 0. and 1. representing the position along the segment.
///
/// # Examples
///
@ -350,8 +349,8 @@ impl PathSegment {
/// 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));
/// let line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(2., 2.));
/// assert_eq!(line.sample_at(0.5), DVec2::new(1., 1.));
/// ```
pub fn sample_at(&self, t: f64) -> DVec2 {
match *self {
@ -405,22 +404,22 @@ impl PathSegment {
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 k = (4. / 3.) * (theta / 4.).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 start = DVec2::new(1., 0.);
let control1 = DVec2::new(1., 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();
let start = (matrix * start.extend(1.)).truncate();
let control1 = (matrix * control1.extend(1.)).truncate();
let control2 = (matrix * control2.extend(1.)).truncate();
let end = (matrix * end.extend(1.)).truncate();
PathSegment::Cubic(start, control1, control2, end)
})
@ -466,7 +465,7 @@ impl PathArcSegmentCenterParametrization {
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;
let fs = self.delta_theta > 0.;
xy1 = start.unwrap_or(xy1);
xy2 = end.unwrap_or(xy2);
@ -509,27 +508,27 @@ 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;
let a = 3. * (-p0 + 3. * p1 - 3. * p2 + p3);
let b = 6. * (p0 - 2. * p1 + p2);
let c = 3. * (p1 - p0);
let d = b * b - 4. * a * c;
if d < 0.0 || a == 0.0 {
if d < 0. || a == 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 t0 = (-b - sqrt_d) / (2. * a);
if 0. < t0 && t0 < 1. {
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 t1 = (-b + sqrt_d) / (2. * a);
if 0. < t1 && t1 < 1. {
let x1 = eval_cubic_1d(p0, p1, p2, p3, t1);
min = min.min(x1);
max = max.max(x1);
@ -570,14 +569,14 @@ 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;
let denominator = p0 - 2. * p1 + p2;
if denominator == 0.0 {
if denominator == 0. {
return (min, max);
}
let t = (p0 - p1) / denominator;
if (0.0..=1.0).contains(&t) {
if (0.0..=1.).contains(&t) {
let x = eval_quadratic_1d(p0, p1, p2, t);
min = min.min(x);
max = max.max(x);
@ -595,7 +594,7 @@ impl PathSegment {
///
/// # Returns
///
/// An `AaBb` representing the axis-aligned bounding box of the segment.
/// 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 {
@ -617,34 +616,34 @@ impl PathSegment {
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);
let mut bounding_box = extend_bounding_box(Some(bounding_box_around_point(start, 0.)), end);
if phi == 0.0 || rx == ry {
// FIXME: the following gives false positives, resulting in larger boxes
if phi == 0. || rx == ry {
// TODO: Fix the fact that 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) {
if in_interval(-PI / 2., center_param.theta1, theta2) || in_interval(3. * PI / 2., 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) {
if in_interval(0., center_param.theta1, theta2) || in_interval(2. * 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) {
if in_interval(PI / 2., center_param.theta1, theta2) || in_interval(5. * PI / 2., 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
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);
// TODO: Don't convert to cubics
let cubics = self.arc_segment_to_cubics(PI / 16.);
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))
bounding_box.unwrap_or_else(|| bounding_box_around_point(start, 0.))
}
} else {
extend_bounding_box(Some(bounding_box_around_point(start, 0.0)), end)
extend_bounding_box(Some(bounding_box_around_point(start, 0.)), end)
}
}
}
@ -654,7 +653,7 @@ impl PathSegment {
///
/// # Arguments
///
/// * `t` - A value between 0.0 and 1.0 representing the split point along the segment.
/// * `t` - A value between 0. and 1. representing the split point along the segment.
///
/// # Returns
///
@ -666,10 +665,10 @@ impl PathSegment {
/// use path_bool::PathSegment;
/// use glam::DVec2;
///
/// let line = PathSegment::Line(DVec2::new(0.0, 0.0), DVec2::new(2.0, 2.0));
/// let line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(2., 2.));
/// 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));
/// assert_eq!(first_half.end(), DVec2::new(1., 1.));
/// assert_eq!(second_half.start(), DVec2::new(1., 1.));
/// ```
pub fn split_at(&self, t: f64) -> (PathSegment, PathSegment) {
match *self {

View file

@ -51,8 +51,6 @@
//! This approach allows for efficient and accurate boolean operations, even on
//! complex paths with many intersections or self-intersections.
use slotmap::{new_key_type, SlotMap};
new_key_type! {
pub struct MajorVertexKey;
pub struct MajorEdgeKey;
@ -74,9 +72,12 @@ use crate::path_segment::PathSegment;
#[cfg(feature = "logging")]
use crate::path_to_path_data;
use crate::quad_tree::QuadTree;
use glam::DVec2;
use slotmap::{new_key_type, SlotMap};
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt::Display;
/// Represents the types of boolean operations that can be performed on paths.
#[derive(Debug, Clone, Copy)]
@ -183,6 +184,7 @@ impl MinorGraphEdge {
}
}
}
// Compares Segments based on their derivative at the start. If the derivative
// is equal, check the curvature instead. This should correctly sort most instances.
fn compare_segments(a: &PathSegment, b: &PathSegment) -> Ordering {
@ -368,7 +370,7 @@ fn split_at_self_intersections(edges: &mut Vec<MajorGraphEdgeStage1>) {
new_edges.push((seg2, *parent));
} else {
let (seg1, tmp_seg) = seg.split_at(t1);
let (seg2, seg3) = &tmp_seg.split_at((t2 - t1) / (1.0 - t1));
let (seg2, seg3) = &tmp_seg.split_at((t2 - t1) / (1. - t1));
*seg = seg1;
new_edges.push((*seg2, *parent));
new_edges.push((*seg3, *parent));
@ -447,17 +449,17 @@ fn split_at_intersections(edges: &[MajorGraphEdgeStage1]) -> (Vec<MajorGraphEdge
let mut splits = splits.clone();
splits.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mut tmp_seg = seg;
let mut prev_t = 0.0;
let mut prev_t = 0.;
for &t in splits.iter() {
if t > 1.0 - EPS.param {
if t > 1. - EPS.param {
break;
}
let tt = (t - prev_t) / (1.0 - prev_t);
let tt = (t - prev_t) / (1. - prev_t);
prev_t = t;
if tt < EPS.param {
continue;
}
if tt > 1.0 - EPS.param {
if tt > 1. - EPS.param {
continue;
}
let (seg1, seg2) = tmp_seg.split_at(tt);
@ -651,7 +653,7 @@ fn compute_minor(major_graph: &MajorGraph) -> MinorGraph {
segments.push(edge.seg);
visited.insert(edge.incident_vertices[1]);
let next_vertex = &major_graph.vertices[edge.incident_vertices[1]];
// choose the edge which is not our twin so we can make progress
// Choose the edge which is not our twin so we can make progress
edge_key = *next_vertex.outgoing_edges.iter().find(|&&e| Some(e) != edge.twin).unwrap();
edge = &major_graph.edges[edge_key];
}
@ -718,7 +720,7 @@ fn compute_minor(major_graph: &MajorGraph) -> MinorGraph {
}
fn remove_dangling_edges(graph: &mut MinorGraph) {
// Basically DFS for each parent with bfs number
// Basically DFS for each parent with BFS number
fn walk(parent: u8, graph: &MinorGraph) -> HashSet<MinorVertexKey> {
let mut kept_vertices = HashSet::new();
let mut vertex_to_level = HashMap::new();
@ -780,36 +782,10 @@ fn remove_dangling_edges(graph: &mut MinorGraph) {
});
}
fn get_incidence_angle(edge: &MinorGraphEdge) -> f64 {
let seg = &edge.segments[0]; // TODO: explain in comment why this is always the incident one in both fwd and bwd
// eprintln!("{edge:?}"); //, edge.direction_flag.forward());
let (p0, p1) = if edge.direction_flag.forward() {
(seg.sample_at(0.0), seg.sample_at(EPS.param))
} else {
(seg.sample_at(1.0), seg.sample_at(1.0 - EPS.param))
};
// eprintln!("{p0:?} {p1:?}");
let angle = (p1.y - p0.y).atan2(p1.x - p0.x);
// eprintln!("angle: {}", angle);
(angle * 10000.).round() / 1000.
}
fn sort_outgoing_edges_by_angle(graph: &mut MinorGraph) {
for (vertex_key, vertex) in graph.vertices.iter_mut() {
if vertex.outgoing_edges.len() > 2 {
vertex.outgoing_edges.sort_by(|&a, &b| {
// TODO(@TrueDoctor): Make more robust. The js version seems to sort the data slightly differently when the angles are reallly close. In that case put the edge wich was discovered later first.
let new = graph.edges[a].partial_cmp(&graph.edges[b]).unwrap();
let old = (get_incidence_angle(&graph.edges[a]) - (a.0.as_ffi() & 0xFFFFFF) as f64 / 1000000.)
.partial_cmp(&(get_incidence_angle(&graph.edges[b]) - (b.0.as_ffi() & 0xFFFFFF) as f64 / 1000000.))
.unwrap_or(b.cmp(&a));
if new != old {
// dbg!(new, old, a, b);
}
new
});
vertex.outgoing_edges.sort_by(|&a, &b| graph.edges[a].partial_cmp(&graph.edges[b]).unwrap());
if cfg!(feature = "logging") {
eprintln!("Outgoing edges for {:?}:", vertex_key);
for &edge_key in &vertex.outgoing_edges {
@ -824,18 +800,15 @@ fn sort_outgoing_edges_by_angle(graph: &mut MinorGraph) {
fn face_to_polygon(face: &DualGraphVertex, edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>) -> Vec<DVec2> {
const CNT: usize = 3;
// #[cfg(feature = "logging")]
// eprintln!("incident node counts {}", face.incident_edges.len());
face.incident_edges
.iter()
.flat_map(|&edge_key| {
let edge = &edges[edge_key];
// eprintln!("{}", path_to_path_data(&edge.segments, 0.001));
edge.segments.iter().flat_map(move |seg| {
(0..CNT).map(move |i| {
let t0 = i as f64 / CNT as f64;
let t = if edge.direction_flag.forward() { t0 } else { 1.0 - t0 };
let t = if edge.direction_flag.forward() { t0 } else { 1. - t0 };
seg.sample_at(t)
})
})
@ -908,18 +881,11 @@ fn compute_signed_area(face: &DualGraphVertex, edges: &SlotMap<DualEdgeKey, Dual
let b = polygon[(i + 1) % polygon.len()];
area += a.x * b.y;
area -= b.x * a.y;
// let center = (a + b + c) / 3.;
// let winding = compute_point_winding(&polygon, center);
// if winding != 0 {
// return (winding, center);
// }
}
#[cfg(feature = "logging")]
eprintln!("winding: {}", area);
area
// panic!("No ear in polygon found.");
}
/// Computes the dual graph from the minor graph.
@ -1030,16 +996,17 @@ fn compute_dual(minor_graph: &MinorGraph) -> Result<DualGraph, BooleanError> {
let mut visited_edges = HashSet::new();
if cfg!(feature = "logging") {
// eeprintln!("minor_to_dual: {:#?}", minor_to_dual_edge);
eprintln!("faces: {}, dual-edges: {}, cycles: {}", new_vertices.len(), dual_edges.len(), minor_graph.cycles.len())
}
// This can be very useful for debugging:
// Copy the face outlines to a file called faces_combined.csv and then use this
// gnuplot command:
// `plot 'faces_combined.csv' i 0:99 w l, 'faces_combined.csv' index 0 w l lc 'red'`
// the first part of the command plots all faces to the graph and the second comand
// plots one surface, specifed by the index, in red. This allows you to check if all surfaces are closed paths and can be used in conjunction with the flag debugging to identify issues later down the line as well
// Copy the face outlines to a file called faces_combined.csv and then use this gnuplot command:
// ```
// plot 'faces_combined.csv' i 0:99 w l, 'faces_combined.csv' index 0 w l lc 'red'
// ```
// The first part of the command plots all faces to the graph and the second comand plots one surface,
// specified by the index, in red. This allows you to check if all surfaces are closed paths and can
// be used in conjunction with the flag debugging to identify issues later down the line as well.
#[cfg(feature = "logging")]
for (vertex_key, vertex) in &dual_vertices {
eprintln!("\n\n#{:?}", vertex_key.0);
@ -1118,7 +1085,6 @@ fn compute_dual(minor_graph: &MinorGraph) -> Result<DualGraph, BooleanError> {
reverse_winding = true;
}
let outer_face_key = if count != 1 {
// return Err(BooleanError::MultipleOuterFaces);
#[cfg(feature = "logging")]
eprintln!("Found multiple outer faces: {areas:?}, falling back to area calculation");
let (key, _) = *areas.iter().max_by_key(|(_, area)| ((area.abs() * 1000.) as u64)).unwrap();
@ -1335,7 +1301,6 @@ fn flag_faces(
}
visited_faces.insert(face_key);
// dbg!(face_key, a_count, b_count);
let a_flag = get_flag(a_count, a_fill_rule);
let b_flag = get_flag(b_count, b_fill_rule);
*flags.entry(face_key).or_default() = a_flag | (b_flag << 1);
@ -1458,7 +1423,7 @@ fn dump_faces(
}
}
// poke holes in the face
// Poke holes in the face
if let Some(subtrees) = tree.outgoing_edges.get(&face_key) {
for subtree in subtrees {
let outer_face_key = subtree.component.outer_face.unwrap();
@ -1499,7 +1464,6 @@ const OPERATION_PREDICATES: [fn(u8) -> bool; 6] = [
|flag: u8| flag > 0, // Fracture
];
// TODO: Impl error trait
/// Represents errors that can occur during boolean operations on paths.
#[derive(Debug)]
pub enum BooleanError {
@ -1507,6 +1471,17 @@ pub enum BooleanError {
MultipleOuterFaces,
/// Indicates that no valid ear was found in a polygon during triangulation. <https://en.wikipedia.org/wiki/Vertex_(geometry)#Ears>
NoEarInPolygon,
InvalidPathCommand(char),
}
impl Display for BooleanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MultipleOuterFaces => f.write_str("Found multiple candidates for the outer face in a connected component of the dual graph."),
Self::NoEarInPolygon => f.write_str("Failed to compute winding order for one of the faces, this usually happens when the polygon is malformed."),
Self::InvalidPathCommand(cmd) => f.write_fmt(format_args!("Encountered a '{cmd}' while parsing the svg data which was not recogniezed")),
}
}
}
/// Performs boolean operations on two paths.
@ -1519,8 +1494,8 @@ pub enum BooleanError {
/// ```
/// use path_bool::{path_boolean, FillRule, PathBooleanOperation, path_from_path_data, path_to_path_data};
///
/// let path_a = path_from_path_data("M 10 10 L 50 10 L 30 40 Z");
/// let path_b = path_from_path_data("M 20 30 L 60 30 L 60 50 L 20 50 Z");
/// let path_a = path_from_path_data("M 10 10 L 50 10 L 30 40 Z").unwrap();
/// let path_b = path_from_path_data("M 20 30 L 60 30 L 60 50 L 20 50 Z").unwrap();
///
/// let result = path_boolean(
/// &path_a,
@ -1573,13 +1548,12 @@ pub fn path_boolean(a: &Path, a_fill_rule: FillRule, b: &Path, b_fill_rule: Fill
#[cfg(feature = "logging")]
for (edge, _, _) in split_edges.iter() {
// eprintln!("{}", edge.format_path());
eprintln!("{}", path_to_path_data(&vec![*edge], 0.001));
}
let total_bounding_box = match total_bounding_box {
Some(bb) => bb,
None => return Ok(Vec::new()), // input geometry is empty
None => return Ok(Vec::new()), // Input geometry is empty
};
let major_graph = find_vertices(&split_edges, total_bounding_box);
@ -1604,7 +1578,6 @@ pub fn path_boolean(a: &Path, a_fill_rule: FillRule, b: &Path, b_fill_rule: Fill
#[cfg(feature = "logging")]
for (key, edge) in minor_graph.edges.iter() {
// eprintln!("{}", edge.format_path());
eprintln!("{key:?}:\n{}", path_to_path_data(&edge.segments, 0.001));
}
#[cfg(feature = "logging")]
@ -1662,10 +1635,9 @@ pub fn path_boolean(a: &Path, a_fill_rule: FillRule, b: &Path, b_fill_rule: Fill
#[cfg(test)]
mod tests {
use std::f64::consts::TAU;
use super::*;
use glam::DVec2; // Assuming DVec2 is defined in your crate
use glam::DVec2;
use std::f64::consts::TAU; // Assuming DVec2 is defined in your crate
#[test]
fn test_split_at_intersections() {
@ -1690,14 +1662,14 @@ mod tests {
fn unsplit_edges() -> Vec<(PathSegment, u8)> {
let unsplit_edges = vec![
(PathSegment::Arc(DVec2::new(39.0, 20.0), 19.0, 19.0, 0.0, false, true, DVec2::new(20.0, 39.0)), 1),
(PathSegment::Arc(DVec2::new(20.0, 39.0), 19.0, 19.0, 0.0, false, true, DVec2::new(1.0, 20.0)), 1),
(PathSegment::Arc(DVec2::new(1.0, 20.0), 19.0, 19.0, 0.0, false, true, DVec2::new(20.0, 1.0)), 1),
(PathSegment::Arc(DVec2::new(20.0, 1.0), 19.0, 19.0, 0.0, false, true, DVec2::new(39.0, 20.0)), 1),
(PathSegment::Arc(DVec2::new(47.0, 28.0), 19.0, 19.0, 0.0, false, true, DVec2::new(28.0, 47.0)), 2),
(PathSegment::Arc(DVec2::new(28.0, 47.0), 19.0, 19.0, 0.0, false, true, DVec2::new(9.0, 28.0)), 2),
(PathSegment::Arc(DVec2::new(9.0, 28.0), 19.0, 19.0, 0.0, false, true, DVec2::new(28.0, 9.0)), 2),
(PathSegment::Arc(DVec2::new(28.0, 9.0), 19.0, 19.0, 0.0, false, true, DVec2::new(47.0, 28.0)), 2),
(PathSegment::Arc(DVec2::new(39., 20.), 19., 19., 0., false, true, DVec2::new(20., 39.)), 1),
(PathSegment::Arc(DVec2::new(20., 39.), 19., 19., 0., false, true, DVec2::new(1., 20.)), 1),
(PathSegment::Arc(DVec2::new(1., 20.), 19., 19., 0., false, true, DVec2::new(20., 1.)), 1),
(PathSegment::Arc(DVec2::new(20., 1.), 19., 19., 0., false, true, DVec2::new(39., 20.)), 1),
(PathSegment::Arc(DVec2::new(47., 28.), 19., 19., 0., false, true, DVec2::new(28., 47.)), 2),
(PathSegment::Arc(DVec2::new(28., 47.), 19., 19., 0., false, true, DVec2::new(9., 28.)), 2),
(PathSegment::Arc(DVec2::new(9., 28.), 19., 19., 0., false, true, DVec2::new(28., 9.)), 2),
(PathSegment::Arc(DVec2::new(28., 9.), 19., 19., 0., false, true, DVec2::new(47., 28.)), 2),
];
unsplit_edges
}
@ -1713,7 +1685,7 @@ mod tests {
let minor_graph = compute_minor(&major_graph);
// Print minor graph state
// eprintln!("Minor Graph:");
eprintln!("Minor Graph:");
print_minor_graph_state(&minor_graph);
// Assertions
@ -1817,18 +1789,18 @@ mod tests {
fn get_incidence_angle(edge: &MinorGraphEdge) -> f64 {
let seg = &edge.segments[0]; // First segment is always the incident one in both fwd and bwd
let (p0, p1) = if edge.direction_flag.forward() {
(seg.sample_at(0.0), seg.sample_at(0.1))
(seg.sample_at(0.), seg.sample_at(0.1))
} else {
(seg.sample_at(1.0), seg.sample_at(1.0 - 0.1))
(seg.sample_at(1.), seg.sample_at(1. - 0.1))
};
((p1.y - p0.y).atan2(p1.x - p0.x) + TAU) % TAU
}
#[test]
fn test_path_segment_horizontal_ray_intersection_count() {
let orig_seg = PathSegment::Arc(DVec2::new(24.0, 10.090978), 13.909023, 13.909023, 0.0, false, true, DVec2::new(47., 24.0));
let orig_seg = PathSegment::Arc(DVec2::new(24., 10.090978), 13.909023, 13.909023, 0., false, true, DVec2::new(47., 24.));
let point = DVec2::new(37.99, 24.0);
let point = DVec2::new(37.99, 24.);
eprintln!("Starting test with segment: {:?}", orig_seg);
eprintln!("Test point: {:?}", point);
@ -1844,15 +1816,15 @@ mod tests {
#[test]
fn test_bounding_box_intersects_horizontal_ray() {
let bbox = Aabb {
top: 10.0,
right: 40.0,
bottom: 30.0,
left: 20.0,
top: 10.,
right: 40.,
bottom: 30.,
left: 20.,
};
assert!(bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(0.0, 30.0)));
assert!(bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(20.0, 30.0)));
assert!(bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(10.0, 20.0)));
assert!(!bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(30.0, 40.0)));
assert!(bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(0., 30.)));
assert!(bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(20., 30.)));
assert!(bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(10., 20.)));
assert!(!bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(30., 40.)));
}
}

View file

@ -15,7 +15,7 @@ pub fn vector_angle(u: DVec2, v: DVec2) -> f64 {
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
// TODO: `u` can be scaled
return PI;
}

View file

@ -72,15 +72,15 @@ impl<T: Clone> QuadTree<T> {
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;
let mid_x = (self.bounding_box.left + self.bounding_box.right) / 2.;
let mid_y = (self.bounding_box.top + self.bounding_box.bottom) / 2.;
self.subtrees = Some(Box::new([
QuadTree::new(
Aabb {
top: self.bounding_box.top,
right: midx,
bottom: midy,
right: mid_x,
bottom: mid_y,
left: self.bounding_box.left,
},
self.depth - 1,
@ -90,16 +90,16 @@ impl<T: Clone> QuadTree<T> {
Aabb {
top: self.bounding_box.top,
right: self.bounding_box.right,
bottom: midy,
left: midx,
bottom: mid_y,
left: mid_x,
},
self.depth - 1,
self.inner_node_capacity,
),
QuadTree::new(
Aabb {
top: midy,
right: midx,
top: mid_y,
right: mid_x,
bottom: self.bounding_box.bottom,
left: self.bounding_box.left,
},
@ -108,10 +108,10 @@ impl<T: Clone> QuadTree<T> {
),
QuadTree::new(
Aabb {
top: midy,
top: mid_y,
right: self.bounding_box.right,
bottom: self.bounding_box.bottom,
left: midx,
left: mid_x,
},
self.depth - 1,
self.inner_node_capacity,

View file

@ -1,3 +1,6 @@
use crate::path_boolean::{self, FillRule, PathBooleanOperation};
use crate::path_data::{path_from_path_data, path_to_path_data};
use core::panic;
use glob::glob;
use image::{DynamicImage, GenericImageView, RgbaImage};
@ -8,9 +11,6 @@ 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 {
@ -31,7 +31,7 @@ fn visual_tests() {
("fracture", PathBooleanOperation::Fracture),
];
let folders: Vec<(String, PathBuf, &str, PathBooleanOperation)> = glob("__fixtures__/visual-tests/*/")
let folders: Vec<(String, PathBuf, &str, PathBooleanOperation)> = glob("visual-tests/*/")
.expect("Failed to read glob pattern")
.flat_map(|entry| {
let dir = entry.expect("Failed to get directory entry");
@ -58,6 +58,7 @@ fn visual_tests() {
let mut width = String::new();
let mut height = String::new();
let mut view_box = String::new();
let mut transform = String::new();
for event in svg_tree {
match event {
Event::Tag("svg", svg::node::element::tag::Type::Start, attributes) => {
@ -65,6 +66,11 @@ fn visual_tests() {
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("g", svg::node::element::tag::Type::Start, attributes) => {
if let Some(transform_attr) = attributes.get("transform") {
transform = transform_attr.to_string();
}
}
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());
@ -98,19 +104,25 @@ fn visual_tests() {
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 = path_from_path_data(&a_node.0).unwrap();
let b = path_from_path_data(&b_node.0).unwrap();
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
// Create the result SVG with correct dimensions and transform
let mut result_svg = format!("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" viewBox=\"{}\">", width, height, view_box);
if !transform.is_empty() {
result_svg.push_str(&format!("<g transform=\"{}\">", transform));
}
for path in &result {
result_svg.push_str(&format!("<path d=\"{}\" {}/>", path_to_path_data(path, 1e-4), first_path_attributes));
}
if !transform.is_empty() {
result_svg.push_str("</g>");
}
result_svg.push_str("</svg>");
// Save the result SVG

View file

Before

Width:  |  Height:  |  Size: 889 B

After

Width:  |  Height:  |  Size: 889 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 970 B

After

Width:  |  Height:  |  Size: 970 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 889 B

After

Width:  |  Height:  |  Size: 889 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 970 B

After

Width:  |  Height:  |  Size: 970 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 351 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 239 B

After

Width:  |  Height:  |  Size: 239 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 351 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 239 B

After

Width:  |  Height:  |  Size: 239 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 976 B

After

Width:  |  Height:  |  Size: 976 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 351 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 742 B

After

Width:  |  Height:  |  Size: 742 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 861 B

After

Width:  |  Height:  |  Size: 861 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 911 B

After

Width:  |  Height:  |  Size: 911 B

Before After
Before After

View file

@ -0,0 +1,5 @@
for dir in */; do
for fn in difference division exclusion fracture intersection union; do
cp "${dir}test-results/$fn-ours.svg" "$dir$fn.svg"
done
done

View file

@ -0,0 +1,10 @@
INKSCAPE_CMD=inkscape
OPS=(union difference intersection exclusion division fracture)
for dir in */; do
for op in "${OPS[@]}"; do
if [ ! -e "$dir/$op.svg" ]; then
$INKSCAPE_CMD --actions="select-all; path-$op; export-filename:$dir/$op.svg; export-plain-svg; export-do; file-close" "$dir/original.svg"
fi
done
done

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 778 B

After

Width:  |  Height:  |  Size: 778 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 949 B

After

Width:  |  Height:  |  Size: 949 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 776 B

After

Width:  |  Height:  |  Size: 776 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 877 B

After

Width:  |  Height:  |  Size: 877 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 716 B

After

Width:  |  Height:  |  Size: 716 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 777 B

After

Width:  |  Height:  |  Size: 777 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1,017 B

After

Width:  |  Height:  |  Size: 1,017 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 775 B

After

Width:  |  Height:  |  Size: 775 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more