//! Doubly-Connected Edge List (DCEL) for planar subdivision vector drawing. //! //! Each vector layer keyframe stores a DCEL representing a Flash-style planar //! subdivision. Strokes live on edges, fills live on faces, and the topology is //! maintained such that wherever two strokes intersect there is a vertex. //! //! Half-edges leaving a vertex are maintained in sorted CCW order. This enables //! efficient face detection by ray-casting to the nearest edge and walking CCW. pub mod topology; pub mod query; pub mod stroke; pub mod region; use crate::shape::{FillRule, ShapeColor, StrokeStyle}; use kurbo::{CubicBez, Point}; use rstar::{PointDistance, RTree, RTreeObject, AABB}; use serde::{Deserialize, Serialize}; use std::fmt; // --------------------------------------------------------------------------- // Index types // --------------------------------------------------------------------------- macro_rules! define_id { ($name:ident) => { #[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct $name(pub u32); impl $name { pub const NONE: Self = Self(u32::MAX); #[inline] pub fn is_none(self) -> bool { self.0 == u32::MAX } #[inline] pub fn idx(self) -> usize { self.0 as usize } } impl fmt::Debug for $name { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_none() { write!(f, "{}(NONE)", stringify!($name)) } else { write!(f, "{}({})", stringify!($name), self.0) } } } }; } define_id!(VertexId); define_id!(HalfEdgeId); define_id!(EdgeId); define_id!(FaceId); // --------------------------------------------------------------------------- // Core structs // --------------------------------------------------------------------------- /// A vertex in the DCEL. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Vertex { pub position: Point, /// One outgoing half-edge (any one; iteration via twin.next gives the CCW fan). /// NONE if the vertex is isolated (no edges). pub outgoing: HalfEdgeId, #[serde(default)] pub deleted: bool, } /// A half-edge in the DCEL. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct HalfEdge { pub origin: VertexId, pub twin: HalfEdgeId, /// Next half-edge around the face (CCW). pub next: HalfEdgeId, /// Previous half-edge around the face (CCW). pub prev: HalfEdgeId, /// Face to the left of this half-edge. pub face: FaceId, /// Parent edge (shared between this half-edge and its twin). pub edge: EdgeId, #[serde(default)] pub deleted: bool, } /// Geometric and style data for an edge (shared by the two half-edges). #[derive(Clone, Debug, Serialize, Deserialize)] pub struct EdgeData { /// The two half-edges: [forward, backward]. /// Forward goes from curve.p0 to curve.p3. pub half_edges: [HalfEdgeId; 2], pub curve: CubicBez, pub stroke_style: Option, pub stroke_color: Option, #[serde(default)] pub deleted: bool, } /// A face (region) in the DCEL. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Face { /// One half-edge on the outer boundary. NONE for the unbounded face (face 0). pub outer_half_edge: HalfEdgeId, /// Half-edges on inner boundary cycles (holes). pub inner_half_edges: Vec, pub fill_color: Option, pub image_fill: Option, pub fill_rule: FillRule, #[serde(default)] pub deleted: bool, } // --------------------------------------------------------------------------- // Spatial index for vertex snapping // --------------------------------------------------------------------------- #[derive(Clone, Debug)] pub struct VertexEntry { pub id: VertexId, pub position: [f64; 2], } impl RTreeObject for VertexEntry { type Envelope = AABB<[f64; 2]>; fn envelope(&self) -> Self::Envelope { AABB::from_point(self.position) } } impl PointDistance for VertexEntry { fn distance_2(&self, point: &[f64; 2]) -> f64 { let dx = self.position[0] - point[0]; let dy = self.position[1] - point[1]; dx * dx + dy * dy } } // --------------------------------------------------------------------------- // Debug recorder // --------------------------------------------------------------------------- #[derive(Clone, Debug, Default)] pub struct DebugRecorder { pub strokes: Vec>, pub paint_points: Vec, } impl DebugRecorder { pub fn record_stroke(&mut self, segments: &[CubicBez]) { self.strokes.push(segments.to_vec()); } pub fn record_paint(&mut self, point: Point) { self.paint_points.push(point); } pub fn dump_test(&self, name: &str) { eprintln!(" #[test]"); eprintln!(" fn {name}() {{"); eprintln!(" let mut dcel = Dcel::new();"); eprintln!(); for (i, stroke) in self.strokes.iter().enumerate() { eprintln!(" // Stroke {i}"); eprintln!(" dcel.insert_stroke(&["); for seg in stroke { eprintln!( " CubicBez::new(Point::new({:.1}, {:.1}), Point::new({:.1}, {:.1}), Point::new({:.1}, {:.1}), Point::new({:.1}, {:.1})),", seg.p0.x, seg.p0.y, seg.p1.x, seg.p1.y, seg.p2.x, seg.p2.y, seg.p3.x, seg.p3.y, ); } eprintln!(" ], None, None, 5.0);"); eprintln!(); } for (i, pt) in self.paint_points.iter().enumerate() { eprintln!(" // Paint {i}"); eprintln!( " let _f{i} = dcel.find_face_at_point(Point::new({:.1}, {:.1}));", pt.x, pt.y ); } eprintln!(" }}"); } pub fn dump_and_reset(&mut self, name: &str) { self.dump_test(name); self.strokes.clear(); self.paint_points.clear(); } } // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- /// Default snap epsilon in document coordinate units. pub const DEFAULT_SNAP_EPSILON: f64 = 0.5; // --------------------------------------------------------------------------- // DCEL container // --------------------------------------------------------------------------- #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Dcel { pub vertices: Vec, pub half_edges: Vec, pub edges: Vec, pub faces: Vec, free_vertices: Vec, free_half_edges: Vec, free_edges: Vec, free_faces: Vec, #[serde(skip)] vertex_rtree: Option>, #[serde(skip)] pub debug_recorder: Option, } impl Default for Dcel { fn default() -> Self { Self::new() } } impl Dcel { /// Create a new empty DCEL with just the unbounded outer face (face 0). pub fn new() -> Self { let unbounded = Face { outer_half_edge: HalfEdgeId::NONE, inner_half_edges: Vec::new(), fill_color: None, image_fill: None, fill_rule: FillRule::NonZero, deleted: false, }; let debug_recorder = if std::env::var("DAW_DCEL_RECORD").is_ok() { Some(DebugRecorder::default()) } else { None }; Dcel { vertices: Vec::new(), half_edges: Vec::new(), edges: Vec::new(), faces: vec![unbounded], free_vertices: Vec::new(), free_half_edges: Vec::new(), free_edges: Vec::new(), free_faces: Vec::new(), vertex_rtree: None, debug_recorder, } } // ----------------------------------------------------------------------- // Debug recording // ----------------------------------------------------------------------- pub fn set_recording(&mut self, enabled: bool) { if enabled { self.debug_recorder.get_or_insert_with(DebugRecorder::default); } else { self.debug_recorder = None; } } pub fn is_recording(&self) -> bool { self.debug_recorder.is_some() } pub fn dump_recorded_test(&mut self, name: &str) { if let Some(ref mut rec) = self.debug_recorder { rec.dump_and_reset(name); } } pub fn record_paint_point(&mut self, point: Point) { if let Some(ref mut rec) = self.debug_recorder { rec.record_paint(point); } } // ----------------------------------------------------------------------- // Allocation // ----------------------------------------------------------------------- pub fn alloc_vertex(&mut self, position: Point) -> VertexId { let id = if let Some(idx) = self.free_vertices.pop() { let id = VertexId(idx); self.vertices[id.idx()] = Vertex { position, outgoing: HalfEdgeId::NONE, deleted: false, }; id } else { let id = VertexId(self.vertices.len() as u32); self.vertices.push(Vertex { position, outgoing: HalfEdgeId::NONE, deleted: false, }); id }; self.vertex_rtree = None; id } pub fn alloc_half_edge_pair(&mut self) -> (HalfEdgeId, HalfEdgeId) { let tombstone = HalfEdge { origin: VertexId::NONE, twin: HalfEdgeId::NONE, next: HalfEdgeId::NONE, prev: HalfEdgeId::NONE, face: FaceId::NONE, edge: EdgeId::NONE, deleted: false, }; let alloc_one = |dcel: &mut Dcel| -> HalfEdgeId { if let Some(idx) = dcel.free_half_edges.pop() { let id = HalfEdgeId(idx); dcel.half_edges[id.idx()] = tombstone.clone(); id } else { let id = HalfEdgeId(dcel.half_edges.len() as u32); dcel.half_edges.push(tombstone.clone()); id } }; let a = alloc_one(self); let b = alloc_one(self); self.half_edges[a.idx()].twin = b; self.half_edges[b.idx()].twin = a; (a, b) } pub fn alloc_edge(&mut self, curve: CubicBez) -> EdgeId { let data = EdgeData { half_edges: [HalfEdgeId::NONE, HalfEdgeId::NONE], curve, stroke_style: None, stroke_color: None, deleted: false, }; if let Some(idx) = self.free_edges.pop() { let id = EdgeId(idx); self.edges[id.idx()] = data; id } else { let id = EdgeId(self.edges.len() as u32); self.edges.push(data); id } } pub fn alloc_face(&mut self) -> FaceId { let face = Face { outer_half_edge: HalfEdgeId::NONE, inner_half_edges: Vec::new(), fill_color: None, image_fill: None, fill_rule: FillRule::NonZero, deleted: false, }; if let Some(idx) = self.free_faces.pop() { let id = FaceId(idx); self.faces[id.idx()] = face; id } else { let id = FaceId(self.faces.len() as u32); self.faces.push(face); id } } // ----------------------------------------------------------------------- // Deallocation // ----------------------------------------------------------------------- pub fn free_vertex(&mut self, id: VertexId) { debug_assert!(!id.is_none()); self.vertices[id.idx()].deleted = true; self.free_vertices.push(id.0); self.vertex_rtree = None; } pub fn free_half_edge(&mut self, id: HalfEdgeId) { debug_assert!(!id.is_none()); self.half_edges[id.idx()].deleted = true; self.free_half_edges.push(id.0); } pub fn free_edge(&mut self, id: EdgeId) { debug_assert!(!id.is_none()); self.edges[id.idx()].deleted = true; self.free_edges.push(id.0); } pub fn free_face(&mut self, id: FaceId) { debug_assert!(!id.is_none()); debug_assert!(id.0 != 0, "cannot free the unbounded face"); self.faces[id.idx()].deleted = true; self.free_faces.push(id.0); } // ----------------------------------------------------------------------- // Accessors // ----------------------------------------------------------------------- #[inline] pub fn vertex(&self, id: VertexId) -> &Vertex { &self.vertices[id.idx()] } #[inline] pub fn vertex_mut(&mut self, id: VertexId) -> &mut Vertex { &mut self.vertices[id.idx()] } #[inline] pub fn half_edge(&self, id: HalfEdgeId) -> &HalfEdge { &self.half_edges[id.idx()] } #[inline] pub fn half_edge_mut(&mut self, id: HalfEdgeId) -> &mut HalfEdge { &mut self.half_edges[id.idx()] } #[inline] pub fn edge(&self, id: EdgeId) -> &EdgeData { &self.edges[id.idx()] } #[inline] pub fn edge_mut(&mut self, id: EdgeId) -> &mut EdgeData { &mut self.edges[id.idx()] } #[inline] pub fn face(&self, id: FaceId) -> &Face { &self.faces[id.idx()] } #[inline] pub fn face_mut(&mut self, id: FaceId) -> &mut Face { &mut self.faces[id.idx()] } /// Destination vertex of a half-edge (origin of its twin). #[inline] pub fn half_edge_dest(&self, he: HalfEdgeId) -> VertexId { let twin = self.half_edge(he).twin; self.half_edge(twin).origin } } // --------------------------------------------------------------------------- // Bezier utilities // --------------------------------------------------------------------------- /// Split a cubic bezier at parameter t using de Casteljau's algorithm. pub fn subdivide_cubic(c: CubicBez, t: f64) -> (CubicBez, CubicBez) { let p01 = lerp_point(c.p0, c.p1, t); let p12 = lerp_point(c.p1, c.p2, t); let p23 = lerp_point(c.p2, c.p3, t); let p012 = lerp_point(p01, p12, t); let p123 = lerp_point(p12, p23, t); let p0123 = lerp_point(p012, p123, t); ( CubicBez::new(c.p0, p01, p012, p0123), CubicBez::new(p0123, p123, p23, c.p3), ) } /// Extract subsegment of a cubic bezier for parameter range [t0, t1]. pub fn subsegment_cubic(c: CubicBez, t0: f64, t1: f64) -> CubicBez { if (t0).abs() < 1e-10 && (t1 - 1.0).abs() < 1e-10 { return c; } if (t0).abs() < 1e-10 { subdivide_cubic(c, t1).0 } else if (t1 - 1.0).abs() < 1e-10 { subdivide_cubic(c, t0).1 } else { let (_, upper) = subdivide_cubic(c, t0); let remapped_t1 = (t1 - t0) / (1.0 - t0); subdivide_cubic(upper, remapped_t1).0 } } #[inline] pub fn lerp_point(a: Point, b: Point, t: f64) -> Point { Point::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t) } /// Convert a `BezPath` into a list of sub-paths, each a `Vec`. pub fn bezpath_to_cubic_segments(path: &kurbo::BezPath) -> Vec> { use kurbo::PathEl; let mut result: Vec> = Vec::new(); let mut current: Vec = Vec::new(); let mut subpath_start = Point::ZERO; let mut cursor = Point::ZERO; for el in path.elements() { match *el { PathEl::MoveTo(p) => { if !current.is_empty() { result.push(std::mem::take(&mut current)); } subpath_start = p; cursor = p; } PathEl::LineTo(p) => { let c1 = lerp_point(cursor, p, 1.0 / 3.0); let c2 = lerp_point(cursor, p, 2.0 / 3.0); current.push(CubicBez::new(cursor, c1, c2, p)); cursor = p; } PathEl::QuadTo(p1, p2) => { let cp1 = Point::new( cursor.x + (2.0 / 3.0) * (p1.x - cursor.x), cursor.y + (2.0 / 3.0) * (p1.y - cursor.y), ); let cp2 = Point::new( p2.x + (2.0 / 3.0) * (p1.x - p2.x), p2.y + (2.0 / 3.0) * (p1.y - p2.y), ); current.push(CubicBez::new(cursor, cp1, cp2, p2)); cursor = p2; } PathEl::CurveTo(p1, p2, p3) => { current.push(CubicBez::new(cursor, p1, p2, p3)); cursor = p3; } PathEl::ClosePath => { let dist = ((cursor.x - subpath_start.x).powi(2) + (cursor.y - subpath_start.y).powi(2)) .sqrt(); if dist > 1e-9 { let c1 = lerp_point(cursor, subpath_start, 1.0 / 3.0); let c2 = lerp_point(cursor, subpath_start, 2.0 / 3.0); current.push(CubicBez::new(cursor, c1, c2, subpath_start)); } cursor = subpath_start; if !current.is_empty() { result.push(std::mem::take(&mut current)); } } } } if !current.is_empty() { result.push(current); } result }