mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Split the graphic module into type, paint, walk, glue, and legacy modules
This commit is contained in:
File diff suppressed because it is too large
Load Diff
467
node-graph/libraries/graphic-types/src/graphic/glue.rs
Normal file
467
node-graph/libraries/graphic-types/src/graphic/glue.rs
Normal file
@@ -0,0 +1,467 @@
|
||||
use super::Graphic;
|
||||
use core_types::Color;
|
||||
use core_types::list::{Item, List};
|
||||
use raster_types::{CPU, Raster};
|
||||
use vector_types::Vector;
|
||||
|
||||
/// The graphic with every `Group` deep-copied to its owned form, which
|
||||
/// survives the arena generation but cannot be read until
|
||||
/// [`map_groups_to_resident`] re-parks it into a serving arena.
|
||||
pub fn map_groups_to_owned<'out>(graphic: &Graphic<'_>) -> Graphic<'out> {
|
||||
match graphic {
|
||||
Graphic::Group(group) => Graphic::Group(group.copy_out()),
|
||||
Graphic::Graphic(children) => {
|
||||
let mut out = List::new();
|
||||
for item in children.clone().into_iter() {
|
||||
let (element, attributes) = item.into_parts();
|
||||
out.push(Item::from_parts(map_groups_to_owned(&element), attributes));
|
||||
}
|
||||
Graphic::Graphic(out)
|
||||
}
|
||||
Graphic::Vector(vector) => Graphic::Vector(vector.clone()),
|
||||
Graphic::RasterCPU(raster) => Graphic::RasterCPU(raster.clone()),
|
||||
Graphic::RasterGPU(raster) => Graphic::RasterGPU(raster.clone()),
|
||||
Graphic::Color(color) => Graphic::Color(*color),
|
||||
Graphic::Gradient(gradient) => Graphic::Gradient(gradient.clone()),
|
||||
Graphic::Text(text) => Graphic::Text(text.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The graphic with every owned `Group` re-parked into `arena`; `None`
|
||||
/// reports arena exhaustion.
|
||||
pub fn map_groups_to_resident<'a>(graphic: &Graphic<'a>, arena: &'a core_types::arena::Arena) -> Option<Graphic<'a>> {
|
||||
match graphic {
|
||||
Graphic::Group(group) => group.replay(arena).map(Graphic::Group),
|
||||
Graphic::Graphic(children) => {
|
||||
let mut children = children.clone();
|
||||
for child in children.iter_element_values_mut() {
|
||||
*child = map_groups_to_resident(child, arena)?;
|
||||
}
|
||||
Some(Graphic::Graphic(children))
|
||||
}
|
||||
other => Some(other.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The deep copy-out for `Graphic` elements: a plain clone of a group
|
||||
/// interior would carry frame pointers into the evaluation's arena, so memo
|
||||
/// and capture seams copy out the owned-group form.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must point at a live parked `Graphic` element field.
|
||||
unsafe fn deep_clone_graphic(ptr: *const u8) -> Box<dyn std::any::Any + Send + Sync> {
|
||||
let graphic = unsafe { core_types::record::borrow_element::<Graphic>(core_types::record::Rec::new(ptr)) };
|
||||
Box::new(map_groups_to_owned(graphic))
|
||||
}
|
||||
|
||||
/// The deep re-park for `Graphic` elements: owned groups replay into the
|
||||
/// serving arena before the graphic parks.
|
||||
///
|
||||
/// # Safety
|
||||
/// `value` must hold a `Graphic` and `dst` must be a live `Graphic` element
|
||||
/// field.
|
||||
unsafe fn deep_repark_graphic(value: &(dyn std::any::Any + Send + Sync), dst: *mut u8, arena: &core_types::arena::Arena) -> Option<()> {
|
||||
let graphic = value.downcast_ref::<Graphic>().expect("an element replays at its own type");
|
||||
let resident = map_groups_to_resident(graphic, arena)?;
|
||||
let retained = graphic_retained_heap(&resident);
|
||||
unsafe { core_types::record::write_element_sized(dst, resident, arena, retained) }
|
||||
}
|
||||
|
||||
/// The graphic with every `Group` promoted into the persistent region: an
|
||||
/// interior already living there is shared rather than copied, so the cost is
|
||||
/// what this level newly produced. `None` reports arena exhaustion.
|
||||
pub fn map_groups_to_persistent<'p>(graphic: &Graphic<'_>, promotion: &core_types::record::Promotion<'p>) -> Option<Graphic<'p>> {
|
||||
match graphic {
|
||||
Graphic::Group(group) => group.to_persistent(promotion).map(Graphic::Group),
|
||||
Graphic::Graphic(children) => {
|
||||
let mut out = List::new();
|
||||
for item in children.clone().into_iter() {
|
||||
let (element, attributes) = item.into_parts();
|
||||
out.push(Item::from_parts(map_groups_to_persistent(&element, promotion)?, attributes));
|
||||
}
|
||||
Some(Graphic::Graphic(out))
|
||||
}
|
||||
Graphic::Vector(vector) => Some(Graphic::Vector(vector.clone())),
|
||||
Graphic::RasterCPU(raster) => Some(Graphic::RasterCPU(raster.clone())),
|
||||
Graphic::RasterGPU(raster) => Some(Graphic::RasterGPU(raster.clone())),
|
||||
Graphic::Color(color) => Some(Graphic::Color(*color)),
|
||||
Graphic::Gradient(gradient) => Some(Graphic::Gradient(gradient.clone())),
|
||||
Graphic::Text(text) => Some(Graphic::Text(text.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
/// The promote for `Graphic` elements: the generic path would deep-copy every
|
||||
/// interior through an owned intermediate, while this shares the interiors the
|
||||
/// persistent region already holds. A group-free graphic references nothing the
|
||||
/// evaluation owns, so its header moves and its heap is never copied.
|
||||
///
|
||||
/// # Safety
|
||||
/// `src` must point at a live parked `Graphic` element field, and `dst` at the
|
||||
/// element field the promoted reference is written to.
|
||||
unsafe fn promote_graphic(src: *const u8, dst: *mut u8, promotion: &core_types::record::Promotion<'_>) -> Option<()> {
|
||||
let graphic = unsafe { core_types::record::borrow_element::<Graphic>(core_types::record::Rec::new(src)) };
|
||||
if !graphic_contains_groups(graphic) {
|
||||
// SAFETY: a parked element slot holds one reference at offset 0, and a
|
||||
// group-free graphic owns all of its content.
|
||||
let header = unsafe { src.cast::<*const u8>().read() };
|
||||
if let Some(moved) = unsafe { promotion.move_park::<Graphic<'static>>(header, graphic_retained_heap(graphic)) } {
|
||||
// SAFETY: as above, into the promoted image's own element slot.
|
||||
unsafe { dst.cast::<*const Graphic<'static>>().write(moved) };
|
||||
return Some(());
|
||||
}
|
||||
}
|
||||
let promoted = map_groups_to_persistent(graphic, promotion)?;
|
||||
let retained = graphic_retained_heap(&promoted);
|
||||
unsafe { core_types::record::write_element_sized(dst, promoted, promotion.persistent(), retained) }
|
||||
}
|
||||
|
||||
/// The heap a graphic's own payload owns. Group interiors are excluded: their
|
||||
/// lanes park through this same glue and are counted as they land.
|
||||
fn graphic_retained_heap(graphic: &Graphic<'_>) -> usize {
|
||||
match graphic {
|
||||
Graphic::Vector(vector) => vector_retained_heap(vector),
|
||||
Graphic::RasterCPU(raster) => raster.data.len() * size_of::<Color>(),
|
||||
Graphic::Text(text) => text.len(),
|
||||
Graphic::Gradient(gradient) => gradient.len() * size_of::<(f64, Color)>(),
|
||||
Graphic::Graphic(children) => (0..children.len()).filter_map(|index| children.element(index)).map(graphic_retained_heap).sum(),
|
||||
Graphic::Group(_) | Graphic::RasterGPU(_) | Graphic::Color(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The heap a vector's domain columns own, summed over the columns it
|
||||
/// exposes, so the segment domain's private parallel columns are undercounted.
|
||||
fn vector_retained_heap(vector: &Vector) -> usize {
|
||||
size_of_val(vector.point_domain.ids())
|
||||
+ size_of_val(vector.point_domain.positions())
|
||||
+ size_of_val(vector.segment_domain.ids())
|
||||
+ size_of_val(vector.region_domain.ids())
|
||||
+ size_of_val(vector.colinear_manipulators.as_slice())
|
||||
}
|
||||
|
||||
fn graphic_contains_groups(graphic: &Graphic) -> bool {
|
||||
match graphic {
|
||||
Graphic::Group(_) => true,
|
||||
Graphic::Graphic(children) => list_contains_groups(children),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn list_contains_groups(list: &List<Graphic>) -> bool {
|
||||
(0..list.len()).any(|index| list.element(index).is_some_and(graphic_contains_groups))
|
||||
}
|
||||
|
||||
/// The heap a graphic list's elements own, group interiors excluded as
|
||||
/// [`graphic_retained_heap`] excludes them.
|
||||
fn list_retained_heap(list: &List<Graphic>) -> usize {
|
||||
(0..list.len()).filter_map(|index| list.element(index)).map(graphic_retained_heap).sum()
|
||||
}
|
||||
|
||||
/// The deep copy-out for graphic-list field values (the paint markers' owned
|
||||
/// form): content groups leave in their owned form. Declines (`None`) for
|
||||
/// group-free content, which already owns everything.
|
||||
fn deep_clone_graphic_list(value: &dyn core_types::list::AnyAttributeValue) -> Option<Box<dyn core_types::list::AnyAttributeValue>> {
|
||||
let list = value.as_any().downcast_ref::<Option<List<Graphic>>>().expect("a graphic list field deep-copies at its own type");
|
||||
let list = list.as_ref().filter(|list| list_contains_groups(list))?;
|
||||
let mut list = list.clone();
|
||||
for element in list.iter_element_values_mut() {
|
||||
*element = map_groups_to_owned(element);
|
||||
}
|
||||
Some(Box::new(Some(list)))
|
||||
}
|
||||
|
||||
/// The deep replay for graphic-list field values: owned content groups replay
|
||||
/// into the serving arena before the field re-parks. `Some(None)` declines
|
||||
/// for group-free content; `None` reports arena exhaustion.
|
||||
fn deep_repark_graphic_list(value: &dyn core_types::list::AnyAttributeValue, arena: &core_types::arena::Arena) -> Option<Option<Box<dyn core_types::list::AnyAttributeValue>>> {
|
||||
let list = value.as_any().downcast_ref::<Option<List<Graphic>>>().expect("a graphic list field replays at its own type");
|
||||
let Some(list) = list.as_ref().filter(|list| list_contains_groups(list)) else {
|
||||
return Some(None);
|
||||
};
|
||||
let mut list = list.clone();
|
||||
for element in list.iter_element_values_mut() {
|
||||
*element = map_groups_to_resident(element, arena)?;
|
||||
}
|
||||
let list = unsafe { core_types::record::erase_static(list) };
|
||||
Some(Some(Box::new(Some(list))))
|
||||
}
|
||||
|
||||
/// The promote for graphic-list fields, the deep field glue's third half: the
|
||||
/// owned halves copy content groups out to the owned form and replay them back,
|
||||
/// while this maps the content transient-to-persistent in one pass.
|
||||
///
|
||||
/// THE TWO-LEVEL SHARING LAW: the field's own header is not provenance-shared.
|
||||
/// It moves where the content is group-free, since the payload then owns all of
|
||||
/// its content and the transient arena confirms the reference is its own park,
|
||||
/// and otherwise it clones into a fresh persistent park. One level inside, a
|
||||
/// content group's interior is arena-resident and its provenance is decidable,
|
||||
/// so it takes [`map_groups_to_persistent`]'s Cow dispatch: an interior the
|
||||
/// persistent region already holds is shared pointer for pointer.
|
||||
///
|
||||
/// # Safety
|
||||
/// `src` must point at a live parked graphic-list field, and `dst` at the field
|
||||
/// slot the promoted reference is written to.
|
||||
unsafe fn promote_graphic_list(src: *const u8, dst: *mut u8, promotion: &core_types::record::Promotion<'_>) -> Option<()> {
|
||||
// SAFETY: the caller's contract; the slot holds one optional reference.
|
||||
let Some(list) = (unsafe { src.cast::<Option<&List<Graphic<'static>>>>().read() }) else {
|
||||
// SAFETY: as above, into the promoted image's own field slot.
|
||||
unsafe { dst.cast::<Option<&List<Graphic<'static>>>>().write(None) };
|
||||
return Some(());
|
||||
};
|
||||
let retained = list_retained_heap(list);
|
||||
if !list_contains_groups(list) {
|
||||
// SAFETY: a group-free list owns all of its content, and the arena
|
||||
// declines a reference that is not a park at the list's own address
|
||||
// and size.
|
||||
if let Some(moved) = unsafe { promotion.move_park::<List<Graphic<'static>>>(std::ptr::from_ref(list).cast(), retained) } {
|
||||
// SAFETY: the move published a live list in the persistent region.
|
||||
unsafe { dst.cast::<Option<&List<Graphic<'static>>>>().write(Some(&*moved)) };
|
||||
return Some(());
|
||||
}
|
||||
}
|
||||
let mut promoted = list.clone();
|
||||
for element in promoted.iter_element_values_mut() {
|
||||
*element = map_groups_to_persistent(element, promotion)?;
|
||||
}
|
||||
// SAFETY: every borrow the clone carried was replaced by persistent content
|
||||
// above, so the erased form outlives the evaluation.
|
||||
let promoted = unsafe { core_types::record::erase_static(promoted) };
|
||||
let (parked, _) = promotion.persistent().alloc_sized(promoted, retained)?;
|
||||
// SAFETY: the slot holds one optional reference.
|
||||
unsafe { dst.cast::<Option<&List<Graphic<'static>>>>().write(Some(parked)) };
|
||||
Some(())
|
||||
}
|
||||
|
||||
const _: () = {
|
||||
fn register_all() {
|
||||
core_types::record::register_deep_element_clone::<Graphic>(deep_clone_graphic, deep_repark_graphic);
|
||||
core_types::record::register_deep_field_value::<Option<List<Graphic>>>(deep_clone_graphic_list, deep_repark_graphic_list);
|
||||
core_types::record::register_field_promote::<Option<&'static List<Graphic<'static>>>>(promote_graphic_list);
|
||||
core_types::record::register_element_promote::<Graphic>(promote_graphic);
|
||||
core_types::record::register_retained_heap::<Graphic>(|value| value.downcast_ref::<Graphic>().map_or(0, graphic_retained_heap));
|
||||
core_types::record::register_retained_heap::<Vector>(|value| value.downcast_ref::<Vector>().map_or(0, vector_retained_heap));
|
||||
core_types::record::register_retained_heap::<Raster<CPU>>(|value| value.downcast_ref::<Raster<CPU>>().map_or(0, |raster| raster.data.len() * size_of::<Color>()));
|
||||
core_types::record::register_retained_heap::<String>(|value| value.downcast_ref::<String>().map_or(0, String::len));
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[core_types::ctor::ctor]
|
||||
fn register() {
|
||||
register_all();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[unsafe(export_name = "__node_registry_deep_element_graphic")]
|
||||
extern "C" fn register() {
|
||||
register_all();
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod run_tests {
|
||||
use super::*;
|
||||
use crate::graphic::test_support::{native_group_paint, unit_square_at};
|
||||
use crate::graphic::{group_to_legacy_list, map_groups_to_legacy};
|
||||
use crate::markers::Fill;
|
||||
use core_types::attribute::Attribute;
|
||||
use core_types::lane::LaneSource;
|
||||
use core_types::record::{FieldWrite, RunBuilder, RunView, element_write_hashed};
|
||||
use glam::DVec2;
|
||||
|
||||
#[test]
|
||||
fn an_owned_group_replays_content_equal_after_the_source_dies() {
|
||||
let paint = List::new_from_element(Graphic::Color(Color::BLACK));
|
||||
let vector = unit_square_at(DVec2::ZERO);
|
||||
|
||||
let source = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
|
||||
let lane = builder.push(vector.clone()).unwrap();
|
||||
builder.attr::<Fill>(lane, Some(&paint));
|
||||
let group = core_types::record::Group { row: None, content: builder.finish() };
|
||||
let expected = group_to_legacy_list(&group);
|
||||
let owned = map_groups_to_owned(&Graphic::Group(group));
|
||||
drop(source);
|
||||
|
||||
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
let resident = map_groups_to_resident(&owned, &arena).expect("the arena holds the replay");
|
||||
let Graphic::Group(group) = &resident else { panic!("the replay keeps the group form") };
|
||||
assert_eq!(group_to_legacy_list(group), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_owned_group_replays_nested_groups_through_the_element_glue() {
|
||||
let vector = unit_square_at(DVec2::ZERO);
|
||||
let source = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[], 1).unwrap();
|
||||
builder.push(vector.clone()).unwrap();
|
||||
let nested = Graphic::Group(core_types::record::Group { row: None, content: builder.finish() });
|
||||
|
||||
let mut builder = RunBuilder::new(&source, element_write_hashed::<Graphic>(), &[], 1).unwrap();
|
||||
builder.push(nested).unwrap();
|
||||
let group = core_types::record::Group { row: None, content: builder.finish() };
|
||||
let expected = group_to_legacy_list(&group);
|
||||
let owned = map_groups_to_owned(&Graphic::Group(group));
|
||||
drop(source);
|
||||
|
||||
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
let resident = map_groups_to_resident(&owned, &arena).expect("the arena holds the replay");
|
||||
let Graphic::Group(group) = &resident else { panic!("the replay keeps the group form") };
|
||||
assert_eq!(group_to_legacy_list(group), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_owned_run_deep_copies_graphic_list_fields() {
|
||||
let inner_vector = unit_square_at(DVec2::ZERO);
|
||||
let source = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
// SAFETY: the erased native list serves only while `source` is live; the
|
||||
// deep glue under test replaces its borrows at the copy-out seam.
|
||||
let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) };
|
||||
|
||||
let vector = unit_square_at(DVec2::new(4., 4.));
|
||||
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
|
||||
let lane = builder.push(vector.clone()).unwrap();
|
||||
builder.attr::<Fill>(lane, Some(&paint));
|
||||
let item = builder.finish();
|
||||
let owned = item.copy_out();
|
||||
let expected = map_groups_to_legacy(paint.element(0).unwrap());
|
||||
drop(item);
|
||||
drop(paint);
|
||||
drop(source);
|
||||
|
||||
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
let replayed = owned.replay(&arena).expect("the arena holds the replay");
|
||||
let run = RunView::<Vector>::new(&replayed).expect("the run holds vector elements");
|
||||
let served = run.attr::<Fill>(0).expect("the fill replays present");
|
||||
assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_owned_record_deep_copies_graphic_list_fields() {
|
||||
let inner_vector = unit_square_at(DVec2::ZERO);
|
||||
let source = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
// SAFETY: the erased native list serves only while `source` is live; the
|
||||
// deep glue under test replaces its borrows at the copy-out seam.
|
||||
let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) };
|
||||
|
||||
let vector = unit_square_at(DVec2::new(4., 4.));
|
||||
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
|
||||
let lane = builder.push(vector.clone()).unwrap();
|
||||
builder.attr::<Fill>(lane, Some(&paint));
|
||||
let item = builder.finish();
|
||||
let layout = item.layout().clone();
|
||||
let offset = layout.offset_of(Fill::NAME, 0).unwrap();
|
||||
// SAFETY: the item's lane is a live record of `layout`.
|
||||
let owned = unsafe { core_types::record::OwnedRecord::copy_out(&layout, item.lanes().get(0).rec()) };
|
||||
let expected = map_groups_to_legacy(paint.element(0).unwrap());
|
||||
drop(item);
|
||||
drop(paint);
|
||||
drop(source);
|
||||
|
||||
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
let frames = core_types::record::test_frames(layout.frame_bytes());
|
||||
let mut slot = frames.claim(&layout);
|
||||
owned.replay_into(&mut slot, &arena).expect("the arena holds the replay");
|
||||
// SAFETY: the replay completes the record in the claimed frame.
|
||||
let value = unsafe { slot.finish() };
|
||||
// SAFETY: the replay wrote a record of `layout`.
|
||||
let served = unsafe { layout.rec(&value).read::<Option<&List<Graphic>>>(offset) }.expect("the fill replays present");
|
||||
assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected);
|
||||
}
|
||||
|
||||
/// A `lanes`-long frame promoted out of `transient` into `persistent`, with
|
||||
/// `fill` written into the paint field of every lane. The frame buffer comes
|
||||
/// back so it outlives the promote's reads.
|
||||
fn promote_paint_field(
|
||||
fill: Option<&List<Graphic<'static>>>,
|
||||
lanes: usize,
|
||||
transient: &core_types::arena::Arena,
|
||||
persistent: &core_types::arena::Arena,
|
||||
) -> (core_types::record::Layout, core_types::record::MaterializedSpan, Vec<u64>) {
|
||||
use core_types::record::{Layout, MaterializedSpan, Promotion, element_write, write_field};
|
||||
|
||||
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[FieldWrite::of::<Fill>(0)]);
|
||||
let offset = layout.offset_of(Fill::NAME, 0).unwrap();
|
||||
let stride = layout.lane_stride();
|
||||
let mut buffer = vec![0u64; (lanes * stride).div_ceil(8)];
|
||||
let base = buffer.as_mut_ptr().cast::<u8>();
|
||||
let bounds = (base as usize, buffer.len() * 8);
|
||||
for lane in 0..lanes {
|
||||
// SAFETY: the frame is this layout's, written at the element slot and
|
||||
// at the paint field's own offset.
|
||||
unsafe {
|
||||
base.add(lane * stride).cast::<f64>().write(lane as f64);
|
||||
write_field::<Option<&List<Graphic<'static>>>>(base.add(lane * stride), offset, fill);
|
||||
}
|
||||
}
|
||||
// SAFETY: the frames hold `lanes` live records of `layout`.
|
||||
let batch = unsafe { core_types::node::RecordBatch::new(base.cast_const(), lanes, &layout) };
|
||||
let promotion = Promotion::new(transient, bounds, persistent);
|
||||
// SAFETY: as above.
|
||||
let span = unsafe { MaterializedSpan::to_persistent(&batch, &promotion) }.expect("the region holds the promote");
|
||||
(layout, span, buffer)
|
||||
}
|
||||
|
||||
/// The promoted paint of one lane, at the layout the promote published.
|
||||
fn promoted_paint<'p>(
|
||||
span: &core_types::record::MaterializedSpan,
|
||||
layout: &core_types::record::Layout,
|
||||
lane: usize,
|
||||
persistent: &'p core_types::arena::Arena,
|
||||
) -> &'p List<Graphic<'p>> {
|
||||
let offset = layout.offset_of(Fill::NAME, 0).unwrap();
|
||||
let batch = span.batch(persistent, layout).expect("the span resolves in its own region");
|
||||
// SAFETY: the promote wrote a record of `layout` into every lane.
|
||||
unsafe { batch.get(lane).rec().read::<Option<&List<Graphic>>>(offset) }.expect("the paint promotes present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_promoted_paint_field_shares_persistent_interiors() {
|
||||
let inner_vector = unit_square_at(DVec2::ZERO);
|
||||
let transient = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
let persistent = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
|
||||
// The interior an upstream promote already published, named by a paint
|
||||
// list the evaluation parked.
|
||||
let published = native_group_paint(&inner_vector, &persistent);
|
||||
let interior = {
|
||||
let Some(Graphic::Group(group)) = published.element(0) else { panic!("the paint carries a native group") };
|
||||
group.content.lanes().get(0).rec().ptr()
|
||||
};
|
||||
// SAFETY: the list serves only while `persistent` is live, and the
|
||||
// promote under test replaces every borrow it carries.
|
||||
let (paint, _) = transient.alloc_sized_keyed(unsafe { core_types::record::erase_static(published) }, 0).unwrap();
|
||||
|
||||
let occupied = persistent.occupancy();
|
||||
let (layout, span, _frames) = promote_paint_field(Some(paint), 1, &transient, &persistent);
|
||||
let served = promoted_paint(&span, &layout, 0, &persistent);
|
||||
|
||||
let Some(Graphic::Group(group)) = served.element(0) else { panic!("the promote keeps the group form") };
|
||||
assert_eq!(group.content.lanes().get(0).rec().ptr(), interior, "a persistent interior is shared pointer for pointer");
|
||||
assert!(
|
||||
persistent.occupancy() - occupied <= layout.frame_bytes() + size_of::<List<Graphic>>() + align_of::<List<Graphic>>(),
|
||||
"the promote allocated the lane slab and the field's own header, never the owned form of the shared interior"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_group_free_paint_field_moves_its_parked_header() {
|
||||
let mut transient = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
let persistent = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
|
||||
let paint = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::ZERO)));
|
||||
let heap = {
|
||||
let Some(Graphic::Vector(vector)) = paint.element(0) else { panic!("the paint carries a vector") };
|
||||
vector.point_domain.positions().as_ptr()
|
||||
};
|
||||
let (paint, _) = transient.alloc_sized_keyed(paint, 0).unwrap();
|
||||
|
||||
let (layout, span, _frames) = promote_paint_field(Some(paint), 2, &transient, &persistent);
|
||||
let served = promoted_paint(&span, &layout, 0, &persistent);
|
||||
let Some(Graphic::Vector(vector)) = served.element(0) else { panic!("the promote keeps the vector") };
|
||||
assert_eq!(vector.point_domain.positions().as_ptr(), heap, "the promote moved the header, so the served paint names the pre-promote heap");
|
||||
assert!(std::ptr::eq(served, promoted_paint(&span, &layout, 1, &persistent)), "a paint two lanes share moves once");
|
||||
|
||||
transient.reset();
|
||||
let served = promoted_paint(&span, &layout, 0, &persistent);
|
||||
assert!(matches!(served.element(0), Some(Graphic::Vector(_))), "the moved paint survives the transient reset");
|
||||
}
|
||||
}
|
||||
156
node-graph/libraries/graphic-types/src/graphic/legacy.rs
Normal file
156
node-graph/libraries/graphic-types/src/graphic/legacy.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
use super::walk::push_lane_paint_into_interiors;
|
||||
use super::{Graphic, detable_items};
|
||||
use crate::markers::{ATTR_FILL, ATTR_STROKE};
|
||||
use core_types::Color;
|
||||
use core_types::list::{AttributeValueDyn, Item, List};
|
||||
use raster_types::{CPU, GPU, Raster};
|
||||
use vector_types::{GradientStops, Vector};
|
||||
|
||||
/// One typed run as an owned list, elements cloned and every attribute copied
|
||||
/// through its erased read. Content keeps its native form; the legacy
|
||||
/// conversions layer their mapping on top.
|
||||
pub fn run_to_list<T: Clone + Send + Sync + dyn_any::StaticTypeSized>(item: &core_types::record::GroupItem) -> Option<List<T>> {
|
||||
let lanes = item.typed_lanes::<T>()?;
|
||||
let mut list = List::new();
|
||||
for lane in 0..lanes.len() {
|
||||
list.push(Item::new_from_element(lanes.element_ref(lane).clone()));
|
||||
}
|
||||
for field in &item.layout().fields {
|
||||
for lane in 0..lanes.len() {
|
||||
// SAFETY: the offset comes from the item's own layout.
|
||||
let value = unsafe { (field.read_erased)(item.lanes().get(lane).rec().ptr().add(field.offset)) };
|
||||
list.set_attribute_value_dyn(field.name, lane, AttributeValueDyn(value));
|
||||
}
|
||||
}
|
||||
Some(list)
|
||||
}
|
||||
|
||||
/// Converts the group content of the list's paint attribute values to legacy
|
||||
/// form, so a legacy product owns everything its attributes reach.
|
||||
pub fn map_paint_attrs_to_legacy<T>(list: &mut List<T>) {
|
||||
for key in [ATTR_FILL, ATTR_STROKE, crate::markers::ATTR_EDITOR_MERGED_LAYERS] {
|
||||
let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(key) else { continue };
|
||||
for value in values.flatten() {
|
||||
for element in value.iter_element_values_mut() {
|
||||
*element = map_groups_to_legacy(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One typed run as a legacy list: [`run_to_list`] with the paint attribute
|
||||
/// contents converted to their legacy form.
|
||||
pub(crate) fn run_to_legacy_list<T: Clone + Send + Sync + dyn_any::StaticTypeSized>(item: &core_types::record::GroupItem) -> Option<List<T>> {
|
||||
let mut list = run_to_list::<T>(item)?;
|
||||
map_paint_attrs_to_legacy(&mut list);
|
||||
Some(list)
|
||||
}
|
||||
|
||||
pub fn map_groups_to_legacy<'out>(graphic: &Graphic<'_>) -> Graphic<'out> {
|
||||
match graphic {
|
||||
Graphic::Group(group) => group_to_legacy_graphic(group),
|
||||
Graphic::Graphic(children) => {
|
||||
let mut out = List::new();
|
||||
for item in children.clone().into_iter() {
|
||||
let (element, attributes) = item.into_parts();
|
||||
out.push(Item::from_parts(map_groups_to_legacy(&element), attributes));
|
||||
}
|
||||
map_paint_attrs_to_legacy(&mut out);
|
||||
Graphic::Graphic(out)
|
||||
}
|
||||
Graphic::Vector(vector) => Graphic::Vector(vector.clone()),
|
||||
Graphic::RasterCPU(raster) => Graphic::RasterCPU(raster.clone()),
|
||||
Graphic::RasterGPU(raster) => Graphic::RasterGPU(raster.clone()),
|
||||
Graphic::Color(color) => Graphic::Color(*color),
|
||||
Graphic::Gradient(gradient) => Graphic::Gradient(gradient.clone()),
|
||||
Graphic::Text(text) => Graphic::Text(text.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Graphic>` the
|
||||
/// pre-flip wrap applied; everything else becomes the legacy group list.
|
||||
pub fn group_to_legacy_graphic(group: &core_types::record::Group) -> Graphic<'static> {
|
||||
if group.row.is_none() {
|
||||
let item = &group.content;
|
||||
let typed = None
|
||||
.or_else(|| run_to_legacy_list::<Vector>(item).map(|list| detable_items(list, Graphic::Vector)))
|
||||
.or_else(|| run_to_legacy_list::<Raster<CPU>>(item).map(|list| detable_items(list, Graphic::RasterCPU)))
|
||||
.or_else(|| run_to_legacy_list::<Raster<GPU>>(item).map(|list| detable_items(list, Graphic::RasterGPU)))
|
||||
.or_else(|| run_to_legacy_list::<Color>(item).map(|list| detable_items(list, Graphic::Color)))
|
||||
.or_else(|| run_to_legacy_list::<GradientStops>(item).map(|list| detable_items(list, Graphic::Gradient)))
|
||||
.or_else(|| run_to_legacy_list::<String>(item).map(|list| detable_items(list, Graphic::Text)));
|
||||
if let Some(typed) = typed {
|
||||
return Graphic::Graphic(typed);
|
||||
}
|
||||
}
|
||||
Graphic::Graphic(group_to_legacy_list(group))
|
||||
}
|
||||
|
||||
/// The group as a legacy `List<Graphic>`: a `Graphic` run becomes the items,
|
||||
/// another typed run becomes one item holding its typed list.
|
||||
pub fn group_to_legacy_list(group: &core_types::record::Group) -> List<Graphic<'static>> {
|
||||
let item = &group.content;
|
||||
if let Some(mut list) = run_to_legacy_list::<Graphic>(item) {
|
||||
for element in list.iter_element_values_mut() {
|
||||
*element = map_groups_to_legacy(element);
|
||||
}
|
||||
push_lane_paint_into_interiors(&mut list);
|
||||
return list;
|
||||
}
|
||||
None.or_else(|| run_to_legacy_list::<Vector>(item).map(|list| detable_items(list, Graphic::Vector)))
|
||||
.or_else(|| run_to_legacy_list::<Raster<CPU>>(item).map(|list| detable_items(list, Graphic::RasterCPU)))
|
||||
.or_else(|| run_to_legacy_list::<Raster<GPU>>(item).map(|list| detable_items(list, Graphic::RasterGPU)))
|
||||
.or_else(|| run_to_legacy_list::<Color>(item).map(|list| detable_items(list, Graphic::Color)))
|
||||
.or_else(|| run_to_legacy_list::<GradientStops>(item).map(|list| detable_items(list, Graphic::Gradient)))
|
||||
.or_else(|| run_to_legacy_list::<String>(item).map(|list| detable_items(list, Graphic::Text)))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod run_tests {
|
||||
use super::*;
|
||||
use crate::graphic::test_support::{native_group_paint, unit_square_at};
|
||||
use crate::markers::Fill;
|
||||
use core_types::attribute::Attribute;
|
||||
use core_types::record::{FieldWrite, RunBuilder, element_write_hashed};
|
||||
use glam::DVec2;
|
||||
|
||||
#[test]
|
||||
fn a_legacy_list_owns_its_paint_attr_content() {
|
||||
let inner_vector = unit_square_at(DVec2::ZERO);
|
||||
let source = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
// SAFETY: the erased native list serves only while `source` is live; the
|
||||
// deep glue under test replaces its borrows at the copy-out seam.
|
||||
let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) };
|
||||
|
||||
let vector = unit_square_at(DVec2::new(4., 4.));
|
||||
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
|
||||
let lane = builder.push(vector.clone()).unwrap();
|
||||
builder.attr::<Fill>(lane, Some(&paint));
|
||||
let item = builder.finish();
|
||||
let legacy = run_to_legacy_list::<Vector>(&item).expect("the run lowers to a legacy vector list");
|
||||
let expected = map_groups_to_legacy(paint.element(0).unwrap());
|
||||
drop(item);
|
||||
drop(paint);
|
||||
drop(source);
|
||||
|
||||
let served = legacy.attribute::<Option<List<Graphic>>>(Fill::NAME, 0).expect("the fill attribute rides the list");
|
||||
let served = served.as_ref().expect("the fill is present");
|
||||
assert_eq!(served.element(0).unwrap(), &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_run_list_keeps_native_group_elements() {
|
||||
let inner_vector = unit_square_at(DVec2::ZERO);
|
||||
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
let content = native_group_paint(&inner_vector, &arena);
|
||||
let element = content.element(0).unwrap();
|
||||
|
||||
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Graphic>(), &[], 1).unwrap();
|
||||
builder.push(element.clone()).unwrap();
|
||||
let item = builder.finish();
|
||||
let list = run_to_list::<Graphic>(&item).expect("the run holds graphic lanes");
|
||||
assert!(matches!(list.element(0), Some(Graphic::Group(_))), "the list keeps the native group form");
|
||||
}
|
||||
}
|
||||
697
node-graph/libraries/graphic-types/src/graphic/mod.rs
Normal file
697
node-graph/libraries/graphic-types/src/graphic/mod.rs
Normal file
@@ -0,0 +1,697 @@
|
||||
mod glue;
|
||||
mod legacy;
|
||||
mod paint;
|
||||
mod walk;
|
||||
|
||||
pub(crate) use glue::list_contains_groups;
|
||||
pub use glue::{map_groups_to_owned, map_groups_to_persistent, map_groups_to_resident};
|
||||
pub(crate) use legacy::run_to_legacy_list;
|
||||
pub use legacy::{group_to_legacy_graphic, group_to_legacy_list, map_groups_to_legacy, map_paint_attrs_to_legacy, run_to_list};
|
||||
pub use paint::{
|
||||
LanePaint, PaintColumns, PaintOverlay, PaintOverlayColumn, PaintReach, bake_paint_transforms, has_paint, is_paint_present, paint_graphics, set_paint_attribute, set_paint_attribute_at,
|
||||
vector_can_reduce_to_clip_path,
|
||||
};
|
||||
pub use walk::{GraphicLevel, GraphicLevelColumn, RowStep, VectorRow, direct_vector_len, flatten_vector_rows, group_is_empty, lane_attributes, run_lane_attributes, walk_vector_rows};
|
||||
use walk::{group_all_clipped, group_bounding_box, group_is_fully_transparent, group_is_opaque, group_render_complexity};
|
||||
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::graphene_hash::CacheHash;
|
||||
use core_types::list::{Item, List};
|
||||
use core_types::ops::{FromAnchorPosition, ListConvert};
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use core_types::uuid::NodeId;
|
||||
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use raster_types::{CPU, GPU, Raster};
|
||||
use vector_types::GradientStops;
|
||||
pub use vector_types::Vector;
|
||||
|
||||
/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
|
||||
/// A leaf holds its element directly; its attributes ride the containing
|
||||
/// lane. Multi-element content is a [`core_types::record::Group`] run, or
|
||||
/// transitionally the legacy `Graphic` list.
|
||||
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)]
|
||||
pub enum Graphic<'e> {
|
||||
Graphic(List<Graphic<'e>>),
|
||||
Vector(Vector),
|
||||
RasterCPU(Raster<CPU>),
|
||||
RasterGPU(Raster<GPU>),
|
||||
Color(Color),
|
||||
Gradient(GradientStops),
|
||||
Text(String),
|
||||
Group(core_types::record::Group<'e>),
|
||||
}
|
||||
|
||||
impl Default for Graphic<'_> {
|
||||
fn default() -> Self {
|
||||
Self::Graphic(List::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// A typed legacy list as a legacy graphic list: each item de-tables to a
|
||||
/// leaf element, keeping its attributes on the containing lane.
|
||||
pub(in crate::graphic) fn detable_items<'e, T: Clone + Send + Sync + 'static>(list: List<T>, leaf: fn(T) -> Graphic<'e>) -> List<Graphic<'e>> {
|
||||
let mut out = List::new();
|
||||
for item in list.into_iter() {
|
||||
let (element, attributes) = item.into_parts();
|
||||
out.push(Item::from_parts(leaf(element), attributes));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The element-space coercion into `Graphic`: a leaf converts in place and a
|
||||
/// legacy list becomes a native group built over the arena, so the coercion
|
||||
/// never constructs a legacy interior.
|
||||
pub trait IntoGraphicElement: Clone + Send + Sync + CacheHash + 'static {
|
||||
/// `None` reports arena exhaustion.
|
||||
fn into_graphic_element(self, arena: &core_types::arena::Arena) -> Option<Graphic>;
|
||||
}
|
||||
|
||||
fn list_group<T: Clone + Send + Sync + CacheHash + PartialEq + dyn_any::StaticTypeSized>(list: List<T>, arena: &core_types::arena::Arena) -> Option<Graphic>
|
||||
where
|
||||
T::Static: Clone + Send + Sync,
|
||||
{
|
||||
Some(Graphic::Group(core_types::record::Group {
|
||||
row: None,
|
||||
content: core_types::record::GroupItem::from_list(list, arena)?,
|
||||
}))
|
||||
}
|
||||
|
||||
macro_rules! into_graphic_element {
|
||||
($($leaf:ident: $element:ty;)*) => {
|
||||
$(
|
||||
impl IntoGraphicElement for $element {
|
||||
fn into_graphic_element(self, _arena: &core_types::arena::Arena) -> Option<Graphic> {
|
||||
Some(Graphic::$leaf(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicElement for List<$element> {
|
||||
fn into_graphic_element(self, arena: &core_types::arena::Arena) -> Option<Graphic> {
|
||||
list_group(self, arena)
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
into_graphic_element! {
|
||||
Vector: Vector;
|
||||
RasterCPU: Raster<CPU>;
|
||||
RasterGPU: Raster<GPU>;
|
||||
Color: Color;
|
||||
Gradient: GradientStops;
|
||||
Text: String;
|
||||
}
|
||||
|
||||
impl IntoGraphicElement for Graphic<'static> {
|
||||
fn into_graphic_element(self, _arena: &core_types::arena::Arena) -> Option<Graphic> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicElement for List<Graphic<'static>> {
|
||||
fn into_graphic_element(self, arena: &core_types::arena::Arena) -> Option<Graphic> {
|
||||
list_group(self, arena)
|
||||
}
|
||||
}
|
||||
|
||||
// Vector
|
||||
impl From<Vector> for Graphic<'_> {
|
||||
fn from(vector: Vector) -> Self {
|
||||
Graphic::Vector(vector)
|
||||
}
|
||||
}
|
||||
|
||||
// Raster<CPU>
|
||||
impl From<Raster<CPU>> for Graphic<'_> {
|
||||
fn from(raster: Raster<CPU>) -> Self {
|
||||
Graphic::RasterCPU(raster)
|
||||
}
|
||||
}
|
||||
|
||||
// Raster<GPU>
|
||||
impl From<Raster<GPU>> for Graphic<'_> {
|
||||
fn from(raster: Raster<GPU>) -> Self {
|
||||
Graphic::RasterGPU(raster)
|
||||
}
|
||||
}
|
||||
|
||||
// Color
|
||||
impl From<Color> for Graphic<'_> {
|
||||
fn from(color: Color) -> Self {
|
||||
Graphic::Color(color)
|
||||
}
|
||||
}
|
||||
// Note: List<Color> -> Option<Color> is in gcore (Color is defined there)
|
||||
|
||||
// GradientStops
|
||||
impl From<GradientStops> for Graphic<'_> {
|
||||
fn from(gradient: GradientStops) -> Self {
|
||||
Graphic::Gradient(gradient)
|
||||
}
|
||||
}
|
||||
|
||||
// String
|
||||
impl From<String> for Graphic<'_> {
|
||||
fn from(text: String) -> Self {
|
||||
Graphic::Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deeply flattens a `List<Graphic>`, collecting only elements matching a specific variant (extracted by `extract_variant`)
|
||||
/// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-`List`s composes transforms and opacity.
|
||||
fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>) -> List<T> {
|
||||
fn flatten_recursive<T>(output: &mut List<T>, current_graphic_list: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>, parent_layer_path: Option<&[NodeId]>) {
|
||||
for current_graphic_item in current_graphic_list.into_iter() {
|
||||
// Whether the parent carries each attribute: a structural fact (column presence), never a value comparison.
|
||||
// Flattening composes a parent attribute onto its children only when the parent has it,
|
||||
// so an absent parent attribute never invents a column the children didn't already have.
|
||||
let parent_has_transform = current_graphic_item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some();
|
||||
let parent_has_opacity = current_graphic_item.attribute::<f64>(ATTR_OPACITY).is_some();
|
||||
let parent_has_fill = current_graphic_item.attribute::<f64>(ATTR_OPACITY_FILL).is_some();
|
||||
|
||||
let current_transform: DAffine2 = current_graphic_item.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||
let current_opacity: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY, 1.);
|
||||
let current_fill: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
|
||||
let lane_layer_path: Option<Vec<NodeId>> = current_graphic_item.attribute::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH).cloned();
|
||||
|
||||
let (element, attributes) = current_graphic_item.into_parts();
|
||||
match element {
|
||||
// Compose the parent's transform/opacity/fill onto each child, but only for attributes the parent carries.
|
||||
// A child lacking one is padded with the composition identity (`1.` for opacity/fill, identity for transform), so composing through it is a no-op.
|
||||
Graphic::Graphic(mut sub_list) => {
|
||||
if parent_has_transform {
|
||||
for v in sub_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||
*v = current_transform * *v;
|
||||
}
|
||||
}
|
||||
if parent_has_opacity {
|
||||
for v in sub_list.iter_attribute_values_mut_or_default::<f64>(ATTR_OPACITY) {
|
||||
*v *= current_opacity;
|
||||
}
|
||||
}
|
||||
if parent_has_fill {
|
||||
for v in sub_list.iter_attribute_values_mut_or_default::<f64>(ATTR_OPACITY_FILL) {
|
||||
*v *= current_fill;
|
||||
}
|
||||
}
|
||||
|
||||
flatten_recursive(output, sub_list, extract_variant, lane_layer_path.as_deref());
|
||||
}
|
||||
// A bridge row's native group flattens through its legacy lowering; the arm dies with the legacy interior.
|
||||
Graphic::Group(group) => {
|
||||
let lowered = List::new_from_item(Item::from_parts(group_to_legacy_graphic(&group), attributes.clone()));
|
||||
flatten_recursive(output, lowered, extract_variant, parent_layer_path);
|
||||
}
|
||||
// A de-tabled leaf is one attr-less element; the extracted row rides with its containing lane's full attributes, paint included.
|
||||
// The enclosing group lane's own layer path overrides, one hop only, matching the native walk.
|
||||
other => {
|
||||
if let Some(typed_list) = extract_variant(other) {
|
||||
for item in typed_list.into_iter() {
|
||||
let mut row = Item::from_parts(item.into_element(), attributes.clone());
|
||||
if let Some(layer_path) = parent_layer_path {
|
||||
row.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.to_vec());
|
||||
}
|
||||
output.push(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut output = List::new();
|
||||
flatten_recursive(&mut output, content, extract_variant, None);
|
||||
output
|
||||
}
|
||||
|
||||
/// Maps from a concrete element type to its corresponding `Graphic` enum variant,
|
||||
/// enabling type-directed casting of typed `List`s from a `Graphic` value.
|
||||
pub trait TryFromGraphic: Clone + Sized {
|
||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>>;
|
||||
}
|
||||
|
||||
impl TryFromGraphic for Vector {
|
||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
||||
if let Graphic::Vector(t) = graphic { Some(List::new_from_element(t)) } else { None }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFromGraphic for Raster<CPU> {
|
||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
||||
if let Graphic::RasterCPU(t) = graphic { Some(List::new_from_element(t)) } else { None }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFromGraphic for Color {
|
||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
||||
if let Graphic::Color(t) = graphic { Some(List::new_from_element(t)) } else { None }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFromGraphic for GradientStops {
|
||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
||||
if let Graphic::Gradient(t) = graphic { Some(List::new_from_element(t)) } else { None }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFromGraphic for String {
|
||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
||||
if let Graphic::Text(t) = graphic { Some(List::new_from_element(t)) } else { None }
|
||||
}
|
||||
}
|
||||
|
||||
// Local trait to convert types to List<Graphic> (avoids orphan rule issues)
|
||||
pub trait IntoGraphicList: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static {
|
||||
fn into_graphic_list(self) -> List<Graphic<'static>>;
|
||||
|
||||
/// Deeply flattens any content of type `T` within a `List<Graphic>`, discarding all other content, and returning a flat `List<T>`.
|
||||
fn into_flattened_list<T: TryFromGraphic>(self) -> List<T>
|
||||
where
|
||||
Self: std::marker::Sized,
|
||||
{
|
||||
flatten_graphic_list(self.into_graphic_list(), T::try_from_graphic)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicList for List<Graphic<'static>> {
|
||||
fn into_graphic_list(self) -> List<Graphic<'static>> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicList for List<Vector> {
|
||||
fn into_graphic_list(self) -> List<Graphic<'static>> {
|
||||
detable_items(self, Graphic::Vector)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicList for List<Raster<CPU>> {
|
||||
fn into_graphic_list(self) -> List<Graphic<'static>> {
|
||||
detable_items(self, Graphic::RasterCPU)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicList for List<Raster<GPU>> {
|
||||
fn into_graphic_list(self) -> List<Graphic<'static>> {
|
||||
detable_items(self, Graphic::RasterGPU)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicList for List<Color> {
|
||||
fn into_graphic_list(self) -> List<Graphic<'static>> {
|
||||
detable_items(self, Graphic::Color)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicList for List<GradientStops> {
|
||||
fn into_graphic_list(self) -> List<Graphic<'static>> {
|
||||
detable_items(self, Graphic::Gradient)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicList for List<String> {
|
||||
fn into_graphic_list(self) -> List<Graphic<'static>> {
|
||||
detable_items(self, Graphic::Text)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicList for DAffine2 {
|
||||
fn into_graphic_list(self) -> List<Graphic<'static>> {
|
||||
List::new_from_element(Graphic::default())
|
||||
}
|
||||
}
|
||||
|
||||
// DAffine2
|
||||
impl From<DAffine2> for Graphic<'_> {
|
||||
fn from(_: DAffine2) -> Self {
|
||||
Graphic::default()
|
||||
}
|
||||
}
|
||||
|
||||
// DVec2
|
||||
impl From<DVec2> for Graphic<'_> {
|
||||
fn from(position: DVec2) -> Self {
|
||||
Graphic::Vector(Vector::from_anchor_position(position))
|
||||
}
|
||||
}
|
||||
// Note: List conversions handled by blanket impl in gcore
|
||||
|
||||
impl<'e> Graphic<'e> {
|
||||
pub fn as_graphic(&self) -> Option<&List<Graphic>> {
|
||||
match self {
|
||||
Graphic::Graphic(graphic) => Some(graphic),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_graphic_mut(&mut self) -> Option<&mut List<Graphic<'e>>> {
|
||||
match self {
|
||||
Graphic::Graphic(graphic) => Some(graphic),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_vector(&self) -> Option<&Vector> {
|
||||
match self {
|
||||
Graphic::Vector(vector) => Some(vector),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_raster(&self) -> Option<&Raster<CPU>> {
|
||||
match self {
|
||||
Graphic::RasterCPU(raster) => Some(raster),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_raster_mut(&mut self) -> Option<&mut Raster<CPU>> {
|
||||
match self {
|
||||
Graphic::RasterCPU(raster) => Some(raster),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A leaf carries no clipping attribute, which rides its containing lane.
|
||||
pub fn had_clip_enabled(&self) -> bool {
|
||||
fn all_clipped<T>(list: &List<T>) -> bool {
|
||||
list.iter_attribute_values_or_default::<bool>(ATTR_CLIPPING_MASK).all(|clip| clip)
|
||||
}
|
||||
|
||||
match self {
|
||||
Graphic::Graphic(list) => all_clipped(list),
|
||||
Graphic::Group(group) => group_all_clipped(group),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_reduce_to_clip_path(&self) -> bool {
|
||||
match self {
|
||||
Graphic::Vector(vector) => vector_can_reduce_to_clip_path(&core_types::lane::Single(vector)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
match self {
|
||||
Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque),
|
||||
// A bare leaf carries no paint attribute, which rides its lane, so
|
||||
// nothing here claims opacity.
|
||||
Graphic::Vector(_) => false,
|
||||
Graphic::Color(color) => color.is_opaque(),
|
||||
Graphic::Gradient(stops) => stops.iter().all(|stop| stop.color.is_opaque()),
|
||||
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false,
|
||||
Graphic::Group(group) => group_is_opaque(group),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_fully_transparent(&self) -> bool {
|
||||
match self {
|
||||
Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent),
|
||||
// A bare leaf carries no paint attribute, so only an unstroked
|
||||
// vector is invisible on its own.
|
||||
Graphic::Vector(vector) => vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()),
|
||||
Graphic::Color(color) => color.a() == 0.,
|
||||
Graphic::Gradient(stops) => stops.iter().all(|stop| stop.color.a() == 0.),
|
||||
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false,
|
||||
Graphic::Group(group) => group_is_fully_transparent(group),
|
||||
}
|
||||
}
|
||||
|
||||
/// True if this paint opaquely covers the entire fill region.
|
||||
/// Vector, Raster, and a nested Graphic may leave gaps, so they return false.
|
||||
pub fn covers_opaquely(&self) -> bool {
|
||||
matches!(self, Graphic::Color(_) | Graphic::Gradient(_)) && self.is_opaque()
|
||||
}
|
||||
|
||||
/// Whether the graphic holds no content: a leaf always holds its element.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Graphic::Graphic(list) => list.is_empty(),
|
||||
Graphic::Group(group) => group_is_empty(group),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for Graphic<'_> {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||
match self {
|
||||
Graphic::Vector(vector) => BoundingBox::bounding_box(vector, transform, include_stroke),
|
||||
Graphic::RasterCPU(raster) => raster.bounding_box(transform, include_stroke),
|
||||
Graphic::RasterGPU(raster) => raster.bounding_box(transform, include_stroke),
|
||||
Graphic::Graphic(list) => list.bounding_box(transform, include_stroke),
|
||||
Graphic::Color(color) => color.bounding_box(transform, include_stroke),
|
||||
Graphic::Gradient(gradient) => gradient.bounding_box(transform, include_stroke),
|
||||
Graphic::Text(text) => text.bounding_box(transform, include_stroke),
|
||||
Graphic::Group(group) => group_bounding_box(group, transform, include_stroke, false),
|
||||
}
|
||||
}
|
||||
|
||||
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||
match self {
|
||||
Graphic::Vector(vector) => vector.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::RasterCPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::RasterGPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::Graphic(graphic) => graphic.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::Color(color) => color.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::Gradient(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::Text(list) => list.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::Group(group) => group_bounding_box(group, transform, include_stroke, true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'e> ListConvert<Graphic<'e>> for Vector {
|
||||
fn convert_item(self) -> Graphic<'e> {
|
||||
Graphic::Vector(self)
|
||||
}
|
||||
}
|
||||
impl<'e> ListConvert<Graphic<'e>> for Raster<CPU> {
|
||||
fn convert_item(self) -> Graphic<'e> {
|
||||
Graphic::RasterCPU(self)
|
||||
}
|
||||
}
|
||||
impl<'e> ListConvert<Graphic<'e>> for Raster<GPU> {
|
||||
fn convert_item(self) -> Graphic<'e> {
|
||||
Graphic::RasterGPU(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderComplexity for Graphic<'_> {
|
||||
fn render_complexity(&self) -> usize {
|
||||
match self {
|
||||
Self::Graphic(list) => list.render_complexity(),
|
||||
Self::Vector(list) => list.render_complexity(),
|
||||
Self::RasterCPU(list) => list.render_complexity(),
|
||||
Self::RasterGPU(list) => list.render_complexity(),
|
||||
Self::Color(list) => list.render_complexity(),
|
||||
Self::Gradient(list) => list.render_complexity(),
|
||||
Self::Text(list) => list.render_complexity(),
|
||||
Self::Group(group) => group_render_complexity(group),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Node definitions moved to graphic-nodes crate
|
||||
|
||||
pub trait AtIndex {
|
||||
type Output;
|
||||
fn at_index(&self, index: usize) -> Option<Self::Output>;
|
||||
fn at_index_from_end(&self, index: usize) -> Option<Self::Output>;
|
||||
}
|
||||
impl<T: Clone> AtIndex for Vec<T> {
|
||||
type Output = T;
|
||||
|
||||
fn at_index(&self, index: usize) -> Option<Self::Output> {
|
||||
self.get(index).cloned()
|
||||
}
|
||||
|
||||
fn at_index_from_end(&self, index: usize) -> Option<Self::Output> {
|
||||
if index == 0 || index > self.len() { None } else { self.get(self.len() - index).cloned() }
|
||||
}
|
||||
}
|
||||
impl<T: Clone> AtIndex for List<T> {
|
||||
type Output = List<T>;
|
||||
|
||||
fn at_index(&self, index: usize) -> Option<Self::Output> {
|
||||
self.clone_item(index).map(|item| {
|
||||
let mut result_list = Self::default();
|
||||
result_list.push(item);
|
||||
result_list
|
||||
})
|
||||
}
|
||||
|
||||
fn at_index_from_end(&self, index: usize) -> Option<Self::Output> {
|
||||
if index == 0 || index > self.len() { None } else { self.at_index(self.len() - index) }
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OmitIndex {
|
||||
fn omit_index(&self, index: usize) -> Self;
|
||||
fn omit_index_from_end(&self, index: usize) -> Self;
|
||||
}
|
||||
impl<T: Clone> OmitIndex for Vec<T> {
|
||||
fn omit_index(&self, index: usize) -> Self {
|
||||
self.iter().enumerate().filter(|(i, _)| *i != index).map(|(_, v)| v.clone()).collect()
|
||||
}
|
||||
|
||||
fn omit_index_from_end(&self, index: usize) -> Self {
|
||||
if index == 0 || index > self.len() {
|
||||
return self.clone();
|
||||
}
|
||||
self.omit_index(self.len() - index)
|
||||
}
|
||||
}
|
||||
impl<T: Clone> OmitIndex for List<T> {
|
||||
fn omit_index(&self, index: usize) -> Self {
|
||||
let mut result = Self::default();
|
||||
for i in 0..self.len() {
|
||||
if i != index
|
||||
&& let Some(item) = self.clone_item(i)
|
||||
{
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn omit_index_from_end(&self, index: usize) -> Self {
|
||||
if index == 0 || index > self.len() {
|
||||
return self.clone();
|
||||
}
|
||||
self.omit_index(self.len() - index)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core_types::list::List;
|
||||
|
||||
fn vector_graphic() -> Graphic<'static> {
|
||||
Graphic::Vector(Vector::default())
|
||||
}
|
||||
|
||||
// Flattening must not invent attribute columns that neither the parent graphic nor the child carried
|
||||
#[test]
|
||||
fn flatten_does_not_invent_attributes() {
|
||||
let graphics = List::new_from_element(vector_graphic());
|
||||
let flattened: List<Vector> = graphics.into_flattened_list();
|
||||
for key in [ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, ATTR_EDITOR_LAYER_PATH] {
|
||||
assert!(!flattened.attribute_keys().any(|k| k == key), "flatten invented the `{key}` attribute");
|
||||
}
|
||||
}
|
||||
|
||||
// A parent attribute that is present must compose onto the flattened children
|
||||
#[test]
|
||||
fn flatten_propagates_present_attributes() {
|
||||
let mut graphics = List::new_from_element(vector_graphic());
|
||||
graphics.set_attribute(ATTR_OPACITY, 0, 0.5_f64);
|
||||
let flattened: List<Vector> = graphics.into_flattened_list();
|
||||
assert_eq!(flattened.attribute_cloned_or_default::<f64>(ATTR_OPACITY, 0), 0.5);
|
||||
|
||||
let mut group = List::new_from_element(Graphic::Graphic(List::new_from_element(vector_graphic())));
|
||||
group.set_attribute(ATTR_OPACITY, 0, 0.5_f64);
|
||||
let flattened: List<Vector> = group.into_flattened_list();
|
||||
assert_eq!(flattened.attribute_cloned_or_default::<f64>(ATTR_OPACITY, 0), 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod graphic_is_opaque_tests {
|
||||
use vector_types::GradientStop;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn color_graphic(alpha: f64) -> Graphic<'static> {
|
||||
let color = Color::from_rgbaf32(1., 0., 0., alpha as f32).unwrap();
|
||||
Graphic::Color(color)
|
||||
}
|
||||
|
||||
fn gradient_graphic(gradient: GradientStops) -> Graphic<'static> {
|
||||
Graphic::Gradient(gradient)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opaque_color_is_opaque() {
|
||||
let g = color_graphic(1.);
|
||||
assert!(g.is_opaque());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transparent_color_is_not_opaque() {
|
||||
let g = color_graphic(0.5);
|
||||
assert!(!g.is_opaque());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_is_not_opaque() {
|
||||
let g = Graphic::Vector(Vector::default());
|
||||
assert!(!g.is_opaque());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradient_with_all_opaque_stops_is_opaque() {
|
||||
let color_1 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
|
||||
let color_2 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
|
||||
let gradient = GradientStops::new(vec![
|
||||
GradientStop {
|
||||
position: 0.,
|
||||
midpoint: 0.5,
|
||||
color: color_1,
|
||||
},
|
||||
GradientStop {
|
||||
position: 1.,
|
||||
midpoint: 0.5,
|
||||
color: color_2,
|
||||
},
|
||||
]);
|
||||
let g = gradient_graphic(gradient);
|
||||
assert!(g.is_opaque());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradient_with_transparent_stop_is_not_opaque() {
|
||||
let color_1 = Color::from_rgbaf32(1., 0., 0., 0.5).unwrap();
|
||||
let color_2 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
|
||||
let gradient = GradientStops::new(vec![
|
||||
GradientStop {
|
||||
position: 0.,
|
||||
midpoint: 0.5,
|
||||
color: color_1,
|
||||
},
|
||||
GradientStop {
|
||||
position: 1.,
|
||||
midpoint: 0.5,
|
||||
color: color_2,
|
||||
},
|
||||
]);
|
||||
let g = gradient_graphic(gradient);
|
||||
assert!(!g.is_opaque());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_support {
|
||||
use super::Graphic;
|
||||
use core_types::list::List;
|
||||
use core_types::record::{RunBuilder, element_write_hashed};
|
||||
use glam::DVec2;
|
||||
use vector_types::Vector;
|
||||
use vector_types::subpath::Subpath;
|
||||
use vector_types::vector::PointId;
|
||||
|
||||
pub(in crate::graphic) fn unit_square_at(corner: DVec2) -> Vector {
|
||||
Vector::from_subpath(Subpath::<PointId>::new_rectangle(corner, corner + DVec2::ONE))
|
||||
}
|
||||
|
||||
pub(in crate::graphic) fn native_group_paint<'a>(vector: &Vector, arena: &'a core_types::arena::Arena) -> List<Graphic<'a>> {
|
||||
let mut builder = RunBuilder::new(arena, element_write_hashed::<Vector>(), &[], 1).unwrap();
|
||||
builder.push(vector.clone()).unwrap();
|
||||
List::new_from_element(Graphic::Group(core_types::record::Group { row: None, content: builder.finish() }))
|
||||
}
|
||||
}
|
||||
273
node-graph/libraries/graphic-types/src/graphic/paint.rs
Normal file
273
node-graph/libraries/graphic-types/src/graphic/paint.rs
Normal file
@@ -0,0 +1,273 @@
|
||||
use super::{Graphic, IntoGraphicList};
|
||||
use crate::markers::{ATTR_FILL, ATTR_STROKE, Fill, Stroke};
|
||||
use core_types::ATTR_TRANSFORM;
|
||||
use core_types::attribute::{Attribute, Opacity};
|
||||
use core_types::lane::{LaneColumn, LaneSource};
|
||||
use core_types::list::{ItemAttributeValues, List};
|
||||
use glam::DAffine2;
|
||||
use vector_types::Vector;
|
||||
|
||||
/// Whether a normalized paint graphic list actually carries renderable paint.
|
||||
/// A 0-item list, or a list whose first graphic is empty, is treated as no paint.
|
||||
pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool {
|
||||
graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty())
|
||||
}
|
||||
|
||||
/// Look up the paint graphics stored under the marker `A`, in the canonical `List<Graphic>` form.
|
||||
pub fn paint_graphics<'a, A, S>(source: &'a S, index: usize) -> Option<&'a List<Graphic<'static>>>
|
||||
where
|
||||
S: LaneSource,
|
||||
A: Attribute<Value<'a> = Option<&'a List<Graphic<'static>>>>,
|
||||
{
|
||||
source
|
||||
.attr::<A>(index)
|
||||
// Treat a blank paint attribute as absent so an empty attribute doesn't count as painted
|
||||
.filter(|graphic_list| is_paint_present(graphic_list))
|
||||
}
|
||||
|
||||
/// Whether the item carries a non-blank canonical `List<Graphic>` paint under the marker `A`,
|
||||
/// checked by borrowing without cloning the renderable list.
|
||||
pub fn has_paint<'a, A, S>(source: &'a S, index: usize) -> bool
|
||||
where
|
||||
S: LaneSource,
|
||||
A: Attribute<Value<'a> = Option<&'a List<Graphic<'static>>>>,
|
||||
{
|
||||
paint_graphics::<A, S>(source, index).is_some()
|
||||
}
|
||||
|
||||
/// Whether every lane of a vector source draws as a plain clip path: fully
|
||||
/// opaque, fill absent or opaque, stroke invisible or fully transparent.
|
||||
pub fn vector_can_reduce_to_clip_path<S: LaneSource<Element = Vector>>(source: &S) -> bool {
|
||||
(0..source.lane_count()).all(|index| {
|
||||
let Some(element) = source.element(index) else { return false };
|
||||
let opacity: f64 = source.attr::<Opacity>(index);
|
||||
|
||||
let fill_opaque_or_absent = paint_graphics::<Fill, _>(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque()));
|
||||
|
||||
let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke())
|
||||
|| paint_graphics::<Stroke, _>(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
|
||||
|
||||
opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent
|
||||
})
|
||||
}
|
||||
|
||||
/// The paint a lane carries for its interiors, in the reference form
|
||||
/// [`PaintOverlay`] threads down.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct LanePaint<'a> {
|
||||
pub fill: Option<&'a List<Graphic<'static>>>,
|
||||
pub stroke: Option<&'a List<Graphic<'static>>>,
|
||||
}
|
||||
|
||||
impl<'a> LanePaint<'a> {
|
||||
pub const NONE: Self = Self { fill: None, stroke: None };
|
||||
|
||||
pub fn is_present(&self) -> bool {
|
||||
self.fill.is_some() || self.stroke.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// A source's fill and stroke columns, resolved once for per-lane reads.
|
||||
pub struct PaintColumns<'a, S: LaneSource + 'a> {
|
||||
fill: S::Column<'a, Fill>,
|
||||
stroke: S::Column<'a, Stroke>,
|
||||
}
|
||||
|
||||
impl<'a, S: LaneSource> PaintColumns<'a, S> {
|
||||
pub fn new(source: &'a S) -> Self {
|
||||
Self {
|
||||
fill: source.column::<Fill>(),
|
||||
stroke: source.column::<Stroke>(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The lane's present, non-blank paint.
|
||||
pub fn read(&self, lane: usize) -> LanePaint<'a> {
|
||||
let present = |value: Option<Option<&'a List<Graphic<'static>>>>| value.flatten().filter(|list| is_paint_present(list));
|
||||
LanePaint {
|
||||
fill: present(self.fill.try_get(lane)),
|
||||
stroke: present(self.stroke.try_get(lane)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How far a lane's paint reaches into the element beneath it, mirroring the
|
||||
/// legacy conversion's paint push: vector interiors directly and vector
|
||||
/// children of a nested graphic list, one level deep.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PaintReach<'a> {
|
||||
pub paint: LanePaint<'a>,
|
||||
hops: u8,
|
||||
}
|
||||
|
||||
impl<'a> PaintReach<'a> {
|
||||
pub const NONE: Self = Self { paint: LanePaint::NONE, hops: 0 };
|
||||
|
||||
/// The lane's effective reach: an inherited paint stays authoritative
|
||||
/// (lane paint below a push's origin is inert in the legacy model), an
|
||||
/// absent one reads the lane's own paint.
|
||||
pub fn for_lane<S: LaneSource>(self, columns: &PaintColumns<'a, S>, index: usize) -> Self {
|
||||
match self.paint.is_present() {
|
||||
true => self,
|
||||
false => Self { paint: columns.read(index), hops: 2 },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn applies(&self) -> bool {
|
||||
self.hops > 0 && self.paint.is_present()
|
||||
}
|
||||
|
||||
/// The reach one graphic nesting level further down.
|
||||
pub fn nested(self) -> Self {
|
||||
Self {
|
||||
paint: self.paint,
|
||||
hops: self.hops.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
/// The reach entering a group's own graphic run: a spent or absent reach
|
||||
/// resets so the group's own lane paint applies at its own boundary.
|
||||
pub fn into_group_graphics(self) -> Self {
|
||||
match self.applies() {
|
||||
true => self.nested(),
|
||||
false => Self::NONE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A source with a lane's paint forced over its fill and stroke columns,
|
||||
/// reaching the interiors the legacy conversion's paint push reached.
|
||||
pub struct PaintOverlay<'a, S> {
|
||||
inner: &'a S,
|
||||
paint: LanePaint<'a>,
|
||||
}
|
||||
|
||||
impl<'a, S> PaintOverlay<'a, S> {
|
||||
pub fn new(inner: &'a S, paint: LanePaint<'a>) -> Self {
|
||||
Self { inner, paint }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PaintOverlayColumn<'a, S: LaneSource + 'a, A: Attribute> {
|
||||
inner: S::Column<'a, A>,
|
||||
forced: Option<A::Value<'a>>,
|
||||
}
|
||||
|
||||
impl<'a, S: LaneSource, A: Attribute> LaneColumn<'a, A> for PaintOverlayColumn<'a, S, A> {
|
||||
fn try_get(&self, lane: usize) -> Option<A::Value<'a>> {
|
||||
match self.forced {
|
||||
Some(forced) => Some(forced),
|
||||
None => self.inner.try_get(lane),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The forced value for the marker `A`: the lane paint where `A` is this
|
||||
/// crate's fill or stroke marker, absent otherwise.
|
||||
fn forced_paint<'a, A: Attribute>(paint: LanePaint<'a>) -> Option<A::Value<'a>> {
|
||||
let slot = match A::NAME {
|
||||
name if name == Fill::NAME => paint.fill,
|
||||
name if name == Stroke::NAME => paint.stroke,
|
||||
_ => None,
|
||||
}?;
|
||||
assert_eq!(
|
||||
std::any::TypeId::of::<A::Value<'static>>(),
|
||||
std::any::TypeId::of::<Option<&'static List<Graphic<'static>>>>(),
|
||||
"attribute `{}` is declared at another value type than this crate's paint form",
|
||||
A::NAME
|
||||
);
|
||||
assert_eq!(
|
||||
size_of::<A::Value<'a>>(),
|
||||
size_of::<Option<&'a List<Graphic<'a>>>>(),
|
||||
"the paint value form must span the marker's value"
|
||||
);
|
||||
// SAFETY: the census admits one value type per attribute name and panics on
|
||||
// a conflict at registration, and the asserts above re-check it, so a marker
|
||||
// named `fill` or `stroke` carries this crate's `Option<&List<Graphic>>`
|
||||
// value form at the same size.
|
||||
Some(unsafe { std::mem::transmute_copy::<Option<&'a List<Graphic>>, A::Value<'a>>(&Some(slot)) })
|
||||
}
|
||||
|
||||
impl<'a, S: LaneSource> LaneSource for PaintOverlay<'a, S> {
|
||||
type Element = S::Element;
|
||||
type Column<'b, A: Attribute>
|
||||
= PaintOverlayColumn<'b, S, A>
|
||||
where
|
||||
Self: 'b;
|
||||
|
||||
fn lane_count(&self) -> usize {
|
||||
self.inner.lane_count()
|
||||
}
|
||||
|
||||
fn element(&self, lane: usize) -> Option<&S::Element> {
|
||||
self.inner.element(lane)
|
||||
}
|
||||
|
||||
fn column<A: Attribute>(&self) -> PaintOverlayColumn<'_, S, A> {
|
||||
PaintOverlayColumn {
|
||||
inner: self.inner.column::<A>(),
|
||||
forced: forced_paint::<A>(self.paint),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores a paint attribute in the paint marker's owned form, the only representation paint readers accept.
|
||||
pub fn set_paint_attribute(attributes: &mut ItemAttributeValues, key: &str, paint: impl IntoGraphicList) {
|
||||
attributes.insert(key, Some(paint.into_graphic_list()));
|
||||
}
|
||||
|
||||
/// Stores a paint attribute at a list index in the paint marker's owned form, the only representation paint readers accept.
|
||||
pub fn set_paint_attribute_at<T>(list: &mut List<T>, index: usize, key: &str, paint: impl IntoGraphicList) {
|
||||
list.set_attribute(key, index, Some(paint.into_graphic_list()));
|
||||
}
|
||||
|
||||
/// Bake the provided transform into the per-item transforms of the paint graphics stored under the
|
||||
/// canonical `List<Graphic>` fill and stroke attributes.
|
||||
pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) {
|
||||
fn bake_graphic_paint_transform(graphics: &mut List<Graphic>, transform: DAffine2) {
|
||||
for item_transform in graphics.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||
*item_transform = transform * *item_transform;
|
||||
}
|
||||
for graphic in graphics.iter_element_values_mut() {
|
||||
if let Graphic::Graphic(list) = graphic {
|
||||
bake_graphic_paint_transform(list, transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for paint_key in [ATTR_FILL, ATTR_STROKE] {
|
||||
if let Some(Some(graphics)) = attributes.get_mut::<Option<List<Graphic>>>(paint_key) {
|
||||
bake_graphic_paint_transform(graphics, transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod run_tests {
|
||||
use super::*;
|
||||
use crate::graphic::run_to_legacy_list;
|
||||
use crate::graphic::test_support::unit_square_at;
|
||||
use core_types::Color;
|
||||
use core_types::record::{FieldWrite, RunBuilder, RunView, element_write_hashed};
|
||||
use glam::DVec2;
|
||||
|
||||
#[test]
|
||||
fn a_run_serves_the_parked_paint_reference() {
|
||||
let paint = List::new_from_element(Graphic::Color(Color::BLACK));
|
||||
let vector = unit_square_at(DVec2::ZERO);
|
||||
|
||||
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
|
||||
let lane = builder.push(vector.clone()).unwrap();
|
||||
builder.attr::<Fill>(lane, Some(&paint));
|
||||
let item = builder.finish();
|
||||
let run = RunView::<Vector>::new(&item).expect("the run holds vector elements");
|
||||
|
||||
assert_eq!(run.attr::<Fill>(0), Some(&paint));
|
||||
assert_eq!(paint_graphics::<Fill, _>(&run, 0), Some(&paint));
|
||||
assert_eq!(paint_graphics::<Stroke, _>(&run, 0), None);
|
||||
|
||||
let legacy = run_to_legacy_list::<Vector>(&item).expect("the run lowers to a legacy vector list");
|
||||
assert_eq!(paint_graphics::<Fill, _>(&legacy, 0), paint_graphics::<Fill, _>(&run, 0));
|
||||
}
|
||||
}
|
||||
571
node-graph/libraries/graphic-types/src/graphic/walk.rs
Normal file
571
node-graph/libraries/graphic-types/src/graphic/walk.rs
Normal file
@@ -0,0 +1,571 @@
|
||||
use super::Graphic;
|
||||
use super::paint::{LanePaint, PaintColumns, PaintReach, is_paint_present, paint_graphics, set_paint_attribute_at};
|
||||
use crate::markers::{ATTR_FILL, ATTR_STROKE, Fill};
|
||||
use core_types::attribute::{Attribute, ClippingMask, EditorLayerPath, Opacity, OpacityFill, Transform};
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::lane::LaneSource;
|
||||
use core_types::list::{AttributeValueDyn, Item, ItemAttributeValues, List};
|
||||
use core_types::record::FieldOffset;
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use core_types::uuid::NodeId;
|
||||
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use raster_types::{CPU, GPU, Raster};
|
||||
use vector_types::{GradientStops, Vector};
|
||||
|
||||
/// One run's attribute tokens, minted once so the lane loops read at an offset.
|
||||
struct RunAttrs {
|
||||
transform: Option<FieldOffset<Transform>>,
|
||||
opacity: Option<FieldOffset<Opacity>>,
|
||||
opacity_fill: Option<FieldOffset<OpacityFill>>,
|
||||
clipping_mask: Option<FieldOffset<ClippingMask>>,
|
||||
}
|
||||
|
||||
impl RunAttrs {
|
||||
fn of(item: &core_types::record::GroupItem) -> Self {
|
||||
let layout = item.layout();
|
||||
Self {
|
||||
transform: FieldOffset::of(layout, 0),
|
||||
opacity: FieldOffset::of(layout, 0),
|
||||
opacity_fill: FieldOffset::of(layout, 0),
|
||||
clipping_mask: FieldOffset::of(layout, 0),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_or<'i, A: Attribute>(item: &'i core_types::record::GroupItem, field: Option<FieldOffset<A>>, lane: usize, default: A::Value<'i>) -> A::Value<'i> {
|
||||
match field.and_then(|field| item.lanes().get(lane).try_attr_at(field)) {
|
||||
Some(value) => value,
|
||||
None => default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn group_is_empty(group: &core_types::record::Group) -> bool {
|
||||
group.content.is_empty()
|
||||
}
|
||||
|
||||
pub(in crate::graphic) fn group_all_clipped(group: &core_types::record::Group) -> bool {
|
||||
let item = &group.content;
|
||||
let attrs = RunAttrs::of(item);
|
||||
(0..item.len()).all(|lane| RunAttrs::read_or(item, attrs.clipping_mask, lane, false))
|
||||
}
|
||||
|
||||
pub(in crate::graphic) fn group_is_opaque(group: &core_types::record::Group) -> bool {
|
||||
let item = &group.content;
|
||||
let attrs = RunAttrs::of(item);
|
||||
let lanes = item.typed_lanes::<Graphic>();
|
||||
!item.is_empty()
|
||||
&& (0..item.len()).all(|lane| {
|
||||
RunAttrs::read_or(item, attrs.opacity, lane, 1.) >= 1.
|
||||
&& RunAttrs::read_or(item, attrs.opacity_fill, lane, 1.) >= 1.
|
||||
&& lanes.as_ref().is_some_and(|lanes| lanes.element_ref(lane).is_opaque())
|
||||
})
|
||||
}
|
||||
|
||||
pub(in crate::graphic) fn group_is_fully_transparent(group: &core_types::record::Group) -> bool {
|
||||
let item = &group.content;
|
||||
let attrs = RunAttrs::of(item);
|
||||
let lanes = item.typed_lanes::<Graphic>();
|
||||
(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()))
|
||||
}
|
||||
|
||||
pub(in crate::graphic) fn group_bounding_box(group: &core_types::record::Group, transform: DAffine2, include_stroke: bool, thumbnail: bool) -> RenderBoundingBox {
|
||||
fn combine(combined: &mut Option<[DVec2; 2]>, any_infinite: &mut bool, bounds: RenderBoundingBox, thumbnail: bool) -> Option<RenderBoundingBox> {
|
||||
match bounds {
|
||||
RenderBoundingBox::None => None,
|
||||
RenderBoundingBox::Infinite if thumbnail => {
|
||||
*any_infinite = true;
|
||||
None
|
||||
}
|
||||
RenderBoundingBox::Infinite => Some(RenderBoundingBox::Infinite),
|
||||
RenderBoundingBox::Rectangle(bounds) => {
|
||||
*combined = Some(match *combined {
|
||||
Some(existing) => core_types::math::quad::Quad::combine_bounds(existing, bounds),
|
||||
None => bounds,
|
||||
});
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
fn typed_run<T: dyn_any::StaticTypeSized + BoundingBox>(item: &core_types::record::GroupItem, transform: DAffine2, include_stroke: bool, thumbnail: bool) -> Option<RenderBoundingBox> {
|
||||
let lanes = item.typed_lanes::<T>()?;
|
||||
let transform_offset = RunAttrs::of(item).transform;
|
||||
let mut combined = None;
|
||||
let mut any_infinite = false;
|
||||
for lane in 0..lanes.len() {
|
||||
let lane_transform = transform * RunAttrs::read_or(item, transform_offset, lane, DAffine2::IDENTITY);
|
||||
let element = lanes.element_ref(lane);
|
||||
let bounds = match thumbnail {
|
||||
true => element.thumbnail_bounding_box(lane_transform, include_stroke),
|
||||
false => element.bounding_box(lane_transform, include_stroke),
|
||||
};
|
||||
if let Some(short_circuit) = combine(&mut combined, &mut any_infinite, bounds, thumbnail) {
|
||||
return Some(short_circuit);
|
||||
}
|
||||
}
|
||||
Some(match (combined, any_infinite) {
|
||||
(Some(bounds), _) => RenderBoundingBox::Rectangle(bounds),
|
||||
(None, true) => RenderBoundingBox::Infinite,
|
||||
(None, false) => RenderBoundingBox::None,
|
||||
})
|
||||
}
|
||||
fn run_bounding_box(item: &core_types::record::GroupItem, transform: DAffine2, include_stroke: bool, thumbnail: bool) -> RenderBoundingBox {
|
||||
None.or_else(|| typed_run::<Graphic>(item, transform, include_stroke, thumbnail))
|
||||
.or_else(|| typed_run::<Vector>(item, transform, include_stroke, thumbnail))
|
||||
.or_else(|| typed_run::<Raster<CPU>>(item, transform, include_stroke, thumbnail))
|
||||
.or_else(|| typed_run::<Raster<GPU>>(item, transform, include_stroke, thumbnail))
|
||||
.or_else(|| typed_run::<Color>(item, transform, include_stroke, thumbnail))
|
||||
.or_else(|| typed_run::<GradientStops>(item, transform, include_stroke, thumbnail))
|
||||
.or_else(|| typed_run::<String>(item, transform, include_stroke, thumbnail))
|
||||
.unwrap_or(RenderBoundingBox::Infinite)
|
||||
}
|
||||
run_bounding_box(&group.content, transform, include_stroke, thumbnail)
|
||||
}
|
||||
|
||||
/// One step of the vector-row walk: continue to the next row or stop early.
|
||||
pub enum RowStep {
|
||||
Continue,
|
||||
Stop,
|
||||
}
|
||||
|
||||
/// The ancestor composition a flattened row inherits: transform, opacity and
|
||||
/// fill opacity multiply down, each composing only where some ancestor
|
||||
/// carries the attribute, matching the legacy flatten.
|
||||
#[derive(Clone, Copy)]
|
||||
struct FlattenScale {
|
||||
has_transform: bool,
|
||||
transform: DAffine2,
|
||||
has_opacity: bool,
|
||||
opacity: f64,
|
||||
has_fill_opacity: bool,
|
||||
fill_opacity: f64,
|
||||
}
|
||||
|
||||
impl FlattenScale {
|
||||
const ROOT: Self = Self {
|
||||
has_transform: false,
|
||||
transform: DAffine2::IDENTITY,
|
||||
has_opacity: false,
|
||||
opacity: 1.,
|
||||
has_fill_opacity: false,
|
||||
fill_opacity: 1.,
|
||||
};
|
||||
|
||||
fn composed<S: LaneSource>(self, source: &S, lane: usize) -> Self {
|
||||
let transform = source.try_attr::<Transform>(lane);
|
||||
let opacity = source.try_attr::<Opacity>(lane);
|
||||
let fill_opacity = source.try_attr::<OpacityFill>(lane);
|
||||
Self {
|
||||
has_transform: self.has_transform || transform.is_some(),
|
||||
transform: self.transform * transform.unwrap_or(DAffine2::IDENTITY),
|
||||
has_opacity: self.has_opacity || opacity.is_some(),
|
||||
opacity: self.opacity * opacity.unwrap_or(1.),
|
||||
has_fill_opacity: self.has_fill_opacity || fill_opacity.is_some(),
|
||||
fill_opacity: self.fill_opacity * fill_opacity.unwrap_or(1.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A graphic level in either of its two storages, as one lane source.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum GraphicLevel<'a> {
|
||||
Legacy(&'a List<Graphic<'a>>),
|
||||
Run(&'a core_types::record::GroupItem<'a>),
|
||||
}
|
||||
|
||||
pub enum GraphicLevelColumn<'a, A: Attribute> {
|
||||
Legacy(core_types::list::ListColumn<'a, A>),
|
||||
Run(core_types::record::RunColumn<'a, A>),
|
||||
}
|
||||
|
||||
impl<'a, A: Attribute> core_types::lane::LaneColumn<'a, A> for GraphicLevelColumn<'a, A> {
|
||||
fn try_get(&self, lane: usize) -> Option<A::Value<'a>> {
|
||||
match self {
|
||||
GraphicLevelColumn::Legacy(column) => column.try_get(lane),
|
||||
GraphicLevelColumn::Run(column) => column.try_get(lane),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> LaneSource for GraphicLevel<'a> {
|
||||
type Element = Graphic<'a>;
|
||||
type Column<'b, A: Attribute>
|
||||
= GraphicLevelColumn<'b, A>
|
||||
where
|
||||
Self: 'b;
|
||||
|
||||
fn lane_count(&self) -> usize {
|
||||
match self {
|
||||
GraphicLevel::Legacy(list) => list.len(),
|
||||
GraphicLevel::Run(item) => item.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn element(&self, lane: usize) -> Option<&Graphic<'a>> {
|
||||
match self {
|
||||
GraphicLevel::Legacy(list) => list.element(lane),
|
||||
GraphicLevel::Run(item) => {
|
||||
let lanes = item.typed_lanes::<Graphic>()?;
|
||||
if lane >= lanes.len() {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: the layout records the element type, and a parked
|
||||
// element stores its reference at offset 0.
|
||||
Some(unsafe { core_types::record::borrow_element::<Graphic>(item.lanes().get(lane).rec()) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn column<A: Attribute>(&self) -> GraphicLevelColumn<'_, A> {
|
||||
match self {
|
||||
GraphicLevel::Legacy(list) => GraphicLevelColumn::Legacy(list.column::<A>()),
|
||||
GraphicLevel::Run(item) => GraphicLevelColumn::Run(core_types::record::RunColumn::of(item)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The lane's attributes as an owned set, read through the erased glue.
|
||||
pub fn run_lane_attributes(item: &core_types::record::GroupItem, lane: usize) -> ItemAttributeValues {
|
||||
let mut scratch: List<Vector> = List::new_from_element(Vector::default());
|
||||
for field in &item.layout().fields {
|
||||
// SAFETY: the offset comes from the item's own layout.
|
||||
let value = unsafe { (field.read_erased)(item.lanes().get(lane).rec().ptr().add(field.offset)) };
|
||||
scratch.set_attribute_value_dyn(field.name, 0, AttributeValueDyn(value));
|
||||
}
|
||||
scratch.clone_item_attributes(0)
|
||||
}
|
||||
|
||||
/// The lane's attributes as an owned set, from either level storage.
|
||||
pub fn lane_attributes(level: GraphicLevel<'_>, lane: usize) -> ItemAttributeValues {
|
||||
match level {
|
||||
GraphicLevel::Legacy(list) => list.clone_item_attributes(lane),
|
||||
GraphicLevel::Run(item) => run_lane_attributes(item, lane),
|
||||
}
|
||||
}
|
||||
|
||||
/// One flattened vector row served by [`walk_vector_rows`]: cheap probes
|
||||
/// first, the full row built on demand.
|
||||
pub struct VectorRow<'w> {
|
||||
source: RowSourceRef<'w>,
|
||||
scale: FlattenScale,
|
||||
layer_path: Option<&'w [NodeId]>,
|
||||
paint: LanePaint<'w>,
|
||||
}
|
||||
|
||||
enum RowSourceRef<'w> {
|
||||
/// A de-tabled vector leaf on a graphic lane: the lane is the row.
|
||||
Lane(GraphicLevel<'w>, usize),
|
||||
/// A lane of a vector run.
|
||||
Run(&'w core_types::record::RunView<'w, Vector>, &'w core_types::record::GroupItem<'w>, usize),
|
||||
}
|
||||
|
||||
impl VectorRow<'_> {
|
||||
/// The row's vector, borrowed.
|
||||
pub fn element(&self) -> &Vector {
|
||||
match &self.source {
|
||||
RowSourceRef::Lane(level, index) => level.element(*index).and_then(Graphic::as_vector).expect("the walk visits vector lanes"),
|
||||
RowSourceRef::Run(run, _, index) => LaneSource::element(*run, *index).expect("the walk visits held lanes"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the built row will carry fill paint: the reaching lane paint,
|
||||
/// else the row's own.
|
||||
pub fn has_fill(&self) -> bool {
|
||||
if self.paint.fill.is_some() {
|
||||
return true;
|
||||
}
|
||||
match &self.source {
|
||||
RowSourceRef::Lane(level, index) => paint_graphics::<Fill, _>(level, *index).is_some(),
|
||||
RowSourceRef::Run(run, _, index) => paint_graphics::<Fill, _>(*run, *index).is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the row at the end of `out`, applying the reach paint and the
|
||||
/// inherited composition.
|
||||
pub fn build_into(&self, out: &mut List<Vector>) {
|
||||
let index = out.len();
|
||||
match &self.source {
|
||||
RowSourceRef::Lane(level, lane) => {
|
||||
let vector = self.element().clone();
|
||||
out.push(Item::from_parts(vector, lane_attributes(*level, *lane)));
|
||||
}
|
||||
RowSourceRef::Run(run, item, lane) => {
|
||||
let vector = LaneSource::element(*run, *lane).expect("the walk visits held lanes").clone();
|
||||
out.push(Item::from_parts(vector, run_lane_attributes(item, *lane)));
|
||||
}
|
||||
}
|
||||
for (key, slot) in [(ATTR_FILL, self.paint.fill), (ATTR_STROKE, self.paint.stroke)] {
|
||||
if let Some(paint) = slot {
|
||||
set_paint_attribute_at(out, index, key, paint.clone());
|
||||
}
|
||||
}
|
||||
if self.scale.has_transform || out.attribute::<DAffine2>(ATTR_TRANSFORM, index).is_some() {
|
||||
let row_transform: DAffine2 = out.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||
out.set_attribute(ATTR_TRANSFORM, index, self.scale.transform * row_transform);
|
||||
}
|
||||
if self.scale.has_opacity || out.attribute::<f64>(ATTR_OPACITY, index).is_some() {
|
||||
let row_opacity: f64 = out.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
||||
out.set_attribute(ATTR_OPACITY, index, self.scale.opacity * row_opacity);
|
||||
}
|
||||
if self.scale.has_fill_opacity || out.attribute::<f64>(ATTR_OPACITY_FILL, index).is_some() {
|
||||
let row_fill: f64 = out.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
||||
out.set_attribute(ATTR_OPACITY_FILL, index, self.scale.fill_opacity * row_fill);
|
||||
}
|
||||
if let Some(layer_path) = self.layer_path {
|
||||
out.set_attribute(ATTR_EDITOR_LAYER_PATH, index, layer_path.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_rows_of_run(item: &core_types::record::GroupItem, scale: FlattenScale, layer_path: Option<&[NodeId]>, paint: LanePaint<'_>, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep) -> RowStep {
|
||||
let Some(run) = core_types::record::RunView::<Vector>::new(item) else {
|
||||
return RowStep::Continue;
|
||||
};
|
||||
for lane in 0..item.len() {
|
||||
if let RowStep::Stop = visit(VectorRow {
|
||||
source: RowSourceRef::Run(&run, item, lane),
|
||||
scale,
|
||||
layer_path,
|
||||
paint,
|
||||
}) {
|
||||
return RowStep::Stop;
|
||||
}
|
||||
}
|
||||
RowStep::Continue
|
||||
}
|
||||
|
||||
/// Walks a graphic level into its flattened vector rows, matching the legacy
|
||||
/// push-then-flatten lowering: lane paint threads with [`PaintReach`],
|
||||
/// ancestor transform, opacity and fill opacity compose down, the containing
|
||||
/// level's parent layer path overwrites its rows, and non-vector content is
|
||||
/// discarded. A de-tabled leaf's row is its lane, attributes included.
|
||||
pub fn walk_vector_rows(level: GraphicLevel<'_>, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep) {
|
||||
walk_vector_rows_impl(level, FlattenScale::ROOT, None, PaintReach::NONE, visit);
|
||||
}
|
||||
|
||||
fn walk_vector_rows_impl<'a>(
|
||||
level: GraphicLevel<'a>,
|
||||
scale: FlattenScale,
|
||||
parent_layer_path: Option<&'a [NodeId]>,
|
||||
inherited: PaintReach<'a>,
|
||||
visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep,
|
||||
) -> RowStep {
|
||||
if let GraphicLevel::Run(item) = level {
|
||||
// A vector-typed run is already its rows.
|
||||
if item.typed_lanes::<Vector>().is_some() {
|
||||
let paint = match inherited.applies() {
|
||||
true => inherited.paint,
|
||||
false => LanePaint::NONE,
|
||||
};
|
||||
return walk_rows_of_run(item, scale, parent_layer_path, paint, visit);
|
||||
}
|
||||
}
|
||||
let columns = PaintColumns::new(&level);
|
||||
for index in 0..level.lane_count() {
|
||||
let Some(element) = level.element(index) else { continue };
|
||||
let reach = inherited.for_lane(&columns, index);
|
||||
let row_paint = match reach.applies() {
|
||||
true => reach.paint,
|
||||
false => LanePaint::NONE,
|
||||
};
|
||||
let step = match element {
|
||||
Graphic::Vector(_) => visit(VectorRow {
|
||||
source: RowSourceRef::Lane(level, index),
|
||||
scale,
|
||||
layer_path: parent_layer_path,
|
||||
paint: row_paint,
|
||||
}),
|
||||
Graphic::Graphic(children) => walk_vector_rows_impl(
|
||||
GraphicLevel::Legacy(children),
|
||||
scale.composed(&level, index),
|
||||
level.try_attr::<EditorLayerPath>(index),
|
||||
reach.nested(),
|
||||
visit,
|
||||
),
|
||||
Graphic::Group(group) => {
|
||||
let item = &group.content;
|
||||
if item.typed_lanes::<Vector>().is_some() {
|
||||
walk_rows_of_run(item, scale.composed(&level, index), level.try_attr::<EditorLayerPath>(index), row_paint, visit)
|
||||
} else if item.typed_lanes::<Graphic>().is_some() {
|
||||
walk_vector_rows_impl(
|
||||
GraphicLevel::Run(item),
|
||||
scale.composed(&level, index),
|
||||
level.try_attr::<EditorLayerPath>(index),
|
||||
reach.into_group_graphics(),
|
||||
visit,
|
||||
)
|
||||
} else {
|
||||
RowStep::Continue
|
||||
}
|
||||
}
|
||||
_ => RowStep::Continue,
|
||||
};
|
||||
if let RowStep::Stop = step {
|
||||
return RowStep::Stop;
|
||||
}
|
||||
}
|
||||
RowStep::Continue
|
||||
}
|
||||
|
||||
/// The level's flattened vector rows as one owned list, the walk's collect
|
||||
/// form.
|
||||
pub fn flatten_vector_rows(level: GraphicLevel<'_>) -> List<Vector> {
|
||||
let mut out = List::new();
|
||||
walk_vector_rows(level, &mut |row| {
|
||||
row.build_into(&mut out);
|
||||
RowStep::Continue
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// The transitional paint placement: a lane-level fill or stroke paint
|
||||
/// attribute moves onto the vector interiors the legacy paint readers
|
||||
/// inspect, reaching as far as the pre-flip broadcast did.
|
||||
pub(in crate::graphic) fn push_lane_paint_into_interiors(list: &mut List<Graphic>) {
|
||||
for index in 0..list.len() {
|
||||
for key in [ATTR_FILL, ATTR_STROKE] {
|
||||
let stored = list.attribute::<Option<List<Graphic>>>(key, index).and_then(|optional| optional.as_ref());
|
||||
let Some(paint) = stored.filter(|paint| is_paint_present(paint)).cloned() else {
|
||||
continue;
|
||||
};
|
||||
let Some(Graphic::Graphic(children)) = list.element_mut(index) else { continue };
|
||||
for child in 0..children.len() {
|
||||
if matches!(children.element(child), Some(Graphic::Vector(_))) {
|
||||
set_paint_attribute_at(children, child, key, paint.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(_) => 1,
|
||||
Graphic::Group(group) => match &group.row {
|
||||
None => group.content.typed_lanes::<Vector>().map_or(0, |lanes| lanes.len()),
|
||||
_ => 0,
|
||||
},
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::graphic) fn group_render_complexity(group: &core_types::record::Group) -> usize {
|
||||
fn typed_run<T: dyn_any::StaticTypeSized + RenderComplexity>(item: &core_types::record::GroupItem) -> Option<usize> {
|
||||
let lanes = item.typed_lanes::<T>()?;
|
||||
Some((0..lanes.len()).map(|lane| lanes.element_ref(lane).render_complexity()).sum())
|
||||
}
|
||||
let item = &group.content;
|
||||
None.or_else(|| typed_run::<Graphic>(item))
|
||||
.or_else(|| typed_run::<Vector>(item))
|
||||
.or_else(|| typed_run::<Raster<CPU>>(item))
|
||||
.or_else(|| typed_run::<Raster<GPU>>(item))
|
||||
.or_else(|| typed_run::<Color>(item))
|
||||
.or_else(|| typed_run::<GradientStops>(item))
|
||||
.or_else(|| typed_run::<String>(item))
|
||||
.unwrap_or(item.len())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod run_tests {
|
||||
use super::*;
|
||||
use crate::graphic::test_support::unit_square_at;
|
||||
use crate::graphic::{IntoGraphicList, map_groups_to_legacy, run_to_legacy_list};
|
||||
use core_types::record::{FieldWrite, RunBuilder, RunView, element_write_hashed};
|
||||
|
||||
#[test]
|
||||
fn the_vector_row_walk_matches_the_legacy_flatten() {
|
||||
let inner_vector = unit_square_at(DVec2::ZERO);
|
||||
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[], 1).unwrap();
|
||||
builder.push(inner_vector.clone()).unwrap();
|
||||
let inner_item = builder.finish();
|
||||
|
||||
let mut painted = List::new();
|
||||
painted.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::ZERO))));
|
||||
painted.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::ONE))));
|
||||
painted.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(1., 0.)));
|
||||
painted.set_attribute(core_types::ATTR_TRANSFORM, 1, DAffine2::from_translation(DVec2::new(0., 1.)));
|
||||
set_paint_attribute_at(&mut painted, 1, ATTR_FILL, List::new_from_element(Graphic::Color(Color::WHITE)));
|
||||
|
||||
let mut nested = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(2., 2.))));
|
||||
nested.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_scale(DVec2::splat(2.)));
|
||||
|
||||
let mut top = List::new();
|
||||
top.push(Item::new_from_element(Graphic::Graphic(painted)));
|
||||
top.push(Item::new_from_element(Graphic::Graphic(nested)));
|
||||
top.push(Item::new_from_element(Graphic::Group(core_types::record::Group { row: None, content: inner_item })));
|
||||
top.push(Item::new_from_element(Graphic::Color(Color::BLACK)));
|
||||
top.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(6., 0.)))));
|
||||
top.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(5., 5.)));
|
||||
top.set_attribute(core_types::ATTR_EDITOR_LAYER_PATH, 0, vec![core_types::uuid::NodeId(7)]);
|
||||
set_paint_attribute_at(&mut top, 0, ATTR_FILL, List::new_from_element(Graphic::Color(Color::BLACK)));
|
||||
top.set_attribute(core_types::ATTR_OPACITY, 1, 0.5);
|
||||
top.set_attribute(core_types::ATTR_TRANSFORM, 2, DAffine2::from_scale(DVec2::splat(3.)));
|
||||
top.set_attribute(core_types::ATTR_TRANSFORM, 4, DAffine2::from_translation(DVec2::new(0., 7.)));
|
||||
top.set_attribute(core_types::ATTR_EDITOR_LAYER_PATH, 4, vec![core_types::uuid::NodeId(9)]);
|
||||
set_paint_attribute_at(&mut top, 4, ATTR_FILL, List::new_from_element(Graphic::Color(Color::WHITE)));
|
||||
|
||||
let legacy = {
|
||||
let mut list = List::new();
|
||||
for item in top.clone().into_iter() {
|
||||
let (element, attributes) = item.into_parts();
|
||||
list.push(Item::from_parts(map_groups_to_legacy(&element), attributes));
|
||||
}
|
||||
push_lane_paint_into_interiors(&mut list);
|
||||
list.into_flattened_list::<Vector>()
|
||||
};
|
||||
let native = flatten_vector_rows(GraphicLevel::Legacy(&top));
|
||||
assert_eq!(native.len(), legacy.len());
|
||||
for row in 0..native.len() {
|
||||
assert_eq!(
|
||||
native.attribute::<DAffine2>(core_types::ATTR_TRANSFORM, row),
|
||||
legacy.attribute::<DAffine2>(core_types::ATTR_TRANSFORM, row),
|
||||
"transform, row {row}"
|
||||
);
|
||||
assert_eq!(
|
||||
native.attribute::<f64>(core_types::ATTR_OPACITY, row),
|
||||
legacy.attribute::<f64>(core_types::ATTR_OPACITY, row),
|
||||
"opacity, row {row}"
|
||||
);
|
||||
assert_eq!(
|
||||
native.attribute::<Vec<core_types::uuid::NodeId>>(core_types::ATTR_EDITOR_LAYER_PATH, row),
|
||||
legacy.attribute::<Vec<core_types::uuid::NodeId>>(core_types::ATTR_EDITOR_LAYER_PATH, row),
|
||||
"layer path, row {row}"
|
||||
);
|
||||
assert_eq!(
|
||||
native.attribute::<Option<List<Graphic>>>(ATTR_FILL, row),
|
||||
legacy.attribute::<Option<List<Graphic>>>(ATTR_FILL, row),
|
||||
"fill, row {row}"
|
||||
);
|
||||
}
|
||||
assert_eq!(native, legacy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_run_and_its_legacy_list_agree_on_bounding_boxes() {
|
||||
let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(4., 4.))];
|
||||
let transforms = [DAffine2::from_translation(DVec2::new(1., 2.)), DAffine2::from_scale(DVec2::splat(3.))];
|
||||
|
||||
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<core_types::attribute::Transform>(0)], 2).unwrap();
|
||||
for lane in 0..2 {
|
||||
let lane = builder.push(vectors[lane].clone()).unwrap();
|
||||
builder.attr::<core_types::attribute::Transform>(lane, transforms[lane]);
|
||||
}
|
||||
let item = builder.finish();
|
||||
let run = RunView::<Vector>::new(&item).expect("the run holds vector elements");
|
||||
let legacy = run_to_legacy_list::<Vector>(&item).expect("the run lowers to a legacy vector list");
|
||||
|
||||
let outer = DAffine2::from_angle(0.3);
|
||||
for include_stroke in [false, true] {
|
||||
let bounds = run.bounding_box(outer, include_stroke);
|
||||
assert_eq!(bounds, legacy.bounding_box(outer, include_stroke));
|
||||
assert!(matches!(bounds, RenderBoundingBox::Rectangle(_)));
|
||||
assert_eq!(run.thumbnail_bounding_box(outer, include_stroke), legacy.thumbnail_bounding_box(outer, include_stroke));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user