Lightningbeam/lightningbeam-ui/lightningbeam-core/src/dcel2/mod.rs

570 lines
17 KiB
Rust

//! 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<StrokeStyle>,
pub stroke_color: Option<ShapeColor>,
#[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<HalfEdgeId>,
pub fill_color: Option<ShapeColor>,
pub image_fill: Option<uuid::Uuid>,
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<Vec<CubicBez>>,
pub paint_points: Vec<Point>,
}
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<Vertex>,
pub half_edges: Vec<HalfEdge>,
pub edges: Vec<EdgeData>,
pub faces: Vec<Face>,
free_vertices: Vec<u32>,
free_half_edges: Vec<u32>,
free_edges: Vec<u32>,
free_faces: Vec<u32>,
#[serde(skip)]
vertex_rtree: Option<RTree<VertexEntry>>,
#[serde(skip)]
pub debug_recorder: Option<DebugRecorder>,
}
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<CubicBez>`.
pub fn bezpath_to_cubic_segments(path: &kurbo::BezPath) -> Vec<Vec<CubicBez>> {
use kurbo::PathEl;
let mut result: Vec<Vec<CubicBez>> = Vec::new();
let mut current: Vec<CubicBez> = 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
}