Implement dynamic table attributes to generalize the graphic-specific Table type (#4050)

* Feature-gate serde derives behind cfg_attr in all runtime node graph type crates

* Refactor Table to move its hard-coded fields into an attributes field

* Encapsulate TableRow/TableRowRef/TableRowMut attribute fields behind accessor methods

* Remove TaggedValue::GraphicUnused

* Refactor Table<T> to use dynamic attributes instead fixed names

* Fix code review soundness concerns

* Add todo work

* Replace row-oriented Table<T> API with column-oriented access

* Fix attribute propagation bugs

---------
This commit is contained in:
Keavon Chambers
2026-04-28 02:10:24 -07:00
parent 324b9e664c
commit 76938eb69a
75 changed files with 2330 additions and 1349 deletions

View File

@@ -33,9 +33,9 @@ impl From<std::io::Error> for Error {
pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Table<Raster<CPU>>) -> Table<Raster<CPU>> {
image_frame
.into_iter()
.filter_map(|mut row| {
let image_frame_transform = row.transform;
let image = row.element;
.filter_map(|row| {
let image_frame_transform: DAffine2 = row.attribute_cloned_or_default("transform");
let (image, mut attributes) = row.into_parts();
// Resize the image using the image crate
let data = bytemuck::cast_vec(image.data.clone());
@@ -86,10 +86,9 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Tabl
// we need to adjust the offset if we truncate the offset calculation
let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
attributes.insert("transform", new_transform);
row.transform = new_transform;
row.element = Raster::new_cpu(image);
Some(row)
Some(TableRow::from_parts(Raster::new_cpu(image), attributes))
})
.collect()
}
@@ -114,23 +113,20 @@ pub fn combine_channels(
.zip(alpha)
.filter_map(|(((red, green), blue), alpha)| {
// Turn any default zero-sized image rows into None
let red = red.filter(|i| i.element.width > 0 && i.element.height > 0);
let green = green.filter(|i| i.element.width > 0 && i.element.height > 0);
let blue = blue.filter(|i| i.element.width > 0 && i.element.height > 0);
let alpha = alpha.filter(|i| i.element.width > 0 && i.element.height > 0);
let red = red.filter(|i| i.element().width > 0 && i.element().height > 0);
let green = green.filter(|i| i.element().width > 0 && i.element().height > 0);
let blue = blue.filter(|i| i.element().width > 0 && i.element().height > 0);
let alpha = alpha.filter(|i| i.element().width > 0 && i.element().height > 0);
// Get this row's transform and alpha blending mode from the first non-empty channel
let (transform, alpha_blending, source_node_id) = [&red, &green, &blue, &alpha]
.iter()
.find_map(|i| i.as_ref())
.map(|i| (i.transform, i.alpha_blending, i.source_node_id))?;
let attributes = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| i.attributes().clone())?;
// Get the common width and height of the channels, which must have equal dimensions
let channel_dimensions = [
red.as_ref().map(|r| (r.element.width, r.element.height)),
green.as_ref().map(|g| (g.element.width, g.element.height)),
blue.as_ref().map(|b| (b.element.width, b.element.height)),
alpha.as_ref().map(|a| (a.element.width, a.element.height)),
red.as_ref().map(|r| (r.element().width, r.element().height)),
green.as_ref().map(|g| (g.element().width, g.element().height)),
blue.as_ref().map(|b| (b.element().width, b.element().height)),
alpha.as_ref().map(|a| (a.element().width, a.element().height)),
];
if channel_dimensions.iter().all(Option::is_none)
|| channel_dimensions
@@ -150,22 +146,22 @@ pub fn combine_channels(
for x in 0..image.width() {
let image_pixel = image.get_pixel_mut(x, y).unwrap();
if let Some(r) = red.as_ref().and_then(|r| r.element.get_pixel(x, y)) {
if let Some(r) = red.as_ref().and_then(|r| r.element().get_pixel(x, y)) {
image_pixel.set_red(r.l().cast_linear_channel());
} else {
image_pixel.set_red(Channel::from_linear(0.));
}
if let Some(g) = green.as_ref().and_then(|g| g.element.get_pixel(x, y)) {
if let Some(g) = green.as_ref().and_then(|g| g.element().get_pixel(x, y)) {
image_pixel.set_green(g.l().cast_linear_channel());
} else {
image_pixel.set_green(Channel::from_linear(0.));
}
if let Some(b) = blue.as_ref().and_then(|b| b.element.get_pixel(x, y)) {
if let Some(b) = blue.as_ref().and_then(|b| b.element().get_pixel(x, y)) {
image_pixel.set_blue(b.l().cast_linear_channel());
} else {
image_pixel.set_blue(Channel::from_linear(0.));
}
if let Some(a) = alpha.as_ref().and_then(|a| a.element.get_pixel(x, y)) {
if let Some(a) = alpha.as_ref().and_then(|a| a.element().get_pixel(x, y)) {
image_pixel.set_alpha(a.l().cast_linear_channel());
} else {
image_pixel.set_alpha(Channel::from_linear(1.));
@@ -173,12 +169,7 @@ pub fn combine_channels(
}
}
Some(TableRow {
element: Raster::new_cpu(image),
transform,
alpha_blending,
source_node_id,
})
Some(TableRow::from_parts(Raster::new_cpu(image), attributes))
})
.collect()
}
@@ -197,32 +188,34 @@ pub fn mask(
// No stencil provided so we return the original image
return image;
};
let stencil_size = DVec2::new(stencil.element.width as f64, stencil.element.height as f64);
let stencil_size = DVec2::new(stencil.element().width as f64, stencil.element().height as f64);
image
.into_iter()
.filter_map(|mut row| {
let image_size = DVec2::new(row.element.width as f64, row.element.height as f64);
let mask_size = stencil.transform.scale_magnitudes();
let image_size = DVec2::new(row.element().width as f64, row.element().height as f64);
let stencil_transform: DAffine2 = stencil.attribute_cloned_or_default("transform");
let mask_size = stencil_transform.scale_magnitudes();
if mask_size == DVec2::ZERO {
return None;
}
// Transforms a point from the background image to the foreground image
let bg_to_fg = row.transform * DAffine2::from_scale(1. / image_size);
let stencil_transform_inverse = stencil.transform.inverse();
let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform");
let bg_to_fg = transform_attribute * DAffine2::from_scale(1. / image_size);
let stencil_transform_inverse = stencil_transform.inverse();
for y in 0..row.element.height {
for x in 0..row.element.width {
for y in 0..row.element().height {
for x in 0..row.element().width {
let image_point = DVec2::new(x as f64, y as f64);
let mask_point = bg_to_fg.transform_point2(image_point);
let local_mask_point = stencil_transform_inverse.transform_point2(mask_point);
let mask_point = stencil.transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE));
let mask_point = (DAffine2::from_scale(stencil_size) * stencil.transform.inverse()).transform_point2(mask_point);
let mask_point = stencil_transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE));
let mask_point = (DAffine2::from_scale(stencil_size) * stencil_transform.inverse()).transform_point2(mask_point);
let image_pixel = row.element.data_mut().get_pixel_mut(x, y).unwrap();
let mask_pixel = stencil.element.sample(mask_point);
let image_pixel = row.element_mut().data_mut().get_pixel_mut(x, y).unwrap();
let mask_pixel = stencil.element().sample(mask_point);
*image_pixel = image_pixel.multiplied_alpha(mask_pixel.l().cast_linear_channel());
}
}
@@ -237,20 +230,21 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: Table<Raster<CPU>>, bounds: DA
image
.into_iter()
.map(|mut row| {
let image_aabb = Bbox::unit().affine_transform(row.transform).to_axis_aligned_bbox();
let row_transform: DAffine2 = row.attribute_cloned_or_default("transform");
let image_aabb = Bbox::unit().affine_transform(row_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) {
return row;
}
let image_data = &row.element.data;
let (image_width, image_height) = (row.element.width, row.element.height);
let image_data = &row.element().data;
let (image_width, image_height) = (row.element().width, row.element().height);
if image_width == 0 || image_height == 0 {
return empty_image((), bounds, Table::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap();
}
let orig_image_scale = DVec2::new(image_width as f64, image_height as f64);
let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * row.transform.inverse();
let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * row_transform.inverse();
let bounds_in_image_space = Bbox::unit().affine_transform(layer_to_image_space * bounds).to_axis_aligned_bbox();
let new_start = bounds_in_image_space.start.floor().min(DVec2::ZERO);
@@ -270,10 +264,10 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: Table<Raster<CPU>>, bounds: DA
// Compute new transform.
// 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 = row.transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale);
let new_texture_to_layer_space = row_transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale);
row.element = Raster::new_cpu(new_image);
row.transform = new_texture_to_layer_space;
*row.element_mut() = Raster::new_cpu(new_image);
row.set_attribute("transform", new_texture_to_layer_space);
row
})
.collect()
@@ -284,13 +278,12 @@ pub fn empty_image(_: impl Ctx, transform: DAffine2, color: Table<Color>) -> Tab
let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32;
let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32;
let color: Option<Color> = color.into();
let image = Image::new(width, height, color.unwrap_or(Color::WHITE));
let color = color.element(0).copied().unwrap_or(Color::WHITE);
let image = Image::new(width, height, color);
let mut result_table = Table::new_from_element(Raster::new_cpu(image));
let row = result_table.get_mut(0).unwrap();
*row.transform = transform;
*row.alpha_blending = AlphaBlending::default();
result_table.set_attribute("transform", 0, transform);
result_table.set_attribute("alpha_blending", 0, AlphaBlending::default());
// Callers of empty_image can safely unwrap on returned table
result_table
@@ -383,11 +376,12 @@ pub fn noise_pattern(
}
}
return Table::new_from_row(TableRow {
element: Raster::new_cpu(image),
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
..Default::default()
});
return Table::new_from_row(
TableRow::new_from_element(Raster::new_cpu(image))
.with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size))
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
);
}
};
noise.set_noise_type(Some(noise_type));
@@ -445,11 +439,12 @@ pub fn noise_pattern(
}
}
Table::new_from_row(TableRow {
element: Raster::new_cpu(image),
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
..Default::default()
})
Table::new_from_row(
TableRow::new_from_element(Raster::new_cpu(image))
.with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size))
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
)
}
#[node_macro::node(category("Raster: Pattern"))]
@@ -487,16 +482,17 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> Table<Raster<CPU>> {
}
}
Table::new_from_row(TableRow {
element: Raster::new_cpu(Image {
Table::new_from_row(
TableRow::new_from_element(Raster::new_cpu(Image {
width,
height,
data,
..Default::default()
}),
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
..Default::default()
})
}))
.with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size))
.with_attribute("alpha_blending", AlphaBlending::default())
.with_attribute("source_node_id", None::<core_types::uuid::NodeId>),
)
}
#[inline(always)]