diff --git a/cli/src/main.rs b/cli/src/main.rs index 73e18800..d67667a8 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -144,6 +144,12 @@ fn main() { .help("Change the color of the window's background") .takes_value(true), ) + .arg( + Arg::with_name("ASSUME_NO_INTERSECTION") + .long("assume-no-intersection") + .help("Run the tessellation algorithm without intersection checks") + .required(false), + ) ) .subcommand( declare_tess_params(SubCommand::with_name("reduce"), true) @@ -384,7 +390,7 @@ fn get_path(matches: &ArgMatches) -> Option { if let Err(e) = parser.parse(&options, &mut Source::new(path_str.chars()), &mut builder) { println!("Error while parsing path: {}", path_str); - println!("{:?}", e); + println!("{}", e); } Some(builder.build()) @@ -417,12 +423,15 @@ fn get_tess_command(command: &ArgMatches, need_path: bool) -> TessellateCmd { let dots = get_dots(command); let fill_rule = get_fill_rule(command); let orientation = get_orientation(command); + let assume_no_intersection = command.is_present("ASSUME_NO_INTERSECTION"); + let fill = if command.is_present("FILL") || (stroke.is_none() && hatch.is_none() && dots.is_none()) { Some( FillOptions::tolerance(get_tolerance(command)) .with_fill_rule(fill_rule) - .with_sweep_orientation(orientation), + .with_sweep_orientation(orientation) + .with_intersections(!assume_no_intersection) ) } else { None diff --git a/crates/tessellation/src/error.rs b/crates/tessellation/src/error.rs index 86f9593a..4f2b75c5 100644 --- a/crates/tessellation/src/error.rs +++ b/crates/tessellation/src/error.rs @@ -36,9 +36,18 @@ pub enum InternalError { InsufficientNumberOfEdges, MergeVertexOutside, InvalidNumberOfEdgesBelowVertex, + // TODO: ErrorCode(1) is used to signal that something unexpected happened + // while tessellating with handle_intersections = false. Add a proper error + // variant for this for the next major release. ErrorCode(i16), } +impl InternalError { + pub(crate) fn intersections_disabled() -> Self { + InternalError::ErrorCode(1) + } +} + #[cfg(feature = "std")] impl core::fmt::Display for InternalError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { diff --git a/crates/tessellation/src/fill.rs b/crates/tessellation/src/fill.rs index 5ad6adf9..6c88a10b 100644 --- a/crates/tessellation/src/fill.rs +++ b/crates/tessellation/src/fill.rs @@ -768,8 +768,14 @@ impl FillTessellator { self.fill_rule = options.fill_rule; self.orientation = options.sweep_orientation; - self.tolerance = options.tolerance * 0.5; self.assume_no_intersection = !options.handle_intersections; + self.tolerance = if true || options.handle_intersections { + options.tolerance * 0.5 + } else { + // The tolerance theshold allows the tessellator to simplify geometry by collapsing + // nearby vertices. This can cause a non-self-intersecting path to self-intersect. + f32::EPSILON + }; builder.begin_geometry(); @@ -791,6 +797,13 @@ impl FillTessellator { debug_assert!(self.fill.spans.is_empty()); } + if self.assume_no_intersection && (!self.active.edges.is_empty() | !self.fill.spans.is_empty()) { + tess_log!(self, "Tessellation failed with TessellatorOptions::handle_intersections = false. "); + builder.abort_geometry(); + + return Err(InternalError::intersections_disabled().into()); + } + // There shouldn't be any span left after the tessellation ends. // If for whatever reason (bug) there are, flush them so that we don't // miss the triangles they contain. @@ -987,6 +1000,13 @@ impl FillTessellator { #[cfg(debug_assertions)] fn check_active_edges(&self) { + if self.assume_no_intersection { + // Invariants can break in an uncontrollable ways if we try to tessellate + // a self-intersecting polygon without intersection checks, so we only + // try to avoid panicking in this case. + return; + } + let mut winding = WindingState::new(); for (idx, edge) in self.active.edges.iter().enumerate() { winding.update(self.fill_rule, edge.winding); @@ -1917,7 +1937,7 @@ impl FillTessellator { let mut w = winding_number; tess_log!(self, "Fixing up merge vertex after sort."); let mut idx = i; - loop { + while idx > 0 { // Roll back previous edge winding and swap. w -= self.active.edges[idx - 1].winding; self.active.edges.swap(idx, idx - 1); @@ -1938,12 +1958,14 @@ impl FillTessellator { self.sort_active_edges(); - debug_assert!(self - .active - .edges - .first() - .map(|e| !e.is_merge) - .unwrap_or(true)); + if !self.assume_no_intersection { + debug_assert!(self + .active + .edges + .first() + .map(|e| !e.is_merge) + .unwrap_or(true)); + } // This can only happen if we ignore self-intersections, // so we are in a pretty broken state already. // There isn't a fully correct solution for this (other