diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index fefeaf4ebb..52f37ffa18 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -3,14 +3,16 @@ 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::{bake_paint_transforms, set_paint_attribute}; +use graphic_types::graphic::{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}; use graphic_types::vector_types::gradient::{GradientSpreadMethod, GradientType}; 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 graphic_types::{ATTR_FILL, ATTR_STROKE, Graphic, IntoGraphicList, Vector}; use linesweeper::topology::Topology; use linesweeper::{BinaryOp, FillRule, binary_op}; use smallvec::SmallVec; @@ -24,7 +26,8 @@ pub use vector_types::vector::misc::BooleanOperation; #[allow(clippy::type_complexity)] fn boolean_core<'e>( arena: &'e core_types::arena::Arena, - content: List, + flattened: List, + snapshot: List, operation: BooleanOperation, ) -> Result< ( @@ -42,7 +45,6 @@ fn boolean_core<'e>( core_types::gpoll::Interrupt, > { // The first index is the bottom of the stack - let flattened = flatten_vector(&content); let mut result_vector_list = boolean_operation_on_vector_list(&flattened, operation); // Replace the transformation matrix with a mutation of the vector points themselves @@ -79,7 +81,7 @@ fn boolean_core<'e>( 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. - let merged_layers = arena.alloc(content).ok_or_else(exhausted)?.0; + let merged_layers = arena.alloc(snapshot).ok_or_else(exhausted)?.0; Ok(( element, @@ -125,10 +127,12 @@ 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 content = graphic_types::graphic::run_to_render_list::(&item) + 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 snapshot = graphic_types::graphic::run_to_render_list::(&item) .expect("the run holds the row's element type") .into_graphic_list(); - boolean_core(ctx.arena(), content, operation) + boolean_core(ctx.arena(), flattened, snapshot, operation) } /// The boolean operation over a plain vector level, as [`boolean_operation`]. @@ -154,10 +158,11 @@ fn boolean_operation_vector<'e>( > { // 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) + let flattened = graphic_types::graphic::run_to_list::(&item).expect("the run holds vector lanes"); + let snapshot = graphic_types::graphic::run_to_render_list::(&item) .expect("the run holds the row's element type") .into_graphic_list(); - boolean_core(ctx.arena(), content, operation) + boolean_core(ctx.arena(), flattened, snapshot, operation) } pub use _boolean_operation_vector_mod::boolean_operation_vector_entries; @@ -277,6 +282,177 @@ 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()) + .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() +} + +/// 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)); + + let mut element = Vector::default(); + element.set_stroke_transform(DAffine2::IDENTITY); + + Item::from_parts(element, attributes) + }) + .collect() +} + +/// 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(); + + 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() +} + +/// A text row: 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() + .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() +} + +fn push_rows(out: &mut List, rows: Vec>) { + for row in rows { + out.push(row); + } +} + +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 }; + let index = out.len(); + out.push(item); + 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, index, key, paint.clone()); + } + } + } + let current: DAffine2 = out.attribute_cloned_or_default(ATTR_TRANSFORM, index); + out.set_attribute(ATTR_TRANSFORM, index, composed * current); + } +} + +fn push_union(out: &mut List, flattened: List) { + for row in boolean_operation_on_vector_list(&flattened, BooleanOperation::Union).into_iter() { + out.push(row); + } +} + +/// 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 { + let mut out = List::new(); + flatten_vector_run_into(&mut out, source, 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 }; + let reach = inherited.for_lane(&columns, index); + let composed = transform * source.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::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)), + } + } +} + +/// A group flattens as its legacy lowering did: a vector run serves its rows, +/// a graphic run unions like a nested list, and another typed run serves its +/// stand-in rows. +fn flatten_group(out: &mut List, group: &core_types::record::Group, composed: DAffine2, reach: PaintReach<'_>) { + 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 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)); + } else if let Some(gradient) = graphic_types::graphic::run_to_list::(item) { + push_rows(out, gradient_paint_rows(&gradient)); + } 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| { @@ -512,3 +688,68 @@ pub fn boolean_intersect(a: &BezPath, b: &BezPath) -> Vec { } } } + +#[cfg(test)] +mod tests { + use super::*; + use core_types::record::Group; + + fn square(corner: DVec2) -> Vector { + Vector::from_subpath(Subpath::::new_rectangle(corner, corner + DVec2::ONE)) + } + + fn black_paint() -> List { + List::new_from_element(Graphic::Color(List::new_from_element(Color::BLACK))) + } + + #[test] + fn the_native_flatten_matches_the_legacy_flatten() { + 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()]; + // SAFETY: `inner_bytes` is one lane of `inner_layout`; a parked element + // stores its reference. + unsafe { inner_bytes.as_mut_ptr().cast::<&Vector>().write(&inner_vector) }; + // 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::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.))); + + 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); + } +}