mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
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:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user