Merge branch 'master' into fix-rgb-decimal-sliding

This commit is contained in:
Jatin Bharti 2025-12-21 08:45:56 +05:30 committed by GitHub
commit b04d8c057b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 328 additions and 11 deletions

1
Cargo.lock generated
View file

@ -6716,6 +6716,7 @@ dependencies = [
"bytemuck",
"core-types",
"dyn-any",
"fixedbitset",
"glam",
"kurbo 0.12.0",
"log",

View file

@ -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<Vector> {
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<Vector> {
});
}
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<Vector> {
(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<Vector> {
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<Vector> {
});
// 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<Vector> {
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<Vector> {
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<Vector> {
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.),

View file

@ -31,3 +31,4 @@ tinyvec = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }
fixedbitset = "0.5.7"

View file

@ -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<T: ControlPoints + ParamCurveDeriv> 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)]

View file

@ -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<FaceSide>,
face_start: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct FaceIterator<'a, Upstream> {
vector: &'a Vector<Upstream>,
faces: Faces,
current_face: usize,
}
impl<Upstream> FaceIterator<'_, Upstream> {
fn new<'a>(faces: Faces, vector: &'a Vector<Upstream>) -> 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<Upstream> Iterator for FaceIterator<'_, Upstream> {
type Item = kurbo::BezPath;
fn next(&mut self) -> Option<Self::Item> {
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<Upstream> Vector<Upstream> {
/// 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<Upstream> Vector<Upstream> {
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<FaceSide>> = 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<Vec<FaceSide>>, 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)]