Add rank-0 Item leaf variants to the Graphic enum alongside its List variants (#4437)

* Add the rank-0 Item leaf variants to the Graphic enum, rendering leaves and list rows through shared per-row logic

* Traverse the rank-0 Graphic leaf variants in the vector node helpers and flattening

* Reach rank-0 vector graphics from the styling and gradient-fitting helpers

* Two code review fixes

* Fix comment
This commit is contained in:
Keavon Chambers
2026-08-16 02:24:15 -07:00
committed by GitHub
parent ba7cbd83bc
commit c507b35645
13 changed files with 2412 additions and 1665 deletions

View File

@@ -47,14 +47,31 @@ trait VectorListIterMut {
impl VectorListIterMut for List<Graphic> {
fn for_each_vector_list_mut(&mut self, mut f: impl FnMut(&mut List<Vector>)) {
for graphic in self.iter_element_values_mut() {
if let Some(vector_list) = graphic.as_vector_list_mut() {
f(vector_list);
};
match graphic {
// A lone vector is lifted into a one-item list for the duration of the call, so the shared per-list logic reaches it
Graphic::Vector(item) => {
let mut lifted = List::new_from_item(std::mem::take(&mut **item));
f(&mut lifted);
if let Some(updated) = lifted.into_iter().next() {
**item = updated;
}
}
graphic => {
if let Some(vector_list) = graphic.as_vector_list_mut() {
f(vector_list);
}
}
}
}
}
fn vector_count(&self) -> usize {
self.iter_element_values().filter_map(|element| element.as_vector_list()).map(|list| list.len()).sum()
self.iter_element_values()
.map(|element| match element {
Graphic::Vector(_) => 1,
element => element.as_vector_list().map_or(0, List::len),
})
.sum()
}
}
@@ -83,10 +100,18 @@ impl VectorItemMut for Item<Vector> {
impl VectorItemMut for Item<Graphic> {
fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut Vector, DAffine2)) {
let Some(vector_list) = self.element_mut().as_vector_list_mut() else { return };
let (elements, transforms) = vector_list.element_and_attribute_slices_mut::<DAffine2>(ATTR_TRANSFORM);
for (vector, transform) in elements.iter_mut().zip(transforms.iter()) {
f(vector, *transform);
match self.element_mut() {
Graphic::Vector(item) => {
let transform = item.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM);
f(item.element_mut(), transform);
}
element => {
let Some(vector_list) = element.as_vector_list_mut() else { return };
let (elements, transforms) = vector_list.element_and_attribute_slices_mut::<DAffine2>(ATTR_TRANSFORM);
for (vector, transform) in elements.iter_mut().zip(transforms.iter()) {
f(vector, *transform);
}
}
}
}
}
@@ -117,6 +142,8 @@ impl MapVectorItems for Graphic {
// Collecting from zero items would drop the attribute columns, so an empty list is left alone
Graphic::VectorList(list) if !list.is_empty() => *list = std::mem::take(list).into_iter().map(&mut *f).collect(),
Graphic::GraphicList(list) => list.iter_element_values_mut().for_each(|nested| map_nested(nested, f)),
Graphic::Vector(item) => **item = f(std::mem::take(&mut **item)),
Graphic::Graphic(item) => map_nested(item.element_mut(), f),
_ => {}
}
}
@@ -132,6 +159,8 @@ impl MapVectorItems for Graphic {
match graphic {
Graphic::VectorList(list) => elements.extend(list.iter_element_values_mut()),
Graphic::GraphicList(list) => list.iter_element_values_mut().for_each(|nested| collect(nested, elements)),
Graphic::Vector(item) => elements.push(item.element_mut()),
Graphic::Graphic(item) => collect(item.element_mut(), elements),
_ => {}
}
}
@@ -167,6 +196,12 @@ impl ExpandVectorItems for Graphic {
*list = expanded;
}
Graphic::GraphicList(list) => list.iter_element_values_mut().for_each(|nested| expand_nested(nested, f)),
// One item expanding into many is a rank raise, so the leaf becomes the list it grew into
Graphic::Vector(item) => {
let expanded = f(std::mem::take(&mut **item));
*graphic = Graphic::VectorList(expanded);
}
Graphic::Graphic(item) => expand_nested(item.element_mut(), f),
_ => {}
}
}
@@ -1510,6 +1545,12 @@ impl SolidifyStroke for Graphic {
match graphic {
Graphic::VectorList(list) if !list.is_empty() => *list = solidify_stroke_list_with_snapshot(std::mem::take(list)),
Graphic::GraphicList(list) => list.iter_element_values_mut().for_each(solidify_nested),
// Solidifying can split one path into separate fill and stroke items, so the leaf becomes a list
Graphic::Vector(item) => {
let solidified = solidify_stroke_list_with_snapshot(List::new_from_item(std::mem::take(&mut **item)));
*graphic = Graphic::VectorList(solidified);
}
Graphic::Graphic(item) => solidify_nested(item.element_mut()),
_ => {}
}
}
@@ -2571,6 +2612,25 @@ async fn morph<I: IntoGraphicList>(
build_transform_with_y_preservation(metadata_source_transform, start, end)
}
/// The two paint kinds that can interpolate, read from either rank so the pairings below stay at four cases.
/// A gradient normalizes to the list form because the interpolation carries its placement attributes along.
enum InterpolablePaint<'a> {
Color(&'a Color),
Gradient(List<Gradient>),
}
impl<'a> InterpolablePaint<'a> {
fn from_graphic(graphic: &'a Graphic) -> Option<Self> {
match graphic {
Graphic::Color(item) => Some(InterpolablePaint::Color(item.element())),
Graphic::ColorList(list) => list.element(0).map(InterpolablePaint::Color),
Graphic::Gradient(item) => Some(InterpolablePaint::Gradient(List::new_from_item(item.clone()))),
Graphic::GradientList(list) => list.element(0).is_some().then(|| InterpolablePaint::Gradient(list.clone())),
_ => None,
}
}
}
// Lerp between two graphics. Solid color and gradient pairings interpolate; all other pairings step at the midpoint.
fn lerp_graphic(a: Option<&List<Graphic>>, b: Option<&List<Graphic>>, time: f64) -> Option<List<Graphic>> {
let transparent = List::new_from_element(Color::TRANSPARENT).into_graphic_list();
@@ -2595,30 +2655,30 @@ async fn morph<I: IntoGraphicList>(
Graphic::GradientList(gradient_list)
};
let graphic = match (a.element(0), b.element(0)) {
(Some(Graphic::ColorList(color_list_a)), Some(Graphic::ColorList(color_list_b))) => color_list_a
.element(0)
.zip(color_list_b.element(0))
.map(|(color_a, color_b)| Graphic::from(color_a.lerp(color_b, time as f32))),
(Some(Graphic::ColorList(color_list_a)), Some(Graphic::GradientList(gradient_list_b))) => color_list_a.element(0).zip(gradient_list_b.element(0)).map(|(color_a, stops_b)| {
let graphic = match (a.element(0).and_then(InterpolablePaint::from_graphic), b.element(0).and_then(InterpolablePaint::from_graphic)) {
(Some(InterpolablePaint::Color(color_a)), Some(InterpolablePaint::Color(color_b))) => Some(Graphic::from(color_a.lerp(color_b, time as f32))),
(Some(InterpolablePaint::Color(color_a)), Some(InterpolablePaint::Gradient(gradient_list_b))) => gradient_list_b.element(0).cloned().map(|stops_b| {
let solid_to_gradient = stops_b.map_colors(|_| *color_a);
let stops = solid_to_gradient.lerp(stops_b, time);
gradient_with_stops(gradient_list_b.clone(), stops)
let stops = solid_to_gradient.lerp(&stops_b, time);
gradient_with_stops(gradient_list_b, stops)
}),
(Some(Graphic::GradientList(gradient_list_a)), Some(Graphic::ColorList(color_list_b))) => gradient_list_a.element(0).zip(color_list_b.element(0)).map(|(stops_a, color_b)| {
(Some(InterpolablePaint::Gradient(gradient_list_a)), Some(InterpolablePaint::Color(color_b))) => gradient_list_a.element(0).cloned().map(|stops_a| {
let gradient_to_solid = stops_a.map_colors(|_| *color_b);
let stops = stops_a.lerp(&gradient_to_solid, time);
gradient_with_stops(gradient_list_a.clone(), stops)
gradient_with_stops(gradient_list_a, stops)
}),
(Some(Graphic::GradientList(gradient_list_a)), Some(Graphic::GradientList(gradient_list_b))) => gradient_list_a.element(0).zip(gradient_list_b.element(0)).map(|(stops_a, stops_b)| {
let stops = stops_a.lerp(stops_b, time);
let metadata_source = if time < 0.5 { gradient_list_a } else { gradient_list_b };
(Some(InterpolablePaint::Gradient(gradient_list_a)), Some(InterpolablePaint::Gradient(gradient_list_b))) => gradient_list_a
.element(0)
.zip(gradient_list_b.element(0))
.map(|(stops_a, stops_b)| stops_a.lerp(stops_b, time))
.map(|stops| {
let transform = lerp_gradient_transform(&gradient_list_a, &gradient_list_b, time);
let mut gradient_list = metadata_source.clone();
gradient_list.set_attribute(ATTR_TRANSFORM, 0, lerp_gradient_transform(gradient_list_a, gradient_list_b, time));
let mut gradient_list = if time < 0.5 { gradient_list_a } else { gradient_list_b };
gradient_list.set_attribute(ATTR_TRANSFORM, 0, transform);
gradient_with_stops(gradient_list, stops)
}),
gradient_with_stops(gradient_list, stops)
}),
// Pairings beyond solid colors and gradients (raster, vector, or mixed) can't be interpolated, so step at the midpoint
_ => return Some(if time < 0.5 { a.clone() } else { b.clone() }),
};
@@ -4106,4 +4166,45 @@ mod test {
assert_eq!(beveled.point_domain.positions().len(), 6);
assert_eq!(beveled.segment_domain.ids().len(), 5);
}
// A rank-0 vector reaches the same per-list styling path as a vector list, rather than being skipped
#[tokio::test]
async fn assign_colors_reaches_rank_0_vector_graphics() {
let leaf = Graphic::Vector(Box::new(vector_item_from_bezpath(Rect::new(0., 0., 10., 10.).to_path(DEFAULT_ACCURACY))));
let content = List::new_from_element(leaf);
let styled = super::assign_colors(
Footprint::default(),
content,
Item::new_from_element(true),
Item::new_from_element(false),
Item::new_from_element(Gradient::from(vec![Color::BLACK, Color::WHITE])),
Item::new_from_element(false),
Item::new_from_element(false),
Item::new_from_element(SeedValue::default()),
Item::new_from_element(0_u32),
)
.await;
let Some(Graphic::Vector(item)) = styled.element(0) else {
panic!("the leaf should stay a rank-0 vector")
};
let appearance = item.attribute::<Appearance>(ATTR_APPEARANCE).expect("the leaf should have gained an appearance");
assert!(appearance.has_painted_cover(Cover::Fill), "the fill of a rank-0 vector should be styled like a list element");
}
// Fill's automatic gradient placement measures rank-0 vector content instead of falling back to the unit box
#[test]
fn vector_item_mut_reaches_a_rank_0_vector_graphic() {
let transform = DAffine2::from_translation(DVec2::new(7., 3.));
let item = create_vector_item(Rect::new(0., 0., 10., 10.).to_path(DEFAULT_ACCURACY), transform);
let mut content = Item::new_from_element(Graphic::Vector(Box::new(item)));
let mut visited = Vec::new();
content.for_each_vector_mut(|vector, vector_transform| visited.push((vector.bounding_box(), vector_transform)));
assert_eq!(visited.len(), 1, "the lone vector should be visited exactly once");
assert_eq!(visited[0].1, transform, "its own transform attribute should come along for placement");
assert!(visited[0].0.is_some(), "its geometry should be measurable for the automatic gradient bounds");
}
}