Convert the node catalog to rank-polymorphic Item and List kernels, materialize stored values as ranked wires, and display wire rank in the graph

This commit is contained in:
Keavon Chambers
2026-07-20 21:27:13 -07:00
committed by Dennis Kobert
parent ead622b969
commit 8bddd5680b
46 changed files with 1281 additions and 456 deletions

View File

@@ -16,7 +16,7 @@ wgpu = ["dep:raster-types", "raster-types/wgpu"]
# Local dependencies
dyn-any = { workspace = true }
core-types = { workspace = true }
graphene-hash = { workspace = true }
graphene-hash = { workspace = true, features = ["derive"] }
vector-types = { workspace = true }
text-nodes = { workspace = true }
graphene-resource = { workspace = true }

View File

@@ -1,6 +1,7 @@
use core_types::transform::Footprint;
use dyn_any::{DynAny, StaticType, StaticTypeSized};
use glam::DVec2;
use graphene_hash::CacheHash;
use std::fmt::Debug;
use std::hash::{Hash, Hasher};
use std::ptr::addr_of;
@@ -61,7 +62,7 @@ pub trait GetEditorPreferences {
fn max_render_region_area(&self) -> u32;
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ExportFormat {
#[default]
@@ -69,14 +70,14 @@ pub enum ExportFormat {
Raster,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, graphene_hash::CacheHash)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TimingInformation {
pub time: f64,
pub animation_time: Duration,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, graphene_hash::CacheHash)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RenderConfig {
pub viewport: Footprint,
@@ -136,7 +137,7 @@ impl<Io> Hash for EditorApi<Io> {
}
}
impl<Io> core_types::graphene_hash::CacheHash for EditorApi<Io> {
impl<Io> CacheHash for EditorApi<Io> {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(self, state);
}

View File

@@ -601,6 +601,11 @@ impl ItemAttributeValues {
self.0.iter().map(|(key, value)| (key.as_str(), &**value))
}
/// Returns a type-erased reference to the value of the attribute with the given key, if it exists.
pub fn get_any(&self, key: &str) -> Option<&dyn std::any::Any> {
self.0.iter().find_map(|(existing_key, value)| if existing_key == key { Some((**value).as_any()) } else { None })
}
/// Returns a debug-formatted string representation of the attribute value for the given key, if it exists.
/// The `overrides` function can provide custom formatting for specific type.
pub fn display_value(&self, key: &str, overrides: fn(&dyn std::any::Any) -> Option<String>) -> Option<String> {
@@ -1326,6 +1331,10 @@ impl<T> Item<T> {
}
}
unsafe impl<T: StaticTypeSized> StaticType for Item<T> {
type Static = Item<T::Static>;
}
// ===========
// ItemIter<T>
// ===========

View File

@@ -61,6 +61,27 @@ impl Clampable for DVec2 {
}
}
// Implement for ranked wires (element-wise clamping across the frame)
use crate::list::{Item, List};
impl<T: Clampable> Clampable for Item<T> {
fn clamp_hard_min(self, min: f64) -> Self {
let (element, attributes) = self.into_parts();
Item::from_parts(element.clamp_hard_min(min), attributes)
}
fn clamp_hard_max(self, max: f64) -> Self {
let (element, attributes) = self.into_parts();
Item::from_parts(element.clamp_hard_max(max), attributes)
}
}
impl<T: Clampable> Clampable for List<T> {
fn clamp_hard_min(self, min: f64) -> Self {
self.into_iter().map(|item| item.clamp_hard_min(min)).collect()
}
fn clamp_hard_max(self, max: f64) -> Self {
self.into_iter().map(|item| item.clamp_hard_max(max)).collect()
}
}
#[cfg(feature = "serde")]
#[derive(serde::Deserialize)]
struct LegacyTable<T> {

View File

@@ -77,8 +77,7 @@ impl Convert<DVec2, ()> for DVec2 {
}
/// Constructs `Self` from a single anchor point at the given position. Implemented by the vector crate's
/// path type so the `Convert` impl below can build a single-point path without core-types depending on
/// that crate (mirroring how [`ListConvert`] bridges per-item list conversions).
/// path type so a position wire can convert to a single-point path without core-types depending on that crate.
pub trait FromAnchorPosition {
fn from_anchor_position(position: DVec2) -> Self;
}

View File

@@ -217,6 +217,23 @@ impl From<()> for Footprint {
}
}
/// Consumes an item's `transform` attribute by baking it into the underlying value itself.
pub trait BakeTransform {
fn bake_transform(&mut self, transform: &DAffine2);
}
impl BakeTransform for DAffine2 {
fn bake_transform(&mut self, transform: &DAffine2) {
*self = *transform * *self;
}
}
impl BakeTransform for DVec2 {
fn bake_transform(&mut self, transform: &DAffine2) {
*self = transform.transform_point2(*self);
}
}
pub trait ApplyTransform {
fn apply_transform(&mut self, modification: &DAffine2);
fn left_apply_transform(&mut self, modification: &DAffine2);

View File

@@ -1767,7 +1767,7 @@ fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut
}
fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
// Aggregate all items' targets per element_id so multi-item lists (e.g. 'Text' node with "Separate Glyphs" active) 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]`.
let item_zero_transform: DAffine2 = if source.lane_count() > 0 { source.attr::<Transform>(0) } else { DAffine2::IDENTITY };
let item_zero_inverse = if transform_is_invertible(item_zero_transform) {

View File

@@ -63,6 +63,13 @@ impl core_types::ops::FromAnchorPosition for Vector {
}
}
// Lets a position wire feed a ranked vector connector through the input adapter's element conversion
impl From<DVec2> for Vector {
fn from(position: DVec2) -> Self {
<Self as core_types::ops::FromAnchorPosition>::from_anchor_position(position)
}
}
// Identity item conversion so `List<Vector>` satisfies the blanket `Convert<List<U>, ()> for List<T>`, letting its
// auto-inserted input wrapper be a `ConvertNode` (which also accepts a `DVec2` anchor position) rather than an `IntoNode`.
impl core_types::ops::ListConvert<Vector> for Vector {
@@ -71,6 +78,15 @@ impl core_types::ops::ListConvert<Vector> for Vector {
}
}
impl core_types::transform::BakeTransform for Vector {
fn bake_transform(&mut self, transform: &glam::DAffine2) {
for (_, point) in self.point_domain.positions_mut() {
*point = transform.transform_point2(*point);
}
self.segment_domain.transform(*transform);
}
}
impl Vector {
/// Add a subpath to this vector path.
pub fn append_subpath(&mut self, subpath: impl Borrow<Subpath<PointId>>, preserve_id: bool) {

View File

@@ -20,7 +20,7 @@ fn upload_to_texture(device: &wgpu::Device, queue: &wgpu::Queue, image: &Raster<
device.create_texture_with_data(
queue,
&TextureDescriptor {
label: Some("upload_texture node texture"),
label: Some("upload_to_texture staging texture"),
size: Extent3d {
width: image.width,
height: image.height,