From e73e524f3df1cb3fd0cd394ecff3a1790cf466fd Mon Sep 17 00:00:00 2001 From: Nicholas Liu <86247452+liunicholas6@users.noreply.github.com> Date: Sat, 20 Dec 2025 18:59:37 -0800 Subject: [PATCH] Implement proper fill rendering for vector meshes (#3474) * Implement branching mesh rendering for SVG * Patch mesh fill for Vello renderer * Patch tangent_at_start and tangent_at_end --------- Co-authored-by: Keavon Chambers --- Cargo.lock | 1 + .../libraries/rendering/src/renderer.rs | 62 +++++- node-graph/libraries/vector-types/Cargo.toml | 1 + .../libraries/vector-types/src/vector/misc.rs | 89 ++++++++- .../src/vector/vector_attributes.rs | 186 +++++++++++++++++- 5 files changed, 328 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86c97660c..24637bbd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6716,6 +6716,7 @@ dependencies = [ "bytemuck", "core-types", "dyn-any", + "fixedbitset", "glam", "kurbo 0.12.0", "log", diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 9ff677299..79bf08742 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -22,6 +22,7 @@ use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, Stroke, StrokeAlign}; use graphic_types::{Artboard, Graphic}; use kurbo::Affine; +use kurbo::Shape; use num_traits::Zero; use std::collections::{HashMap, HashSet}; use std::fmt::Write; @@ -729,10 +730,10 @@ impl Render for Table { let can_draw_aligned_stroke = path_is_closed && vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()); let can_use_paint_order = !(row.element.style.fill().is_none() || !row.element.style.fill().is_opaque() || mask_type == MaskType::Clip); - let needs_separate_fill = can_draw_aligned_stroke && !can_use_paint_order; + let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order; let wants_stroke_below = vector.style.stroke().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow); - if needs_separate_fill && !wants_stroke_below { + if needs_separate_alignment_fill && !wants_stroke_below { render.leaf_tag("path", |attributes| { attributes.push("d", path.clone()); let matrix = format_transform_matrix(element_transform); @@ -753,7 +754,7 @@ impl Render for Table { }); } - let push_id = needs_separate_fill.then_some({ + let push_id = needs_separate_alignment_fill.then_some({ let id = format!("alignment-{}", generate_uuid()); let mut element = row.element.clone(); @@ -770,6 +771,32 @@ impl Render for Table { (id, mask_type, vector_row) }); + if vector.is_branching() { + for mut face_path in vector.construct_faces().filter(|face| !(face.area() < 0.0)) { + face_path.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); + + let face_d = face_path.to_svg(); + render.leaf_tag("path", |attributes| { + attributes.push("d", face_d.clone()); + let matrix = format_transform_matrix(element_transform); + if !matrix.is_empty() { + attributes.push("transform", matrix); + } + let mut style = row.element.style.clone(); + style.clear_stroke(); + let fill_only = style.render( + &mut attributes.0.svg_defs, + element_transform, + applied_stroke_transform, + bounds_matrix, + transformed_bounds_matrix, + render_params, + ); + attributes.push_val(fill_only); + }); + } + } + render.leaf_tag("path", |attributes| { attributes.push("d", path.clone()); let matrix = format_transform_matrix(element_transform); @@ -807,7 +834,7 @@ impl Render for Table { render_params.override_paint_order = can_draw_aligned_stroke && can_use_paint_order; let mut style = row.element.style.clone(); - if needs_separate_fill { + if needs_separate_alignment_fill || vector.is_branching() { style.clear_fill(); } @@ -830,7 +857,7 @@ impl Render for Table { }); // When splitting passes and stroke is below, draw the fill after the stroke. - if needs_separate_fill && wants_stroke_below { + if needs_separate_alignment_fill && wants_stroke_below { render.leaf_tag("path", |attributes| { attributes.push("d", path); let matrix = format_transform_matrix(element_transform); @@ -916,10 +943,10 @@ impl Render for Table { let wants_stroke_below = row.element.style.stroke().is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow); // Closures to avoid duplicated fill/stroke drawing logic - let do_fill = |scene: &mut Scene| match row.element.style.fill() { + let do_fill_path = |scene: &mut Scene, path: &kurbo::BezPath| match row.element.style.fill() { Fill::Solid(color) => { let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])); - scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path); + scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path); } Fill::Gradient(gradient) => { let mut stops = peniko::ColorStops::new(); @@ -971,11 +998,28 @@ impl Render for Table { Default::default() }; let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array()); - scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path); + scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), path); } Fill::None => {} }; + let do_fill = |scene: &mut Scene| { + if row.element.is_branching() { + // For branching paths, fill each face separately + for mut face_path in row.element.construct_faces().filter(|face| !(face.area() < 0.0)) { + face_path.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); + let mut kurbo_path = kurbo::BezPath::new(); + for element in face_path { + kurbo_path.push(element); + } + do_fill_path(scene, &kurbo_path); + } + } else { + // Simple fill of the entire path + do_fill_path(scene, &path); + } + }; + let do_stroke = |scene: &mut Scene, width_scale: f64| { if let Some(stroke) = row.element.style.stroke() { let color = match stroke.color { @@ -1090,7 +1134,7 @@ impl Render for Table { false => [Op::Fill, Op::Stroke], // Default }; - for operation in order { + for operation in &order { match operation { Op::Fill => do_fill(scene), Op::Stroke => do_stroke(scene, 1.), diff --git a/node-graph/libraries/vector-types/Cargo.toml b/node-graph/libraries/vector-types/Cargo.toml index 8eead4783..b7c6704f1 100644 --- a/node-graph/libraries/vector-types/Cargo.toml +++ b/node-graph/libraries/vector-types/Cargo.toml @@ -31,3 +31,4 @@ tinyvec = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } +fixedbitset = "0.5.7" diff --git a/node-graph/libraries/vector-types/src/vector/misc.rs b/node-graph/libraries/vector-types/src/vector/misc.rs index 63dc5649a..088e5daaf 100644 --- a/node-graph/libraries/vector-types/src/vector/misc.rs +++ b/node-graph/libraries/vector-types/src/vector/misc.rs @@ -4,7 +4,7 @@ use crate::subpath::{BezierHandles, ManipulatorGroup}; use crate::vector::{SegmentId, Vector}; use dyn_any::DynAny; use glam::DVec2; -use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez}; +use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, PathSeg, Point, QuadBez}; use std::ops::Sub; /// Represents different geometric interpretations of calculating the centroid (center of mass). @@ -246,6 +246,93 @@ pub fn pathseg_abs_diff_eq(seg1: PathSeg, seg2: PathSeg, max_abs_diff: f64) -> b seg1_points.len() == seg2_points.len() && seg1_points.into_iter().zip(seg2_points).all(|(a, b)| cmp(a.x, b.x) && cmp(a.y, b.y)) } +pub trait Tangent { + fn tangent_at(&self, t: f64) -> DVec2; + + fn tangent_at_start(&self) -> DVec2 { + self.tangent_at(0.0) + } + + fn tangent_at_end(&self) -> DVec2 { + self.tangent_at(1.0) + } +} + +trait ControlPoints { + type Points: AsRef<[Point]>; + fn control_points(&self) -> Self::Points; +} + +impl ControlPoints for kurbo::Line { + type Points = [Point; 2]; + fn control_points(&self) -> Self::Points { + [self.p0, self.p1] + } +} + +impl ControlPoints for kurbo::QuadBez { + type Points = [Point; 3]; + fn control_points(&self) -> Self::Points { + [self.p0, self.p1, self.p2] + } +} + +impl ControlPoints for kurbo::CubicBez { + type Points = [Point; 4]; + fn control_points(&self) -> Self::Points { + [self.p0, self.p1, self.p2, self.p3] + } +} + +impl Tangent for T { + fn tangent_at(&self, t: f64) -> DVec2 { + point_to_dvec2(self.deriv().eval(t)) + } + + fn tangent_at_start(&self) -> DVec2 { + let pts = self.control_points(); + let pts = pts.as_ref(); + let mut iter = pts.iter(); + iter.next() + .and_then(|&start| iter.find(|&&p| p != start).map(|&p| DVec2 { x: p.x - start.x, y: p.y - start.y })) + .unwrap_or_default() + } + + fn tangent_at_end(&self) -> DVec2 { + let pts = self.control_points(); + let pts = pts.as_ref(); + let mut iter = pts.iter().rev(); + iter.next() + .and_then(|&end| iter.find(|&&p| p != end).map(|&p| DVec2 { x: end.x - p.x, y: end.y - p.y })) + .unwrap_or_default() + } +} + +impl Tangent for kurbo::PathSeg { + fn tangent_at(&self, t: f64) -> DVec2 { + match self { + PathSeg::Line(line) => line.tangent_at(t), + PathSeg::Quad(quad) => quad.tangent_at(t), + PathSeg::Cubic(cubic) => cubic.tangent_at(t), + } + } + + fn tangent_at_start(&self) -> DVec2 { + match self { + PathSeg::Line(line) => line.tangent_at_start(), + PathSeg::Quad(quad) => quad.tangent_at_start(), + PathSeg::Cubic(cubic) => cubic.tangent_at_start(), + } + } + + fn tangent_at_end(&self) -> DVec2 { + match self { + PathSeg::Line(line) => line.tangent_at_end(), + PathSeg::Quad(quad) => quad.tangent_at_end(), + PathSeg::Cubic(cubic) => cubic.tangent_at_end(), + } + } +} /// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curvature). #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)] diff --git a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs index 5d2549aa4..5aaaec5da 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs @@ -1,7 +1,8 @@ use crate::subpath::{Bezier, BezierHandles, Identifier, ManipulatorGroup, Subpath}; -use crate::vector::misc::{HandleId, dvec2_to_point}; +use crate::vector::misc::{HandleId, Tangent, dvec2_to_point}; use crate::vector::vector_types::Vector; use dyn_any::DynAny; +use fixedbitset::FixedBitSet; use glam::{DAffine2, DVec2}; use kurbo::{CubicBez, Line, PathSeg, QuadBez}; use std::collections::HashMap; @@ -684,6 +685,113 @@ impl FoundSubpath { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +struct FaceSide { + segment_index: usize, + reversed: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct FaceSideSet { + set: FixedBitSet, +} +impl FaceSideSet { + fn new(size: usize) -> Self { + Self { + set: FixedBitSet::with_capacity(size * 2), + } + } + + fn index(&self, side: FaceSide) -> usize { + (side.segment_index << 1) | (side.reversed as usize) + } + + fn insert(&mut self, side: FaceSide) { + self.set.insert(self.index(side)); + } + + fn contains(&self, side: FaceSide) -> bool { + self.set.contains(self.index(side)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct Faces { + sides: Vec, + face_start: Vec, +} + +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct FaceIterator<'a, Upstream> { + vector: &'a Vector, + faces: Faces, + current_face: usize, +} + +impl FaceIterator<'_, Upstream> { + fn new<'a>(faces: Faces, vector: &'a Vector) -> FaceIterator<'a, Upstream> { + FaceIterator { vector, faces, current_face: 0 } + } + + fn get_point(&self, point: usize) -> kurbo::Point { + dvec2_to_point(self.vector.point_domain.positions()[point]) + } +} + +impl Iterator for FaceIterator<'_, Upstream> { + type Item = kurbo::BezPath; + fn next(&mut self) -> Option { + let start_side = self.faces.face_start.get(self.current_face).copied()?; + self.current_face += 1; + let end_side = self.faces.face_start.get(self.current_face).copied().unwrap_or(self.faces.sides.len()); + + let mut path = kurbo::BezPath::new(); + + let segment_domain = &self.vector.segment_domain; + let first_side = self.faces.sides.get(start_side)?; + let start_point_index = if first_side.reversed { + segment_domain.end_point[first_side.segment_index] + } else { + segment_domain.start_point[first_side.segment_index] + }; + path.move_to(self.get_point(start_point_index)); + for side in &self.faces.sides[start_side..end_side] { + let (handle, end_index) = match side.reversed { + false => (segment_domain.handles[side.segment_index], segment_domain.end_point[side.segment_index]), + true => (segment_domain.handles[side.segment_index].reversed(), segment_domain.start_point[side.segment_index]), + }; + let path_element = match handle { + BezierHandles::Linear => kurbo::PathEl::LineTo(self.get_point(end_index)), + BezierHandles::Quadratic { handle } => kurbo::PathEl::QuadTo(dvec2_to_point(handle), self.get_point(end_index)), + BezierHandles::Cubic { handle_start, handle_end } => kurbo::PathEl::CurveTo(dvec2_to_point(handle_start), dvec2_to_point(handle_end), self.get_point(end_index)), + }; + path.push(path_element); + } + + Some(path) + } +} + +impl Faces { + pub fn new() -> Self { + Self { + sides: Vec::new(), + face_start: Vec::new(), + } + } + pub fn add_side(&mut self, side: FaceSide) { + self.sides.push(side); + } + pub fn start_new_face(&mut self) { + self.face_start.push(self.sides.len()); + } + pub fn backtrack(&mut self) { + if let Some(last_start) = self.face_start.pop() { + self.sides.truncate(last_start); + } + } +} + impl Vector { /// Construct a [`kurbo::PathSeg`] by resolving the points from their ids. fn path_segment_from_index(&self, start: usize, end: usize, handles: BezierHandles) -> PathSeg { @@ -989,6 +1097,82 @@ impl Vector { self.segment_domain.map_ids(&id_map); self.region_domain.map_ids(&id_map); } + + pub fn is_branching(&self) -> bool { + (0..self.point_domain.len()).any(|point_index| self.segment_domain.connected_count(point_index) > 2) + } + + pub fn construct_faces(&self) -> FaceIterator<'_, Upstream> { + let mut adjacency: Vec> = vec![Vec::new(); self.point_domain.len()]; + for (segment_index, (&start, &end)) in self.segment_domain.start_point.iter().zip(&self.segment_domain.end_point).enumerate() { + adjacency[start].push(FaceSide { segment_index, reversed: false }); + adjacency[end].push(FaceSide { segment_index, reversed: true }); + } + + for neighbors in &mut adjacency { + neighbors.sort_by(|a, b| { + let angle = [a, b].map(|side| { + let curve = PathSeg::from(self.path_segment_from_index( + self.segment_domain.start_point[side.segment_index], + self.segment_domain.end_point[side.segment_index], + self.segment_domain.handles[side.segment_index], + )); + let curve = if side.reversed { curve.reverse() } else { curve }; + let tangent = curve.tangent_at_start(); + tangent.y.atan2(tangent.x) + }); + angle[0].partial_cmp(&angle[1]).unwrap_or(std::cmp::Ordering::Equal) + }) + } + + let mut faces: Faces = Faces::new(); + let mut seen = FaceSideSet::new(self.segment_domain.id.len()); + + for segment_index in 0..self.segment_domain.id.len() { + for &reversed in &[false, true] { + let side = FaceSide { segment_index, reversed }; + if seen.contains(side) { + continue; + } + if (self.construct_face(&adjacency, side, &mut faces, &mut seen)).is_none() { + faces.backtrack(); + } + } + } + + return FaceIterator::new(faces, self); + } + + fn construct_face(&self, adjacency: &Vec>, first: FaceSide, faces: &mut Faces, seen: &mut FaceSideSet) -> Option<()> { + faces.start_new_face(); + let max_iterations = self.segment_domain.id.len() * 2; + let mut side = first; + for _iteration in 1..max_iterations { + if seen.contains(side) { + log::debug!("Encountered seen side {:?}, aborting face construction", side); + return None; + } + seen.insert(side); + faces.add_side(side.clone()); + let next_vertex = if side.reversed { + self.segment_domain.start_point[side.segment_index] + } else { + self.segment_domain.end_point[side.segment_index] + }; + let neighbors = &adjacency[next_vertex]; + let side_index = neighbors.iter().position(|s| { + FaceSide { + segment_index: s.segment_index, + reversed: !s.reversed, + } == side + })?; + side = neighbors[(side_index + 1) % neighbors.len()]; + if side == first { + return Some(()); + } + } + None + } } #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]