Adopt the cascading "appearance" attribute in place of Vector::stroke and the "fill"/"paint" attributes (#4433)

* Add the appearance model types and attribute constants

* Dual-write the appearance attribute alongside the fill/stroke pair in all paint-writing nodes

* Read paint from the appearance attribute in the renderer, analysis, metadata, and editor, cascading from ancestors

* Retire the fill/stroke attribute pair and the Vector stroke field in favor of the appearance attribute

* Replace the Stroke node's paint order input with the relative chain order of the Fill and Stroke nodes

* Code review fixes

* Re-save demo art

* Stamp coverages in place and fuse the renderer's per-item appearance reads into single walks

* Treat an empty appearance as the undeclared state so padded rows inherit instead of blocking the cascade

* Update demo art

* Treat padded appearance rows as undeclared in the boolean flatten's group recursion
This commit is contained in:
Keavon Chambers
2026-08-14 13:25:23 -07:00
committed by GitHub
parent a034923695
commit d117c3eace
50 changed files with 1448 additions and 595 deletions

View File

@@ -1,6 +1,7 @@
use core_types::list::{Item, List, NodeIdPath};
use core_types::list::{ATTR_APPEARANCE, Item, List, NodeIdPath};
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Ctx};
use glam::{DAffine2, DVec2};
use graphic_types::Appearance;
use graphic_types::graphic::bake_paint_transforms;
use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath};
use graphic_types::vector_types::vector::PointId;
@@ -45,7 +46,15 @@ async fn boolean_operation<I: graphic_types::IntoGraphicList>(
let result_vector = result_vector_list.element_mut(0).unwrap();
Vector::transform(result_vector, transform);
result_vector.set_stroke_transform(DAffine2::IDENTITY);
// The geometry is baked into identity space, so a copied stroke authoring space would be stale
if let Some(appearance) = result_vector_list.attribute::<Appearance>(ATTR_APPEARANCE, 0) {
let mut appearance = appearance.clone();
for coverage in appearance.0.iter_element_values_mut() {
coverage.0.remove_attribute::<DAffine2>(ATTR_TRANSFORM);
}
result_vector_list.set_attribute(ATTR_APPEARANCE, 0, appearance);
}
// Snapshot the input layers as the `editor:merged_layers` attribute so the renderer can recurse into them
// for editor click-target preservation.
@@ -142,12 +151,7 @@ fn boolean_operation_on_vector_list(vector: &List<Vector>, boolean_operation: Bo
bake_paint_transforms(&mut attributes, copy_from_transform);
let copy_from = vector.element(index).unwrap();
let element = Vector {
stroke: copy_from.stroke.clone(),
..Default::default()
};
Item::from_parts(element, attributes)
Item::from_parts(Vector::default(), attributes)
} else {
Item::<Vector>::default()
};
@@ -191,6 +195,7 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
let parent_opacity: f64 = graphic_list.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let parent_fill: f64 = graphic_list.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let layer_path: NodeIdPath = graphic_list.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index);
let parent_appearance = graphic_list.attribute::<Appearance>(ATTR_APPEARANCE, index).and_then(Appearance::declared).cloned();
let compose_parent = |mut item: Item<Vector>| {
if parent_has_transform || item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some() {
@@ -208,6 +213,12 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
if parent_has_layer_path {
item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
}
// Appearance cascades into each child whose own is undeclared, since a declared child wins wholesale
if let Some(appearance) = &parent_appearance
&& item.attribute::<Appearance>(ATTR_APPEARANCE).and_then(Appearance::declared).is_none()
{
item.set_attribute(ATTR_APPEARANCE, appearance.clone());
}
item
};
@@ -231,6 +242,13 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
*fill *= parent_fill;
}
}
if let Some(appearance) = &parent_appearance {
for value in graphic.iter_attribute_values_mut_or_default::<Appearance>(ATTR_APPEARANCE) {
if value.is_empty() {
*value = appearance.clone();
}
}
}
// Unioning each group lets it enter the outer operation as one region, which is why this cannot defer
// to `flatten_graphic_list`. Splicing children in as sibling operands makes them subtract from each other.