diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 99dfdbe41c..21b4b054e3 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -770,6 +770,20 @@ const _: () = { }; /// The graphic with every `Group` converted to its legacy form. +/// The count [`map_groups_to_legacy`] would expose through [`Graphic::as_vector`], +/// read from the run's lanes instead of materializing the legacy list. Mirrors +/// [`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::Group(group) => match (&group.row, &group.content) { + (None, core_types::record::GroupContent::Run(item)) => item.typed_lanes::().map_or(0, |lanes| lanes.len()), + _ => 0, + }, + _ => 0, + } +} + pub fn map_groups_to_legacy(graphic: &Graphic) -> Graphic { match graphic { Graphic::Group(group) => group_to_legacy_graphic(group), diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index a8c7925368..77dc316e0a 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -163,8 +163,9 @@ fn assign_colors_extent( /// under the assign colors identifier. #[node_macro::node(category(""), extent(assign_colors_graphic_extent))] fn assign_colors_graphic<'e>( - ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, + ctx: impl Ctx + CacheHash + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, content: IList, + #[data] lane_offsets: std::sync::Arc>>, #[default(true)] fill: bool, stroke: bool, gradient: IList, @@ -195,9 +196,27 @@ fn assign_colors_graphic<'e>( // The interiors the pre-flip node reached: only a lane's DIRECT vector // list, so wrapped groups keep their own styling and consume no position. - let count_lane = |row: usize| graphic_types::graphic::map_groups_to_legacy(content.element_ref(row)).as_vector().map_or(0, |list| list.len()); - let length: usize = (0..content.len()).map(count_lane).sum(); - let mut position: usize = (0..lane).map(count_lane).sum(); + let key = { + let mut keyed = *ctx; + core_types::context::InjectIndex::set_index(&mut keyed, 0); + core_types::registry::cache_key(&keyed) + }; + let generation = ctx.arena().generation(); + let (length, mut 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); + let mut running = 0; + offsets.push(running); + for row in 0..content.len() { + running += graphic_types::graphic::direct_vector_len(content.element_ref(row)); + offsets.push(running); + } + *cached = Some(LaneOffsets { key, generation, offsets }); + } + let entry = cached.as_ref().expect("populated above"); + (entry.offsets[content.len()], entry.offsets[lane]) + }; if let Some(vector_list) = element.as_vector_mut() { for index in 0..vector_list.len() { @@ -218,6 +237,16 @@ fn assign_colors_graphic<'e>( Ok((element, transform, layer_path)) } +/// Where each lane's colors start in the level's flattened vector run, so the +/// level is counted once per evaluation rather than once per lane. `offsets` +/// holds one entry per lane plus the total. +#[derive(Debug)] +pub struct LaneOffsets { + key: u64, + generation: u64, + offsets: Vec, +} + #[allow(clippy::too_many_arguments)] fn assign_colors_graphic_extent( content: ListIn<'_, Graphic>,