diff --git a/node-graph/libraries/graphic-types/src/artboard.rs b/node-graph/libraries/graphic-types/src/artboard.rs index ac20f25709..ebaea9709d 100644 --- a/node-graph/libraries/graphic-types/src/artboard.rs +++ b/node-graph/libraries/graphic-types/src/artboard.rs @@ -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) } } diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 48b52e0034..4bee35aeda 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -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(item: &core_types::record::GroupItem) -> Option> { +/// 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(item: &core_types::record::GroupItem) -> Option> { let lanes = item.typed_lanes::()?; let mut list = List::new(); for lane in 0..lanes.len() { @@ -810,6 +811,27 @@ pub(crate) fn run_to_legacy_list(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(list: &mut List) { + for key in [ATTR_FILL, ATTR_STROKE, crate::markers::ATTR_EDITOR_MERGED_LAYERS] { + let Some(values) = list.iter_attribute_values_mut::>>(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(item: &core_types::record::GroupItem) -> Option> { + let mut list = run_to_list::(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(item: &core_types::record::GroupItem) -> Option> { @@ -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 } } } + 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::(), &[]); + 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::(), &[FieldWrite::of::(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::>>().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::(&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::>>(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::(), &[]); + 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::(), &[]); + 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::(&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.))]; diff --git a/node-graph/libraries/graphic-types/src/markers.rs b/node-graph/libraries/graphic-types/src/markers.rs index 102da9c250..b827f3d1de 100644 --- a/node-graph/libraries/graphic-types/src/markers.rs +++ b/node-graph/libraries/graphic-types/src/markers.rs @@ -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; diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 579412aa7b..07814fd42f 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -311,14 +311,12 @@ fn default_gradient_paint(paint: &mut List, 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 { // 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::(&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.