ifc_lite_geometry/space_dcel/
remove.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Removal edits on a [`SpacePlate`]: spur and edge deletion plus the orphan
6//! sweep that cleans up after them. Split out of `mod.rs` for the module-size
7//! ratchet; the construction, split/merge/dissolve edits and queries stay there.
8
9use super::walk::FaceWalk;
10use super::{EditError, Face, FaceId, FacePatch, HalfEdgeId, SpacePlate, VertexId, EPS_COLL};
11use super::geom2d::perp_distance;
12
13impl SpacePlate {
14    /// Live degree of a vertex (its number of live outgoing half-edges).
15    pub(super) fn vertex_degree(&self, v: VertexId) -> usize {
16        let vi = v.0 as usize;
17        if vi >= self.vertices.len() || !self.vertices[vi].alive {
18            return 0;
19        }
20        self.outgoing_half_edges(v).count()
21    }
22
23    /// Remove the undirected edge of a **degree-1 spur tip** — a dangling wall
24    /// poking into a face — splicing the face cycle closed and tombstoning the
25    /// tip. `spur_he` may be either half-edge of the spur. Internal; driven by
26    /// `prune_orphans` / `remove_edge`. Area-neutral: the tip's out-and-back
27    /// boundary contributes cancelling shoelace terms, so no face area changes.
28    pub(super) fn remove_spur_edge(&mut self, spur_he: HalfEdgeId) -> Result<(), EditError> {
29        let hi = spur_he.0 as usize;
30        if hi >= self.half_edges.len() || !self.half_edges[hi].alive {
31            return Err(EditError::StaleHandle);
32        }
33        let t = self.half_edges[hi].twin;
34        // Orient so `s = T→J` (origin is the degree-1 tip) and `s_t = J→T`.
35        let (s, s_t) = if self.vertex_degree(self.half_edges[hi].origin) == 1 {
36            (spur_he, t)
37        } else if self.vertex_degree(self.half_edges[t.0 as usize].origin) == 1 {
38            (t, spur_he)
39        } else {
40            return Err(EditError::VertexNotDissolvable); // neither end is a tip
41        };
42        let tip = self.half_edges[s.0 as usize].origin;
43        let j = self.half_edges[s_t.0 as usize].origin;
44        let f = self.half_edges[s.0 as usize].face;
45        // A genuine tip is a peninsula: both half-edges share one face and the
46        // rotation at the tip is the out-and-back pattern. Else it's corrupt.
47        if self.half_edges[s_t.0 as usize].face != f
48            || self.half_edges[s_t.0 as usize].next != s
49            || self.half_edges[s.0 as usize].prev != s_t
50        {
51            return Err(EditError::StaleHandle);
52        }
53        let a = self.half_edges[s_t.0 as usize].prev; // ends at J
54        let b = self.half_edges[s.0 as usize].next; // starts at J
55
56        if a == s {
57            // Lone stick: J is degree-1 too, so this 2-cycle bounds nothing.
58            // Tombstone it whatever face it is tagged with (refusing left
59            // `prune_orphans` a tip it could never remove) and let the face
60            // re-anchor on whatever else it still bounds, or die with it.
61            self.half_edges[s.0 as usize].alive = false;
62            self.half_edges[s_t.0 as usize].alive = false;
63            self.vertices[tip.0 as usize].outgoing = None;
64            self.vertices[tip.0 as usize].alive = false;
65            self.vertices[j.0 as usize].outgoing = None;
66            self.vertices[j.0 as usize].alive = false;
67            self.reanchor_face_if_dead(f);
68            return Ok(());
69        }
70
71        // Splice the spur out of F's cycle: A → B directly.
72        self.half_edges[a.0 as usize].next = b;
73        self.half_edges[b.0 as usize].prev = a;
74        self.half_edges[s.0 as usize].alive = false;
75        self.half_edges[s_t.0 as usize].alive = false;
76        self.vertices[tip.0 as usize].outgoing = None;
77        self.vertices[tip.0 as usize].alive = false;
78        self.repair_vertex_outgoing(j, s_t);
79        if matches!(self.faces[f.0 as usize].half_edge, Some(h) if h == s || h == s_t) {
80            self.faces[f.0 as usize].half_edge = Some(a);
81        }
82        Ok(())
83    }
84
85    /// Remove all orphaned cruft the wall arrangement leaves behind: dangling
86    /// spur walls (degree-1 chains), isolated vertices, and redundant collinear
87    /// degree-2 nodes. Idempotent, and never changes a room's area (spurs bound
88    /// no room; collinear dissolve only straightens a node already on its chord).
89    /// Returns how many topology elements were pruned.
90    pub fn prune_orphans(&mut self) -> usize {
91        let mut removed = 0usize;
92        // Phase A — spur sweep to a fixpoint (chews whole chains). A tip that
93        // `remove_spur_edge` refuses stays degree-1, so "no tips left" alone is
94        // not an exit: a sweep that removes nothing must break, as Phase C does.
95        loop {
96            let mut progress = false;
97            let tips: Vec<VertexId> = (0..self.vertices.len())
98                .map(|i| VertexId(i as u32))
99                .filter(|&v| self.vertex_degree(v) == 1)
100                .collect();
101            for tip in tips {
102                if self.vertex_degree(tip) != 1 {
103                    continue; // a sibling removal already changed it
104                }
105                let s = self.outgoing_half_edges(tip).next();
106                if let Some(s) = s {
107                    if self.remove_spur_edge(s).is_ok() {
108                        removed += 1;
109                        progress = true;
110                    }
111                }
112            }
113            if !progress {
114                break;
115            }
116        }
117        // Phase B — drop leftover degree-0 (isolated) vertices.
118        for i in 0..self.vertices.len() {
119            let v = VertexId(i as u32);
120            if self.vertices[i].alive && self.vertex_degree(v) == 0 {
121                self.vertices[i].alive = false;
122                self.vertices[i].outgoing = None;
123                removed += 1;
124            }
125        }
126        // Phase C — dissolve redundant collinear degree-2 nodes (fixpoint).
127        loop {
128            let mut progress = false;
129            let cands: Vec<VertexId> = (0..self.vertices.len())
130                .map(|i| VertexId(i as u32))
131                .filter(|&v| self.vertex_degree(v) == 2)
132                .collect();
133            for v in cands {
134                if self.vertex_degree(v) != 2 {
135                    continue;
136                }
137                let outs: Vec<HalfEdgeId> = self.outgoing_half_edges(v).collect();
138                let p = self.vertices[v.0 as usize].pos;
139                let x = self.vertices[self.dest(outs[0]).0 as usize].pos;
140                let y = self.vertices[self.dest(outs[1]).0 as usize].pos;
141                if perp_distance(p, x, y) >= EPS_COLL {
142                    continue; // a genuine corner — keep it
143                }
144                if self.dissolve_vertex(v).is_ok() {
145                    removed += 1;
146                    progress = true;
147                }
148            }
149            if !progress {
150                break;
151            }
152        }
153        removed
154    }
155
156    /// Remove the wall `edge`, choosing the right semantics from its two
157    /// incident faces, and auto-clean the orphans it leaves:
158    /// - room ↔ room → union the two rooms (`merge_faces`);
159    /// - bridge (same face both sides) → delete it, give the cycle it cut off
160    ///   its own face (`split_bridge_cycles`), then `prune_orphans`;
161    /// - outer ↔ outer → delete it + `prune_orphans`;
162    /// - room ↔ outer (a real enclosing wall) → `BordersExterior` (don't open a room).
163    pub fn remove_edge(&mut self, edge: HalfEdgeId) -> Result<Vec<FacePatch>, EditError> {
164        let hi = edge.0 as usize;
165        if hi >= self.half_edges.len() || !self.half_edges[hi].alive {
166            return Err(EditError::StaleHandle);
167        }
168        let t = self.half_edges[hi].twin;
169        let f_keep = self.half_edges[hi].face;
170        let f_drop = self.half_edges[t.0 as usize].face;
171        let keep_outer = self.faces[f_keep.0 as usize].is_outer;
172        let drop_outer = self.faces[f_drop.0 as usize].is_outer;
173
174        if f_keep != f_drop && !keep_outer && !drop_outer {
175            return self.merge_faces(edge); // two real rooms → union
176        }
177        if f_keep != f_drop && keep_outer != drop_outer {
178            return Err(EditError::BordersExterior); // would open a room
179        }
180
181        // Bridge (f_keep == f_drop) or outer ↔ outer → delete + clean.
182        let hn = self.half_edges[hi].next;
183        let hp = self.half_edges[hi].prev;
184        let tn = self.half_edges[t.0 as usize].next;
185        let tp = self.half_edges[t.0 as usize].prev;
186        let oh = self.half_edges[hi].origin;
187        let ot = self.half_edges[t.0 as usize].origin;
188
189        self.half_edges[hp.0 as usize].next = tn;
190        self.half_edges[tn.0 as usize].prev = hp;
191        self.half_edges[tp.0 as usize].next = hn;
192        self.half_edges[hn.0 as usize].prev = tp;
193
194        if f_drop != f_keep {
195            // outer ↔ outer: fold f_drop's loop into f_keep.
196            self.faces[f_keep.0 as usize].half_edge = Some(hp);
197            let merged: Vec<HalfEdgeId> = self.face_half_edges(f_keep).collect();
198            for he in merged {
199                self.half_edges[he.0 as usize].face = f_keep;
200            }
201            self.faces[f_drop.0 as usize].alive = false;
202            self.faces[f_drop.0 as usize].half_edge = None;
203        }
204        self.half_edges[hi].alive = false;
205        self.half_edges[t.0 as usize].alive = false;
206        self.repair_vertex_outgoing(oh, edge);
207        self.repair_vertex_outgoing(ot, t);
208        // The face's anchor may have been one of the removed half-edges (esp. a
209        // bridge / spur in the outer face) — re-point it at a survivor.
210        self.reanchor_face_if_dead(f_keep);
211        if f_drop == f_keep {
212            self.split_bridge_cycles(f_keep, hn, tn);
213        }
214
215        self.prune_orphans();
216
217        let mut out = Vec::new();
218        if self.faces[f_keep.0 as usize].alive && !self.faces[f_keep.0 as usize].is_outer {
219            out.push(self.face_patch(f_keep));
220        }
221        Ok(out)
222    }
223
224    /// After a bridge splice in `remove_edge`, the face's one cycle
225    /// `h, hn, …, tp, t, tn, …, hp` has become two: `hn…tp` and `tn…hp`, both
226    /// still tagged `f`. A `Face` anchors one cycle, so the walk from `f`'s
227    /// anchor reached only one of them and the other was a fragment nothing
228    /// could reach or re-home. Give the second cycle its own face, classified
229    /// by signed area exactly as `from_arrangement` does (a cut-off stick or
230    /// island winds CW or flat → outer); `f` stays on the larger-area cycle so
231    /// a room keeps its id when something is cut off it. A spur tip needs
232    /// nothing: its "second cycle" is the dead pair itself.
233    fn split_bridge_cycles(&mut self, f: FaceId, hn: HalfEdgeId, tn: HalfEdgeId) {
234        if !self.half_edges[hn.0 as usize].alive || !self.half_edges[tn.0 as usize].alive {
235            return;
236        }
237        let walk = |start: HalfEdgeId| FaceWalk { plate: self, start: Some(start), cur: None };
238        let cycle_h: Vec<HalfEdgeId> = walk(hn).collect();
239        if cycle_h.contains(&tn) {
240            return; // still one cycle
241        }
242        let cycle_t: Vec<HalfEdgeId> = walk(tn).collect();
243        let (area_h, area_t) = (self.signed_area_of_cycle(&cycle_h), self.signed_area_of_cycle(&cycle_t));
244        let (keep, split, split_area) =
245            if area_h >= area_t { (cycle_h, cycle_t, area_t) } else { (cycle_t, cycle_h, area_h) };
246        let parent = self.faces[f.0 as usize].clone();
247        let is_outer = split_area <= 0.0;
248        let new_face = FaceId(self.faces.len() as u32);
249        self.faces.push(Face {
250            half_edge: split.first().copied(),
251            is_outer,
252            is_room: !is_outer && parent.is_room,
253            alive: true,
254            ..parent
255        });
256        for he in split {
257            self.half_edges[he.0 as usize].face = new_face;
258        }
259        self.faces[f.0 as usize].half_edge = keep.first().copied();
260    }
261}