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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -498,9 +498,14 @@ mod test {
println!("-------------------------------------------------");
println!("Failed test due to receiving a DisplayDialogError while loading a Graphite demo file.");
println!();
println!("NOTE:");
println!("Document upgrading isn't performed in tests like when opening in the actual editor.");
println!("You may need to open and re-save a document in the editor to apply its migrations.");
println!();
println!("DisplayDialogError details:");
println!();
println!("Description: {value}");
println!("Description:");
println!("{value}");
println!("-------------------------------------------------");
println!();
@ -538,7 +543,9 @@ mod test {
});
// Check if the graph renders
editor.eval_graph().await;
if let Err(e) = editor.eval_graph().await {
print_problem_to_terminal_on_failure(&format!("Failed to evaluate the graph for document '{document_name}':\n{e}"));
}
for response in responses {
// Check for the existence of the file format incompatibility warning dialog after opening the test file

View file

@ -289,17 +289,17 @@ impl<'a> ModifyInputsContext<'a> {
log::error!("Node type {} does not exist in ModifyInputsContext::existing_node_id", reference);
return None;
};
// If inserting a path node, insert a flatten vector elements if the type is a graphic group.
// If inserting a path node, insert a Flatten Path if the type is a graphic group.
// TODO: Allow the path node to operate on Graphic Group data by utilizing the reference for each vector data in a group.
if node_definition.identifier == "Path" {
let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]).0.nested_type().clone();
if layer_input_type == concrete!(GraphicGroupTable) {
let Some(flatten_vector_elements_definition) = resolve_document_node_type("Flatten Vector Elements") else {
log::error!("Flatten Vector Elements does not exist in ModifyInputsContext::existing_node_id");
let Some(flatten_path_definition) = resolve_document_node_type("Flatten Path") else {
log::error!("Flatten Path does not exist in ModifyInputsContext::existing_node_id");
return None;
};
let node_id = NodeId::new();
self.network_interface.insert_node(node_id, flatten_vector_elements_definition.default_node_template(), &[]);
self.network_interface.insert_node(node_id, flatten_path_definition.default_node_template(), &[]);
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[]);
}
}

View file

@ -3805,6 +3805,15 @@ impl NodeNetworkInterface {
node_metadata.persistent_metadata.network_metadata = metadata.network_metadata;
}
/// Keep metadata in sync with the new implementation if this is used by anything other than the upgrade scripts
pub fn replace_reference_name(&mut self, node_id: &NodeId, network_path: &[NodeId], reference_name: String) {
let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else {
log::error!("Could not get node metadata in replace_reference_name");
return;
};
node_metadata.persistent_metadata.reference = Some(reference_name);
}
/// Keep metadata in sync with the new implementation if this is used by anything other than the upgrade scripts
pub fn set_manual_compostion(&mut self, node_id: &NodeId, network_path: &[NodeId], manual_composition: Option<Type>) {
let Some(network) = self.network_mut(network_path) else {

View file

@ -467,7 +467,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
}
};
const REPLACEMENTS: [(&str, &str); 36] = [
const REPLACEMENTS: [(&str, &str); 37] = [
("graphene_core::AddArtboardNode", "graphene_core::graphic_element::AppendArtboardNode"),
("graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"),
("graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"),
@ -507,6 +507,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
("graphene_std::raster::SampleNode", "graphene_std::raster::SampleImageNode"),
("graphene_core::transform::CullNode", "graphene_core::ops::IdentityNode"),
("graphene_std::raster::MaskImageNode", "graphene_std::raster::MaskNode"),
("graphene_core::vector::FlattenVectorElementsNode", "graphene_core::vector::FlattenPathNode"),
];
let mut network = document.network_interface.document_network().clone();
network.generate_node_paths(&[]);
@ -946,6 +947,22 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[3].clone(), network_path);
}
if reference == "Flatten Vector Elements" {
let node_definition = resolve_document_node_type("Flatten Path").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.replace_reference_name(node_id, network_path, "Flatten Path".to_string());
}
}
// TODO: Eventually remove this document upgrade code

View file

@ -91,9 +91,9 @@ pub fn merge_layers(document: &DocumentMessageHandler, first_layer: LayerNodeIde
delete_children: false,
});
// Add a flatten vector elements node after the merge
// Add a Flatten Path node after the merge
let flatten_node_id = NodeId::new();
let flatten_node = document_node_definitions::resolve_document_node_type("Flatten Vector Elements")
let flatten_node = document_node_definitions::resolve_document_node_type("Flatten Path")
.expect("Failed to create flatten node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {

View file

@ -563,7 +563,10 @@ mod test_artboard {
pub use crate::test_utils::test_prelude::*;
async fn get_artboards(editor: &mut EditorTestUtils) -> Vec<graphene_core::Artboard> {
let instrumented = editor.eval_graph().await;
let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {}", e),
};
instrumented.grab_all_input::<graphene_core::append_artboard::ArtboardInput>(&editor.runtime).collect()
}

View file

@ -330,7 +330,10 @@ mod test_ellipse {
}
async fn get_ellipse(editor: &mut EditorTestUtils) -> Vec<ResolvedEllipse> {
let instrumented = editor.eval_graph().await;
let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {e}"),
};
let document = editor.active_document();
let layers = document.metadata().all_layers();

View file

@ -161,7 +161,10 @@ mod test_fill {
use graphene_std::vector::style::Fill;
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<Fill> {
let instrumented = editor.eval_graph().await;
let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {e}"),
};
instrumented.grab_all_input::<fill::FillInput<Fill>>(&editor.runtime).collect()
}

View file

@ -545,7 +545,10 @@ mod test_gradient {
use super::gradient_space_transform;
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<(Fill, DAffine2)> {
let instrumented = editor.eval_graph().await;
let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {}", e),
};
let document = editor.active_document();
let layers = document.metadata().all_layers();

View file

@ -670,7 +670,9 @@ mod test_spline_tool {
editor.handle_message(SplineToolMessage::Confirm).await;
// Evaluate the graph to ensure everything is processed
editor.eval_graph().await;
if let Err(e) = editor.eval_graph().await {
panic!("Graph evaluation failed: {}", e);
}
// Get the layer and vector data
let document = editor.active_document();
@ -708,7 +710,9 @@ mod test_spline_tool {
editor.handle_message(SplineToolMessage::Confirm).await;
// Evaluating the graph to ensure everything is processed
editor.eval_graph().await;
if let Err(e) = editor.eval_graph().await {
panic!("Graph evaluation failed: {}", e);
}
// Get the layer and vector data
let document = editor.active_document();
@ -744,7 +748,9 @@ mod test_spline_tool {
editor.handle_message(SplineToolMessage::Confirm).await;
// Evaluating the graph to ensure everything is processed
editor.eval_graph().await;
if let Err(e) = editor.eval_graph().await {
panic!("Graph evaluation failed: {}", e);
}
// Get the layer and vector data
let document = editor.active_document();
@ -781,7 +787,9 @@ mod test_spline_tool {
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150.0, 100.0), ModifierKeys::empty()).await;
editor.handle_message(SplineToolMessage::Confirm).await;
editor.eval_graph().await;
if let Err(e) = editor.eval_graph().await {
panic!("Graph evaluation failed: {}", e);
}
// Get the layer and vector data
let document = editor.active_document();

View file

@ -36,30 +36,35 @@ impl EditorTestUtils {
Self { editor, runtime }
}
pub fn eval_graph<'a>(&'a mut self) -> impl std::future::Future<Output = Instrumented> + 'a {
pub fn eval_graph<'a>(&'a mut self) -> impl std::future::Future<Output = Result<Instrumented, String>> + 'a {
// An inner function is required since async functions in traits are a bit weird
async fn run<'a>(editor: &'a mut Editor, runtime: &'a mut NodeRuntime) -> Instrumented {
async fn run<'a>(editor: &'a mut Editor, runtime: &'a mut NodeRuntime) -> Result<Instrumented, String> {
let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler;
let exector = &mut portfolio.executor;
let document = portfolio.documents.get_mut(&portfolio.active_document_id.unwrap()).unwrap();
let instrumented = exector.update_node_graph_instrumented(document).expect("update_node_graph_instrumented failed");
let instrumented = match exector.update_node_graph_instrumented(document) {
Ok(instrumented) => instrumented,
Err(e) => return Err(format!("update_node_graph_instrumented failed\n\n{e}")),
};
let viewport_resolution = glam::UVec2::ONE;
exector
.submit_current_node_graph_evaluation(document, viewport_resolution, Default::default())
.expect("submit_current_node_graph_evaluation failed");
if let Err(e) = exector.submit_current_node_graph_evaluation(document, viewport_resolution, Default::default()) {
return Err(format!("submit_current_node_graph_evaluation failed\n\n{e}"));
}
runtime.run().await;
let mut messages = VecDeque::new();
editor.poll_node_graph_evaluation(&mut messages).expect("Graph should render");
if let Err(e) = editor.poll_node_graph_evaluation(&mut messages) {
return Err(format!("Graph should render\n\n{e}"));
}
let frontend_messages = messages.into_iter().flat_map(|message| editor.handle_message(message));
for message in frontend_messages {
message.check_node_graph_error();
}
instrumented
Ok(instrumented)
}
run(&mut self.editor, &mut self.runtime)
@ -69,7 +74,9 @@ impl EditorTestUtils {
self.editor.handle_message(message);
// Required to process any buffered messages
self.eval_graph().await;
if let Err(e) = self.eval_graph().await {
panic!("Failed to evaluate graph: {e}");
}
}
pub async fn new_document(&mut self) {

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>]),