mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 06:38:03 +08:00
Flatten boolean content natively
This commit is contained in:
@@ -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<Graphic>,
|
||||
flattened: List<Vector>,
|
||||
snapshot: List<Graphic>,
|
||||
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::<Graphic>(&item)
|
||||
let run = core_types::record::RunView::<Graphic>::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::<Graphic>(&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::<Vector>(&item)
|
||||
let flattened = graphic_types::graphic::run_to_list::<Vector>(&item).expect("the run holds vector lanes");
|
||||
let snapshot = graphic_types::graphic::run_to_render_list::<Vector>(&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<Vector>, 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<T>(image: &List<T>, parent_transform: DAffine2) -> Vec<Item<Vector>> {
|
||||
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<NodeId> = 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<Color>) -> Vec<Item<Vector>> {
|
||||
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<GradientStops>) -> Vec<Item<Vector>> {
|
||||
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::<DAffine2>(ATTR_TRANSFORM) {
|
||||
gradient_paint.set_attribute(ATTR_TRANSFORM, 0, transform);
|
||||
}
|
||||
if let Some(gradient_type) = attributes.remove::<GradientType>(ATTR_GRADIENT_TYPE) {
|
||||
gradient_paint.set_attribute(ATTR_GRADIENT_TYPE, 0, gradient_type);
|
||||
}
|
||||
if let Some(spread_method) = attributes.remove::<GradientSpreadMethod>(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<String>, parent_transform: DAffine2) -> Vec<Item<Vector>> {
|
||||
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<Vector>, rows: Vec<Item<Vector>>) {
|
||||
for row in rows {
|
||||
out.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
fn push_vector_rows(out: &mut List<Vector>, inner: &List<Vector>, 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<Vector>, flattened: List<Vector>) {
|
||||
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<Element = Graphic>>(source: &'a S, transform: DAffine2, inherited: PaintReach<'a>) -> List<Vector> {
|
||||
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<Element = Graphic>>(out: &mut List<Vector>, 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::<TransformAttr>(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<Vector>, group: &core_types::record::Group, composed: DAffine2, reach: PaintReach<'_>) {
|
||||
let item = &group.content;
|
||||
if let Some(rows) = graphic_types::graphic::run_to_list::<Vector>(item) {
|
||||
push_vector_rows(out, &rows, composed, reach);
|
||||
} else if let Some(run) = core_types::record::RunView::<Graphic>::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::<Raster<CPU>>(item) {
|
||||
push_rows(out, raster_stand_in_rows(&image, composed));
|
||||
} else if let Some(image) = graphic_types::graphic::run_to_list::<Raster<GPU>>(item) {
|
||||
push_rows(out, raster_stand_in_rows(&image, composed));
|
||||
} else if let Some(color) = graphic_types::graphic::run_to_list::<Color>(item) {
|
||||
push_rows(out, color_paint_rows(&color));
|
||||
} else if let Some(gradient) = graphic_types::graphic::run_to_list::<GradientStops>(item) {
|
||||
push_rows(out, gradient_paint_rows(&gradient));
|
||||
} else if let Some(text) = graphic_types::graphic::run_to_list::<String>(item) {
|
||||
push_rows(out, text_rows(&text, composed));
|
||||
}
|
||||
}
|
||||
|
||||
/// The legacy baseline the flatten law compares against.
|
||||
#[cfg(test)]
|
||||
fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
||||
(0..graphic_list.len())
|
||||
.flat_map(|index| {
|
||||
@@ -512,3 +688,68 @@ pub fn boolean_intersect(a: &BezPath, b: &BezPath) -> Vec<BezPath> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core_types::record::Group;
|
||||
|
||||
fn square(corner: DVec2) -> Vector {
|
||||
Vector::from_subpath(Subpath::<PointId>::new_rectangle(corner, corner + DVec2::ONE))
|
||||
}
|
||||
|
||||
fn black_paint() -> List<Graphic> {
|
||||
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::<Vector>(), &[]);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user