diff --git a/node-graph/libraries/core-types/src/lane.rs b/node-graph/libraries/core-types/src/lane.rs index c1537fdfd9..aaefa72baa 100644 --- a/node-graph/libraries/core-types/src/lane.rs +++ b/node-graph/libraries/core-types/src/lane.rs @@ -38,6 +38,99 @@ pub trait LaneSource { } } +/// A bare element as a one-lane source: no columns, so every marker reads its +/// census default. The read surface for a de-tabled leaf, whose attributes +/// ride the containing lane. +pub struct Single<'a, T>(pub &'a T); + +/// The column of a [`Single`]: always absent, so reads fall to the census +/// default. +pub struct NoColumn; + +impl<'a, A: Attribute> LaneColumn<'a, A> for NoColumn { + fn try_get(&self, _lane: usize) -> Option> { + None + } +} + +impl LaneSource for Single<'_, T> { + type Element = T; + type Column<'a, A: Attribute> + = NoColumn + where + Self: 'a; + + fn lane_count(&self) -> usize { + 1 + } + + fn element(&self, lane: usize) -> Option<&T> { + (lane == 0).then_some(self.0) + } + + fn column(&self) -> NoColumn { + NoColumn + } +} + +impl crate::bounds::BoundingBox for Single<'_, T> { + fn bounding_box(&self, transform: glam::DAffine2, include_stroke: bool) -> crate::bounds::RenderBoundingBox { + self.0.bounding_box(transform, include_stroke) + } + + fn thumbnail_bounding_box(&self, transform: glam::DAffine2, include_stroke: bool) -> crate::bounds::RenderBoundingBox { + self.0.thumbnail_bounding_box(transform, include_stroke) + } +} + +/// One lane of a source re-based as a one-lane source of a leaf element: the +/// de-tabled leaf read with its containing lane's attributes. +pub struct LeafLane<'a, S, T> { + source: &'a S, + index: usize, + element: &'a T, +} + +impl<'a, S, T> LeafLane<'a, S, T> { + pub fn new(source: &'a S, index: usize, element: &'a T) -> Self { + Self { source, index, element } + } +} + +pub struct LaneColumnAt<'a, S: LaneSource + 'a, A: Attribute> { + inner: S::Column<'a, A>, + index: usize, +} + +impl<'a, S: LaneSource, A: Attribute> LaneColumn<'a, A> for LaneColumnAt<'a, S, A> { + fn try_get(&self, lane: usize) -> Option> { + (lane == 0).then(|| self.inner.try_get(self.index)).flatten() + } +} + +impl LaneSource for LeafLane<'_, S, T> { + type Element = T; + type Column<'a, A: Attribute> + = LaneColumnAt<'a, S, A> + where + Self: 'a; + + fn lane_count(&self) -> usize { + 1 + } + + fn element(&self, lane: usize) -> Option<&T> { + (lane == 0).then_some(self.element) + } + + fn column(&self) -> LaneColumnAt<'_, S, A> { + LaneColumnAt { + inner: self.source.column::(), + index: self.index, + } + } +} + #[cfg(test)] mod tests { use super::LaneSource; diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index 2d1f89ca7c..f89318a67b 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -1966,6 +1966,17 @@ pub struct RunColumn<'a, A: crate::attribute::Attribute> { marker: std::marker::PhantomData, } +impl<'a, A: crate::attribute::Attribute> RunColumn<'a, A> { + /// The marker's column on an item, its offset resolved once. + pub fn of(item: &'a GroupItem) -> Self { + Self { + item, + offset: item.layout().offset_of(A::NAME, 0), + marker: std::marker::PhantomData, + } + } +} + impl<'a, A: crate::attribute::Attribute> crate::lane::LaneColumn<'a, A> for RunColumn<'a, A> { fn try_get(&self, lane: usize) -> Option> { // SAFETY: the offset comes from the item's own layout, whose field at @@ -1990,11 +2001,7 @@ impl<'a, T: 'static> crate::lane::LaneSource for RunView<'a, T> { } fn column(&self) -> RunColumn<'_, A> { - RunColumn { - item: self.item, - offset: self.item.layout().offset_of(A::NAME, 0), - marker: std::marker::PhantomData, - } + RunColumn::of(self.item) } } diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index c4dc2c39d7..8d2acd9e13 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -15,15 +15,18 @@ use vector_types::GradientStops; pub use vector_types::Vector; /// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax. +/// A leaf holds its element directly; its attributes ride the containing +/// lane. Multi-element content is a [`core_types::record::Group`] run, or +/// transitionally the legacy `Graphic` list. #[derive(Clone, Debug, CacheHash, PartialEq, DynAny)] pub enum Graphic { Graphic(List), - Vector(List), - RasterCPU(List>), - RasterGPU(List>), - Color(List), - Gradient(List), - Text(List), + Vector(Vector), + RasterCPU(Raster), + RasterGPU(Raster), + Color(Color), + Gradient(GradientStops), + Text(String), Group(core_types::record::Group), } @@ -40,15 +43,26 @@ impl From> for Graphic { } } +/// A typed legacy list as a legacy graphic list: each item de-tables to a +/// leaf element, keeping its attributes on the containing lane. +fn detable_items(list: List, leaf: fn(T) -> Graphic) -> List { + let mut out = List::new(); + for item in list.into_iter() { + let (element, attributes) = item.into_parts(); + out.push(Item::from_parts(leaf(element), attributes)); + } + out +} + // Vector impl From for Graphic { fn from(vector: Vector) -> Self { - Graphic::Vector(List::new_from_element(vector)) + Graphic::Vector(vector) } } impl From> for Graphic { fn from(vector: List) -> Self { - Graphic::Vector(vector) + Graphic::Graphic(detable_items(vector, Graphic::Vector)) } } @@ -57,12 +71,12 @@ impl From> for Graphic { // Raster impl From> for Graphic { fn from(raster: Raster) -> Self { - Graphic::RasterCPU(List::new_from_element(raster)) + Graphic::RasterCPU(raster) } } impl From>> for Graphic { fn from(raster: List>) -> Self { - Graphic::RasterCPU(raster) + Graphic::Graphic(detable_items(raster, Graphic::RasterCPU)) } } // Note: List conversions handled by blanket impl in gcore @@ -70,12 +84,12 @@ impl From>> for Graphic { // Raster impl From> for Graphic { fn from(raster: Raster) -> Self { - Graphic::RasterGPU(List::new_from_element(raster)) + Graphic::RasterGPU(raster) } } impl From>> for Graphic { fn from(raster: List>) -> Self { - Graphic::RasterGPU(raster) + Graphic::Graphic(detable_items(raster, Graphic::RasterGPU)) } } // Note: List conversions handled by blanket impl in gcore @@ -83,12 +97,12 @@ impl From>> for Graphic { // Color impl From for Graphic { fn from(color: Color) -> Self { - Graphic::Color(List::new_from_element(color)) + Graphic::Color(color) } } impl From> for Graphic { fn from(color: List) -> Self { - Graphic::Color(color) + Graphic::Graphic(detable_items(color, Graphic::Color)) } } // Note: List conversions handled by blanket impl in gcore @@ -97,31 +111,31 @@ impl From> for Graphic { // GradientStops impl From for Graphic { fn from(gradient: GradientStops) -> Self { - Graphic::Gradient(List::new_from_element(gradient)) + Graphic::Gradient(gradient) } } impl From> for Graphic { fn from(gradient: List) -> Self { - Graphic::Gradient(gradient) + Graphic::Graphic(detable_items(gradient, Graphic::Gradient)) } } // String impl From for Graphic { fn from(text: String) -> Self { - Graphic::Text(List::new_from_element(text)) + Graphic::Text(text) } } impl From> for Graphic { fn from(text: List) -> Self { - Graphic::Text(text) + Graphic::Graphic(detable_items(text, Graphic::Text)) } } /// Deeply flattens a `List`, collecting only elements matching a specific variant (extracted by `extract_variant`) /// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-`List`s composes transforms and opacity. fn flatten_graphic_list(content: List, extract_variant: fn(Graphic) -> Option>) -> List { - fn flatten_recursive(output: &mut List, current_graphic_list: List, extract_variant: fn(Graphic) -> Option>) { + fn flatten_recursive(output: &mut List, current_graphic_list: List, extract_variant: fn(Graphic) -> Option>, parent_layer_path: Option<&[NodeId]>) { for current_graphic_item in current_graphic_list.into_iter() { // Whether the parent carries each attribute: a structural fact (column presence), never a value comparison. // Flattening composes a parent attribute onto its children only when the parent has it, @@ -129,14 +143,14 @@ fn flatten_graphic_list(content: List, extract_variant: fn(Graphic) let parent_has_transform = current_graphic_item.attribute::(ATTR_TRANSFORM).is_some(); let parent_has_opacity = current_graphic_item.attribute::(ATTR_OPACITY).is_some(); let parent_has_fill = current_graphic_item.attribute::(ATTR_OPACITY_FILL).is_some(); - let parent_has_layer_path = current_graphic_item.attribute::>(ATTR_EDITOR_LAYER_PATH).is_some(); - let layer_path: Vec = current_graphic_item.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH); let current_transform: DAffine2 = current_graphic_item.attribute_cloned_or_default(ATTR_TRANSFORM); let current_opacity: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY, 1.); let current_fill: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.); + let lane_layer_path: Option> = current_graphic_item.attribute::>(ATTR_EDITOR_LAYER_PATH).cloned(); - match current_graphic_item.into_element() { + let (element, attributes) = current_graphic_item.into_parts(); + match element { // Compose the parent's transform/opacity/fill onto each child, but only for attributes the parent carries. // A child lacking one is padded with the composition identity (`1.` for opacity/fill, identity for transform), so composing through it is a no-op. Graphic::Graphic(mut sub_list) => { @@ -156,31 +170,18 @@ fn flatten_graphic_list(content: List, extract_variant: fn(Graphic) } } - flatten_recursive(output, sub_list, extract_variant); + flatten_recursive(output, sub_list, extract_variant, lane_layer_path.as_deref()); } - // Extract the target variant and push its items, composing the parent's attributes onto each + // A de-tabled leaf is one attr-less element; the extracted row rides with its containing lane's full attributes, paint included. + // The enclosing group lane's own layer path overrides, one hop only, matching the native walk. other => { if let Some(typed_list) = extract_variant(other) { - for mut item in typed_list.into_iter() { - // Each `|| item.attribute(...)` keeps an attribute the item itself carries - // (recomposed with the parent's identity value) even when the parent lacks it - if parent_has_transform || item.attribute::(ATTR_TRANSFORM).is_some() { - let item_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); - item.set_attribute(ATTR_TRANSFORM, current_transform * item_transform); + for item in typed_list.into_iter() { + let mut row = Item::from_parts(item.into_element(), attributes.clone()); + if let Some(layer_path) = parent_layer_path { + row.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.to_vec()); } - if parent_has_opacity || item.attribute::(ATTR_OPACITY).is_some() { - let item_opacity: f64 = item.attribute_cloned_or(ATTR_OPACITY, 1.); - item.set_attribute(ATTR_OPACITY, current_opacity * item_opacity); - } - if parent_has_fill || item.attribute::(ATTR_OPACITY_FILL).is_some() { - let item_fill: f64 = item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.); - item.set_attribute(ATTR_OPACITY_FILL, current_fill * item_fill); - } - if parent_has_layer_path { - item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone()); - } - - output.push(item); + output.push(row); } } } @@ -189,7 +190,7 @@ fn flatten_graphic_list(content: List, extract_variant: fn(Graphic) } let mut output = List::new(); - flatten_recursive(&mut output, content, extract_variant); + flatten_recursive(&mut output, content, extract_variant, None); output } @@ -398,22 +399,13 @@ pub fn set_paint_attribute_at(list: &mut List, index: usize, key: &str, pa /// Bake the provided transform into the per-item transforms of the paint graphics stored under the /// canonical `List` fill and stroke attributes. pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) { - fn bake_list_transform(list: &mut List, transform: DAffine2) { - for item_transform in list.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + fn bake_graphic_paint_transform(graphics: &mut List, transform: DAffine2) { + for item_transform in graphics.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { *item_transform = transform * *item_transform; } - } - - fn bake_graphic_paint_transform(graphics: &mut List, transform: DAffine2) { for graphic in graphics.iter_element_values_mut() { - match graphic { - Graphic::Graphic(list) => bake_list_transform(list, transform), - Graphic::Vector(list) => bake_list_transform(list, transform), - Graphic::RasterCPU(list) => bake_list_transform(list, transform), - Graphic::RasterGPU(list) => bake_list_transform(list, transform), - Graphic::Gradient(list) => bake_list_transform(list, transform), - Graphic::Text(list) => bake_list_transform(list, transform), - Graphic::Color(_) | Graphic::Group(_) => {} + if let Graphic::Graphic(list) = graphic { + bake_graphic_paint_transform(list, transform); } } } @@ -433,31 +425,31 @@ pub trait TryFromGraphic: Clone + Sized { impl TryFromGraphic for Vector { fn try_from_graphic(graphic: Graphic) -> Option> { - if let Graphic::Vector(t) = graphic { Some(t) } else { None } + if let Graphic::Vector(t) = graphic { Some(List::new_from_element(t)) } else { None } } } impl TryFromGraphic for Raster { fn try_from_graphic(graphic: Graphic) -> Option> { - if let Graphic::RasterCPU(t) = graphic { Some(t) } else { None } + if let Graphic::RasterCPU(t) = graphic { Some(List::new_from_element(t)) } else { None } } } impl TryFromGraphic for Color { fn try_from_graphic(graphic: Graphic) -> Option> { - if let Graphic::Color(t) = graphic { Some(t) } else { None } + if let Graphic::Color(t) = graphic { Some(List::new_from_element(t)) } else { None } } } impl TryFromGraphic for GradientStops { fn try_from_graphic(graphic: Graphic) -> Option> { - if let Graphic::Gradient(t) = graphic { Some(t) } else { None } + if let Graphic::Gradient(t) = graphic { Some(List::new_from_element(t)) } else { None } } } impl TryFromGraphic for String { fn try_from_graphic(graphic: Graphic) -> Option> { - if let Graphic::Text(t) = graphic { Some(t) } else { None } + if let Graphic::Text(t) = graphic { Some(List::new_from_element(t)) } else { None } } } @@ -482,49 +474,37 @@ impl IntoGraphicList for List { impl IntoGraphicList for List { fn into_graphic_list(self) -> List { - // Propagate `editor:layer_path` from item 0 onto the wrapper Graphic item so a subsequent - // `flatten_graphic_list` doesn't overwrite the inner Vector's stamp with an empty value - let layer_path: Vec = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0); - let mut graphic_list = List::new_from_element(Graphic::Vector(self)); - if !layer_path.is_empty() { - graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path); - } - graphic_list + detable_items(self, Graphic::Vector) } } impl IntoGraphicList for List> { fn into_graphic_list(self) -> List { - List::new_from_element(Graphic::RasterCPU(self)) + detable_items(self, Graphic::RasterCPU) } } impl IntoGraphicList for List> { fn into_graphic_list(self) -> List { - List::new_from_element(Graphic::RasterGPU(self)) + detable_items(self, Graphic::RasterGPU) } } impl IntoGraphicList for List { fn into_graphic_list(self) -> List { - List::new_from_element(Graphic::Color(self)) + detable_items(self, Graphic::Color) } } impl IntoGraphicList for List { fn into_graphic_list(self) -> List { - List::new_from_element(Graphic::Gradient(self)) + detable_items(self, Graphic::Gradient) } } impl IntoGraphicList for List { fn into_graphic_list(self) -> List { - let layer_path: Vec = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0); - let mut graphic_list = List::new_from_element(Graphic::Text(self)); - if !layer_path.is_empty() { - graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path); - } - graphic_list + detable_items(self, Graphic::Text) } } @@ -544,7 +524,7 @@ impl From for Graphic { // DVec2 impl From for Graphic { fn from(position: DVec2) -> Self { - Graphic::Vector(List::new_from_element(Vector::from_anchor_position(position))) + Graphic::Vector(Vector::from_anchor_position(position)) } } // Note: List conversions handled by blanket impl in gcore @@ -564,54 +544,50 @@ impl Graphic { } } - pub fn as_vector(&self) -> Option<&List> { + pub fn as_vector(&self) -> Option<&Vector> { match self { Graphic::Vector(vector) => Some(vector), _ => None, } } - pub fn as_vector_mut(&mut self) -> Option<&mut List> { + pub fn as_vector_mut(&mut self) -> Option<&mut Vector> { match self { Graphic::Vector(vector) => Some(vector), _ => None, } } - pub fn as_raster(&self) -> Option<&List>> { + pub fn as_raster(&self) -> Option<&Raster> { match self { Graphic::RasterCPU(raster) => Some(raster), _ => None, } } - pub fn as_raster_mut(&mut self) -> Option<&mut List>> { + pub fn as_raster_mut(&mut self) -> Option<&mut Raster> { match self { Graphic::RasterCPU(raster) => Some(raster), _ => None, } } + /// A leaf carries no clipping attribute, which rides its containing lane. pub fn had_clip_enabled(&self) -> bool { fn all_clipped(list: &List) -> bool { list.iter_attribute_values_or_default::(ATTR_CLIPPING_MASK).all(|clip| clip) } match self { - Graphic::Vector(list) => all_clipped(list), Graphic::Graphic(list) => all_clipped(list), - Graphic::RasterCPU(list) => all_clipped(list), - Graphic::RasterGPU(list) => all_clipped(list), - Graphic::Color(list) => all_clipped(list), - Graphic::Gradient(list) => all_clipped(list), - Graphic::Text(list) => all_clipped(list), Graphic::Group(group) => group_all_clipped(group), + _ => false, } } pub fn can_reduce_to_clip_path(&self) -> bool { match self { - Graphic::Vector(vector) => vector_can_reduce_to_clip_path(vector), + Graphic::Vector(vector) => vector_can_reduce_to_clip_path(&core_types::lane::Single(vector)), _ => false, } } @@ -619,23 +595,11 @@ impl Graphic { pub fn is_opaque(&self) -> bool { match self { Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque), - Graphic::Vector(list) => { - fn is_paint_opaque_at<'a, A: Attribute = Option<&'a List>>>(list: &'a List, index: usize) -> bool { - paint_graphics::(list, index).is_some_and(|graphic_list| graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque())) - } - - !list.is_empty() - && (0..list.len()).all(|i| { - let Some(vector) = list.element(i) else { return false }; - let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.); - let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.); - let fill_opaque = opacity_fill >= 1. - f64::EPSILON && is_paint_opaque_at::(list, i); - let stroke_opaque_or_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_opaque_at::(list, i); - opacity >= 1. - f64::EPSILON && fill_opaque && stroke_opaque_or_invisible - }) - } - Graphic::Color(list) => list.element(0).is_some_and(|color| color.is_opaque()), - Graphic::Gradient(list) => list.element(0).is_some_and(|stops| stops.iter().all(|stop| stop.color.is_opaque())), + // A bare leaf carries no paint attribute, which rides its lane, so + // nothing here claims opacity. + Graphic::Vector(_) => false, + Graphic::Color(color) => color.is_opaque(), + Graphic::Gradient(stops) => stops.iter().all(|stop| stop.color.is_opaque()), Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false, Graphic::Group(group) => group_is_opaque(group), } @@ -644,23 +608,11 @@ impl Graphic { pub fn is_fully_transparent(&self) -> bool { match self { Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent), - Graphic::Vector(list) => (0..list.len()).all(|i| { - let Some(vector) = list.element(i) else { return false }; - fn is_paint_fully_transparent_at<'a, A: Attribute = Option<&'a List>>>(list: &'a List, index: usize) -> bool { - paint_graphics::(list, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())) - } - - let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.); - if opacity <= f64::EPSILON { - return true; - } - let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.); - let fill_invisible = opacity_fill <= f64::EPSILON || is_paint_fully_transparent_at::(list, i); - let stroke_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_fully_transparent_at::(list, i); - fill_invisible && stroke_invisible - }), - Graphic::Color(list) => list.iter_element_values().all(|color| color.a() == 0.), - Graphic::Gradient(list) => list.iter_element_values().all(|stops| stops.iter().all(|stop| stop.color.a() == 0.)), + // A bare leaf carries no paint attribute, so only an unstroked + // vector is invisible on its own. + Graphic::Vector(vector) => vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()), + Graphic::Color(color) => color.a() == 0., + Graphic::Gradient(stops) => stops.iter().all(|stop| stop.color.a() == 0.), Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false, Graphic::Group(group) => group_is_fully_transparent(group), } @@ -672,17 +624,12 @@ impl Graphic { matches!(self, Graphic::Color(_) | Graphic::Gradient(_)) && self.is_opaque() } - /// Returns true if this graphic's inner list is empty. + /// Whether the graphic holds no content: a leaf always holds its element. pub fn is_empty(&self) -> bool { match self { Graphic::Graphic(list) => list.is_empty(), - Graphic::Vector(list) => list.is_empty(), - Graphic::Color(list) => list.is_empty(), - Graphic::Gradient(list) => list.is_empty(), - Graphic::RasterCPU(list) => list.is_empty(), - Graphic::RasterGPU(list) => list.is_empty(), - Graphic::Text(list) => list.is_empty(), Graphic::Group(group) => group_is_empty(group), + _ => false, } } } @@ -881,6 +828,83 @@ impl FlattenScale { } } +/// A graphic level in either of its two storages, as one lane source. +#[derive(Clone, Copy)] +pub enum GraphicLevel<'a> { + Legacy(&'a List), + Run(&'a core_types::record::GroupItem), +} + +pub enum GraphicLevelColumn<'a, A: Attribute> { + Legacy(core_types::list::ListColumn<'a, A>), + Run(core_types::record::RunColumn<'a, A>), +} + +impl<'a, A: Attribute> core_types::lane::LaneColumn<'a, A> for GraphicLevelColumn<'a, A> { + fn try_get(&self, lane: usize) -> Option> { + match self { + GraphicLevelColumn::Legacy(column) => column.try_get(lane), + GraphicLevelColumn::Run(column) => column.try_get(lane), + } + } +} + +impl<'a> LaneSource for GraphicLevel<'a> { + type Element = Graphic; + type Column<'b, A: Attribute> + = GraphicLevelColumn<'b, A> + where + Self: 'b; + + fn lane_count(&self) -> usize { + match self { + GraphicLevel::Legacy(list) => list.len(), + GraphicLevel::Run(item) => item.len(), + } + } + + fn element(&self, lane: usize) -> Option<&Graphic> { + match self { + GraphicLevel::Legacy(list) => list.element(lane), + GraphicLevel::Run(item) => { + let lanes = item.typed_lanes::()?; + if lane >= lanes.len() { + return None; + } + // SAFETY: the layout records the element type, and a parked + // element stores its reference at offset 0. + Some(unsafe { core_types::record::borrow_element::(item.lanes().get(lane).rec()) }) + } + } + } + + fn column(&self) -> GraphicLevelColumn<'_, A> { + match self { + GraphicLevel::Legacy(list) => GraphicLevelColumn::Legacy(list.column::()), + GraphicLevel::Run(item) => GraphicLevelColumn::Run(core_types::record::RunColumn::of(item)), + } + } +} + +/// The lane's attributes as an owned set, read through the erased glue. +pub fn run_lane_attributes(item: &core_types::record::GroupItem, lane: usize) -> ItemAttributeValues { + let mut scratch: List = List::new_from_element(Vector::default()); + for field in &item.layout().fields { + // SAFETY: the offset comes from the item's own layout. + let value = unsafe { (field.read_erased)(item.lanes().get(lane).rec().ptr().add(field.offset)) }; + scratch.set_attribute_value_dyn(field.name, 0, AttributeValueDyn(value)); + } + scratch.clone_item_attributes(0) +} + +/// The lane's attributes as an owned set, from either level storage. +pub fn lane_attributes(level: GraphicLevel<'_>, lane: usize) -> ItemAttributeValues { + match level { + GraphicLevel::Legacy(list) => list.clone_item_attributes(lane), + GraphicLevel::Run(item) => run_lane_attributes(item, lane), + } +} + /// One flattened vector row served by [`walk_vector_rows`]: cheap probes /// first, the full row built on demand. pub struct VectorRow<'w> { @@ -891,7 +915,9 @@ pub struct VectorRow<'w> { } enum RowSourceRef<'w> { - Legacy(&'w List, usize), + /// A de-tabled vector leaf on a graphic lane: the lane is the row. + Lane(GraphicLevel<'w>, usize), + /// A lane of a vector run. Run(&'w core_types::record::RunView<'w, Vector>, &'w core_types::record::GroupItem, usize), } @@ -899,7 +925,7 @@ impl VectorRow<'_> { /// The row's vector, borrowed. pub fn element(&self) -> &Vector { match &self.source { - RowSourceRef::Legacy(list, index) => list.element(*index).expect("the walk visits held rows"), + RowSourceRef::Lane(level, index) => level.element(*index).and_then(Graphic::as_vector).expect("the walk visits vector lanes"), RowSourceRef::Run(run, _, index) => LaneSource::element(*run, *index).expect("the walk visits held lanes"), } } @@ -911,10 +937,7 @@ impl VectorRow<'_> { return true; } match &self.source { - RowSourceRef::Legacy(list, index) => list - .attribute::>>(ATTR_FILL, *index) - .and_then(|paint| paint.as_ref()) - .is_some_and(is_paint_present), + RowSourceRef::Lane(level, index) => paint_graphics::(level, *index).is_some(), RowSourceRef::Run(run, _, index) => paint_graphics::(*run, *index).is_some(), } } @@ -924,16 +947,13 @@ impl VectorRow<'_> { pub fn build_into(&self, out: &mut List) { let index = out.len(); match &self.source { - RowSourceRef::Legacy(list, row) => { - out.push(list.clone_item(*row).expect("the walk visits held rows")); + RowSourceRef::Lane(level, lane) => { + let vector = self.element().clone(); + out.push(Item::from_parts(vector, lane_attributes(*level, *lane))); } RowSourceRef::Run(run, item, lane) => { - out.push(Item::new_from_element(LaneSource::element(*run, *lane).expect("the walk visits held lanes").clone())); - for field in &item.layout().fields { - // SAFETY: the offset comes from the item's own layout. - let value = unsafe { (field.read_erased)(item.lanes().get(*lane).rec().ptr().add(field.offset)) }; - out.set_attribute_value_dyn(field.name, index, AttributeValueDyn(value)); - } + let vector = LaneSource::element(*run, *lane).expect("the walk visits held lanes").clone(); + out.push(Item::from_parts(vector, run_lane_attributes(item, *lane))); } } for (key, slot) in [(ATTR_FILL, self.paint.fill), (ATTR_STROKE, self.paint.stroke)] { @@ -959,20 +979,6 @@ impl VectorRow<'_> { } } -fn walk_rows_of_list(list: &List, scale: FlattenScale, layer_path: Option<&[NodeId]>, paint: LanePaint<'_>, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep) -> RowStep { - for row in 0..list.len() { - if let RowStep::Stop = visit(VectorRow { - source: RowSourceRef::Legacy(list, row), - scale, - layer_path, - paint, - }) { - return RowStep::Stop; - } - } - RowStep::Continue -} - fn walk_rows_of_run( item: &core_types::record::GroupItem, scale: FlattenScale, @@ -998,36 +1004,68 @@ fn walk_rows_of_run( /// Walks a graphic level into its flattened vector rows, matching the legacy /// push-then-flatten lowering: lane paint threads with [`PaintReach`], -/// ancestor transform, opacity and fill opacity compose down, the immediate -/// parent's layer path overwrites its rows, and non-vector content is -/// discarded. -pub fn walk_vector_rows>(source: &S, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep) { - walk_vector_rows_impl(source, FlattenScale::ROOT, PaintReach::NONE, visit); +/// ancestor transform, opacity and fill opacity compose down, the containing +/// level's parent layer path overwrites its rows, and non-vector content is +/// discarded. A de-tabled leaf's row is its lane, attributes included. +pub fn walk_vector_rows(level: GraphicLevel<'_>, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep) { + walk_vector_rows_impl(level, FlattenScale::ROOT, None, PaintReach::NONE, visit); } -fn walk_vector_rows_impl<'a, S: LaneSource>( - source: &'a S, +fn walk_vector_rows_impl<'a>( + level: GraphicLevel<'a>, scale: FlattenScale, + parent_layer_path: Option<&'a [NodeId]>, inherited: PaintReach<'a>, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep, ) -> RowStep { - let columns = PaintColumns::new(source); - for index in 0..source.lane_count() { - let Some(element) = source.element(index) else { continue }; + if let GraphicLevel::Run(item) = level { + // A vector-typed run is already its rows. + if item.typed_lanes::().is_some() { + let paint = match inherited.applies() { + true => inherited.paint, + false => LanePaint::NONE, + }; + return walk_rows_of_run(item, scale, parent_layer_path, paint, visit); + } + } + let columns = PaintColumns::new(&level); + for index in 0..level.lane_count() { + let Some(element) = level.element(index) else { continue }; let reach = inherited.for_lane(&columns, index); - let lane_scale = scale.composed(source, index); - let layer_path = source.try_attr::(index); let row_paint = match reach.applies() { true => reach.paint, false => LanePaint::NONE, }; let step = match element { - Graphic::Vector(inner) => walk_rows_of_list(inner, lane_scale, layer_path, row_paint, visit), - Graphic::Graphic(children) => walk_vector_rows_impl(children, lane_scale, reach.nested(), visit), - Graphic::Group(group) => match core_types::record::RunView::::new(&group.content) { - Some(run) => walk_vector_rows_impl(&run, lane_scale, reach.into_group_graphics(), visit), - None => walk_rows_of_run(&group.content, lane_scale, layer_path, row_paint, visit), - }, + Graphic::Vector(_) => visit(VectorRow { + source: RowSourceRef::Lane(level, index), + scale, + layer_path: parent_layer_path, + paint: row_paint, + }), + Graphic::Graphic(children) => walk_vector_rows_impl( + GraphicLevel::Legacy(children), + scale.composed(&level, index), + level.try_attr::(index), + reach.nested(), + visit, + ), + Graphic::Group(group) => { + let item = &group.content; + if item.typed_lanes::().is_some() { + walk_rows_of_run(item, scale.composed(&level, index), level.try_attr::(index), row_paint, visit) + } else if item.typed_lanes::().is_some() { + walk_vector_rows_impl( + GraphicLevel::Run(item), + scale.composed(&level, index), + level.try_attr::(index), + reach.into_group_graphics(), + visit, + ) + } else { + RowStep::Continue + } + } _ => RowStep::Continue, }; if let RowStep::Stop = step { @@ -1039,9 +1077,9 @@ fn walk_vector_rows_impl<'a, S: LaneSource>( /// The level's flattened vector rows as one owned list, the walk's collect /// form. -pub fn flatten_vector_rows>(source: &S) -> List { +pub fn flatten_vector_rows(level: GraphicLevel<'_>) -> List { let mut out = List::new(); - walk_vector_rows(source, &mut |row| { + walk_vector_rows(level, &mut |row| { row.build_into(&mut out); RowStep::Continue }); @@ -1071,22 +1109,11 @@ fn push_lane_paint_into_interiors(list: &mut List) { let Some(paint) = stored.filter(|paint| is_paint_present(paint)).cloned() else { continue; }; - let Some(element) = list.element_mut(index) else { continue }; - let fill_list = |inner: &mut List| { - for item in 0..inner.len() { - set_paint_attribute_at(inner, item, key, paint.clone()); + let Some(Graphic::Graphic(children)) = list.element_mut(index) else { continue }; + for child in 0..children.len() { + if matches!(children.element(child), Some(Graphic::Vector(_))) { + set_paint_attribute_at(children, child, key, paint.clone()); } - }; - match element { - Graphic::Vector(inner) => fill_list(inner), - Graphic::Graphic(children) => { - for child in children.iter_element_values_mut() { - if let Some(inner) = child.as_vector_mut() { - fill_list(inner); - } - } - } - _ => {} } } } @@ -1213,7 +1240,7 @@ const _: () = { /// [`group_to_legacy_graphic`]'s typed-run path, where `Vector` is tried first. pub fn direct_vector_len(graphic: &Graphic) -> usize { match graphic { - Graphic::Vector(list) => list.len(), + Graphic::Vector(_) => 1, Graphic::Group(group) => match &group.row { None => group.content.typed_lanes::().map_or(0, |lanes| lanes.len()), _ => 0, @@ -1244,14 +1271,14 @@ pub fn group_to_legacy_graphic(group: &core_types::record::Group) -> Graphic { if group.row.is_none() { let item = &group.content; let typed = None - .or_else(|| run_to_legacy_list::(item).map(Graphic::Vector)) - .or_else(|| run_to_legacy_list::>(item).map(Graphic::RasterCPU)) - .or_else(|| run_to_legacy_list::>(item).map(Graphic::RasterGPU)) - .or_else(|| run_to_legacy_list::(item).map(Graphic::Color)) - .or_else(|| run_to_legacy_list::(item).map(Graphic::Gradient)) - .or_else(|| run_to_legacy_list::(item).map(Graphic::Text)); + .or_else(|| run_to_legacy_list::(item).map(|list| detable_items(list, Graphic::Vector))) + .or_else(|| run_to_legacy_list::>(item).map(|list| detable_items(list, Graphic::RasterCPU))) + .or_else(|| run_to_legacy_list::>(item).map(|list| detable_items(list, Graphic::RasterGPU))) + .or_else(|| run_to_legacy_list::(item).map(|list| detable_items(list, Graphic::Color))) + .or_else(|| run_to_legacy_list::(item).map(|list| detable_items(list, Graphic::Gradient))) + .or_else(|| run_to_legacy_list::(item).map(|list| detable_items(list, Graphic::Text))); if let Some(typed) = typed { - return typed; + return Graphic::Graphic(typed); } } Graphic::Graphic(group_to_legacy_list(group)) @@ -1268,17 +1295,13 @@ pub fn group_to_legacy_list(group: &core_types::record::Group) -> List push_lane_paint_into_interiors(&mut list); return list; } - let element = None - .or_else(|| run_to_legacy_list::(item).map(Graphic::Vector)) - .or_else(|| run_to_legacy_list::>(item).map(Graphic::RasterCPU)) - .or_else(|| run_to_legacy_list::>(item).map(Graphic::RasterGPU)) - .or_else(|| run_to_legacy_list::(item).map(Graphic::Color)) - .or_else(|| run_to_legacy_list::(item).map(Graphic::Gradient)) - .or_else(|| run_to_legacy_list::(item).map(Graphic::Text)); - match element { - Some(element) => List::new_from_element(element), - None => List::new(), - } + None.or_else(|| run_to_legacy_list::(item).map(|list| detable_items(list, Graphic::Vector))) + .or_else(|| run_to_legacy_list::>(item).map(|list| detable_items(list, Graphic::RasterCPU))) + .or_else(|| run_to_legacy_list::>(item).map(|list| detable_items(list, Graphic::RasterGPU))) + .or_else(|| run_to_legacy_list::(item).map(|list| detable_items(list, Graphic::Color))) + .or_else(|| run_to_legacy_list::(item).map(|list| detable_items(list, Graphic::Gradient))) + .or_else(|| run_to_legacy_list::(item).map(|list| detable_items(list, Graphic::Text))) + .unwrap_or_default() } fn group_render_complexity(group: &core_types::record::Group) -> usize { @@ -1300,13 +1323,13 @@ fn group_render_complexity(group: &core_types::record::Group) -> usize { impl BoundingBox for Graphic { fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { match self { - Graphic::Vector(list) => list.bounding_box(transform, include_stroke), - Graphic::RasterCPU(list) => list.bounding_box(transform, include_stroke), - Graphic::RasterGPU(list) => list.bounding_box(transform, include_stroke), + Graphic::Vector(vector) => BoundingBox::bounding_box(vector, transform, include_stroke), + Graphic::RasterCPU(raster) => raster.bounding_box(transform, include_stroke), + Graphic::RasterGPU(raster) => raster.bounding_box(transform, include_stroke), Graphic::Graphic(list) => list.bounding_box(transform, include_stroke), - Graphic::Color(list) => list.bounding_box(transform, include_stroke), - Graphic::Gradient(list) => list.bounding_box(transform, include_stroke), - Graphic::Text(list) => list.bounding_box(transform, include_stroke), + Graphic::Color(color) => color.bounding_box(transform, include_stroke), + Graphic::Gradient(gradient) => gradient.bounding_box(transform, include_stroke), + Graphic::Text(text) => text.bounding_box(transform, include_stroke), Graphic::Group(group) => group_bounding_box(group, transform, include_stroke, false), } } @@ -1327,17 +1350,17 @@ impl BoundingBox for Graphic { impl ListConvert for Vector { fn convert_item(self) -> Graphic { - Graphic::Vector(List::new_from_element(self)) + Graphic::Vector(self) } } impl ListConvert for Raster { fn convert_item(self) -> Graphic { - Graphic::RasterCPU(List::new_from_element(self)) + Graphic::RasterCPU(self) } } impl ListConvert for Raster { fn convert_item(self) -> Graphic { - Graphic::RasterGPU(List::new_from_element(self)) + Graphic::RasterGPU(self) } } @@ -1433,7 +1456,7 @@ mod tests { use core_types::list::List; fn vector_graphic() -> Graphic { - Graphic::Vector(List::new_from_element(Vector::default())) + Graphic::Vector(Vector::default()) } // Flattening must not invent attribute columns that neither the parent graphic nor the child carried @@ -1479,7 +1502,7 @@ mod run_tests { #[test] fn a_run_serves_the_parked_paint_reference() { - let paint = List::new_from_element(Graphic::Color(List::new_from_element(Color::BLACK))); + let paint = List::new_from_element(Graphic::Color(Color::BLACK)); let vector = unit_square_at(DVec2::ZERO); let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); @@ -1505,7 +1528,7 @@ mod run_tests { #[test] fn an_owned_group_replays_content_equal_after_the_source_dies() { - let paint = List::new_from_element(Graphic::Color(List::new_from_element(Color::BLACK))); + let paint = List::new_from_element(Graphic::Color(Color::BLACK)); let vector = unit_square_at(DVec2::ZERO); let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); @@ -1711,30 +1734,32 @@ mod run_tests { let inner_item = unsafe { GroupItem::from_resident(RecordBatch::new(inner_bytes.as_ptr(), 1, &inner_layout)) }; let mut painted = List::new(); - painted.push(Item::new_from_element(unit_square_at(DVec2::ZERO))); - painted.push(Item::new_from_element(unit_square_at(DVec2::ONE))); + painted.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::ZERO)))); + painted.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::ONE)))); painted.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(1., 0.))); painted.set_attribute(core_types::ATTR_TRANSFORM, 1, DAffine2::from_translation(DVec2::new(0., 1.))); - set_paint_attribute_at(&mut painted, 1, ATTR_FILL, List::new_from_element(Graphic::Color(List::new_from_element(Color::WHITE)))); + set_paint_attribute_at(&mut painted, 1, ATTR_FILL, List::new_from_element(Graphic::Color(Color::WHITE))); - let mut nested_child = List::new(); - nested_child.push(Item::new_from_element(unit_square_at(DVec2::new(2., 2.)))); - let mut nested = List::new_from_element(Graphic::Vector(nested_child)); + let mut nested = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(2., 2.)))); nested.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_scale(DVec2::splat(2.))); let mut top = List::new(); - top.push(Item::new_from_element(Graphic::Vector(painted))); + top.push(Item::new_from_element(Graphic::Graphic(painted))); top.push(Item::new_from_element(Graphic::Graphic(nested))); top.push(Item::new_from_element(Graphic::Group(core_types::record::Group { row: None, content: inner_item, }))); - top.push(Item::new_from_element(Graphic::Color(List::new_from_element(Color::BLACK)))); + top.push(Item::new_from_element(Graphic::Color(Color::BLACK))); + top.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(6., 0.))))); top.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(5., 5.))); top.set_attribute(core_types::ATTR_EDITOR_LAYER_PATH, 0, vec![core_types::uuid::NodeId(7)]); - set_paint_attribute_at(&mut top, 0, ATTR_FILL, List::new_from_element(Graphic::Color(List::new_from_element(Color::BLACK)))); + set_paint_attribute_at(&mut top, 0, ATTR_FILL, List::new_from_element(Graphic::Color(Color::BLACK))); top.set_attribute(core_types::ATTR_OPACITY, 1, 0.5); top.set_attribute(core_types::ATTR_TRANSFORM, 2, DAffine2::from_scale(DVec2::splat(3.))); + top.set_attribute(core_types::ATTR_TRANSFORM, 4, DAffine2::from_translation(DVec2::new(0., 7.))); + top.set_attribute(core_types::ATTR_EDITOR_LAYER_PATH, 4, vec![core_types::uuid::NodeId(9)]); + set_paint_attribute_at(&mut top, 4, ATTR_FILL, List::new_from_element(Graphic::Color(Color::WHITE))); let legacy = { let mut list = top.clone(); @@ -1744,7 +1769,19 @@ mod run_tests { push_lane_paint_into_interiors(&mut list); list.into_flattened_list::() }; - assert_eq!(flatten_vector_rows(&top), legacy); + let native = flatten_vector_rows(GraphicLevel::Legacy(&top)); + assert_eq!(native.len(), legacy.len()); + for row in 0..native.len() { + assert_eq!(native.attribute::(core_types::ATTR_TRANSFORM, row), legacy.attribute::(core_types::ATTR_TRANSFORM, row), "transform, row {row}"); + assert_eq!(native.attribute::(core_types::ATTR_OPACITY, row), legacy.attribute::(core_types::ATTR_OPACITY, row), "opacity, row {row}"); + assert_eq!( + native.attribute::>(core_types::ATTR_EDITOR_LAYER_PATH, row), + legacy.attribute::>(core_types::ATTR_EDITOR_LAYER_PATH, row), + "layer path, row {row}" + ); + assert_eq!(native.attribute::>>(ATTR_FILL, row), legacy.attribute::>>(ATTR_FILL, row), "fill, row {row}"); + } + assert_eq!(native, legacy); } #[test] @@ -1780,19 +1817,17 @@ mod run_tests { #[cfg(test)] mod graphic_is_opaque_tests { - use vector_types::{ATTR_SPREAD_METHOD, GradientSpreadMethod, GradientStop}; + use vector_types::GradientStop; use super::*; fn color_graphic(alpha: f64) -> Graphic { let color = Color::from_rgbaf32(1., 0., 0., alpha as f32).unwrap(); - Graphic::Color(List::new_from_element(color)) + Graphic::Color(color) } fn gradient_graphic(gradient: GradientStops) -> Graphic { - let mut gradient_list = List::new_from_element(gradient); - gradient_list.set_attribute(ATTR_SPREAD_METHOD, 0, GradientSpreadMethod::Pad); - Graphic::Gradient(gradient_list) + Graphic::Gradient(gradient) } #[test] @@ -1809,7 +1844,7 @@ mod graphic_is_opaque_tests { #[test] fn vector_is_not_opaque() { - let g = Graphic::Vector(List::default()); + let g = Graphic::Vector(Vector::default()); assert!(!g.is_opaque()); } diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index cb10ed88c6..6f64c3de71 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -3,7 +3,6 @@ use crate::{Render, RenderSvgSegmentList, SvgRender}; use core_types::Color; use core_types::attribute::Transform; use core_types::color::SRGBA8; -use core_types::lane::LaneSource; use core_types::list::List; use core_types::uuid::generate_uuid; use glam::{DAffine2, DVec2}; @@ -53,6 +52,20 @@ pub trait RenderExt { ) -> Self::Output; } +/// The color paint attribute over any color lane source. +pub fn render_color_paint>(source: &S, target: PaintTarget) -> String { + let Some(color) = source.element(0) else { + return format!(r#" {}="none""#, target.paint_attr()); + }; + + let mut result = format!(r##" {}="#{}""##, target.paint_attr(), SRGBA8::from(*color).to_rgb_hex()); + if color.a() < 1. { + let _ = write!(result, r#" {}="{}""#, target.opacity_attr(), (color.a() * 1000.).round() / 1000.); + } + + result +} + impl RenderExt for List { type Output = String; @@ -66,16 +79,7 @@ impl RenderExt for List { _render_params: &RenderParams, target: PaintTarget, ) -> Self::Output { - let Some(color) = self.element(0) else { - return format!(r#" {}="none""#, target.paint_attr()); - }; - - let mut result = format!(r##" {}="#{}""##, target.paint_attr(), SRGBA8::from(*color).to_rgb_hex()); - if color.a() < 1. { - let _ = write!(result, r#" {}="{}""#, target.opacity_attr(), (color.a() * 1000.).round() / 1000.); - } - - result + render_color_paint(self, target) } } @@ -93,12 +97,20 @@ impl RenderExt for List { _render_params: &RenderParams, _target: PaintTarget, ) -> Self::Output { - let mut stop = String::new(); + render_gradient_paint(self, svg_defs, item_transform, element_transform) + } +} - let Some(stops) = self.element(0) else { return 0 }; - let gradient_type: GradientType = self.attr::(0); - let local_gradient_transform: DAffine2 = self.attr::(0); - let spread_method: GradientSpreadMethod = self.attr::(0); +/// Adds the gradient def through mutating `svg_defs`, returning the gradient +/// ID, over any gradient lane source. +pub fn render_gradient_paint>(source: &S, svg_defs: &mut String, item_transform: DAffine2, element_transform: DAffine2) -> u64 { + let mut stop = String::new(); + + { + let Some(stops) = source.element(0) else { return 0 }; + let gradient_type: GradientType = source.attr::(0); + let local_gradient_transform: DAffine2 = source.attr::(0); + let spread_method: GradientSpreadMethod = source.attr::(0); for (position, color, original_midpoint) in stops.interpolated_samples() { stop.push_str(" { let paint_attr = target.paint_attr(); match fill_graphic { - Some(Graphic::Color(color_list)) => color_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, render_params, target), - Some(Graphic::Gradient(gradient_list)) => { - let gradient_id = gradient_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, render_params, target); + Some(Graphic::Color(color)) => render_color_paint(&core_types::lane::LeafLane::new(self, 0, color), target), + Some(Graphic::Gradient(gradient)) => { + let gradient_id = render_gradient_paint(&core_types::lane::LeafLane::new(self, 0, gradient), svg_defs, item_transform, element_transform); format!(r##" {paint_attr}="url(#{gradient_id})""##) } Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) | Some(Graphic::Text(_)) | Some(Graphic::Group(_)) => { diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 837f1ed5ba..90b32fbe24 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -12,6 +12,7 @@ use core_types::color::Color; use core_types::color::SRGBA8; use core_types::lane::LaneSource; use core_types::list::{Item, List}; +use core_types::lane::{LeafLane, Single}; use core_types::record::{Group, RunView}; use core_types::math::quad::Quad; use core_types::render_complexity::RenderComplexity; @@ -399,7 +400,7 @@ pub(crate) fn gradient_placement(transform: DAffine2, gradient_type: GradientTyp } } -fn create_peniko_gradient_brush(gradient_list: &List, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> { +fn create_peniko_gradient_brush>(gradient_list: &S, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> { let stops = gradient_list.element(0)?; let gradient_type: GradientType = gradient_list.attr::(0); @@ -552,12 +553,12 @@ impl Render for Graphic { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { match self { Graphic::Graphic(list) => list.render_svg(render, render_params), - Graphic::Vector(list) => list.render_svg(render, render_params), - Graphic::RasterCPU(list) => list.render_svg(render, render_params), + Graphic::Vector(vector) => render_vector_svg(&Single(vector), render, render_params), + Graphic::RasterCPU(raster) => render_raster_cpu_svg(&Single(raster), render, render_params), Graphic::RasterGPU(_) => (), - Graphic::Color(list) => list.render_svg(render, render_params), - Graphic::Gradient(list) => list.render_svg(render, render_params), - Graphic::Text(list) => list.render_svg(render, render_params), + Graphic::Color(color) => render_color_svg(&Single(color), render, render_params), + Graphic::Gradient(gradient) => render_gradient_svg(&Single(gradient), render, render_params), + Graphic::Text(text) => render_text_svg(&Single(text), render, render_params), Graphic::Group(group) => render_group_svg(group, PaintReach::NONE, render, render_params), } } @@ -565,18 +566,18 @@ impl Render for Graphic { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { match self { Graphic::Graphic(list) => list.render_to_vello(scene, transform, context, render_params), - Graphic::Vector(list) => list.render_to_vello(scene, transform, context, render_params), - Graphic::RasterCPU(list) => list.render_to_vello(scene, transform, context, render_params), - Graphic::RasterGPU(list) => list.render_to_vello(scene, transform, context, render_params), - Graphic::Color(list) => list.render_to_vello(scene, transform, context, render_params), - Graphic::Gradient(list) => list.render_to_vello(scene, transform, context, render_params), - Graphic::Text(list) => list.render_to_vello(scene, transform, context, render_params), + Graphic::Vector(vector) => render_vector_vello(&Single(vector), scene, transform, context, render_params), + Graphic::RasterCPU(raster) => render_raster_cpu_vello(&Single(raster), scene, transform, render_params), + Graphic::RasterGPU(raster) => render_raster_gpu_vello(&Single(raster), scene, transform, context, render_params), + Graphic::Color(color) => render_color_vello(&Single(color), scene, render_params), + Graphic::Gradient(gradient) => render_gradient_vello(&Single(gradient), scene, transform, render_params), + Graphic::Text(text) => render_text_vello(&Single(text), scene, transform, render_params), Graphic::Group(group) => render_group_vello(group, PaintReach::NONE, scene, transform, context, render_params), } } fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { - collect_element_metadata(self, PaintReach::NONE, metadata, footprint, element_id) + collect_element_metadata(self, PaintReach::NONE, DAffine2::IDENTITY, None, metadata, footprint, element_id) } fn add_upstream_click_targets(&self, click_targets: &mut Vec) { @@ -590,33 +591,22 @@ impl Render for Graphic { fn contains_artboard(&self) -> bool { match self { Graphic::Graphic(list) => list.contains_artboard(), - Graphic::Vector(list) => list.contains_artboard(), - Graphic::RasterCPU(list) => list.contains_artboard(), - Graphic::RasterGPU(list) => list.contains_artboard(), - Graphic::Color(list) => list.contains_artboard(), - Graphic::Gradient(list) => list.contains_artboard(), - Graphic::Text(list) => list.contains_artboard(), - Graphic::Group(_) => false, + _ => false, } } fn new_ids_from_hash(&mut self, reference: Option) { match self { Graphic::Graphic(list) => list.new_ids_from_hash(reference), - Graphic::Vector(list) => list.new_ids_from_hash(reference), - Graphic::RasterCPU(_) => (), - Graphic::RasterGPU(_) => (), - Graphic::Color(_) => (), - Graphic::Gradient(_) => (), - Graphic::Text(_) => (), - Graphic::Group(_) => (), + Graphic::Vector(vector) => vector.vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default()), + _ => (), } } } fn render_element_svg<'a>(element: &'a Graphic, reach: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) { match element { - Graphic::Vector(inner) if reach.applies() => render_vector_svg(&PaintOverlay::new(inner, reach.paint), render, render_params), + Graphic::Vector(vector) if reach.applies() => render_vector_svg(&PaintOverlay::new(&Single(vector), reach.paint), render, render_params), Graphic::Graphic(inner) => render_graphic_svg_with(inner, reach.nested(), render, render_params), Graphic::Group(group) => render_group_svg(group, reach, render, render_params), _ => element.render_svg(render, render_params), @@ -625,7 +615,7 @@ fn render_element_svg<'a>(element: &'a Graphic, reach: PaintReach<'a>, render: & fn render_element_vello<'a>(element: &'a Graphic, reach: PaintReach<'a>, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { match element { - Graphic::Vector(inner) if reach.applies() => render_vector_vello(&PaintOverlay::new(inner, reach.paint), scene, transform, context, render_params), + Graphic::Vector(vector) if reach.applies() => render_vector_vello(&PaintOverlay::new(&Single(vector), reach.paint), scene, transform, context, render_params), Graphic::Graphic(inner) => render_graphic_vello_with(inner, reach.nested(), scene, transform, context, render_params), Graphic::Group(group) => render_group_vello(group, reach, scene, transform, context, render_params), _ => element.render_to_vello(scene, transform, context, render_params), @@ -634,7 +624,7 @@ fn render_element_vello<'a>(element: &'a Graphic, reach: PaintReach<'a>, scene: fn element_can_reduce_to_clip_path<'a>(element: &'a Graphic, reach: PaintReach<'a>) -> bool { match element { - Graphic::Vector(inner) if reach.applies() => vector_can_reduce_to_clip_path(&PaintOverlay::new(inner, reach.paint)), + Graphic::Vector(vector) if reach.applies() => vector_can_reduce_to_clip_path(&PaintOverlay::new(&Single(vector), reach.paint)), Graphic::Group(group) => match RunView::::new(&group.content) { Some(run) if reach.applies() => vector_can_reduce_to_clip_path(&PaintOverlay::new(&run, reach.paint)), Some(run) => vector_can_reduce_to_clip_path(&run), @@ -644,80 +634,40 @@ fn element_can_reduce_to_clip_path<'a>(element: &'a Graphic, reach: PaintReach<' } } -fn collect_element_metadata<'a>(element: &'a Graphic, reach: PaintReach<'a>, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { +fn collect_element_metadata<'a>( + element: &'a Graphic, + reach: PaintReach<'a>, + lane_transform: DAffine2, + lane_source: Option, + metadata: &mut RenderMetadata, + footprint: Footprint, + element_id: Option, +) { if let Some(element_id) = element_id { + metadata.upstream_footprints.insert(element_id, footprint); match element { - Graphic::Group(group) => { - metadata.upstream_footprints.insert(element_id, footprint); - collect_group_row_metadata(group, metadata, element_id); + Graphic::Group(group) => collect_group_row_metadata(group, metadata, element_id), + Graphic::Graphic(_) => {} + // A leaf's layer identity and transform ride its containing lane. + Graphic::Vector(_) => { + metadata.first_element_source_id.insert(element_id, lane_source); + metadata.local_transforms.insert(element_id, lane_transform); } - Graphic::Graphic(_) => { - metadata.upstream_footprints.insert(element_id, footprint); - } - Graphic::Vector(list) => { - metadata.upstream_footprints.insert(element_id, footprint); - // TODO: Find a way to handle more than the first item - if !list.is_empty() { - let layer_path: &[NodeId] = list.attr::(0); - let layer = layer_path.last().copied(); - let transform: DAffine2 = list.attr::(0); - - metadata.first_element_source_id.insert(element_id, layer); - metadata.local_transforms.insert(element_id, transform); - } - } - Graphic::RasterCPU(list) => { - metadata.upstream_footprints.insert(element_id, footprint); - - // TODO: Find a way to handle more than the first item - if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attr::(0)); - } - } - Graphic::RasterGPU(list) => { - metadata.upstream_footprints.insert(element_id, footprint); - - // TODO: Find a way to handle more than the first item - if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attr::(0)); - } - } - Graphic::Color(list) => { - metadata.upstream_footprints.insert(element_id, footprint); - - // TODO: Find a way to handle more than the first item - if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attr::(0)); - } - } - Graphic::Gradient(list) => { - metadata.upstream_footprints.insert(element_id, footprint); - - // TODO: Find a way to handle more than the first item - if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attr::(0)); - } - } - Graphic::Text(list) => { - metadata.upstream_footprints.insert(element_id, footprint); - - // TODO: Find a way to handle more than the first item - if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attr::(0)); - } + _ => { + metadata.local_transforms.insert(element_id, lane_transform); } } } match element { Graphic::Graphic(list) => collect_graphic_metadata_with(list, reach.nested(), metadata, footprint, element_id), - Graphic::Vector(list) if reach.applies() => collect_vector_metadata(&PaintOverlay::new(list, reach.paint), metadata, footprint, element_id), - Graphic::Vector(list) => collect_vector_metadata(list, metadata, footprint, element_id), - Graphic::RasterCPU(list) => collect_raster_metadata(list, metadata, footprint, element_id), - Graphic::RasterGPU(list) => collect_raster_metadata(list, metadata, footprint, element_id), + Graphic::Vector(vector) if reach.applies() => collect_vector_metadata(&PaintOverlay::new(&Single(vector), reach.paint), metadata, footprint, element_id), + Graphic::Vector(vector) => collect_vector_metadata(&Single(vector), metadata, footprint, element_id), + Graphic::RasterCPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id), + Graphic::RasterGPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id), Graphic::Color(_) => {} Graphic::Gradient(_) => {} - Graphic::Text(list) => collect_text_metadata(list, metadata, footprint, element_id), + Graphic::Text(text) => collect_text_metadata(&Single(text), metadata, footprint, element_id), Graphic::Group(group) => collect_group_metadata(group, reach, metadata, footprint, element_id), } } @@ -754,11 +704,11 @@ fn collect_group_row_metadata(group: &Group, metadata: &mut RenderMetadata, elem fn add_element_upstream_click_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, click_targets: &mut Vec) { match element { Graphic::Graphic(list) => add_graphic_upstream_click_targets_with(list, reach.nested(), click_targets), - Graphic::Vector(list) if reach.applies() => add_vector_upstream_click_targets(&PaintOverlay::new(list, reach.paint), click_targets), - Graphic::Vector(list) => add_vector_upstream_click_targets(list, click_targets), + Graphic::Vector(vector) if reach.applies() => add_vector_upstream_click_targets(&PaintOverlay::new(&Single(vector), reach.paint), click_targets), + Graphic::Vector(vector) => add_vector_upstream_click_targets(&Single(vector), click_targets), Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(click_targets), Graphic::Color(_) | Graphic::Gradient(_) => {} - Graphic::Text(list) => add_text_upstream_click_targets(list, click_targets), + Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), click_targets), Graphic::Group(group) => add_group_upstream_click_targets(group, reach, click_targets), } } @@ -766,11 +716,11 @@ fn add_element_upstream_click_targets<'a>(element: &'a Graphic, reach: PaintReac fn add_element_upstream_outline_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, outlines: &mut Vec) { match element { Graphic::Graphic(list) => add_graphic_upstream_outline_targets_with(list, reach.nested(), outlines), - Graphic::Vector(list) if reach.applies() => add_vector_upstream_outline_targets(&PaintOverlay::new(list, reach.paint), outlines), - Graphic::Vector(list) => add_vector_upstream_outline_targets(list, outlines), + Graphic::Vector(vector) if reach.applies() => add_vector_upstream_outline_targets(&PaintOverlay::new(&Single(vector), reach.paint), outlines), + Graphic::Vector(vector) => add_vector_upstream_outline_targets(&Single(vector), outlines), Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(outlines), Graphic::Color(_) | Graphic::Gradient(_) => {} - Graphic::Text(list) => add_text_upstream_click_targets(list, outlines), + Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), outlines), Graphic::Group(group) => add_group_upstream_outline_targets(group, reach, outlines), } } @@ -1190,10 +1140,10 @@ fn collect_graphic_metadata_with<'a, S: LaneSource>(source: & footprint.transform *= item_transform; if let Some(element_id) = layer { - collect_element_metadata(element, reach, metadata, footprint, Some(element_id)); + collect_element_metadata(element, reach, item_transform, layer, metadata, footprint, Some(element_id)); } else { // Recurse through anonymous wrapper items to reach nested content with editor:layer_path tags - collect_element_metadata(element, reach, metadata, footprint, None); + collect_element_metadata(element, reach, item_transform, layer, metadata, footprint, None); } } @@ -1601,14 +1551,12 @@ fn render_vector_vello>(source: &S, scene: &mut for paint_index in 0..fill_graphic.len() { let Some(paint) = fill_graphic.element(paint_index) else { continue }; match paint { - Graphic::Color(list) => { - let Some(color) = list.element(0) else { continue }; - + Graphic::Color(color) => { let fill = peniko::Brush::Solid(SRGBA8::from(*color).to_peniko_color()); scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path); } - Graphic::Gradient(list) => { - let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(list, &multiplied_transform) else { + Graphic::Gradient(gradient) => { + let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(&LeafLane::new(fill_graphic, paint_index, gradient), &multiplied_transform) else { continue; }; @@ -1683,14 +1631,13 @@ fn render_vector_vello>(source: &S, scene: &mut }; match stroke_graphic { - Graphic::Color(list) => { - let Some(color) = list.element(0) else { continue }; + Graphic::Color(color) => { let brush = peniko::Brush::Solid(SRGBA8::from(*color).to_peniko_color()); scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, None, &path); } - Graphic::Gradient(list) => { - let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(list, &multiplied_transform) else { + Graphic::Gradient(gradient) => { + let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(&LeafLane::new(stroke_graphic_list, paint_index, gradient), &multiplied_transform) else { continue; }; let inverse_element_transform = if transform_is_invertible(element_transform) { @@ -2655,7 +2602,7 @@ pub fn graphic_list_bounding_box>(source: &S, t let item_transform = transform * source.attr::(index); let Some(graphic) = source.element(index) else { continue }; let bounds = match graphic { - Graphic::Text(text_list) => text_list_bounding_box(text_list, item_transform), + Graphic::Text(text) => text_list_bounding_box(&Single(text), item_transform), Graphic::Graphic(sub_list) => graphic_list_bounding_box(sub_list, item_transform), other => other.thumbnail_bounding_box(item_transform, true), }; @@ -3081,7 +3028,6 @@ mod group_walk_tests { use core_types::attribute::Attribute; use core_types::node::RecordBatch; use core_types::record::{FieldWrite, GroupItem, Layout, element_write_hashed}; - use graphic_types::graphic::group_to_legacy_graphic; use graphic_types::markers::Fill; use graphic_types::vector_types::vector::PointId; @@ -3090,7 +3036,7 @@ mod group_walk_tests { } fn color_paint() -> List { - List::new_from_element(Graphic::Color(List::new_from_element(Color::from_rgbaf32(0.8, 0.2, 0.33, 1.).unwrap()))) + List::new_from_element(Graphic::Color(Color::from_rgbaf32(0.8, 0.2, 0.33, 1.).unwrap())) } fn rendered_svg(render: impl FnOnce(&mut SvgRender)) -> (String, String) { @@ -3117,7 +3063,7 @@ mod group_walk_tests { } #[test] - fn a_vector_run_group_renders_like_its_legacy_form() { + fn a_vector_run_group_renders_its_rows_without_layer_wrappers() { let paint = color_paint(); let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(3., 1.))]; let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); @@ -3129,16 +3075,15 @@ mod group_walk_tests { let params = RenderParams::default(); let native = rendered_svg(|render| Graphic::Group(group.clone()).render_svg(render, ¶ms)); - let legacy = rendered_svg(|render| group_to_legacy_graphic(&group).render_svg(render, ¶ms)); - assert!(native.0.contains(r##"fill="#"##), "the run's fill paint must render: {}", native.0); - assert_eq!(native, legacy); + let expected = "\n\n"; + assert_eq!(native, (expected.to_string(), String::new())); } #[test] fn lane_paint_on_a_graphic_run_reaches_vector_interiors() { let paint = color_paint(); - let inner = Graphic::Vector(List::new_from_element(unit_square_at(DVec2::ZERO))); + let inner = Graphic::Vector(unit_square_at(DVec2::ZERO)); let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); // SAFETY: the layout carries a parked graphic element and the fill field. let bytes = unsafe { write_lanes::(&layout, &[&inner], &[Some(&paint)]) }; @@ -3155,7 +3100,7 @@ mod group_walk_tests { } #[test] - fn a_group_collects_like_its_legacy_lowering() { + fn a_group_collects_its_lane_metadata_for_the_caller() { let paint = color_paint(); let vectors = [unit_square_at(DVec2::ZERO)]; let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); @@ -3171,12 +3116,12 @@ mod group_walk_tests { let mut native = RenderMetadata::default(); Graphic::Group(group.clone()).collect_metadata(&mut native, footprint, Some(caller)); - let mut legacy = RenderMetadata::default(); - group_to_legacy_graphic(&group).collect_metadata(&mut legacy, footprint, Some(caller)); - assert!(native.click_targets.get(&caller).is_some_and(|targets| !targets.is_empty())); + assert!(native.outlines.get(&caller).is_some_and(|targets| !targets.is_empty())); assert!(native.local_transforms.contains_key(&caller)); - assert_eq!(native, legacy); + assert!(native.upstream_footprints.contains_key(&caller)); + assert_eq!(native.vector_data.get(&caller).map(|vector| vector.as_ref()), Some(&vectors[0])); + assert!(native.fill_attributes.get(&caller).is_some_and(|fill| matches!(fill.element(0), Some(Graphic::Color(_))))); } #[test] diff --git a/node-graph/nodes/graphic/src/record.rs b/node-graph/nodes/graphic/src/record.rs index b077ca154d..34d2ef338b 100644 --- a/node-graph/nodes/graphic/src/record.rs +++ b/node-graph/nodes/graphic/src/record.rs @@ -300,7 +300,7 @@ mod tests { } fn text(label: &str) -> Graphic { - Graphic::Text(List::new_from_element(label.to_string())) + Graphic::Text(label.to_string()) } fn group(children: Vec<(Graphic, DAffine2)>) -> Graphic { @@ -313,10 +313,10 @@ mod tests { } fn text_of(graphic: &Graphic) -> &str { - let Graphic::Text(list) = graphic else { + let Graphic::Text(text) = graphic else { panic!("expected a text leaf, got {graphic:?}"); }; - list.element(0).expect("a text leaf holds its string") + text } fn translation(x: f64) -> DAffine2 { @@ -367,7 +367,7 @@ mod tests { let arg = core_types::ExtractVarArgs::vararg(input, 0).ok()?; let list = arg.downcast_ref::>()?; let Graphic::Text(text) = list.element(0)? else { return None }; - Some(text.element(0)?.clone()) + Some(text.clone()) } impl<'e> Node> for PerRowSource { diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 52f37ffa18..11dc8094a2 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -3,7 +3,7 @@ use core_types::list::{Item, List}; use core_types::uuid::NodeId; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, BlendMode, Color, Ctx}; use glam::{DAffine2, DVec2}; -use graphic_types::graphic::{PaintColumns, PaintReach, bake_paint_transforms, set_paint_attribute, set_paint_attribute_at}; +use graphic_types::graphic::{GraphicLevel, PaintColumns, PaintReach, bake_paint_transforms, set_paint_attribute, set_paint_attribute_at}; use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::vector_types::GradientStops; use graphic_types::markers::{EditorMergedLayers, Fill, Stroke}; @@ -127,8 +127,7 @@ fn boolean_operation<'e>( > { // SAFETY: a materialized input's frames are arena-resident. let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) }; - let run = core_types::record::RunView::::new(&item).expect("the run holds graphic lanes"); - let flattened = flatten_vector_run(&run, DAffine2::IDENTITY, PaintReach::NONE); + let flattened = flatten_vector_run(GraphicLevel::Run(&item), DAffine2::IDENTITY, PaintReach::NONE); let snapshot = graphic_types::graphic::run_to_render_list::(&item) .expect("the run holds the row's element type") .into_graphic_list(); @@ -282,85 +281,69 @@ fn boolean_operation_on_vector_list(vector: &List, boolean_operation: Bo list } -/// A raster stand-in row: the image's unit rectangle under its transform, -/// black-filled, keeping the layer routing and blending attributes. -fn raster_stand_in_rows(image: &List, parent_transform: DAffine2) -> Vec> { - let make_item = |transform, layer, blend_mode: BlendMode, opacity: f64, fill: f64, clip: bool| { - let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); - subpath.apply_transform(transform); - - let element = Vector::from_subpath(subpath); - - let mut item = Item::new_from_element(element) - .with_attribute(ATTR_BLEND_MODE, blend_mode) - .with_attribute(ATTR_OPACITY, opacity) - .with_attribute(ATTR_OPACITY_FILL, fill) - .with_attribute(ATTR_CLIPPING_MASK, clip) - .with_attribute(ATTR_EDITOR_LAYER_PATH, layer); - set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); - item - }; - - (0..image.len()) +/// A raster stand-in row per lane: the image's unit rectangle under its +/// transform, black-filled, keeping the layer routing and blending +/// attributes. +fn raster_stand_in_rows(image: &S, parent_transform: DAffine2) -> Vec> { + (0..image.lane_count()) .map(|i| { - let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i); - let layer: Vec = image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, i); - let blend_mode: BlendMode = image.attribute_cloned_or_default(ATTR_BLEND_MODE, i); - let opacity: f64 = image.attribute_cloned_or(ATTR_OPACITY, i, 1.); - let fill: f64 = image.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.); - let clip: bool = image.attribute_cloned_or_default(ATTR_CLIPPING_MASK, i); - make_item(parent_transform * row_transform, layer, blend_mode, opacity, fill, clip) + let row_transform: DAffine2 = image.attr::(i); + let layer: Vec = image.attr::(i).to_vec(); + let blend_mode: BlendMode = image.attr::(i); + let opacity: f64 = image.attr::(i); + let fill: f64 = image.attr::(i); + let clip: bool = image.attr::(i); + + let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); + subpath.apply_transform(parent_transform * row_transform); + + let element = Vector::from_subpath(subpath); + + let mut item = Item::new_from_element(element) + .with_attribute(ATTR_BLEND_MODE, blend_mode) + .with_attribute(ATTR_OPACITY, opacity) + .with_attribute(ATTR_OPACITY_FILL, fill) + .with_attribute(ATTR_CLIPPING_MASK, clip) + .with_attribute(ATTR_EDITOR_LAYER_PATH, layer); + set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); + item }) .collect() } -/// A color row: an empty vector carrying the color as its fill paint. -fn color_paint_rows(color: &List) -> Vec> { - color - .clone() - .into_iter() - .map(|row| { - let (color, mut attributes) = row.into_parts(); - set_paint_attribute(&mut attributes, ATTR_FILL, List::new_from_element(color)); +/// A color row: an empty vector carrying the color as its fill paint over the +/// lane's attributes. +fn color_paint_row(color: Color, mut attributes: core_types::list::ItemAttributeValues) -> Item { + set_paint_attribute(&mut attributes, ATTR_FILL, List::new_from_element(color)); - let mut element = Vector::default(); - element.set_stroke_transform(DAffine2::IDENTITY); + let mut element = Vector::default(); + element.set_stroke_transform(DAffine2::IDENTITY); - Item::from_parts(element, attributes) - }) - .collect() + Item::from_parts(element, attributes) } /// A gradient row: an empty vector carrying the stops as its fill paint, the /// gradient keys moved onto the paint. -fn gradient_paint_rows(gradient: &List) -> Vec> { - gradient - .clone() - .into_iter() - .map(|row| { - let (stops, mut attributes) = row.into_parts(); +fn gradient_paint_row(stops: GradientStops, mut attributes: core_types::list::ItemAttributeValues) -> Item { + let mut gradient_paint = List::new_from_element(Graphic::Gradient(stops)); + if let Some(transform) = attributes.remove::(ATTR_TRANSFORM) { + gradient_paint.set_attribute(ATTR_TRANSFORM, 0, transform); + } + if let Some(gradient_type) = attributes.remove::(ATTR_GRADIENT_TYPE) { + gradient_paint.set_attribute(ATTR_GRADIENT_TYPE, 0, gradient_type); + } + if let Some(spread_method) = attributes.remove::(ATTR_SPREAD_METHOD) { + gradient_paint.set_attribute(ATTR_SPREAD_METHOD, 0, spread_method); + } + attributes.insert(ATTR_FILL, Some(gradient_paint)); - let mut gradient_paint = List::new_from_element(stops); - if let Some(transform) = attributes.remove::(ATTR_TRANSFORM) { - gradient_paint.set_attribute(ATTR_TRANSFORM, 0, transform); - } - if let Some(gradient_type) = attributes.remove::(ATTR_GRADIENT_TYPE) { - gradient_paint.set_attribute(ATTR_GRADIENT_TYPE, 0, gradient_type); - } - if let Some(spread_method) = attributes.remove::(ATTR_SPREAD_METHOD) { - gradient_paint.set_attribute(ATTR_SPREAD_METHOD, 0, spread_method); - } - set_paint_attribute(&mut attributes, ATTR_FILL, gradient_paint); + let mut element = Vector::default(); + element.set_stroke_transform(DAffine2::IDENTITY); - let mut element = Vector::default(); - element.set_stroke_transform(DAffine2::IDENTITY); - - Item::from_parts(element, attributes) - }) - .collect() + Item::from_parts(element, attributes) } -/// A text row: the shaped glyph vectors under the composed transform. +/// A text lane's rows: the shaped glyph vectors under the composed transform. fn text_rows(text: &List, parent_transform: DAffine2) -> Vec> { text_nodes::shape_text_list(text, false) .into_iter() @@ -378,9 +361,25 @@ fn push_rows(out: &mut List, rows: Vec>) { } } -fn push_vector_rows(out: &mut List, inner: &List, composed: DAffine2, reach: PaintReach<'_>) { - for row in 0..inner.len() { - let Some(item) = inner.clone_item(row) else { continue }; +/// A de-tabled vector leaf as one row: the lane's attributes with the reach +/// paint and the ancestor transform composed. +fn push_leaf_vector_row(out: &mut List, level: GraphicLevel<'_>, index: usize, vector: &Vector, ancestors: DAffine2, reach: PaintReach<'_>) { + let out_index = out.len(); + out.push(Item::from_parts(vector.clone(), graphic_types::graphic::lane_attributes(level, index))); + if reach.applies() { + for (key, slot) in [(ATTR_FILL, reach.paint.fill), (ATTR_STROKE, reach.paint.stroke)] { + if let Some(paint) = slot { + set_paint_attribute_at(out, out_index, key, paint.clone()); + } + } + } + let current: DAffine2 = out.attribute_cloned_or_default(ATTR_TRANSFORM, out_index); + out.set_attribute(ATTR_TRANSFORM, out_index, ancestors * current); +} + +fn push_vector_rows(out: &mut List, rows: &List, composed: DAffine2, reach: PaintReach<'_>) { + for row in 0..rows.len() { + let Some(item) = rows.clone_item(row) else { continue }; let index = out.len(); out.push(item); if reach.applies() { @@ -401,30 +400,34 @@ fn push_union(out: &mut List, flattened: List) { } } -/// The native flatten over a graphic level: the legacy flatten's arms over a -/// lane source, with lane paint threaded by [`PaintReach`] in place of the -/// legacy pre-push, and native group runs walked directly. -fn flatten_vector_run<'a, S: core_types::lane::LaneSource>(source: &'a S, transform: DAffine2, inherited: PaintReach<'a>) -> List { +/// The native flatten over a graphic level: the legacy flatten's arms over +/// either level storage, with lane paint threaded by [`PaintReach`], leaf +/// attributes read from their lanes, and native group runs walked directly. +fn flatten_vector_run(level: GraphicLevel<'_>, transform: DAffine2, inherited: PaintReach<'_>) -> List { let mut out = List::new(); - flatten_vector_run_into(&mut out, source, transform, inherited); + flatten_vector_run_into(&mut out, level, transform, inherited); out } -fn flatten_vector_run_into<'a, S: core_types::lane::LaneSource>(out: &mut List, source: &'a S, transform: DAffine2, inherited: PaintReach<'a>) { - let columns = PaintColumns::new(source); - for index in 0..source.lane_count() { - let Some(element) = source.element(index) else { continue }; +fn flatten_vector_run_into<'a>(out: &mut List, level: GraphicLevel<'a>, transform: DAffine2, inherited: PaintReach<'a>) { + use core_types::lane::{LaneSource, LeafLane}; + let columns = PaintColumns::new(&level); + for index in 0..level.lane_count() { + let Some(element) = level.element(index) else { continue }; let reach = inherited.for_lane(&columns, index); - let composed = transform * source.attr::(index); + let composed = transform * level.attr::(index); match element { - Graphic::Vector(inner) => push_vector_rows(out, inner, composed, reach), - Graphic::Graphic(children) => push_union(out, flatten_vector_run(children, composed, reach.nested())), + Graphic::Vector(vector) => push_leaf_vector_row(out, level, index, vector, transform, reach), + Graphic::Graphic(children) => push_union(out, flatten_vector_run(GraphicLevel::Legacy(children), composed, reach.nested())), Graphic::Group(group) => flatten_group(out, group, composed, reach), - Graphic::RasterCPU(image) => push_rows(out, raster_stand_in_rows(image, composed)), - Graphic::RasterGPU(image) => push_rows(out, raster_stand_in_rows(image, composed)), - Graphic::Color(color) => push_rows(out, color_paint_rows(color)), - Graphic::Gradient(gradient) => push_rows(out, gradient_paint_rows(gradient)), - Graphic::Text(text) => push_rows(out, text_rows(text, composed)), + Graphic::RasterCPU(raster) => push_rows(out, raster_stand_in_rows(&LeafLane::new(&level, index, raster), transform)), + Graphic::RasterGPU(raster) => push_rows(out, raster_stand_in_rows(&LeafLane::new(&level, index, raster), transform)), + Graphic::Color(color) => push_rows(out, vec![color_paint_row(*color, graphic_types::graphic::lane_attributes(level, index))]), + Graphic::Gradient(gradient) => push_rows(out, vec![gradient_paint_row(gradient.clone(), graphic_types::graphic::lane_attributes(level, index))]), + Graphic::Text(text) => { + let one = List::new_from_item(Item::from_parts(text.clone(), graphic_types::graphic::lane_attributes(level, index))); + push_rows(out, text_rows(&one, composed)); + } } } } @@ -436,172 +439,26 @@ fn flatten_group(out: &mut List, group: &core_types::record::Group, comp let item = &group.content; if let Some(rows) = graphic_types::graphic::run_to_list::(item) { push_vector_rows(out, &rows, composed, reach); - } else if let Some(run) = core_types::record::RunView::::new(item) { - push_union(out, flatten_vector_run(&run, composed, reach.into_group_graphics())); + } else if core_types::record::RunView::::new(item).is_some() { + push_union(out, flatten_vector_run(GraphicLevel::Run(item), composed, reach.into_group_graphics())); } else if let Some(image) = graphic_types::graphic::run_to_list::>(item) { push_rows(out, raster_stand_in_rows(&image, composed)); } else if let Some(image) = graphic_types::graphic::run_to_list::>(item) { push_rows(out, raster_stand_in_rows(&image, composed)); } else if let Some(color) = graphic_types::graphic::run_to_list::(item) { - push_rows(out, color_paint_rows(&color)); + push_rows(out, (0..color.len()).filter_map(|i| Some(color_paint_row(*color.element(i)?, color.clone_item_attributes(i)))).collect()); } else if let Some(gradient) = graphic_types::graphic::run_to_list::(item) { - push_rows(out, gradient_paint_rows(&gradient)); + push_rows( + out, + (0..gradient.len()) + .filter_map(|i| Some(gradient_paint_row(gradient.element(i)?.clone(), gradient.clone_item_attributes(i)))) + .collect(), + ); } else if let Some(text) = graphic_types::graphic::run_to_list::(item) { push_rows(out, text_rows(&text, composed)); } } -/// The legacy baseline the flatten law compares against. -#[cfg(test)] -fn flatten_vector(graphic_list: &List) -> List { - (0..graphic_list.len()) - .flat_map(|index| { - let graphic = graphic_list.element(index).unwrap(); - match graphic.clone() { - Graphic::Group(_) => Vec::new(), - Graphic::Vector(vector) => { - // Apply the parent graphic's transform to each element of the `List` - let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); - vector - .into_iter() - .map(|mut sub_vector| { - let current_transform: DAffine2 = sub_vector.attribute_cloned_or_default(ATTR_TRANSFORM); - *sub_vector.attribute_mut_or_insert_default(ATTR_TRANSFORM) = parent_transform * current_transform; - sub_vector - }) - .collect::>() - } - Graphic::RasterCPU(image) => { - let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let make_item = |transform, layer, blend_mode: BlendMode, opacity: f64, fill: f64, clip: bool| { - let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); - subpath.apply_transform(transform); - - let element = Vector::from_subpath(subpath); - - let mut item = Item::new_from_element(element) - .with_attribute(ATTR_BLEND_MODE, blend_mode) - .with_attribute(ATTR_OPACITY, opacity) - .with_attribute(ATTR_OPACITY_FILL, fill) - .with_attribute(ATTR_CLIPPING_MASK, clip) - .with_attribute(ATTR_EDITOR_LAYER_PATH, layer); - set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); - item - }; - - // Apply the parent graphic's transform to each raster element, preserving each item's layer - // and alpha_blending so the boolean op downstream can route clicks (and inherit blending state) - // back to the originating raster layer - (0..image.len()) - .map(|i| { - let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i); - let layer: Vec = image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, i); - let blend_mode: BlendMode = image.attribute_cloned_or_default(ATTR_BLEND_MODE, i); - let opacity: f64 = image.attribute_cloned_or(ATTR_OPACITY, i, 1.); - let fill: f64 = image.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.); - let clip: bool = image.attribute_cloned_or_default(ATTR_CLIPPING_MASK, i); - make_item(parent_transform * row_transform, layer, blend_mode, opacity, fill, clip) - }) - .collect::>() - } - Graphic::RasterGPU(image) => { - let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let make_item = |transform, layer, blend_mode: BlendMode, opacity: f64, fill: f64, clip: bool| { - let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); - subpath.apply_transform(transform); - - let element = Vector::from_subpath(subpath); - - let mut item = Item::new_from_element(element) - .with_attribute(ATTR_BLEND_MODE, blend_mode) - .with_attribute(ATTR_OPACITY, opacity) - .with_attribute(ATTR_OPACITY_FILL, fill) - .with_attribute(ATTR_CLIPPING_MASK, clip) - .with_attribute(ATTR_EDITOR_LAYER_PATH, layer); - set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); - item - }; - - // Apply the parent graphic's transform to each raster element, preserving each item's layer - // and alpha_blending so the boolean op downstream can route clicks (and inherit blending state) - // back to the originating raster layer - (0..image.len()) - .map(|i| { - let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i); - let layer: Vec = image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, i); - let blend_mode: BlendMode = image.attribute_cloned_or_default(ATTR_BLEND_MODE, i); - let opacity: f64 = image.attribute_cloned_or(ATTR_OPACITY, i, 1.); - let fill: f64 = image.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.); - let clip: bool = image.attribute_cloned_or_default(ATTR_CLIPPING_MASK, i); - make_item(parent_transform * row_transform, layer, blend_mode, opacity, fill, clip) - }) - .collect::>() - } - Graphic::Graphic(mut graphic) => { - let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); - // Apply the parent graphic's transform to each element of the inner `List` - for transform in graphic.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { - *transform = parent_transform * *transform; - } - - // Recursively flatten the inner `List` into the output `List` - let flattened = flatten_vector(&graphic); - let unioned = boolean_operation_on_vector_list(&flattened, BooleanOperation::Union); - - unioned.into_iter().collect::>() - } - Graphic::Color(color) => color - .into_iter() - .map(|row| { - let (color, mut attributes) = row.into_parts(); - set_paint_attribute(&mut attributes, ATTR_FILL, List::new_from_element(color)); - - let mut element = Vector::default(); - element.set_stroke_transform(DAffine2::IDENTITY); - - Item::from_parts(element, attributes) - }) - .collect::>(), - Graphic::Gradient(gradient) => gradient - .into_iter() - .map(|row| { - let (stops, mut attributes) = row.into_parts(); - - let mut gradient_paint = List::new_from_element(stops); - if let Some(transform) = attributes.remove::(ATTR_TRANSFORM) { - gradient_paint.set_attribute(ATTR_TRANSFORM, 0, transform); - } - if let Some(gradient_type) = attributes.remove::(ATTR_GRADIENT_TYPE) { - gradient_paint.set_attribute(ATTR_GRADIENT_TYPE, 0, gradient_type); - } - if let Some(spread_method) = attributes.remove::(ATTR_SPREAD_METHOD) { - gradient_paint.set_attribute(ATTR_SPREAD_METHOD, 0, spread_method); - } - set_paint_attribute(&mut attributes, ATTR_FILL, gradient_paint); - - let mut element = Vector::default(); - element.set_stroke_transform(DAffine2::IDENTITY); - - Item::from_parts(element, attributes) - }) - .collect::>(), - Graphic::Text(text) => { - // Shape the glyphs into vectors (each item's own transform is applied), then compose the parent's transform like the other arms - let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); - text_nodes::shape_text_list(&text, false) - .into_iter() - .map(|mut sub_vector| { - let current_transform: DAffine2 = sub_vector.attribute_cloned_or_default(ATTR_TRANSFORM); - *sub_vector.attribute_mut_or_insert_default(ATTR_TRANSFORM) = parent_transform * current_transform; - sub_vector - }) - .collect::>() - } - } - }) - .collect() -} - // This quantization should potentially be removed since it's not conceptually necessary, // but without it, the oak leaf in the Changing Seasons demo artwork is funky because // quantization is needed for the top and bottom points to line up vertically. @@ -699,11 +556,11 @@ mod tests { } fn black_paint() -> List { - List::new_from_element(Graphic::Color(List::new_from_element(Color::BLACK))) + List::new_from_element(Graphic::Color(Color::BLACK)) } #[test] - fn the_native_flatten_matches_the_legacy_flatten() { + fn the_native_flatten_reads_lanes_groups_and_reach() { let inner_vector = square(DVec2::ZERO); let inner_layout = core_types::record::Layout::default().with_writes(0, core_types::record::element_write_hashed::(), &[]); let mut inner_bytes = vec![0u8; inner_layout.lane_stride()]; @@ -713,43 +570,32 @@ mod tests { // SAFETY: `inner_bytes` holds one lane of `inner_layout` at its stride. let inner_item = unsafe { core_types::record::GroupItem::from_resident(core_types::node::RecordBatch::new(inner_bytes.as_ptr(), 1, &inner_layout)) }; - let mut painted = List::new(); - painted.push(Item::new_from_element(square(DVec2::ZERO))); - painted.push(Item::new_from_element(square(DVec2::ONE))); - painted.set_attribute(ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(1., 0.))); - set_paint_attribute_at(&mut painted, 1, ATTR_FILL, List::new_from_element(Graphic::Color(List::new_from_element(Color::WHITE)))); - - let mut nested_child = List::new(); - nested_child.push(Item::new_from_element(square(DVec2::new(2., 2.)))); - let mut nested = List::new_from_element(Graphic::Vector(nested_child)); - nested.set_attribute(ATTR_TRANSFORM, 0, DAffine2::from_scale(DVec2::splat(2.))); - - let mut colors = List::new_from_element(Color::BLACK); - colors.set_attribute(ATTR_OPACITY, 0, 0.5); - let mut top = List::new(); - top.push(Item::new_from_element(Graphic::Vector(painted))); - top.push(Item::new_from_element(Graphic::Graphic(nested))); - top.push(Item::new_from_element(Graphic::Color(colors))); + top.push(Item::new_from_element(Graphic::Vector(square(DVec2::ZERO)))); + top.push(Item::new_from_element(Graphic::Color(Color::BLACK))); top.push(Item::new_from_element(Graphic::Group(Group { row: None, content: inner_item }))); top.set_attribute(ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(5., 5.))); set_paint_attribute_at(&mut top, 0, ATTR_FILL, black_paint()); - top.set_attribute(ATTR_TRANSFORM, 3, DAffine2::from_scale(DVec2::splat(3.))); + top.set_attribute(ATTR_OPACITY, 1, 0.5); + top.set_attribute(ATTR_TRANSFORM, 2, DAffine2::from_scale(DVec2::splat(3.))); - let legacy = { - let mut prepared = top.clone(); - // The legacy pre-push, written by hand: the painted lane's fill - // lands on every interior item. - if let Some(Graphic::Vector(inner)) = prepared.element_mut(0) { - for index in 0..inner.len() { - set_paint_attribute_at(inner, index, ATTR_FILL, black_paint()); - } - } - if let Some(element) = prepared.element_mut(3) { - *element = graphic_types::graphic::map_groups_to_legacy(element); - } - flatten_vector(&prepared) - }; - assert_eq!(flatten_vector_run(&top, DAffine2::IDENTITY, PaintReach::NONE), legacy); + let rows = flatten_vector_run(GraphicLevel::Legacy(&top), DAffine2::IDENTITY, PaintReach::NONE); + assert_eq!(rows.len(), 3); + + // Lane 0: the leaf row keeps its lane attributes, with the lane fill + // present and the ancestor composition the identity. + assert_eq!(rows.attribute_cloned_or_default::(ATTR_TRANSFORM, 0), DAffine2::from_translation(DVec2::new(5., 5.))); + assert!(graphic_types::graphic::paint_graphics::(&rows, 0).is_some()); + + // Lane 1: the color stand-in carries the lane opacity and the color as + // its fill. + assert_eq!(rows.attribute_cloned_or::(ATTR_OPACITY, 1, 1.), 0.5); + let fill = graphic_types::graphic::paint_graphics::(&rows, 1).expect("the color row carries its fill"); + assert!(matches!(fill.element(0), Some(Graphic::Color(color)) if *color == Color::BLACK)); + + // Lane 2: the group's vector run serves its row under the lane + // transform. + assert_eq!(rows.attribute_cloned_or_default::(ATTR_TRANSFORM, 2), DAffine2::from_scale(DVec2::splat(3.))); + assert_eq!(rows.element(2).unwrap(), &inner_vector); } } diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index cde7734061..9a14fc78b4 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -202,7 +202,7 @@ fn assign_colors_graphic<'e>( core_types::registry::cache_key(&keyed) }; let generation = ctx.arena().generation(); - let (length, mut position) = { + let (length, position) = { let mut cached = lane_offsets.lock().unwrap_or_else(std::sync::PoisonError::into_inner); if !matches!(cached.as_ref(), Some(entry) if entry.key == key && entry.generation == generation) { let mut offsets = Vec::with_capacity(content.len() + 1); @@ -218,20 +218,29 @@ fn assign_colors_graphic<'e>( (entry.offsets[content.len()], entry.offsets[lane]) }; - if let Some(vector_list) = element.as_vector_mut() { - for index in 0..vector_list.len() { - let color = assign_color_at(gradient_element, position, length, randomize, seed, repeat_every); + // A de-tabled vector leaf carries no attributes, so the paint rides the + // containing lane: a bare leaf wraps into a one-lane list, and a lowered + // vector run's leaves take one color per lane. + if graphic_types::graphic::direct_vector_len(content.element_ref(lane)) > 0 { + let mut children = match element { + Graphic::Graphic(children) => children, + leaf => List::new_from_element(leaf), + }; + let mut consumed = 0; + for index in 0..children.len() { + let Some(Graphic::Vector(vector)) = children.element(index) else { continue }; + let has_stroke = vector.stroke.is_some(); + let color = assign_color_at(gradient_element, position + consumed, length, randomize, seed, repeat_every); let paint = List::new_from_element(color).into_graphic_list(); - if fill { - set_paint_attribute_at(vector_list, index, ATTR_FILL, paint.clone()); + set_paint_attribute_at(&mut children, index, ATTR_FILL, paint.clone()); } - if stroke && vector_list.element(index).is_some_and(|vector| vector.stroke.is_some()) { - set_paint_attribute_at(vector_list, index, ATTR_STROKE, paint.clone()); + if stroke && has_stroke { + set_paint_attribute_at(&mut children, index, ATTR_STROKE, paint.clone()); } - - position += 1; + consumed += 1; } + element = Graphic::Graphic(children); } Ok((element, transform, layer_path)) @@ -277,21 +286,20 @@ fn park_paint(arena: &core_types::arena::Arena, paint: List) -> Result< /// The gradient defaulting the legacy fill performed, applied to the nested /// stops list the paint table wraps. fn default_gradient_paint(paint: &mut List, bounds: Option<[DVec2; 2]>, gradient_type: GradientType, spread_method: GradientSpreadMethod, transform: Option) { + let has_type = paint.iter_attribute_values::(ATTR_GRADIENT_TYPE).is_some(); + let has_spread = paint.iter_attribute_values::(ATTR_SPREAD_METHOD).is_some(); + let has_transform = paint.iter_attribute_values::(ATTR_TRANSFORM).is_some(); for index in 0..paint.len() { - let Some(Graphic::Gradient(gradient)) = paint.element_mut(index) else { continue }; - if gradient.iter_attribute_values::(ATTR_GRADIENT_TYPE).is_none() { - for value in gradient.iter_attribute_values_mut_or_default::(ATTR_GRADIENT_TYPE) { - *value = gradient_type; - } + if !matches!(paint.element(index), Some(Graphic::Gradient(_))) { + continue; } - - if gradient.iter_attribute_values::(ATTR_SPREAD_METHOD).is_none() { - for value in gradient.iter_attribute_values_mut_or_default::(ATTR_SPREAD_METHOD) { - *value = spread_method; - } + if !has_type { + paint.set_attribute(ATTR_GRADIENT_TYPE, index, gradient_type); } - - if gradient.iter_attribute_values::(ATTR_TRANSFORM).is_none() { + if !has_spread { + paint.set_attribute(ATTR_SPREAD_METHOD, index, spread_method); + } + if !has_transform { let transform = transform.unwrap_or_else(|| { // Nudge a degenerate axis so the gradient transform stays invertible, matching the editor's `nonzero_bounding_box` let [min, mut max] = bounds.unwrap_or([DVec2::ZERO, DVec2::ONE]); @@ -303,10 +311,7 @@ fn default_gradient_paint(paint: &mut List, bounds: Option<[DVec2; 2]>, } initial_gradient_transform_for_bounding_box([min, max]) }); - - for value in gradient.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { - *value = transform; - } + paint.set_attribute(ATTR_TRANSFORM, index, transform); } } } @@ -420,18 +425,13 @@ fn stroke<'e>( /// The vector items of a graphic lane's interior, one wrap level deep, the /// reach of the pre-flip broadcast over a legacy list. fn for_each_interior_vector_mut(element: &mut Graphic, mut f: impl FnMut(&mut Vector, DAffine2)) { - let mut walk_list = |list: &mut List| { - let (elements, transforms) = list.element_and_attribute_slices_mut::(ATTR_TRANSFORM); - for (vector, transform) in elements.iter_mut().zip(transforms.iter()) { - f(vector, *transform); - } - }; match element { - Graphic::Vector(list) => walk_list(list), + Graphic::Vector(vector) => f(vector, DAffine2::IDENTITY), Graphic::Graphic(children) => { - for child in children.iter_element_values_mut() { - if let Some(list) = child.as_vector_mut() { - walk_list(list); + for index in 0..children.len() { + let transform: DAffine2 = children.attribute_cloned_or_default(ATTR_TRANSFORM, index); + if let Some(Graphic::Vector(vector)) = children.element_mut(index) { + f(vector, transform); } } } @@ -1449,9 +1449,9 @@ fn solidify_rows(flattened: List) -> List { /// lane addresses (a fill-bearing row serves two lanes), builds and splits /// only that row, and lane 0 additionally carries the merged-layers snapshot. #[allow(clippy::type_complexity)] -fn solidify_native_lane<'e, S: core_types::lane::LaneSource>( +fn solidify_native_lane<'e>( arena: &'e core_types::arena::Arena, - source: &S, + level: graphic_types::graphic::GraphicLevel<'_>, snapshot: impl FnOnce() -> List, lane: usize, ) -> Result< @@ -1472,7 +1472,7 @@ fn solidify_native_lane<'e, S: core_types::lane::LaneSource>( use graphic_types::graphic::RowStep; let mut remaining = lane; let mut located: Option> = None; - graphic_types::graphic::walk_vector_rows(source, &mut |row| { + graphic_types::graphic::walk_vector_rows(level, &mut |row| { let parts = 1 + row.has_fill() as usize; if remaining >= parts { remaining -= parts; @@ -1622,8 +1622,7 @@ fn solidify_stroke<'e>( > { // SAFETY: a materialized input's frames are arena-resident. let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) }; - let run = core_types::record::RunView::::new(&item).expect("the run holds graphic lanes"); - solidify_native_lane(ctx.arena(), &run, || legacy_graphic_list_of(content), ctx.index() as usize) + solidify_native_lane(ctx.arena(), graphic_types::graphic::GraphicLevel::Run(&item), || legacy_graphic_list_of(content), ctx.index() as usize) } /// A fill-bearing row splits into a fill lane and a solidified stroke lane, @@ -1663,7 +1662,7 @@ fn solidify_stroke_vector<'e>( Interrupt, > { let wrapper = wrap_vector_level(content); - solidify_native_lane(ctx.arena(), &wrapper, || legacy_graphic_list_of(content), ctx.index() as usize) + solidify_native_lane(ctx.arena(), graphic_types::graphic::GraphicLevel::Legacy(&wrapper), || legacy_graphic_list_of(content), ctx.index() as usize) } fn solidify_stroke_vector_extent(content: ListIn<'_, Vector>, level: LevelIn) -> GPoll { @@ -1905,8 +1904,7 @@ pub fn flatten_path<'e>( > { // SAFETY: a materialized input's frames are arena-resident. let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) }; - let run = core_types::record::RunView::::new(&item).expect("the run holds graphic lanes"); - let flattened = graphic_types::graphic::flatten_vector_rows(&run); + let flattened = graphic_types::graphic::flatten_vector_rows(graphic_types::graphic::GraphicLevel::Run(&item)); let snapshot = graphic_types::graphic::run_to_render_list::(&item).expect("the run holds the row's element type"); flatten_path_core(ctx.arena(), flattened, snapshot) } @@ -1929,7 +1927,7 @@ pub fn flatten_path_vector<'e>( Interrupt, > { let wrapper = wrap_vector_level(content); - let flattened = graphic_types::graphic::flatten_vector_rows(&wrapper); + let flattened = graphic_types::graphic::flatten_vector_rows(graphic_types::graphic::GraphicLevel::Legacy(&wrapper)); let snapshot = legacy_graphic_list_of(content); flatten_path_core(ctx.arena(), flattened, snapshot) } @@ -2724,9 +2722,9 @@ fn morph_core(flattened: List, snapshot: List, progression: f64 } } - fn lerp_gradient_transform(gradient_list_a: &List, gradient_list_b: &List, time: f64) -> DAffine2 { - let transform_a = gradient_list_a.attribute_cloned_or_default::(ATTR_TRANSFORM, 0); - let transform_b = gradient_list_b.attribute_cloned_or_default::(ATTR_TRANSFORM, 0); + fn lerp_gradient_transform(paint_a: &List, paint_b: &List, time: f64) -> DAffine2 { + let transform_a = paint_a.attribute_cloned_or_default::(ATTR_TRANSFORM, 0); + let transform_b = paint_b.attribute_cloned_or_default::(ATTR_TRANSFORM, 0); let start_a = transform_a.translation; let end_a = transform_a.translation + transform_a.matrix2.x_axis; @@ -2754,47 +2752,37 @@ fn morph_core(flattened: List, snapshot: List, progression: f64 (Some(a), Some(b)) => (a, b), }; - // This keeps the gradient metadata attributes - let gradient_with_stops = |mut gradient_list: List, stops: GradientStops| -> Graphic { - if let Some(target) = gradient_list.element_mut(0) { - *target = stops; - } else { - gradient_list.push(Item::new_from_element(stops)); + // This keeps the gradient metadata attributes, which ride the paint lane + let gradient_paint = |metadata_source: &List, stops: GradientStops, transform: Option| -> List { + let mut out = List::new_from_item(Item::from_parts(Graphic::Gradient(stops), metadata_source.clone_item_attributes(0))); + if let Some(transform) = transform { + out.set_attribute(ATTR_TRANSFORM, 0, transform); } - Graphic::Gradient(gradient_list) + out }; - let graphic = match (a.element(0), b.element(0)) { - (Some(Graphic::Color(color_list_a)), Some(Graphic::Color(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::Color(color_list_a)), Some(Graphic::Gradient(gradient_list_b))) => color_list_a.element(0).zip(gradient_list_b.element(0)).map(|(color_a, stops_b)| { + match (a.element(0), b.element(0)) { + (Some(Graphic::Color(color_a)), Some(Graphic::Color(color_b))) => Some(List::new_from_element(Graphic::from(color_a.lerp(color_b, time as f32)))), + (Some(Graphic::Color(color_a)), Some(Graphic::Gradient(stops_b))) => { let mut solid_to_gradient = stops_b.clone(); solid_to_gradient.color.iter_mut().for_each(|color| *color = *color_a); let stops = solid_to_gradient.lerp(stops_b, time); - gradient_with_stops(gradient_list_b.clone(), stops) - }), - (Some(Graphic::Gradient(gradient_list_a)), Some(Graphic::Color(color_list_b))) => gradient_list_a.element(0).zip(color_list_b.element(0)).map(|(stops_a, color_b)| { + Some(gradient_paint(b, stops, None)) + } + (Some(Graphic::Gradient(stops_a)), Some(Graphic::Color(color_b))) => { let mut gradient_to_solid = stops_a.clone(); gradient_to_solid.color.iter_mut().for_each(|color| *color = *color_b); let stops = stops_a.lerp(&gradient_to_solid, time); - gradient_with_stops(gradient_list_a.clone(), stops) - }), - (Some(Graphic::Gradient(gradient_list_a)), Some(Graphic::Gradient(gradient_list_b))) => gradient_list_a.element(0).zip(gradient_list_b.element(0)).map(|(stops_a, stops_b)| { + Some(gradient_paint(a, stops, None)) + } + (Some(Graphic::Gradient(stops_a)), Some(Graphic::Gradient(stops_b))) => { let stops = stops_a.lerp(stops_b, time); - let metadata_source = if time < 0.5 { gradient_list_a } else { gradient_list_b }; - - let mut gradient_list = metadata_source.clone(); - gradient_list.set_attribute(ATTR_TRANSFORM, 0, lerp_gradient_transform(gradient_list_a, gradient_list_b, time)); - - gradient_with_stops(gradient_list, stops) - }), + let metadata_source = if time < 0.5 { a } else { b }; + Some(gradient_paint(metadata_source, stops, Some(lerp_gradient_transform(a, b, time)))) + } // 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() }), - }; - - graphic.map(List::new_from_element) + _ => Some(if time < 0.5 { a.clone() } else { b.clone() }), + } } // Preserve the original legacy snapshot as upstream data so this group layer's nested layers can be edited by the tools. @@ -3323,8 +3311,7 @@ fn morph<'e>( let path = graphic_types::graphic::run_to_list::(&path_item).expect("the run holds vector lanes"); // SAFETY: a materialized input's frames are arena-resident. let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) }; - let run = core_types::record::RunView::::new(&item).expect("the run holds graphic lanes"); - let flattened = graphic_types::graphic::flatten_vector_rows(&run); + let flattened = graphic_types::graphic::flatten_vector_rows(graphic_types::graphic::GraphicLevel::Run(&item)); morph_lane(ctx.arena(), flattened, legacy_graphic_list_of(content), progression, reverse, distribution, path) } @@ -3357,7 +3344,7 @@ fn morph_vector<'e>( let path_item = unsafe { core_types::record::GroupItem::from_resident(path.batch()) }; let path = graphic_types::graphic::run_to_list::(&path_item).expect("the run holds vector lanes"); let wrapper = wrap_vector_level(content); - let flattened = graphic_types::graphic::flatten_vector_rows(&wrapper); + let flattened = graphic_types::graphic::flatten_vector_rows(graphic_types::graphic::GraphicLevel::Legacy(&wrapper)); morph_lane(ctx.arena(), flattened, legacy_graphic_list_of(content), progression, reverse, distribution, path) } @@ -3961,10 +3948,10 @@ mod test { let fill = paint_graphics::(&morphed, 0).expect("Morph should keep the fill paint at the midpoint"); // Interpolated color between red and blue should have >0 value on both R and B - let Some(Graphic::Color(colors)) = fill.element(0) else { + let Some(Graphic::Color(color)) = fill.element(0) else { panic!("Expected a solid color fill, got {:?}", fill.element(0)); }; - let color = *colors.element(0).expect("Color present"); + let color = *color; assert!(color.r() > 0. && color.b() > 0., "Fill should be a red-to-blue blend, got {color:?}"); }