use core_types::attribute::{Attr, Transform as TransformAttr}; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn}; use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt, Level}; use core_types::list::{AttributeDyn, AttributeValueDyn, Item, List, ListDyn}; use core_types::registry::types::{Angle, SignedInteger}; use core_types::uuid::NodeId; use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, Color, Context, Ctx, DeriveCtx, ExtractIndex, InjectIndex}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, Artboard, Vector}; use raster_types::{CPU, GPU, Raster}; use vector_types::gradient::{GradientSpreadMethod, GradientType}; use vector_types::{GradientStop, GradientStops, ReferencePoint}; /// Returns the value at the specified index in the list. /// If no value exists at that index, the type's default value is returned. #[node_macro::node(category("General"))] pub fn index_elements( _: impl Ctx, /// The list of data. #[implementations( List, List, List, List>, List>, List, List, List, List, List, List, )] list: T, /// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item. index: SignedInteger, ) -> T::Output where T::Output: Clone + Default, { let index = index as i32; if index < 0 { list.at_index_from_end(-index as usize) } else { list.at_index(index as usize) }.unwrap_or_default() } /// Returns the list with the element at the specified index removed. /// If no value exists at that index, the list is returned unchanged. #[node_macro::node(category("General"))] pub fn omit_element( _: impl Ctx, /// The list of data. #[implementations( List, List, List, List, List>, List>, List, List, )] list: T, /// The index of the item to remove, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item. index: SignedInteger, ) -> T { let index = index as i32; if index < 0 { list.omit_index_from_end(index.unsigned_abs() as usize) } else { list.omit_index(index as usize) } } /// Returns the bare element (without the item's attributes) at the specified index in a `List`. /// Use this when downstream nodes want just the inner value rather than a `List` containing a single item. /// If no value exists at that index, the element type's default is returned. #[node_macro::node(category("General"))] pub fn extract_element( _: impl Ctx, /// The `List` of data to extract from. #[implementations( List, List, List, List, List, List, List, List>, List, List, )] list: List, /// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item. index: SignedInteger, ) -> T { let len = list.len(); let index = index as i32; let resolved = if index < 0 { let from_end = index.unsigned_abs() as usize; if from_end > len { return T::default(); } len - from_end } else { index as usize }; list.element(resolved).cloned().unwrap_or_default() } #[node_macro::node(category("General"))] fn map( ctx: impl Ctx + DeriveCtx, #[implementations( List, List, List>, List, List, List, )] content: List, #[implementations( Context -> List, Context -> List, Context -> List>, Context -> List, Context -> List, Context -> List, )] mapped: impl Node, Output = List>, ) -> Result, Interrupt> { let spilled = ctx.index_head(); let mut rows = List::new(); for (i, row) in content.into_iter().enumerate() { let item = List::new_from_item(row); let scoped = ctx.push_vararg(&item); let list = mapped.eval(&scoped.ctx().promoted(&spilled, i as u64))?; rows.extend(list); } Ok(rows) } #[node_macro::node(category("General"))] fn mirror( _: impl Ctx, #[implementations( List, List, List, List>, List, List, )] content: List, #[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint, #[unit(" px")] offset: f64, #[range] #[soft(-90..90)] angle: Angle, #[default(true)] keep_original: bool, ) -> List where List: BoundingBox, { // Normalize the direction vector let normal = DVec2::from_angle(angle.to_radians()); // The mirror reference may be based on the bounding box if an explicit reference point is chosen let RenderBoundingBox::Rectangle(bounding_box) = content.bounding_box(DAffine2::IDENTITY, false) else { return content; }; let reference_point_location = relative_to_bounds.point_in_bounding_box((bounding_box[0], bounding_box[1]).into()); let mirror_reference_point = reference_point_location.map(|point| point + normal * offset); // Create the reflection matrix let reflection = DAffine2::from_mat2_translation( glam::DMat2::from_cols( DVec2::new(1. - 2. * normal.x * normal.x, -2. * normal.y * normal.x), DVec2::new(-2. * normal.x * normal.y, 1. - 2. * normal.y * normal.y), ), DVec2::ZERO, ); // Apply reflection around the reference point let reflected_transform = if let Some(mirror_reference_point) = mirror_reference_point { DAffine2::from_translation(mirror_reference_point) * reflection * DAffine2::from_translation(-mirror_reference_point) } else { reflection * DAffine2::from_translation(DVec2::from_angle(angle.to_radians()) * DVec2::splat(-offset)) }; let mut result_list = List::new(); // Add original items depending on the keep_original flag if keep_original { for item in content.clone().into_iter() { result_list.push(item); } } // Create and add mirrored items for mut row in content.into_iter() { let current_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); row.set_attribute(ATTR_TRANSFORM, reflected_transform * current_transform); result_list.push(row); } result_list } /// Returns the path identifying the subgraph (network) that contains this proto node — i.e. the input `node_path` /// with its own trailing entry dropped. The terminating element of the returned path is the document node whose /// encapsulated network we live in, so the path doubles as a unique reference to that node at any nesting depth. /// Used as the value source for stamping the `editor:layer_path` attribute on each item of a layer's output, which lets /// editor tools (e.g. selection, click target routing) trace data back to its owning layer regardless of whether /// the layer is at the root document network or nested inside a custom subgraph. #[node_macro::node(name("Path of Subgraph"), category(""))] pub fn path_of_subgraph(_: impl Ctx, node_path: List) -> List { let len = node_path.len(); node_path.into_iter().take(len.saturating_sub(1)).collect() } /// Sets a named attribute on the input `List`, computing one value per item via the value-producing input. That input /// is evaluated once per item, with the item's index and the item itself (as a `List` containing only that item, /// passed as a vararg) provided via context, so the upstream pipeline can return a different value per item that may /// be derived from the item's own data. If the attribute already exists, its values are replaced; if not, it's added. /// The value is type-erased into an `AttributeValueDyn` by an auto-inserted convert node, so this node only /// monomorphizes over `T` instead of the cartesian product `(T, U)`. #[node_macro::node(category("Attributes: Write"))] fn write_attribute( ctx: impl Ctx + DeriveCtx, /// The `List` to set the named attribute on (one value per item). #[implementations( List, List, List, List>, List, List, List, List, List, List, List, List, List, )] mut content: List, /// The attribute name (key) to write or replace. name: String, /// The node that produces the attribute value for each item. Called once per item with the item's index in context. #[implementations(Context -> AttributeValueDyn)] value: impl Node, Output = AttributeValueDyn>, ) -> Result, Interrupt> { let spilled = ctx.index_head(); for index in 0..content.len() { let row = content.clone_item(index).expect("index is within bounds"); let item = List::new_from_item(row); let scoped = ctx.push_vararg(&item); let v = value.eval(&scoped.ctx().promoted(&spilled, index as u64))?; content.set_attribute_value_dyn(&name, index, v); } Ok(content) } /// Sets a named attribute on the primary list, with each value taken from the corresponding item's element in the source list (paired by index, wrapping if the source has fewer items). /// The source is type-erased into an `AttributeDyn` by an auto-inserted convert node, so this node only monomorphizes over `T` instead of the cartesian product `(T, U)`. #[node_macro::node(category("Attributes: Write"))] fn attach_attribute( _: impl Ctx, /// The `List` to attach the new attribute to. #[implementations( List, List, List, List>, List, List, List, List, List, List, List, List, List, )] mut content: List, /// The source values to attach. #[expose] source: AttributeDyn, /// The name to assign to the new destination attribute. name: String, ) -> List { if source.is_empty() { return content; } content.set_attribute_dyn(name, source); content } /// Reads a named `Vector` attribute from the input list, outputting each value as an element of a new `Vector[]`. #[node_macro::node(category("Attributes: Read"))] fn read_attribute_vector( _: impl Ctx, content: ListDyn, /// The attribute name (key) to read. name: String, ) -> List { let mut result = List::with_capacity(content.len()); for index in 0..content.len() { let Some(value) = content.attribute::(&name, index) else { continue }; result.push(Item::new_from_element(value.clone())); } result } /// Reads a named numeric attribute (`f64`, `u64`, or `u32`) from the input list, outputting each value as an element of a new `f64[]`. Integer values are converted to `f64`. #[node_macro::node(category("Attributes: Read"))] fn read_attribute_number( _: impl Ctx, content: ListDyn, /// The attribute name (key) to read. name: String, ) -> List { let mut result = List::with_capacity(content.len()); for index in 0..content.len() { let value = content .attribute::(&name, index) .copied() .or_else(|| content.attribute::(&name, index).map(|v| *v as f64)) .or_else(|| content.attribute::(&name, index).map(|v| *v as f64)); let Some(value) = value else { continue }; result.push(Item::new_from_element(value)); } result } /// Reads a named `bool` attribute from the input list, outputting each value as an element of a new `bool[]`. #[node_macro::node(category("Attributes: Read"))] fn read_attribute_bool( _: impl Ctx, content: ListDyn, /// The attribute name (key) to read. name: String, ) -> List { let mut result = List::with_capacity(content.len()); for index in 0..content.len() { let Some(value) = content.attribute::(&name, index) else { continue }; result.push(Item::new_from_element(*value)); } result } /// Reads a named `String` attribute from the input list, outputting each value as an element of a new `String[]`. #[node_macro::node(category("Attributes: Read"))] fn read_attribute_string( _: impl Ctx, content: ListDyn, /// The attribute name (key) to read. name: String, ) -> List { let mut result = List::with_capacity(content.len()); for index in 0..content.len() { let Some(value) = content.attribute::(&name, index) else { continue }; result.push(Item::new_from_element(value.clone())); } result } /// Reads a named `DAffine2` transform attribute from the input list, outputting each value as an element of a new `DAffine2[]`. #[node_macro::node(category("Attributes: Read"))] fn read_attribute_transform( _: impl Ctx, content: ListDyn, /// The attribute name (key) to read. name: String, ) -> List { let mut result = List::with_capacity(content.len()); for index in 0..content.len() { let Some(value) = content.attribute::(&name, index) else { continue }; result.push(Item::new_from_element(*value)); } result } /// Reads a named `Color` attribute from the input list, outputting each value as an element of a new `Color[]`. #[node_macro::node(category("Attributes: Read"))] fn read_attribute_color( _: impl Ctx, content: ListDyn, /// The attribute name (key) to read. name: String, ) -> List { let mut result = List::with_capacity(content.len()); for index in 0..content.len() { let Some(value) = content.attribute::(&name, index) else { continue }; result.push(Item::new_from_element(*value)); } result } /// Reads a named `BlendMode` attribute from the input list, outputting each value as an element of a new `BlendMode[]`. #[node_macro::node(category("Attributes: Read"))] fn read_attribute_blend_mode( _: impl Ctx, content: ListDyn, /// The attribute name (key) to read. name: String, ) -> List { let mut result = List::with_capacity(content.len()); for index in 0..content.len() { let Some(value) = content.attribute::(&name, index) else { continue }; result.push(Item::new_from_element(*value)); } result } /// Reads a named `GradientType` attribute from the input list, outputting each value as an element of a new `GradientType[]`. #[node_macro::node(category("Attributes: Read"))] fn read_attribute_gradient_type( _: impl Ctx, content: ListDyn, /// The attribute name (key) to read. name: String, ) -> List { let mut result = List::with_capacity(content.len()); for index in 0..content.len() { let Some(value) = content.attribute::(&name, index) else { continue }; result.push(Item::new_from_element(*value)); } result } /// Reads a named `GradientSpreadMethod` attribute from the input list, outputting each value as an element of a new `GradientSpreadMethod[]`. #[node_macro::node(category("Attributes: Read"))] fn read_attribute_spread_method( _: impl Ctx, content: ListDyn, /// The attribute name (key) to read. name: String, ) -> List { let mut result = List::with_capacity(content.len()); for index in 0..content.len() { let Some(value) = content.attribute::(&name, index) else { continue }; result.push(Item::new_from_element(*value)); } result } /// Reads a named `GradientStops` attribute from the input list, outputting each value as an element of a new `GradientStops[]`. #[node_macro::node(category("Attributes: Read"))] fn read_attribute_gradient_stops( _: impl Ctx, content: ListDyn, /// The attribute name (key) to read. name: String, ) -> List { let mut result = List::with_capacity(content.len()); for index in 0..content.len() { let Some(value) = content.attribute::(&name, index) else { continue }; result.push(Item::new_from_element(value.clone())); } result } /// Reads a named `Artboard` attribute from the input list, outputting each value as an element of a new `Artboard[]`. #[node_macro::node(category("Attributes: Read"))] fn read_attribute_artboard( _: impl Ctx, content: ListDyn, /// The attribute name (key) to read. name: String, ) -> List { let mut result = List::with_capacity(content.len()); for index in 0..content.len() { let Some(value) = content.attribute::(&name, index) else { continue }; result.push(Item::new_from_element(value.clone())); } result } /// Reads a named `Raster` attribute from the input list, outputting each value as an element of a new `Raster[]`. #[node_macro::node(category("Attributes: Read"))] fn read_attribute_raster( _: impl Ctx, content: ListDyn, /// The attribute name (key) to read. name: String, ) -> List> { let mut result = List::with_capacity(content.len()); for index in 0..content.len() { let Some(value) = content.attribute::>(&name, index) else { continue }; result.push(Item::new_from_element(value.clone())); } result } /// Joins two levels of the same type, the base's lanes followed by the new's. #[node_macro::node(category("General"), extent(extend_extent))] pub fn extend( ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, /// The wire whose lanes appear at the start of the extended level. base: impl Node, Output = T>, /// The wire whose lanes appear at the end of the extended level. #[expose] new: impl Node, Output = T>, ) -> Result { let split = match base.extent(ctx, Level::Total) { GPoll::Final(Extent::Exactly(count)) => count as u64, // A scalar side joins the concat as a single lane, per `Extent::sum`. GPoll::Final(Extent::Free) => 1, GPoll::Pending => return Err(Interrupt::Pending), _ => return Err(GraphError::new("extend over a non-exact base extent").into()), }; let lane = ctx.innermost_index(); match lane < split { true => base.eval(ctx), false => { let mut shifted = *ctx; shifted.set_index(lane - split); new.eval(&shifted) } } } /// The top level sums both sides; inner levels must agree (rectangular), a /// free side defers to the other. fn extend_extent(base: ExtentIn<'_>, new: ExtentIn<'_>, level: LevelIn) -> GPoll { match level.top() { true => Extent::sum(base.at(level), new.at(level)), false => base.at(level).zip(new.at(level)).and_then(|extents| match extents { (Extent::Free, other) | (other, Extent::Free) => GPoll::Final(other), (base, new) if base == new => GPoll::Final(base), _ => GPoll::error("extend inner extents differ"), }), } } // TODO: Eventually remove this document upgrade code /// Performs an obsolete function as part of a migration from an older document format. /// Users are advised to delete this node and replace it with a new one. #[node_macro::node(category(""))] pub fn legacy_layer_extend( _: impl Ctx, #[implementations(List, List, List, List, List>, List>, List, List)] base: List, #[expose] #[implementations(List, List, List, List, List>, List>, List, List)] new: List, nested_node_path: List, ) -> List { // Get the penultimate element of the node path, or None if the path is too short // This is used to get the ID of the user-facing parent layer-style node (which encapsulates this internal node). let layer = { let index = nested_node_path.len().wrapping_sub(2); nested_node_path.element(index).copied() }; let mut base = base; for mut row in new.into_iter() { row.set_attribute(ATTR_EDITOR_LAYER_PATH, layer); base.push(row); } base } /// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input. /// The inverse of this node is 'Flatten Graphic'. #[node_macro::node(category("General"), extent(wrap_graphic_extent))] pub fn wrap_graphic(_: impl Ctx + ExtractIndex + InjectIndex + Copy, content: IList) -> Result, Interrupt> { // SAFETY: a materialized input's frames are arena-resident. let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) }; Ok(Graphic::Group(core_types::record::Group { row: None, content: core_types::record::GroupContent::Run(item), })) } /// The collected group is the level's single lane. fn wrap_graphic_extent(_content: ListIn<'_, Graphic>, _level: LevelIn) -> GPoll { GPoll::Final(Extent::Exactly(1)) } /// Converts the level's elements into `Graphic` elements. A `Graphic` level passes through unchanged. #[node_macro::node(category("General"))] pub fn to_graphic + Clone + Send + Sync + core_types::CacheHash + 'static>( _: impl Ctx, #[implementations(Graphic, Vector, Raster, Raster, Color, GradientStops, String)] content: T, ) -> Graphic { content.into() } /// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled. #[node_macro::node(category("General"), extent(flatten_graphic_extent))] pub fn flatten_graphic(ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, content: IList, fully_flatten: bool) -> Result)>, Interrupt> { let mut remaining = ctx.innermost_index() as usize; for row in 0..content.len() { let graphic = content.element_ref(row); let count = crate::record::leaf_count(graphic, fully_flatten, 0); if remaining >= count { remaining -= count; continue; } let transform: DAffine2 = content.lane(row).attr::(); if let Some((leaf, composed)) = crate::record::locate(graphic, transform, fully_flatten, 0, &mut remaining) { return Ok((leaf, Attr(composed))); } } Err(GraphError::new("flatten addressed past its leaf count").into()) } /// The level holds one row per leaf of the walk. fn flatten_graphic_extent(content: ListIn<'_, Graphic>, fully_flatten: ValueIn<'_, bool>, level: LevelIn) -> GPoll { match level.top() { true => fully_flatten .get() .zip(content.get()) .map(|(fully_flatten, content)| Extent::Exactly((0..content.len()).map(|row| crate::record::leaf_count(content.element_ref(row), fully_flatten, 0)).sum())), false => GPoll::Final(Extent::Exactly(1)), } } /// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content. #[node_macro::node(category("Vector"))] pub fn flatten_vector(_: impl Ctx, #[implementations(List, List)] content: T) -> List { let graphic_list = content.into_graphic_list(); let mut output: List = graphic_list.clone().into_flattened_list(); // TODO: Replace this snapshot hack with per-layer metadata driven by each layer's Monitor node. // TODO: Flattening here erases the upstream `List` hierarchy that editor metadata collection walks // TODO: to populate `upstream_footprints` / `local_transforms` / `click_targets` per child layer. As a workaround // TODO: we stash the pre-flattened list on the output so `List::collect_metadata` can recurse into it, // TODO: which conflates render output with editor metadata and forces the pre-compensation dance below. // TODO: The cleaner fix is to drive each layer's metadata from its own Monitor's captured `(Context, List)`, // TODO: at which point this attribute (and the equivalents in Boolean Operation, Solidify Stroke, Flatten Path, // TODO: Morph, Rasterize) become unnecessary. if !output.is_empty() { // Item 0 carries a composed transform inherited from the flattened input, but the merged_layers // already holds the original transforms; pre-compensate by item 0's inverse so the renderer's // `upstream_footprint *= item_0_transform` recursion cancels out and leaves the originals intact. let mut graphic_list = graphic_list; let item_0_transform: DAffine2 = output.attribute_cloned_or_default(ATTR_TRANSFORM, 0); if item_0_transform.matrix2.determinant().abs() > f64::EPSILON { let inverse = item_0_transform.inverse(); for transform in graphic_list.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { *transform = inverse * *transform; } } output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list); } output } /// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content. #[node_macro::node(category("Raster"))] pub fn flatten_raster(_: impl Ctx, #[implementations(List, List>)] content: T) -> List> { content.into_flattened_list() } /// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content. #[node_macro::node(category("General"))] pub fn flatten_color(_: impl Ctx, #[implementations(List, List)] content: T) -> List { content.into_flattened_list() } /// Converts a `Graphic[]` into a `GradientStops[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content. #[node_macro::node(category("General"))] pub fn flatten_gradient(_: impl Ctx, #[implementations(List, List)] content: T) -> List { content.into_flattened_list() } /// Constructs a gradient from a `Color[]`, where the colors are evenly distributed as gradient stops across the range from 0 to 1. #[node_macro::node(category("Color"))] fn colors_to_gradient(_: impl Ctx + ExtractIndex + InjectIndex + Copy, colors: IList) -> GradientStops { let stop = |position: f64, color: Color| GradientStop { position, midpoint: 0.5, color }; match colors.len() { 0 => GradientStops::new(vec![stop(0., Color::BLACK), stop(1., Color::BLACK)]), 1 => GradientStops::new(vec![stop(0., colors.get(0)), stop(1., colors.get(0))]), total => GradientStops::new((0..total).map(|index| stop(index as f64 / (total - 1) as f64, colors.get(index)))), } }