Migrate attribute call sites from string keys to typed keys

This commit is contained in:
Timon
2026-07-23 11:49:35 -07:00
committed by Keavon Chambers
parent 58178fd48e
commit 475c67facc
31 changed files with 529 additions and 511 deletions
@@ -2774,7 +2774,7 @@ impl DocumentMessageHandler {
let has_fill = fill_graphic_list.is_some_and(|list| is_paint_present(list)); let has_fill = fill_graphic_list.is_some_and(|list| is_paint_present(list));
// `Vector.stroke` captures stroke geometry, even with weight 0 or transparent paint. // `Vector.stroke` captures stroke geometry, even with weight 0 or transparent paint.
// So stroke visibility must be checked from `ATTR_STROKE`, the paint source of truth. // So stroke visibility must be checked from `graphic_types::attr::Stroke`, the paint source of truth.
let stroke_visible = stroke_graphic_list.is_some_and(|list| list.element(0).is_some_and(|g| !g.is_fully_transparent())); let stroke_visible = stroke_graphic_list.is_some_and(|list| list.element(0).is_some_and(|g| !g.is_fully_transparent()));
let has_stroke = stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible; let has_stroke = stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible;
@@ -175,7 +175,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
DocumentNode { DocumentNode {
inputs: vec![ inputs: vec![
NodeInput::node(NodeId(1), 0), NodeInput::node(NodeId(1), 0),
NodeInput::value(TaggedValue::String(graphene_std::ATTR_EDITOR_LAYER_PATH.to_string()), false), NodeInput::value(TaggedValue::String(attr::editor::LayerPath::name().to_string()), false),
NodeInput::node(NodeId(2), 0), NodeInput::node(NodeId(2), 0),
], ],
implementation: DocumentNodeImplementation::ProtoNode(graphic::write_attribute::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(graphic::write_attribute::IDENTIFIER),
@@ -305,7 +305,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
DocumentNode { DocumentNode {
inputs: vec![ inputs: vec![
NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(0), 0),
NodeInput::value(TaggedValue::String(graphene_std::ATTR_EDITOR_LAYER_PATH.to_string()), false), NodeInput::value(TaggedValue::String(attr::editor::LayerPath::name().to_string()), false),
NodeInput::node(NodeId(1), 0), NodeInput::node(NodeId(1), 0),
], ],
implementation: DocumentNodeImplementation::ProtoNode(graphic::write_attribute::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(graphic::write_attribute::IDENTIFIER),
@@ -12,7 +12,7 @@ use crate::messages::tool::common_functionality::shapes::shape_utility::format_r
use core::borrow::Borrow; use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, PI, TAU}; use core::f64::consts::{FRAC_PI_2, PI, TAU};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::ATTR_TRANSFORM; use graphene_std::attr;
use graphene_std::list::List; use graphene_std::list::List;
use graphene_std::math::quad::Quad; use graphene_std::math::quad::Quad;
use graphene_std::subpath::{self, Subpath}; use graphene_std::subpath::{self, Subpath};
@@ -1169,7 +1169,7 @@ impl OverlayContextInternal {
// Use the existing bezier_to_path infrastructure to convert Vector to BezPath // Use the existing bezier_to_path infrastructure to convert Vector to BezPath
let mut path = BezPath::new(); let mut path = BezPath::new();
let mut last_point = None; let mut last_point = None;
let transform: DAffine2 = text_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform = text_list.attr_cloned_or_default::<attr::Transform>(index);
let Some(element) = text_list.element(index) else { continue }; let Some(element) = text_list.element(index) else { continue };
for (_, bezier, start_id, end_id) in element.segment_iter() { for (_, bezier, start_id, end_id) in element.segment_iter() {
@@ -41,10 +41,10 @@ pub struct DocumentMetadata {
/// Vector data keyed by layer ID, used as fallback when no Path node exists. /// Vector data keyed by layer ID, used as fallback when no Path node exists.
/// This provides accurate SegmentIds for layers without explicit Path nodes. /// This provides accurate SegmentIds for layers without explicit Path nodes.
pub layer_vector_data: HashMap<LayerNodeIdentifier, Arc<Vector>>, pub layer_vector_data: HashMap<LayerNodeIdentifier, Arc<Vector>>,
/// Per-layer `ATTR_FILL` attribute, exposed so message handlers can read paint /// Per-layer `graphic_types::attr::Fill` attribute, exposed so message handlers can read paint
/// information that lives on the list. /// information that lives on the list.
pub layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>, pub layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>,
/// Per-layer `ATTR_STROKE` attribute, exposed so message handlers can read /// Per-layer `graphic_types::attr::Stroke` attribute, exposed so message handlers can read
/// stroke paint information that lives on the list. /// stroke paint information that lives on the list.
pub layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>, pub layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>,
/// Transform from document space to viewport space. /// Transform from document space to viewport space.
@@ -3454,12 +3454,12 @@ impl NodeNetworkInterface {
self.document_metadata.layer_vector_data = new_layer_vector_data; self.document_metadata.layer_vector_data = new_layer_vector_data;
} }
/// Update the per-layer `ATTR_FILL` snapshot. /// Update the per-layer `graphic_types::attr::Fill` snapshot.
pub fn update_fill_attributes(&mut self, new_layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>) { pub fn update_fill_attributes(&mut self, new_layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>) {
self.document_metadata.layer_fill_attributes = new_layer_fill_attributes; self.document_metadata.layer_fill_attributes = new_layer_fill_attributes;
} }
/// Update the per-layer `ATTR_STROKE` snapshot. /// Update the per-layer `graphic_types::attr::Stroke` snapshot.
pub fn update_stroke_attributes(&mut self, new_layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>) { pub fn update_stroke_attributes(&mut self, new_layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>) {
self.document_metadata.layer_stroke_attributes = new_layer_stroke_attributes; self.document_metadata.layer_stroke_attributes = new_layer_stroke_attributes;
} }
@@ -572,6 +572,7 @@ impl Fsm for ArtboardToolFsmState {
mod test_artboard { mod test_artboard {
pub use crate::test_utils::test_prelude::*; pub use crate::test_utils::test_prelude::*;
use graphene_std::Artboard; use graphene_std::Artboard;
use graphene_std::attr;
use graphene_std::list::List; use graphene_std::list::List;
async fn get_artboards(editor: &mut EditorTestUtils) -> List<Artboard> { async fn get_artboards(editor: &mut EditorTestUtils) -> List<Artboard> {
@@ -601,8 +602,8 @@ mod test_artboard {
let artboards = get_artboards(editor).await; let artboards = get_artboards(editor).await;
let artboards = (0..artboards.len()) let artboards = (0..artboards.len())
.map(|index| { .map(|index| {
let location: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_LOCATION, index); let location = artboards.attr_cloned_or_default::<attr::Location>(index);
let dimensions: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_DIMENSIONS, index); let dimensions = artboards.attr_cloned_or_default::<attr::Dimensions>(index);
ArtboardLayoutDocument::new(location, dimensions) ArtboardLayoutDocument::new(location, dimensions)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
+2 -2
View File
@@ -15,7 +15,7 @@ use graphene_std::raster::{CPU, Raster};
use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box}; use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box};
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::{Vector, graphic_types}; use graphene_std::vector::{Vector, graphic_types};
use graphene_std::{ATTR_TRANSFORM, Context, Graphic, NodeInputDecleration}; use graphene_std::{Context, Graphic, NodeInputDecleration, attr};
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
use std::any::Any; use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
@@ -870,7 +870,7 @@ fn redirect_export_chain(network: &mut NodeNetwork, full_path: &[NodeId]) -> boo
fn measure_fill_geometry(data: &Arc<dyn Any + Send + Sync>) -> Option<(DAffine2, DAffine2)> { fn measure_fill_geometry(data: &Arc<dyn Any + Send + Sync>) -> Option<(DAffine2, DAffine2)> {
if let Some(list) = introspected_output::<List<Vector>>(data) { if let Some(list) = introspected_output::<List<Vector>>(data) {
let vector = list.element(0)?; let vector = list.element(0)?;
let item_transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); let item_transform = list.attr_cloned_or_default::<attr::Transform>(0);
let bounds = vector.nonzero_bounding_box(); let bounds = vector.nonzero_bounding_box();
let bounding_box_affine = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); let bounding_box_affine = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
return Some((bounding_box_affine, item_transform)); return Some((bounding_box_affine, item_transform));
+3 -2
View File
@@ -9,6 +9,7 @@ use graph_craft::document::{NodeId, NodeNetwork};
use graph_craft::graphene_compiler::Compiler; use graph_craft::graphene_compiler::Compiler;
use graph_craft::proto::GraphErrors; use graph_craft::proto::GraphErrors;
use graphene_std::application_io::{ApplicationIo, ExportFormat, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig, Texture}; use graphene_std::application_io::{ApplicationIo, ExportFormat, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig, Texture};
use graphene_std::attr;
use graphene_std::bounds::RenderBoundingBox; use graphene_std::bounds::RenderBoundingBox;
use graphene_std::list::{Item, List}; use graphene_std::list::{Item, List};
use graphene_std::memo::IORecord; use graphene_std::memo::IORecord;
@@ -536,8 +537,8 @@ impl NodeRuntime {
fn artboard_clip_bounds(artboards: &List<Artboard>) -> RenderBoundingBox { fn artboard_clip_bounds(artboards: &List<Artboard>) -> RenderBoundingBox {
let mut combined: Option<[DVec2; 2]> = None; let mut combined: Option<[DVec2; 2]> = None;
for index in 0..artboards.len() { for index in 0..artboards.len() {
let location: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_LOCATION, index); let location = artboards.attr_cloned_or_default::<attr::Location>(index);
let dimensions: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_DIMENSIONS, index); let dimensions = artboards.attr_cloned_or_default::<attr::Dimensions>(index);
let bounds = [location, location + dimensions]; let bounds = [location, location + dimensions];
combined = Some(match combined { combined = Some(match combined {
Some(existing) => [existing[0].min(bounds[0]), existing[1].max(bounds[1])], Some(existing) => [existing[0].min(bounds[0]), existing[1].max(bounds[1])],
@@ -185,7 +185,7 @@ fn transform_network(content: TaggedValue, rotation: TaggedValue) -> ProtoNetwor
#[test] #[test]
fn transform_composes_onto_item_wire() { fn transform_composes_onto_item_wire() {
use glam::{DAffine2, DVec2}; use glam::DVec2;
let network = transform_network(TaggedValue::TypeDefault(item!(Vector)), TaggedValue::F64(0.)); let network = transform_network(TaggedValue::TypeDefault(item!(Vector)), TaggedValue::F64(0.));
let output = network.output; let output = network.output;
@@ -197,7 +197,7 @@ fn transform_composes_onto_item_wire() {
let context: Context = None; let context: Context = None;
let result: Option<Item<Vector>> = futures::executor::block_on(tree.eval(output, context)); let result: Option<Item<Vector>> = futures::executor::block_on(tree.eval(output, context));
let item = result.expect("A rank-0 chain through Transform should stay rank 0"); let item = result.expect("A rank-0 chain through Transform should stay rank 0");
let transform = item.attribute_cloned_or_default::<DAffine2>(core_types::ATTR_TRANSFORM); let transform = item.attr_cloned_or_default::<core_types::attr::Transform>();
assert_eq!(transform.translation, DVec2::new(5., 0.), "The translation should compose onto the item's transform attribute"); assert_eq!(transform.translation, DVec2::new(5., 0.), "The translation should compose onto the item's transform attribute");
} }
@@ -219,8 +219,8 @@ fn transform_broadcasts_item_content_across_a_framed_parameter() {
let list = result.expect("The broadcast should produce a List"); let list = result.expect("The broadcast should produce a List");
assert_eq!(list.len(), 2, "One output item per frame slot"); assert_eq!(list.len(), 2, "One output item per frame slot");
let first: DAffine2 = list.attribute_cloned_or_default(core_types::ATTR_TRANSFORM, 0); let first: DAffine2 = list.attr_cloned_or_default::<core_types::attr::Transform>(0);
let second: DAffine2 = list.attribute_cloned_or_default(core_types::ATTR_TRANSFORM, 1); let second: DAffine2 = list.attr_cloned_or_default::<core_types::attr::Transform>(1);
assert!((first.matrix2.col(0).y - 0.).abs() < 1e-10, "Slot 0 should be unrotated"); assert!((first.matrix2.col(0).y - 0.).abs() < 1e-10, "Slot 0 should be unrotated");
assert!((second.matrix2.col(0).y - 1.).abs() < 1e-10, "Slot 1 should be rotated 90 degrees"); assert!((second.matrix2.col(0).y - 1.).abs() < 1e-10, "Slot 1 should be rotated 90 degrees");
} }
@@ -320,7 +320,7 @@ fn value_wires_materialize_as_items_at_resolution() {
let context: Context = None; let context: Context = None;
let result: Option<Item<DAffine2>> = futures::executor::block_on(tree.eval(NodeId(5), context)); let result: Option<Item<DAffine2>> = futures::executor::block_on(tree.eval(NodeId(5), context));
let item = result.expect("A value matrix should flow through Transform as an Item"); let item = result.expect("A value matrix should flow through Transform as an Item");
let transform = item.attribute_cloned_or_default::<DAffine2>(core_types::ATTR_TRANSFORM); let transform = item.attr_cloned_or_default::<core_types::attr::Transform>();
assert_eq!(transform.translation, DVec2::new(7., 0.), "The translation should compose onto the gained transform attribute"); assert_eq!(transform.translation, DVec2::new(7., 0.), "The translation should compose onto the gained transform attribute");
} }
@@ -512,6 +512,14 @@ impl ListDyn {
.find_map(|(k, attribute)| if k == key { attribute.get_any(index)?.downcast_ref::<U>() } else { None }) .find_map(|(k, attribute)| if k == key { attribute.get_any(index)?.downcast_ref::<U>() } else { None })
} }
/// Returns a reference to the attribute value at the given runtime key and item index, downcast to `U`, if present and matching.
/// For keys known at compile time use [`Self::attr`]; this variant is for keys only known at runtime (e.g. the attribute nodes).
pub fn attribute_dyn<U: 'static>(&self, key: &str, index: usize) -> Option<&U> {
self.attributes
.iter()
.find_map(|(k, attribute)| if k == key { attribute.get_any(index)?.downcast_ref::<U>() } else { None })
}
/// Returns a reference to the value of the typed attribute at the given item index, if present. /// Returns a reference to the value of the typed attribute at the given item index, if present.
pub fn attr<A: Attr>(&self, index: usize) -> Option<&A::Value> { pub fn attr<A: Attr>(&self, index: usize) -> Option<&A::Value> {
self.attribute(A::name(), index) self.attribute(A::name(), index)
@@ -1,9 +1,10 @@
use core_types::Color;
use core_types::attr::{self, Attr};
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash; use core_types::graphene_hash::CacheHash;
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, NodeIdPath}; use core_types::list::{Item, ItemAttributeValues, List};
use core_types::ops::FromAnchorPosition; use core_types::ops::FromAnchorPosition;
use core_types::render_complexity::RenderComplexity; use core_types::render_complexity::RenderComplexity;
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use raster_types::{CPU, GPU, Raster}; use raster_types::{CPU, GPU, Raster};
@@ -119,32 +120,32 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
// Whether the parent carries each attribute: a structural fact (column presence), never a value comparison. // Whether the parent carries each attribute: a structural fact (column presence), never a value comparison.
// Flattening composes a parent attribute onto its children only when the parent has it, // Flattening composes a parent attribute onto its children only when the parent has it,
// so an absent parent attribute never invents a column the children didn't already have. // so an absent parent attribute never invents a column the children didn't already have.
let parent_has_transform = current_graphic_item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some(); let parent_has_transform = current_graphic_item.attr::<attr::Transform>().is_some();
let parent_has_opacity = current_graphic_item.attribute::<f64>(ATTR_OPACITY).is_some(); let parent_has_opacity = current_graphic_item.attr::<attr::Opacity>().is_some();
let parent_has_fill = current_graphic_item.attribute::<f64>(ATTR_OPACITY_FILL).is_some(); let parent_has_fill = current_graphic_item.attr::<attr::OpacityFill>().is_some();
let parent_has_layer_path = current_graphic_item.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH).is_some(); let parent_has_layer_path = current_graphic_item.attr::<attr::editor::LayerPath>().is_some();
let layer_path: NodeIdPath = current_graphic_item.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH); let layer_path = current_graphic_item.attr_cloned_or_default::<attr::editor::LayerPath>();
let current_transform: DAffine2 = current_graphic_item.attribute_cloned_or_default(ATTR_TRANSFORM); let current_transform = current_graphic_item.attr_cloned_or_default::<attr::Transform>();
let current_opacity: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY, 1.); let current_opacity = current_graphic_item.attr_cloned_or_default::<attr::Opacity>();
let current_fill: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.); let current_fill = current_graphic_item.attr_cloned_or_default::<attr::OpacityFill>();
match current_graphic_item.into_element() { match current_graphic_item.into_element() {
// Compose the parent's transform/opacity/fill onto each child, but only for attributes the parent carries. // Compose the parent's transform/opacity/fill onto each child, but only for attributes the parent carries.
// A child lacking one is padded with the composition identity (`1.` for opacity/fill, identity for transform), so composing through it is a no-op. // A child lacking one is padded with the composition identity (`1.` for opacity/fill, identity for transform), so composing through it is a no-op.
Graphic::Graphic(mut sub_list) => { Graphic::Graphic(mut sub_list) => {
if parent_has_transform { if parent_has_transform {
for v in sub_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) { for v in sub_list.iter_attr_values_mut_or_default::<attr::Transform>() {
*v = current_transform * *v; *v = current_transform * *v;
} }
} }
if parent_has_opacity { if parent_has_opacity {
for v in sub_list.iter_attribute_values_mut_or_default::<f64>(ATTR_OPACITY) { for v in sub_list.iter_attr_values_mut_or_default::<attr::Opacity>() {
*v *= current_opacity; *v *= current_opacity;
} }
} }
if parent_has_fill { if parent_has_fill {
for v in sub_list.iter_attribute_values_mut_or_default::<f64>(ATTR_OPACITY_FILL) { for v in sub_list.iter_attr_values_mut_or_default::<attr::OpacityFill>() {
*v *= current_fill; *v *= current_fill;
} }
} }
@@ -155,22 +156,22 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
other => { other => {
if let Some(typed_list) = extract_variant(other) { if let Some(typed_list) = extract_variant(other) {
for mut item in typed_list.into_iter() { for mut item in typed_list.into_iter() {
// Each `|| item.attribute(...)` keeps an attribute the item itself carries // Each `|| item.attr::<...>()` keeps an attribute the item itself carries
// (recomposed with the parent's identity value) even when the parent lacks it // (recomposed with the parent's identity value) even when the parent lacks it
if parent_has_transform || item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some() { if parent_has_transform || item.attr::<attr::Transform>().is_some() {
let item_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); let item_transform = item.attr_cloned_or_default::<attr::Transform>();
item.set_attribute(ATTR_TRANSFORM, current_transform * item_transform); item.set_attr::<attr::Transform>(current_transform * item_transform);
} }
if parent_has_opacity || item.attribute::<f64>(ATTR_OPACITY).is_some() { if parent_has_opacity || item.attr::<attr::Opacity>().is_some() {
let item_opacity: f64 = item.attribute_cloned_or(ATTR_OPACITY, 1.); let item_opacity = item.attr_cloned_or_default::<attr::Opacity>();
item.set_attribute(ATTR_OPACITY, current_opacity * item_opacity); item.set_attr::<attr::Opacity>(current_opacity * item_opacity);
} }
if parent_has_fill || item.attribute::<f64>(ATTR_OPACITY_FILL).is_some() { if parent_has_fill || item.attr::<attr::OpacityFill>().is_some() {
let item_fill: f64 = item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.); let item_fill = item.attr_cloned_or_default::<attr::OpacityFill>();
item.set_attribute(ATTR_OPACITY_FILL, current_fill * item_fill); item.set_attr::<attr::OpacityFill>(current_fill * item_fill);
} }
if parent_has_layer_path { if parent_has_layer_path {
item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone()); item.set_attr::<attr::editor::LayerPath>(layer_path.clone());
} }
output.push(item); output.push(item);
@@ -192,9 +193,9 @@ pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool {
graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty()) graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty())
} }
/// Look up the paint graphics stored under attribute for a vector item, in the canonical `List<Graphic>` form. /// Look up the paint graphics stored under the typed attribute for a vector item, in the canonical `List<Graphic>` form.
pub fn graphic_list_at<'a>(list: &'a List<Vector>, index: usize, attribute: &str) -> Option<Cow<'a, List<Graphic>>> { pub fn graphic_list_at<A: Attr<Value = List<Graphic>>>(list: &List<Vector>, index: usize) -> Option<Cow<'_, List<Graphic>>> {
list.attribute::<List<Graphic>>(attribute, index) list.attr::<A>(index)
.map(Cow::Borrowed) .map(Cow::Borrowed)
// Treat a blank paint attribute as absent so an empty attribute doesn't count as painted // Treat a blank paint attribute as absent so an empty attribute doesn't count as painted
.filter(|graphic_list| is_paint_present(graphic_list)) .filter(|graphic_list| is_paint_present(graphic_list))
@@ -202,25 +203,25 @@ pub fn graphic_list_at<'a>(list: &'a List<Vector>, index: usize, attribute: &str
/// Whether the item carries a non-blank canonical `List<Graphic>` paint attribute, /// Whether the item carries a non-blank canonical `List<Graphic>` paint attribute,
/// checked by borrowing without cloning the renderable list. /// checked by borrowing without cloning the renderable list.
pub fn has_paint_at(list: &List<Vector>, index: usize, attribute: &str) -> bool { pub fn has_paint_at<A: Attr<Value = List<Graphic>>>(list: &List<Vector>, index: usize) -> bool {
list.attribute::<List<Graphic>>(attribute, index).is_some_and(is_paint_present) list.attr::<A>(index).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 its canonical `List<Graphic>` form, the only representation paint readers accept.
pub fn set_paint_attribute(attributes: &mut ItemAttributeValues, key: &str, paint: impl IntoGraphicList) { pub fn set_paint_attribute<A: Attr<Value = List<Graphic>>>(attributes: &mut ItemAttributeValues, paint: impl IntoGraphicList) {
attributes.insert(key, paint.into_graphic_list()); attributes.set_attr::<A>(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 its canonical `List<Graphic>` 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) { pub fn set_paint_attribute_at<A: Attr<Value = List<Graphic>>, T>(list: &mut List<T>, index: usize, paint: impl IntoGraphicList) {
list.set_attribute(key, index, paint.into_graphic_list()); list.set_attr::<A>(index, paint.into_graphic_list());
} }
/// Bake the provided transform into the per-item transforms of the paint graphics stored under the /// Bake the provided transform into the per-item transforms of the paint graphics stored under the
/// canonical `List<Graphic>` fill and stroke attributes. /// canonical `List<Graphic>` fill and stroke attributes.
pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) { pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) {
fn bake_list_transform<T>(list: &mut List<T>, transform: DAffine2) { fn bake_list_transform<T>(list: &mut List<T>, transform: DAffine2) {
for item_transform in list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) { for item_transform in list.iter_attr_values_mut_or_default::<attr::Transform>() {
*item_transform = transform * *item_transform; *item_transform = transform * *item_transform;
} }
} }
@@ -240,7 +241,7 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA
} }
} }
for paint_key in [ATTR_FILL, ATTR_STROKE] { for paint_key in [crate::attr::Fill::name(), crate::attr::Stroke::name()] {
if let Some(graphics) = attributes.get_mut::<List<Graphic>>(paint_key) { if let Some(graphics) = attributes.get_mut::<List<Graphic>>(paint_key) {
bake_graphic_paint_transform(graphics, transform); bake_graphic_paint_transform(graphics, transform);
} }
@@ -306,10 +307,10 @@ impl IntoGraphicList for List<Vector> {
fn into_graphic_list(self) -> List<Graphic> { fn into_graphic_list(self) -> List<Graphic> {
// Propagate the `editor:layer_path` column (if present) from item 0 onto the wrapper Graphic item so a // Propagate the `editor:layer_path` column (if present) from item 0 onto the wrapper Graphic item so a
// subsequent `flatten_graphic_list` doesn't drop the inner Vector's layer stamp // subsequent `flatten_graphic_list` doesn't drop the inner Vector's layer stamp
let layer_path = self.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, 0).cloned(); let layer_path = self.attr::<attr::editor::LayerPath>(0).cloned();
let mut graphic_list = List::new_from_element(Graphic::Vector(self)); let mut graphic_list = List::new_from_element(Graphic::Vector(self));
if let Some(layer_path) = layer_path { if let Some(layer_path) = layer_path {
graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path); graphic_list.set_attr::<attr::editor::LayerPath>(0, layer_path);
} }
graphic_list graphic_list
} }
@@ -341,10 +342,10 @@ impl IntoGraphicList for List<Gradient> {
impl IntoGraphicList for List<String> { impl IntoGraphicList for List<String> {
fn into_graphic_list(self) -> List<Graphic> { fn into_graphic_list(self) -> List<Graphic> {
let layer_path = self.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, 0).cloned(); let layer_path = self.attr::<attr::editor::LayerPath>(0).cloned();
let mut graphic_list = List::new_from_element(Graphic::Text(self)); let mut graphic_list = List::new_from_element(Graphic::Text(self));
if let Some(layer_path) = layer_path { if let Some(layer_path) = layer_path {
graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path); graphic_list.set_attr::<attr::editor::LayerPath>(0, layer_path);
} }
graphic_list graphic_list
} }
@@ -416,7 +417,7 @@ impl Graphic {
pub fn had_clip_enabled(&self) -> bool { pub fn had_clip_enabled(&self) -> bool {
fn all_clipped<T>(list: &List<T>) -> bool { fn all_clipped<T>(list: &List<T>) -> bool {
list.iter_attribute_values_or_default::<bool>(ATTR_CLIPPING_MASK).all(|clip| clip) list.iter_attr_values_or_default::<attr::ClippingMask>().all(|clip| clip)
} }
match self { match self {
@@ -435,12 +436,12 @@ impl Graphic {
match self { match self {
Graphic::Vector(vector) => (0..vector.len()).all(|index| { Graphic::Vector(vector) => (0..vector.len()).all(|index| {
let Some(element) = vector.element(index) else { return false }; let Some(element) = vector.element(index) else { return false };
let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity = vector.attr_cloned_or_default::<attr::Opacity>(index);
let fill_opaque_or_absent = graphic_list_at(vector, index, ATTR_FILL).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque())); let fill_opaque_or_absent = graphic_list_at::<crate::attr::Fill>(vector, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque()));
let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke())
|| graphic_list_at(vector, index, ATTR_STROKE).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())); || graphic_list_at::<crate::attr::Stroke>(vector, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent
}), }),
@@ -453,15 +454,17 @@ impl Graphic {
Graphic::None => false, Graphic::None => false,
Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque), Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque),
Graphic::Vector(list) => { Graphic::Vector(list) => {
let is_paint_opaque_at = |key: &str, index: usize| graphic_list_at(list, index, key).is_some_and(|graphic_list| graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque())); fn is_paint_opaque_at<A: Attr<Value = List<Graphic>>>(list: &List<Vector>, index: usize) -> bool {
graphic_list_at::<A>(list, index).is_some_and(|graphic_list| graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque()))
}
!list.is_empty() !list.is_empty()
&& (0..list.len()).all(|i| { && (0..list.len()).all(|i| {
let Some(vector) = list.element(i) else { return false }; let Some(vector) = list.element(i) else { return false };
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.); let opacity = list.attr_cloned_or_default::<attr::Opacity>(i);
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.); let opacity_fill = list.attr_cloned_or_default::<attr::OpacityFill>(i);
let fill_opaque = opacity_fill >= 1. - f64::EPSILON && is_paint_opaque_at(ATTR_FILL, i); let fill_opaque = opacity_fill >= 1. - f64::EPSILON && is_paint_opaque_at::<crate::attr::Fill>(list, i);
let stroke_opaque_or_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_opaque_at(ATTR_STROKE, i); let stroke_opaque_or_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_opaque_at::<crate::attr::Stroke>(list, i);
opacity >= 1. - f64::EPSILON && fill_opaque && stroke_opaque_or_invisible opacity >= 1. - f64::EPSILON && fill_opaque && stroke_opaque_or_invisible
}) })
} }
@@ -477,16 +480,17 @@ impl Graphic {
Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent), Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent),
Graphic::Vector(list) => (0..list.len()).all(|i| { Graphic::Vector(list) => (0..list.len()).all(|i| {
let Some(vector) = list.element(i) else { return false }; let Some(vector) = list.element(i) else { return false };
let is_paint_fully_transparent_at = fn is_paint_fully_transparent_at<A: Attr<Value = List<Graphic>>>(list: &List<Vector>, index: usize) -> bool {
|key: &str, index: usize| graphic_list_at(list, index, key).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())); graphic_list_at::<A>(list, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()))
}
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.); let opacity = list.attr_cloned_or_default::<attr::Opacity>(i);
if opacity <= f64::EPSILON { if opacity <= f64::EPSILON {
return true; return true;
} }
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.); let opacity_fill = list.attr_cloned_or_default::<attr::OpacityFill>(i);
let fill_invisible = opacity_fill <= f64::EPSILON || is_paint_fully_transparent_at(ATTR_FILL, i); let fill_invisible = opacity_fill <= f64::EPSILON || is_paint_fully_transparent_at::<crate::attr::Fill>(list, i);
let stroke_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_fully_transparent_at(ATTR_STROKE, i); let stroke_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_fully_transparent_at::<crate::attr::Stroke>(list, i);
fill_invisible && stroke_invisible fill_invisible && stroke_invisible
}), }),
Graphic::Color(list) => list.iter_element_values().all(|color| color.a() == 0.), Graphic::Color(list) => list.iter_element_values().all(|color| color.a() == 0.),
@@ -644,7 +648,7 @@ mod tests {
fn flatten_does_not_invent_attributes() { fn flatten_does_not_invent_attributes() {
let graphics = List::new_from_element(vector_graphic()); let graphics = List::new_from_element(vector_graphic());
let flattened: List<Vector> = graphics.into_flattened_list(); let flattened: List<Vector> = graphics.into_flattened_list();
for key in [ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, ATTR_EDITOR_LAYER_PATH] { for key in [attr::Opacity::name(), attr::OpacityFill::name(), attr::Transform::name(), attr::editor::LayerPath::name()] {
assert!(!flattened.attribute_keys().any(|k| k == key), "flatten invented the `{key}` attribute"); assert!(!flattened.attribute_keys().any(|k| k == key), "flatten invented the `{key}` attribute");
} }
} }
@@ -653,20 +657,19 @@ mod tests {
#[test] #[test]
fn flatten_propagates_present_attributes() { fn flatten_propagates_present_attributes() {
let mut graphics = List::new_from_element(vector_graphic()); let mut graphics = List::new_from_element(vector_graphic());
graphics.set_attribute(ATTR_OPACITY, 0, 0.5_f64); graphics.set_attr::<attr::Opacity>(0, 0.5);
let flattened: List<Vector> = graphics.into_flattened_list(); let flattened: List<Vector> = graphics.into_flattened_list();
assert_eq!(flattened.attribute_cloned_or_default::<f64>(ATTR_OPACITY, 0), 0.5); assert_eq!(flattened.attr_cloned_or_default::<attr::Opacity>(0), 0.5);
let mut group = List::new_from_element(Graphic::Graphic(List::new_from_element(vector_graphic()))); let mut group = List::new_from_element(Graphic::Graphic(List::new_from_element(vector_graphic())));
group.set_attribute(ATTR_OPACITY, 0, 0.5_f64); group.set_attr::<attr::Opacity>(0, 0.5);
let flattened: List<Vector> = group.into_flattened_list(); let flattened: List<Vector> = group.into_flattened_list();
assert_eq!(flattened.attribute_cloned_or_default::<f64>(ATTR_OPACITY, 0), 0.5); assert_eq!(flattened.attr_cloned_or_default::<attr::Opacity>(0), 0.5);
} }
} }
#[cfg(test)] #[cfg(test)]
mod graphic_is_opaque_tests { mod graphic_is_opaque_tests {
use core_types::ATTR_SPREAD_METHOD;
use vector_types::{GradientSpreadMethod, GradientStop}; use vector_types::{GradientSpreadMethod, GradientStop};
use super::*; use super::*;
@@ -678,7 +681,7 @@ mod graphic_is_opaque_tests {
fn gradient_graphic(gradient: Gradient) -> Graphic { fn gradient_graphic(gradient: Gradient) -> Graphic {
let mut gradient_list = List::new_from_element(gradient); let mut gradient_list = List::new_from_element(gradient);
gradient_list.set_attribute(ATTR_SPREAD_METHOD, 0, GradientSpreadMethod::Pad); gradient_list.set_attr::<vector_types::attr::SpreadMethod>(0, GradientSpreadMethod::Pad);
Graphic::Gradient(gradient_list) Graphic::Gradient(gradient_list)
} }
@@ -3,7 +3,7 @@ use crate::{Render, RenderSvgSegmentList, SvgRender};
use core_types::color::SRGBA8; use core_types::color::SRGBA8;
use core_types::list::List; use core_types::list::List;
use core_types::uuid::generate_uuid; use core_types::uuid::generate_uuid;
use core_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color}; use core_types::{Color, attr};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphic_types::Graphic; use graphic_types::Graphic;
use graphic_types::vector_types::gradient::GradientType; use graphic_types::vector_types::gradient::GradientType;
@@ -93,9 +93,9 @@ impl RenderExt for List<Gradient> {
let mut stop = String::new(); let mut stop = String::new();
let Some(stops) = self.element(0) else { return 0 }; let Some(stops) = self.element(0) else { return 0 };
let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0); let gradient_type = self.attr_cloned_or_default::<vector_types::attr::GradientType>(0);
let local_gradient_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0); let local_gradient_transform = self.attr_cloned_or_default::<attr::Transform>(0);
let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0); let spread_method = self.attr_cloned_or_default::<vector_types::attr::SpreadMethod>(0);
for (position, color, original_midpoint) in stops.interpolated_samples() { for (position, color, original_midpoint) in stops.interpolated_samples() {
stop.push_str("<stop"); stop.push_str("<stop");
+141 -142
View File
@@ -1,26 +1,23 @@
use crate::render_ext::{PaintTarget, RenderExt}; use crate::render_ext::{PaintTarget, RenderExt};
use crate::to_peniko::{BlendModeExt, ToPenikoColor}; use crate::to_peniko::{BlendModeExt, ToPenikoColor};
use core_types::CacheHash; use core_types::CacheHash;
use core_types::attr;
use core_types::blending::BlendMode; use core_types::blending::BlendMode;
use core_types::bounds::BoundingBox; use core_types::bounds::BoundingBox;
use core_types::bounds::RenderBoundingBox; use core_types::bounds::RenderBoundingBox;
use core_types::color::Color; use core_types::color::Color;
use core_types::color::SRGBA8; use core_types::color::SRGBA8;
use core_types::consts::DEFAULT_FONT_SIZE; use core_types::consts::DEFAULT_FONT_SIZE;
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List, NodeIdPath}; use core_types::list::{Item, List};
use core_types::math::quad::Quad; use core_types::math::quad::Quad;
use core_types::render_complexity::RenderComplexity; use core_types::render_complexity::RenderComplexity;
use core_types::transform::Footprint; use core_types::transform::Footprint;
use core_types::uuid::{NodeId, generate_uuid}; use core_types::uuid::{NodeId, generate_uuid};
use core_types::{
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT,
ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD,
ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
};
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphene_hash::CacheHashWrapper; use graphene_hash::CacheHashWrapper;
use graphene_resource::Resource; use graphene_resource::Resource;
use graphic_types::attr as graphic_attr;
use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute}; use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute};
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture}; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
use graphic_types::vector_types::gradient::{Gradient, GradientType}; use graphic_types::vector_types::gradient::{Gradient, GradientType};
@@ -39,6 +36,8 @@ use std::fmt::Write;
use std::hash::Hash; use std::hash::Hash;
use std::ops::Deref; use std::ops::Deref;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use text_nodes::attr as text_attr;
use vector_types::attr as vector_attr;
use vector_types::gradient::GradientSpreadMethod; use vector_types::gradient::GradientSpreadMethod;
use vello::*; use vello::*;
@@ -360,7 +359,7 @@ fn emit_svg_fill_path(
attributes.push("d", d); attributes.push("d", d);
let matrix = format_transform_matrix(element_transform); let matrix = format_transform_matrix(element_transform);
if !matrix.is_empty() { if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix); attributes.push("transform", matrix);
} }
let defs = &mut attributes.0.svg_defs; let defs = &mut attributes.0.svg_defs;
let fill_attribute = fill_graphic_list let fill_attribute = fill_graphic_list
@@ -397,9 +396,9 @@ pub(crate) fn gradient_placement(transform: DAffine2, gradient_type: GradientTyp
fn create_peniko_gradient_brush(gradient_list: &List<Gradient>, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> { fn create_peniko_gradient_brush(gradient_list: &List<Gradient>, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
let stops = gradient_list.element(0)?; let stops = gradient_list.element(0)?;
let gradient_type: GradientType = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0); let gradient_type = gradient_list.attr_cloned_or_default::<vector_attr::GradientType>(0);
let gradient_transform: DAffine2 = gradient_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); let gradient_transform = gradient_list.attr_cloned_or_default::<attr::Transform>(0);
let spread_method: GradientSpreadMethod = gradient_list.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0); let spread_method = gradient_list.attr_cloned_or_default::<vector_attr::SpreadMethod>(0);
let mut peniko_stops = peniko::ColorStops::new(); let mut peniko_stops = peniko::ColorStops::new();
for (position, color, _) in stops.interpolated_samples() { for (position, color, _) in stops.interpolated_samples() {
@@ -457,10 +456,10 @@ pub struct RenderMetadata {
pub text_frames: HashMap<NodeId, DAffine2>, pub text_frames: HashMap<NodeId, DAffine2>,
pub clip_targets: HashSet<NodeId>, pub clip_targets: HashSet<NodeId>,
pub vector_data: HashMap<NodeId, Arc<Vector>>, pub vector_data: HashMap<NodeId, Arc<Vector>>,
/// Per-layer `ATTR_FILL` row attribute, exposed so message handlers can read it. /// Per-layer `graphic_types::attr::Fill` row attribute, exposed so message handlers can read it.
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pub fill_attributes: HashMap<NodeId, Arc<List<Graphic>>>, pub fill_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
/// Per-layer `ATTR_STROKE` row attribute, exposed so message handlers can read it. /// Per-layer `graphic_types::attr::Stroke` row attribute, exposed so message handlers can read it.
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pub stroke_attributes: HashMap<NodeId, Arc<List<Graphic>>>, pub stroke_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
pub backgrounds: Vec<Background>, pub backgrounds: Vec<Background>,
@@ -581,9 +580,9 @@ impl Render for Graphic {
metadata.upstream_footprints.insert(element_id, footprint); metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than the first item // TODO: Find a way to handle more than the first item
if !list.is_empty() { if !list.is_empty() {
let layer_path: List<NodeId> = list.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, 0).0; let layer_path: List<NodeId> = list.attr_cloned_or_default::<attr::editor::LayerPath>(0).0;
let layer = layer_path.iter_element_values().next_back().copied(); let layer = layer_path.iter_element_values().next_back().copied();
let transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); let transform = list.attr_cloned_or_default::<attr::Transform>(0);
metadata.first_element_source_id.insert(element_id, layer); metadata.first_element_source_id.insert(element_id, layer);
metadata.local_transforms.insert(element_id, transform); metadata.local_transforms.insert(element_id, transform);
@@ -594,7 +593,7 @@ impl Render for Graphic {
// TODO: Find a way to handle more than the first item // TODO: Find a way to handle more than the first item
if !list.is_empty() { if !list.is_empty() {
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); metadata.local_transforms.insert(element_id, list.attr_cloned_or_default::<attr::Transform>(0));
} }
} }
Graphic::RasterGPU(list) => { Graphic::RasterGPU(list) => {
@@ -602,7 +601,7 @@ impl Render for Graphic {
// TODO: Find a way to handle more than the first item // TODO: Find a way to handle more than the first item
if !list.is_empty() { if !list.is_empty() {
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); metadata.local_transforms.insert(element_id, list.attr_cloned_or_default::<attr::Transform>(0));
} }
} }
Graphic::Color(list) => { Graphic::Color(list) => {
@@ -610,7 +609,7 @@ impl Render for Graphic {
// TODO: Find a way to handle more than the first item // TODO: Find a way to handle more than the first item
if !list.is_empty() { if !list.is_empty() {
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); metadata.local_transforms.insert(element_id, list.attr_cloned_or_default::<attr::Transform>(0));
} }
} }
Graphic::Gradient(list) => { Graphic::Gradient(list) => {
@@ -618,7 +617,7 @@ impl Render for Graphic {
// TODO: Find a way to handle more than the first item // TODO: Find a way to handle more than the first item
if !list.is_empty() { if !list.is_empty() {
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); metadata.local_transforms.insert(element_id, list.attr_cloned_or_default::<attr::Transform>(0));
} }
} }
Graphic::Text(list) => { Graphic::Text(list) => {
@@ -626,7 +625,7 @@ impl Render for Graphic {
// TODO: Find a way to handle more than the first item // TODO: Find a way to handle more than the first item
if !list.is_empty() { if !list.is_empty() {
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); metadata.local_transforms.insert(element_id, list.attr_cloned_or_default::<attr::Transform>(0));
} }
} }
} }
@@ -699,10 +698,10 @@ impl Render for Graphic {
/// Reads the artboard metadata for the item at `index` from a `List<Artboard>`. /// Reads the artboard metadata for the item at `index` from a `List<Artboard>`.
fn read_artboard_attributes(list: &List<Artboard>, index: usize) -> (DVec2, DVec2, Color, bool) { fn read_artboard_attributes(list: &List<Artboard>, index: usize) -> (DVec2, DVec2, Color, bool) {
let location: DVec2 = list.attribute_cloned_or_default(ATTR_LOCATION, index); let location = list.attr_cloned_or_default::<attr::Location>(index);
let dimensions: DVec2 = list.attribute_cloned_or_default(ATTR_DIMENSIONS, index); let dimensions = list.attr_cloned_or_default::<attr::Dimensions>(index);
let background: Color = list.attribute_cloned_or_default(ATTR_BACKGROUND, index); let background = list.attr_cloned_or_default::<attr::Background>(index);
let clip: bool = list.attribute_cloned_or_default(ATTR_CLIP, index); let clip = list.attr_cloned_or_default::<attr::Clip>(index);
(location, dimensions, background, clip) (location, dimensions, background, clip)
} }
@@ -737,7 +736,7 @@ impl Render for List<Artboard> {
|attributes| { |attributes| {
let matrix = format_transform_matrix(DAffine2::from_translation(location)); let matrix = format_transform_matrix(DAffine2::from_translation(location));
if !matrix.is_empty() { if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix); attributes.push("transform", matrix);
} }
if clip { if clip {
@@ -800,7 +799,7 @@ impl Render for List<Artboard> {
let Some(content) = self.element(index).map(Artboard::as_graphic_list) else { continue }; let Some(content) = self.element(index).map(Artboard::as_graphic_list) else { continue };
let (location, dimensions, _background, clip) = read_artboard_attributes(self, index); let (location, dimensions, _background, clip) = read_artboard_attributes(self, index);
let layer_path: List<NodeId> = self.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index).0; let layer_path: List<NodeId> = self.attr_cloned_or_default::<attr::editor::LayerPath>(index).0;
let element_id = layer_path.iter_element_values().next_back().copied(); let element_id = layer_path.iter_element_values().next_back().copied();
if let Some(element_id) = element_id { if let Some(element_id) = element_id {
@@ -823,7 +822,7 @@ impl Render for List<Artboard> {
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for index in 0..self.len() { for index in 0..self.len() {
let dimensions: DVec2 = self.attribute_cloned_or_default(ATTR_DIMENSIONS, index); let dimensions = self.attr_cloned_or_default::<attr::Dimensions>(index);
let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions); let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions);
click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.)); click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.));
} }
@@ -839,10 +838,10 @@ impl Render for List<Graphic> {
let mut mask_state = None; let mut mask_state = None;
for index in 0..self.len() { for index in 0..self.len() {
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform = self.attr_cloned_or_default::<attr::Transform>(index);
let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode = self.attr_cloned_or_default::<attr::BlendMode>(index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr = self.attr_cloned_or_default::<attr::Opacity>(index);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr = self.attr_cloned_or_default::<attr::OpacityFill>(index);
let element = self.element(index).unwrap(); let element = self.element(index).unwrap();
render.parent_tag( render.parent_tag(
@@ -850,7 +849,7 @@ impl Render for List<Graphic> {
|attributes| { |attributes| {
let matrix = format_transform_matrix(transform); let matrix = format_transform_matrix(transform);
if !matrix.is_empty() { if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix); attributes.push("transform", matrix);
} }
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
@@ -895,11 +894,11 @@ impl Render for List<Graphic> {
let mut mask_element_and_transform = None; let mut mask_element_and_transform = None;
for index in 0..self.len() { for index in 0..self.len() {
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let item_transform = self.attr_cloned_or_default::<attr::Transform>(index);
let transform = transform * item_transform; let transform = transform * item_transform;
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode_attr = self.attr_cloned_or_default::<attr::BlendMode>(index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr = self.attr_cloned_or_default::<attr::Opacity>(index);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr = self.attr_cloned_or_default::<attr::OpacityFill>(index);
let element = self.element(index).unwrap(); let element = self.element(index).unwrap();
let mut layer = false; let mut layer = false;
@@ -971,8 +970,8 @@ impl Render for List<Graphic> {
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
for index in 0..self.len() { for index in 0..self.len() {
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let item_transform = self.attr_cloned_or_default::<attr::Transform>(index);
let layer_path: List<NodeId> = self.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index).0; let layer_path: List<NodeId> = self.attr_cloned_or_default::<attr::editor::LayerPath>(index).0;
let layer = layer_path.iter_element_values().next_back().copied(); let layer = layer_path.iter_element_values().next_back().copied();
let element = self.element(index).unwrap(); let element = self.element(index).unwrap();
@@ -992,7 +991,7 @@ impl Render for List<Graphic> {
let mut all_upstream_outlines = Vec::new(); let mut all_upstream_outlines = Vec::new();
for index in 0..self.len() { for index in 0..self.len() {
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let item_transform = self.attr_cloned_or_default::<attr::Transform>(index);
let element = self.element(index).unwrap(); let element = self.element(index).unwrap();
let mut new_click_targets = Vec::new(); let mut new_click_targets = Vec::new();
@@ -1019,7 +1018,7 @@ impl Render for List<Graphic> {
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for index in 0..self.len() { for index in 0..self.len() {
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let item_transform = self.attr_cloned_or_default::<attr::Transform>(index);
let element = self.element(index).unwrap(); let element = self.element(index).unwrap();
let mut new_click_targets = Vec::new(); let mut new_click_targets = Vec::new();
@@ -1035,7 +1034,7 @@ impl Render for List<Graphic> {
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) { fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
for index in 0..self.len() { for index in 0..self.len() {
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let item_transform = self.attr_cloned_or_default::<attr::Transform>(index);
let element = self.element(index).unwrap(); let element = self.element(index).unwrap();
let mut new_outlines = Vec::new(); let mut new_outlines = Vec::new();
@@ -1054,7 +1053,7 @@ impl Render for List<Graphic> {
} }
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) { fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {
let (elements, layers) = self.element_and_attribute_slices_mut::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH); let (elements, layers) = self.element_and_attr_slices_mut::<attr::editor::LayerPath>();
for (element, layer) in elements.iter_mut().zip(layers.iter()) { for (element, layer) in elements.iter_mut().zip(layers.iter()) {
element.new_ids_from_hash(layer.0.iter_element_values().next_back().copied()); element.new_ids_from_hash(layer.0.iter_element_values().next_back().copied());
} }
@@ -1065,10 +1064,10 @@ impl Render for List<Vector> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for index in 0..self.len() { for index in 0..self.len() {
let Some(vector) = self.element(index) else { continue }; let Some(vector) = self.element(index) else { continue };
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let item_transform = self.attr_cloned_or_default::<attr::Transform>(index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode_attr = self.attr_cloned_or_default::<attr::BlendMode>(index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr = self.attr_cloned_or_default::<attr::Opacity>(index);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr = self.attr_cloned_or_default::<attr::OpacityFill>(index);
// Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform // Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform
let has_real_stroke = vector.stroke.as_ref().filter(|stroke| stroke.weight() > 0.); let has_real_stroke = vector.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
@@ -1097,10 +1096,10 @@ impl Render for List<Vector> {
MaskType::Mask MaskType::Mask
}; };
let fill_graphic_list = graphic_list_at(self, index, ATTR_FILL); let fill_graphic_list = graphic_list_at::<graphic_attr::Fill>(self, index);
let fill_graphic = fill_graphic_list.as_ref().and_then(|l| l.element(0)); let fill_graphic = fill_graphic_list.as_ref().and_then(|l| l.element(0));
let stroke_graphic_list = graphic_list_at(self, index, ATTR_STROKE); let stroke_graphic_list = graphic_list_at::<graphic_attr::Stroke>(self, index);
let stroke_graphic = stroke_graphic_list.as_ref().and_then(|l| l.element(0)); let stroke_graphic = stroke_graphic_list.as_ref().and_then(|l| l.element(0));
let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed()); let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
@@ -1135,8 +1134,8 @@ impl Render for List<Vector> {
// The mask must draw at full alpha so the SVG `<mask>`/`<clipPath>` fully zeroes the path interior. // The mask must draw at full alpha so the SVG `<mask>`/`<clipPath>` fully zeroes the path interior.
// The wrapping SVG group (above) handles the user-set opacity. // The wrapping SVG group (above) handles the user-set opacity.
let mut mask_item = Item::new_from_element(cloned_vector).with_attribute(ATTR_TRANSFORM, item_transform); let mut mask_item = Item::new_from_element(cloned_vector).with_attr::<attr::Transform>(item_transform);
set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); set_paint_attribute::<graphic_attr::Fill>(mask_item.attributes_mut(), List::new_from_element(Color::BLACK));
let vector_item = List::new_from_item(mask_item); let vector_item = List::new_from_item(mask_item);
(id, mask_type, vector_item) (id, mask_type, vector_item)
@@ -1164,7 +1163,7 @@ impl Render for List<Vector> {
attributes.push("d", path.clone()); attributes.push("d", path.clone());
let matrix = format_transform_matrix(element_transform); let matrix = format_transform_matrix(element_transform);
if !matrix.is_empty() { if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix); attributes.push("transform", matrix);
} }
let defs = &mut attributes.0.svg_defs; let defs = &mut attributes.0.svg_defs;
@@ -1281,10 +1280,10 @@ impl Render for List<Vector> {
use graphic_types::vector_types::vector; use graphic_types::vector_types::vector;
let Some(element) = self.element(index) else { continue }; let Some(element) = self.element(index) else { continue };
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let item_transform = self.attr_cloned_or_default::<attr::Transform>(index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode_attr = self.attr_cloned_or_default::<attr::BlendMode>(index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr = self.attr_cloned_or_default::<attr::Opacity>(index);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr = self.attr_cloned_or_default::<attr::OpacityFill>(index);
let multiplied_transform = parent_transform * item_transform; let multiplied_transform = parent_transform * item_transform;
let has_real_stroke = element.stroke.as_ref().filter(|stroke| stroke.weight() > 0.); let has_real_stroke = element.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform)); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform));
@@ -1310,8 +1309,8 @@ impl Render for List<Vector> {
} }
} }
let fill_graphic_list = graphic_list_at(self, index, ATTR_FILL); let fill_graphic_list = graphic_list_at::<graphic_attr::Fill>(self, index);
let stroke_graphic_list = graphic_list_at(self, index, ATTR_STROKE); let stroke_graphic_list = graphic_list_at::<graphic_attr::Stroke>(self, index);
// If we're using opacity or a blend mode, we need to push a layer // If we're using opacity or a blend mode, we need to push a layer
let blend_mode = match render_params.render_mode { let blend_mode = match render_params.render_mode {
@@ -1484,8 +1483,8 @@ impl Render for List<Vector> {
// The mask must draw at full alpha so `SrcOut` fully zeroes the path interior. // The mask must draw at full alpha so `SrcOut` fully zeroes the path interior.
// The outer opacity/blend layer (above) handles the user-set opacity. // The outer opacity/blend layer (above) handles the user-set opacity.
let mut mask_item = Item::new_from_element(cloned_element).with_attribute(ATTR_TRANSFORM, item_transform); let mut mask_item = Item::new_from_element(cloned_element).with_attr::<attr::Transform>(item_transform);
set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); set_paint_attribute::<graphic_attr::Fill>(mask_item.attributes_mut(), List::new_from_element(Color::BLACK));
let vector_list = List::new_from_item(mask_item); let vector_list = List::new_from_item(mask_item);
let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
@@ -1559,7 +1558,7 @@ impl Render for List<Vector> {
// Aggregate all items' targets per element_id so multi-item lists (e.g. the "Text to Vector Glyphs" node) produce hit areas for every glyph. // Aggregate all items' targets per element_id so multi-item lists (e.g. the "Text to Vector Glyphs" node) produce hit areas for every glyph.
// Targets are baked relative to item 0's transform since `Graphic::collect_metadata` records that as `local_transforms[element_id]`. // Targets are baked relative to item 0's transform since `Graphic::collect_metadata` records that as `local_transforms[element_id]`.
let item_zero_transform: DAffine2 = if !self.is_empty() { let item_zero_transform: DAffine2 = if !self.is_empty() {
self.attribute_cloned_or_default(ATTR_TRANSFORM, 0) self.attr_cloned_or_default::<attr::Transform>(0)
} else { } else {
DAffine2::IDENTITY DAffine2::IDENTITY
}; };
@@ -1574,8 +1573,8 @@ impl Render for List<Vector> {
for index in 0..self.len() { for index in 0..self.len() {
let Some(source) = self.element(index) else { continue }; let Some(source) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform = self.attr_cloned_or_default::<attr::Transform>(index);
let layer_path: List<NodeId> = self.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index).0; let layer_path: List<NodeId> = self.attr_cloned_or_default::<attr::editor::LayerPath>(index).0;
let layer = layer_path.iter_element_values().next_back().copied(); let layer = layer_path.iter_element_values().next_back().copied();
if let Some(element_id) = caller_element_id.or(layer) { if let Some(element_id) = caller_element_id.or(layer) {
@@ -1588,7 +1587,7 @@ impl Render for List<Vector> {
} }
// Use click-target override if the item provides one (e.g. 'Text' node's per-glyph bboxes) // Use click-target override if the item provides one (e.g. 'Text' node's per-glyph bboxes)
let click_target_vector = self.attribute::<Vector>(ATTR_EDITOR_CLICK_TARGET, index).unwrap_or(source); let click_target_vector = self.attr::<vector_attr::editor::ClickTarget>(index).unwrap_or(source);
let item_relative_transform = item_zero_inverse * transform; let item_relative_transform = item_zero_inverse * transform;
@@ -1608,16 +1607,16 @@ impl Render for List<Vector> {
if let std::collections::hash_map::Entry::Vacant(e) = metadata.vector_data.entry(element_id) { if let std::collections::hash_map::Entry::Vacant(e) = metadata.vector_data.entry(element_id) {
e.insert(Arc::new(source.clone())); e.insert(Arc::new(source.clone()));
if let Some(fill_graphic) = graphic_list_at(self, index, ATTR_FILL) { if let Some(fill_graphic) = graphic_list_at::<graphic_attr::Fill>(self, index) {
metadata.fill_attributes.insert(element_id, Arc::new(fill_graphic.into_owned())); metadata.fill_attributes.insert(element_id, Arc::new(fill_graphic.into_owned()));
} }
if let Some(stroke_graphic) = graphic_list_at(self, index, ATTR_STROKE) { if let Some(stroke_graphic) = graphic_list_at::<graphic_attr::Stroke>(self, index) {
metadata.stroke_attributes.insert(element_id, Arc::new(stroke_graphic.into_owned())); metadata.stroke_attributes.insert(element_id, Arc::new(stroke_graphic.into_owned()));
} }
} }
// Surface `editor:text_frame` for the Text tool's drag cage // Surface `editor:text_frame` for the Text tool's drag cage
if let Some(&frame) = self.attribute::<DAffine2>(ATTR_EDITOR_TEXT_FRAME, index) { if let Some(&frame) = self.attr::<attr::editor::TextFrame>(index) {
metadata.text_frames.entry(element_id).or_insert(frame); metadata.text_frames.entry(element_id).or_insert(frame);
} }
} }
@@ -1625,7 +1624,7 @@ impl Render for List<Vector> {
// If this item carries a snapshot of upstream graphic content (e.g. it was produced by Boolean Operation, // If this item carries a snapshot of upstream graphic content (e.g. it was produced by Boolean Operation,
// Combine Paths, Morph, or any other destructive merge), recurse into that snapshot so the editor can // Combine Paths, Morph, or any other destructive merge), recurse into that snapshot so the editor can
// surface the original child layers' click targets. // 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.attr_cloned_or_default::<graphic_attr::editor::MergedLayers>(index);
if !upstream_nested_layers.is_empty() { if !upstream_nested_layers.is_empty() {
let mut upstream_footprint = footprint; let mut upstream_footprint = footprint;
upstream_footprint.transform *= transform; upstream_footprint.transform *= transform;
@@ -1645,10 +1644,10 @@ impl Render for List<Vector> {
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for index in 0..self.len() { for index in 0..self.len() {
let Some(source) = self.element(index) else { continue }; let Some(source) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform = self.attr_cloned_or_default::<attr::Transform>(index);
// Use click-target override geometry if the item provides one (e.g. 'Text' node's per-glyph bounding boxes) // Use click-target override geometry if the item provides one (e.g. 'Text' node's per-glyph bounding boxes)
let vector = self.attribute::<Vector>(ATTR_EDITOR_CLICK_TARGET, index).unwrap_or(source); let vector = self.attr::<vector_attr::editor::ClickTarget>(index).unwrap_or(source);
extend_targets_from_vector(click_targets, self, index, vector, transform); extend_targets_from_vector(click_targets, self, index, vector, transform);
} }
@@ -1658,7 +1657,7 @@ impl Render for List<Vector> {
// Source geometry only, ignoring `editor:click_target`, so outlines reflect actual letterforms // Source geometry only, ignoring `editor:click_target`, so outlines reflect actual letterforms
for index in 0..self.len() { for index in 0..self.len() {
let Some(source) = self.element(index) else { continue }; let Some(source) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform = self.attr_cloned_or_default::<attr::Transform>(index);
extend_targets_from_vector(outlines, self, index, source, transform); extend_targets_from_vector(outlines, self, index, source, transform);
} }
@@ -1674,7 +1673,7 @@ impl Render for List<Vector> {
/// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work /// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work
/// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append. /// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append.
fn extend_targets_from_vector(targets: &mut Vec<ClickTarget>, vector_list: &List<Vector>, index: usize, geometry: &Vector, transform: DAffine2) { fn extend_targets_from_vector(targets: &mut Vec<ClickTarget>, vector_list: &List<Vector>, index: usize, geometry: &Vector, transform: DAffine2) {
let filled = has_paint_at(vector_list, index, ATTR_FILL); let filled = has_paint_at::<graphic_attr::Fill>(vector_list, index);
let mut subpaths: Vec<Subpath<_>> = geometry.stroke_bezier_paths().collect(); let mut subpaths: Vec<Subpath<_>> = geometry.stroke_bezier_paths().collect();
let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed()); let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed());
@@ -1730,10 +1729,10 @@ impl Render for List<Raster<CPU>> {
for index in 0..self.len() { for index in 0..self.len() {
let Some(image) = self.element(index) else { continue }; let Some(image) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform = self.attr_cloned_or_default::<attr::Transform>(index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode_attr = self.attr_cloned_or_default::<attr::BlendMode>(index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr = self.attr_cloned_or_default::<attr::Opacity>(index);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr = self.attr_cloned_or_default::<attr::OpacityFill>(index);
if image.data.is_empty() { if image.data.is_empty() {
continue; continue;
@@ -1752,7 +1751,7 @@ impl Render for List<Raster<CPU>> {
let matrix = transform * DAffine2::from_scale(1. / size); let matrix = transform * DAffine2::from_scale(1. / size);
let matrix = format_transform_matrix(matrix); let matrix = format_transform_matrix(matrix);
if !matrix.is_empty() { if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix); attributes.push("transform", matrix);
} }
attributes.push("width", size.x.to_string()); attributes.push("width", size.x.to_string());
@@ -1795,7 +1794,7 @@ impl Render for List<Raster<CPU>> {
attributes.push("href", base64_string); attributes.push("href", base64_string);
let matrix = format_transform_matrix(transform); let matrix = format_transform_matrix(transform);
if !matrix.is_empty() { if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix); attributes.push("transform", matrix);
} }
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
@@ -1817,9 +1816,9 @@ impl Render for List<Raster<CPU>> {
continue; continue;
} }
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode_attr = self.attr_cloned_or_default::<attr::BlendMode>(index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr = self.attr_cloned_or_default::<attr::Opacity>(index);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr = self.attr_cloned_or_default::<attr::OpacityFill>(index);
let blend_mode = blend_mode_attr.to_peniko(); let blend_mode = blend_mode_attr.to_peniko();
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
@@ -1834,7 +1833,7 @@ impl Render for List<Raster<CPU>> {
layer = true; layer = true;
} }
let transform_attribute: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform_attribute = self.attr_cloned_or_default::<attr::Transform>(index);
if let RenderMode::Outline = render_params.render_mode { if let RenderMode::Outline = render_params.render_mode {
let outline_transform: DAffine2 = transform * transform_attribute; let outline_transform: DAffine2 = transform * transform_attribute;
@@ -1874,7 +1873,7 @@ impl Render for List<Raster<CPU>> {
metadata.upstream_footprints.insert(element_id, footprint); metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one item of the `List<Raster<...>>` // TODO: Find a way to handle more than one item of the `List<Raster<...>>`
if !self.is_empty() { if !self.is_empty() {
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0); let transform = self.attr_cloned_or_default::<attr::Transform>(0);
metadata.local_transforms.insert(element_id, transform); metadata.local_transforms.insert(element_id, transform);
// If this raster carries a snapshot of upstream graphic content (e.g. it was produced by Rasterize, // If this raster carries a snapshot of upstream graphic content (e.g. it was produced by Rasterize,
@@ -1883,7 +1882,7 @@ impl Render for List<Raster<CPU>> {
// The snapshot was captured before Rasterize shifted its input transforms to align with the rasterization // 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 // 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). // 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.attr_cloned_or_default::<graphic_attr::editor::MergedLayers>(0);
if !upstream_nested_layers.is_empty() { if !upstream_nested_layers.is_empty() {
upstream_nested_layers.collect_metadata(metadata, footprint, None); upstream_nested_layers.collect_metadata(metadata, footprint, None);
} }
@@ -1906,10 +1905,10 @@ impl Render for List<Raster<GPU>> {
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
for index in 0..self.len() { for index in 0..self.len() {
let Some(raster) = self.element(index) else { continue }; let Some(raster) = self.element(index) else { continue };
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode_attr = self.attr_cloned_or_default::<attr::BlendMode>(index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr = self.attr_cloned_or_default::<attr::Opacity>(index);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr = self.attr_cloned_or_default::<attr::OpacityFill>(index);
let clip_attr: bool = self.attribute_cloned_or_default(ATTR_CLIPPING_MASK, index); let clip_attr = self.attr_cloned_or_default::<attr::ClippingMask>(index);
let blend_mode = match render_params.render_mode { let blend_mode = match render_params.render_mode {
RenderMode::Outline => peniko::Mix::Normal, RenderMode::Outline => peniko::Mix::Normal,
_ => blend_mode_attr.to_peniko(), _ => blend_mode_attr.to_peniko(),
@@ -1928,7 +1927,7 @@ impl Render for List<Raster<GPU>> {
layer = true; layer = true;
} }
let transform_attribute: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform_attribute = self.attr_cloned_or_default::<attr::Transform>(index);
if let RenderMode::Outline = render_params.render_mode { if let RenderMode::Outline = render_params.render_mode {
let outline_transform = transform * transform_attribute; let outline_transform = transform * transform_attribute;
@@ -1969,7 +1968,7 @@ impl Render for List<Raster<GPU>> {
metadata.upstream_footprints.insert(element_id, footprint); metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one item of the `List<Raster<...>>` // TODO: Find a way to handle more than one item of the `List<Raster<...>>`
if !self.is_empty() { if !self.is_empty() {
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0); let transform = self.attr_cloned_or_default::<attr::Transform>(0);
metadata.local_transforms.insert(element_id, transform); metadata.local_transforms.insert(element_id, transform);
// If this raster carries a snapshot of upstream graphic content (e.g. it was produced by Rasterize, // If this raster carries a snapshot of upstream graphic content (e.g. it was produced by Rasterize,
@@ -1978,7 +1977,7 @@ impl Render for List<Raster<GPU>> {
// The snapshot was captured before Rasterize shifted its input transforms to align with the rasterization // 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 // 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). // 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.attr_cloned_or_default::<graphic_attr::editor::MergedLayers>(0);
if !upstream_nested_layers.is_empty() { if !upstream_nested_layers.is_empty() {
upstream_nested_layers.collect_metadata(metadata, footprint, None); upstream_nested_layers.collect_metadata(metadata, footprint, None);
} }
@@ -2000,9 +1999,9 @@ impl Render for List<Raster<GPU>> {
impl Render for List<Color> { impl Render for List<Color> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for (index, color) in self.iter_element_values().enumerate() { for (index, color) in self.iter_element_values().enumerate() {
let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode = self.attr_cloned_or_default::<attr::BlendMode>(index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr = self.attr_cloned_or_default::<attr::Opacity>(index);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr = self.attr_cloned_or_default::<attr::OpacityFill>(index);
render.leaf_tag("polyline", |attributes| { render.leaf_tag("polyline", |attributes| {
// Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses // Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses
// precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning. // precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning.
@@ -2031,9 +2030,9 @@ impl Render for List<Color> {
use vello::peniko; use vello::peniko;
for (index, color) in self.iter_element_values().enumerate() { for (index, color) in self.iter_element_values().enumerate() {
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode_attr = self.attr_cloned_or_default::<attr::BlendMode>(index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr = self.attr_cloned_or_default::<attr::Opacity>(index);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr = self.attr_cloned_or_default::<attr::OpacityFill>(index);
let blend_mode = blend_mode_attr.to_peniko(); let blend_mode = blend_mode_attr.to_peniko();
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
@@ -2071,12 +2070,12 @@ impl Render for List<Gradient> {
for index in 0..self.len() { for index in 0..self.len() {
let Some(gradient) = self.element(index) else { continue }; let Some(gradient) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform = self.attr_cloned_or_default::<attr::Transform>(index);
let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode = self.attr_cloned_or_default::<attr::BlendMode>(index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr = self.attr_cloned_or_default::<attr::Opacity>(index);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr = self.attr_cloned_or_default::<attr::OpacityFill>(index);
let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, index); let spread_method = self.attr_cloned_or_default::<vector_attr::SpreadMethod>(index);
let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, index); let gradient_type = self.attr_cloned_or_default::<vector_attr::GradientType>(index);
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" }; let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
render.leaf_tag(tag, |attributes| { render.leaf_tag(tag, |attributes| {
if let Some((min, size)) = thumbnail_rect { if let Some((min, size)) = thumbnail_rect {
@@ -2160,13 +2159,13 @@ impl Render for List<Gradient> {
for (((index, gradient), spread_method), gradient_type) in self for (((index, gradient), spread_method), gradient_type) in self
.iter_element_values() .iter_element_values()
.enumerate() .enumerate()
.zip(self.iter_attribute_values_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD)) .zip(self.iter_attr_values_or_default::<vector_attr::SpreadMethod>())
.zip(self.iter_attribute_values_or_default::<GradientType>(ATTR_GRADIENT_TYPE)) .zip(self.iter_attr_values_or_default::<vector_attr::GradientType>())
{ {
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform = self.attr_cloned_or_default::<attr::Transform>(index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode_attr = self.attr_cloned_or_default::<attr::BlendMode>(index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr = self.attr_cloned_or_default::<attr::Opacity>(index);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr = self.attr_cloned_or_default::<attr::OpacityFill>(index);
let gradient_transform = parent_transform * transform; let gradient_transform = parent_transform * transform;
let blend_mode = blend_mode_attr.to_peniko(); let blend_mode = blend_mode_attr.to_peniko();
@@ -2314,16 +2313,16 @@ fn draw_glyph_run_to_bezpaths(glyph_run: &parley::GlyphRun<'_, ()>, x_offset: f3
fn text_item_size_and_transform(list: &List<String>, index: usize) -> Option<(DVec2, DAffine2)> { fn text_item_size_and_transform(list: &List<String>, index: usize) -> Option<(DVec2, DAffine2)> {
let text = list.element(index)?; let text = list.element(index)?;
let font: Resource = { let font: Resource = {
let f: Resource = list.attribute_cloned_or_default(ATTR_FONT, index); let f = list.attr_cloned_or_default::<text_attr::Font>(index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f } if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f }
}; };
let font_size: f64 = list.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); let font_size = list.attr_cloned_or::<attr::FontSize>(index, DEFAULT_FONT_SIZE);
let line_height: f64 = list.attribute_cloned_or(ATTR_LINE_HEIGHT, index, 1.2); let line_height = list.attr_cloned_or::<attr::LineHeight>(index, 1.2);
let letter_spacing: f64 = list.attribute_cloned_or(ATTR_LETTER_SPACING, index, 0.); let letter_spacing = list.attr_cloned_or_default::<attr::LetterSpacing>(index);
let max_width: Option<f64> = list.attribute_cloned_or(ATTR_MAX_WIDTH, index, None); let max_width = list.attr_cloned_or_default::<attr::MaxWidth>(index);
let max_height: Option<f64> = list.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None); let max_height = list.attr_cloned_or_default::<attr::MaxHeight>(index);
let align: text_nodes::TextAlign = list.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index); let align = list.attr_cloned_or_default::<text_attr::TextAlign>(index);
let transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform = list.attr_cloned_or_default::<attr::Transform>(index);
let typesetting = text_nodes::TypesettingConfig { let typesetting = text_nodes::TypesettingConfig {
font_size, font_size,
@@ -2375,7 +2374,7 @@ pub fn graphic_list_bounding_box(list: &List<Graphic>, transform: DAffine2) -> R
let mut any_infinite = false; let mut any_infinite = false;
for index in 0..list.len() { for index in 0..list.len() {
let item_transform = transform * list.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, index); let item_transform = transform * list.attr_cloned_or_default::<attr::Transform>(index);
let Some(graphic) = list.element(index) else { continue }; let Some(graphic) = list.element(index) else { continue };
let bounds = match graphic { let bounds = match graphic {
Graphic::Text(text_list) => text_list_bounding_box(text_list, item_transform), Graphic::Text(text_list) => text_list_bounding_box(text_list, item_transform),
@@ -2409,21 +2408,21 @@ impl Render for List<String> {
continue; continue;
} }
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform = self.attr_cloned_or_default::<attr::Transform>(index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr = self.attr_cloned_or_default::<attr::Opacity>(index);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr = self.attr_cloned_or_default::<attr::OpacityFill>(index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode_attr = self.attr_cloned_or_default::<attr::BlendMode>(index);
let font: Resource = { let font: Resource = {
let f: Resource = self.attribute_cloned_or_default(ATTR_FONT, index); let f = self.attr_cloned_or_default::<text_attr::Font>(index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f } if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f }
}; };
let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); let font_size = self.attr_cloned_or::<attr::FontSize>(index, DEFAULT_FONT_SIZE);
let line_height: f64 = self.attribute_cloned_or(ATTR_LINE_HEIGHT, index, 1.2); let line_height = self.attr_cloned_or::<attr::LineHeight>(index, 1.2);
let letter_spacing: f64 = self.attribute_cloned_or(ATTR_LETTER_SPACING, index, 0.); let letter_spacing = self.attr_cloned_or_default::<attr::LetterSpacing>(index);
let max_width: Option<f64> = self.attribute_cloned_or(ATTR_MAX_WIDTH, index, None); let max_width = self.attr_cloned_or_default::<attr::MaxWidth>(index);
let max_height: Option<f64> = self.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None); let max_height = self.attr_cloned_or_default::<attr::MaxHeight>(index);
let letter_tilt: f64 = self.attribute_cloned_or(ATTR_LETTER_TILT, index, 0.); let letter_tilt = self.attr_cloned_or_default::<attr::LetterTilt>(index);
let align: text_nodes::TextAlign = self.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index); let align = self.attr_cloned_or_default::<text_attr::TextAlign>(index);
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let typesetting = text_nodes::TypesettingConfig { let typesetting = text_nodes::TypesettingConfig {
@@ -2494,21 +2493,21 @@ impl Render for List<String> {
continue; continue;
} }
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let item_transform = self.attr_cloned_or_default::<attr::Transform>(index);
let font: Resource = { let font: Resource = {
let f: Resource = self.attribute_cloned_or_default(ATTR_FONT, index); let f = self.attr_cloned_or_default::<text_attr::Font>(index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f } if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f }
}; };
let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); let font_size = self.attr_cloned_or::<attr::FontSize>(index, DEFAULT_FONT_SIZE);
let line_height: f64 = self.attribute_cloned_or(ATTR_LINE_HEIGHT, index, 1.2); let line_height = self.attr_cloned_or::<attr::LineHeight>(index, 1.2);
let letter_spacing: f64 = self.attribute_cloned_or(ATTR_LETTER_SPACING, index, 0.); let letter_spacing = self.attr_cloned_or_default::<attr::LetterSpacing>(index);
let max_width: Option<f64> = self.attribute_cloned_or(ATTR_MAX_WIDTH, index, None); let max_width = self.attr_cloned_or_default::<attr::MaxWidth>(index);
let max_height: Option<f64> = self.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None); let max_height = self.attr_cloned_or_default::<attr::MaxHeight>(index);
let letter_tilt: f64 = self.attribute_cloned_or(ATTR_LETTER_TILT, index, 0.); let letter_tilt = self.attr_cloned_or_default::<attr::LetterTilt>(index);
let align: text_nodes::TextAlign = self.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index); let align = self.attr_cloned_or_default::<text_attr::TextAlign>(index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode_attr = self.attr_cloned_or_default::<attr::BlendMode>(index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr = self.attr_cloned_or_default::<attr::Opacity>(index);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr = self.attr_cloned_or_default::<attr::OpacityFill>(index);
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let typesetting = text_nodes::TypesettingConfig { let typesetting = text_nodes::TypesettingConfig {
@@ -2559,7 +2558,7 @@ impl Render for List<String> {
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
// Click targets are baked relative to item 0's transform, which `Graphic::collect_metadata` records as `local_transforms[element_id]`. // Click targets are baked relative to item 0's transform, which `Graphic::collect_metadata` records as `local_transforms[element_id]`.
let item_zero_transform: DAffine2 = if !self.is_empty() { let item_zero_transform: DAffine2 = if !self.is_empty() {
self.attribute_cloned_or_default(ATTR_TRANSFORM, 0) self.attr_cloned_or_default::<attr::Transform>(0)
} else { } else {
DAffine2::IDENTITY DAffine2::IDENTITY
}; };
@@ -2572,7 +2571,7 @@ impl Render for List<String> {
let mut accumulated_click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new(); let mut accumulated_click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new();
for index in 0..self.len() { for index in 0..self.len() {
let layer_path: List<NodeId> = self.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index).0; let layer_path: List<NodeId> = self.attr_cloned_or_default::<attr::editor::LayerPath>(index).0;
let layer = layer_path.iter_element_values().next_back().copied(); let layer = layer_path.iter_element_values().next_back().copied();
let Some(element_id) = caller_element_id.or(layer) else { continue }; let Some(element_id) = caller_element_id.or(layer) else { continue };
+8 -7
View File
@@ -1,6 +1,7 @@
use core_types::attr;
use core_types::list::Item; use core_types::list::Item;
use core_types::registry::types::Percentage; 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::Graphic;
use graphic_types::Vector; use graphic_types::Vector;
use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::raster_types::{CPU, GPU, Raster};
@@ -19,7 +20,7 @@ fn blend_mode<T>(
let mut content = content; let mut content = content;
let blend_mode = *blend_mode.element(); let blend_mode = *blend_mode.element();
content.set_attribute(ATTR_BLEND_MODE, blend_mode); content.set_attr::<attr::BlendMode>(blend_mode);
content 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()); let (has_opacity, opacity, has_fill, fill) = (*has_opacity.element(), *opacity.element(), *has_fill.element(), *fill.element());
if has_opacity { if has_opacity {
let multiplied = content.attribute_cloned_or(ATTR_OPACITY, 1.) * (opacity / 100.); let multiplied = content.attr_cloned_or_default::<attr::Opacity>() * (opacity / 100.);
content.set_attribute(ATTR_OPACITY, multiplied); content.set_attr::<attr::Opacity>(multiplied);
} }
if has_fill { if has_fill {
let multiplied = content.attribute_cloned_or(ATTR_OPACITY_FILL, 1.) * (fill / 100.); let multiplied = content.attr_cloned_or_default::<attr::OpacityFill>() * (fill / 100.);
content.set_attribute(ATTR_OPACITY_FILL, multiplied); content.set_attr::<attr::OpacityFill>(multiplied);
} }
content content
@@ -79,6 +80,6 @@ fn clipping_mask<T>(
let mut content = content; let mut content = content;
let clip = *clip.element(); let clip = *clip.element();
content.set_attribute(ATTR_CLIPPING_MASK, clip); content.set_attr::<attr::ClippingMask>(clip);
content content
} }
+8 -8
View File
@@ -1,6 +1,6 @@
use crate::brush_cache::BrushCache; use crate::brush_cache::BrushCache;
use crate::brush_stroke::{BrushStyle, BrushTrace}; use crate::brush_stroke::{BrushStyle, BrushTrace};
use core_types::ATTR_TRANSFORM; use core_types::attr;
use core_types::blending::BlendMode; use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::color::{Alpha, Color, Pixel, Sample}; use core_types::color::{Alpha, Color, Pixel, Sample};
@@ -91,7 +91,7 @@ where
return target; 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()) { for (element, transform_attribute) in elements.iter_mut().zip(transforms.iter()) {
let target_width = element.width; let target_width = element.width;
let target_height = element.height; 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)); 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 { if has_erase_or_restore_strokes {
let opaque_image = Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE); 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()) { for stroke in trace.into_iter().map(|row| row.into_element()) {
let mut brush_texture = cache.get_cached_brush(&stroke.style); 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, // 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. // 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.element_mut() = actual_image.into_element();
result_item.set_attribute(ATTR_TRANSFORM, transform); result_item.set_attr::<attr::Transform>(transform);
result_item 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); 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 // Transforms a point from the background image to the foreground image
let foreground_transform: DAffine2 = foreground.attribute_cloned_or_default(ATTR_TRANSFORM); let foreground_transform = foreground.attr_cloned_or_default::<attr::Transform>();
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 = DAffine2::from_scale(foreground_size) * foreground_transform.inverse() * background_transform * DAffine2::from_scale(1. / background_size); 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 // 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); 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 // 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); 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 // Footprint of the foreground image (0, 0)..(1, 1) in the background image space
+2 -2
View File
@@ -1,6 +1,6 @@
use crate::brush_stroke::BrushStroke; use crate::brush_stroke::BrushStroke;
use crate::brush_stroke::BrushStyle; use crate::brush_stroke::BrushStyle;
use core_types::ATTR_TRANSFORM; use core_types::attr;
use core_types::graphene_hash::CacheHashWrapper; use core_types::graphene_hash::CacheHashWrapper;
use core_types::list::Item; use core_types::list::Item;
use raster_types::CPU; use raster_types::CPU;
@@ -51,7 +51,7 @@ impl BrushCacheImpl {
// Check if the first non-blended stroke is an extension of the last one. // 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. // 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 mut first_stroke_point_skip = 0;
let strokes = input[num_blended_strokes..].to_vec(); let strokes = input[num_blended_strokes..].to_vec();
if !strokes.is_empty() && self.prev_input.len() > num_blended_strokes { if !strokes.is_empty() && self.prev_input.len() > num_blended_strokes {
+6 -5
View File
@@ -1,6 +1,7 @@
use core_types::attr;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::transform::TransformMut; 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 glam::{DAffine2, DVec2};
use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::graphic::{Graphic, IntoGraphicList};
use graphic_types::{Artboard, Vector}; 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 // Name is not stored here, it's resolved live from the parent layer's display name
Item::new_from_element(Artboard::new(content)) Item::new_from_element(Artboard::new(content))
.with_attribute(ATTR_LOCATION, normalized_location) .with_attr::<attr::Location>(normalized_location)
.with_attribute(ATTR_DIMENSIONS, normalized_dimensions) .with_attr::<attr::Dimensions>(normalized_dimensions)
.with_attribute(ATTR_BACKGROUND, background) .with_attr::<attr::Background>(background)
.with_attribute(ATTR_CLIP, clip) .with_attr::<attr::Clip>(clip)
} }
+24 -23
View File
@@ -1,7 +1,8 @@
use core_types::attr;
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::list::{AttributeValueDyn, Item, List, ListDyn, NodeIdPath}; use core_types::list::{AttributeValueDyn, Item, List, ListDyn, NodeIdPath};
use core_types::registry::types::{Angle, SeedValue, SignedInteger}; 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 glam::{DAffine2, DVec2};
use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::graphic::{Graphic, IntoGraphicList};
use graphic_types::{Artboard, Vector}; 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()); 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 // 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 { let RenderBoundingBox::Rectangle(bounding_box) = content.element().bounding_box(item_transform, false) else {
return List::new_from_item(content); 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 // Add the mirrored copy with the reflection composed onto its transform
let mut mirrored = content; 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.push(mirrored);
result_list result_list
@@ -600,7 +601,7 @@ fn read_attribute_vector(
let name = name.into_element(); let name = name.into_element();
let mut result = List::with_capacity(content.len()); let mut result = List::with_capacity(content.len());
for index in 0..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.push(Item::new_from_element(value.clone()));
} }
result result
@@ -618,10 +619,10 @@ fn read_attribute_number(
let mut result = List::with_capacity(content.len()); let mut result = List::with_capacity(content.len());
for index in 0..content.len() { for index in 0..content.len() {
let value = content let value = content
.attribute::<f64>(&name, index) .attribute_dyn::<f64>(&name, index)
.copied() .copied()
.or_else(|| content.attribute::<u64>(&name, index).map(|v| *v as f64)) .or_else(|| content.attribute_dyn::<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::<u32>(&name, index).map(|v| *v as f64));
let Some(value) = value else { continue }; let Some(value) = value else { continue };
result.push(Item::new_from_element(value)); result.push(Item::new_from_element(value));
} }
@@ -639,7 +640,7 @@ fn read_attribute_bool(
let name = name.into_element(); let name = name.into_element();
let mut result = List::with_capacity(content.len()); let mut result = List::with_capacity(content.len());
for index in 0..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.push(Item::new_from_element(*value));
} }
result result
@@ -656,7 +657,7 @@ fn read_attribute_string(
let name = name.into_element(); let name = name.into_element();
let mut result = List::with_capacity(content.len()); let mut result = List::with_capacity(content.len());
for index in 0..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.push(Item::new_from_element(value.clone()));
} }
result result
@@ -673,7 +674,7 @@ fn read_attribute_transform(
let name = name.into_element(); let name = name.into_element();
let mut result = List::with_capacity(content.len()); let mut result = List::with_capacity(content.len());
for index in 0..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.push(Item::new_from_element(*value));
} }
result result
@@ -690,7 +691,7 @@ fn read_attribute_color(
let name = name.into_element(); let name = name.into_element();
let mut result = List::with_capacity(content.len()); let mut result = List::with_capacity(content.len());
for index in 0..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.push(Item::new_from_element(*value));
} }
result result
@@ -707,7 +708,7 @@ fn read_attribute_blend_mode(
let name = name.into_element(); let name = name.into_element();
let mut result = List::with_capacity(content.len()); let mut result = List::with_capacity(content.len());
for index in 0..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.push(Item::new_from_element(*value));
} }
result result
@@ -724,7 +725,7 @@ fn read_attribute_gradient_type(
let name = name.into_element(); let name = name.into_element();
let mut result = List::with_capacity(content.len()); let mut result = List::with_capacity(content.len());
for index in 0..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.push(Item::new_from_element(*value));
} }
result result
@@ -741,7 +742,7 @@ fn read_attribute_spread_method(
let name = name.into_element(); let name = name.into_element();
let mut result = List::with_capacity(content.len()); let mut result = List::with_capacity(content.len());
for index in 0..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.push(Item::new_from_element(*value));
} }
result result
@@ -758,7 +759,7 @@ fn read_attribute_gradient_stops(
let name = name.into_element(); let name = name.into_element();
let mut result = List::with_capacity(content.len()); let mut result = List::with_capacity(content.len());
for index in 0..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.push(Item::new_from_element(value.clone()));
} }
result result
@@ -775,7 +776,7 @@ fn read_attribute_artboard(
let name = name.into_element(); let name = name.into_element();
let mut result = List::with_capacity(content.len()); let mut result = List::with_capacity(content.len());
for index in 0..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.push(Item::new_from_element(value.clone()));
} }
result result
@@ -792,7 +793,7 @@ fn read_attribute_raster(
let name = name.into_element(); let name = name.into_element();
let mut result = List::with_capacity(content.len()); let mut result = List::with_capacity(content.len());
for index in 0..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.push(Item::new_from_element(value.clone()));
} }
result result
@@ -869,7 +870,7 @@ pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
let mut base = base; let mut base = base;
for mut row in new.into_iter() { 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); 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() { for index in 0..current_graphic_list.len() {
let Some(current_element) = current_graphic_list.element(index) else { continue }; let Some(current_element) = current_graphic_list.element(index) else { continue };
let current_element = current_element.clone(); 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; 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 // If we're allowed to recurse, flatten any graphics we encounter
Graphic::Graphic(mut current_element) if recurse => { Graphic::Graphic(mut current_element) if recurse => {
// Apply the parent graphic's transform to all child elements // 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; *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 // 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. // `upstream_footprint *= item_0_transform` recursion cancels out and leaves the originals intact.
let mut graphic_list = graphic_list; 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 { if item_0_transform.matrix2.determinant().abs() > f64::EPSILON {
let inverse = item_0_transform.inverse(); 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; *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 output
@@ -10,9 +10,9 @@ use core_types::list::List;
use core_types::math::bbox::Bbox; use core_types::math::bbox::Bbox;
use core_types::ops::Convert; use core_types::ops::Convert;
use core_types::transform::Footprint; use core_types::transform::Footprint;
#[cfg(target_family = "wasm")]
use core_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, WasmNotSend};
use core_types::{Color, Ctx}; 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::resource::{Resource, ResourceHash};
pub use graph_craft::application_io::*; pub use graph_craft::application_io::*;
pub use graph_craft::document::value::RenderOutputType; pub use graph_craft::document::value::RenderOutputType;
@@ -243,7 +243,7 @@ where
..Default::default() ..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; *transform = DAffine2::from_translation(-aabb.start) * *transform;
} }
data.render_svg(&mut render, &render_params); 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); let image = Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32);
List::new_from_item( List::new_from_item(
Item::new_from_element(Raster::new_cpu(image)) Item::new_from_element(Raster::new_cpu(image))
.with_attribute(ATTR_TRANSFORM, footprint.transform) .with_attr::<attr::Transform>(footprint.transform)
.with_attribute(ATTR_EDITOR_MERGED_LAYERS, upstream_graphic_list), .with_attr::<graphic_types::attr::editor::MergedLayers>(upstream_graphic_list),
) )
} }
+9 -9
View File
@@ -1,6 +1,6 @@
use core_types::Ctx;
use core_types::consts::{DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT}; use core_types::consts::{DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT};
use core_types::list::{Item, List}; 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 graph_craft::application_io::resource::Resource;
use graphic_types::Vector; use graphic_types::Vector;
pub use text_nodes::*; pub use text_nodes::*;
@@ -69,28 +69,28 @@ fn text(
let mut item = Item::new_from_element(text); let mut item = Item::new_from_element(text);
if font != Resource::default() { if font != Resource::default() {
item.set_attribute(ATTR_FONT, font); item.set_attr::<attr::Font>(font);
} }
if (size - DEFAULT_FONT_SIZE).abs() > f64::EPSILON { 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 { 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. { 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. { 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 { 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 { 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() { if align != TextAlign::default() {
item.set_attribute(ATTR_TEXT_ALIGN, align); item.set_attr::<attr::TextAlign>(align);
} }
item item
+2 -2
View File
@@ -1381,7 +1381,7 @@ fn gradient_value(_: impl Ctx, _primary: (), gradient: Item<Gradient>) -> Item<G
#[node_macro::node(category("Color"))] #[node_macro::node(category("Color"))]
fn gradient_type(_: impl Ctx, gradient: Item<Gradient>, gradient_type: Item<vector_types::GradientType>) -> Item<Gradient> { fn gradient_type(_: impl Ctx, gradient: Item<Gradient>, gradient_type: Item<vector_types::GradientType>) -> Item<Gradient> {
let mut gradient = 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 gradient
} }
@@ -1389,7 +1389,7 @@ fn gradient_type(_: impl Ctx, gradient: Item<Gradient>, gradient_type: Item<vect
#[node_macro::node(category("Color"))] #[node_macro::node(category("Color"))]
fn spread_method(_: impl Ctx, gradient: Item<Gradient>, spread_method: Item<vector_types::GradientSpreadMethod>) -> Item<Gradient> { fn spread_method(_: impl Ctx, gradient: Item<Gradient>, spread_method: Item<vector_types::GradientSpreadMethod>) -> Item<Gradient> {
let mut gradient = 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 gradient
} }
+46 -36
View File
@@ -1,10 +1,8 @@
use core_types::list::{ATTR_FILL, Item, ItemAttributeValues, List}; use core_types::attr::{self, Attr};
use core_types::{ use core_types::list::{Item, ItemAttributeValues, List};
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::{Color, Ctx};
};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphic_types::graphic::{bake_paint_transforms, set_paint_attribute}; 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::subpath::{ManipulatorGroup, Subpath};
use graphic_types::vector_types::vector::PointId; use graphic_types::vector_types::vector::PointId;
use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt; 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 // Replace the transformation matrix with a mutation of the vector points themselves
if result_vector_list.element_mut(0).is_some() { if result_vector_list.element_mut(0).is_some() {
let transform: DAffine2 = result_vector_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); let transform = result_vector_list.attr_cloned_or_default::<attr::Transform>(0);
result_vector_list.set_attribute(ATTR_TRANSFORM, 0, DAffine2::IDENTITY); result_vector_list.set_attr::<attr::Transform>(0, DAffine2::IDENTITY);
let result_vector = result_vector_list.element_mut(0).unwrap(); let result_vector = result_vector_list.element_mut(0).unwrap();
Vector::transform(result_vector, transform); 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 // Snapshot the input layers as the `editor:merged_layers` attribute so the renderer can recurse into them
// for editor click-target preservation. // 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 // 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); 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 row = if let Some(index) = copy_from_index {
let mut attributes = vector.clone_item_attributes(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 // 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); 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() { for index in 0..vector.len() {
let element = vector.element(index).unwrap(); 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) { 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::None => Vec::new(),
Graphic::Vector(vector) => { Graphic::Vector(vector) => {
// Apply the parent graphic's transform to each element of the `List<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 vector
.into_iter() .into_iter()
.map(|mut sub_vector| { .map(|mut sub_vector| {
let current_transform: DAffine2 = sub_vector.attribute_cloned_or_default(ATTR_TRANSFORM); let current_transform = sub_vector.attr_cloned_or_default::<attr::Transform>();
*sub_vector.attribute_mut_or_insert_default(ATTR_TRANSFORM) = parent_transform * current_transform; *sub_vector.attr_mut_or_insert_default::<attr::Transform>() = parent_transform * current_transform;
sub_vector sub_vector
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
Graphic::RasterCPU(image) => { 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 make_item = |transform: DAffine2, source_attributes: &ItemAttributeValues| {
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform); subpath.apply_transform(transform);
@@ -204,10 +202,16 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
let element = Vector::from_subpath(subpath); let element = Vector::from_subpath(subpath);
let mut item = Item::new_from_element(element); 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); 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 item
}; };
@@ -216,14 +220,14 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
// back to the originating raster layer // back to the originating raster layer
(0..image.len()) (0..image.len())
.map(|i| { .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); let source_attributes = image.clone_item_attributes(i);
make_item(parent_transform * row_transform, &source_attributes) make_item(parent_transform * row_transform, &source_attributes)
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
Graphic::RasterGPU(image) => { 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 make_item = |transform: DAffine2, source_attributes: &ItemAttributeValues| {
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform); subpath.apply_transform(transform);
@@ -231,10 +235,16 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
let element = Vector::from_subpath(subpath); let element = Vector::from_subpath(subpath);
let mut item = Item::new_from_element(element); 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); 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 item
}; };
@@ -243,16 +253,16 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
// back to the originating raster layer // back to the originating raster layer
(0..image.len()) (0..image.len())
.map(|i| { .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); let source_attributes = image.clone_item_attributes(i);
make_item(parent_transform * row_transform, &source_attributes) make_item(parent_transform * row_transform, &source_attributes)
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
Graphic::Graphic(mut graphic) => { 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` // 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; *transform = parent_transform * *transform;
} }
@@ -266,7 +276,7 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
.into_iter() .into_iter()
.map(|row| { .map(|row| {
let (color, mut attributes) = row.into_parts(); 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(); let mut element = Vector::default();
element.set_stroke_transform(DAffine2::IDENTITY); 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 (stops, mut attributes) = row.into_parts();
let mut gradient_paint = List::new_from_element(stops); let mut gradient_paint = List::new_from_element(stops);
if let Some(transform) = attributes.remove::<DAffine2>(ATTR_TRANSFORM) { if let Some(transform) = attributes.remove_attr::<attr::Transform>() {
gradient_paint.set_attribute(ATTR_TRANSFORM, 0, transform); gradient_paint.set_attr::<attr::Transform>(0, transform);
} }
if let Some(gradient_type) = attributes.remove::<GradientType>(ATTR_GRADIENT_TYPE) { if let Some(gradient_type) = attributes.remove_attr::<vector_types::attr::GradientType>() {
gradient_paint.set_attribute(ATTR_GRADIENT_TYPE, 0, gradient_type); gradient_paint.set_attr::<vector_types::attr::GradientType>(0, gradient_type);
} }
if let Some(spread_method) = attributes.remove::<GradientSpreadMethod>(ATTR_SPREAD_METHOD) { if let Some(spread_method) = attributes.remove_attr::<vector_types::attr::SpreadMethod>() {
gradient_paint.set_attribute(ATTR_SPREAD_METHOD, 0, spread_method); 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(); let mut element = Vector::default();
element.set_stroke_transform(DAffine2::IDENTITY); element.set_stroke_transform(DAffine2::IDENTITY);
@@ -299,12 +309,12 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
Graphic::Text(text) => { 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 // 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) text_nodes::shape_text_list(&text, false)
.into_iter() .into_iter()
.map(|mut sub_vector| { .map(|mut sub_vector| {
let current_transform: DAffine2 = sub_vector.attribute_cloned_or_default(ATTR_TRANSFORM); let current_transform = sub_vector.attr_cloned_or_default::<attr::Transform>();
*sub_vector.attribute_mut_or_insert_default(ATTR_TRANSFORM) = parent_transform * current_transform; *sub_vector.attr_mut_or_insert_default::<attr::Transform>() = parent_transform * current_transform;
sub_vector sub_vector
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
+11 -11
View File
@@ -1,5 +1,5 @@
use crate::adjustments::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, NoiseType}; 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::Color;
use core_types::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut}; use core_types::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut};
use core_types::context::{Ctx, ExtractFootprint}; use core_types::context::{Ctx, ExtractFootprint};
@@ -32,7 +32,7 @@ impl From<std::io::Error> for Error {
#[node_macro::node(category("Debug"))] #[node_macro::node(category("Debug"))]
pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Item<Raster<CPU>>) -> Item<Raster<CPU>> { 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 footprint = ctx.footprint();
let viewport_bounds = footprint.viewport_bounds_in_local_space(); 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 // 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); 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) Item::from_parts(Raster::new_cpu(image), attributes)
} }
@@ -163,7 +163,7 @@ pub fn mask(
let mut row = image; let mut row = image;
let image_size = DVec2::new(row.element().width as f64, row.element().height as f64); 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(); let mask_size = stencil_transform.scale_magnitudes();
if mask_size == DVec2::ZERO { if mask_size == DVec2::ZERO {
@@ -171,7 +171,7 @@ pub fn mask(
} }
// Transforms a point from the background image to the foreground image // 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 bg_to_fg = transform_attribute * DAffine2::from_scale(1. / image_size);
let stencil_transform_inverse = stencil_transform.inverse(); 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>> { pub fn extend_image_to_bounds(_: impl Ctx, image: Item<Raster<CPU>>, bounds: Item<DAffine2>) -> Item<Raster<CPU>> {
let bounds = *bounds.element(); 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 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(); 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) { 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 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); 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) 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()); 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(""))] #[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)); 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"))] #[node_macro::node(category("Raster: Pattern"))]
@@ -478,7 +478,7 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> Item<Raster<CPU>> {
data, data,
..Default::default() ..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)] #[inline(always)]
+9 -8
View File
@@ -1,8 +1,9 @@
use crate::gcore::Context; use crate::gcore::Context;
use core::f64::consts::TAU; use core::f64::consts::TAU;
use core_types::attr;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::registry::types::{Angle, PixelSize}; 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 glam::{DAffine2, DVec2};
use graphic_types::{Artboard, Graphic, Vector}; use graphic_types::{Artboard, Graphic, Vector};
use raster_types::{CPU, GPU, Raster}; 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() { for row_index in 0..generated_content.len() {
let Some(mut row) = generated_content.clone_item(row_index) else { continue }; 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_translation = DAffine2::from_translation(local_transform.translation);
let local_matrix = DAffine2::from_mat2(local_transform.matrix2); 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); result_list.push(row);
} }
@@ -158,10 +159,10 @@ async fn repeat_radial<T: Send + Clone + 'static>(
for row_index in 0..generated_content.len() { for row_index in 0..generated_content.len() {
let Some(mut row) = generated_content.clone_item(row_index) else { continue }; 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_translation = DAffine2::from_translation(local_transform.translation);
let local_matrix = DAffine2::from_mat2(local_transform.matrix2); 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); result_list.push(row);
} }
@@ -200,7 +201,7 @@ async fn repeat_on_points<T: Send + Clone + 'static>(
for points_index in 0..points.len() { for points_index in 0..points.len() {
let Some(points_element) = points.element(points_index) else { continue }; 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 mut iteration = async |index, point| {
let transformed_point = transform.transform_point2(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; let generated_content = content.eval(new_ctx.into_context()).await;
for mut generated_row in generated_content.into_iter() { 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); result_list.push(generated_row);
} }
}; };
@@ -301,7 +302,7 @@ mod test {
let bounds = generated let bounds = generated
.element(index) .element(index)
.unwrap() .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(); .unwrap();
assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10)); assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10));
assert_eq!((bounds[1] - bounds[0]).x, position.y); assert_eq!((bounds[1] - bounds[0]).x, position.y);
+2 -2
View File
@@ -1,5 +1,5 @@
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::{ATTR_TYPE, Ctx}; use core_types::{Ctx, attr};
use serde_json::Value; use serde_json::Value;
use crate::unescape_string; use crate::unescape_string;
@@ -265,7 +265,7 @@ fn query_json_all(
let mut results = Vec::new(); let mut results = Vec::new();
resolve_all(&value, &segments, !*unquote_strings.element(), &mut results); 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. /// A parsed segment of a JSON access path.
+12 -12
View File
@@ -1,5 +1,5 @@
use core_types::attr;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::{ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_TEXT_FRAME, ATTR_TRANSFORM};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use parley::GlyphRun; use parley::GlyphRun;
use skrifa::GlyphId; use skrifa::GlyphId;
@@ -15,13 +15,13 @@ pub struct PathBuilder {
origin: DVec2, origin: DVec2,
glyph_subpaths: Vec<Subpath<PointId>>, glyph_subpaths: Vec<Subpath<PointId>>,
pub vector_list: List<Vector>, 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]>, merged_click_target_bboxes: Vec<[DVec2; 2]>,
/// Per-glyph baselines, parallel to `merged_click_target_bboxes`. Groups glyphs by line for the widening pass. /// Per-glyph baselines, parallel to `merged_click_target_bboxes`. Groups glyphs by line for the widening pass.
merged_click_target_baselines: Vec<f64>, merged_click_target_baselines: Vec<f64>,
/// Per-glyph AABBs in glyph-local space (multi-item mode), widened in `finalize()` to fill gaps. /// Per-glyph AABBs in glyph-local space (multi-item mode), widened in `finalize()` to fill gaps.
per_glyph_bboxes: Vec<Option<[DVec2; 2]>>, 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, text_frame_size: DVec2,
/// First glyph's baseline offset (pre-height-filter). Used for the empty placeholder item so /// 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. /// `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 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)) 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_attr::<attr::Transform>(DAffine2::from_translation(glyph_offset))
.with_attribute(ATTR_EDITOR_TEXT_FRAME, frame_in_item_local); .with_attr::<attr::editor::TextFrame>(frame_in_item_local);
self.vector_list.push(item); self.vector_list.push(item);
// Defer click target creation to `finalize()` where adjacent AABBs get widened // Defer click target creation to `finalize()` where adjacent AABBs get widened
@@ -170,8 +170,8 @@ impl PathBuilder {
if self.vector_list.is_empty() { 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 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()) let item = Item::new_from_element(Vector::default())
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(self.first_glyph_offset)) .with_attr::<attr::Transform>(DAffine2::from_translation(self.first_glyph_offset))
.with_attribute(ATTR_EDITOR_TEXT_FRAME, frame_in_item_local); .with_attr::<attr::editor::TextFrame>(frame_in_item_local);
self.vector_list.push(item); self.vector_list.push(item);
} }
@@ -184,7 +184,7 @@ impl PathBuilder {
.enumerate() .enumerate()
.filter_map(|(index, bbox)| { .filter_map(|(index, bbox)| {
let bbox = (*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])) Some((index, offset, [bbox[0] + offset, bbox[1] + offset]))
}) })
.collect(); .collect();
@@ -197,7 +197,7 @@ impl PathBuilder {
for (entry, widened) in entries.iter().zip(layer_bboxes.iter()) { for (entry, widened) in entries.iter().zip(layer_bboxes.iter()) {
let glyph_local = [widened[0] - entry.1, widened[1] - entry.1]; let glyph_local = [widened[0] - entry.1, widened[1] - entry.1];
let rect = Subpath::new_rectangle(glyph_local[0], glyph_local[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); 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(); 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) // 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); let frame = DAffine2::from_scale(self.text_frame_size);
for index in 0..self.vector_list.len() { for index in 0..self.vector_list.len() {
if self.vector_list.attribute::<DAffine2>(ATTR_EDITOR_TEXT_FRAME, index).is_none() { if self.vector_list.attr::<attr::editor::TextFrame>(index).is_none() {
self.vector_list.set_attribute(ATTR_EDITOR_TEXT_FRAME, index, frame); self.vector_list.set_attr::<attr::editor::TextFrame>(index, frame);
} }
} }
+4 -7
View File
@@ -1,6 +1,6 @@
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::registry::types::SignedInteger; 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. /// 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"))] #[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 start = captured.map_or(0_u64, |m| m.start() as u64);
let end = captured.map_or(0_u64, |m| m.end() 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(); let name = capture_names.get(i).cloned().flatten().unwrap_or_default();
Item::new_from_element(text) Item::new_from_element(text).with_attr::<attr::Start>(start).with_attr::<attr::End>(end).with_attr::<attr::Name>(name)
.with_attribute(ATTR_START, start)
.with_attribute(ATTR_END, end)
.with_attribute(ATTR_NAME, name)
}) })
.collect() .collect()
} }
@@ -208,8 +205,8 @@ fn regex_find_all(
.filter_map(|m| m.ok()) .filter_map(|m| m.ok())
.map(|m| { .map(|m| {
Item::new_from_element(m.as_str().to_string()) Item::new_from_element(m.as_str().to_string())
.with_attribute(ATTR_START, m.start() as u64) .with_attr::<attr::Start>(m.start() as u64)
.with_attribute(ATTR_END, m.end() as u64) .with_attr::<attr::End>(m.end() as u64)
}) })
.collect() .collect()
} }
+21 -25
View File
@@ -1,11 +1,7 @@
use super::TypesettingConfig; use super::TypesettingConfig;
use super::text_context::TextContext; use super::text_context::TextContext;
use core_types::blending::BlendMode; use core_types::attr;
use core_types::list::{Item, List, NodeIdPath}; use core_types::list::{Item, List};
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 glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_resource::Resource; use graphene_resource::Resource;
use vector_types::Vector; 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. // Use fallback font when none is explicitly attached.
let font: Resource = { 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 } if font.is_empty() { super::FALLBACK_FONT_RESOURCE.clone() } else { font }
}; };
let defaults = TypesettingConfig::default(); let defaults = TypesettingConfig::default();
let typesetting = TypesettingConfig { let typesetting = TypesettingConfig {
font_size: item.attribute_cloned_or(ATTR_FONT_SIZE, defaults.font_size), font_size: item.attr_cloned_or::<attr::FontSize>(defaults.font_size),
line_height_ratio: item.attribute_cloned_or(ATTR_LINE_HEIGHT, defaults.line_height_ratio), line_height_ratio: item.attr_cloned_or::<attr::LineHeight>(defaults.line_height_ratio),
letter_spacing: item.attribute_cloned_or(ATTR_LETTER_SPACING, defaults.letter_spacing), letter_spacing: item.attr_cloned_or::<attr::LetterSpacing>(defaults.letter_spacing),
letter_tilt: item.attribute_cloned_or(ATTR_LETTER_TILT, defaults.letter_tilt), letter_tilt: item.attr_cloned_or::<attr::LetterTilt>(defaults.letter_tilt),
max_width: item.attribute_cloned_or::<Option<f64>>(ATTR_MAX_WIDTH, defaults.max_width), max_width: item.attr_cloned_or::<attr::MaxWidth>(defaults.max_width),
max_height: item.attribute_cloned_or::<Option<f64>>(ATTR_MAX_HEIGHT, defaults.max_height), max_height: item.attr_cloned_or::<attr::MaxHeight>(defaults.max_height),
align: item.attribute_cloned_or(ATTR_TEXT_ALIGN, defaults.align), align: item.attr_cloned_or::<crate::attr::TextAlign>(defaults.align),
}; };
let vectors = to_path(text, &font, typesetting, separate_glyphs); let vectors = to_path(text, &font, typesetting, separate_glyphs);
let transform = item.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM); let transform = item.attr_cloned_or_default::<attr::Transform>();
let layer_path = item.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH).cloned(); let layer_path = item.attr::<attr::editor::LayerPath>().cloned();
let blend_mode = item.attribute::<BlendMode>(ATTR_BLEND_MODE).copied(); let blend_mode = item.attr::<attr::BlendMode>().copied();
let opacity = item.attribute::<f64>(ATTR_OPACITY).copied(); let opacity = item.attr::<attr::Opacity>().copied();
let opacity_fill = item.attribute::<f64>(ATTR_OPACITY_FILL).copied(); let opacity_fill = item.attr::<attr::OpacityFill>().copied();
let mut result = List::new(); let mut result = List::new();
for mut produced in vectors.into_iter() { for mut produced in vectors.into_iter() {
if transform != DAffine2::IDENTITY { if transform != DAffine2::IDENTITY {
let local = produced.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM); let local = produced.attr_cloned_or_default::<attr::Transform>();
produced.set_attribute(ATTR_TRANSFORM, transform * local); produced.set_attr::<attr::Transform>(transform * local);
} }
if let Some(layer_path) = &layer_path { 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 { 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 { if let Some(opacity) = opacity {
produced.set_attribute(ATTR_OPACITY, opacity); produced.set_attr::<attr::Opacity>(opacity);
} }
if let Some(opacity_fill) = opacity_fill { 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); result.push(produced);
} }
@@ -1,8 +1,9 @@
use core::f64; use core::f64;
use core_types::attr;
use core_types::color::Color; use core_types::color::Color;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::transform::{ApplyTransform, ScaleType, Transform}; 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 glam::{DAffine2, DMat2, DVec2};
use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::raster_types::{CPU, GPU, Raster};
use graphic_types::{Artboard, Graphic, Vector}; use graphic_types::{Artboard, Graphic, Vector};
@@ -106,7 +107,7 @@ fn reset_transform<T>(
let mut content = content; let mut content = content;
let (reset_translation, reset_rotation, reset_scale) = (*reset_translation.element(), *reset_rotation.element(), *reset_scale.element()); 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 { if reset_translation {
item_transform.translation = DVec2::ZERO; item_transform.translation = DVec2::ZERO;
@@ -147,14 +148,14 @@ fn replace_transform<T>(
let mut content = content; let mut content = content;
let transform = *transform.element(); let transform = *transform.element();
content.set_attribute(ATTR_TRANSFORM, transform.transform()); content.set_attr::<attr::Transform>(transform.transform());
content content
} }
/// Obtains the transform of the input content. /// Obtains the transform of the input content.
#[node_macro::node(category("Math: Transform"), path(core_types::vector))] #[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> { 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. /// Produces the inverse of the input transform, which is the transform that undoes the effect of the original transform.
@@ -1,7 +1,7 @@
use core_types::list::{Item, List, NodeIdPath}; use core_types::list::{Item, List, NodeIdPath};
use core_types::transform::BakeTransform; use core_types::transform::BakeTransform;
use core_types::uuid::NodeId; 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 glam::{DAffine2, DVec2};
use graphic_types::Vector; use graphic_types::Vector;
use vector_types::vector::VectorModification; 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()); modification.into_element().apply(vector.element_mut());
// Drop the stale click-target override so hit testing uses the geometry the user is now editing // 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`), // 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. // 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(); let len = node_path.len();
node_path.into_iter().take(len.saturating_sub(1)).collect() 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 }; 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 vector
} }
@@ -33,7 +33,7 @@ async fn path_modify(_ctx: impl Ctx, vector: Item<Vector>, modification: Item<Bo
#[node_macro::node(category("Vector"))] #[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> { 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; 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); content.element_mut().bake_transform(&transform);
} }
+111 -113
View File
@@ -1,16 +1,14 @@
use core::cmp::Ordering; use core::cmp::Ordering;
use core::f64::consts::{PI, TAU}; use core::f64::consts::{PI, TAU};
use core::hash::{Hash, Hasher}; use core::hash::{Hash, Hasher};
use core_types::attr::{self, Attr};
use core_types::blending::BlendMode; use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox}; 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::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
use core_types::transform::{Footprint, Transform}; use core_types::transform::{Footprint, Transform};
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{ use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
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 glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Vector; use graphic_types::Vector;
use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at}; 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 { trait VectorItemMut {
fn for_each_vector_mut(&mut self, f: impl FnMut(&mut Vector, DAffine2)); 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> { impl VectorItemMut for Item<Vector> {
fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut Vector, DAffine2)) { 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); f(self.element_mut(), 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>) {
self.set_attribute(key, paint); self.set_attr::<A>(paint);
} }
} }
impl VectorItemMut for Item<Graphic> { impl VectorItemMut for Item<Graphic> {
fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut Vector, DAffine2)) { 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 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()) { for (vector, transform) in elements.iter_mut().zip(transforms.iter()) {
f(vector, *transform); 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 }; 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(); *slot = paint.clone();
} }
} }
@@ -161,10 +159,10 @@ where
let paint = List::new_from_element(color).into_graphic_list(); let paint = List::new_from_element(color).into_graphic_list();
if fill { 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()) { 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; i += 1;
@@ -208,19 +206,19 @@ where
for graphic in fill.iter_element_values_mut() { for graphic in fill.iter_element_values_mut() {
let Graphic::Gradient(gradient) = graphic else { continue }; let Graphic::Gradient(gradient) = graphic else { continue };
if gradient.iter_attribute_values::<GradientType>(ATTR_GRADIENT_TYPE).is_none() { if gradient.iter_attr_values::<vector_types::attr::GradientType>().is_none() {
for value in gradient.iter_attribute_values_mut_or_default::<GradientType>(ATTR_GRADIENT_TYPE) { for value in gradient.iter_attr_values_mut_or_default::<vector_types::attr::GradientType>() {
*value = _gradient_type; *value = _gradient_type;
} }
} }
if gradient.iter_attribute_values::<GradientSpreadMethod>(ATTR_SPREAD_METHOD).is_none() { if gradient.iter_attr_values::<vector_types::attr::SpreadMethod>().is_none() {
for value in gradient.iter_attribute_values_mut_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD) { for value in gradient.iter_attr_values_mut_or_default::<vector_types::attr::SpreadMethod>() {
*value = _spread_method; *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) // Without an explicit placement, derive one covering the paint target's bounding box (the CSS `auto` behavior)
let transform = if _has_transform { let transform = if _has_transform {
_transform _transform
@@ -246,13 +244,13 @@ where
initial_gradient_transform_for_bounding_box([min, max]) 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; *value = transform;
} }
} }
} }
content.set_vector_paint(ATTR_FILL, fill); content.set_vector_paint::<graphic_types::attr::Fill>(fill);
content content
} }
@@ -326,7 +324,7 @@ where
}); });
let paint = paint.into_graphic_list(); let paint = paint.into_graphic_list();
content.set_vector_paint(ATTR_STROKE, paint); content.set_vector_paint::<graphic_types::attr::Stroke>(paint);
content 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_scale = random_scale_difference.abs() > 1e-6;
let do_rotation = random_rotation.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() { for &point in row.element().point_domain.positions() {
let translation = points_transform.transform_point2(point); 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() { for row_index in 0..content.len() {
let Some(mut row) = content.clone_item(row_index) else { continue }; let Some(mut row) = content.clone_item(row_index) else { continue };
let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); let row_transform: DAffine2 = row.attr_cloned_or_default::<attr::Transform>();
row.set_attribute(ATTR_TRANSFORM, transform * row_transform); row.set_attr::<attr::Transform>(transform * row_transform);
result_list.push(row); result_list.push(row);
} }
@@ -446,7 +444,7 @@ async fn round_corners(
min_angle_threshold: Item<Angle>, min_angle_threshold: Item<Angle>,
) -> Item<Vector> { ) -> Item<Vector> {
let (radius, roundness, edge_length_limit, min_angle_threshold) = (*radius.element(), *roundness.element(), *edge_length_limit.element(), *min_angle_threshold.element()); 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_transform_inverse = source_transform.inverse();
let (source, attributes) = source.into_parts(); let (source, attributes) = source.into_parts();
@@ -550,7 +548,7 @@ pub fn merge_by_distance(
match algorithm { match algorithm {
MergeByDistanceAlgorithm::Spatial => { 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); content.element_mut().merge_by_distance_spatial(transform, distance);
} }
MergeByDistanceAlgorithm::Topological => content.element_mut().merge_by_distance_topological(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))] #[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> { 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 target = rectangle.into_element();
let mut row = content; 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()); let vector = std::mem::take(row.element_mut());
// Get the bounding box of the source vector geometry // 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 // Reset the transform since we've applied it directly to the points
*row.element_mut() = result; *row.element_mut() = result;
row.set_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY); row.set_attr::<attr::Transform>(DAffine2::IDENTITY);
} }
row row
} }
@@ -942,8 +940,8 @@ where
RowsOrColumns::Rows => DVec2::new(strip.along_position, strip.cross_position), RowsOrColumns::Rows => DVec2::new(strip.along_position, strip.cross_position),
RowsOrColumns::Columns => DVec2::new(strip.cross_position, strip.along_position), RowsOrColumns::Columns => DVec2::new(strip.cross_position, strip.along_position),
}; };
let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); let row_transform: DAffine2 = row.attr_cloned_or_default::<attr::Transform>();
row.set_attribute(ATTR_TRANSFORM, DAffine2::from_translation(target_position - top_left) * row_transform); row.set_attr::<attr::Transform>(DAffine2::from_translation(target_position - top_left) * row_transform);
strip.along_position += along + separation; strip.along_position += along + separation;
} else { } else {
@@ -954,8 +952,8 @@ where
RowsOrColumns::Rows => DVec2::new(0., new_cross), RowsOrColumns::Rows => DVec2::new(0., new_cross),
RowsOrColumns::Columns => DVec2::new(new_cross, 0.), RowsOrColumns::Columns => DVec2::new(new_cross, 0.),
}; };
let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); let row_transform: DAffine2 = row.attr_cloned_or_default::<attr::Transform>();
row.set_attribute(ATTR_TRANSFORM, DAffine2::from_translation(target_position - top_left) * row_transform); row.set_attr::<attr::Transform>(DAffine2::from_translation(target_position - top_left) * row_transform);
strips.push(Strip { strips.push(Strip {
along_position: along + separation, along_position: along + separation,
@@ -986,7 +984,7 @@ async fn auto_tangents(
) -> Item<Vector> { ) -> Item<Vector> {
let (spread, preserve_existing) = (*spread.element(), *preserve_existing.element()); 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 (source, attributes) = source.into_parts();
let mut result = Vector { 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> { async fn dimensions(_: impl Ctx, content: Item<Vector>) -> Item<DVec2> {
let dimensions = content let dimensions = content
.element() .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) .map(|[top_left, bottom_right]| bottom_right - top_left)
.unwrap_or_default(); .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 mut content = content;
let (distance, join, miter_limit) = (*distance.element(), *join.element(), *miter_limit.element()); 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 transform = Affine::new(transform_attribute.to_cols_array());
let vector = std::mem::take(content.element_mut()); let vector = std::mem::take(content.element_mut());
@@ -1406,7 +1404,7 @@ where
let flattened: List<Vector> = graphic_list.clone().into_flattened_list(); let flattened: List<Vector> = graphic_list.clone().into_flattened_list();
// A fill exists when the canonical attribute carries paint // 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 let mut output: List<Vector> = flattened
.into_iter() .into_iter()
@@ -1466,14 +1464,14 @@ where
vector.stroke = None; vector.stroke = None;
let mut fill_attributes = attributes.clone(); let mut fill_attributes = attributes.clone();
// No stroke remains on the fill row // 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) Item::from_parts(vector, fill_attributes)
}); });
let mut stroke_attributes = attributes; let mut stroke_attributes = attributes;
// Drop the original fill and use the stroke paint to fill the outlined stroke // Drop the original fill and use the stroke paint to fill the outlined stroke
stroke_attributes.remove::<List<Graphic>>(ATTR_FILL); stroke_attributes.remove_attr::<graphic_types::attr::Fill>();
stroke_attributes.rename(ATTR_STROKE, 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); 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 // 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. // `upstream_footprint *= row_0_transform` recursion cancels out and leaves the originals intact.
let mut graphic_list = graphic_list; 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 { if row_0_transform.matrix2.determinant().abs() > f64::EPSILON {
let inverse = row_0_transform.inverse(); 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; *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 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 // Concatenate every vector element's subpaths into the single output compound path
for index in 0..flattened.len() { for index in 0..flattened.len() {
let Some(element) = flattened.element(index) else { continue }; 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 node_id = layer_path.iter_element_values().next_back().map(|node_id| node_id.0).unwrap_or_default();
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
(index, node_id).hash(&mut hasher); (index, node_id).hash(&mut hasher);
let collision_hash_seed = hasher.finish(); 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); output.concat(element, source_transform, collision_hash_seed);
// TODO: Make this instead use the first encountered stroke // 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 source_attributes = flattened.clone_item_attributes(primary);
let mut attributes = ItemAttributeValues::new(); let mut attributes = ItemAttributeValues::new();
attributes.insert_cloned_from(&source_attributes, ATTR_FILL); attributes.insert_cloned_from(&source_attributes, graphic_types::attr::Fill::name());
attributes.insert_cloned_from(&source_attributes, ATTR_STROKE); 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 // 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); bake_paint_transforms(&mut attributes, source_transform);
let output = std::mem::take(output_list.element_mut(0).unwrap()); 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 // 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. // 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. // 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() 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), stroke: std::mem::take(&mut content.element_mut().stroke),
}; };
// Transfer the stroke transform from the input vector content to the result. // 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() { for local_bezpath in content.element().stroke_bezpath_iter() {
// Apply the transform to compute sample locations in world space (for correct distance-based spacing) // Apply the transform to compute sample locations in world space (for correct distance-based spacing)
let mut world_bezpath = local_bezpath.clone(); 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())); world_bezpath.apply_affine(Affine::new(transform_attribute.to_cols_array()));
// Per-segment perimeter lengths (transform-baked) for distance-based spacing // Per-segment perimeter lengths (transform-baked) for distance-based spacing
@@ -1718,7 +1716,7 @@ async fn simplify(
let options = SimplifyOptions::default(); 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 transform = Affine::new(transform_attribute.to_cols_array());
let inverse_transform = transform.inverse(); let inverse_transform = transform.inverse();
@@ -1812,7 +1810,7 @@ async fn decimate(
points.iter().enumerate().filter(|(i, _)| keep[*i]).map(|(_, p)| *p).collect() 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 transform = Affine::new(transform_attribute.to_cols_array());
let inverse_transform = transform.inverse(); 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 (progression, reverse, parameterized_distance) = (progression.into_element(), reverse.into_element(), parameterized_distance.into_element());
let euclidian = !parameterized_distance; 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 mut bezpaths: Vec<_> = content.element().stroke_bezpath_iter().map(|bezpath| (bezpath, transform)).collect();
let bezpath_count = bezpaths.len() as f64; let bezpath_count = bezpaths.len() as f64;
let progression = progression.clamp(0., bezpath_count); 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 (progression, reverse, parameterized_distance, radians) = (progression.into_element(), reverse.into_element(), parameterized_distance.into_element(), radians.into_element());
let euclidian = !parameterized_distance; 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 mut bezpaths: Vec<_> = content.element().stroke_bezpath_iter().map(|bezpath| (bezpath, transform)).collect();
let bezpath_count = bezpaths.len() as f64; let bezpath_count = bezpaths.len() as f64;
let progression = progression.clamp(0., bezpath_count); 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 (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 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 inverse_linear = inverse_linear_or_repair(transform_attribute.matrix2);
let deltas: Vec<_> = (0..content.element().point_domain.positions().len()) let deltas: Vec<_> = (0..content.element().point_domain.positions().len())
@@ -2247,7 +2245,7 @@ async fn jitter_points(
}) })
.collect(); .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); apply_point_deltas(content.element_mut(), &deltas, transform);
content content
@@ -2267,7 +2265,7 @@ async fn offset_points(
) -> Item<Vector> { ) -> Item<Vector> {
let mut content = content; let mut content = content;
let distance = *distance.element(); 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 inverse_linear = inverse_linear_or_repair(transform_attribute.matrix2);
let deltas: Vec<_> = (0..content.element().point_domain.positions().len()) let deltas: Vec<_> = (0..content.element().point_domain.positions().len())
@@ -2285,7 +2283,7 @@ async fn offset_points(
}) })
.collect(); .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); apply_point_deltas(content.element_mut(), &deltas, transform);
content 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 { 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_a = gradient_list_a.attr_cloned_or_default::<attr::Transform>(0);
let transform_b = gradient_list_b.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0); let transform_b = gradient_list_b.attr_cloned_or_default::<attr::Transform>(0);
let start_a = transform_a.translation; let start_a = transform_a.translation;
let end_a = transform_a.translation + transform_a.matrix2.x_axis; 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 metadata_source = if time < 0.5 { gradient_list_a } else { gradient_list_b };
let mut gradient_list = metadata_source.clone(); 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) gradient_with_stops(gradient_list, stops)
}), }),
@@ -2499,7 +2497,7 @@ async fn morph<I: IntoGraphicList>(
let default_polyline = || { let default_polyline = || {
let mut default_path = BezPath::new(); let mut default_path = BezPath::new();
for index in 0..content.len() { 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 origin = transform_attribute.translation;
let point = kurbo::Point::new(origin.x, origin.y); let point = kurbo::Point::new(origin.x, origin.y);
if index == 0 { if index == 0 {
@@ -2513,7 +2511,7 @@ async fn morph<I: IntoGraphicList>(
let control_bezpaths: Vec<BezPath> = { let control_bezpaths: Vec<BezPath> = {
// User-provided path: collect all subpaths with the path's transform applied // 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 let paths: Vec<BezPath> = path
.element() .element()
.stroke_bezpath_iter() .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() { if content.element(source_index).is_none() || content.element(target_index).is_none() {
return 0.; return 0.;
} }
let source_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, source_index); let source_transform: DAffine2 = content.attr_cloned_or_default::<attr::Transform>(source_index);
let target_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, target_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 (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(); 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 // 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 source_blend_mode: BlendMode = content.attr_cloned_or_default::<attr::BlendMode>(source_index);
let target_blend_mode: BlendMode = content.attribute_cloned_or_default(ATTR_BLEND_MODE, target_index); let target_blend_mode: BlendMode = content.attr_cloned_or_default::<attr::BlendMode>(target_index);
let source_opacity: f64 = content.attribute_cloned_or(ATTR_OPACITY, source_index, 1.); let source_opacity: f64 = content.attr_cloned_or_default::<attr::Opacity>(source_index);
let target_opacity: f64 = content.attribute_cloned_or(ATTR_OPACITY, target_index, 1.); let target_opacity: f64 = content.attr_cloned_or_default::<attr::Opacity>(target_index);
let source_fill: f64 = content.attribute_cloned_or(ATTR_OPACITY_FILL, source_index, 1.); let source_fill: f64 = content.attr_cloned_or_default::<attr::OpacityFill>(source_index);
let target_fill: f64 = content.attribute_cloned_or(ATTR_OPACITY_FILL, target_index, 1.); let target_fill: f64 = content.attr_cloned_or_default::<attr::OpacityFill>(target_index);
let source_clip: bool = content.attribute_cloned_or_default(ATTR_CLIPPING_MASK, source_index); let source_clip: bool = content.attr_cloned_or_default::<attr::ClippingMask>(source_index);
let target_clip: bool = content.attribute_cloned_or_default(ATTR_CLIPPING_MASK, target_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_blend_mode = if time < 0.5 { source_blend_mode } else { target_blend_mode };
let lerped_opacity = source_opacity + (target_opacity - source_opacity) * time; 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() // 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. // correctly cancels the element transform, keeping the stroke uniform when Stroke is after Transform.
let lerped_transform = { let lerped_transform = {
let source_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, source_index); let source_transform: DAffine2 = content.attr_cloned_or_default::<attr::Transform>(source_index);
let target_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, target_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 (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(); 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. // in which case we skip pre-compensation to avoid propagating NaN through merged_layers transforms.
if lerped_transform.matrix2.determinant().abs() > f64::EPSILON { if lerped_transform.matrix2.determinant().abs() > f64::EPSILON {
let lerped_inverse = lerped_transform.inverse(); 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; *transform = lerped_inverse * *transform;
} }
} }
@@ -2729,8 +2727,8 @@ async fn morph<I: IntoGraphicList>(
let endpoint_element = content.element(endpoint_index).unwrap(); let endpoint_element = content.element(endpoint_index).unwrap();
let mut attributes = content.clone_item_attributes(endpoint_index); let mut attributes = content.clone_item_attributes(endpoint_index);
attributes.insert(ATTR_TRANSFORM, lerped_transform); attributes.set_attr::<attr::Transform>(lerped_transform);
attributes.insert(ATTR_EDITOR_MERGED_LAYERS, graphic_list_content); attributes.set_attr::<graphic_types::attr::editor::MergedLayers>(graphic_list_content);
return Item::from_parts(endpoint_element.clone(), attributes); 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 mut vector = Vector { stroke, ..Default::default() };
let fill_paint = { let fill_paint = {
let source = graphic_list_at(&content, source_index, ATTR_FILL); let source = graphic_list_at::<graphic_types::attr::Fill>(&content, source_index);
let target = graphic_list_at(&content, target_index, ATTR_FILL); let target = graphic_list_at::<graphic_types::attr::Fill>(&content, target_index);
lerp_graphic(source.as_deref(), target.as_deref(), time) lerp_graphic(source.as_deref(), target.as_deref(), time)
}; };
let stroke_paint = { let stroke_paint = {
let source = graphic_list_at(&content, source_index, ATTR_STROKE); let source = graphic_list_at::<graphic_types::attr::Stroke>(&content, source_index);
let target = graphic_list_at(&content, target_index, ATTR_STROKE); let target = graphic_list_at::<graphic_types::attr::Stroke>(&content, target_index);
lerp_graphic(source.as_deref(), target.as_deref(), time) 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) // 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 primary_index = if time < 0.5 { source_index } else { target_index };
let mut item = Item::new_from_element(vector); 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 // 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() { if content.attr::<attr::BlendMode>(source_index).is_some() {
item.set_attribute(ATTR_BLEND_MODE, lerped_blend_mode); item.set_attr::<attr::BlendMode>(lerped_blend_mode);
} }
if content.attribute::<f64>(ATTR_OPACITY, source_index).is_some() { if content.attr::<attr::Opacity>(source_index).is_some() {
item.set_attribute(ATTR_OPACITY, lerped_opacity); item.set_attr::<attr::Opacity>(lerped_opacity);
} }
if content.attribute::<f64>(ATTR_OPACITY_FILL, source_index).is_some() { if content.attr::<attr::OpacityFill>(source_index).is_some() {
item.set_attribute(ATTR_OPACITY_FILL, lerped_fill); item.set_attr::<attr::OpacityFill>(lerped_fill);
} }
if content.attribute::<bool>(ATTR_CLIPPING_MASK, source_index).is_some() { if content.attr::<attr::ClippingMask>(source_index).is_some() {
item.set_attribute(ATTR_CLIPPING_MASK, lerped_clip); item.set_attr::<attr::ClippingMask>(lerped_clip);
} }
if let Some(layer_path) = content.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, primary_index) { if let Some(layer_path) = content.attr::<attr::editor::LayerPath>(primary_index) {
item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone()); 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 { 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 { if let Some(stroke) = stroke_paint {
item.set_attribute(ATTR_STROKE, stroke); item.set_attr::<graphic_types::attr::Stroke>(stroke);
} }
item 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> { fn bevel(_: impl Ctx, source: Item<Vector>, #[default(10.)] distance: Item<Length>) -> Item<Vector> {
let distance = *distance.element(); 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(); let (element, attributes) = source.into_parts();
Item::from_parts(bevel_algorithm(element, transform, distance), attributes) 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))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
fn point_inside(_: impl Ctx, source: Item<Vector>, point: Item<DVec2>) -> Item<bool> { fn point_inside(_: impl Ctx, source: Item<Vector>, point: Item<DVec2>) -> Item<bool> {
let point = point.into_element(); 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); let inside = source.element().check_point_inside_shape(transform, point);
Item::new_from_element(inside) Item::new_from_element(inside)
@@ -3292,7 +3290,7 @@ async fn index_points(
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn path_length(_: impl Ctx, source: Item<Vector>) -> Item<f64> { 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 let length = source
.element() .element()
.stroke_bezpath_iter() .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 new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
let vector = content.eval(new_ctx).await; 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_scale = transform.matrix2.determinant().abs();
let area = vector.element().stroke_bezpath_iter().map(|subpath| subpath.area() * area_scale).sum::<f64>(); 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 new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
let vector = content.eval(new_ctx).await; 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); let position = element_centroid(vector.element(), transform, centroid_type);
Item::new_from_element(position) Item::new_from_element(position)
@@ -3390,7 +3388,7 @@ mod test {
fn create_vector_item(bezpath: BezPath, transform: DAffine2) -> Item<Vector> { fn create_vector_item(bezpath: BezPath, transform: DAffine2) -> Item<Vector> {
let mut row = Vector::default(); let mut row = Vector::default();
row.append_bezpath(bezpath); 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> { fn item<T>(value: T) -> Item<T> {
@@ -3559,7 +3557,7 @@ mod test {
// Test a rectangular path with non-zero rotation // Test a rectangular path with non-zero rotation
let square = Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)); let square = Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY));
let mut square = List::new_from_element(square); 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 = BoundingBoxNodeMapped { content: FutureWrapperNode(square) }.eval(Footprint::default()).await;
let bounding_box = bounding_box.element(0).unwrap(); let bounding_box = bounding_box.element(0).unwrap();
assert_eq!(bounding_box.region_manipulator_groups().count(), 1); assert_eq!(bounding_box.region_manipulator_groups().count(), 1);
@@ -3692,7 +3690,7 @@ mod test {
async fn morph() { async fn morph() {
let mut rectangles = vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); 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(); 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); rectangles.push(second_rectangle);
let morphed = super::morph( 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.)] 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) // 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] #[tokio::test]
@@ -3724,11 +3722,11 @@ mod test {
}; };
let item_a = Item::new_from_element(rect()) let item_a = Item::new_from_element(rect())
.with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY) .with_attr::<attr::Transform>(DAffine2::IDENTITY)
.with_attribute(ATTR_FILL, List::new_from_element(Color::RED).into_graphic_list()); .with_attr::<graphic_types::attr::Fill>(List::new_from_element(Color::RED).into_graphic_list());
let item_b = Item::new_from_element(rect()) let item_b = Item::new_from_element(rect())
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation((-100., -100.).into())) .with_attr::<attr::Transform>(DAffine2::from_translation((-100., -100.).into()))
.with_attribute(ATTR_FILL, List::new_from_element(Color::BLUE).into_graphic_list()); .with_attr::<graphic_types::attr::Fill>(List::new_from_element(Color::BLUE).into_graphic_list());
let mut content = List::new_from_item(item_a); let mut content = List::new_from_item(item_a);
content.push(item_b); content.push(item_b);
@@ -3744,7 +3742,7 @@ mod test {
.await; .await;
let morphed = List::new_from_item(morphed); 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 // Interpolated color between red and blue should have >0 value on both R and B
let Some(Graphic::Color(colors)) = fill.element(0) else { let Some(Graphic::Color(colors)) = fill.element(0) else {
@@ -3825,7 +3823,7 @@ mod test {
source.push(curve.as_path_el()); source.push(curve.as_path_el());
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.)); 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 = super::bevel((), vector_item, Item::new_from_element(2_f64.sqrt() * 100.));
let beveled = beveled.element(); let beveled = beveled.element();