From 8c67ad508f71a91da18989cabd85b0c7d20e0b9f Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 24 Aug 2026 00:43:44 +0000 Subject: [PATCH] Restore pre-flip color parity: rng replay, typed wrap, boolean marker paints --- .../node_graph/document_node_definitions.rs | 19 ++------- .../libraries/graphic-types/src/graphic.rs | 39 +++++++++++++++---- node-graph/nodes/graphic/src/graphic.rs | 9 ++++- node-graph/nodes/path-bool/src/lib.rs | 26 ++++++++----- node-graph/nodes/vector/src/vector_nodes.rs | 16 +++++--- 5 files changed, 68 insertions(+), 41 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 6a3a77500a..8b9497a4ee 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -159,9 +159,10 @@ fn document_node_definitions() -> HashMap HashMap HashMap>(paint_key) { bake_graphic_paint_transform(graphics, transform); + } else if let Some(Some(graphics)) = attributes.get_mut::>>(paint_key) { + bake_graphic_paint_transform(graphics, transform); } } } @@ -604,9 +606,7 @@ fn group_is_fully_transparent(group: &core_types::record::Group) -> bool { core_types::record::GroupContent::Run(item) => { let attrs = RunAttrs::of(item); let lanes = item.typed_lanes::(); - (0..item.len()).all(|lane| { - RunAttrs::read_or(item, attrs.opacity, lane, 1.) <= 0. || lanes.as_ref().is_some_and(|lanes| lanes.element_ref(lane).is_fully_transparent()) - }) + (0..item.len()).all(|lane| RunAttrs::read_or(item, attrs.opacity, lane, 1.) <= 0. || lanes.as_ref().is_some_and(|lanes| lanes.element_ref(lane).is_fully_transparent())) } core_types::record::GroupContent::Stack(children) => children.iter().all(group_is_fully_transparent), } @@ -719,7 +719,9 @@ pub fn run_to_render_list(item: &core_types::r fn push_lane_paint_into_interiors(list: &mut List) { for index in 0..list.len() { for key in [ATTR_FILL, ATTR_STROKE] { - let Some(paint) = paint_at(list, index, key).filter(|paint| is_paint_present(paint)).cloned() else { continue }; + let Some(paint) = paint_at(list, index, key).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() { @@ -767,10 +769,10 @@ const _: () = { } }; -/// The graphic with every `Group` converted to its legacy list form. +/// The graphic with every `Group` converted to its legacy form. pub fn map_groups_to_legacy(graphic: &Graphic) -> Graphic { match graphic { - Graphic::Group(group) => Graphic::Graphic(group_to_legacy_list(group)), + Graphic::Group(group) => group_to_legacy_graphic(group), Graphic::Graphic(children) => { let mut children = children.clone(); for child in children.iter_element_values_mut() { @@ -782,6 +784,27 @@ pub fn map_groups_to_legacy(graphic: &Graphic) -> Graphic { } } +/// The group as one legacy graphic. A bare (row-less) wrap of a single typed +/// run keeps the run's typed variant, matching the `Into` the +/// pre-flip wrap applied; everything else becomes the legacy group list. +pub fn group_to_legacy_graphic(group: &core_types::record::Group) -> Graphic { + if group.row.is_none() + && let core_types::record::GroupContent::Run(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)); + if let Some(typed) = typed { + return typed; + } + } + Graphic::Graphic(group_to_legacy_list(group)) +} + /// The group as a legacy `List`: a `Graphic` run becomes the items, /// another typed run becomes one item holding its typed list, and stack /// segments become one item each with the segment's row attributes. @@ -810,7 +833,7 @@ pub fn group_to_legacy_list(group: &core_types::record::Group) -> List core_types::record::GroupContent::Stack(children) => { let mut list = List::new(); for child in children { - list.push(Item::new_from_element(Graphic::Graphic(group_to_legacy_list(child)))); + list.push(Item::new_from_element(group_to_legacy_graphic(child))); let index = list.len() - 1; if let Some(row) = &child.row { if !row.is_empty() { diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 07f3f224f3..8fd991f7f5 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -468,9 +468,14 @@ pub fn legacy_layer_extend( } /// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input. +/// The wrapped run keeps the level's element type, so the legacy boundary can +/// lower a wrapped vector level to the bare typed graphic the pre-flip wrap made. /// The inverse of this node is 'Flatten Graphic'. #[node_macro::node(category("General"), extent(wrap_graphic_extent))] -pub fn wrap_graphic(_: impl Ctx + ExtractIndex + InjectIndex + Copy, content: IList) -> Result, Interrupt> { +pub fn wrap_graphic( + _: impl Ctx + ExtractIndex + InjectIndex + Copy, + #[implementations(Graphic, Vector, Raster, Raster, Color, GradientStops, String)] content: IList, +) -> Result, Interrupt> { // SAFETY: a materialized input's frames are arena-resident. let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) }; Ok(Graphic::Group(core_types::record::Group { @@ -480,7 +485,7 @@ pub fn wrap_graphic(_: impl Ctx + ExtractIndex + InjectIndex + Copy, content: IL } /// The collected group is the level's single lane. -fn wrap_graphic_extent(_content: ListIn<'_, Graphic>, _level: LevelIn) -> GPoll { +fn wrap_graphic_extent(_content: ListIn<'_, T>, _level: LevelIn) -> GPoll { GPoll::Final(Extent::Exactly(1)) } diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 8e6a0edf8e..f356aeb865 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -1,15 +1,15 @@ +use core_types::attribute::{Attr, BlendMode as BlendModeAttr, ClippingMask, EditorLayerPath, Opacity, OpacityFill, Transform as TransformAttr}; use core_types::list::{Item, List}; use core_types::uuid::NodeId; -use core_types::attribute::{Attr, BlendMode as BlendModeAttr, ClippingMask, EditorLayerPath, Opacity, OpacityFill, Transform as TransformAttr}; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, BlendMode, Color, Ctx}; -use graphic_types::markers::{EditorMergedLayers, Fill, Stroke}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{bake_paint_transforms, set_paint_attribute}; +use graphic_types::markers::{EditorMergedLayers, Fill, Stroke}; use graphic_types::vector_types::gradient::{GradientSpreadMethod, GradientType}; -use graphic_types::vector_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD}; use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath}; use graphic_types::vector_types::vector::PointId; use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt; +use graphic_types::vector_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD}; use graphic_types::{ATTR_FILL, Graphic, IntoGraphicList, Vector}; use linesweeper::topology::Topology; use linesweeper::{BinaryOp, FillRule, binary_op}; @@ -21,7 +21,11 @@ pub use vector_types::vector::misc::BooleanOperation; // TODO: since before we used a Vec of single-item `List`s and now we use a single `List` // TODO: with multiple items while still assuming a single item for the boolean operations. -fn boolean_core<'e>(arena: &'e core_types::arena::Arena, content: List, operation: BooleanOperation) -> Result< +fn boolean_core<'e>( + arena: &'e core_types::arena::Arena, + content: List, + operation: BooleanOperation, +) -> Result< ( Vector, Attr<'e, TransformAttr>, @@ -54,10 +58,12 @@ fn boolean_core<'e>(arena: &'e core_types::arena::Arena, content: List, result_vector_list.element_mut(0).unwrap().merge_by_distance_spatial(merge_transform, 0.0001); } - let exhausted = || core_types::gpoll::Interrupt::from(core_types::gpoll::GraphError { - kind: core_types::gpoll::ErrorKind::ArenaExhausted, - trace: Vec::new(), - }); + let exhausted = || { + core_types::gpoll::Interrupt::from(core_types::gpoll::GraphError { + kind: core_types::gpoll::ErrorKind::ArenaExhausted, + trace: Vec::new(), + }) + }; let park_paint = |paint: Option>| -> Result>, core_types::gpoll::Interrupt> { match paint { Some(list) => Ok(Some(arena.alloc(list).ok_or_else(exhausted)?.0)), @@ -66,8 +72,8 @@ fn boolean_core<'e>(arena: &'e core_types::arena::Arena, content: List, }; let element = result_vector_list.element(0).cloned().unwrap_or_default(); - let fill = park_paint(result_vector_list.attribute::>(graphic_types::ATTR_FILL, 0).cloned())?; - let stroke = park_paint(result_vector_list.attribute::>(graphic_types::ATTR_STROKE, 0).cloned())?; + let fill = park_paint(graphic_types::graphic::graphic_list_at(&result_vector_list, 0, graphic_types::ATTR_FILL).map(|paint| paint.into_owned()))?; + let stroke = park_paint(graphic_types::graphic::graphic_list_at(&result_vector_list, 0, graphic_types::ATTR_STROKE).map(|paint| paint.into_owned()))?; let layer_path: Vec = result_vector_list .attribute::>(ATTR_EDITOR_LAYER_PATH, 0) .map(|path| path.iter_element_values().copied().collect()) diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index daea2dba56..6e186fd09d 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -58,7 +58,11 @@ fn assign_color_at(gradient: &GradientStops, position: usize, length: usize, ran let factor = match randomize { true => { let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into()); - (0..=position).map(|_| rng.random::()).next_back().unwrap_or_default() + let mut draw = 0.; + for _ in 0..=position { + draw = rng.random::(); + } + draw } false => match repeat_every { 0 => position as f64 / (length - 1).max(1) as f64, @@ -171,7 +175,7 @@ fn assign_colors_graphic<'e>( if lane >= content.len() { return Err(GraphError::past_end().into()); } - let mut element = content.element_ref(lane).clone(); + let mut element = graphic_types::graphic::map_groups_to_legacy(content.element_ref(lane)); let (transform, layer_path) = carried_lane_attrs(ctx.arena(), content.lane(lane))?; if gradient.len() == 0 { @@ -187,9 +191,11 @@ fn assign_colors_graphic<'e>( false => gradient_element, }; - let interior_count = |graphic: &Graphic| graphic.as_vector().map_or(0, |list| list.len()); - let length: usize = (0..content.len()).map(|row| interior_count(content.element_ref(row))).sum(); - let mut position: usize = (0..lane).map(|row| interior_count(content.element_ref(row))).sum(); + // 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(); if let Some(vector_list) = element.as_vector_mut() { for index in 0..vector_list.len() {