Store paint content in its native form

This commit is contained in:
Dennis Kobert
2026-08-27 12:33:28 +00:00
parent 34ea9c59fd
commit 79f3571b02
4 changed files with 84 additions and 10 deletions

View File

@@ -38,6 +38,7 @@ impl Artboard {
for element in content.iter_element_values_mut() {
*element = crate::graphic::map_groups_to_legacy(element);
}
crate::graphic::map_paint_attrs_to_legacy(&mut content);
Artboard(content)
}
}

View File

@@ -792,9 +792,10 @@ fn group_bounding_box(group: &core_types::record::Group, transform: DAffine2, in
}
}
/// One typed run as a legacy list, elements cloned and every attribute
/// copied through its erased read.
pub(crate) fn run_to_legacy_list<T: Clone + Send + Sync + 'static>(item: &core_types::record::GroupItem) -> Option<List<T>> {
/// One typed run as an owned list, elements cloned and every attribute copied
/// through its erased read. Content keeps its native form; the legacy
/// conversions layer their mapping on top.
pub fn run_to_list<T: Clone + Send + Sync + 'static>(item: &core_types::record::GroupItem) -> Option<List<T>> {
let lanes = item.typed_lanes::<T>()?;
let mut list = List::new();
for lane in 0..lanes.len() {
@@ -810,6 +811,27 @@ pub(crate) fn run_to_legacy_list<T: Clone + Send + Sync + 'static>(item: &core_t
Some(list)
}
/// Converts the group content of the list's paint attribute values to legacy
/// form, so a legacy product owns everything its attributes reach.
pub fn map_paint_attrs_to_legacy<T: 'static>(list: &mut List<T>) {
for key in [ATTR_FILL, ATTR_STROKE, crate::markers::ATTR_EDITOR_MERGED_LAYERS] {
let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(key) else { continue };
for value in values.flatten() {
for element in value.iter_element_values_mut() {
*element = map_groups_to_legacy(element);
}
}
}
}
/// One typed run as a legacy list: [`run_to_list`] with the paint attribute
/// contents converted to their legacy form.
pub(crate) fn run_to_legacy_list<T: Clone + Send + Sync + 'static>(item: &core_types::record::GroupItem) -> Option<List<T>> {
let mut list = run_to_list::<T>(item)?;
map_paint_attrs_to_legacy(&mut list);
Some(list)
}
/// One typed run as the legacy list its `Render` impl consumes, nested
/// groups converted to their legacy form.
pub fn run_to_render_list<T: Clone + Send + Sync + 'static>(item: &core_types::record::GroupItem) -> Option<List<T>> {
@@ -992,6 +1014,7 @@ pub fn map_groups_to_legacy(graphic: &Graphic) -> Graphic {
for child in children.iter_element_values_mut() {
*child = map_groups_to_legacy(child);
}
map_paint_attrs_to_legacy(&mut children);
Graphic::Graphic(children)
}
other => other.clone(),
@@ -1059,6 +1082,7 @@ pub fn group_to_legacy_list(group: &core_types::record::Group) -> List<Graphic>
}
}
}
map_paint_attrs_to_legacy(&mut list);
push_lane_paint_into_interiors(&mut list);
list
}
@@ -1436,6 +1460,56 @@ mod run_tests {
assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected);
}
#[test]
fn a_legacy_list_owns_its_paint_attr_content() {
let inner_vector = unit_square_at(DVec2::ZERO);
let inner_layout = Layout::default().with_writes(0, element_write_hashed::<Vector>(), &[]);
let mut inner_bytes = vec![0u8; inner_layout.lane_stride()];
let paint = native_group_paint(&inner_vector, &inner_layout, &mut inner_bytes);
let vector = unit_square_at(DVec2::new(4., 4.));
let layout = Layout::default().with_writes(0, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)]);
let mut bytes = vec![0u8; layout.lane_stride()];
// SAFETY: `bytes` is one lane of `layout`; a parked element stores its
// reference, and the fill field stores the marker's value form.
unsafe {
bytes.as_mut_ptr().cast::<&Vector>().write(&vector);
bytes.as_mut_ptr().add(layout.offset_of(Fill::NAME, 0).unwrap()).cast::<Option<&List<Graphic>>>().write(Some(&paint));
}
let (legacy, expected) = {
// SAFETY: `bytes` holds one lane of `layout` at its stride.
let item = unsafe { GroupItem::from_resident(RecordBatch::new(bytes.as_ptr(), 1, &layout)) };
let legacy = run_to_legacy_list::<Vector>(&item).expect("the run lowers to a legacy vector list");
(legacy, map_groups_to_legacy(paint.element(0).unwrap()))
};
bytes.fill(u8::MAX);
inner_bytes.fill(u8::MAX);
drop(paint);
let served = legacy.attribute::<Option<List<Graphic>>>(Fill::NAME, 0).expect("the fill attribute rides the list");
let served = served.as_ref().expect("the fill is present");
assert_eq!(served.element(0).unwrap(), &expected);
}
#[test]
fn a_run_list_keeps_native_group_elements() {
let inner_vector = unit_square_at(DVec2::ZERO);
let inner_layout = Layout::default().with_writes(0, element_write_hashed::<Vector>(), &[]);
let mut inner_bytes = vec![0u8; inner_layout.lane_stride()];
let content = native_group_paint(&inner_vector, &inner_layout, &mut inner_bytes);
let element = content.element(0).unwrap();
let layout = Layout::default().with_writes(0, element_write_hashed::<Graphic>(), &[]);
let mut bytes = vec![0u8; layout.lane_stride()];
// SAFETY: `bytes` is one lane of `layout`; a parked element stores its
// reference.
unsafe { bytes.as_mut_ptr().cast::<&Graphic>().write(element) };
// SAFETY: `bytes` holds one lane of `layout` at its stride.
let item = unsafe { GroupItem::from_resident(RecordBatch::new(bytes.as_ptr(), 1, &layout)) };
let list = run_to_list::<Graphic>(&item).expect("the run holds graphic lanes");
assert!(matches!(list.element(0), Some(Graphic::Group(_))), "the list keeps the native group form");
}
#[test]
fn a_run_and_its_legacy_list_agree_on_bounding_boxes() {
let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(4., 4.))];

View File

@@ -1,8 +1,9 @@
//! Attribute markers whose value types live in this crate, with their name
//! constants for the string-keyed legacy readers and writers.
//!
//! The list-valued markers deep-copy by cloning the list, so their values must
//! stay free of arena-borrowing content such as [`Graphic::Group`].
//! The list-valued markers may carry native [`Graphic::Group`] content: the
//! registered deep field glue owns it across persistence seams, and legacy
//! products convert it through [`crate::graphic::map_paint_attrs_to_legacy`].
use crate::Graphic;
use core_types::attribute::Attribute;

View File

@@ -311,14 +311,12 @@ fn default_gradient_paint(paint: &mut List<Graphic>, bounds: Option<[DVec2; 2]>,
}
}
/// The materialized paint level as the canonical paint table.
/// The materialized paint level as the canonical owned paint list, content
/// kept in its native form.
fn paint_table(paint: core_types::node::List<'_, Graphic>) -> List<Graphic> {
// SAFETY: a materialized input's frames are arena-resident.
let item = unsafe { core_types::record::GroupItem::from_resident(paint.batch()) };
graphic_types::graphic::group_to_legacy_list(&core_types::record::Group {
row: None,
content: core_types::record::GroupContent::Run(item),
})
graphic_types::graphic::run_to_list::<Graphic>(&item).expect("a paint level holds graphic lanes")
}
/// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry.