From 7ee290c6322f7daf8ae039a956d3697d2f759ef3 Mon Sep 17 00:00:00 2001 From: Stefan Altmayer Date: Tue, 21 May 2024 23:44:11 +0200 Subject: [PATCH] Makes `LineIntersectionIterator` public and adds documentation and some utilty functions. --- examples/svg_renderer/main.rs | 5 + examples/svg_renderer/quicksketch/convert.rs | 11 +- examples/svg_renderer/quicksketch/mod.rs | 49 +++++++ examples/svg_renderer/scenario_list.rs | 123 ++++++++++++++++++ .../line_intersection_iterator_scenario.svg | 89 +++++++++++++ images/preview.html | 2 +- src/cdt.rs | 23 ++++ src/intersection_iterator.rs | 118 ++++++++++++++++- src/lib.rs | 1 + 9 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 images/line_intersection_iterator_scenario.svg diff --git a/examples/svg_renderer/main.rs b/examples/svg_renderer/main.rs index febb032..783becf 100644 --- a/examples/svg_renderer/main.rs +++ b/examples/svg_renderer/main.rs @@ -6,6 +6,11 @@ use anyhow::Result; /// Used for rendering SVGs for documentation. These are inlined (via #[doc = include_str!(...)]) /// into the doc comment of a few items. That makes sure they will be visible even for offline users. fn main() -> Result<()> { + scenario_list::line_intersection_iterator_scenario()?.save_to_svg( + "line_intersection_iterator_scenario", + "images/line_intersection_iterator_scenario.svg", + )?; + scenario_list::natural_neighbor_area_scenario(false)?.save_to_svg( "natural_neighbor_insertion_cell", "images/natural_neighbor_insertion_cell.svg", diff --git a/examples/svg_renderer/quicksketch/convert.rs b/examples/svg_renderer/quicksketch/convert.rs index 32d5c28..462ac7c 100644 --- a/examples/svg_renderer/quicksketch/convert.rs +++ b/examples/svg_renderer/quicksketch/convert.rs @@ -1,6 +1,6 @@ use super::{ ArrowType, PathPoint, Point, Sketch, SketchCircle, SketchColor, SketchElement, SketchLine, - SketchPath, SketchText, Style, + SketchPath, SketchRectangle, SketchText, Style, }; use cgmath::num_traits::zero; use cgmath::{Bounded, Deg, InnerSpace, Vector2}; @@ -150,6 +150,14 @@ impl SketchConverter { .set("r", round(*radius)) .set("style", style.get_attribute_string(self)), ), + SketchElement::Rectangle(SketchRectangle { c0, c1, style }) => svg.add( + Rectangle::new() + .set("x", round(c0.x)) + .set("y", round(c0.y)) + .set("width", round(c1.x - c0.x)) + .set("height", round(c1.y - c0.y)) + .set("style", style.get_attribute_string(self)), + ), SketchElement::Path(SketchPath { data_points, style }) => { let mut data = Data::new(); for point in data_points { @@ -305,6 +313,7 @@ impl SketchConverter { let offset = Vector2::new(*radius, *radius); (center - offset, center + offset) } + SketchElement::Rectangle(rect) => (rect.c0, rect.c1), SketchElement::Path(SketchPath { data_points, .. }) => { let mut min = Point::max_value(); let mut max = Point::min_value(); diff --git a/examples/svg_renderer/quicksketch/mod.rs b/examples/svg_renderer/quicksketch/mod.rs index dd86461..38cebe9 100644 --- a/examples/svg_renderer/quicksketch/mod.rs +++ b/examples/svg_renderer/quicksketch/mod.rs @@ -230,6 +230,40 @@ impl Default for SketchCircle { } } +#[derive(Clone, Debug, PartialEq)] +pub struct SketchRectangle { + c0: Point, + c1: Point, + style: Style, +} + +impl SketchRectangle { + pub fn fill(mut self, fill: impl Into) -> Self { + self.style.fill = Some(fill.into()); + self + } + + pub fn stroke_width(mut self, width: f64) -> Self { + self.style.stroke_width = Some(width); + self + } + + pub fn stroke_color(mut self, color: SketchColor) -> Self { + self.style.stroke_color = Some(color); + self + } + + pub fn stroke_style(mut self, stroke_style: StrokeStyle) -> Self { + self.style.stroke_style = Some(stroke_style); + self + } + + pub fn opacity(mut self, opacity: f64) -> Self { + self.style.opacity = Some(opacity); + self + } +} + #[derive(Clone, Debug, PartialEq)] pub struct SketchText { text: String, @@ -475,6 +509,7 @@ impl SketchLine { pub enum SketchElement { Text(SketchText), Circle(SketchCircle), + Rectangle(SketchRectangle), Path(SketchPath), Line(SketchLine), } @@ -488,6 +523,14 @@ impl SketchElement { } } + pub fn rectangle(c0: Point, c1: Point) -> SketchRectangle { + SketchRectangle { + c0, + c1, + style: Default::default(), + } + } + pub fn line(from: Point, to: Point) -> SketchLine { SketchLine { from, @@ -534,6 +577,12 @@ impl From for SketchElement { } } +impl From for SketchElement { + fn from(rectangle: SketchRectangle) -> Self { + SketchElement::Rectangle(rectangle) + } +} + impl From for SketchElement { fn from(polygon: SketchPath) -> Self { SketchElement::Path(polygon) diff --git a/examples/svg_renderer/scenario_list.rs b/examples/svg_renderer/scenario_list.rs index c47df7d..21fc5bb 100644 --- a/examples/svg_renderer/scenario_list.rs +++ b/examples/svg_renderer/scenario_list.rs @@ -1390,3 +1390,126 @@ pub fn refinement_maximum_area_scenario(max_area: Option) -> Sketch { result } + +pub fn line_intersection_iterator_scenario() -> Result { + use spade::Intersection::*; + + let vertices = vec![ + VertexType::new(-30.0, -20.0), + VertexType::new(0.0, 20.0), + VertexType::new(0.0, -20.0), + VertexType::new(30.0, 0.0), + VertexType::new(14.0, 0.0), + ]; + + let triangulation = Triangulation::bulk_load_stable(vertices)?; + + let mut result = convert_triangulation(&triangulation, &Default::default()); + + for (index, vertex) in triangulation.vertices().enumerate() { + result.add( + SketchElement::text(format!("v{}", index)) + .font_size(2.4) + .horizontal_alignment(HorizontalAlignment::Middle) + .dy(0.85) + .position(convert_point(vertex.position())), + ); + } + + fn small_line(from: Point2, to: Point2) -> crate::quicksketch::SketchLine { + SketchElement::line(from, to) + .with_arrow_start(ArrowType::HalfArrow) + .stroke_color(SketchColor::ROYAL_BLUE) + .shift_from(-4.3) + .shift_to(-2.8) + .stroke_width(0.6) + } + + for intersection in spade::LineIntersectionIterator::new( + &triangulation, + spade::Point2::new(-30.0, 0.0), + spade::Point2::new(40.0, 0.0), + ) { + println!("{:?}", intersection); + match intersection { + EdgeIntersection(edge) => { + let [from, to] = edge.positions().map(convert_point); + let offset = Vector2::new(0.8, 0.0); + let from = from + offset; + let to = to + offset; + + result.add(small_line(from, to)); + } + VertexIntersection(v) => { + result.add( + SketchElement::circle(convert_point(v.position()), v.data().radius - 0.3) + .fill(SketchColor::ROYAL_BLUE), + ); + } + + EdgeOverlap(edge) => { + let [from, to] = edge.positions().map(convert_point); + let offset = Vector2::new(0.0, -1.0); + let from = from + offset; + let to = to + offset; + + result.add(small_line(to, from)); + } + } + } + let intersection_start = Point2::new(-30.0, 0.0); + let intersection_end = Point2::new(40.0, 0.0); + result.add( + SketchElement::line(intersection_start, intersection_end) + .stroke_color(SketchColor::SALMON) + .with_arrow_end(ArrowType::FilledArrow) + .shift_to(-4.0) + .stroke_width(0.5) + .stroke_style(StrokeStyle::SmallDashed), + ); + + for (index, point) in [intersection_start, intersection_end] + .into_iter() + .enumerate() + { + result.add(SketchElement::circle(point, 1.3).fill(SketchColor::SALMON)); + + let text = if index == 0 { "s" } else { "e" }; + + result.add( + SketchElement::text(text) + .position(point) + .horizontal_alignment(HorizontalAlignment::Middle) + .dy(0.5) + .font_size(2.0), + ); + } + + fn add_label(result: &mut Sketch, text: &'static str, position: Point2) { + let offset = Vector2::new(1.5, 2.0); + let c0 = position - offset; + let c1 = position + offset; + let rect = SketchElement::rectangle(c0, c1) + .fill(SketchColor::WHITE) + .opacity(0.82); + + let text = SketchElement::text(text) + .font_size(3.9) + .position(position) + .horizontal_alignment(HorizontalAlignment::Middle) + .dy(1.3); + + result.add(rect); + result.add(text); + } + + const LABEL_Y: f64 = -3.5; + + add_label(&mut result, "0", Point2::new(-12.0, LABEL_Y)); + add_label(&mut result, "1", Point2::new(2.4, LABEL_Y)); + add_label(&mut result, "2", Point2::new(14.0, LABEL_Y)); + add_label(&mut result, "3", Point2::new(22.0, LABEL_Y)); + add_label(&mut result, "4", Point2::new(30.0, LABEL_Y)); + + Ok(result) +} diff --git a/images/line_intersection_iterator_scenario.svg b/images/line_intersection_iterator_scenario.svg new file mode 100644 index 0000000..42d320b --- /dev/null +++ b/images/line_intersection_iterator_scenario.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +v0 + + +v1 + + +v2 + + +v3 + + +v4 + + + + + + + + + +s + + + +e + + + +0 + + + +1 + + + +2 + + + +3 + + + +4 + + + \ No newline at end of file diff --git a/images/preview.html b/images/preview.html index a7da86c..d91ea2f 100644 --- a/images/preview.html +++ b/images/preview.html @@ -23,7 +23,7 @@ } - + \ No newline at end of file diff --git a/src/cdt.rs b/src/cdt.rs index b1a79a5..fb6ad35 100644 --- a/src/cdt.rs +++ b/src/cdt.rs @@ -721,6 +721,29 @@ where } } + /// Returns all constraint edges that would prevent creating a new constraint between two points. + pub fn get_conflicting_edges_between_points( + &self, + from: Point2<::Scalar>, + to: Point2<::Scalar>, + ) -> impl Iterator, F>> { + LineIntersectionIterator::new(self, from, to) + .flat_map(|intersection| intersection.as_edge_intersection()) + .filter(|e| e.is_constraint_edge()) + } + + /// Returns all constraint edges that would prevent inserting a new constraint connecting two existing + /// vertices. + pub fn get_conflicting_edges_between_vertices( + &self, + from: FixedVertexHandle, + to: FixedVertexHandle, + ) -> impl Iterator, F>> { + LineIntersectionIterator::new_from_handles(self, from, to) + .flat_map(|intersection| intersection.as_edge_intersection()) + .filter(|e| e.is_constraint_edge()) + } + fn make_constraint_edge(&mut self, edge: FixedUndirectedEdgeHandle) -> bool { if !self.is_constraint_edge(edge) { self.dcel diff --git a/src/intersection_iterator.rs b/src/intersection_iterator.rs index 28f052a..2ae269d 100644 --- a/src/intersection_iterator.rs +++ b/src/intersection_iterator.rs @@ -2,6 +2,60 @@ use crate::delaunay_core::math; use crate::handles::{DirectedEdgeHandle, FixedVertexHandle, VertexHandle}; use crate::{HasPosition, Point2, Triangulation, TriangulationExt}; +/// An iterator over all intersections of a straight line across the triangulation. +/// +/// This iterator can, for example, be used to detect which edges prevent the insertion +/// of a new constraint edge. +/// +/// Three different intersection kinds are possible: +/// - The line crosses a different edge +/// - The line goes through a vertex +/// - The line overlaps with an existing edge +/// +/// These intersections are defined by the [Intersection] enum. Intersections are always returned in the same +/// order as the line, i.e. the closest intersection to the starting point is returned first. +/// +/// # Example +/// +/// This triangulation is created by the code below: +/// +#[doc = include_str!("../images/line_intersection_iterator_scenario.svg")] +/// +/// The line (s -> e) generates 5 intersections (labeled with `0..=4`): two edge intersections, +/// a vertex intersection, an edge overlap and a final vertex intersection. +/// +/// ``` +/// use spade::{LineIntersectionIterator, Point2, DelaunayTriangulation}; +/// # use spade::InsertionError; +/// # fn main() -> Result<(), InsertionError> { +/// let vertices = vec![ +/// Point2::new(-30.0, 20.0), // v0 +/// Point2::new(0.0, -20.0), // v1 +/// Point2::new(0.0, 20.0), // v2 +/// Point2::new(14.0, 0.0), // v3 +/// Point2::new(30.0, 0.0), // v4 +/// ]; +/// +/// let triangulation = DelaunayTriangulation::<_>::bulk_load_stable(vertices)?; +/// for intersection in spade::LineIntersectionIterator::new( +/// &triangulation, +/// spade::Point2::new(-30.0, 0.0), +/// spade::Point2::new(40.0, 0.0), +/// ) { +/// println!("{:?}", intersection); +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// Output (simplified): +/// ```text +/// EdgeIntersection(DirectedEdgeHandle v0 -> v1) +/// EdgeIntersection(DirectedEdgeHandle v2 -> v1) +/// VertexIntersection(VertexHandle(3)) +/// EdgeOverlap(DirectedEdgeHandle v3 -> v4) +/// VertexIntersection(VertexHandle(4)) +/// ``` pub struct LineIntersectionIterator<'a, V, DE, UE, F> where V: HasPosition, @@ -14,13 +68,24 @@ where line_to: Point2, } +/// An intersection that can occur when moving through a triangulation along a straight line. +/// +/// This is used as return type for [LineIntersectionIterator]. #[allow(clippy::enum_variant_names)] pub enum Intersection<'a, V, DE, UE, F> where V: HasPosition, { + /// Indicates that the line is either crossing or touching an existing edge. + /// The line's destination will always be either on the edge or on its left side (in a left handed coordinate system). EdgeIntersection(DirectedEdgeHandle<'a, V, DE, UE, F>), + /// Indicates that the line is touching a vertex. + /// A line beginning or starting on a vertex also generates this intersection. A "line" beginning and starting on the same + /// vertex will also return this intersection. VertexIntersection(VertexHandle<'a, V, DE, UE, F>), + /// Indicates that a line is (partially) overlapping an existing edge. + /// + /// This implies that the line points in the same direction as the edge and that they share a common line segment. EdgeOverlap(DirectedEdgeHandle<'a, V, DE, UE, F>), } @@ -64,6 +129,19 @@ where } } +impl<'a, V, DE, UE, F> Intersection<'a, V, DE, UE, F> +where + V: HasPosition, +{ + /// Returns the intersected edge if this is an edge intersection or `None` otherwise. + pub fn as_edge_intersection(&self) -> Option> { + match self { + Intersection::EdgeIntersection(ref edge) => Some(*edge), + _ => None, + } + } +} + impl<'a, V, DE, UE, F> LineIntersectionIterator<'a, V, DE, UE, F> where V: HasPosition, @@ -71,6 +149,8 @@ where UE: Default, F: Default, { + /// Creates a new `LineIntersectionIterator` covering an arbitrary line. + /// See [LineIntersectionIterator] for more information. pub fn new( delaunay: &'a T, line_from: Point2, @@ -87,6 +167,32 @@ where } } + /// Creates a new line iterator covering the line spanned by two existing vertices. + /// + /// Both start and end vertex are part of the iteration result. + /// + /// # Example + /// ``` + /// use spade::{DelaunayTriangulation, Intersection, LineIntersectionIterator, Point2, Triangulation}; + /// # use spade::InsertionError; + /// # fn main() -> Result<(), InsertionError> { + /// let mut triangulation = DelaunayTriangulation::>::new(); + /// let v0 = triangulation.insert(Point2::new(0.0, 0.0))?; + /// let v1 = triangulation.insert(Point2::new(1.0, 1.0))?; + /// + /// let expected_edge_overlap = triangulation.get_edge_from_neighbors(v0, v1).unwrap(); + /// let all_intersections = LineIntersectionIterator::new_from_handles(&triangulation, v0, v1).collect::>(); + /// + /// let v0 = triangulation.vertex(v0); + /// let v1 = triangulation.vertex(v1); + /// assert_eq!(all_intersections, vec![ + /// Intersection::VertexIntersection(v0), + /// Intersection::EdgeOverlap(expected_edge_overlap), + /// Intersection::VertexIntersection(v1), + /// ]); + /// # Ok(()) + /// # } + /// ``` pub fn new_from_handles( delaunay: &T, from: FixedVertexHandle, @@ -490,6 +596,10 @@ mod test { assert!(LineIntersectionIterator::new(&delaunay, from, to) .next() .is_none()); + + assert!(LineIntersectionIterator::new(&delaunay, from, from) + .next() + .is_none()); Ok(()) } @@ -638,11 +748,13 @@ mod test { #[test] fn test_intersecting_single_vertex() -> Result<(), InsertionError> { let mut delaunay = Triangulation::new(); - let v0 = delaunay.insert(Point2::new(0.5, 0.5))?; + let pos = Point2::new(0.5, 0.5); + let v0 = delaunay.insert(pos)?; let v0 = delaunay.vertex(v0); let from = Point2::new(1.0, 0.0); let to = Point2::new(0.0, 1.0); check(&delaunay, from, to, vec![VertexIntersection(v0)]); + check(&delaunay, pos, pos, vec![VertexIntersection(v0)]); let to = Point2::new(1.234, 42.0); check(&delaunay, from, to, vec![]); Ok(()) @@ -688,6 +800,10 @@ mod test { let from = Point2::new(0.5, -0.5); let to = Point2::new(-0.5, 0.5); check(&d, from, to, vec![v2]); + + let single_vertex = Point2::new(-2.0, -2.0); + check(&d, single_vertex, single_vertex, vec![v1]); + Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index c02611a..8fe66fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,7 @@ pub use delaunay_core::{ pub use crate::delaunay_core::interpolation::{Barycentric, NaturalNeighbor}; pub use delaunay_core::LineSideInfo; +pub use intersection_iterator::{Intersection, LineIntersectionIterator}; pub use triangulation::{FloatTriangulation, PositionInTriangulation, Triangulation}; #[cfg(not(fuzzing))]