Add the group variant to Graphic with typed run walks

This commit is contained in:
Dennis Kobert
2026-08-21 19:34:03 +00:00
parent da8ae2be43
commit b5c4aa4598
5 changed files with 193 additions and 4 deletions

View File

@@ -336,6 +336,7 @@ impl TableItemLayout for Graphic {
Self::Color(list) => list.identifier(),
Self::Gradient(list) => list.identifier(),
Self::Text(list) => list.identifier(),
Self::Group(_) => "Group".to_string(),
}
}
// Don't put a breadcrumb for Graphic
@@ -351,6 +352,7 @@ impl TableItemLayout for Graphic {
Self::Color(list) => list.layout_with_breadcrumb(data),
Self::Gradient(list) => list.layout_with_breadcrumb(data),
Self::Text(list) => list.layout_with_breadcrumb(data),
Self::Group(_) => Vec::new(),
}
}
}

View File

@@ -22,6 +22,7 @@ pub enum Graphic {
Color(List<Color>),
Gradient(List<GradientStops>),
Text(List<String>),
Group(core_types::record::Group),
}
impl Default for Graphic {
@@ -238,7 +239,7 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA
Graphic::RasterGPU(list) => bake_list_transform(list, transform),
Graphic::Gradient(list) => bake_list_transform(list, transform),
Graphic::Text(list) => bake_list_transform(list, transform),
Graphic::Color(_) => {}
Graphic::Color(_) | Graphic::Group(_) => {}
}
}
}
@@ -430,6 +431,7 @@ impl Graphic {
Graphic::Color(list) => all_clipped(list),
Graphic::Gradient(list) => all_clipped(list),
Graphic::Text(list) => all_clipped(list),
Graphic::Group(group) => group_all_clipped(group),
}
}
@@ -469,6 +471,7 @@ impl Graphic {
Graphic::Color(list) => list.element(0).is_some_and(|color| color.is_opaque()),
Graphic::Gradient(list) => list.element(0).is_some_and(|stops| stops.iter().all(|stop| stop.color.is_opaque())),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false,
Graphic::Group(group) => group_is_opaque(group),
}
}
@@ -492,6 +495,7 @@ impl Graphic {
Graphic::Color(list) => list.iter_element_values().all(|color| color.a() == 0.),
Graphic::Gradient(list) => list.iter_element_values().all(|stops| stops.iter().all(|stop| stop.color.a() == 0.)),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false,
Graphic::Group(group) => group_is_fully_transparent(group),
}
}
@@ -511,10 +515,181 @@ impl Graphic {
Graphic::RasterCPU(list) => list.is_empty(),
Graphic::RasterGPU(list) => list.is_empty(),
Graphic::Text(list) => list.is_empty(),
Graphic::Group(group) => group_is_empty(group),
}
}
}
/// One run's attribute offsets, resolved once so the lane loops read raw.
struct RunAttrs {
transform: Option<usize>,
opacity: Option<usize>,
opacity_fill: Option<usize>,
clipping_mask: Option<usize>,
}
impl RunAttrs {
fn of(item: &core_types::record::GroupItem) -> Self {
let layout = item.layout();
Self {
transform: layout.offset_of(ATTR_TRANSFORM, 0),
opacity: layout.offset_of(ATTR_OPACITY, 0),
opacity_fill: layout.offset_of(ATTR_OPACITY_FILL, 0),
clipping_mask: layout.offset_of(ATTR_CLIPPING_MASK, 0),
}
}
fn read_or<T: Copy>(item: &core_types::record::GroupItem, offset: Option<usize>, lane: usize, default: T) -> T {
match offset {
// SAFETY: the offset comes from the item's own layout.
Some(offset) => unsafe { item.lanes().get(lane).rec().read(offset) },
None => default,
}
}
}
fn group_row_transform(group: &core_types::record::Group) -> DAffine2 {
match &group.row {
Some(row) if !row.is_empty() => RunAttrs::read_or(row, RunAttrs::of(row).transform, 0, DAffine2::IDENTITY),
_ => DAffine2::IDENTITY,
}
}
pub fn group_is_empty(group: &core_types::record::Group) -> bool {
match &group.content {
core_types::record::GroupContent::Run(item) => item.is_empty(),
core_types::record::GroupContent::Stack(children) => children.iter().all(group_is_empty),
}
}
fn group_all_clipped(group: &core_types::record::Group) -> bool {
match &group.content {
core_types::record::GroupContent::Run(item) => {
let attrs = RunAttrs::of(item);
(0..item.len()).all(|lane| RunAttrs::read_or(item, attrs.clipping_mask, lane, false))
}
core_types::record::GroupContent::Stack(children) => children.iter().all(group_all_clipped),
}
}
fn group_is_opaque(group: &core_types::record::Group) -> bool {
match &group.content {
core_types::record::GroupContent::Run(item) => {
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())
})
}
core_types::record::GroupContent::Stack(children) => !children.is_empty() && children.iter().all(group_is_opaque),
}
}
fn group_is_fully_transparent(group: &core_types::record::Group) -> bool {
match &group.content {
core_types::record::GroupContent::Run(item) => {
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())
})
}
core_types::record::GroupContent::Stack(children) => children.iter().all(group_is_fully_transparent),
}
}
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: 'static + 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)
}
match &group.content {
core_types::record::GroupContent::Run(item) => run_bounding_box(item, transform, include_stroke, thumbnail),
core_types::record::GroupContent::Stack(children) => {
let mut combined = None;
let mut any_infinite = false;
for child in children {
let bounds = group_bounding_box(child, transform * group_row_transform(child), include_stroke, thumbnail);
if let Some(short_circuit) = combine(&mut combined, &mut any_infinite, bounds, thumbnail) {
return short_circuit;
}
}
match (combined, any_infinite) {
(Some(bounds), _) => RenderBoundingBox::Rectangle(bounds),
(None, true) => RenderBoundingBox::Infinite,
(None, false) => RenderBoundingBox::None,
}
}
}
}
fn group_render_complexity(group: &core_types::record::Group) -> usize {
fn typed_run<T: 'static + 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())
}
match &group.content {
core_types::record::GroupContent::Run(item) => 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()),
core_types::record::GroupContent::Stack(children) => children.iter().map(group_render_complexity).sum(),
}
}
impl BoundingBox for Graphic {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
match self {
@@ -525,6 +700,7 @@ impl BoundingBox for Graphic {
Graphic::Color(list) => list.bounding_box(transform, include_stroke),
Graphic::Gradient(list) => list.bounding_box(transform, include_stroke),
Graphic::Text(list) => list.bounding_box(transform, include_stroke),
Graphic::Group(group) => group_bounding_box(group, transform, include_stroke, false),
}
}
@@ -537,6 +713,7 @@ impl BoundingBox for Graphic {
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),
}
}
}
@@ -567,6 +744,7 @@ impl RenderComplexity for Graphic {
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),
}
}
}

View File

@@ -241,7 +241,7 @@ impl RenderExt for List<Graphic> {
let gradient_id = gradient_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, render_params, target);
format!(r##" {paint_attr}="url(#{gradient_id})""##)
}
Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) | Some(Graphic::Text(_)) => {
Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) | Some(Graphic::Text(_)) | Some(Graphic::Group(_)) => {
let bounds = if target == PaintTarget::Stroke {
// To prevent a wraparound artefact occurring when the tile boundary and the stroke region are perfectly aligned, the local coordinate is expanded slightly.
let inverse = |len: f64| if len > 0. { 1. / len } else { 0. };

View File

@@ -553,6 +553,7 @@ impl Render for Graphic {
Graphic::Color(list) => list.render_svg(render, render_params),
Graphic::Gradient(list) => list.render_svg(render, render_params),
Graphic::Text(list) => list.render_svg(render, render_params),
Graphic::Group(_) => (),
}
}
@@ -565,12 +566,14 @@ impl Render for Graphic {
Graphic::Color(list) => list.render_to_vello(scene, transform, context, render_params),
Graphic::Gradient(list) => list.render_to_vello(scene, transform, context, render_params),
Graphic::Text(list) => list.render_to_vello(scene, transform, context, render_params),
Graphic::Group(_) => (),
}
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
if let Some(element_id) = element_id {
match self {
Graphic::Group(_) => {}
Graphic::Graphic(_) => {
metadata.upstream_footprints.insert(element_id, footprint);
}
@@ -637,6 +640,7 @@ impl Render for Graphic {
Graphic::Color(list) => list.collect_metadata(metadata, footprint, element_id),
Graphic::Gradient(list) => list.collect_metadata(metadata, footprint, element_id),
Graphic::Text(list) => list.collect_metadata(metadata, footprint, element_id),
Graphic::Group(_) => (),
}
}
@@ -649,6 +653,7 @@ impl Render for Graphic {
Graphic::Color(list) => list.add_upstream_click_targets(click_targets),
Graphic::Gradient(list) => list.add_upstream_click_targets(click_targets),
Graphic::Text(list) => list.add_upstream_click_targets(click_targets),
Graphic::Group(_) => (),
}
}
@@ -661,6 +666,7 @@ impl Render for Graphic {
Graphic::Color(list) => list.add_upstream_outline_targets(outlines),
Graphic::Gradient(list) => list.add_upstream_outline_targets(outlines),
Graphic::Text(list) => list.add_upstream_outline_targets(outlines),
Graphic::Group(_) => (),
}
}
@@ -673,6 +679,7 @@ impl Render for Graphic {
Graphic::Color(list) => list.contains_artboard(),
Graphic::Gradient(list) => list.contains_artboard(),
Graphic::Text(list) => list.contains_artboard(),
Graphic::Group(_) => false,
}
}
@@ -685,6 +692,7 @@ impl Render for Graphic {
Graphic::Color(_) => (),
Graphic::Gradient(_) => (),
Graphic::Text(_) => (),
Graphic::Group(_) => (),
}
}
}
@@ -1367,7 +1375,7 @@ impl Render for List<Vector> {
let brush_transform = kurbo::Affine::new((inverse_element_transform * gradient_to_device).to_cols_array());
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &brush, Some(brush_transform), path);
}
Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) | Graphic::Text(_) => {
Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) | Graphic::Text(_) | Graphic::Group(_) => {
scene.push_clip_layer(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), path);
paint.render_to_vello(scene, multiplied_transform, context, render_params);
scene.pop_layer();
@@ -1449,7 +1457,7 @@ impl Render for List<Vector> {
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, Some(brush_transform), &path);
}
Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) | Graphic::Text(_) => {
Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) | Graphic::Text(_) | Graphic::Group(_) => {
let stroked = peniko::kurbo::stroke(path.iter(), &stroke, &StrokeOpts::default(), 0.01);
scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &stroked);

View File

@@ -183,6 +183,7 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
.flat_map(|index| {
let graphic = graphic_list.element(index).unwrap();
match graphic.clone() {
Graphic::Group(_) => Vec::new(),
Graphic::Vector(vector) => {
// Apply the parent graphic's transform to each element of the `List<Vector>`
let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index);