Instance tables refactor part 8: Make repeater nodes use pivot not bbox and output instance type not group; rename 'Flatten Vector Elements' to 'Flatten Path' and add 'Flatten Vector' (#2697)

Make repeater nodes use pivot not bbox and output instance type not group; rename 'Flatten Vector Elements' to 'Flatten Path' and add 'Flatten Vector'
This commit is contained in:
Keavon Chambers 2025-06-07 02:24:04 -07:00
parent 523cc27523
commit cea1a1c6a8
26 changed files with 252 additions and 125 deletions

View file

@ -423,6 +423,49 @@ async fn flatten_group(_: impl Ctx, group: GraphicGroupTable, fully_flatten: boo
output
}
#[node_macro::node(category("General"))]
async fn flatten_vector(_: impl Ctx, group: GraphicGroupTable) -> VectorDataTable {
// TODO: Avoid mutable reference, instead return a new GraphicGroupTable?
fn flatten_group(output_group_table: &mut VectorDataTable, current_group_table: GraphicGroupTable) {
for current_instance in current_group_table.instance_ref_iter() {
let current_element = current_instance.instance.clone();
let reference = *current_instance.source_node_id;
match current_element {
// If we're allowed to recurse, flatten any GraphicGroups we encounter
GraphicElement::GraphicGroup(mut current_element) => {
// Apply the parent group's transform to all child elements
for graphic_element in current_element.instance_mut_iter() {
*graphic_element.transform = *current_instance.transform * *graphic_element.transform;
}
flatten_group(output_group_table, current_element);
}
// Handle any leaf elements we encounter, which can be either non-GraphicGroup elements or GraphicGroups that we don't want to flatten
GraphicElement::VectorData(vector_instance) => {
for current_element in vector_instance.instance_ref_iter() {
output_group_table.push(Instance {
instance: current_element.instance.clone(),
transform: *current_instance.transform * *current_element.transform,
alpha_blending: AlphaBlending {
opacity: current_instance.alpha_blending.opacity * current_element.alpha_blending.opacity,
blend_mode: current_element.alpha_blending.blend_mode,
},
source_node_id: reference,
});
}
}
_ => {}
}
}
}
let mut output = VectorDataTable::default();
flatten_group(&mut output, group);
output
}
#[node_macro::node(category(""))]
async fn to_artboard<Data: Into<GraphicGroupTable> + 'n>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,

View file

@ -921,6 +921,10 @@ impl GraphicElementRendered for RasterDataTable<CPU> {
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
click_targets.push(ClickTarget::new(subpath, 0.));
}
fn to_graphic_element(&self) -> GraphicElement {
GraphicElement::RasterDataCPU(self.clone())
}
}
impl GraphicElementRendered for RasterDataTable<GPU> {

View file

@ -33,6 +33,13 @@ impl<T> Instances<T> {
self.source_node_id.push(instance.source_node_id);
}
pub fn extend(&mut self, instances: Instances<T>) {
self.instance.extend(instances.instance);
self.transform.extend(instances.transform);
self.alpha_blending.extend(instances.alpha_blending);
self.source_node_id.extend(instances.source_node_id);
}
pub fn instance_iter(self) -> impl DoubleEndedIterator<Item = Instance<T>> {
self.instance
.into_iter()

View file

@ -44,12 +44,26 @@ async fn switch<T, C: Send + 'n + Clone>(
condition: bool,
#[expose]
#[implementations(
Context -> String, Context -> bool, Context -> f64, Context -> u32, Context -> u64, Context -> DVec2, Context -> VectorDataTable, Context -> DAffine2,
Context -> String,
Context -> bool,
Context -> f64,
Context -> u32,
Context -> u64,
Context -> DVec2,
Context -> VectorDataTable,
Context -> DAffine2,
)]
if_true: impl Node<C, Output = T>,
#[expose]
#[implementations(
Context -> String, Context -> bool, Context -> f64, Context -> u32, Context -> u64, Context -> DVec2, Context -> VectorDataTable, Context -> DAffine2,
Context -> String,
Context -> bool,
Context -> f64,
Context -> u32,
Context -> u64,
Context -> DVec2,
Context -> VectorDataTable,
Context -> DAffine2,
)]
if_false: impl Node<C, Output = T>,
) -> T {

View file

@ -1,18 +1,22 @@
use crate::instances::{InstanceRef, Instances};
use crate::raster_types::{CPU, RasterDataTable};
use crate::transform::TransformMut;
use crate::vector::VectorDataTable;
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractIndex, ExtractVarArgs, GraphicElement, GraphicGroupTable, OwnedContextImpl};
use glam::DVec2;
#[node_macro::node(name("Instance on Points"), category("Instancing"), path(graphene_core::vector))]
async fn instance_on_points<T: Into<GraphicElement> + Default + Clone + 'static>(
async fn instance_on_points<T: Into<GraphicElement> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx,
points: VectorDataTable,
#[implementations(Context -> GraphicGroupTable, Context -> VectorDataTable, Context -> RasterDataTable<CPU>)] instance: impl Node<'n, Context<'static>, Output = Instances<T>>,
#[implementations(
Context -> GraphicGroupTable,
Context -> VectorDataTable,
Context -> RasterDataTable<CPU>
)]
instance: impl Node<'n, Context<'static>, Output = Instances<T>>,
reverse: bool,
) -> GraphicGroupTable {
let mut result_table = GraphicGroupTable::default();
) -> Instances<T> {
let mut result_table = Instances::<T>::default();
for InstanceRef { instance: points, transform, .. } in points.instance_ref_iter() {
let mut iteration = async |index, point| {
@ -22,8 +26,8 @@ async fn instance_on_points<T: Into<GraphicElement> + Default + Clone + 'static>
let generated_instance = instance.eval(new_ctx.into_context()).await;
for mut instanced in generated_instance.instance_iter() {
instanced.transform.translate(transformed_point);
result_table.push(instanced.to_graphic_element());
instanced.transform.translation = transformed_point;
result_table.push(instanced);
}
};
@ -43,15 +47,20 @@ async fn instance_on_points<T: Into<GraphicElement> + Default + Clone + 'static>
}
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn instance_repeat<T: Into<GraphicElement> + Default + Clone + 'static>(
async fn instance_repeat<T: Into<GraphicElement> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
#[implementations(Context -> GraphicGroupTable, Context -> VectorDataTable, Context -> RasterDataTable<CPU>)] instance: impl Node<'n, Context<'static>, Output = Instances<T>>,
#[implementations(
Context -> GraphicGroupTable,
Context -> VectorDataTable,
Context -> RasterDataTable<CPU>
)]
instance: impl Node<'n, Context<'static>, Output = Instances<T>>,
#[default(1)] count: u64,
reverse: bool,
) -> GraphicGroupTable {
) -> Instances<T> {
let count = count.max(1) as usize;
let mut result_table = GraphicGroupTable::default();
let mut result_table = Instances::<T>::default();
for index in 0..count {
let index = if reverse { count - index - 1 } else { index };
@ -60,7 +69,7 @@ async fn instance_repeat<T: Into<GraphicElement> + Default + Clone + 'static>(
let generated_instance = instance.eval(new_ctx.into_context()).await;
for instanced in generated_instance.instance_iter() {
result_table.push(instanced.to_graphic_element());
result_table.push(instanced);
}
}
@ -124,16 +133,7 @@ mod test {
let repeated = super::instance_on_points(owned, points, &rect, false).await;
assert_eq!(repeated.len(), positions.len());
for (position, instanced) in positions.into_iter().zip(repeated.instance_ref_iter()) {
let bounds = instanced
.instance
.as_vector_data()
.unwrap()
.instance_ref_iter()
.next()
.unwrap()
.instance
.bounding_box_with_transform(*instanced.transform)
.unwrap();
let bounds = instanced.instance.bounding_box_with_transform(*instanced.transform).unwrap();
assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10));
assert_eq!((bounds[1] - bounds[0]).x, position.y);
}

View file

@ -19,6 +19,7 @@ use glam::{DAffine2, DVec2};
use kurbo::{Affine, BezPath, Shape};
use rand::{Rng, SeedableRng};
use std::collections::hash_map::DefaultHasher;
use std::f64::consts::TAU;
/// Implemented for types that can be converted to an iterator of vector data.
/// Used for the fill and stroke node so they can be used on VectorData or GraphicGroup
@ -201,7 +202,7 @@ where
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn repeat<I: 'n + Send>(
async fn repeat<I: 'n + Send + Clone>(
_: impl Ctx,
// TODO: Implement other GraphicElementRendered types.
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] instance: Instances<I>,
@ -210,78 +211,72 @@ async fn repeat<I: 'n + Send>(
direction: PixelSize,
angle: Angle,
#[default(4)] instances: IntegerCount,
) -> GraphicGroupTable
) -> Instances<I>
where
Instances<I>: GraphicElementRendered,
{
let angle = angle.to_radians();
let instances = instances.max(1);
let total = (instances - 1) as f64;
let count = instances.max(1);
let total = (count - 1) as f64;
let mut result_table = GraphicGroupTable::default();
let mut result_table = Instances::<I>::default();
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY, false) else {
return result_table;
};
let center = (bounding_box[0] + bounding_box[1]) / 2.;
for index in 0..instances {
for index in 0..count {
let angle = index as f64 * angle / total;
let translation = index as f64 * direction / total;
let transform = DAffine2::from_translation(center) * DAffine2::from_angle(angle) * DAffine2::from_translation(translation) * DAffine2::from_translation(-center);
let transform = DAffine2::from_angle(angle) * DAffine2::from_translation(translation);
result_table.push(Instance {
instance: instance.to_graphic_element().clone(),
transform,
alpha_blending: Default::default(),
source_node_id: None,
});
for instance in instance.instance_ref_iter() {
let mut instance = instance.to_instance_cloned();
let local_translation = DAffine2::from_translation(instance.transform.translation);
let local_matrix = DAffine2::from_mat2(instance.transform.matrix2);
instance.transform = local_translation * transform * local_matrix;
result_table.push(instance);
}
}
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn circular_repeat<I: 'n + Send>(
async fn circular_repeat<I: 'n + Send + Clone>(
_: impl Ctx,
// TODO: Implement other GraphicElementRendered types.
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] instance: Instances<I>,
angle_offset: Angle,
#[default(5)] radius: f64,
#[default(5)] instances: IntegerCount,
) -> GraphicGroupTable
) -> Instances<I>
where
Instances<I>: GraphicElementRendered,
{
let instances = instances.max(1);
let count = instances.max(1);
let mut result_table = GraphicGroupTable::default();
let mut result_table = Instances::<I>::default();
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY, false) else {
return result_table;
};
for index in 0..count {
let angle = DAffine2::from_angle((TAU / count as f64) * index as f64 + angle_offset.to_radians());
let translation = DAffine2::from_translation(radius * DVec2::Y);
let transform = angle * translation;
let center = (bounding_box[0] + bounding_box[1]) / 2.;
let base_transform = DVec2::new(0., radius) - center;
for instance in instance.instance_ref_iter() {
let mut instance = instance.to_instance_cloned();
for index in 0..instances {
let rotation = DAffine2::from_angle((std::f64::consts::TAU / instances as f64) * index as f64 + angle_offset.to_radians());
let transform = DAffine2::from_translation(center) * rotation * DAffine2::from_translation(base_transform);
let local_translation = DAffine2::from_translation(instance.transform.translation);
let local_matrix = DAffine2::from_mat2(instance.transform.matrix2);
instance.transform = local_translation * transform * local_matrix;
result_table.push(Instance {
instance: instance.to_graphic_element().clone(),
transform,
alpha_blending: Default::default(),
source_node_id: None,
});
result_table.push(instance);
}
}
result_table
}
#[node_macro::node(name("Copy to Points"), category("Vector"), path(graphene_core::vector))]
async fn copy_to_points<I: 'n + Send>(
async fn copy_to_points<I: 'n + Send + Clone>(
_: impl Ctx,
points: VectorDataTable,
#[expose]
@ -308,17 +303,14 @@ async fn copy_to_points<I: 'n + Send>(
random_rotation: Angle,
/// Seed to determine unique variations on all the randomized instance angles.
random_rotation_seed: SeedValue,
) -> GraphicGroupTable
) -> Instances<I>
where
Instances<I>: GraphicElementRendered,
{
let mut result_table = GraphicGroupTable::default();
let mut result_table = Instances::<I>::default();
let random_scale_difference = random_scale_max - random_scale_min;
let instance_bounding_box = instance.bounding_box(DAffine2::IDENTITY, false).unwrap_or_default();
let instance_center = -0.5 * (instance_bounding_box[0] + instance_bounding_box[1]);
for point_instance in points.instance_iter() {
let mut scale_rng = rand::rngs::StdRng::seed_from_u64(random_scale_seed.into());
let mut rotation_rng = rand::rngs::StdRng::seed_from_u64(random_rotation_seed.into());
@ -328,13 +320,11 @@ where
let points_transform = point_instance.transform;
for &point in point_instance.instance.point_domain.positions() {
let center_transform = DAffine2::from_translation(instance_center);
let translation = points_transform.transform_point2(point);
let rotation = if do_rotation {
let degrees = (rotation_rng.random::<f64>() - 0.5) * random_rotation;
degrees / 360. * std::f64::consts::TAU
degrees / 360. * TAU
} else {
0.
};
@ -353,22 +343,23 @@ where
random_scale_min
};
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation) * center_transform;
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation);
result_table.push(Instance {
instance: instance.to_graphic_element().clone(),
transform,
alpha_blending: Default::default(),
source_node_id: None,
});
for mut instance in instance.instance_ref_iter().map(|instance| instance.to_instance_cloned()) {
let local_matrix = DAffine2::from_mat2(instance.transform.matrix2);
instance.transform = transform * local_matrix;
result_table.push(instance);
}
}
}
result_table
}
// TODO: Make this node return Instances<I> instead of GraphicGroupTable, while preserving the current transform behavior as the `reference_point` and `offset` parameters are varied
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn mirror<I: 'n + Send>(
async fn mirror<I: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] instance: Instances<I>,
#[default(ReferencePoint::Center)] reference_point: ReferencePoint,
@ -388,10 +379,13 @@ where
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY, false) else {
return result_table;
};
let mirror_reference_point = reference_point
.point_in_bounding_box((bounding_box[0], bounding_box[1]).into())
.unwrap_or_else(|| (bounding_box[0] + bounding_box[1]) / 2.)
+ normal * offset;
// TODO: If the reference point is not None, use the current behavior but make it work correctly with local pivot origins of each Instances<I> row
let reference_point_location = reference_point.point_in_bounding_box((bounding_box[0], bounding_box[1]).into()).unwrap_or_else(|| {
// TODO: In this None case, use the input's local pivot origin point instead of a point relative to its bounding box
(bounding_box[0] + bounding_box[1]) / 2.
});
let mirror_reference_point = reference_point_location + normal * offset;
// Create the reflection matrix
let reflection = DAffine2::from_mat2_translation(
@ -1164,9 +1158,12 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn flatten_vector_elements(_: impl Ctx, graphic_group_input: GraphicGroupTable) -> VectorDataTable {
async fn flatten_path<I: 'n + Send>(_: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable)] graphic_group_input: Instances<I>) -> VectorDataTable
where
Instances<I>: GraphicElementRendered,
{
// A node based solution to support passing through vector data could be a network node with a cache node connected to
// a flatten vector elements connected to an if else node, another connection from the cache directly
// a Flatten Path connected to an if else node, another connection from the cache directly
// To the if else node, and another connection from the cache to a matches type node connected to the if else node.
fn flatten_group(graphic_group_table: &GraphicGroupTable, output: &mut InstanceMut<VectorData>) {
for (group_index, current_element) in graphic_group_table.instance_ref_iter().enumerate() {
@ -1208,7 +1205,8 @@ async fn flatten_vector_elements(_: impl Ctx, graphic_group_input: GraphicGroupT
};
// Flatten the graphic group input into the output VectorData instance
flatten_group(&graphic_group_input, &mut output);
let base_graphic_group = GraphicGroupTable::new(graphic_group_input.to_graphic_element());
flatten_group(&base_graphic_group, &mut output);
// Return the single-row VectorDataTable containing the flattened VectorData subpaths
output_table
@ -1485,7 +1483,7 @@ async fn jitter_points(_: impl Ctx, vector_data: VectorDataTable, #[default(5.)]
let deltas = (0..vector_data_instance.instance.point_domain.positions().len())
.map(|_| {
let angle = rng.random::<f64>() * std::f64::consts::TAU;
let angle = rng.random::<f64>() * TAU;
inverse_transform.transform_vector2(DVec2::from_angle(angle) * rng.random::<f64>() * amount)
})
@ -1859,7 +1857,7 @@ mod test {
let direction = DVec2::X * 1.5;
let instances = 3;
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
let vector_data = super::flatten_path(Footprint::default(), repeated).await;
let vector_data = vector_data.instance_ref_iter().next().unwrap().instance;
assert_eq!(vector_data.region_bezier_paths().count(), 3);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
@ -1871,7 +1869,7 @@ mod test {
let direction = DVec2::new(12., 10.);
let instances = 8;
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
let vector_data = super::flatten_path(Footprint::default(), repeated).await;
let vector_data = vector_data.instance_ref_iter().next().unwrap().instance;
assert_eq!(vector_data.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
@ -1881,7 +1879,7 @@ mod test {
#[tokio::test]
async fn circular_repeat() {
let repeated = super::circular_repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)), 45., 4., 8).await;
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
let vector_data = super::flatten_path(Footprint::default(), repeated).await;
let vector_data = vector_data.instance_ref_iter().next().unwrap().instance;
assert_eq!(vector_data.region_bezier_paths().count(), 8);
@ -1927,8 +1925,8 @@ mod test {
let expected_points = VectorData::from_subpath(points.clone()).point_domain.positions().to_vec();
let copy_to_points = super::copy_to_points(Footprint::default(), vector_node(points), vector_node(instance), 1., 1., 0., 0, 0., 0).await;
let flatten_vector_elements = super::flatten_vector_elements(Footprint::default(), copy_to_points).await;
let flattened_copy_to_points = flatten_vector_elements.instance_ref_iter().next().unwrap().instance;
let flatten_path = super::flatten_path(Footprint::default(), copy_to_points).await;
let flattened_copy_to_points = flatten_path.instance_ref_iter().next().unwrap().instance;
assert_eq!(flattened_copy_to_points.region_bezier_paths().count(), expected_points.len());

View file

@ -69,6 +69,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<graphene_core::uuid::NodeId>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::Color]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Box<graphene_core::vector::VectorModification>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Image<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => VectorDataTable]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => RasterDataTable<CPU>]),