Store paint attributes in the paint markers' owned optional form

This commit is contained in:
Dennis Kobert
2026-08-26 14:02:01 +00:00
parent 182c2a1554
commit eb11eec5bb
5 changed files with 45 additions and 34 deletions

View File

@@ -206,13 +206,9 @@ pub fn graphic_list_at<'a>(list: &'a List<Vector>, index: usize, attribute: &str
.filter(|graphic_list| is_paint_present(graphic_list))
}
/// The paint attribute's list, in either transitional storage form: the
/// canonical `List<Graphic>` value, or the fill marker's owned
/// `Option<List<Graphic>>` clone as the render bridge copies it.
/// The paint attribute's list. Storage is the paint marker's owned
/// `Option<List<Graphic>>` form, which every writer produces.
fn paint_at<'a, T>(list: &'a List<T>, index: usize, attribute: &str) -> Option<&'a List<Graphic>> {
if let Some(graphic_list) = list.attribute::<List<Graphic>>(attribute, index) {
return Some(graphic_list);
}
list.attribute::<Option<List<Graphic>>>(attribute, index).and_then(|optional| optional.as_ref())
}
@@ -222,14 +218,14 @@ pub fn has_paint_at(list: &List<Vector>, index: usize, attribute: &str) -> bool
paint_at(list, index, attribute).is_some_and(is_paint_present)
}
/// Stores a paint attribute in its canonical `List<Graphic>` form, the only representation paint readers accept.
/// 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, paint.into_graphic_list());
attributes.insert(key, Some(paint.into_graphic_list()));
}
/// Stores a paint attribute at a list index in its canonical `List<Graphic>` form, the only representation paint readers accept.
/// 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, paint.into_graphic_list());
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
@@ -256,9 +252,7 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA
}
for paint_key in [ATTR_FILL, ATTR_STROKE] {
if let Some(graphics) = attributes.get_mut::<List<Graphic>>(paint_key) {
bake_graphic_paint_transform(graphics, transform);
} else if let Some(Some(graphics)) = attributes.get_mut::<Option<List<Graphic>>>(paint_key) {
if let Some(Some(graphics)) = attributes.get_mut::<Option<List<Graphic>>>(paint_key) {
bake_graphic_paint_transform(graphics, transform);
}
}

View File

@@ -1626,7 +1626,7 @@ impl Render for List<Vector> {
// If this item carries a snapshot of upstream graphic content (e.g. it was produced by Boolean Operation,
// Flatten Path, Morph, or any other destructive merge), recurse into that snapshot so the editor can
// surface the original child layers' click targets.
let upstream_nested_layers = self.attribute_cloned_or_default::<List<Graphic>>(ATTR_EDITOR_MERGED_LAYERS, index);
let upstream_nested_layers = self.attribute_cloned_or_default::<Option<List<Graphic>>>(ATTR_EDITOR_MERGED_LAYERS, index).unwrap_or_default();
if !upstream_nested_layers.is_empty() {
let mut upstream_footprint = footprint;
upstream_footprint.transform *= transform;
@@ -1884,7 +1884,7 @@ impl Render for List<Raster<CPU>> {
// The snapshot was captured before Rasterize shifted its input transforms to align with the rasterization
// area, so the children are already in the coordinate space matching `footprint` here — we must NOT
// multiply in `transform` (which is the rasterization area, not a layer-stack transform).
let upstream_nested_layers = self.attribute_cloned_or_default::<List<Graphic>>(ATTR_EDITOR_MERGED_LAYERS, 0);
let upstream_nested_layers = self.attribute_cloned_or_default::<Option<List<Graphic>>>(ATTR_EDITOR_MERGED_LAYERS, 0).unwrap_or_default();
if !upstream_nested_layers.is_empty() {
upstream_nested_layers.collect_metadata(metadata, footprint, None);
}
@@ -1979,7 +1979,7 @@ impl Render for List<Raster<GPU>> {
// The snapshot was captured before Rasterize shifted its input transforms to align with the rasterization
// area, so the children are already in the coordinate space matching `footprint` here — we must NOT
// multiply in `transform` (which is the rasterization area, not a layer-stack transform).
let upstream_nested_layers = self.attribute_cloned_or_default::<List<Graphic>>(ATTR_EDITOR_MERGED_LAYERS, 0);
let upstream_nested_layers = self.attribute_cloned_or_default::<Option<List<Graphic>>>(ATTR_EDITOR_MERGED_LAYERS, 0).unwrap_or_default();
if !upstream_nested_layers.is_empty() {
upstream_nested_layers.collect_metadata(metadata, footprint, None);
}

View File

@@ -224,8 +224,8 @@ where
if mirrored {
transform = reflected_transform.expect("a mirrored lane exists only under a reflection") * transform;
}
let fill = park_paint(legacy.attribute::<List<Graphic>>(graphic_types::ATTR_FILL, source).cloned())?;
let stroke = park_paint(legacy.attribute::<List<Graphic>>(graphic_types::ATTR_STROKE, source).cloned())?;
let fill = park_paint(legacy.attribute::<Option<List<Graphic>>>(graphic_types::ATTR_FILL, source).cloned().flatten())?;
let stroke = park_paint(legacy.attribute::<Option<List<Graphic>>>(graphic_types::ATTR_STROKE, source).cloned().flatten())?;
let layer_path: Vec<NodeId> = legacy.attribute::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH, source).map(|path| path.clone()).unwrap_or_default();
let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0;
@@ -624,7 +624,7 @@ pub fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Gr
}
}
output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list);
output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, Some(graphic_list));
}
output

View File

@@ -267,7 +267,7 @@ where
List::new_from_item(
Item::new_from_element(Raster::new_cpu(image))
.with_attribute(ATTR_TRANSFORM, footprint.transform)
.with_attribute(ATTR_EDITOR_MERGED_LAYERS, upstream_graphic_list),
.with_attribute(ATTR_EDITOR_MERGED_LAYERS, Some(upstream_graphic_list)),
)
}

View File

@@ -1427,13 +1427,13 @@ fn solidify_stroke_core(graphic_list: List<Graphic>) -> List<Vector> {
vector.stroke = None;
let mut fill_attributes = attributes.clone();
// No stroke remains on the fill row
fill_attributes.remove::<List<Graphic>>(ATTR_STROKE);
fill_attributes.remove::<Option<List<Graphic>>>(ATTR_STROKE);
Item::from_parts(vector, fill_attributes)
});
let mut stroke_attributes = attributes;
// Drop the original fill and use the stroke paint to fill the outlined stroke
stroke_attributes.remove::<List<Graphic>>(ATTR_FILL);
stroke_attributes.remove::<Option<List<Graphic>>>(ATTR_FILL);
stroke_attributes.rename(ATTR_STROKE, ATTR_FILL);
let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes);
@@ -1461,7 +1461,7 @@ fn solidify_stroke_core(graphic_list: List<Graphic>) -> List<Vector> {
}
}
output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list);
output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, Some(graphic_list));
}
output
@@ -1523,12 +1523,21 @@ fn emit_legacy_lane<'e>(
};
let element = output.element(lane).cloned().unwrap_or_default();
let fill = output.attribute::<List<Graphic>>(ATTR_FILL, lane).map(|paint| park_paint(arena, paint.clone())).transpose()?;
let stroke = output.attribute::<List<Graphic>>(ATTR_STROKE, lane).map(|paint| park_paint(arena, paint.clone())).transpose()?;
let fill = output
.attribute::<Option<List<Graphic>>>(ATTR_FILL, lane)
.and_then(|paint| paint.as_ref())
.map(|paint| park_paint(arena, paint.clone()))
.transpose()?;
let stroke = output
.attribute::<Option<List<Graphic>>>(ATTR_STROKE, lane)
.and_then(|paint| paint.as_ref())
.map(|paint| park_paint(arena, paint.clone()))
.transpose()?;
let layer_path: Vec<NodeId> = output.attribute::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH, lane).cloned().unwrap_or_default();
let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0;
let merged_layers = output
.attribute::<List<Graphic>>(ATTR_EDITOR_MERGED_LAYERS, lane)
.attribute::<Option<List<Graphic>>>(ATTR_EDITOR_MERGED_LAYERS, lane)
.and_then(|layers| layers.as_ref())
.map(|layers| arena.alloc(layers.clone()).ok_or_else(exhausted).map(|(parked, _)| parked))
.transpose()?;
@@ -1813,8 +1822,16 @@ fn flatten_path_core<'e>(
bake_paint_transforms(&mut attributes, source_transform);
let carrier = List::new_from_item(Item::from_parts(Vector::default(), attributes));
fill = carrier.attribute::<List<Graphic>>(ATTR_FILL, 0).map(|paint| park_paint(arena, paint.clone())).transpose()?;
stroke = carrier.attribute::<List<Graphic>>(ATTR_STROKE, 0).map(|paint| park_paint(arena, paint.clone())).transpose()?;
fill = carrier
.attribute::<Option<List<Graphic>>>(ATTR_FILL, 0)
.and_then(|paint| paint.as_ref())
.map(|paint| park_paint(arena, paint.clone()))
.transpose()?;
stroke = carrier
.attribute::<Option<List<Graphic>>>(ATTR_STROKE, 0)
.and_then(|paint| paint.as_ref())
.map(|paint| park_paint(arena, paint.clone()))
.transpose()?;
// Adopt the last input item's layer so the editor can also bucket clicks under a contributing child layer
layer_path = flattened.attribute_cloned_or_default::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH, primary);
@@ -2995,7 +3012,7 @@ fn morph_core(content: List<Graphic>, progression: f64, reverse: bool, distribut
let mut attributes = content.clone_item_attributes(endpoint_index);
attributes.insert(ATTR_TRANSFORM, lerped_transform);
attributes.insert(ATTR_EDITOR_MERGED_LAYERS, graphic_list_content);
attributes.insert(ATTR_EDITOR_MERGED_LAYERS, Some(graphic_list_content));
return List::new_from_item(Item::from_parts(endpoint_element.clone(), attributes));
}
@@ -3186,13 +3203,13 @@ fn morph_core(content: List<Graphic>, progression: f64, reverse: bool, distribut
.with_attribute(ATTR_OPACITY_FILL, lerped_fill)
.with_attribute(ATTR_CLIPPING_MASK, lerped_clip)
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer_path)
.with_attribute(ATTR_EDITOR_MERGED_LAYERS, graphic_list_content);
.with_attribute(ATTR_EDITOR_MERGED_LAYERS, Some(graphic_list_content));
if let Some(fill) = fill_paint {
item.set_attribute(ATTR_FILL, fill);
item.set_attribute(ATTR_FILL, Some(fill));
}
if let Some(stroke) = stroke_paint {
item.set_attribute(ATTR_STROKE, stroke);
item.set_attribute(ATTR_STROKE, Some(stroke));
}
List::new_from_item(item)
@@ -3885,10 +3902,10 @@ mod test {
let item_a = Item::new_from_element(rect())
.with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY)
.with_attribute(ATTR_FILL, List::new_from_element(Color::RED).into_graphic_list());
.with_attribute(ATTR_FILL, Some(List::new_from_element(Color::RED).into_graphic_list()));
let item_b = Item::new_from_element(rect())
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation((-100., -100.).into()))
.with_attribute(ATTR_FILL, List::new_from_element(Color::BLUE).into_graphic_list());
.with_attribute(ATTR_FILL, Some(List::new_from_element(Color::BLUE).into_graphic_list()));
let mut content = List::new_from_item(item_a);
content.push(item_b);