Migrate attribute call sites from string keys to typed keys

This commit is contained in:
Timon
2026-07-18 13:02:52 +00:00
committed by Keavon Chambers
parent 58178fd48e
commit 475c67facc
31 changed files with 529 additions and 511 deletions

View File

@@ -1,6 +1,7 @@
use core_types::attr;
use core_types::list::Item;
use core_types::registry::types::Percentage;
use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_OPACITY, ATTR_OPACITY_FILL, BlendMode, Color, Ctx};
use core_types::{BlendMode, Color, Ctx};
use graphic_types::Graphic;
use graphic_types::Vector;
use graphic_types::raster_types::{CPU, GPU, Raster};
@@ -19,7 +20,7 @@ fn blend_mode<T>(
let mut content = content;
let blend_mode = *blend_mode.element();
content.set_attribute(ATTR_BLEND_MODE, blend_mode);
content.set_attr::<attr::BlendMode>(blend_mode);
content
}
@@ -54,13 +55,13 @@ fn opacity<T>(
let (has_opacity, opacity, has_fill, fill) = (*has_opacity.element(), *opacity.element(), *has_fill.element(), *fill.element());
if has_opacity {
let multiplied = content.attribute_cloned_or(ATTR_OPACITY, 1.) * (opacity / 100.);
content.set_attribute(ATTR_OPACITY, multiplied);
let multiplied = content.attr_cloned_or_default::<attr::Opacity>() * (opacity / 100.);
content.set_attr::<attr::Opacity>(multiplied);
}
if has_fill {
let multiplied = content.attribute_cloned_or(ATTR_OPACITY_FILL, 1.) * (fill / 100.);
content.set_attribute(ATTR_OPACITY_FILL, multiplied);
let multiplied = content.attr_cloned_or_default::<attr::OpacityFill>() * (fill / 100.);
content.set_attr::<attr::OpacityFill>(multiplied);
}
content
@@ -79,6 +80,6 @@ fn clipping_mask<T>(
let mut content = content;
let clip = *clip.element();
content.set_attribute(ATTR_CLIPPING_MASK, clip);
content.set_attr::<attr::ClippingMask>(clip);
content
}

View File

@@ -1,6 +1,6 @@
use crate::brush_cache::BrushCache;
use crate::brush_stroke::{BrushStyle, BrushTrace};
use core_types::ATTR_TRANSFORM;
use core_types::attr;
use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::color::{Alpha, Color, Pixel, Sample};
@@ -91,7 +91,7 @@ where
return target;
}
let (elements, transforms) = target.element_and_attribute_slices_mut::<DAffine2>(ATTR_TRANSFORM);
let (elements, transforms) = target.element_and_attr_slices_mut::<attr::Transform>();
for (element, transform_attribute) in elements.iter_mut().zip(transforms.iter()) {
let target_width = element.width;
let target_height = element.height;
@@ -281,7 +281,7 @@ async fn brush(
let has_erase_or_restore_strokes = trace.iter_element_values().any(|s| matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore));
if has_erase_or_restore_strokes {
let opaque_image = Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE);
let mut erase_restore_mask = Item::new_from_element(Raster::new_cpu(opaque_image)).with_attribute(ATTR_TRANSFORM, background_bounds);
let mut erase_restore_mask = Item::new_from_element(Raster::new_cpu(opaque_image)).with_attr::<attr::Transform>(background_bounds);
for stroke in trace.into_iter().map(|row| row.into_element()) {
let mut brush_texture = cache.get_cached_brush(&stroke.style);
@@ -315,10 +315,10 @@ async fn brush(
// The paint operation changes only the raster and its bounds, so set just the resulting transform; blending, opacity,
// clipping, and layer-path attributes carry through from the input `background` rather than being invented here.
let transform: DAffine2 = actual_image.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform = actual_image.attr_cloned_or_default::<attr::Transform>();
*result_item.element_mut() = actual_image.into_element();
result_item.set_attribute(ATTR_TRANSFORM, transform);
result_item.set_attr::<attr::Transform>(transform);
result_item
}
@@ -328,8 +328,8 @@ pub fn blend_image_closure(foreground: Item<Raster<CPU>>, mut background: Item<R
let background_size = DVec2::new(background.element().width as f64, background.element().height as f64);
// Transforms a point from the background image to the foreground image
let foreground_transform: DAffine2 = foreground.attribute_cloned_or_default(ATTR_TRANSFORM);
let background_transform: DAffine2 = background.attribute_cloned_or_default(ATTR_TRANSFORM);
let foreground_transform = foreground.attr_cloned_or_default::<attr::Transform>();
let background_transform = background.attr_cloned_or_default::<attr::Transform>();
let background_to_foreground = DAffine2::from_scale(foreground_size) * foreground_transform.inverse() * background_transform * DAffine2::from_scale(1. / background_size);
// Footprint of the foreground image (0, 0)..(1, 1) in the background image space
@@ -360,7 +360,7 @@ pub fn blend_stamp_closure(foreground: BrushStampGenerator<Color>, mut backgroun
let background_size = DVec2::new(background.element().width as f64, background.element().height as f64);
// Transforms a point from the background image to the foreground image
let background_transform: DAffine2 = background.attribute_cloned_or_default(ATTR_TRANSFORM);
let background_transform = background.attr_cloned_or_default::<attr::Transform>();
let background_to_foreground = background_transform * DAffine2::from_scale(1. / background_size);
// Footprint of the foreground image (0, 0)..(1, 1) in the background image space

View File

@@ -1,6 +1,6 @@
use crate::brush_stroke::BrushStroke;
use crate::brush_stroke::BrushStyle;
use core_types::ATTR_TRANSFORM;
use core_types::attr;
use core_types::graphene_hash::CacheHashWrapper;
use core_types::list::Item;
use raster_types::CPU;
@@ -51,7 +51,7 @@ impl BrushCacheImpl {
// Check if the first non-blended stroke is an extension of the last one.
// Transform is set to ZERO (not the default IDENTITY) as a sentinel to mark this item as uninitialized.
let mut first_stroke_texture = Item::new_from_element(Raster::<CPU>::default()).with_attribute(ATTR_TRANSFORM, glam::DAffine2::ZERO);
let mut first_stroke_texture = Item::new_from_element(Raster::<CPU>::default()).with_attr::<attr::Transform>(glam::DAffine2::ZERO);
let mut first_stroke_point_skip = 0;
let strokes = input[num_blended_strokes..].to_vec();
if !strokes.is_empty() && self.prev_input.len() > num_blended_strokes {

View File

@@ -1,6 +1,7 @@
use core_types::attr;
use core_types::list::{Item, List};
use core_types::transform::TransformMut;
use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use graphic_types::graphic::{Graphic, IntoGraphicList};
use graphic_types::{Artboard, Vector};
@@ -53,8 +54,8 @@ pub async fn create_artboard<T: IntoGraphicList>(
// Name is not stored here, it's resolved live from the parent layer's display name
Item::new_from_element(Artboard::new(content))
.with_attribute(ATTR_LOCATION, normalized_location)
.with_attribute(ATTR_DIMENSIONS, normalized_dimensions)
.with_attribute(ATTR_BACKGROUND, background)
.with_attribute(ATTR_CLIP, clip)
.with_attr::<attr::Location>(normalized_location)
.with_attr::<attr::Dimensions>(normalized_dimensions)
.with_attr::<attr::Background>(background)
.with_attr::<attr::Clip>(clip)
}

View File

@@ -1,7 +1,8 @@
use core_types::attr;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::list::{AttributeValueDyn, Item, List, ListDyn, NodeIdPath};
use core_types::registry::types::{Angle, SeedValue, SignedInteger};
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
use core_types::{AnyHash, BlendMode, CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use graphic_types::graphic::{Graphic, IntoGraphicList};
use graphic_types::{Artboard, Vector};
@@ -489,7 +490,7 @@ async fn mirror<T: BoundingBox + 'n + Send + Clone>(
let normal = DVec2::from_angle(angle.to_radians());
// The mirror reference may be based on the bounding box if an explicit reference point is chosen
let item_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let item_transform = content.attr_cloned_or_default::<attr::Transform>();
let RenderBoundingBox::Rectangle(bounding_box) = content.element().bounding_box(item_transform, false) else {
return List::new_from_item(content);
};
@@ -521,7 +522,7 @@ async fn mirror<T: BoundingBox + 'n + Send + Clone>(
// Add the mirrored copy with the reflection composed onto its transform
let mut mirrored = content;
mirrored.set_attribute(ATTR_TRANSFORM, reflected_transform * item_transform);
mirrored.set_attr::<attr::Transform>(reflected_transform * item_transform);
result_list.push(mirrored);
result_list
@@ -600,7 +601,7 @@ fn read_attribute_vector(
let name = name.into_element();
let mut result = List::with_capacity(content.len());
for index in 0..content.len() {
let Some(value) = content.attribute::<Vector>(&name, index) else { continue };
let Some(value) = content.attribute_dyn::<Vector>(&name, index) else { continue };
result.push(Item::new_from_element(value.clone()));
}
result
@@ -618,10 +619,10 @@ fn read_attribute_number(
let mut result = List::with_capacity(content.len());
for index in 0..content.len() {
let value = content
.attribute::<f64>(&name, index)
.attribute_dyn::<f64>(&name, index)
.copied()
.or_else(|| content.attribute::<u64>(&name, index).map(|v| *v as f64))
.or_else(|| content.attribute::<u32>(&name, index).map(|v| *v as f64));
.or_else(|| content.attribute_dyn::<u64>(&name, index).map(|v| *v as f64))
.or_else(|| content.attribute_dyn::<u32>(&name, index).map(|v| *v as f64));
let Some(value) = value else { continue };
result.push(Item::new_from_element(value));
}
@@ -639,7 +640,7 @@ fn read_attribute_bool(
let name = name.into_element();
let mut result = List::with_capacity(content.len());
for index in 0..content.len() {
let Some(value) = content.attribute::<bool>(&name, index) else { continue };
let Some(value) = content.attribute_dyn::<bool>(&name, index) else { continue };
result.push(Item::new_from_element(*value));
}
result
@@ -656,7 +657,7 @@ fn read_attribute_string(
let name = name.into_element();
let mut result = List::with_capacity(content.len());
for index in 0..content.len() {
let Some(value) = content.attribute::<String>(&name, index) else { continue };
let Some(value) = content.attribute_dyn::<String>(&name, index) else { continue };
result.push(Item::new_from_element(value.clone()));
}
result
@@ -673,7 +674,7 @@ fn read_attribute_transform(
let name = name.into_element();
let mut result = List::with_capacity(content.len());
for index in 0..content.len() {
let Some(value) = content.attribute::<DAffine2>(&name, index) else { continue };
let Some(value) = content.attribute_dyn::<DAffine2>(&name, index) else { continue };
result.push(Item::new_from_element(*value));
}
result
@@ -690,7 +691,7 @@ fn read_attribute_color(
let name = name.into_element();
let mut result = List::with_capacity(content.len());
for index in 0..content.len() {
let Some(value) = content.attribute::<Color>(&name, index) else { continue };
let Some(value) = content.attribute_dyn::<Color>(&name, index) else { continue };
result.push(Item::new_from_element(*value));
}
result
@@ -707,7 +708,7 @@ fn read_attribute_blend_mode(
let name = name.into_element();
let mut result = List::with_capacity(content.len());
for index in 0..content.len() {
let Some(value) = content.attribute::<BlendMode>(&name, index) else { continue };
let Some(value) = content.attribute_dyn::<BlendMode>(&name, index) else { continue };
result.push(Item::new_from_element(*value));
}
result
@@ -724,7 +725,7 @@ fn read_attribute_gradient_type(
let name = name.into_element();
let mut result = List::with_capacity(content.len());
for index in 0..content.len() {
let Some(value) = content.attribute::<GradientType>(&name, index) else { continue };
let Some(value) = content.attribute_dyn::<GradientType>(&name, index) else { continue };
result.push(Item::new_from_element(*value));
}
result
@@ -741,7 +742,7 @@ fn read_attribute_spread_method(
let name = name.into_element();
let mut result = List::with_capacity(content.len());
for index in 0..content.len() {
let Some(value) = content.attribute::<GradientSpreadMethod>(&name, index) else { continue };
let Some(value) = content.attribute_dyn::<GradientSpreadMethod>(&name, index) else { continue };
result.push(Item::new_from_element(*value));
}
result
@@ -758,7 +759,7 @@ fn read_attribute_gradient_stops(
let name = name.into_element();
let mut result = List::with_capacity(content.len());
for index in 0..content.len() {
let Some(value) = content.attribute::<Gradient>(&name, index) else { continue };
let Some(value) = content.attribute_dyn::<Gradient>(&name, index) else { continue };
result.push(Item::new_from_element(value.clone()));
}
result
@@ -775,7 +776,7 @@ fn read_attribute_artboard(
let name = name.into_element();
let mut result = List::with_capacity(content.len());
for index in 0..content.len() {
let Some(value) = content.attribute::<Artboard>(&name, index) else { continue };
let Some(value) = content.attribute_dyn::<Artboard>(&name, index) else { continue };
result.push(Item::new_from_element(value.clone()));
}
result
@@ -792,7 +793,7 @@ fn read_attribute_raster(
let name = name.into_element();
let mut result = List::with_capacity(content.len());
for index in 0..content.len() {
let Some(value) = content.attribute::<Raster<CPU>>(&name, index) else { continue };
let Some(value) = content.attribute_dyn::<Raster<CPU>>(&name, index) else { continue };
result.push(Item::new_from_element(value.clone()));
}
result
@@ -869,7 +870,7 @@ pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
let mut base = base;
for mut row in new.into_iter() {
row.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
row.set_attr::<attr::editor::LayerPath>(layer_path.clone());
base.push(row);
}
@@ -926,7 +927,7 @@ pub async fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten:
for index in 0..current_graphic_list.len() {
let Some(current_element) = current_graphic_list.element(index) else { continue };
let current_element = current_element.clone();
let current_transform: DAffine2 = current_graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let current_transform = current_graphic_list.attr_cloned_or_default::<attr::Transform>(index);
let recurse = fully_flatten || recursion_depth == 0;
@@ -934,7 +935,7 @@ pub async fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten:
// If we're allowed to recurse, flatten any graphics we encounter
Graphic::Graphic(mut current_element) if recurse => {
// Apply the parent graphic's transform to all child elements
for graphic_transform in current_element.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
for graphic_transform in current_element.iter_attr_values_mut_or_default::<attr::Transform>() {
*graphic_transform = current_transform * *graphic_transform;
}
@@ -974,15 +975,15 @@ pub async fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(L
// already holds the original transforms; pre-compensate by item 0's inverse so the renderer's
// `upstream_footprint *= item_0_transform` recursion cancels out and leaves the originals intact.
let mut graphic_list = graphic_list;
let item_0_transform: DAffine2 = output.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
let item_0_transform = output.attr_cloned_or_default::<attr::Transform>(0);
if item_0_transform.matrix2.determinant().abs() > f64::EPSILON {
let inverse = item_0_transform.inverse();
for transform in graphic_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
for transform in graphic_list.iter_attr_values_mut_or_default::<attr::Transform>() {
*transform = inverse * *transform;
}
}
output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list);
output.set_attr::<graphic_types::attr::editor::MergedLayers>(0, graphic_list);
}
output

View File

@@ -10,9 +10,9 @@ use core_types::list::List;
use core_types::math::bbox::Bbox;
use core_types::ops::Convert;
use core_types::transform::Footprint;
#[cfg(target_family = "wasm")]
use core_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, WasmNotSend};
use core_types::{Color, Ctx};
#[cfg(target_family = "wasm")]
use core_types::{WasmNotSend, attr};
pub use graph_craft::application_io::resource::{Resource, ResourceHash};
pub use graph_craft::application_io::*;
pub use graph_craft::document::value::RenderOutputType;
@@ -243,7 +243,7 @@ where
..Default::default()
};
for transform in data.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
for transform in data.iter_attr_values_mut_or_default::<attr::Transform>() {
*transform = DAffine2::from_translation(-aabb.start) * *transform;
}
data.render_svg(&mut render, &render_params);
@@ -270,8 +270,8 @@ where
let image = Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32);
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_attr::<attr::Transform>(footprint.transform)
.with_attr::<graphic_types::attr::editor::MergedLayers>(upstream_graphic_list),
)
}

View File

@@ -1,6 +1,6 @@
use core_types::Ctx;
use core_types::consts::{DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT};
use core_types::list::{Item, List};
use core_types::{ATTR_FONT, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_TEXT_ALIGN, Ctx};
use graph_craft::application_io::resource::Resource;
use graphic_types::Vector;
pub use text_nodes::*;
@@ -69,28 +69,28 @@ fn text(
let mut item = Item::new_from_element(text);
if font != Resource::default() {
item.set_attribute(ATTR_FONT, font);
item.set_attr::<attr::Font>(font);
}
if (size - DEFAULT_FONT_SIZE).abs() > f64::EPSILON {
item.set_attribute(ATTR_FONT_SIZE, size);
item.set_attr::<core_types::attr::FontSize>(size);
}
if (line_height - DEFAULT_LINE_HEIGHT).abs() > f64::EPSILON {
item.set_attribute(ATTR_LINE_HEIGHT, line_height);
item.set_attr::<core_types::attr::LineHeight>(line_height);
}
if letter_spacing != 0. {
item.set_attribute(ATTR_LETTER_SPACING, letter_spacing);
item.set_attr::<core_types::attr::LetterSpacing>(letter_spacing);
}
if letter_tilt != 0. {
item.set_attribute(ATTR_LETTER_TILT, letter_tilt);
item.set_attr::<core_types::attr::LetterTilt>(letter_tilt);
}
if has_max_width {
item.set_attribute(ATTR_MAX_WIDTH, Some(max_width));
item.set_attr::<core_types::attr::MaxWidth>(Some(max_width));
}
if has_max_height {
item.set_attribute(ATTR_MAX_HEIGHT, Some(max_height));
item.set_attr::<core_types::attr::MaxHeight>(Some(max_height));
}
if align != TextAlign::default() {
item.set_attribute(ATTR_TEXT_ALIGN, align);
item.set_attr::<attr::TextAlign>(align);
}
item

View File

@@ -1381,7 +1381,7 @@ fn gradient_value(_: impl Ctx, _primary: (), gradient: Item<Gradient>) -> Item<G
#[node_macro::node(category("Color"))]
fn gradient_type(_: impl Ctx, gradient: Item<Gradient>, gradient_type: Item<vector_types::GradientType>) -> Item<Gradient> {
let mut gradient = gradient;
gradient.set_attribute(core_types::ATTR_GRADIENT_TYPE, *gradient_type.element());
gradient.set_attr::<vector_types::attr::GradientType>(*gradient_type.element());
gradient
}
@@ -1389,7 +1389,7 @@ fn gradient_type(_: impl Ctx, gradient: Item<Gradient>, gradient_type: Item<vect
#[node_macro::node(category("Color"))]
fn spread_method(_: impl Ctx, gradient: Item<Gradient>, spread_method: Item<vector_types::GradientSpreadMethod>) -> Item<Gradient> {
let mut gradient = gradient;
gradient.set_attribute(core_types::ATTR_SPREAD_METHOD, *spread_method.element());
gradient.set_attr::<vector_types::attr::SpreadMethod>(*spread_method.element());
gradient
}

View File

@@ -1,10 +1,8 @@
use core_types::list::{ATTR_FILL, Item, ItemAttributeValues, List};
use core_types::{
ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color, Ctx,
};
use core_types::attr::{self, Attr};
use core_types::list::{Item, ItemAttributeValues, List};
use core_types::{Color, Ctx};
use glam::{DAffine2, DVec2};
use graphic_types::graphic::{bake_paint_transforms, set_paint_attribute};
use graphic_types::vector_types::gradient::{GradientSpreadMethod, GradientType};
use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath};
use graphic_types::vector_types::vector::PointId;
use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
@@ -43,8 +41,8 @@ async fn boolean_operation<I: graphic_types::IntoGraphicList>(
// Replace the transformation matrix with a mutation of the vector points themselves
if result_vector_list.element_mut(0).is_some() {
let transform: DAffine2 = result_vector_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
result_vector_list.set_attribute(ATTR_TRANSFORM, 0, DAffine2::IDENTITY);
let transform = result_vector_list.attr_cloned_or_default::<attr::Transform>(0);
result_vector_list.set_attr::<attr::Transform>(0, DAffine2::IDENTITY);
let result_vector = result_vector_list.element_mut(0).unwrap();
Vector::transform(result_vector, transform);
@@ -52,10 +50,10 @@ async fn boolean_operation<I: graphic_types::IntoGraphicList>(
// Snapshot the input layers as the `editor:merged_layers` attribute so the renderer can recurse into them
// for editor click-target preservation.
result_vector_list.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, content.clone());
result_vector_list.set_attr::<graphic_types::attr::editor::MergedLayers>(0, content.clone());
// Clean up the boolean operation result by merging duplicated points
let merge_transform: DAffine2 = result_vector_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
let merge_transform = result_vector_list.attr_cloned_or_default::<attr::Transform>(0);
result_vector_list.element_mut(0).unwrap().merge_by_distance_spatial(merge_transform, 0.0001);
}
@@ -139,9 +137,9 @@ fn boolean_operation_on_vector_list(vector: &List<Vector>, boolean_operation: Bo
};
let mut row = if let Some(index) = copy_from_index {
let mut attributes = vector.clone_item_attributes(index);
let copy_from_transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let copy_from_transform = vector.attr_cloned_or_default::<attr::Transform>(index);
// The boolean op bakes input transforms into the output geometry, so the result item carries no transform of its own
attributes.insert(ATTR_TRANSFORM, DAffine2::IDENTITY);
attributes.set_attr::<attr::Transform>(DAffine2::IDENTITY);
bake_paint_transforms(&mut attributes, copy_from_transform);
@@ -157,7 +155,7 @@ fn boolean_operation_on_vector_list(vector: &List<Vector>, boolean_operation: Bo
for index in 0..vector.len() {
let element = vector.element(index).unwrap();
paths.push(to_bez_path(element, vector.attribute_cloned_or_default(ATTR_TRANSFORM, index)));
paths.push(to_bez_path(element, vector.attr_cloned_or_default::<attr::Transform>(index)));
}
let top = match Topology::<WindingNumber>::from_paths(paths.iter().enumerate().map(|(idx, path)| (path, (idx, paths.len()))), EPSILON) {
@@ -185,18 +183,18 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
Graphic::None => 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);
let parent_transform = graphic_list.attr_cloned_or_default::<attr::Transform>(index);
vector
.into_iter()
.map(|mut sub_vector| {
let current_transform: DAffine2 = sub_vector.attribute_cloned_or_default(ATTR_TRANSFORM);
*sub_vector.attribute_mut_or_insert_default(ATTR_TRANSFORM) = parent_transform * current_transform;
let current_transform = sub_vector.attr_cloned_or_default::<attr::Transform>();
*sub_vector.attr_mut_or_insert_default::<attr::Transform>() = parent_transform * current_transform;
sub_vector
})
.collect::<Vec<_>>()
}
Graphic::RasterCPU(image) => {
let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let parent_transform = graphic_list.attr_cloned_or_default::<attr::Transform>(index);
let make_item = |transform: DAffine2, source_attributes: &ItemAttributeValues| {
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform);
@@ -204,10 +202,16 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
let element = Vector::from_subpath(subpath);
let mut item = Item::new_from_element(element);
for key in [ATTR_BLEND_MODE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH] {
for key in [
attr::BlendMode::name(),
attr::Opacity::name(),
attr::OpacityFill::name(),
attr::ClippingMask::name(),
attr::editor::LayerPath::name(),
] {
item.attributes_mut().insert_cloned_from(source_attributes, key);
}
set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
set_paint_attribute::<graphic_types::attr::Fill>(item.attributes_mut(), List::new_from_element(Color::BLACK));
item
};
@@ -216,14 +220,14 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
// back to the originating raster layer
(0..image.len())
.map(|i| {
let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i);
let row_transform = image.attr_cloned_or_default::<attr::Transform>(i);
let source_attributes = image.clone_item_attributes(i);
make_item(parent_transform * row_transform, &source_attributes)
})
.collect::<Vec<_>>()
}
Graphic::RasterGPU(image) => {
let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let parent_transform = graphic_list.attr_cloned_or_default::<attr::Transform>(index);
let make_item = |transform: DAffine2, source_attributes: &ItemAttributeValues| {
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform);
@@ -231,10 +235,16 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
let element = Vector::from_subpath(subpath);
let mut item = Item::new_from_element(element);
for key in [ATTR_BLEND_MODE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH] {
for key in [
attr::BlendMode::name(),
attr::Opacity::name(),
attr::OpacityFill::name(),
attr::ClippingMask::name(),
attr::editor::LayerPath::name(),
] {
item.attributes_mut().insert_cloned_from(source_attributes, key);
}
set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
set_paint_attribute::<graphic_types::attr::Fill>(item.attributes_mut(), List::new_from_element(Color::BLACK));
item
};
@@ -243,16 +253,16 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
// back to the originating raster layer
(0..image.len())
.map(|i| {
let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i);
let row_transform = image.attr_cloned_or_default::<attr::Transform>(i);
let source_attributes = image.clone_item_attributes(i);
make_item(parent_transform * row_transform, &source_attributes)
})
.collect::<Vec<_>>()
}
Graphic::Graphic(mut graphic) => {
let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let parent_transform = graphic_list.attr_cloned_or_default::<attr::Transform>(index);
// Apply the parent graphic's transform to each element of the inner `List`
for transform in graphic.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
for transform in graphic.iter_attr_values_mut_or_default::<attr::Transform>() {
*transform = parent_transform * *transform;
}
@@ -266,7 +276,7 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
.into_iter()
.map(|row| {
let (color, mut attributes) = row.into_parts();
set_paint_attribute(&mut attributes, ATTR_FILL, List::new_from_element(color));
set_paint_attribute::<graphic_types::attr::Fill>(&mut attributes, List::new_from_element(color));
let mut element = Vector::default();
element.set_stroke_transform(DAffine2::IDENTITY);
@@ -280,16 +290,16 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
let (stops, mut attributes) = row.into_parts();
let mut gradient_paint = List::new_from_element(stops);
if let Some(transform) = attributes.remove::<DAffine2>(ATTR_TRANSFORM) {
gradient_paint.set_attribute(ATTR_TRANSFORM, 0, transform);
if let Some(transform) = attributes.remove_attr::<attr::Transform>() {
gradient_paint.set_attr::<attr::Transform>(0, transform);
}
if let Some(gradient_type) = attributes.remove::<GradientType>(ATTR_GRADIENT_TYPE) {
gradient_paint.set_attribute(ATTR_GRADIENT_TYPE, 0, gradient_type);
if let Some(gradient_type) = attributes.remove_attr::<vector_types::attr::GradientType>() {
gradient_paint.set_attr::<vector_types::attr::GradientType>(0, gradient_type);
}
if let Some(spread_method) = attributes.remove::<GradientSpreadMethod>(ATTR_SPREAD_METHOD) {
gradient_paint.set_attribute(ATTR_SPREAD_METHOD, 0, spread_method);
if let Some(spread_method) = attributes.remove_attr::<vector_types::attr::SpreadMethod>() {
gradient_paint.set_attr::<vector_types::attr::SpreadMethod>(0, spread_method);
}
set_paint_attribute(&mut attributes, ATTR_FILL, gradient_paint);
set_paint_attribute::<graphic_types::attr::Fill>(&mut attributes, gradient_paint);
let mut element = Vector::default();
element.set_stroke_transform(DAffine2::IDENTITY);
@@ -299,12 +309,12 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
.collect::<Vec<_>>(),
Graphic::Text(text) => {
// Shape the glyphs into vectors (each item's own transform is applied), then compose the parent's transform like the other arms
let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let parent_transform = graphic_list.attr_cloned_or_default::<attr::Transform>(index);
text_nodes::shape_text_list(&text, false)
.into_iter()
.map(|mut sub_vector| {
let current_transform: DAffine2 = sub_vector.attribute_cloned_or_default(ATTR_TRANSFORM);
*sub_vector.attribute_mut_or_insert_default(ATTR_TRANSFORM) = parent_transform * current_transform;
let current_transform = sub_vector.attr_cloned_or_default::<attr::Transform>();
*sub_vector.attr_mut_or_insert_default::<attr::Transform>() = parent_transform * current_transform;
sub_vector
})
.collect::<Vec<_>>()

View File

@@ -1,5 +1,5 @@
use crate::adjustments::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, NoiseType};
use core_types::ATTR_TRANSFORM;
use core_types::attr;
use core_types::color::Color;
use core_types::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut};
use core_types::context::{Ctx, ExtractFootprint};
@@ -32,7 +32,7 @@ impl From<std::io::Error> for Error {
#[node_macro::node(category("Debug"))]
pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Item<Raster<CPU>>) -> Item<Raster<CPU>> {
let image_frame_transform: DAffine2 = image_frame.attribute_cloned_or_default(ATTR_TRANSFORM);
let image_frame_transform = image_frame.attr_cloned_or_default::<attr::Transform>();
let footprint = ctx.footprint();
let viewport_bounds = footprint.viewport_bounds_in_local_space();
@@ -86,7 +86,7 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Item
// we need to adjust the offset if we truncate the offset calculation
let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
attributes.insert(ATTR_TRANSFORM, new_transform);
attributes.set_attr::<attr::Transform>(new_transform);
Item::from_parts(Raster::new_cpu(image), attributes)
}
@@ -163,7 +163,7 @@ pub fn mask(
let mut row = image;
let image_size = DVec2::new(row.element().width as f64, row.element().height as f64);
let stencil_transform: DAffine2 = stencil.attribute_cloned_or_default(ATTR_TRANSFORM);
let stencil_transform = stencil.attr_cloned_or_default::<attr::Transform>();
let mask_size = stencil_transform.scale_magnitudes();
if mask_size == DVec2::ZERO {
@@ -171,7 +171,7 @@ pub fn mask(
}
// Transforms a point from the background image to the foreground image
let transform_attribute: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform_attribute = row.attr_cloned_or_default::<attr::Transform>();
let bg_to_fg = transform_attribute * DAffine2::from_scale(1. / image_size);
let stencil_transform_inverse = stencil_transform.inverse();
@@ -196,7 +196,7 @@ pub fn mask(
pub fn extend_image_to_bounds(_: impl Ctx, image: Item<Raster<CPU>>, bounds: Item<DAffine2>) -> Item<Raster<CPU>> {
let bounds = *bounds.element();
let image_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM);
let image_transform = image.attr_cloned_or_default::<attr::Transform>();
let image_aabb = Bbox::unit().affine_transform(image_transform).to_axis_aligned_bbox();
let bounds_aabb = Bbox::unit().affine_transform(bounds.transform()).to_axis_aligned_bbox();
if image_aabb.contains(bounds_aabb.start) && image_aabb.contains(bounds_aabb.end) {
@@ -232,7 +232,7 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: Item<Raster<CPU>>, bounds: Ite
// let layer_to_new_texture_space = (DAffine2::from_scale(1. / new_scale) * DAffine2::from_translation(new_start) * layer_to_image_space).inverse();
let new_texture_to_layer_space = image_transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale);
attributes.insert(ATTR_TRANSFORM, new_texture_to_layer_space);
attributes.set_attr::<attr::Transform>(new_texture_to_layer_space);
Item::from_parts(Raster::new_cpu(new_image), attributes)
}
@@ -244,7 +244,7 @@ pub fn empty_image(_: impl Ctx, transform: Item<DAffine2>, color: Item<Color>) -
let image = Image::new(width, height, color.into_element());
Item::new_from_element(Raster::new_cpu(image)).with_attribute(ATTR_TRANSFORM, transform)
Item::new_from_element(Raster::new_cpu(image)).with_attr::<attr::Transform>(transform)
}
#[node_macro::node(category(""))]
@@ -376,7 +376,7 @@ pub fn noise_pattern(
}
}
return Item::new_from_element(Raster::new_cpu(image)).with_attribute(ATTR_TRANSFORM, transform);
return Item::new_from_element(Raster::new_cpu(image)).with_attr::<attr::Transform>(transform);
}
};
noise.set_noise_type(Some(noise_type));
@@ -434,7 +434,7 @@ pub fn noise_pattern(
}
}
Item::new_from_element(Raster::new_cpu(image)).with_attribute(ATTR_TRANSFORM, transform)
Item::new_from_element(Raster::new_cpu(image)).with_attr::<attr::Transform>(transform)
}
#[node_macro::node(category("Raster: Pattern"))]
@@ -478,7 +478,7 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> Item<Raster<CPU>> {
data,
..Default::default()
}))
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(offset) * DAffine2::from_scale(size))
.with_attr::<attr::Transform>(DAffine2::from_translation(offset) * DAffine2::from_scale(size))
}
#[inline(always)]

View File

@@ -1,8 +1,9 @@
use crate::gcore::Context;
use core::f64::consts::TAU;
use core_types::attr;
use core_types::list::{Item, List};
use core_types::registry::types::{Angle, PixelSize};
use core_types::{ATTR_TRANSFORM, CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl};
use core_types::{CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use graphic_types::{Artboard, Graphic, Vector};
use raster_types::{CPU, GPU, Raster};
@@ -102,10 +103,10 @@ pub async fn repeat_array<T: Send + Clone + 'static>(
for row_index in 0..generated_content.len() {
let Some(mut row) = generated_content.clone_item(row_index) else { continue };
let local_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let local_transform = row.attr_cloned_or_default::<attr::Transform>();
let local_translation = DAffine2::from_translation(local_transform.translation);
let local_matrix = DAffine2::from_mat2(local_transform.matrix2);
*row.attribute_mut_or_insert_default(ATTR_TRANSFORM) = local_translation * transform * local_matrix;
*row.attr_mut_or_insert_default::<attr::Transform>() = local_translation * transform * local_matrix;
result_list.push(row);
}
@@ -158,10 +159,10 @@ async fn repeat_radial<T: Send + Clone + 'static>(
for row_index in 0..generated_content.len() {
let Some(mut row) = generated_content.clone_item(row_index) else { continue };
let local_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let local_transform = row.attr_cloned_or_default::<attr::Transform>();
let local_translation = DAffine2::from_translation(local_transform.translation);
let local_matrix = DAffine2::from_mat2(local_transform.matrix2);
*row.attribute_mut_or_insert_default(ATTR_TRANSFORM) = local_translation * transform * local_matrix;
*row.attr_mut_or_insert_default::<attr::Transform>() = local_translation * transform * local_matrix;
result_list.push(row);
}
@@ -200,7 +201,7 @@ async fn repeat_on_points<T: Send + Clone + 'static>(
for points_index in 0..points.len() {
let Some(points_element) = points.element(points_index) else { continue };
let transform: DAffine2 = points.attribute_cloned_or_default(ATTR_TRANSFORM, points_index);
let transform = points.attr_cloned_or_default::<attr::Transform>(points_index);
let mut iteration = async |index, point| {
let transformed_point = transform.transform_point2(point);
@@ -209,7 +210,7 @@ async fn repeat_on_points<T: Send + Clone + 'static>(
let generated_content = content.eval(new_ctx.into_context()).await;
for mut generated_row in generated_content.into_iter() {
generated_row.attribute_mut_or_insert_default::<DAffine2>(ATTR_TRANSFORM).translation = transformed_point;
generated_row.attr_mut_or_insert_default::<attr::Transform>().translation = transformed_point;
result_list.push(generated_row);
}
};
@@ -301,7 +302,7 @@ mod test {
let bounds = generated
.element(index)
.unwrap()
.bounding_box_with_transform(generated.attribute_cloned_or_default(ATTR_TRANSFORM, index))
.bounding_box_with_transform(generated.attr_cloned_or_default::<attr::Transform>(index))
.unwrap();
assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10));
assert_eq!((bounds[1] - bounds[0]).x, position.y);

View File

@@ -1,5 +1,5 @@
use core_types::list::{Item, List};
use core_types::{ATTR_TYPE, Ctx};
use core_types::{Ctx, attr};
use serde_json::Value;
use crate::unescape_string;
@@ -265,7 +265,7 @@ fn query_json_all(
let mut results = Vec::new();
resolve_all(&value, &segments, !*unquote_strings.element(), &mut results);
results.into_iter().map(|(text, ty)| Item::new_from_element(text).with_attribute(ATTR_TYPE, ty.to_string())).collect()
results.into_iter().map(|(text, ty)| Item::new_from_element(text).with_attr::<attr::Type>(ty.to_string())).collect()
}
/// A parsed segment of a JSON access path.

View File

@@ -1,5 +1,5 @@
use core_types::attr;
use core_types::list::{Item, List};
use core_types::{ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_TEXT_FRAME, ATTR_TRANSFORM};
use glam::{DAffine2, DVec2};
use parley::GlyphRun;
use skrifa::GlyphId;
@@ -15,13 +15,13 @@ pub struct PathBuilder {
origin: DVec2,
glyph_subpaths: Vec<Subpath<PointId>>,
pub vector_list: List<Vector>,
/// Per-glyph AABBs collected in single-item mode, published as `ATTR_EDITOR_CLICK_TARGET` in `finalize()`.
/// Per-glyph AABBs collected in single-item mode, published as `vector_types::attr::editor::ClickTarget` in `finalize()`.
merged_click_target_bboxes: Vec<[DVec2; 2]>,
/// Per-glyph baselines, parallel to `merged_click_target_bboxes`. Groups glyphs by line for the widening pass.
merged_click_target_baselines: Vec<f64>,
/// Per-glyph AABBs in glyph-local space (multi-item mode), widened in `finalize()` to fill gaps.
per_glyph_bboxes: Vec<Option<[DVec2; 2]>>,
/// Text frame size, stamped per item as `ATTR_EDITOR_TEXT_FRAME` relative to each item's origin.
/// Text frame size, stamped per item as `attr::editor::TextFrame` relative to each item's origin.
text_frame_size: DVec2,
/// First glyph's baseline offset (pre-height-filter). Used for the empty placeholder item so
/// `local_transforms` stays stable when all glyphs are clipped during a resize drag.
@@ -85,8 +85,8 @@ impl PathBuilder {
let frame_in_item_local = DAffine2::from_scale_angle_translation(self.text_frame_size, 0., -glyph_offset);
let item = Item::new_from_element(Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false))
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(glyph_offset))
.with_attribute(ATTR_EDITOR_TEXT_FRAME, frame_in_item_local);
.with_attr::<attr::Transform>(DAffine2::from_translation(glyph_offset))
.with_attr::<attr::editor::TextFrame>(frame_in_item_local);
self.vector_list.push(item);
// Defer click target creation to `finalize()` where adjacent AABBs get widened
@@ -170,8 +170,8 @@ impl PathBuilder {
if self.vector_list.is_empty() {
let frame_in_item_local = DAffine2::from_scale_angle_translation(self.text_frame_size, 0., -self.first_glyph_offset);
let item = Item::new_from_element(Vector::default())
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(self.first_glyph_offset))
.with_attribute(ATTR_EDITOR_TEXT_FRAME, frame_in_item_local);
.with_attr::<attr::Transform>(DAffine2::from_translation(self.first_glyph_offset))
.with_attr::<attr::editor::TextFrame>(frame_in_item_local);
self.vector_list.push(item);
}
@@ -184,7 +184,7 @@ impl PathBuilder {
.enumerate()
.filter_map(|(index, bbox)| {
let bbox = (*bbox)?;
let offset = self.vector_list.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, index).translation;
let offset = self.vector_list.attr_cloned_or_default::<attr::Transform>(index).translation;
Some((index, offset, [bbox[0] + offset, bbox[1] + offset]))
})
.collect();
@@ -197,7 +197,7 @@ impl PathBuilder {
for (entry, widened) in entries.iter().zip(layer_bboxes.iter()) {
let glyph_local = [widened[0] - entry.1, widened[1] - entry.1];
let rect = Subpath::new_rectangle(glyph_local[0], glyph_local[1]);
self.vector_list.set_attribute(ATTR_EDITOR_CLICK_TARGET, entry.0, Vector::from_subpaths([rect], false));
self.vector_list.set_attr::<vector_types::attr::editor::ClickTarget>(entry.0, Vector::from_subpaths([rect], false));
}
}
@@ -207,14 +207,14 @@ impl PathBuilder {
widen_horizontal_gaps(&mut bboxes, &self.merged_click_target_baselines);
let widened_subpaths: Vec<_> = bboxes.iter().map(|[min, max]| Subpath::new_rectangle(*min, *max)).collect();
self.vector_list.set_attribute(ATTR_EDITOR_CLICK_TARGET, 0, Vector::from_subpaths(widened_subpaths, false));
self.vector_list.set_attr::<vector_types::attr::editor::ClickTarget>(0, Vector::from_subpaths(widened_subpaths, false));
}
// Fill in text frame for items that don't have one yet (single-item mode, where item 0 = identity)
let frame = DAffine2::from_scale(self.text_frame_size);
for index in 0..self.vector_list.len() {
if self.vector_list.attribute::<DAffine2>(ATTR_EDITOR_TEXT_FRAME, index).is_none() {
self.vector_list.set_attribute(ATTR_EDITOR_TEXT_FRAME, index, frame);
if self.vector_list.attr::<attr::editor::TextFrame>(index).is_none() {
self.vector_list.set_attr::<attr::editor::TextFrame>(index, frame);
}
}

View File

@@ -1,6 +1,6 @@
use core_types::list::{Item, List};
use core_types::registry::types::SignedInteger;
use core_types::{ATTR_END, ATTR_NAME, ATTR_START, Ctx};
use core_types::{Ctx, attr};
/// Checks whether the string contains a match for the given regular expression pattern. Optionally restricts the match to only the start and/or end of the string.
#[node_macro::node(category("Text: Regex"))]
@@ -159,10 +159,7 @@ fn regex_find(
let start = captured.map_or(0_u64, |m| m.start() as u64);
let end = captured.map_or(0_u64, |m| m.end() as u64);
let name = capture_names.get(i).cloned().flatten().unwrap_or_default();
Item::new_from_element(text)
.with_attribute(ATTR_START, start)
.with_attribute(ATTR_END, end)
.with_attribute(ATTR_NAME, name)
Item::new_from_element(text).with_attr::<attr::Start>(start).with_attr::<attr::End>(end).with_attr::<attr::Name>(name)
})
.collect()
}
@@ -208,8 +205,8 @@ fn regex_find_all(
.filter_map(|m| m.ok())
.map(|m| {
Item::new_from_element(m.as_str().to_string())
.with_attribute(ATTR_START, m.start() as u64)
.with_attribute(ATTR_END, m.end() as u64)
.with_attr::<attr::Start>(m.start() as u64)
.with_attr::<attr::End>(m.end() as u64)
})
.collect()
}

View File

@@ -1,11 +1,7 @@
use super::TypesettingConfig;
use super::text_context::TextContext;
use core_types::blending::BlendMode;
use core_types::list::{Item, List, NodeIdPath};
use core_types::{
ATTR_BLEND_MODE, ATTR_EDITOR_LAYER_PATH, ATTR_FONT, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL,
ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
};
use core_types::attr;
use core_types::list::{Item, List};
use glam::{DAffine2, DVec2};
use graphene_resource::Resource;
use vector_types::Vector;
@@ -33,45 +29,45 @@ pub fn shape_text_item(item: &Item<String>, separate_glyphs: bool) -> List<Vecto
// Use fallback font when none is explicitly attached.
let font: Resource = {
let font: Resource = item.attribute_cloned_or_default(ATTR_FONT);
let font: Resource = item.attr_cloned_or_default::<crate::attr::Font>();
if font.is_empty() { super::FALLBACK_FONT_RESOURCE.clone() } else { font }
};
let defaults = TypesettingConfig::default();
let typesetting = TypesettingConfig {
font_size: item.attribute_cloned_or(ATTR_FONT_SIZE, defaults.font_size),
line_height_ratio: item.attribute_cloned_or(ATTR_LINE_HEIGHT, defaults.line_height_ratio),
letter_spacing: item.attribute_cloned_or(ATTR_LETTER_SPACING, defaults.letter_spacing),
letter_tilt: item.attribute_cloned_or(ATTR_LETTER_TILT, defaults.letter_tilt),
max_width: item.attribute_cloned_or::<Option<f64>>(ATTR_MAX_WIDTH, defaults.max_width),
max_height: item.attribute_cloned_or::<Option<f64>>(ATTR_MAX_HEIGHT, defaults.max_height),
align: item.attribute_cloned_or(ATTR_TEXT_ALIGN, defaults.align),
font_size: item.attr_cloned_or::<attr::FontSize>(defaults.font_size),
line_height_ratio: item.attr_cloned_or::<attr::LineHeight>(defaults.line_height_ratio),
letter_spacing: item.attr_cloned_or::<attr::LetterSpacing>(defaults.letter_spacing),
letter_tilt: item.attr_cloned_or::<attr::LetterTilt>(defaults.letter_tilt),
max_width: item.attr_cloned_or::<attr::MaxWidth>(defaults.max_width),
max_height: item.attr_cloned_or::<attr::MaxHeight>(defaults.max_height),
align: item.attr_cloned_or::<crate::attr::TextAlign>(defaults.align),
};
let vectors = to_path(text, &font, typesetting, separate_glyphs);
let transform = item.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM);
let layer_path = item.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH).cloned();
let blend_mode = item.attribute::<BlendMode>(ATTR_BLEND_MODE).copied();
let opacity = item.attribute::<f64>(ATTR_OPACITY).copied();
let opacity_fill = item.attribute::<f64>(ATTR_OPACITY_FILL).copied();
let transform = item.attr_cloned_or_default::<attr::Transform>();
let layer_path = item.attr::<attr::editor::LayerPath>().cloned();
let blend_mode = item.attr::<attr::BlendMode>().copied();
let opacity = item.attr::<attr::Opacity>().copied();
let opacity_fill = item.attr::<attr::OpacityFill>().copied();
let mut result = List::new();
for mut produced in vectors.into_iter() {
if transform != DAffine2::IDENTITY {
let local = produced.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM);
produced.set_attribute(ATTR_TRANSFORM, transform * local);
let local = produced.attr_cloned_or_default::<attr::Transform>();
produced.set_attr::<attr::Transform>(transform * local);
}
if let Some(layer_path) = &layer_path {
produced.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
produced.set_attr::<attr::editor::LayerPath>(layer_path.clone());
}
if let Some(blend_mode) = blend_mode {
produced.set_attribute(ATTR_BLEND_MODE, blend_mode);
produced.set_attr::<attr::BlendMode>(blend_mode);
}
if let Some(opacity) = opacity {
produced.set_attribute(ATTR_OPACITY, opacity);
produced.set_attr::<attr::Opacity>(opacity);
}
if let Some(opacity_fill) = opacity_fill {
produced.set_attribute(ATTR_OPACITY_FILL, opacity_fill);
produced.set_attr::<attr::OpacityFill>(opacity_fill);
}
result.push(produced);
}

View File

@@ -1,8 +1,9 @@
use core::f64;
use core_types::attr;
use core_types::color::Color;
use core_types::list::{Item, List};
use core_types::transform::{ApplyTransform, ScaleType, Transform};
use core_types::{ATTR_TRANSFORM, CloneVarArgs, Context, Ctx, ExtractAll, InjectFootprint, ModifyFootprint, OwnedContextImpl};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, InjectFootprint, ModifyFootprint, OwnedContextImpl};
use glam::{DAffine2, DMat2, DVec2};
use graphic_types::raster_types::{CPU, GPU, Raster};
use graphic_types::{Artboard, Graphic, Vector};
@@ -106,7 +107,7 @@ fn reset_transform<T>(
let mut content = content;
let (reset_translation, reset_rotation, reset_scale) = (*reset_translation.element(), *reset_rotation.element(), *reset_scale.element());
let item_transform = content.attribute_mut_or_insert_default::<DAffine2>(ATTR_TRANSFORM);
let item_transform = content.attr_mut_or_insert_default::<attr::Transform>();
if reset_translation {
item_transform.translation = DVec2::ZERO;
@@ -147,14 +148,14 @@ fn replace_transform<T>(
let mut content = content;
let transform = *transform.element();
content.set_attribute(ATTR_TRANSFORM, transform.transform());
content.set_attr::<attr::Transform>(transform.transform());
content
}
/// Obtains the transform of the input content.
#[node_macro::node(category("Math: Transform"), path(core_types::vector))]
fn extract_transform<T: 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector, Raster<CPU>, Raster<GPU>, Color, Gradient, String, Artboard)] content: Item<T>) -> Item<DAffine2> {
Item::new_from_element(content.attribute_cloned_or_default(ATTR_TRANSFORM))
Item::new_from_element(content.attr_cloned_or_default::<attr::Transform>())
}
/// Produces the inverse of the input transform, which is the transform that undoes the effect of the original transform.

View File

@@ -1,7 +1,7 @@
use core_types::list::{Item, List, NodeIdPath};
use core_types::transform::BakeTransform;
use core_types::uuid::NodeId;
use core_types::{ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, Ctx};
use core_types::{Ctx, attr};
use glam::{DAffine2, DVec2};
use graphic_types::Vector;
use vector_types::vector::VectorModification;
@@ -13,7 +13,7 @@ async fn path_modify(_ctx: impl Ctx, vector: Item<Vector>, modification: Item<Bo
modification.into_element().apply(vector.element_mut());
// Drop the stale click-target override so hit testing uses the geometry the user is now editing
vector.remove_attribute::<Vector>(ATTR_EDITOR_CLICK_TARGET);
vector.remove_attr::<vector_types::attr::editor::ClickTarget>();
// Set the path to the encapsulating subgraph (drop our own trailing entry from `node_path`),
// matching the `path_of_subgraph` proto so editor tools can route data back to the parent layer.
@@ -22,9 +22,9 @@ async fn path_modify(_ctx: impl Ctx, vector: Item<Vector>, modification: Item<Bo
let len = node_path.len();
node_path.into_iter().take(len.saturating_sub(1)).collect()
};
let existing = vector.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH).0;
let existing = vector.attr_cloned_or_default::<attr::editor::LayerPath>().0;
let layer_path = if existing.is_empty() { subgraph_path } else { existing };
vector.set_attribute(ATTR_EDITOR_LAYER_PATH, NodeIdPath(layer_path));
vector.set_attr::<attr::editor::LayerPath>(NodeIdPath(layer_path));
vector
}
@@ -33,7 +33,7 @@ async fn path_modify(_ctx: impl Ctx, vector: Item<Vector>, modification: Item<Bo
#[node_macro::node(category("Vector"))]
async fn bake_transform<T: BakeTransform + 'n + Send + 'static>(_ctx: impl Ctx, #[implementations(Vector, DAffine2, DVec2)] content: Item<T>) -> Item<T> {
let mut content = content;
if let Some(transform) = content.remove_attribute::<DAffine2>(ATTR_TRANSFORM) {
if let Some(transform) = content.remove_attr::<attr::Transform>() {
content.element_mut().bake_transform(&transform);
}

View File

@@ -1,16 +1,14 @@
use core::cmp::Ordering;
use core::f64::consts::{PI, TAU};
use core::hash::{Hash, Hasher};
use core_types::attr::{self, Attr};
use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn, NodeIdPath};
use core_types::list::{Item, ItemAttributeValues, List, ListDyn};
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
use core_types::transform::{Footprint, Transform};
use core_types::uuid::NodeId;
use core_types::{
ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, CloneVarArgs,
Color, Context, Ctx, ExtractAll, OwnedContextImpl,
};
use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Vector;
use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at};
@@ -73,32 +71,32 @@ impl VectorListIterMut for List<Vector> {
trait VectorItemMut {
fn for_each_vector_mut(&mut self, f: impl FnMut(&mut Vector, DAffine2));
fn set_vector_paint(&mut self, key: &str, paint: List<Graphic>);
fn set_vector_paint<A: Attr<Value = List<Graphic>>>(&mut self, paint: List<Graphic>);
}
impl VectorItemMut for Item<Vector> {
fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut Vector, DAffine2)) {
let transform = self.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM);
let transform = self.attr_cloned_or_default::<attr::Transform>();
f(self.element_mut(), transform);
}
fn set_vector_paint(&mut self, key: &str, paint: List<Graphic>) {
self.set_attribute(key, paint);
fn set_vector_paint<A: Attr<Value = List<Graphic>>>(&mut self, paint: List<Graphic>) {
self.set_attr::<A>(paint);
}
}
impl VectorItemMut for Item<Graphic> {
fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut Vector, DAffine2)) {
let Some(vector_list) = self.element_mut().as_vector_mut() else { return };
let (elements, transforms) = vector_list.element_and_attribute_slices_mut::<DAffine2>(ATTR_TRANSFORM);
let (elements, transforms) = vector_list.element_and_attr_slices_mut::<attr::Transform>();
for (vector, transform) in elements.iter_mut().zip(transforms.iter()) {
f(vector, *transform);
}
}
fn set_vector_paint(&mut self, key: &str, paint: List<Graphic>) {
fn set_vector_paint<A: Attr<Value = List<Graphic>>>(&mut self, paint: List<Graphic>) {
let Some(vector_list) = self.element_mut().as_vector_mut() else { return };
for slot in vector_list.iter_attribute_values_mut_or_default::<List<Graphic>>(key) {
for slot in vector_list.iter_attr_values_mut_or_default::<A>() {
*slot = paint.clone();
}
}
@@ -161,10 +159,10 @@ where
let paint = List::new_from_element(color).into_graphic_list();
if fill {
set_paint_attribute_at(vector_list, index, ATTR_FILL, paint.clone());
set_paint_attribute_at::<graphic_types::attr::Fill, _>(vector_list, index, paint.clone());
}
if stroke && vector_list.element(index).is_some_and(|vector| vector.stroke.is_some()) {
set_paint_attribute_at(vector_list, index, ATTR_STROKE, paint.clone());
set_paint_attribute_at::<graphic_types::attr::Stroke, _>(vector_list, index, paint.clone());
}
i += 1;
@@ -208,19 +206,19 @@ where
for graphic in fill.iter_element_values_mut() {
let Graphic::Gradient(gradient) = graphic else { continue };
if gradient.iter_attribute_values::<GradientType>(ATTR_GRADIENT_TYPE).is_none() {
for value in gradient.iter_attribute_values_mut_or_default::<GradientType>(ATTR_GRADIENT_TYPE) {
if gradient.iter_attr_values::<vector_types::attr::GradientType>().is_none() {
for value in gradient.iter_attr_values_mut_or_default::<vector_types::attr::GradientType>() {
*value = _gradient_type;
}
}
if gradient.iter_attribute_values::<GradientSpreadMethod>(ATTR_SPREAD_METHOD).is_none() {
for value in gradient.iter_attribute_values_mut_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD) {
if gradient.iter_attr_values::<vector_types::attr::SpreadMethod>().is_none() {
for value in gradient.iter_attr_values_mut_or_default::<vector_types::attr::SpreadMethod>() {
*value = _spread_method;
}
}
if gradient.iter_attribute_values::<DAffine2>(ATTR_TRANSFORM).is_none() {
if gradient.iter_attr_values::<attr::Transform>().is_none() {
// Without an explicit placement, derive one covering the paint target's bounding box (the CSS `auto` behavior)
let transform = if _has_transform {
_transform
@@ -246,13 +244,13 @@ where
initial_gradient_transform_for_bounding_box([min, max])
};
for value in gradient.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
for value in gradient.iter_attr_values_mut_or_default::<attr::Transform>() {
*value = transform;
}
}
}
content.set_vector_paint(ATTR_FILL, fill);
content.set_vector_paint::<graphic_types::attr::Fill>(fill);
content
}
@@ -326,7 +324,7 @@ where
});
let paint = paint.into_graphic_list();
content.set_vector_paint(ATTR_STROKE, paint);
content.set_vector_paint::<graphic_types::attr::Stroke>(paint);
content
}
@@ -388,7 +386,7 @@ async fn copy_to_points<I: 'n + Send + Clone>(
let do_scale = random_scale_difference.abs() > 1e-6;
let do_rotation = random_rotation.abs() > 1e-6;
let points_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let points_transform: DAffine2 = row.attr_cloned_or_default::<attr::Transform>();
for &point in row.element().point_domain.positions() {
let translation = points_transform.transform_point2(point);
@@ -417,8 +415,8 @@ async fn copy_to_points<I: 'n + Send + Clone>(
for row_index in 0..content.len() {
let Some(mut row) = content.clone_item(row_index) else { continue };
let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
row.set_attribute(ATTR_TRANSFORM, transform * row_transform);
let row_transform: DAffine2 = row.attr_cloned_or_default::<attr::Transform>();
row.set_attr::<attr::Transform>(transform * row_transform);
result_list.push(row);
}
@@ -446,7 +444,7 @@ async fn round_corners(
min_angle_threshold: Item<Angle>,
) -> Item<Vector> {
let (radius, roundness, edge_length_limit, min_angle_threshold) = (*radius.element(), *roundness.element(), *edge_length_limit.element(), *min_angle_threshold.element());
let source_transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM);
let source_transform: DAffine2 = source.attr_cloned_or_default::<attr::Transform>();
let source_transform_inverse = source_transform.inverse();
let (source, attributes) = source.into_parts();
@@ -550,7 +548,7 @@ pub fn merge_by_distance(
match algorithm {
MergeByDistanceAlgorithm::Spatial => {
let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform: DAffine2 = content.attr_cloned_or_default::<attr::Transform>();
content.element_mut().merge_by_distance_spatial(transform, distance);
}
MergeByDistanceAlgorithm::Topological => content.element_mut().merge_by_distance_topological(distance),
@@ -767,12 +765,12 @@ async fn extrude(_: impl Ctx, source: Item<Vector>, direction: Item<DVec2>, join
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn box_warp(_: impl Ctx, content: Item<Vector>, #[expose] rectangle: Item<Vector>) -> Item<Vector> {
let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM);
let target_transform: DAffine2 = rectangle.attr_cloned_or_default::<attr::Transform>();
let target = rectangle.into_element();
let mut row = content;
{
let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform: DAffine2 = row.attr_cloned_or_default::<attr::Transform>();
let vector = std::mem::take(row.element_mut());
// Get the bounding box of the source vector geometry
@@ -832,7 +830,7 @@ async fn box_warp(_: impl Ctx, content: Item<Vector>, #[expose] rectangle: Item<
// Reset the transform since we've applied it directly to the points
*row.element_mut() = result;
row.set_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY);
row.set_attr::<attr::Transform>(DAffine2::IDENTITY);
}
row
}
@@ -942,8 +940,8 @@ where
RowsOrColumns::Rows => DVec2::new(strip.along_position, strip.cross_position),
RowsOrColumns::Columns => DVec2::new(strip.cross_position, strip.along_position),
};
let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
row.set_attribute(ATTR_TRANSFORM, DAffine2::from_translation(target_position - top_left) * row_transform);
let row_transform: DAffine2 = row.attr_cloned_or_default::<attr::Transform>();
row.set_attr::<attr::Transform>(DAffine2::from_translation(target_position - top_left) * row_transform);
strip.along_position += along + separation;
} else {
@@ -954,8 +952,8 @@ where
RowsOrColumns::Rows => DVec2::new(0., new_cross),
RowsOrColumns::Columns => DVec2::new(new_cross, 0.),
};
let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
row.set_attribute(ATTR_TRANSFORM, DAffine2::from_translation(target_position - top_left) * row_transform);
let row_transform: DAffine2 = row.attr_cloned_or_default::<attr::Transform>();
row.set_attr::<attr::Transform>(DAffine2::from_translation(target_position - top_left) * row_transform);
strips.push(Strip {
along_position: along + separation,
@@ -986,7 +984,7 @@ async fn auto_tangents(
) -> Item<Vector> {
let (spread, preserve_existing) = (*spread.element(), *preserve_existing.element());
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform: DAffine2 = source.attr_cloned_or_default::<attr::Transform>();
let (source, attributes) = source.into_parts();
let mut result = Vector {
@@ -1146,7 +1144,7 @@ async fn bounding_box(_: impl Ctx, content: Item<Vector>) -> Item<Vector> {
async fn dimensions(_: impl Ctx, content: Item<Vector>) -> Item<DVec2> {
let dimensions = content
.element()
.bounding_box_with_transform(content.attribute_cloned_or_default(ATTR_TRANSFORM))
.bounding_box_with_transform(content.attr_cloned_or_default::<attr::Transform>())
.map(|[top_left, bottom_right]| bottom_right - top_left)
.unwrap_or_default();
@@ -1358,7 +1356,7 @@ async fn offset_path(_: impl Ctx, content: Item<Vector>, distance: Item<f64>, jo
let mut content = content;
let (distance, join, miter_limit) = (*distance.element(), *join.element(), *miter_limit.element());
let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform_attribute: DAffine2 = content.attr_cloned_or_default::<attr::Transform>();
let transform = Affine::new(transform_attribute.to_cols_array());
let vector = std::mem::take(content.element_mut());
@@ -1406,7 +1404,7 @@ where
let flattened: List<Vector> = graphic_list.clone().into_flattened_list();
// A fill exists when the canonical attribute carries paint
let has_fills: Vec<bool> = (0..flattened.len()).map(|index| has_paint_at(&flattened, index, ATTR_FILL)).collect();
let has_fills: Vec<bool> = (0..flattened.len()).map(|index| has_paint_at::<graphic_types::attr::Fill>(&flattened, index)).collect();
let mut output: List<Vector> = flattened
.into_iter()
@@ -1466,14 +1464,14 @@ where
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_attr::<graphic_types::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.rename(ATTR_STROKE, ATTR_FILL);
stroke_attributes.remove_attr::<graphic_types::attr::Fill>();
stroke_attributes.rename(graphic_types::attr::Stroke::name(), graphic_types::attr::Fill::name());
let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes);
@@ -1492,15 +1490,15 @@ where
// already holds the original transforms; pre-compensate by row 0's inverse so the renderer's
// `upstream_footprint *= row_0_transform` recursion cancels out and leaves the originals intact.
let mut graphic_list = graphic_list;
let row_0_transform: DAffine2 = output.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
let row_0_transform: DAffine2 = output.attr_cloned_or_default::<attr::Transform>(0);
if row_0_transform.matrix2.determinant().abs() > f64::EPSILON {
let inverse = row_0_transform.inverse();
for transform in graphic_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
for transform in graphic_list.iter_attr_values_mut_or_default::<attr::Transform>() {
*transform = inverse * *transform;
}
}
output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list);
output.set_attr::<graphic_types::attr::editor::MergedLayers>(0, graphic_list);
}
output
@@ -1574,14 +1572,14 @@ pub async fn combine_paths<T: IntoGraphicList>(_: impl Ctx, #[implementations(Li
// Concatenate every vector element's subpaths into the single output compound path
for index in 0..flattened.len() {
let Some(element) = flattened.element(index) else { continue };
let layer_path: List<NodeId> = flattened.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index).0;
let layer_path: List<NodeId> = flattened.attr_cloned_or_default::<attr::editor::LayerPath>(index).0;
let node_id = layer_path.iter_element_values().next_back().map(|node_id| node_id.0).unwrap_or_default();
let mut hasher = DefaultHasher::new();
(index, node_id).hash(&mut hasher);
let collision_hash_seed = hasher.finish();
let source_transform = flattened.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let source_transform = flattened.attr_cloned_or_default::<attr::Transform>(index);
output.concat(element, source_transform, collision_hash_seed);
// TODO: Make this instead use the first encountered stroke
@@ -1595,10 +1593,10 @@ pub async fn combine_paths<T: IntoGraphicList>(_: impl Ctx, #[implementations(Li
let source_attributes = flattened.clone_item_attributes(primary);
let mut attributes = ItemAttributeValues::new();
attributes.insert_cloned_from(&source_attributes, ATTR_FILL);
attributes.insert_cloned_from(&source_attributes, ATTR_STROKE);
attributes.insert_cloned_from(&source_attributes, graphic_types::attr::Fill::name());
attributes.insert_cloned_from(&source_attributes, graphic_types::attr::Stroke::name());
// Adopt the last input item's layer (if any) so the editor can also bucket clicks under a contributing child layer
attributes.insert_cloned_from(&source_attributes, ATTR_EDITOR_LAYER_PATH);
attributes.insert_cloned_from(&source_attributes, attr::editor::LayerPath::name());
bake_paint_transforms(&mut attributes, source_transform);
let output = std::mem::take(output_list.element_mut(0).unwrap());
@@ -1608,7 +1606,7 @@ pub async fn combine_paths<T: IntoGraphicList>(_: impl Ctx, #[implementations(Li
// Preserve a reference to the original upstream `List<Graphic>` so the renderer can recurse into it
// when collecting metadata, exposing the original child layers' click targets to editor tools.
// This is the same mechanism Boolean Operation uses to keep its inputs editable after the merge.
output_list.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list);
output_list.set_attr::<graphic_types::attr::editor::MergedLayers>(0, graphic_list);
output_list.into_iter().next().unwrap_or_default()
}
@@ -1654,12 +1652,12 @@ async fn sample_polyline(
stroke: std::mem::take(&mut content.element_mut().stroke),
};
// Transfer the stroke transform from the input vector content to the result.
result.set_stroke_transform(content.attribute_cloned_or_default(ATTR_TRANSFORM));
result.set_stroke_transform(content.attr_cloned_or_default::<attr::Transform>());
for local_bezpath in content.element().stroke_bezpath_iter() {
// Apply the transform to compute sample locations in world space (for correct distance-based spacing)
let mut world_bezpath = local_bezpath.clone();
let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform_attribute: DAffine2 = content.attr_cloned_or_default::<attr::Transform>();
world_bezpath.apply_affine(Affine::new(transform_attribute.to_cols_array()));
// Per-segment perimeter lengths (transform-baked) for distance-based spacing
@@ -1718,7 +1716,7 @@ async fn simplify(
let options = SimplifyOptions::default();
let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform_attribute: DAffine2 = content.attr_cloned_or_default::<attr::Transform>();
let transform = Affine::new(transform_attribute.to_cols_array());
let inverse_transform = transform.inverse();
@@ -1812,7 +1810,7 @@ async fn decimate(
points.iter().enumerate().filter(|(i, _)| keep[*i]).map(|(_, p)| *p).collect()
}
let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform_attribute: DAffine2 = content.attr_cloned_or_default::<attr::Transform>();
let transform = Affine::new(transform_attribute.to_cols_array());
let inverse_transform = transform.inverse();
@@ -1992,7 +1990,7 @@ async fn position_on_path(
let (progression, reverse, parameterized_distance) = (progression.into_element(), reverse.into_element(), parameterized_distance.into_element());
let euclidian = !parameterized_distance;
let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform: DAffine2 = content.attr_cloned_or_default::<attr::Transform>();
let mut bezpaths: Vec<_> = content.element().stroke_bezpath_iter().map(|bezpath| (bezpath, transform)).collect();
let bezpath_count = bezpaths.len() as f64;
let progression = progression.clamp(0., bezpath_count);
@@ -2031,7 +2029,7 @@ async fn tangent_on_path(
let (progression, reverse, parameterized_distance, radians) = (progression.into_element(), reverse.into_element(), parameterized_distance.into_element(), radians.into_element());
let euclidian = !parameterized_distance;
let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform: DAffine2 = content.attr_cloned_or_default::<attr::Transform>();
let mut bezpaths: Vec<_> = content.element().stroke_bezpath_iter().map(|bezpath| (bezpath, transform)).collect();
let bezpath_count = bezpaths.len() as f64;
let progression = progression.clamp(0., bezpath_count);
@@ -2222,7 +2220,7 @@ async fn jitter_points(
let (max_distance, seed, along_normals) = (*max_distance.element(), *seed.element(), *along_normals.element());
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform_attribute: DAffine2 = content.attr_cloned_or_default::<attr::Transform>();
let inverse_linear = inverse_linear_or_repair(transform_attribute.matrix2);
let deltas: Vec<_> = (0..content.element().point_domain.positions().len())
@@ -2247,7 +2245,7 @@ async fn jitter_points(
})
.collect();
let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform: DAffine2 = content.attr_cloned_or_default::<attr::Transform>();
apply_point_deltas(content.element_mut(), &deltas, transform);
content
@@ -2267,7 +2265,7 @@ async fn offset_points(
) -> Item<Vector> {
let mut content = content;
let distance = *distance.element();
let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform_attribute: DAffine2 = content.attr_cloned_or_default::<attr::Transform>();
let inverse_linear = inverse_linear_or_repair(transform_attribute.matrix2);
let deltas: Vec<_> = (0..content.element().point_domain.positions().len())
@@ -2285,7 +2283,7 @@ async fn offset_points(
})
.collect();
let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform: DAffine2 = content.attr_cloned_or_default::<attr::Transform>();
apply_point_deltas(content.element_mut(), &deltas, transform);
content
@@ -2409,8 +2407,8 @@ async fn morph<I: IntoGraphicList>(
}
fn lerp_gradient_transform(gradient_list_a: &List<Gradient>, gradient_list_b: &List<Gradient>, time: f64) -> DAffine2 {
let transform_a = gradient_list_a.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0);
let transform_b = gradient_list_b.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0);
let transform_a = gradient_list_a.attr_cloned_or_default::<attr::Transform>(0);
let transform_b = gradient_list_b.attr_cloned_or_default::<attr::Transform>(0);
let start_a = transform_a.translation;
let end_a = transform_a.translation + transform_a.matrix2.x_axis;
@@ -2470,7 +2468,7 @@ async fn morph<I: IntoGraphicList>(
let metadata_source = if time < 0.5 { gradient_list_a } else { gradient_list_b };
let mut gradient_list = metadata_source.clone();
gradient_list.set_attribute(ATTR_TRANSFORM, 0, lerp_gradient_transform(gradient_list_a, gradient_list_b, time));
gradient_list.set_attr::<attr::Transform>(0, lerp_gradient_transform(gradient_list_a, gradient_list_b, time));
gradient_with_stops(gradient_list, stops)
}),
@@ -2499,7 +2497,7 @@ async fn morph<I: IntoGraphicList>(
let default_polyline = || {
let mut default_path = BezPath::new();
for index in 0..content.len() {
let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let transform_attribute: DAffine2 = content.attr_cloned_or_default::<attr::Transform>(index);
let origin = transform_attribute.translation;
let point = kurbo::Point::new(origin.x, origin.y);
if index == 0 {
@@ -2513,7 +2511,7 @@ async fn morph<I: IntoGraphicList>(
let control_bezpaths: Vec<BezPath> = {
// User-provided path: collect all subpaths with the path's transform applied
let path_transform: DAffine2 = path.attribute_cloned_or_default(ATTR_TRANSFORM);
let path_transform: DAffine2 = path.attr_cloned_or_default::<attr::Transform>();
let paths: Vec<BezPath> = path
.element()
.stroke_bezpath_iter()
@@ -2585,8 +2583,8 @@ async fn morph<I: IntoGraphicList>(
if content.element(source_index).is_none() || content.element(target_index).is_none() {
return 0.;
}
let source_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, source_index);
let target_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, target_index);
let source_transform: DAffine2 = content.attr_cloned_or_default::<attr::Transform>(source_index);
let target_transform: DAffine2 = content.attr_cloned_or_default::<attr::Transform>(target_index);
let (s_angle, s_scale, s_skew) = source_transform.decompose_rotation_scale_skew();
let (t_angle, t_scale, t_skew) = target_transform.decompose_rotation_scale_skew();
@@ -2658,14 +2656,14 @@ async fn morph<I: IntoGraphicList>(
};
// Lerp blending attributes: opacity/fill interpolate, blend_mode/clip step at the midpoint
let source_blend_mode: BlendMode = content.attribute_cloned_or_default(ATTR_BLEND_MODE, source_index);
let target_blend_mode: BlendMode = content.attribute_cloned_or_default(ATTR_BLEND_MODE, target_index);
let source_opacity: f64 = content.attribute_cloned_or(ATTR_OPACITY, source_index, 1.);
let target_opacity: f64 = content.attribute_cloned_or(ATTR_OPACITY, target_index, 1.);
let source_fill: f64 = content.attribute_cloned_or(ATTR_OPACITY_FILL, source_index, 1.);
let target_fill: f64 = content.attribute_cloned_or(ATTR_OPACITY_FILL, target_index, 1.);
let source_clip: bool = content.attribute_cloned_or_default(ATTR_CLIPPING_MASK, source_index);
let target_clip: bool = content.attribute_cloned_or_default(ATTR_CLIPPING_MASK, target_index);
let source_blend_mode: BlendMode = content.attr_cloned_or_default::<attr::BlendMode>(source_index);
let target_blend_mode: BlendMode = content.attr_cloned_or_default::<attr::BlendMode>(target_index);
let source_opacity: f64 = content.attr_cloned_or_default::<attr::Opacity>(source_index);
let target_opacity: f64 = content.attr_cloned_or_default::<attr::Opacity>(target_index);
let source_fill: f64 = content.attr_cloned_or_default::<attr::OpacityFill>(source_index);
let target_fill: f64 = content.attr_cloned_or_default::<attr::OpacityFill>(target_index);
let source_clip: bool = content.attr_cloned_or_default::<attr::ClippingMask>(source_index);
let target_clip: bool = content.attr_cloned_or_default::<attr::ClippingMask>(target_index);
let lerped_blend_mode = if time < 0.5 { source_blend_mode } else { target_blend_mode };
let lerped_opacity = source_opacity + (target_opacity - source_opacity) * time;
@@ -2687,8 +2685,8 @@ async fn morph<I: IntoGraphicList>(
// This decomposition must match the one used in Stroke::lerp so the renderer's stroke_transform.inverse()
// correctly cancels the element transform, keeping the stroke uniform when Stroke is after Transform.
let lerped_transform = {
let source_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, source_index);
let target_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, target_index);
let source_transform: DAffine2 = content.attr_cloned_or_default::<attr::Transform>(source_index);
let target_transform: DAffine2 = content.attr_cloned_or_default::<attr::Transform>(target_index);
let (s_angle, s_scale, s_skew) = source_transform.decompose_rotation_scale_skew();
let (t_angle, t_scale, t_skew) = target_transform.decompose_rotation_scale_skew();
@@ -2717,7 +2715,7 @@ async fn morph<I: IntoGraphicList>(
// in which case we skip pre-compensation to avoid propagating NaN through merged_layers transforms.
if lerped_transform.matrix2.determinant().abs() > f64::EPSILON {
let lerped_inverse = lerped_transform.inverse();
for transform in graphic_list_content.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
for transform in graphic_list_content.iter_attr_values_mut_or_default::<attr::Transform>() {
*transform = lerped_inverse * *transform;
}
}
@@ -2729,8 +2727,8 @@ async fn morph<I: IntoGraphicList>(
let endpoint_element = content.element(endpoint_index).unwrap();
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.set_attr::<attr::Transform>(lerped_transform);
attributes.set_attr::<graphic_types::attr::editor::MergedLayers>(graphic_list_content);
return Item::from_parts(endpoint_element.clone(), attributes);
}
@@ -2756,13 +2754,13 @@ async fn morph<I: IntoGraphicList>(
let mut vector = Vector { stroke, ..Default::default() };
let fill_paint = {
let source = graphic_list_at(&content, source_index, ATTR_FILL);
let target = graphic_list_at(&content, target_index, ATTR_FILL);
let source = graphic_list_at::<graphic_types::attr::Fill>(&content, source_index);
let target = graphic_list_at::<graphic_types::attr::Fill>(&content, target_index);
lerp_graphic(source.as_deref(), target.as_deref(), time)
};
let stroke_paint = {
let source = graphic_list_at(&content, source_index, ATTR_STROKE);
let target = graphic_list_at(&content, target_index, ATTR_STROKE);
let source = graphic_list_at::<graphic_types::attr::Stroke>(&content, source_index);
let target = graphic_list_at::<graphic_types::attr::Stroke>(&content, target_index);
lerp_graphic(source.as_deref(), target.as_deref(), time)
};
@@ -2913,31 +2911,31 @@ async fn morph<I: IntoGraphicList>(
// the click-target identity (so the editor can route clicks back to one of the contributing layers)
let primary_index = if time < 0.5 { source_index } else { target_index };
let mut item = Item::new_from_element(vector);
item.set_attribute(ATTR_TRANSFORM, lerped_transform);
item.set_attr::<attr::Transform>(lerped_transform);
// Propagate each blending/layer column only when the input carries it, so attribute presence stays determined by the graph rather than by runtime values
if content.attribute::<BlendMode>(ATTR_BLEND_MODE, source_index).is_some() {
item.set_attribute(ATTR_BLEND_MODE, lerped_blend_mode);
if content.attr::<attr::BlendMode>(source_index).is_some() {
item.set_attr::<attr::BlendMode>(lerped_blend_mode);
}
if content.attribute::<f64>(ATTR_OPACITY, source_index).is_some() {
item.set_attribute(ATTR_OPACITY, lerped_opacity);
if content.attr::<attr::Opacity>(source_index).is_some() {
item.set_attr::<attr::Opacity>(lerped_opacity);
}
if content.attribute::<f64>(ATTR_OPACITY_FILL, source_index).is_some() {
item.set_attribute(ATTR_OPACITY_FILL, lerped_fill);
if content.attr::<attr::OpacityFill>(source_index).is_some() {
item.set_attr::<attr::OpacityFill>(lerped_fill);
}
if content.attribute::<bool>(ATTR_CLIPPING_MASK, source_index).is_some() {
item.set_attribute(ATTR_CLIPPING_MASK, lerped_clip);
if content.attr::<attr::ClippingMask>(source_index).is_some() {
item.set_attr::<attr::ClippingMask>(lerped_clip);
}
if let Some(layer_path) = content.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, primary_index) {
item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
if let Some(layer_path) = content.attr::<attr::editor::LayerPath>(primary_index) {
item.set_attr::<attr::editor::LayerPath>(layer_path.clone());
}
item.set_attribute(ATTR_EDITOR_MERGED_LAYERS, graphic_list_content);
item.set_attr::<graphic_types::attr::editor::MergedLayers>(graphic_list_content);
if let Some(fill) = fill_paint {
item.set_attribute(ATTR_FILL, fill);
item.set_attr::<graphic_types::attr::Fill>(fill);
}
if let Some(stroke) = stroke_paint {
item.set_attribute(ATTR_STROKE, stroke);
item.set_attr::<graphic_types::attr::Stroke>(stroke);
}
item
@@ -3217,7 +3215,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
fn bevel(_: impl Ctx, source: Item<Vector>, #[default(10.)] distance: Item<Length>) -> Item<Vector> {
let distance = *distance.element();
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform: DAffine2 = source.attr_cloned_or_default::<attr::Transform>();
let (element, attributes) = source.into_parts();
Item::from_parts(bevel_algorithm(element, transform, distance), attributes)
@@ -3233,7 +3231,7 @@ fn close_path(_: impl Ctx, source: Item<Vector>) -> Item<Vector> {
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
fn point_inside(_: impl Ctx, source: Item<Vector>, point: Item<DVec2>) -> Item<bool> {
let point = point.into_element();
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform: DAffine2 = source.attr_cloned_or_default::<attr::Transform>();
let inside = source.element().check_point_inside_shape(transform, point);
Item::new_from_element(inside)
@@ -3292,7 +3290,7 @@ async fn index_points(
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn path_length(_: impl Ctx, source: Item<Vector>) -> Item<f64> {
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform: DAffine2 = source.attr_cloned_or_default::<attr::Transform>();
let length = source
.element()
.stroke_bezpath_iter()
@@ -3310,7 +3308,7 @@ async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<Cont
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
let vector = content.eval(new_ctx).await;
let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform: DAffine2 = vector.attr_cloned_or_default::<attr::Transform>();
let area_scale = transform.matrix2.determinant().abs();
let area = vector.element().stroke_bezpath_iter().map(|subpath| subpath.area() * area_scale).sum::<f64>();
@@ -3323,7 +3321,7 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
let vector = content.eval(new_ctx).await;
let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM);
let transform: DAffine2 = vector.attr_cloned_or_default::<attr::Transform>();
let position = element_centroid(vector.element(), transform, centroid_type);
Item::new_from_element(position)
@@ -3390,7 +3388,7 @@ mod test {
fn create_vector_item(bezpath: BezPath, transform: DAffine2) -> Item<Vector> {
let mut row = Vector::default();
row.append_bezpath(bezpath);
Item::new_from_element(row).with_attribute(ATTR_TRANSFORM, transform)
Item::new_from_element(row).with_attr::<attr::Transform>(transform)
}
fn item<T>(value: T) -> Item<T> {
@@ -3559,7 +3557,7 @@ mod test {
// Test a rectangular path with non-zero rotation
let square = Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY));
let mut square = List::new_from_element(square);
square.with_attribute_mut_or_default(ATTR_TRANSFORM, 0, |t: &mut DAffine2| *t *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4));
square.with_attr_mut_or_default::<attr::Transform, _, _>(0, |t| *t *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4));
let bounding_box = BoundingBoxNodeMapped { content: FutureWrapperNode(square) }.eval(Footprint::default()).await;
let bounding_box = bounding_box.element(0).unwrap();
assert_eq!(bounding_box.region_manipulator_groups().count(), 1);
@@ -3692,7 +3690,7 @@ mod test {
async fn morph() {
let mut rectangles = vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY));
let mut second_rectangle = rectangles.clone_item(0).unwrap();
*second_rectangle.attribute_mut_or_insert_default::<DAffine2>(ATTR_TRANSFORM) *= DAffine2::from_translation((-100., -100.).into());
*second_rectangle.attr_mut_or_insert_default::<attr::Transform>() *= DAffine2::from_translation((-100., -100.).into());
rectangles.push(second_rectangle);
let morphed = super::morph(
@@ -3712,7 +3710,7 @@ mod test {
vec![DVec2::new(0., 0.), DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]
);
// The interpolated transform carries the midpoint translation (approximate due to arc-length parameterization)
assert!((morphed.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0).translation - DVec2::new(-50., -50.)).length() < 1e-3);
assert!((morphed.attr_cloned_or_default::<attr::Transform>(0).translation - DVec2::new(-50., -50.)).length() < 1e-3);
}
#[tokio::test]
@@ -3724,11 +3722,11 @@ 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_attr::<attr::Transform>(DAffine2::IDENTITY)
.with_attr::<graphic_types::attr::Fill>(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_attr::<attr::Transform>(DAffine2::from_translation((-100., -100.).into()))
.with_attr::<graphic_types::attr::Fill>(List::new_from_element(Color::BLUE).into_graphic_list());
let mut content = List::new_from_item(item_a);
content.push(item_b);
@@ -3744,7 +3742,7 @@ mod test {
.await;
let morphed = List::new_from_item(morphed);
let fill = graphic_list_at(&morphed, 0, ATTR_FILL).expect("Morph should keep the fill paint at the midpoint");
let fill = graphic_list_at::<graphic_types::attr::Fill>(&morphed, 0).expect("Morph should keep the fill paint at the midpoint");
// Interpolated color between red and blue should have >0 value on both R and B
let Some(Graphic::Color(colors)) = fill.element(0) else {
@@ -3825,7 +3823,7 @@ mod test {
source.push(curve.as_path_el());
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.));
let vector_item = Item::new_from_element(Vector::from_bezpath(source)).with_attribute(ATTR_TRANSFORM, transform);
let vector_item = Item::new_from_element(Vector::from_bezpath(source)).with_attr::<attr::Transform>(transform);
let beveled = super::bevel((), vector_item, Item::new_from_element(2_f64.sqrt() * 100.));
let beveled = beveled.element();