1102 lines
44 KiB
Rust
1102 lines
44 KiB
Rust
//! Pure topology operations on the DCEL.
|
|
//!
|
|
//! Core invariants maintained by all operations:
|
|
//! - Half-edges leaving a vertex are in sorted CCW order by angle.
|
|
//! The fan is traversed via: `twin(he).next` gives the next CCW outgoing.
|
|
//! - `he.next` walks CCW around the face to the left of `he`.
|
|
//! - `he.prev` is the inverse of `next`.
|
|
//! - `he.twin.origin` is the destination of `he`.
|
|
//! - Faces are only created when splitting an existing non-F0 face.
|
|
|
|
use super::{
|
|
subdivide_cubic, Dcel, EdgeId, FaceId, HalfEdgeId, VertexId, DEFAULT_SNAP_EPSILON,
|
|
};
|
|
use kurbo::CubicBez;
|
|
|
|
impl Dcel {
|
|
/// Angle of the curve's forward direction at its start (p0 → p1, fallback p0 → p3).
|
|
pub fn curve_start_angle(curve: &CubicBez) -> f64 {
|
|
let dx = curve.p1.x - curve.p0.x;
|
|
let dy = curve.p1.y - curve.p0.y;
|
|
if dx * dx + dy * dy > 1e-18 {
|
|
dy.atan2(dx)
|
|
} else {
|
|
(curve.p3.y - curve.p0.y).atan2(curve.p3.x - curve.p0.x)
|
|
}
|
|
}
|
|
|
|
/// Angle of the curve's backward direction at its end (p3 → p2, fallback p3 → p0).
|
|
pub fn curve_end_angle(curve: &CubicBez) -> f64 {
|
|
let dx = curve.p2.x - curve.p3.x;
|
|
let dy = curve.p2.y - curve.p3.y;
|
|
if dx * dx + dy * dy > 1e-18 {
|
|
dy.atan2(dx)
|
|
} else {
|
|
(curve.p0.y - curve.p3.y).atan2(curve.p0.x - curve.p3.x)
|
|
}
|
|
}
|
|
|
|
/// Outgoing angle of a half-edge at its origin vertex.
|
|
pub fn outgoing_angle(&self, he: HalfEdgeId) -> f64 {
|
|
let edge = &self.edges[self.half_edges[he.idx()].edge.idx()];
|
|
if he == edge.half_edges[0] {
|
|
Self::curve_start_angle(&edge.curve)
|
|
} else {
|
|
Self::curve_end_angle(&edge.curve)
|
|
}
|
|
}
|
|
|
|
/// Find the existing outgoing half-edge from `vertex` that is the immediate
|
|
/// CCW successor of `angle` in the vertex fan.
|
|
///
|
|
/// Returns the half-edge whose angular position is the smallest CCW rotation
|
|
/// from `angle`. This is where a new edge at `angle` should be spliced before.
|
|
fn find_ccw_successor(&self, vertex: VertexId, angle: f64) -> HalfEdgeId {
|
|
let start = self.vertices[vertex.idx()].outgoing;
|
|
debug_assert!(!start.is_none(), "find_ccw_successor on isolated vertex");
|
|
|
|
let mut best = start;
|
|
let mut best_delta = f64::MAX;
|
|
let mut cur = start;
|
|
loop {
|
|
let a = self.outgoing_angle(cur);
|
|
let mut delta = a - angle;
|
|
if delta <= 0.0 {
|
|
delta += std::f64::consts::TAU;
|
|
}
|
|
if delta < best_delta {
|
|
best_delta = delta;
|
|
best = cur;
|
|
}
|
|
let twin = self.half_edges[cur.idx()].twin;
|
|
cur = self.half_edges[twin.idx()].next;
|
|
if cur == start {
|
|
break;
|
|
}
|
|
}
|
|
best
|
|
}
|
|
|
|
/// Insert an edge between two existing vertices.
|
|
///
|
|
/// `face` is the face that both vertices lie on (for the (true, true) case,
|
|
/// the face is determined by the angular sector and `face` is ignored).
|
|
///
|
|
/// Returns `(edge_id, face_id)` where `face_id` is:
|
|
/// - A new face if the edge split an existing non-F0 face
|
|
/// - The face the edge was inserted into otherwise
|
|
///
|
|
/// # Face creation rules
|
|
/// - Faces are only created when both vertices are on the same boundary cycle
|
|
/// of a face that is NOT face 0 (the unbounded face). This is the "split" case.
|
|
/// - Creating a closed cycle in face 0 does NOT auto-create a face.
|
|
/// - The cycle containing the old face's `outer_half_edge` keeps the old face.
|
|
/// The other cycle gets a new face with inherited fill data.
|
|
pub fn insert_edge(
|
|
&mut self,
|
|
v1: VertexId,
|
|
v2: VertexId,
|
|
face: FaceId,
|
|
curve: CubicBez,
|
|
) -> (EdgeId, FaceId) {
|
|
debug_assert!(v1 != v2, "cannot insert self-loop");
|
|
|
|
let v1_isolated = self.vertices[v1.idx()].outgoing.is_none();
|
|
let v2_isolated = self.vertices[v2.idx()].outgoing.is_none();
|
|
|
|
// Allocate edge + half-edge pair
|
|
let (he_fwd, he_bwd) = self.alloc_half_edge_pair();
|
|
let edge_id = self.alloc_edge(curve);
|
|
|
|
self.edges[edge_id.idx()].half_edges = [he_fwd, he_bwd];
|
|
self.half_edges[he_fwd.idx()].edge = edge_id;
|
|
self.half_edges[he_bwd.idx()].edge = edge_id;
|
|
self.half_edges[he_fwd.idx()].origin = v1;
|
|
self.half_edges[he_bwd.idx()].origin = v2;
|
|
|
|
match (v1_isolated, v2_isolated) {
|
|
(true, true) => {
|
|
self.insert_edge_both_isolated(he_fwd, he_bwd, v1, v2, edge_id, face)
|
|
}
|
|
(false, false) => {
|
|
self.insert_edge_both_connected(he_fwd, he_bwd, v1, v2, edge_id, &curve, face)
|
|
}
|
|
_ => {
|
|
self.insert_edge_one_isolated(he_fwd, he_bwd, v1, v2, edge_id, &curve, v1_isolated, face)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Both vertices isolated: first edge, no face split possible.
|
|
fn insert_edge_both_isolated(
|
|
&mut self,
|
|
he_fwd: HalfEdgeId,
|
|
he_bwd: HalfEdgeId,
|
|
v1: VertexId,
|
|
v2: VertexId,
|
|
edge_id: EdgeId,
|
|
face: FaceId,
|
|
) -> (EdgeId, FaceId) {
|
|
// Two half-edges form a trivial 2-cycle
|
|
self.half_edges[he_fwd.idx()].next = he_bwd;
|
|
self.half_edges[he_fwd.idx()].prev = he_bwd;
|
|
self.half_edges[he_bwd.idx()].next = he_fwd;
|
|
self.half_edges[he_bwd.idx()].prev = he_fwd;
|
|
|
|
self.half_edges[he_fwd.idx()].face = face;
|
|
self.half_edges[he_bwd.idx()].face = face;
|
|
|
|
// Register with face
|
|
if face.0 == 0 {
|
|
self.faces[0].inner_half_edges.push(he_fwd);
|
|
} else if self.faces[face.idx()].outer_half_edge.is_none() {
|
|
self.faces[face.idx()].outer_half_edge = he_fwd;
|
|
}
|
|
|
|
self.vertices[v1.idx()].outgoing = he_fwd;
|
|
self.vertices[v2.idx()].outgoing = he_bwd;
|
|
|
|
(edge_id, face)
|
|
}
|
|
|
|
/// One vertex isolated, one connected: spur/antenna edge, no face split.
|
|
fn insert_edge_one_isolated(
|
|
&mut self,
|
|
he_fwd: HalfEdgeId,
|
|
he_bwd: HalfEdgeId,
|
|
v1: VertexId,
|
|
v2: VertexId,
|
|
edge_id: EdgeId,
|
|
curve: &CubicBez,
|
|
v1_is_isolated: bool,
|
|
face_hint: FaceId,
|
|
) -> (EdgeId, FaceId) {
|
|
let (connected, isolated) = if v1_is_isolated { (v2, v1) } else { (v1, v2) };
|
|
|
|
// Determine which half-edge goes OUT from connected vertex
|
|
let (he_out, he_back) = if self.half_edges[he_fwd.idx()].origin == connected {
|
|
(he_fwd, he_bwd)
|
|
} else {
|
|
(he_bwd, he_fwd)
|
|
};
|
|
|
|
// Find where to splice in the fan at the connected vertex
|
|
let out_angle = if self.half_edges[he_fwd.idx()].origin == connected {
|
|
Self::curve_start_angle(curve)
|
|
} else {
|
|
Self::curve_end_angle(curve)
|
|
};
|
|
let ccw_succ = self.find_ccw_successor(connected, out_angle);
|
|
let he_into = self.half_edges[ccw_succ.idx()].prev;
|
|
let angular_face = self.half_edges[he_into.idx()].face;
|
|
|
|
// If angular ordering disagrees with face_hint, try to find the correct
|
|
// sector using find_predecessor_on_face — same logic as insert_edge_both_connected.
|
|
let (he_into, ccw_succ, actual_face) = if angular_face != face_hint {
|
|
if let Some((alt_into, alt_ccw)) =
|
|
self.find_predecessor_on_face(connected, out_angle, face_hint)
|
|
{
|
|
(alt_into, alt_ccw, face_hint)
|
|
} else {
|
|
(he_into, ccw_succ, angular_face)
|
|
}
|
|
} else {
|
|
(he_into, ccw_succ, angular_face)
|
|
};
|
|
|
|
// Splice: ... → he_into → [he_out → he_back] → ccw_succ → ...
|
|
self.half_edges[he_into.idx()].next = he_out;
|
|
self.half_edges[he_out.idx()].prev = he_into;
|
|
self.half_edges[he_out.idx()].next = he_back;
|
|
self.half_edges[he_back.idx()].prev = he_out;
|
|
self.half_edges[he_back.idx()].next = ccw_succ;
|
|
self.half_edges[ccw_succ.idx()].prev = he_back;
|
|
|
|
self.half_edges[he_out.idx()].face = actual_face;
|
|
self.half_edges[he_back.idx()].face = actual_face;
|
|
|
|
self.vertices[isolated.idx()].outgoing = he_back;
|
|
|
|
(edge_id, actual_face)
|
|
}
|
|
|
|
/// Both vertices connected: may split a face.
|
|
///
|
|
/// `face_hint` is the face the caller expects the edge to be in.
|
|
/// If the angular ordering places the edge in F0 but `face_hint` is
|
|
/// a non-F0 face, the opposite angular sector is tried.
|
|
fn insert_edge_both_connected(
|
|
&mut self,
|
|
he_fwd: HalfEdgeId,
|
|
he_bwd: HalfEdgeId,
|
|
v1: VertexId,
|
|
v2: VertexId,
|
|
edge_id: EdgeId,
|
|
curve: &CubicBez,
|
|
face_hint: FaceId,
|
|
) -> (EdgeId, FaceId) {
|
|
let fwd_angle = Self::curve_start_angle(curve);
|
|
let bwd_angle = Self::curve_end_angle(curve);
|
|
|
|
let ccw_v1 = self.find_ccw_successor(v1, fwd_angle);
|
|
let ccw_v2 = self.find_ccw_successor(v2, bwd_angle);
|
|
|
|
let into_v1 = self.half_edges[ccw_v1.idx()].prev;
|
|
let into_v2 = self.half_edges[ccw_v2.idx()].prev;
|
|
|
|
let face_v1 = self.half_edges[into_v1.idx()].face;
|
|
let face_v2 = self.half_edges[into_v2.idx()].face;
|
|
|
|
// If the angular ordering places both predecessors in F0 but the
|
|
// caller expects a non-F0 face, try the opposite sector: use
|
|
// `ccw_v1` and `ccw_v2`'s twins to find the other sector at each vertex.
|
|
let (into_v1, into_v2, ccw_v1, ccw_v2, actual_face) =
|
|
if face_v1 == face_v2 && face_v1.0 == 0 && face_hint.0 != 0 {
|
|
// Try the opposite sector: at each vertex, the predecessor
|
|
// of the OTHER outgoing edge in the face_hint cycle.
|
|
let alt = self.find_predecessor_on_face(v1, fwd_angle, face_hint)
|
|
.zip(self.find_predecessor_on_face(v2, bwd_angle, face_hint));
|
|
if let Some(((alt_into_v1, alt_ccw_v1), (alt_into_v2, alt_ccw_v2))) = alt {
|
|
(alt_into_v1, alt_into_v2, alt_ccw_v1, alt_ccw_v2, face_hint)
|
|
} else {
|
|
debug_assert_eq!(face_v1, face_v2);
|
|
(into_v1, into_v2, ccw_v1, ccw_v2, face_v1)
|
|
}
|
|
} else if face_v1 != face_v2 {
|
|
// Angular ordering disagrees between the two endpoints.
|
|
// Trust face_hint (midpoint probe) as the authoritative face —
|
|
// it correctly determines which face the edge's interior lies in,
|
|
// regardless of which angular sector each vertex landed in.
|
|
let target = face_hint;
|
|
let fix_v1 = if face_v1 == target {
|
|
(into_v1, ccw_v1)
|
|
} else {
|
|
self.find_predecessor_on_face(v1, fwd_angle, target)
|
|
.unwrap_or((into_v1, ccw_v1))
|
|
};
|
|
let fix_v2 = if face_v2 == target {
|
|
(into_v2, ccw_v2)
|
|
} else {
|
|
self.find_predecessor_on_face(v2, bwd_angle, target)
|
|
.unwrap_or((into_v2, ccw_v2))
|
|
};
|
|
(fix_v1.0, fix_v2.0, fix_v1.1, fix_v2.1, target)
|
|
} else {
|
|
debug_assert_eq!(
|
|
face_v1, face_v2,
|
|
"insert_edge_both_connected: into_v1 (HE{}) on {:?} but into_v2 (HE{}) on {:?}",
|
|
into_v1.0, face_v1, into_v2.0, face_v2
|
|
);
|
|
(into_v1, into_v2, ccw_v1, ccw_v2, face_v1)
|
|
};
|
|
let actual_face = actual_face;
|
|
|
|
// Splice:
|
|
// into_v1 → he_fwd → ccw_v2 → ...
|
|
// into_v2 → he_bwd → ccw_v1 → ...
|
|
self.half_edges[he_fwd.idx()].prev = into_v1;
|
|
self.half_edges[he_fwd.idx()].next = ccw_v2;
|
|
self.half_edges[into_v1.idx()].next = he_fwd;
|
|
self.half_edges[ccw_v2.idx()].prev = he_fwd;
|
|
|
|
self.half_edges[he_bwd.idx()].prev = into_v2;
|
|
self.half_edges[he_bwd.idx()].next = ccw_v1;
|
|
self.half_edges[into_v2.idx()].next = he_bwd;
|
|
self.half_edges[ccw_v1.idx()].prev = he_bwd;
|
|
|
|
// Detect split vs bridge: walk from he_fwd. If we return to he_fwd
|
|
// without seeing he_bwd, they are on separate cycles → split.
|
|
let is_split = !self.cycle_contains(he_fwd, he_bwd);
|
|
|
|
if !is_split {
|
|
// Bridge: merged two cycles into one. All on actual_face.
|
|
self.assign_cycle_face(he_fwd, actual_face);
|
|
if actual_face.0 != 0 {
|
|
self.faces[actual_face.idx()].outer_half_edge = he_fwd;
|
|
}
|
|
return (edge_id, actual_face);
|
|
}
|
|
|
|
// Split case: two separate cycles.
|
|
// Only create a new face if the face being split is not F0.
|
|
if actual_face.0 == 0 {
|
|
// In the unbounded face, just assign both cycles to F0.
|
|
self.half_edges[he_fwd.idx()].face = FaceId(0);
|
|
self.assign_cycle_face(he_fwd, FaceId(0));
|
|
self.assign_cycle_face(he_bwd, FaceId(0));
|
|
return (edge_id, FaceId(0));
|
|
}
|
|
|
|
// Determine which cycle keeps the old face: the one containing
|
|
// the old face's outer_half_edge.
|
|
let old_ohe = self.faces[actual_face.idx()].outer_half_edge;
|
|
let fwd_has_old = !old_ohe.is_none() && self.cycle_contains(he_fwd, old_ohe);
|
|
|
|
let (he_old_cycle, he_new_cycle) = if fwd_has_old {
|
|
(he_fwd, he_bwd)
|
|
} else {
|
|
(he_bwd, he_fwd)
|
|
};
|
|
|
|
// Old cycle keeps actual_face
|
|
self.assign_cycle_face(he_old_cycle, actual_face);
|
|
self.faces[actual_face.idx()].outer_half_edge = he_old_cycle;
|
|
|
|
// New cycle gets a new face with inherited fill data
|
|
let new_face = self.alloc_face();
|
|
self.faces[new_face.idx()].fill_color = self.faces[actual_face.idx()].fill_color;
|
|
self.faces[new_face.idx()].image_fill = self.faces[actual_face.idx()].image_fill;
|
|
self.faces[new_face.idx()].fill_rule = self.faces[actual_face.idx()].fill_rule;
|
|
self.faces[new_face.idx()].outer_half_edge = he_new_cycle;
|
|
self.assign_cycle_face(he_new_cycle, new_face);
|
|
|
|
(edge_id, new_face)
|
|
}
|
|
|
|
/// Find the predecessor and CCW-successor half-edges in the fan at `vertex`
|
|
/// that belong to a specific face. Returns `(into_he, ccw_successor)` or
|
|
/// `None` if no sector at `vertex` belongs to the given face.
|
|
///
|
|
/// `into_he` is the HE arriving at `vertex` on the target face's cycle.
|
|
/// `ccw_successor` is the next outgoing HE from `vertex` in the same face's cycle.
|
|
fn find_predecessor_on_face(
|
|
&self,
|
|
vertex: VertexId,
|
|
_angle: f64,
|
|
face: FaceId,
|
|
) -> Option<(HalfEdgeId, HalfEdgeId)> {
|
|
let start = self.vertices[vertex.idx()].outgoing;
|
|
if start.is_none() {
|
|
return None;
|
|
}
|
|
|
|
// Walk the fan at this vertex. For each outgoing HE `cur`, its twin
|
|
// arrives at vertex. twin.next is the next outgoing HE in CCW order.
|
|
// The sector between `cur` (outgoing) and the previous outgoing
|
|
// (i.e., twin of the previous HE) has the face of `prev_twin`.
|
|
// Equivalently: twin of `cur` is on the same face as the sector
|
|
// between `cur` and the next outgoing.
|
|
let mut cur = start;
|
|
loop {
|
|
let twin = self.half_edges[cur.idx()].twin;
|
|
// The face of `twin` is the face of the sector between `cur`
|
|
// (this outgoing) and the next outgoing (twin.next).
|
|
if self.half_edges[twin.idx()].face == face {
|
|
// Found it: `twin` arrives at vertex on the target face,
|
|
// and `twin.next` (= next outgoing) leaves on the target face.
|
|
let next_outgoing = self.half_edges[twin.idx()].next;
|
|
return Some((twin, next_outgoing));
|
|
}
|
|
let next = self.half_edges[twin.idx()].next;
|
|
if next == start {
|
|
break;
|
|
}
|
|
cur = next;
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Check if walking the cycle from `start` encounters `target`.
|
|
fn cycle_contains(&self, start: HalfEdgeId, target: HalfEdgeId) -> bool {
|
|
let mut cur = self.half_edges[start.idx()].next;
|
|
let mut steps = 0;
|
|
while cur != start {
|
|
if cur == target {
|
|
return true;
|
|
}
|
|
cur = self.half_edges[cur.idx()].next;
|
|
steps += 1;
|
|
debug_assert!(steps < 100_000, "infinite cycle in cycle_contains");
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Set the face of every half-edge in the cycle starting at `start`.
|
|
fn assign_cycle_face(&mut self, start: HalfEdgeId, face: FaceId) {
|
|
self.half_edges[start.idx()].face = face;
|
|
let mut cur = self.half_edges[start.idx()].next;
|
|
let mut steps = 0;
|
|
while cur != start {
|
|
self.half_edges[cur.idx()].face = face;
|
|
cur = self.half_edges[cur.idx()].next;
|
|
steps += 1;
|
|
debug_assert!(steps < 100_000, "infinite cycle in assign_cycle_face");
|
|
}
|
|
}
|
|
|
|
/// Split an edge at parameter `t`, inserting a new vertex.
|
|
///
|
|
/// The original edge is shortened to [0, t]. A new edge covers [t, 1].
|
|
/// Stroke style is copied to the new edge.
|
|
/// Returns `(new_vertex, new_edge)`.
|
|
pub fn split_edge(&mut self, edge_id: EdgeId, t: f64) -> (VertexId, EdgeId) {
|
|
let original_curve = self.edges[edge_id.idx()].curve;
|
|
let (curve_a, _) = subdivide_cubic(original_curve, t);
|
|
let split_point = curve_a.p3;
|
|
let vertex = self
|
|
.snap_vertex(split_point, DEFAULT_SNAP_EPSILON)
|
|
.unwrap_or_else(|| self.alloc_vertex(split_point));
|
|
self.split_edge_at_vertex(edge_id, t, vertex)
|
|
}
|
|
|
|
/// Split an edge at parameter `t`, using a specific pre-existing vertex.
|
|
///
|
|
/// The original edge is shortened to [0, t]. A new edge covers [t, 1].
|
|
/// Curve endpoints are snapped to the vertex position.
|
|
/// Returns `(vertex, new_edge)`.
|
|
pub fn split_edge_at_vertex(
|
|
&mut self,
|
|
edge_id: EdgeId,
|
|
t: f64,
|
|
vertex: VertexId,
|
|
) -> (VertexId, EdgeId) {
|
|
debug_assert!((0.0..=1.0).contains(&t), "t out of range");
|
|
|
|
let original_curve = self.edges[edge_id.idx()].curve;
|
|
let (mut curve_a, mut curve_b) = subdivide_cubic(original_curve, t);
|
|
|
|
let vpos = self.vertices[vertex.idx()].position;
|
|
curve_a.p3 = vpos;
|
|
curve_b.p0 = vpos;
|
|
|
|
let [he_fwd, he_bwd] = self.edges[edge_id.idx()].half_edges;
|
|
|
|
// Allocate new edge + half-edge pair for second segment
|
|
let (new_he_fwd, new_he_bwd) = self.alloc_half_edge_pair();
|
|
let new_edge_id = self.alloc_edge(curve_b);
|
|
|
|
self.edges[new_edge_id.idx()].half_edges = [new_he_fwd, new_he_bwd];
|
|
self.half_edges[new_he_fwd.idx()].edge = new_edge_id;
|
|
self.half_edges[new_he_bwd.idx()].edge = new_edge_id;
|
|
|
|
// Copy stroke style
|
|
self.edges[new_edge_id.idx()].stroke_style =
|
|
self.edges[edge_id.idx()].stroke_style.clone();
|
|
self.edges[new_edge_id.idx()].stroke_color = self.edges[edge_id.idx()].stroke_color;
|
|
|
|
// Shorten original edge
|
|
self.edges[edge_id.idx()].curve = curve_a;
|
|
|
|
// Set origins: new_he_fwd goes from vertex onward,
|
|
// new_he_bwd goes from old destination toward vertex
|
|
self.half_edges[new_he_fwd.idx()].origin = vertex;
|
|
let old_dest = self.half_edges[he_bwd.idx()].origin;
|
|
self.half_edges[new_he_bwd.idx()].origin = old_dest;
|
|
|
|
// Splice new_he_fwd into forward cycle:
|
|
// Before: ... → he_fwd → fwd_next → ...
|
|
// After: ... → he_fwd → new_he_fwd → fwd_next → ...
|
|
let fwd_next = self.half_edges[he_fwd.idx()].next;
|
|
self.half_edges[he_fwd.idx()].next = new_he_fwd;
|
|
self.half_edges[new_he_fwd.idx()].prev = he_fwd;
|
|
self.half_edges[new_he_fwd.idx()].next = fwd_next;
|
|
self.half_edges[fwd_next.idx()].prev = new_he_fwd;
|
|
self.half_edges[new_he_fwd.idx()].face = self.half_edges[he_fwd.idx()].face;
|
|
|
|
// Splice new_he_bwd into backward cycle:
|
|
// Before: ... → bwd_prev → he_bwd → ...
|
|
// After: ... → bwd_prev → new_he_bwd → he_bwd → ...
|
|
let bwd_prev = self.half_edges[he_bwd.idx()].prev;
|
|
self.half_edges[bwd_prev.idx()].next = new_he_bwd;
|
|
self.half_edges[new_he_bwd.idx()].prev = bwd_prev;
|
|
self.half_edges[new_he_bwd.idx()].next = he_bwd;
|
|
self.half_edges[he_bwd.idx()].prev = new_he_bwd;
|
|
self.half_edges[new_he_bwd.idx()].face = self.half_edges[he_bwd.idx()].face;
|
|
|
|
// he_bwd now originates from vertex (it covers [vertex → v1])
|
|
self.half_edges[he_bwd.idx()].origin = vertex;
|
|
|
|
// Fix old destination's outgoing if it pointed at he_bwd
|
|
if self.vertices[old_dest.idx()].outgoing == he_bwd {
|
|
self.vertices[old_dest.idx()].outgoing = new_he_bwd;
|
|
}
|
|
|
|
// Set vertex's outgoing (may already have one if vertex is shared)
|
|
if self.vertices[vertex.idx()].outgoing.is_none() {
|
|
self.vertices[vertex.idx()].outgoing = new_he_fwd;
|
|
}
|
|
|
|
(vertex, new_edge_id)
|
|
}
|
|
|
|
/// Remove an edge, merging its two adjacent faces.
|
|
/// Returns the surviving face (lower ID, always keeps face 0).
|
|
pub fn remove_edge(&mut self, edge_id: EdgeId) -> FaceId {
|
|
let [he_fwd, he_bwd] = self.edges[edge_id.idx()].half_edges;
|
|
let face_a = self.half_edges[he_fwd.idx()].face;
|
|
let face_b = self.half_edges[he_bwd.idx()].face;
|
|
|
|
let (surviving, dying) = if face_a.0 <= face_b.0 {
|
|
(face_a, face_b)
|
|
} else {
|
|
(face_b, face_a)
|
|
};
|
|
|
|
let fwd_prev = self.half_edges[he_fwd.idx()].prev;
|
|
let fwd_next = self.half_edges[he_fwd.idx()].next;
|
|
let bwd_prev = self.half_edges[he_bwd.idx()].prev;
|
|
let bwd_next = self.half_edges[he_bwd.idx()].next;
|
|
|
|
let v1 = self.half_edges[he_fwd.idx()].origin;
|
|
let v2 = self.half_edges[he_bwd.idx()].origin;
|
|
|
|
// Splice out half-edges. Four cases based on adjacency.
|
|
if fwd_next == he_bwd && bwd_next == he_fwd {
|
|
// Degenerate 2-cycle: both vertices become isolated
|
|
self.vertices[v1.idx()].outgoing = HalfEdgeId::NONE;
|
|
self.vertices[v2.idx()].outgoing = HalfEdgeId::NONE;
|
|
} else if fwd_next == he_bwd {
|
|
// Spur: he_fwd → he_bwd consecutive. Remove both.
|
|
self.half_edges[fwd_prev.idx()].next = bwd_next;
|
|
self.half_edges[bwd_next.idx()].prev = fwd_prev;
|
|
self.vertices[v2.idx()].outgoing = HalfEdgeId::NONE;
|
|
if self.vertices[v1.idx()].outgoing == he_fwd {
|
|
self.vertices[v1.idx()].outgoing = bwd_next;
|
|
}
|
|
} else if bwd_next == he_fwd {
|
|
// Spur: he_bwd → he_fwd consecutive. Remove both.
|
|
self.half_edges[bwd_prev.idx()].next = fwd_next;
|
|
self.half_edges[fwd_next.idx()].prev = bwd_prev;
|
|
self.vertices[v1.idx()].outgoing = HalfEdgeId::NONE;
|
|
if self.vertices[v2.idx()].outgoing == he_bwd {
|
|
self.vertices[v2.idx()].outgoing = fwd_next;
|
|
}
|
|
} else {
|
|
// Normal: splice out both half-edges
|
|
self.half_edges[fwd_prev.idx()].next = bwd_next;
|
|
self.half_edges[bwd_next.idx()].prev = fwd_prev;
|
|
self.half_edges[bwd_prev.idx()].next = fwd_next;
|
|
self.half_edges[fwd_next.idx()].prev = bwd_prev;
|
|
|
|
if self.vertices[v1.idx()].outgoing == he_fwd {
|
|
self.vertices[v1.idx()].outgoing = bwd_next;
|
|
}
|
|
if self.vertices[v2.idx()].outgoing == he_bwd {
|
|
self.vertices[v2.idx()].outgoing = fwd_next;
|
|
}
|
|
}
|
|
|
|
// Reassign dying face's half-edges to surviving face
|
|
if surviving != dying && !dying.is_none() {
|
|
let walk_start = self.find_surviving_he_for_face(dying, he_fwd, he_bwd, fwd_next, bwd_next);
|
|
if !walk_start.is_none() {
|
|
self.assign_cycle_face(walk_start, surviving);
|
|
}
|
|
// Merge holes
|
|
let inner = std::mem::take(&mut self.faces[dying.idx()].inner_half_edges);
|
|
self.faces[surviving.idx()].inner_half_edges.extend(inner);
|
|
}
|
|
|
|
// Fix surviving face's outer_half_edge
|
|
if self.faces[surviving.idx()].outer_half_edge == he_fwd
|
|
|| self.faces[surviving.idx()].outer_half_edge == he_bwd
|
|
{
|
|
let replacement = [fwd_next, bwd_next]
|
|
.into_iter()
|
|
.find(|&he| he != he_fwd && he != he_bwd && !self.half_edges[he.idx()].deleted)
|
|
.unwrap_or(HalfEdgeId::NONE);
|
|
self.faces[surviving.idx()].outer_half_edge = replacement;
|
|
}
|
|
|
|
// Clean up inner_half_edges references
|
|
self.faces[surviving.idx()]
|
|
.inner_half_edges
|
|
.retain(|&he| he != he_fwd && he != he_bwd);
|
|
|
|
// Free
|
|
self.free_half_edge(he_fwd);
|
|
self.free_half_edge(he_bwd);
|
|
self.free_edge(edge_id);
|
|
if surviving != dying && !dying.is_none() && dying.0 != 0 {
|
|
self.free_face(dying);
|
|
}
|
|
|
|
surviving
|
|
}
|
|
|
|
/// Find a valid starting half-edge for walking a dying face's cycle,
|
|
/// avoiding the two half-edges being removed.
|
|
fn find_surviving_he_for_face(
|
|
&self,
|
|
dying: FaceId,
|
|
he_fwd: HalfEdgeId,
|
|
he_bwd: HalfEdgeId,
|
|
fwd_next: HalfEdgeId,
|
|
bwd_next: HalfEdgeId,
|
|
) -> HalfEdgeId {
|
|
let ohe = self.faces[dying.idx()].outer_half_edge;
|
|
if !ohe.is_none() && ohe != he_fwd && ohe != he_bwd {
|
|
return ohe;
|
|
}
|
|
for &candidate in &[fwd_next, bwd_next] {
|
|
if !candidate.is_none() && candidate != he_fwd && candidate != he_bwd {
|
|
return candidate;
|
|
}
|
|
}
|
|
HalfEdgeId::NONE
|
|
}
|
|
|
|
/// Create a face from an existing cycle of half-edges in F0.
|
|
///
|
|
/// Use this when a closed boundary exists but no face was created
|
|
/// (e.g. paint bucket on a region that hasn't been filled yet).
|
|
/// The cycle's half-edges are assigned to the new face.
|
|
/// Returns the new FaceId.
|
|
pub fn create_face_at_cycle(&mut self, cycle_he: HalfEdgeId) -> FaceId {
|
|
let face = self.alloc_face();
|
|
self.faces[face.idx()].outer_half_edge = cycle_he;
|
|
self.assign_cycle_face(cycle_he, face);
|
|
face
|
|
}
|
|
|
|
/// Re-sort all outgoing half-edges at a vertex by angle and fix the
|
|
/// fan linkage (`twin.next` / `prev`). Call this after operations that
|
|
/// add outgoing half-edges to an existing vertex without maintaining
|
|
/// the CCW fan invariant (e.g. multiple `split_edge_at_vertex` calls
|
|
/// reusing the same vertex).
|
|
pub fn rebuild_vertex_fan(&mut self, vertex: VertexId) {
|
|
let start = self.vertices[vertex.idx()].outgoing;
|
|
if start.is_none() {
|
|
return;
|
|
}
|
|
|
|
// Collect all outgoing half-edges by walking all connected sub-fans.
|
|
// The fan may be broken into disconnected loops, so we gather them
|
|
// by scanning all half-edges with origin == vertex.
|
|
let mut fan: Vec<(f64, HalfEdgeId)> = Vec::new();
|
|
for (i, he) in self.half_edges.iter().enumerate() {
|
|
if he.deleted {
|
|
continue;
|
|
}
|
|
if he.origin == vertex {
|
|
let he_id = HalfEdgeId(i as u32);
|
|
let angle = self.outgoing_angle(he_id);
|
|
fan.push((angle, he_id));
|
|
}
|
|
}
|
|
|
|
if fan.is_empty() {
|
|
return;
|
|
}
|
|
|
|
// Sort by angle CCW
|
|
fan.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
|
|
|
|
// Relink: twin(fan[i]).next = fan[(i+1) % n]
|
|
let n = fan.len();
|
|
for i in 0..n {
|
|
let cur_he = fan[i].1;
|
|
let next_he = fan[(i + 1) % n].1;
|
|
let cur_twin = self.half_edges[cur_he.idx()].twin;
|
|
self.half_edges[cur_twin.idx()].next = next_he;
|
|
self.half_edges[next_he.idx()].prev = cur_twin;
|
|
}
|
|
|
|
self.vertices[vertex.idx()].outgoing = fan[0].1;
|
|
}
|
|
|
|
/// After `rebuild_vertex_fan` re-links `next`/`prev` pointers at a vertex,
|
|
/// face assignments may be wrong in two ways:
|
|
///
|
|
/// 1. **Multiple cycles per face**: A face's single boundary was split
|
|
/// into two separate cycles. Each extra cycle gets a new face.
|
|
///
|
|
/// 2. **Pinched cycle**: A face's boundary visits a vertex more than
|
|
/// once ("figure-8" or "lollipop" shape). The cycle is split at the
|
|
/// repeated vertex into sub-cycles, each becoming its own face.
|
|
///
|
|
/// Returns the list of newly created faces.
|
|
pub fn repair_face_cycles_at_vertex(&mut self, vertex: VertexId) -> Vec<FaceId> {
|
|
let outgoing = self.vertex_outgoing(vertex);
|
|
if outgoing.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
use std::collections::HashMap;
|
|
let mut new_faces = Vec::new();
|
|
|
|
// --- Phase 1: Detect and split pinched cycles ---
|
|
//
|
|
// Walk ALL cycles touching this vertex. When a cycle visits a vertex
|
|
// twice (pinch), extract the loop sub-path as a new cycle.
|
|
// If the loop has positive area, create a new face (inheriting fill
|
|
// from an adjacent non-F0 face if the parent cycle is F0).
|
|
|
|
// Collect unique cycle start HEs touching this vertex
|
|
let mut cycle_starts: Vec<HalfEdgeId> = Vec::new();
|
|
let mut seen_cycle_reps: Vec<HalfEdgeId> = Vec::new();
|
|
for &he in &outgoing {
|
|
for start in [he, self.half_edges[he.idx()].twin] {
|
|
let cycle = self.walk_cycle(start);
|
|
let rep = cycle.iter().copied().min_by_key(|h| h.0).unwrap();
|
|
if !seen_cycle_reps.contains(&rep) {
|
|
seen_cycle_reps.push(rep);
|
|
cycle_starts.push(start);
|
|
}
|
|
}
|
|
}
|
|
|
|
for cycle_start in cycle_starts {
|
|
// Walk the cycle vertex-by-vertex, including one extra step
|
|
// to re-check the start vertex for a closing pinch.
|
|
let mut vertex_first_he: HashMap<VertexId, HalfEdgeId> = HashMap::new();
|
|
let mut cur = cycle_start;
|
|
let cycle_len = self.walk_cycle(cycle_start).len();
|
|
let mut steps = 0;
|
|
let mut finished = false;
|
|
|
|
loop {
|
|
let v = self.half_edges[cur.idx()].origin;
|
|
|
|
if let Some(&first_he) = vertex_first_he.get(&v) {
|
|
// Pinch detected! Extract the loop (first_he..last_of_loop).
|
|
let prev_of_first = self.half_edges[first_he.idx()].prev;
|
|
let last_of_loop = self.half_edges[cur.idx()].prev;
|
|
|
|
// Relink: close the loop and bridge the main cycle
|
|
self.half_edges[last_of_loop.idx()].next = first_he;
|
|
self.half_edges[first_he.idx()].prev = last_of_loop;
|
|
self.half_edges[prev_of_first.idx()].next = cur;
|
|
self.half_edges[cur.idx()].prev = prev_of_first;
|
|
|
|
// Determine area of the extracted loop
|
|
let mut area = self.cycle_signed_area(first_he);
|
|
if area.abs() < 1e-6 {
|
|
area = self.cycle_curve_signed_area(first_he);
|
|
}
|
|
|
|
// Find a non-F0 donor face at the pinch vertex
|
|
let donor_face = if area > 0.0 {
|
|
let mut df = FaceId(0);
|
|
for he_rec in self.half_edges.iter() {
|
|
if he_rec.deleted { continue; }
|
|
if he_rec.origin == v {
|
|
if he_rec.face.0 != 0 {
|
|
df = he_rec.face;
|
|
break;
|
|
}
|
|
let tf = self.half_edges[he_rec.twin.idx()].face;
|
|
if tf.0 != 0 {
|
|
df = tf;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
df
|
|
} else {
|
|
FaceId(0)
|
|
};
|
|
|
|
if area > 0.0 && donor_face.0 != 0 {
|
|
let nf = self.alloc_face();
|
|
self.faces[nf.idx()].fill_color =
|
|
self.faces[donor_face.idx()].fill_color;
|
|
self.faces[nf.idx()].image_fill =
|
|
self.faces[donor_face.idx()].image_fill;
|
|
self.faces[nf.idx()].fill_rule =
|
|
self.faces[donor_face.idx()].fill_rule;
|
|
self.faces[nf.idx()].outer_half_edge = first_he;
|
|
self.assign_cycle_face(first_he, nf);
|
|
new_faces.push(nf);
|
|
} else {
|
|
// Undo the relink
|
|
self.half_edges[last_of_loop.idx()].next = cur;
|
|
self.half_edges[cur.idx()].prev = last_of_loop;
|
|
self.half_edges[prev_of_first.idx()].next = first_he;
|
|
self.half_edges[first_he.idx()].prev = prev_of_first;
|
|
}
|
|
|
|
vertex_first_he.insert(v, cur);
|
|
} else {
|
|
vertex_first_he.insert(v, cur);
|
|
}
|
|
|
|
if finished {
|
|
break;
|
|
}
|
|
|
|
cur = self.half_edges[cur.idx()].next;
|
|
steps += 1;
|
|
if steps > cycle_len + 2 {
|
|
break;
|
|
}
|
|
// When we've come full circle, process cycle_start once
|
|
// more (to detect a closing pinch) then stop.
|
|
if cur == cycle_start {
|
|
finished = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Phase 2: Handle multiple separate cycles per face ---
|
|
// (This handles case 1: rebuild_vertex_fan split one cycle into two
|
|
// distinct cycles, without a pinch.)
|
|
let outgoing = self.vertex_outgoing(vertex);
|
|
let mut cycle_reps: Vec<(HalfEdgeId, FaceId)> = Vec::new();
|
|
let mut seen_reps: Vec<HalfEdgeId> = Vec::new();
|
|
|
|
for &he in &outgoing {
|
|
for start in [he, self.half_edges[he.idx()].twin] {
|
|
let cycle = self.walk_cycle(start);
|
|
let rep = cycle.iter().copied().min_by_key(|h| h.0).unwrap();
|
|
if !seen_reps.contains(&rep) {
|
|
let face = self.half_edges[start.idx()].face;
|
|
cycle_reps.push((rep, face));
|
|
seen_reps.push(rep);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut face_cycles: HashMap<FaceId, Vec<HalfEdgeId>> = HashMap::new();
|
|
for &(rep, face) in &cycle_reps {
|
|
face_cycles.entry(face).or_default().push(rep);
|
|
}
|
|
|
|
for (&face, cycles) in &face_cycles {
|
|
if face.0 == 0 || cycles.len() <= 1 {
|
|
continue;
|
|
}
|
|
|
|
let old_ohe = self.faces[face.idx()].outer_half_edge;
|
|
|
|
for &cycle_rep in cycles {
|
|
let has_old_ohe = !old_ohe.is_none()
|
|
&& (cycle_rep == old_ohe || self.cycle_contains(cycle_rep, old_ohe));
|
|
if has_old_ohe {
|
|
self.assign_cycle_face(cycle_rep, face);
|
|
continue;
|
|
}
|
|
|
|
let area = self.cycle_signed_area(cycle_rep);
|
|
|
|
if area > 0.0 {
|
|
let nf = self.alloc_face();
|
|
self.faces[nf.idx()].fill_color = self.faces[face.idx()].fill_color;
|
|
self.faces[nf.idx()].image_fill = self.faces[face.idx()].image_fill;
|
|
self.faces[nf.idx()].fill_rule = self.faces[face.idx()].fill_rule;
|
|
self.faces[nf.idx()].outer_half_edge = cycle_rep;
|
|
self.assign_cycle_face(cycle_rep, nf);
|
|
new_faces.push(nf);
|
|
} else {
|
|
self.assign_cycle_face(cycle_rep, face);
|
|
if !self.faces[face.idx()].inner_half_edges.contains(&cycle_rep) {
|
|
self.faces[face.idx()].inner_half_edges.push(cycle_rep);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
new_faces
|
|
}
|
|
|
|
/// Merge vertex `v_remove` into `v_keep`. Both must be at the same position
|
|
/// (or close enough). All half-edges originating from `v_remove` are re-homed
|
|
/// to `v_keep`, and the combined fan is re-sorted by angle.
|
|
pub fn merge_vertices(&mut self, v_keep: VertexId, v_remove: VertexId) {
|
|
if v_keep == v_remove {
|
|
return;
|
|
}
|
|
debug_assert!(!self.vertices[v_keep.idx()].outgoing.is_none());
|
|
debug_assert!(!self.vertices[v_remove.idx()].outgoing.is_none());
|
|
|
|
// Re-home all half-edges from v_remove to v_keep
|
|
let start = self.vertices[v_remove.idx()].outgoing;
|
|
let mut cur = start;
|
|
loop {
|
|
self.half_edges[cur.idx()].origin = v_keep;
|
|
let twin = self.half_edges[cur.idx()].twin;
|
|
cur = self.half_edges[twin.idx()].next;
|
|
if cur == start {
|
|
break;
|
|
}
|
|
}
|
|
|
|
self.vertices[v_remove.idx()].outgoing = HalfEdgeId::NONE;
|
|
self.vertices[v_remove.idx()].deleted = true;
|
|
self.free_vertices.push(v_remove.0);
|
|
|
|
// Rebuild the combined fan at v_keep
|
|
self.rebuild_vertex_fan(v_keep);
|
|
self.vertex_rtree = None;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use kurbo::Point;
|
|
|
|
fn line_curve(p0: Point, p1: Point) -> CubicBez {
|
|
let c1 = super::super::lerp_point(p0, p1, 1.0 / 3.0);
|
|
let c2 = super::super::lerp_point(p0, p1, 2.0 / 3.0);
|
|
CubicBez::new(p0, c1, c2, p1)
|
|
}
|
|
|
|
#[test]
|
|
fn insert_single_edge() {
|
|
let mut dcel = Dcel::new();
|
|
let v1 = dcel.alloc_vertex(Point::new(0.0, 0.0));
|
|
let v2 = dcel.alloc_vertex(Point::new(10.0, 0.0));
|
|
let curve = line_curve(Point::new(0.0, 0.0), Point::new(10.0, 0.0));
|
|
|
|
let (edge_id, face) = dcel.insert_edge(v1, v2, FaceId(0), curve);
|
|
assert!(!edge_id.is_none());
|
|
assert_eq!(face, FaceId(0));
|
|
|
|
// Both half-edges should form a 2-cycle
|
|
let [he_fwd, he_bwd] = dcel.edges[edge_id.idx()].half_edges;
|
|
assert_eq!(dcel.half_edges[he_fwd.idx()].next, he_bwd);
|
|
assert_eq!(dcel.half_edges[he_bwd.idx()].next, he_fwd);
|
|
assert_eq!(dcel.half_edges[he_fwd.idx()].origin, v1);
|
|
assert_eq!(dcel.half_edges[he_bwd.idx()].origin, v2);
|
|
}
|
|
|
|
#[test]
|
|
fn insert_spur() {
|
|
let mut dcel = Dcel::new();
|
|
let v1 = dcel.alloc_vertex(Point::new(0.0, 0.0));
|
|
let v2 = dcel.alloc_vertex(Point::new(10.0, 0.0));
|
|
let v3 = dcel.alloc_vertex(Point::new(10.0, 10.0));
|
|
|
|
let c1 = line_curve(Point::new(0.0, 0.0), Point::new(10.0, 0.0));
|
|
let c2 = line_curve(Point::new(10.0, 0.0), Point::new(10.0, 10.0));
|
|
|
|
dcel.insert_edge(v1, v2, FaceId(0), c1);
|
|
let (e2, _) = dcel.insert_edge(v2, v3, FaceId(0), c2);
|
|
|
|
// v3 should have outgoing pointing back toward v2
|
|
let v3_out = dcel.vertices[v3.idx()].outgoing;
|
|
assert!(!v3_out.is_none());
|
|
assert_eq!(dcel.half_edges[v3_out.idx()].origin, v3);
|
|
|
|
// Edge should exist
|
|
assert!(!e2.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn insert_triangle_no_face_in_f0() {
|
|
let mut dcel = Dcel::new();
|
|
let v1 = dcel.alloc_vertex(Point::new(0.0, 0.0));
|
|
let v2 = dcel.alloc_vertex(Point::new(10.0, 0.0));
|
|
let v3 = dcel.alloc_vertex(Point::new(5.0, 10.0));
|
|
|
|
let c1 = line_curve(Point::new(0.0, 0.0), Point::new(10.0, 0.0));
|
|
let c2 = line_curve(Point::new(10.0, 0.0), Point::new(5.0, 10.0));
|
|
let c3 = line_curve(Point::new(5.0, 10.0), Point::new(0.0, 0.0));
|
|
|
|
dcel.insert_edge(v1, v2, FaceId(0), c1);
|
|
dcel.insert_edge(v2, v3, FaceId(0), c2);
|
|
let (_e3, face) = dcel.insert_edge(v3, v1, FaceId(0), c3);
|
|
|
|
// In F0, closing a triangle should NOT create a new face
|
|
assert_eq!(face, FaceId(0));
|
|
}
|
|
|
|
#[test]
|
|
fn split_edge_creates_vertex() {
|
|
let mut dcel = Dcel::new();
|
|
let v1 = dcel.alloc_vertex(Point::new(0.0, 0.0));
|
|
let v2 = dcel.alloc_vertex(Point::new(10.0, 0.0));
|
|
let curve = line_curve(Point::new(0.0, 0.0), Point::new(10.0, 0.0));
|
|
|
|
let (edge_id, _) = dcel.insert_edge(v1, v2, FaceId(0), curve);
|
|
let (new_v, new_e) = dcel.split_edge(edge_id, 0.5);
|
|
|
|
// New vertex should be near (5, 0)
|
|
let pos = dcel.vertices[new_v.idx()].position;
|
|
assert!((pos.x - 5.0).abs() < 0.1);
|
|
assert!((pos.y - 0.0).abs() < 0.1);
|
|
|
|
// Should now have 2 edges
|
|
assert!(!new_e.is_none());
|
|
assert_ne!(edge_id, new_e);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_edge_basic() {
|
|
let mut dcel = Dcel::new();
|
|
let v1 = dcel.alloc_vertex(Point::new(0.0, 0.0));
|
|
let v2 = dcel.alloc_vertex(Point::new(10.0, 0.0));
|
|
let curve = line_curve(Point::new(0.0, 0.0), Point::new(10.0, 0.0));
|
|
|
|
let (edge_id, _) = dcel.insert_edge(v1, v2, FaceId(0), curve);
|
|
let surviving = dcel.remove_edge(edge_id);
|
|
|
|
assert_eq!(surviving, FaceId(0));
|
|
assert!(dcel.vertices[v1.idx()].outgoing.is_none());
|
|
assert!(dcel.vertices[v2.idx()].outgoing.is_none());
|
|
assert!(dcel.edges[edge_id.idx()].deleted);
|
|
}
|
|
|
|
/// Test that `repair_face_cycles_at_vertex` correctly splits a face
|
|
/// when `rebuild_vertex_fan` has broken one cycle into two.
|
|
#[test]
|
|
fn repair_face_cycles_splits_face() {
|
|
use crate::shape::ShapeColor;
|
|
|
|
let mut dcel = Dcel::new();
|
|
|
|
// Build a rectangle manually: 6 vertices, 6 edges
|
|
// The rectangle has split points on the left and right sides
|
|
// to simulate the result of splitting edges at intersection points.
|
|
//
|
|
// v3 ---- v2
|
|
// | |
|
|
// vL vR
|
|
// | |
|
|
// v0 ---- v1
|
|
let v0 = dcel.alloc_vertex(Point::new(0.0, 0.0));
|
|
let v1 = dcel.alloc_vertex(Point::new(100.0, 0.0));
|
|
let v2 = dcel.alloc_vertex(Point::new(100.0, 100.0));
|
|
let v3 = dcel.alloc_vertex(Point::new(0.0, 100.0));
|
|
let vl = dcel.alloc_vertex(Point::new(0.0, 50.0));
|
|
let vr = dcel.alloc_vertex(Point::new(100.0, 50.0));
|
|
|
|
// Insert edges forming the rectangle boundary (with split points)
|
|
// Bottom: v0 → v1
|
|
dcel.insert_edge(v0, v1, FaceId(0), line_curve(Point::new(0.0, 0.0), Point::new(100.0, 0.0)));
|
|
// Right-bottom: v1 → vR
|
|
dcel.insert_edge(v1, vr, FaceId(0), line_curve(Point::new(100.0, 0.0), Point::new(100.0, 50.0)));
|
|
// Right-top: vR → v2
|
|
dcel.insert_edge(vr, v2, FaceId(0), line_curve(Point::new(100.0, 50.0), Point::new(100.0, 100.0)));
|
|
// Top: v2 → v3
|
|
dcel.insert_edge(v2, v3, FaceId(0), line_curve(Point::new(100.0, 100.0), Point::new(0.0, 100.0)));
|
|
// Left-top: v3 → vL
|
|
dcel.insert_edge(v3, vl, FaceId(0), line_curve(Point::new(0.0, 100.0), Point::new(0.0, 50.0)));
|
|
// Left-bottom: vL → v0
|
|
dcel.insert_edge(vl, v0, FaceId(0), line_curve(Point::new(0.0, 50.0), Point::new(0.0, 0.0)));
|
|
|
|
// Create a face on the CCW interior cycle
|
|
let he_opts = dcel.vertex_outgoing(v0);
|
|
let interior_he = he_opts
|
|
.iter()
|
|
.copied()
|
|
.find(|&he| dcel.cycle_signed_area(he) > 0.0)
|
|
.expect("should have a CCW cycle");
|
|
let face = dcel.create_face_at_cycle(interior_he);
|
|
dcel.faces[face.idx()].fill_color = Some(ShapeColor::rgb(255, 0, 0));
|
|
|
|
dcel.validate();
|
|
|
|
// Now insert the cross edge vL → vR (splitting the face)
|
|
let cross_curve = line_curve(Point::new(0.0, 50.0), Point::new(100.0, 50.0));
|
|
let (_, _returned_face) = dcel.insert_edge(vl, vr, face, cross_curve);
|
|
|
|
// insert_edge_both_connected should have split the face
|
|
let filled_faces: Vec<_> = dcel
|
|
.faces
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(i, f)| *i != 0 && !f.deleted && f.fill_color.is_some())
|
|
.collect();
|
|
|
|
assert!(
|
|
filled_faces.len() >= 2,
|
|
"expected at least 2 filled faces after cross edge, got {}",
|
|
filled_faces.len(),
|
|
);
|
|
|
|
dcel.validate();
|
|
}
|
|
}
|