work on dcel

This commit is contained in:
Skyler Lehmkuhl 2026-03-01 00:35:02 -05:00
parent 14a2b0a4c2
commit 9edfc2086a
4 changed files with 108 additions and 4 deletions

View File

@ -4,7 +4,7 @@
//! `Dcel::insert_stroke()`. Undo is handled by snapshotting the DCEL.
use crate::action::Action;
use crate::dcel::{bezpath_to_cubic_segments, Dcel, DEFAULT_SNAP_EPSILON};
use crate::dcel::{bezpath_to_cubic_segments, Dcel, FaceId, DEFAULT_SNAP_EPSILON};
use crate::document::Document;
use crate::layer::AnyLayer;
use crate::shape::{ShapeColor, StrokeStyle};
@ -87,8 +87,24 @@ impl Action for AddShapeAction {
// Apply fill to new faces if this is a closed shape with fill
if self.is_closed {
if let Some(ref fill) = self.fill_color {
for face_id in &result.new_faces {
dcel.face_mut(*face_id).fill_color = Some(fill.clone());
if !result.new_faces.is_empty() {
for face_id in &result.new_faces {
dcel.face_mut(*face_id).fill_color = Some(fill.clone());
}
} else if let Some(&first_edge) = result.new_edges.first() {
// Closed shape in F0 — no face was auto-created.
// One half-edge of the first new edge is on the interior cycle.
// Pick the side with positive signed area (CCW winding).
let [he_a, he_b] = dcel.edge(first_edge).half_edges;
let interior_he = if dcel.cycle_signed_area(he_a) > 0.0 {
he_a
} else {
he_b
};
if dcel.half_edge(interior_he).face == FaceId(0) {
let face_id = dcel.create_face_at_cycle(interior_he);
dcel.face_mut(face_id).fill_color = Some(fill.clone());
}
}
}
}

View File

@ -32,6 +32,23 @@ impl Dcel {
result
}
/// Compute the signed area of the cycle starting at `start`.
/// Positive = CCW (interior), negative = CW (exterior).
pub fn cycle_signed_area(&self, start: HalfEdgeId) -> f64 {
let mut area = 0.0;
let mut cur = start;
loop {
let p0 = self.vertices[self.half_edges[cur.idx()].origin.idx()].position;
cur = self.half_edges[cur.idx()].next;
let p1 = self.vertices[self.half_edges[cur.idx()].origin.idx()].position;
area += p0.x * p1.y - p1.x * p0.y;
if cur == start {
break;
}
}
area * 0.5
}
/// Get all half-edges on a face's outer boundary.
pub fn face_boundary(&self, face_id: FaceId) -> Vec<HalfEdgeId> {
let ohe = self.faces[face_id.idx()].outer_half_edge;

View File

@ -874,6 +874,70 @@ mod tests {
dcel.validate();
}
/// Rectangle with a face, then a stroke drawn across it.
/// The stroke should split two rectangle edges and create sub-edges
/// inside and outside the face. All face assignments must be consistent.
#[test]
fn stroke_across_filled_rectangle() {
let mut dcel = Dcel::new();
// Insert rectangle as 4 line segments (like bezpath_to_cubic_segments would)
let r = 100.0;
let segs = [
// bottom: (0,0) → (r,0)
CubicBez::new(
Point::new(0.0, 0.0), Point::new(r / 3.0, 0.0),
Point::new(2.0 * r / 3.0, 0.0), Point::new(r, 0.0),
),
// right: (r,0) → (r,r)
CubicBez::new(
Point::new(r, 0.0), Point::new(r, r / 3.0),
Point::new(r, 2.0 * r / 3.0), Point::new(r, r),
),
// top: (r,r) → (0,r)
CubicBez::new(
Point::new(r, r), Point::new(2.0 * r / 3.0, r),
Point::new(r / 3.0, r), Point::new(0.0, r),
),
// left: (0,r) → (0,0)
CubicBez::new(
Point::new(0.0, r), Point::new(0.0, 2.0 * r / 3.0),
Point::new(0.0, r / 3.0), Point::new(0.0, 0.0),
),
];
let rect_result = dcel.insert_stroke(&segs, None, None, 1.0);
println!("Rectangle: {} edges, {} vertices",
rect_result.new_edges.len(), rect_result.new_vertices.len());
// Create a face on the interior cycle (like add_shape does)
let first_edge = rect_result.new_edges[0];
let [he_a, he_b] = dcel.edge(first_edge).half_edges;
let interior_he = if dcel.cycle_signed_area(he_a) > 0.0 { he_a } else { he_b };
let face = dcel.create_face_at_cycle(interior_he);
println!("Created face {:?}", face);
dcel.validate();
// Now draw a horizontal stroke across the rectangle at y=50
// from x=-50 to x=150 (extending beyond both sides)
let stroke = CubicBez::new(
Point::new(-50.0, 50.0), Point::new(16.0, 50.0),
Point::new(83.0, 50.0), Point::new(150.0, 50.0),
);
let stroke_result = dcel.insert_stroke(&[stroke], None, None, 1.0);
println!("Stroke: {} edges, {} splits, {} vertices",
stroke_result.new_edges.len(), stroke_result.split_edges.len(),
stroke_result.new_vertices.len());
// Should have split 2 rectangle edges (left and right sides)
assert_eq!(stroke_result.split_edges.len(), 2,
"stroke should cross left and right sides of rectangle");
dcel.validate();
}
#[test]
fn insert_stroke_self_intersecting_segment() {
let mut dcel = Dcel::new();

View File

@ -224,7 +224,14 @@ impl Dcel {
let into_v1 = self.half_edges[ccw_v1.idx()].prev;
let into_v2 = self.half_edges[ccw_v2.idx()].prev;
let actual_face = self.half_edges[into_v1.idx()].face;
let face_v1 = self.half_edges[into_v1.idx()].face;
let face_v2 = self.half_edges[into_v2.idx()].face;
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
);
let actual_face = face_v1;
// Splice:
// into_v1 → he_fwd → ccw_v2 → ...