diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index b021f65231..627423dd56 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -133,6 +133,18 @@ fn node_registry() -> HashMap> { .into_iter() .map(|entry| (graphene_std::path_bool_nodes::boolean_operation::IDENTIFIER.clone(), entry)), ); + // The path flattening's plain vector rows, served under its identifier. + node_types.extend( + graphene_std::vector::flatten_path_vector_entries() + .into_iter() + .map(|entry| (graphene_std::vector::flatten_path::IDENTIFIER.clone(), entry)), + ); + // The solidify's plain vector rows, served under its identifier. + node_types.extend( + graphene_std::vector::solidify_stroke_vector_entries() + .into_iter() + .map(|entry| (graphene_std::vector::solidify_stroke::IDENTIFIER.clone(), entry)), + ); // Element-wise coercion into `Graphic` for single-typed leveled inputs, // served by the to_graphic rows. node_types.extend( diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 700b092b1c..ef6216face 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -405,9 +405,9 @@ mod tests { // Works properly let grid = grid(&(), (), GridType::Isometric, 10., 5, 5, (30., 30.).into()); - assert_eq!(grid.element(0).unwrap().point_domain.ids().len(), 5 * 5); - assert_eq!(grid.element(0).unwrap().segment_bezier_iter().count(), 4 * 5 + 4 * 9); - for (_, bezier, _, _) in grid.element(0).unwrap().segment_bezier_iter() { + assert_eq!(grid.point_domain.ids().len(), 5 * 5); + assert_eq!(grid.segment_bezier_iter().count(), 4 * 5 + 4 * 9); + for (_, bezier, _, _) in grid.segment_bezier_iter() { assert_eq!(bezier.handles, subpath::BezierHandles::Linear); assert!( ((bezier.start - bezier.end).length() - 10.).abs() < 1e-5, @@ -420,20 +420,20 @@ mod tests { #[test] fn skew_isometric_grid_test() { let grid = grid(&(), (), GridType::Isometric, 10., 5, 5, (40., 30.).into()); - assert_eq!(grid.element(0).unwrap().point_domain.ids().len(), 5 * 5); - assert_eq!(grid.element(0).unwrap().segment_bezier_iter().count(), 4 * 5 + 4 * 9); - for (_, bezier, _, _) in grid.element(0).unwrap().segment_bezier_iter() { + assert_eq!(grid.point_domain.ids().len(), 5 * 5); + assert_eq!(grid.segment_bezier_iter().count(), 4 * 5 + 4 * 9); + for (_, bezier, _, _) in grid.segment_bezier_iter() { assert_eq!(bezier.handles, subpath::BezierHandles::Linear); let vector = bezier.start - bezier.end; let angle = (vector.angle_to(DVec2::X).to_degrees() + 180.) % 180.; - assert!([90., 150., 40.].into_iter().any(|target| (target - angle).abs() < 1e-10), "unexpected angle of {angle}") + assert!([90f64, 150., 40.].into_iter().any(|target| (target - angle).abs() < 1e-10), "unexpected angle of {angle}") } } #[test] fn qr_code_test() { let qr = qr_code(&(), (), "https://graphite.art".to_string(), false, 1., QRCodeErrorCorrectionLevel::Low, true); - assert!(qr.element(0).unwrap().point_domain.ids().len() > 0); - assert!(qr.element(0).unwrap().segment_domain.ids().len() > 0); + assert!(qr.point_domain.ids().len() > 0); + assert!(qr.segment_domain.ids().len() > 0); } } diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 121014733f..ff9b4e5474 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -8,10 +8,12 @@ use core_types::list::{Item, ItemAttributeValues, List, ListDyn}; use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; use core_types::transform::{Footprint, Transform}; use core_types::uuid::NodeId; -use core_types::attribute::Attr; +use core_types::attribute::{Attr, BlendMode as BlendModeAttr, ClippingMask, EditorLayerPath, Opacity, OpacityFill}; +use core_types::extent::{ListIn, LevelIn}; +use core_types::gpoll::{Extent, GPoll}; use core_types::gpoll::GraphError; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex}; -use graphic_types::markers::{Fill, Stroke as StrokeAttr}; +use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr}; use core_types::attribute::Transform as TransformAttr; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Vector; @@ -1266,11 +1268,9 @@ fn offset_path(_: impl Ctx, content: List, distance: f64, join: StrokeJo .collect() } -#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -fn solidify_stroke(_: impl Ctx, #[implementations(List, List)] content: T) -> List { +fn solidify_stroke_core(graphic_list: List) -> List { // TODO: Make this node support stroke align, which it currently ignores - let graphic_list = content.into_graphic_list(); let flattened: List = graphic_list.clone().into_flattened_list(); // A fill exists when the canonical attribute carries paint @@ -1374,6 +1374,143 @@ fn solidify_stroke(_: impl Ctx, #[implementations(List( + arena: &'e core_types::arena::Arena, + graphic_list: List, + lane: usize, +) -> Result< + ( + Vector, + Attr<'e, TransformAttr>, + Attr<'e, Fill>, + Attr<'e, StrokeAttr>, + Attr<'e, BlendModeAttr>, + Attr<'e, Opacity>, + Attr<'e, OpacityFill>, + Attr<'e, ClippingMask>, + Attr<'e, EditorLayerPath>, + Attr<'e, EditorMergedLayers>, + ), + Interrupt, +> { + let output = solidify_stroke_core(graphic_list); + if lane >= output.len() { + return Err(GraphError::past_end().into()); + } + let exhausted = || { + Interrupt::from(GraphError { + kind: core_types::gpoll::ErrorKind::ArenaExhausted, + trace: Vec::new(), + }) + }; + + let element = output.element(lane).cloned().unwrap_or_default(); + let fill = output.attribute::>(ATTR_FILL, lane).map(|paint| park_paint(arena, paint.clone())).transpose()?; + let stroke = output.attribute::>(ATTR_STROKE, lane).map(|paint| park_paint(arena, paint.clone())).transpose()?; + let layer_path: Vec = output + .attribute::>(ATTR_EDITOR_LAYER_PATH, lane) + .map(|path| path.iter_element_values().copied().collect()) + .unwrap_or_default(); + let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; + let merged_layers = output + .attribute::>(ATTR_EDITOR_MERGED_LAYERS, lane) + .map(|layers| arena.alloc(layers.clone()).ok_or_else(exhausted).map(|(parked, _)| parked)) + .transpose()?; + + Ok(( + element, + Attr(output.attribute_cloned_or_default(ATTR_TRANSFORM, lane)), + Attr(fill), + Attr(stroke), + Attr(output.attribute_cloned_or_default(ATTR_BLEND_MODE, lane)), + Attr(output.attribute_cloned_or(ATTR_OPACITY, lane, 1.)), + Attr(output.attribute_cloned_or(ATTR_OPACITY_FILL, lane, 1.)), + Attr(output.attribute_cloned_or_default(ATTR_CLIPPING_MASK, lane)), + Attr(layer_path.as_slice()), + Attr(merged_layers), + )) +} + +/// The materialized level as the legacy graphic list the solidify body walks. +fn legacy_graphic_list_of(content: core_types::node::List<'_, T>) -> List +where + List: IntoGraphicList, +{ + // SAFETY: a materialized input's frames are arena-resident. + let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) }; + graphic_types::graphic::run_to_render_list::(&item) + .expect("the run holds the row's element type") + .into_graphic_list() +} + +#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), extent(solidify_stroke_extent))] +fn solidify_stroke<'e>( + ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, + content: IList, +) -> Result< + ( + Vector, + Attr<'e, TransformAttr>, + Attr<'e, Fill>, + Attr<'e, StrokeAttr>, + Attr<'e, BlendModeAttr>, + Attr<'e, Opacity>, + Attr<'e, OpacityFill>, + Attr<'e, ClippingMask>, + Attr<'e, EditorLayerPath>, + Attr<'e, EditorMergedLayers>, + ), + Interrupt, +> { + solidify_lane(ctx.arena(), legacy_graphic_list_of(content), ctx.innermost_index() as usize) +} + +/// A fill-bearing row splits into a fill lane and a solidified stroke lane. +fn solidify_extent_of(graphic_list: List) -> Extent { + let flattened: List = graphic_list.into_flattened_list(); + Extent::Exactly((0..flattened.len()).map(|index| 1 + usize::from(has_paint_at(&flattened, index, ATTR_FILL))).sum()) +} + +fn solidify_stroke_extent(content: ListIn<'_, Graphic>, level: LevelIn) -> GPoll { + match level.top() { + true => content.get().map(|content| solidify_extent_of(legacy_graphic_list_of(content))), + false => GPoll::Final(Extent::Exactly(1)), + } +} + +/// The solidify over a plain vector level, as [`solidify_stroke`]. +/// Registered under the solidify identifier. +#[node_macro::node(category(""), extent(solidify_stroke_vector_extent))] +fn solidify_stroke_vector<'e>( + ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, + content: IList, +) -> Result< + ( + Vector, + Attr<'e, TransformAttr>, + Attr<'e, Fill>, + Attr<'e, StrokeAttr>, + Attr<'e, BlendModeAttr>, + Attr<'e, Opacity>, + Attr<'e, OpacityFill>, + Attr<'e, ClippingMask>, + Attr<'e, EditorLayerPath>, + Attr<'e, EditorMergedLayers>, + ), + Interrupt, +> { + solidify_lane(ctx.arena(), legacy_graphic_list_of(content), ctx.innermost_index() as usize) +} + +fn solidify_stroke_vector_extent(content: ListIn<'_, Vector>, level: LevelIn) -> GPoll { + match level.top() { + true => content.get().map(|content| solidify_extent_of(legacy_graphic_list_of(content))), + false => GPoll::Final(Extent::Exactly(1)), + } +} + +pub use _solidify_stroke_vector_mod::solidify_stroke_vector_entries; + #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] fn separate_subpaths(_: impl Ctx, content: List) -> List { content @@ -1437,18 +1574,15 @@ fn map_points(ctx: impl Ctx + DeriveCtx, content: List, mapped: impl Nod Ok(content) } -// TODO: Rename to "Combine Paths" and make this happen per-element instead of flattening every element into a single path. The migration for this should then become a Flatten Vector -> Combine Paths pair of nodes. -#[node_macro::node(category("Vector"), path(graphene_core::vector))] -pub fn flatten_path(_: impl Ctx, #[implementations(List, List)] content: T) -> List { - let graphic_list = content.into_graphic_list(); +fn flatten_path_core<'e>( + arena: &'e core_types::arena::Arena, + graphic_list: List, +) -> Result<(Vector, Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, Attr<'e, EditorLayerPath>, Attr<'e, EditorMergedLayers>), Interrupt> { let flattened = graphic_list.clone().into_flattened_list::(); - // Create a `List` with one empty `Vector` element, then get a mutable reference to it which we append flattened subpaths to - let mut output_list = List::new_from_element(Vector::default()); + let mut output = Vector::default(); let mut primary_source = None; - let output = output_list.element_mut(0).unwrap(); - // Concatenate every vector element's subpaths into the single output compound path for index in 0..flattened.len() { let Some(element) = flattened.element(index) else { continue }; @@ -1469,6 +1603,9 @@ pub fn flatten_path(_: impl Ctx, #[implementations(List(_: impl Ctx, #[implementations(List>(ATTR_FILL, 0).map(|paint| park_paint(arena, paint.clone())).transpose()?; + stroke = carrier.attribute::>(ATTR_STROKE, 0).map(|paint| park_paint(arena, paint.clone())).transpose()?; // Adopt the last input item's layer so the editor can also bucket clicks under a contributing child layer - let layer_path: List = flattened.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, primary); - output_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path); + layer_path = flattened + .attribute_cloned_or_default::>(ATTR_EDITOR_LAYER_PATH, primary) + .iter_element_values() + .copied() + .collect(); } + let exhausted = || { + Interrupt::from(GraphError { + kind: core_types::gpoll::ErrorKind::ArenaExhausted, + trace: Vec::new(), + }) + }; + let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; + // Snapshot the input layers so the renderer can recurse into them for + // editor click-target preservation, as the boolean operation does. + let merged_layers = arena.alloc(graphic_list).ok_or_else(exhausted)?.0; - // Preserve a reference to the original upstream `List` so the renderer can recurse into it - // when collecting metadata, exposing the original child layers' click targets to editor tools. - // This is the same mechanism Boolean Operation uses to keep its inputs editable after the merge. - output_list.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list); - - output_list + Ok((output, Attr(DAffine2::IDENTITY), Attr(fill), Attr(stroke), Attr(layer_path.as_slice()), Attr(Some(merged_layers)))) } +// TODO: Rename to "Combine Paths" and make this happen per-element instead of flattening every element into a single path. The migration for this should then become a Flatten Vector -> Combine Paths pair of nodes. +#[node_macro::node(category("Vector"), path(graphene_core::vector))] +pub fn flatten_path<'e>( + ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, + content: IList, +) -> Result<(Vector, Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, Attr<'e, EditorLayerPath>, Attr<'e, EditorMergedLayers>), Interrupt> { + // SAFETY: a materialized input's frames are arena-resident. + let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) }; + let content = graphic_types::graphic::run_to_render_list::(&item).expect("the run holds the row's element type"); + flatten_path_core(ctx.arena(), content) +} + +/// The path flattening over a plain vector level, as [`flatten_path`]. +/// Registered under the flatten path identifier. +#[node_macro::node(category(""))] +pub fn flatten_path_vector<'e>( + ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, + content: IList, +) -> Result<(Vector, Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, Attr<'e, EditorLayerPath>, Attr<'e, EditorMergedLayers>), Interrupt> { + // SAFETY: a materialized input's frames are arena-resident. + let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) }; + let content = graphic_types::graphic::run_to_render_list::(&item) + .expect("the run holds the row's element type") + .into_graphic_list(); + flatten_path_core(ctx.arena(), content) +} + +pub use _flatten_path_vector_mod::flatten_path_vector_entries; + /// Convert vector geometry into a polyline composed of evenly spaced points. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("sample_polyline_properties"), memoize)] fn sample_polyline( @@ -3328,8 +3503,8 @@ mod test { let expected_points = Vector::from_bezpath(points.clone()).point_domain.positions().to_vec(); let copy_to_points = super::copy_to_points(&Footprint::default(), vector_node_from_bezpath(points), vector_node_from_bezpath(element), 1., 1., 0., 0, 0., 0); - let flatten_path = super::flatten_path(&Footprint::default(), copy_to_points); - let flattened_copy_to_points = flatten_path.element(0).unwrap(); + let arena = core_types::arena::Arena::new(1 << 16).unwrap(); + let (flattened_copy_to_points, ..) = super::flatten_path_core(&arena, copy_to_points.into_graphic_list()).unwrap(); assert_eq!(flattened_copy_to_points.region_manipulator_groups().count(), expected_points.len());