axiolid_arrangement/
edit.rs

1// SPDX-License-Identifier: MPL-2.0
2
3//! Incremental edits that preserve handles and topological validity.
4//!
5//! # Why these return `Result`
6//!
7//! Every edit here can be asked to do something that would corrupt the
8//! structure: split an edge at a point that is not on it, drag a vertex so a
9//! face self-intersects, connect two vertices that are not on a common face.
10//! Each is refused by name. A DCEL that silently accepts a corrupting edit is
11//! worse than one that cannot edit at all, because the damage surfaces later
12//! as a traversal that never terminates.
13
14use axiolid_core::Point2;
15use axiolid_guarantees::Sign;
16use axiolid_predicates::orient2d;
17
18use crate::entity::{HalfEdge, Vertex};
19use crate::id::{FaceId, HalfEdgeId, VertexId};
20use crate::Arrangement;
21
22/// Why an edit was refused.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum EditError {
26    /// The split point does not lie on the target edge.
27    PointNotOnEdge,
28    /// The two vertices do not share a face, so no chord connects them.
29    VerticesNotOnCommonFace,
30    /// The two vertices are already joined by an edge.
31    AlreadyConnected,
32    /// The edit would leave a face non-convex or self-intersecting.
33    WouldSelfIntersect,
34    /// The half-edge borders the unbounded face, which cannot be merged away.
35    BordersUnboundedFace,
36}
37
38impl core::fmt::Display for EditError {
39    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
40        match self {
41            Self::PointNotOnEdge => write!(f, "split point does not lie on the edge"),
42            Self::VerticesNotOnCommonFace => {
43                write!(f, "vertices do not share a face")
44            }
45            Self::AlreadyConnected => write!(f, "vertices are already connected"),
46            Self::WouldSelfIntersect => {
47                write!(f, "edit would make a face self-intersecting")
48            }
49            Self::BordersUnboundedFace => {
50                write!(f, "half-edge borders the unbounded face")
51            }
52        }
53    }
54}
55
56impl core::error::Error for EditError {}
57
58impl Arrangement {
59    /// Add an isolated vertex.
60    ///
61    /// Legal on its own: a vertex with no edges is a valid, if uninteresting,
62    /// arrangement element, and building incrementally needs it.
63    pub fn add_vertex(&mut self, position: Point2) -> VertexId {
64        self.vertices.push(Vertex {
65            position,
66            outgoing: None,
67        });
68        VertexId::from_index(self.vertices.len() - 1)
69    }
70
71    /// Split an edge at `at`, inserting a new vertex.
72    ///
73    /// The point must lie on the segment. Both the edge and its twin are
74    /// split, so the structure stays consistent.
75    ///
76    /// # Errors
77    ///
78    /// [`EditError::PointNotOnEdge`] if `at` is not collinear with the edge
79    /// or lies outside it.
80    pub fn split_edge(&mut self, edge: HalfEdgeId, at: Point2) -> Result<VertexId, EditError> {
81        let he = self.halfedges[edge.index()];
82        let twin = self.halfedges[he.twin.index()];
83        let from = self.vertices[he.origin.index()].position;
84        let to = self.vertices[twin.origin.index()].position;
85
86        // Exactly collinear, and strictly between the endpoints. Using the
87        // certified predicate rather than a tolerance keeps this decision
88        // consistent with the rest of the kernel.
89        if !matches!(decided(orient2d(from, to, at)), Sign::Zero) {
90            return Err(EditError::PointNotOnEdge);
91        }
92        let within = (at.x - from.x) * (to.x - at.x) + (at.y - from.y) * (to.y - at.y);
93        if within <= 0.0 {
94            return Err(EditError::PointNotOnEdge);
95        }
96
97        let new_vertex = self.add_vertex(at);
98        let a = HalfEdgeId::from_index(self.halfedges.len());
99        let b = HalfEdgeId::from_index(self.halfedges.len() + 1);
100
101        // `a` continues `edge` from the new vertex; `b` continues the twin.
102        self.halfedges.push(HalfEdge {
103            origin: new_vertex,
104            twin: he.twin,
105            next: he.next,
106            prev: edge,
107            face: he.face,
108        });
109        self.halfedges.push(HalfEdge {
110            origin: new_vertex,
111            twin: edge,
112            next: twin.next,
113            prev: he.twin,
114            face: twin.face,
115        });
116
117        let next_of_edge = he.next;
118        let next_of_twin = twin.next;
119        self.halfedges[edge.index()].next = a;
120        self.halfedges[edge.index()].twin = b;
121        self.halfedges[he.twin.index()].next = b;
122        self.halfedges[he.twin.index()].twin = a;
123        self.halfedges[next_of_edge.index()].prev = a;
124        self.halfedges[next_of_twin.index()].prev = b;
125        self.vertices[new_vertex.index()].outgoing = Some(a);
126        Ok(new_vertex)
127    }
128
129    /// Move a vertex, refusing the move if it would break a face.
130    ///
131    /// # Errors
132    ///
133    /// [`EditError::WouldSelfIntersect`] if any incident face would stop
134    /// being simple. The arrangement is left unchanged in that case.
135    pub fn drag_vertex(&mut self, vertex: VertexId, to: Point2) -> Result<(), EditError> {
136        let original = self.vertices[vertex.index()].position;
137        self.vertices[vertex.index()].position = to;
138
139        // Check every face touching the vertex, and roll back as a unit. A
140        // partially applied drag would be worse than a refused one.
141        let faces: Vec<FaceId> = self
142            .vertex_halfedges(vertex)
143            .into_iter()
144            .map(|h| self.halfedges[h.index()].face)
145            .collect();
146        for face in faces {
147            if face == FaceId::OUTER {
148                continue;
149            }
150            if !self.face_is_simple(face) {
151                self.vertices[vertex.index()].position = original;
152                return Err(EditError::WouldSelfIntersect);
153            }
154        }
155        Ok(())
156    }
157
158    /// Whether a face's boundary is a simple counter-clockwise polygon.
159    fn face_is_simple(&self, face: FaceId) -> bool {
160        let outline = self.face_outline(face);
161        if outline.len() < 3 {
162            return false;
163        }
164        // A convex-or-not test is not enough: a simple polygon may be
165        // concave. Check that the boundary does not reverse orientation,
166        // which is the failure a drag actually causes.
167        let area = self.face_area(face);
168        if area <= 0.0 {
169            return false;
170        }
171        // And that no two non-adjacent edges cross.
172        let n = outline.len();
173        for i in 0..n {
174            let (a1, a2) = (outline[i], outline[(i + 1) % n]);
175            for j in (i + 2)..n {
176                if (j + 1) % n == i {
177                    continue;
178                }
179                let (b1, b2) = (outline[j], outline[(j + 1) % n]);
180                if segments_cross(a1, a2, b1, b2) {
181                    return false;
182                }
183            }
184        }
185        true
186    }
187}
188
189/// Certified sign, treating an undecidable result as degenerate.
190fn decided(certified: axiolid_guarantees::Certified) -> Sign {
191    match certified {
192        axiolid_guarantees::Certified::Certain { sign, .. } => sign,
193        _ => Sign::Zero,
194    }
195}
196
197/// Whether two segments properly cross.
198fn segments_cross(a1: Point2, a2: Point2, b1: Point2, b2: Point2) -> bool {
199    let d1 = decided(orient2d(a1, a2, b1));
200    let d2 = decided(orient2d(a1, a2, b2));
201    let d3 = decided(orient2d(b1, b2, a1));
202    let d4 = decided(orient2d(b1, b2, a2));
203    d1 != d2 && d3 != d4 && d1 != Sign::Zero && d2 != Sign::Zero
204}