From ff104a0dbe725e8a4bed5dfc05efe6a8762c3807 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 26 Feb 2026 02:50:06 -0800 Subject: [PATCH 1/5] Reduce recusive flattening algorithm duplication --- .../libraries/graphic-types/src/graphic.rs | 220 +++++------------- 1 file changed, 57 insertions(+), 163 deletions(-) diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 444352046e..b2311530d7 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -104,57 +104,62 @@ impl From> for Graphic { } } -// Local trait to convert types to Table (avoids orphan rule issues) -pub trait IntoGraphicTable { - fn into_graphic_table(self) -> Table; - - /// Deeply flattens any vector content within a graphic table, discarding non-vector content, and returning a table of only vector elements. - fn into_flattened_vector_table(self) -> Table - where - Self: std::marker::Sized, - { - let content = self.into_graphic_table(); - - // TODO: Avoid mutable reference, instead return a new Table? - fn flatten_table(output_vector_table: &mut Table, current_graphic_table: Table) { - for current_graphic_row in current_graphic_table.iter() { - let current_graphic = current_graphic_row.element.clone(); - let source_node_id = *current_graphic_row.source_node_id; - - match current_graphic { - // If we're allowed to recurse, flatten any tables we encounter - Graphic::Graphic(mut current_graphic_table) => { - // Apply the parent graphic's transform to all child elements - for graphic in current_graphic_table.iter_mut() { - *graphic.transform = *current_graphic_row.transform * *graphic.transform; - } - - flatten_table(output_vector_table, current_graphic_table); +/// Deeply flattens a graphic table, collecting only elements matching a specific variant (extracted by `extract_variant`) +/// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-tables composes transforms and opacity. +fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic) -> Option>) -> Table { + fn flatten_recursive(output: &mut Table, current_graphic_table: Table, extract_variant: fn(Graphic) -> Option>) { + for current_graphic_row in current_graphic_table.iter() { + let current_graphic = current_graphic_row.element.clone(); + let source_node_id = *current_graphic_row.source_node_id; + + match current_graphic { + // Recurse into nested graphic tables, composing the parent's transform onto each child + Graphic::Graphic(mut sub_table) => { + for graphic in sub_table.iter_mut() { + *graphic.transform = *current_graphic_row.transform * *graphic.transform; } - // Push any leaf Vector elements we encounter - Graphic::Vector(vector_table) => { - for current_vector_row in vector_table.iter() { - output_vector_table.push(TableRow { - element: current_vector_row.element.clone(), - transform: *current_graphic_row.transform * *current_vector_row.transform, + flatten_recursive(output, sub_table, extract_variant); + } + // Try to extract the target variant; if it matches, push its rows with composed transform and opacity + other => { + if let Some(typed_table) = extract_variant(other) { + for row in typed_table.iter() { + output.push(TableRow { + element: row.element.clone(), + transform: *current_graphic_row.transform * *row.transform, alpha_blending: AlphaBlending { - blend_mode: current_vector_row.alpha_blending.blend_mode, - opacity: current_graphic_row.alpha_blending.opacity * current_vector_row.alpha_blending.opacity, - fill: current_vector_row.alpha_blending.fill, - clip: current_vector_row.alpha_blending.clip, + blend_mode: row.alpha_blending.blend_mode, + opacity: current_graphic_row.alpha_blending.opacity * row.alpha_blending.opacity, + fill: row.alpha_blending.fill, + clip: row.alpha_blending.clip, }, source_node_id, }); } } - _ => {} } } } + } + + let mut output = Table::new(); + flatten_recursive(&mut output, content, extract_variant); + output +} + +// Local trait to convert types to Table (avoids orphan rule issues) +pub trait IntoGraphicTable { + fn into_graphic_table(self) -> Table; - let mut output = Table::new(); - flatten_table(&mut output, content); - output + /// Deeply flattens any vector content within a graphic table, discarding non-vector content, and returning a table of only vector elements. + fn into_flattened_vector_table(self) -> Table + where + Self: std::marker::Sized, + { + flatten_graphic_table(self.into_graphic_table(), |g| match g { + Graphic::Vector(t) => Some(t), + _ => None, + }) } /// Deeply flattens any raster content within a graphic table, discarding non-raster content, and returning a table of only raster elements. @@ -162,47 +167,10 @@ pub trait IntoGraphicTable { where Self: std::marker::Sized, { - let content = self.into_graphic_table(); - - fn flatten_table(output_raster_table: &mut Table>, current_graphic_table: Table) { - for current_graphic_row in current_graphic_table.iter() { - let current_graphic = current_graphic_row.element.clone(); - let source_node_id = *current_graphic_row.source_node_id; - - match current_graphic { - // If we're allowed to recurse, flatten any tables we encounter - Graphic::Graphic(mut current_graphic_table) => { - // Apply the parent graphic's transform to all child elements - for graphic in current_graphic_table.iter_mut() { - *graphic.transform = *current_graphic_row.transform * *graphic.transform; - } - - flatten_table(output_raster_table, current_graphic_table); - } - // Push any leaf RasterCPU elements we encounter - Graphic::RasterCPU(raster_table) => { - for current_raster_row in raster_table.iter() { - output_raster_table.push(TableRow { - element: current_raster_row.element.clone(), - transform: *current_graphic_row.transform * *current_raster_row.transform, - alpha_blending: AlphaBlending { - blend_mode: current_raster_row.alpha_blending.blend_mode, - opacity: current_graphic_row.alpha_blending.opacity * current_raster_row.alpha_blending.opacity, - fill: current_raster_row.alpha_blending.fill, - clip: current_raster_row.alpha_blending.clip, - }, - source_node_id, - }); - } - } - _ => {} - } - } - } - - let mut output = Table::new(); - flatten_table(&mut output, content); - output + flatten_graphic_table(self.into_graphic_table(), |g| match g { + Graphic::RasterCPU(t) => Some(t), + _ => None, + }) } /// Deeply flattens any color content within a graphic table, discarding non-color content, and returning a table of only color elements. @@ -210,47 +178,10 @@ pub trait IntoGraphicTable { where Self: std::marker::Sized, { - let content = self.into_graphic_table(); - - fn flatten_table(output_color_table: &mut Table, current_graphic_table: Table) { - for current_graphic_row in current_graphic_table.iter() { - let current_graphic = current_graphic_row.element.clone(); - let source_node_id = *current_graphic_row.source_node_id; - - match current_graphic { - // If we're allowed to recurse, flatten any tables we encounter - Graphic::Graphic(mut current_graphic_table) => { - // Apply the parent graphic's transform to all child elements - for graphic in current_graphic_table.iter_mut() { - *graphic.transform = *current_graphic_row.transform * *graphic.transform; - } - - flatten_table(output_color_table, current_graphic_table); - } - // Push any leaf Color elements we encounter - Graphic::Color(color_table) => { - for current_color_row in color_table.iter() { - output_color_table.push(TableRow { - element: *current_color_row.element, - transform: *current_graphic_row.transform * *current_color_row.transform, - alpha_blending: AlphaBlending { - blend_mode: current_color_row.alpha_blending.blend_mode, - opacity: current_graphic_row.alpha_blending.opacity * current_color_row.alpha_blending.opacity, - fill: current_color_row.alpha_blending.fill, - clip: current_color_row.alpha_blending.clip, - }, - source_node_id, - }); - } - } - _ => {} - } - } - } - - let mut output = Table::new(); - flatten_table(&mut output, content); - output + flatten_graphic_table(self.into_graphic_table(), |g| match g { + Graphic::Color(t) => Some(t), + _ => None, + }) } /// Deeply flattens any gradient content within a graphic table, discarding non-gradient content, and returning a table of only gradient elements. @@ -258,47 +189,10 @@ pub trait IntoGraphicTable { where Self: std::marker::Sized, { - let content = self.into_graphic_table(); - - fn flatten_table(output_gradient_table: &mut Table, current_graphic_table: Table) { - for current_graphic_row in current_graphic_table.iter() { - let current_graphic = current_graphic_row.element.clone(); - let source_node_id = *current_graphic_row.source_node_id; - - match current_graphic { - // If we're allowed to recurse, flatten any tables we encounter - Graphic::Graphic(mut current_graphic_table) => { - // Apply the parent graphic's transform to all child elements - for graphic in current_graphic_table.iter_mut() { - *graphic.transform = *current_graphic_row.transform * *graphic.transform; - } - - flatten_table(output_gradient_table, current_graphic_table); - } - // Push any leaf GradientStops elements we encounter - Graphic::Gradient(gradient_table) => { - for current_gradient_row in gradient_table.iter() { - output_gradient_table.push(TableRow { - element: current_gradient_row.element.clone(), - transform: *current_graphic_row.transform * *current_gradient_row.transform, - alpha_blending: AlphaBlending { - blend_mode: current_gradient_row.alpha_blending.blend_mode, - opacity: current_graphic_row.alpha_blending.opacity * current_gradient_row.alpha_blending.opacity, - fill: current_gradient_row.alpha_blending.fill, - clip: current_gradient_row.alpha_blending.clip, - }, - source_node_id, - }); - } - } - _ => {} - } - } - } - - let mut output = Table::new(); - flatten_table(&mut output, content); - output + flatten_graphic_table(self.into_graphic_table(), |g| match g { + Graphic::Gradient(t) => Some(t), + _ => None, + }) } } From 0dc856eb985b847c03b4afc5a8875dae08d5461c Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 26 Feb 2026 02:59:59 -0800 Subject: [PATCH 2/5] Generalize further --- .../libraries/graphic-types/src/graphic.rs | 66 +++++++++---------- node-graph/libraries/graphic-types/src/lib.rs | 2 +- node-graph/nodes/graphic/src/graphic.rs | 10 +-- node-graph/nodes/vector/src/vector_nodes.rs | 2 +- 4 files changed, 37 insertions(+), 43 deletions(-) diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index b2311530d7..278eaf0c42 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -147,52 +147,46 @@ fn flatten_graphic_table(content: Table, extract_variant: fn( output } -// Local trait to convert types to Table (avoids orphan rule issues) -pub trait IntoGraphicTable { - fn into_graphic_table(self) -> Table; +/// Maps from a concrete element type to its corresponding `Graphic` enum variant, +/// enabling type-directed casting of typed tables from a `Graphic` value. +pub trait TryFromGraphic: Clone + Sized { + fn try_from_graphic(graphic: Graphic) -> Option>; +} - /// Deeply flattens any vector content within a graphic table, discarding non-vector content, and returning a table of only vector elements. - fn into_flattened_vector_table(self) -> Table - where - Self: std::marker::Sized, - { - flatten_graphic_table(self.into_graphic_table(), |g| match g { - Graphic::Vector(t) => Some(t), - _ => None, - }) +impl TryFromGraphic for Vector { + fn try_from_graphic(graphic: Graphic) -> Option> { + if let Graphic::Vector(t) = graphic { Some(t) } else { None } } +} - /// Deeply flattens any raster content within a graphic table, discarding non-raster content, and returning a table of only raster elements. - fn into_flattened_raster_table(self) -> Table> - where - Self: std::marker::Sized, - { - flatten_graphic_table(self.into_graphic_table(), |g| match g { - Graphic::RasterCPU(t) => Some(t), - _ => None, - }) +impl TryFromGraphic for Raster { + fn try_from_graphic(graphic: Graphic) -> Option> { + if let Graphic::RasterCPU(t) = graphic { Some(t) } else { None } } +} - /// Deeply flattens any color content within a graphic table, discarding non-color content, and returning a table of only color elements. - fn into_flattened_color_table(self) -> Table - where - Self: std::marker::Sized, - { - flatten_graphic_table(self.into_graphic_table(), |g| match g { - Graphic::Color(t) => Some(t), - _ => None, - }) +impl TryFromGraphic for Color { + fn try_from_graphic(graphic: Graphic) -> Option> { + if let Graphic::Color(t) = graphic { Some(t) } else { None } } +} - /// Deeply flattens any gradient content within a graphic table, discarding non-gradient content, and returning a table of only gradient elements. - fn into_flattened_gradient_table(self) -> Table +impl TryFromGraphic for GradientStops { + fn try_from_graphic(graphic: Graphic) -> Option> { + if let Graphic::Gradient(t) = graphic { Some(t) } else { None } + } +} + +// Local trait to convert types to Table (avoids orphan rule issues) +pub trait IntoGraphicTable { + fn into_graphic_table(self) -> Table; + + /// Deeply flattens any content of type `T` within a graphic table, discarding all other content, and returning a flat table of only `T` elements. + fn into_flattened_table(self) -> Table where Self: std::marker::Sized, { - flatten_graphic_table(self.into_graphic_table(), |g| match g { - Graphic::Gradient(t) => Some(t), - _ => None, - }) + flatten_graphic_table(self.into_graphic_table(), T::try_from_graphic) } } diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index 49b23f92f7..998dbc55fe 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -8,7 +8,7 @@ pub use vector_types; // Re-export commonly used types at the crate root pub use artboard::Artboard; -pub use graphic::{Graphic, IntoGraphicTable, Vector}; +pub use graphic::{TryFromGraphic, Graphic, IntoGraphicTable, Vector}; pub mod migrations { use core_types::{ diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index ffd98cec1e..72f1e23208 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -295,31 +295,31 @@ pub async fn flatten_graphic(_: impl Ctx, content: Table, fully_flatten /// Converts a graphic table into a vector table by deeply flattening any vector content it contains, and discarding any non-vector content. #[node_macro::node(category("Vector"))] pub async fn flatten_vector(_: impl Ctx, #[implementations(Table, Table)] content: T) -> Table { - content.into_flattened_vector_table() + content.into_flattened_table() } /// Converts a graphic table into a raster table by deeply flattening any raster content it contains, and discarding any non-raster content. #[node_macro::node(category("Raster"))] pub async fn flatten_raster(_: impl Ctx, #[implementations(Table, Table>)] content: T) -> Table> { - content.into_flattened_raster_table() + content.into_flattened_table() } /// Converts a graphic table into a color table by deeply flattening any color content it contains, and discarding any non-color content. #[node_macro::node(category("General"))] pub async fn flatten_color(_: impl Ctx, #[implementations(Table, Table)] content: T) -> Table { - content.into_flattened_color_table() + content.into_flattened_table() } /// Converts a graphic table into a gradient table by deeply flattening any gradient content it contains, and discarding any non-gradient content. #[node_macro::node(category("General"))] pub async fn flatten_gradient(_: impl Ctx, #[implementations(Table, Table)] content: T) -> Table { - content.into_flattened_gradient_table() + content.into_flattened_table() } /// Constructs a gradient from a table of colors, where the colors are evenly distributed as gradient stops across the range from 0 to 1. #[node_macro::node(category("Color"))] fn colors_to_gradient(_: impl Ctx, #[implementations(Table, Table)] colors: T) -> GradientStops { - let colors = colors.into_flattened_color_table(); + let colors = colors.into_flattened_table::(); let total_colors = colors.len(); if total_colors == 0 { diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 68d26202f1..66dffd557d 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -1782,7 +1782,7 @@ async fn morph( let graphic_table_content = content.clone().into_graphic_table(); // If the input isn't a Table, we convert it into one by flattening any Table content. - let content = content.into_flattened_vector_table(); + let content = content.into_flattened_table::(); // Determine source and target indices and interpolation time fraction let progression = progression.max(0.); From f1d87e0264428cd137b7508fb2ddd9a560cdc785 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 26 Feb 2026 03:13:33 -0800 Subject: [PATCH 3/5] Avoid code duplication in the 'Flatten Path' node --- node-graph/libraries/graphic-types/src/lib.rs | 2 +- node-graph/nodes/vector/src/vector_nodes.rs | 72 +++++-------------- 2 files changed, 18 insertions(+), 56 deletions(-) diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index 998dbc55fe..54e4302bca 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -8,7 +8,7 @@ pub use vector_types; // Re-export commonly used types at the crate root pub use artboard::Artboard; -pub use graphic::{TryFromGraphic, Graphic, IntoGraphicTable, Vector}; +pub use graphic::{Graphic, IntoGraphicTable, TryFromGraphic, Vector}; pub mod migrations { use core_types::{ diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 66dffd557d..87bdaf1d40 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -1212,66 +1212,28 @@ async fn map_points(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: Table Combine Paths pair of nodes. #[node_macro::node(category("Vector"), path(graphene_core::vector))] -pub async fn flatten_path( - _: impl Ctx, - #[implementations( - Table, - Table, - )] - content: Table, -) -> Table -where - Graphic: From>, -{ - // NOTE(AdamGerhant): - // A node-based solution to support passing through vector data could be a network node with a cache node - // connected to a Flatten Path connected to an if else node, another connection from the cache directly to - // the if else node, and another connection from the cache to a matches type node connected to the if else node. - - fn flatten_table(output: &mut TableRowMut, graphic_table: &Table) { - for (current_index, current_element) in graphic_table.iter().enumerate() { - match current_element.element { - Graphic::Vector(vector) => { - // Loop through every row of the `Table` and concatenate each element's subpath into the output `Vector` element. - for (vector_index, row) in vector.iter().enumerate() { - let other = row.element; - let transform = *current_element.transform * *row.transform; - let node_id = current_element.source_node_id.map(|node_id| node_id.0).unwrap_or_default(); - - let mut hasher = DefaultHasher::new(); - (current_index, vector_index, node_id).hash(&mut hasher); - let collision_hash_seed = hasher.finish(); - - output.element.concat(other, transform, collision_hash_seed); - - // TODO: Make this instead use the first encountered style - // Use the last encountered style as the output style - output.element.style = row.element.style.clone(); - } - } - Graphic::Graphic(graphic) => { - let mut graphic = graphic.clone(); - for row in graphic.iter_mut() { - *row.transform = *current_element.transform * *row.transform; - } - - flatten_table(output, &graphic); - } - _ => {} - } - } - } - +pub async fn flatten_path(_: impl Ctx, #[implementations(Table, Table)] content: T) -> Table { // Create a table with one empty `Vector` element, then get a mutable reference to it which we append flattened subpaths to let mut output_table = Table::new_from_element(Vector::default()); - let Some(mut output) = output_table.iter_mut().next() else { return output_table }; + let Some(output) = output_table.iter_mut().next() else { return output_table }; + + // Concatenate every vector element's subpaths into the single output compound path + for (index, row) in content.into_flattened_table().iter().enumerate() { + let node_id = row.source_node_id.map(|node_id| node_id.0).unwrap_or_default(); + + let mut hasher = DefaultHasher::new(); + (index, node_id).hash(&mut hasher); + let collision_hash_seed = hasher.finish(); - // Flatten the graphic input into the output `Vector` element - let base_graphic_table = Table::new_from_element(Graphic::from(content)); - flatten_table(&mut output, &base_graphic_table); + output.element.concat(row.element, *row.transform, collision_hash_seed); + + // TODO: Make this instead use the first encountered style + // Use the last encountered style as the output style + output.element.style = row.element.style.clone(); + } - // Return the single-row Table containing the flattened Vector subpaths output_table } From aa8d22bf8c0c96850e88c32827e204c8a9c317e4 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 26 Feb 2026 04:03:28 -0800 Subject: [PATCH 4/5] Avoid cloning --- .../libraries/graphic-types/src/graphic.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 278eaf0c42..aabd3f520f 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -106,27 +106,26 @@ impl From> for Graphic { /// Deeply flattens a graphic table, collecting only elements matching a specific variant (extracted by `extract_variant`) /// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-tables composes transforms and opacity. -fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic) -> Option>) -> Table { - fn flatten_recursive(output: &mut Table, current_graphic_table: Table, extract_variant: fn(Graphic) -> Option>) { - for current_graphic_row in current_graphic_table.iter() { - let current_graphic = current_graphic_row.element.clone(); - let source_node_id = *current_graphic_row.source_node_id; +fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic) -> Option>) -> Table { + fn flatten_recursive(output: &mut Table, current_graphic_table: Table, extract_variant: fn(Graphic) -> Option>) { + for current_graphic_row in current_graphic_table.into_iter() { + let source_node_id = current_graphic_row.source_node_id; - match current_graphic { + match current_graphic_row.element { // Recurse into nested graphic tables, composing the parent's transform onto each child Graphic::Graphic(mut sub_table) => { for graphic in sub_table.iter_mut() { - *graphic.transform = *current_graphic_row.transform * *graphic.transform; + *graphic.transform = current_graphic_row.transform * *graphic.transform; } flatten_recursive(output, sub_table, extract_variant); } // Try to extract the target variant; if it matches, push its rows with composed transform and opacity other => { if let Some(typed_table) = extract_variant(other) { - for row in typed_table.iter() { + for row in typed_table.into_iter() { output.push(TableRow { - element: row.element.clone(), - transform: *current_graphic_row.transform * *row.transform, + element: row.element, + transform: current_graphic_row.transform * row.transform, alpha_blending: AlphaBlending { blend_mode: row.alpha_blending.blend_mode, opacity: current_graphic_row.alpha_blending.opacity * row.alpha_blending.opacity, From 3ae1af1aa7665c217aa53a166242c3f54606b8fa Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 26 Feb 2026 04:27:31 -0800 Subject: [PATCH 5/5] Include intermediate levels of alpha blending composition --- .../libraries/graphic-types/src/graphic.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index aabd3f520f..001c0c33a2 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -107,6 +107,15 @@ impl From> for Graphic { /// Deeply flattens a graphic table, collecting only elements matching a specific variant (extracted by `extract_variant`) /// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-tables composes transforms and opacity. fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic) -> Option>) -> Table { + fn compose_alpha_blending(parent: AlphaBlending, child: AlphaBlending) -> AlphaBlending { + AlphaBlending { + blend_mode: child.blend_mode, + opacity: parent.opacity * child.opacity, + fill: child.fill, + clip: child.clip, + } + } + fn flatten_recursive(output: &mut Table, current_graphic_table: Table, extract_variant: fn(Graphic) -> Option>) { for current_graphic_row in current_graphic_table.into_iter() { let source_node_id = current_graphic_row.source_node_id; @@ -116,7 +125,9 @@ fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic Graphic::Graphic(mut sub_table) => { for graphic in sub_table.iter_mut() { *graphic.transform = current_graphic_row.transform * *graphic.transform; + *graphic.alpha_blending = compose_alpha_blending(current_graphic_row.alpha_blending, *graphic.alpha_blending); } + flatten_recursive(output, sub_table, extract_variant); } // Try to extract the target variant; if it matches, push its rows with composed transform and opacity @@ -126,12 +137,7 @@ fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic output.push(TableRow { element: row.element, transform: current_graphic_row.transform * row.transform, - alpha_blending: AlphaBlending { - blend_mode: row.alpha_blending.blend_mode, - opacity: current_graphic_row.alpha_blending.opacity * row.alpha_blending.opacity, - fill: row.alpha_blending.fill, - clip: row.alpha_blending.clip, - }, + alpha_blending: compose_alpha_blending(current_graphic_row.alpha_blending, row.alpha_blending), source_node_id, }); }