From fa3e6eeca909fbd08ed16fdd7eb15ed8f3e5d331 Mon Sep 17 00:00:00 2001 From: Oliver Davies Date: Fri, 18 Apr 2025 14:47:19 -0700 Subject: [PATCH] Mostly working instancing + box packing --- libraries/bezier-rs/src/bezier/transform.rs | 4 +- libraries/bezier-rs/src/subpath/transform.rs | 44 ++++++--- node-graph/gcore/src/logic.rs | 16 ++++ .../gcore/src/vector/algorithms/instance.rs | 32 ++++++- node-graph/gcore/src/vector/vector_nodes.rs | 92 +++++++++++++++++++ 5 files changed, 172 insertions(+), 16 deletions(-) diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index 6a48dc73bb..2a2b79e1e0 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -271,9 +271,9 @@ impl Bezier { _ => *self, }; - let should_flip_direction = (self.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE); + let should_flip_direction = (self.start - intersection).normalize_or_zero().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE); intermediate.apply_transformation(|point| { - let mut direction_unit_vector = (intersection - point).normalize(); + let mut direction_unit_vector = (intersection - point).normalize_or_zero(); if should_flip_direction { direction_unit_vector *= -1.; } diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index 88fc4612f3..d63c9aff9c 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -351,25 +351,38 @@ impl Subpath { rotated_subpath } - /// Reduces the segments of the subpath into simple subcurves, then scales each subcurve a set `distance` away. - /// The intersections of segments of the subpath are joined using the method specified by the `join` argument. - /// pub fn offset(&self, distance: f64, join: Join) -> Subpath { - // An offset at a distance 0 from the curve is simply the same curve - // An offset of a single point is not defined + // Early returns - same as before if distance == 0. || self.len() <= 1 || self.len_segments() < 1 { return self.clone(); } - let mut subpaths = self - .iter() - .filter(|bezier| !bezier.is_point()) - .map(|bezier| bezier.offset(distance)) - .filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty). - .collect::>>(); + // Collect valid offset subpaths + let mut subpaths = Vec::new(); + for bezier in self.iter() { + if bezier.is_point() { + continue; + } + + // Try to offset the bezier and handle potential failures + let offset_result = std::panic::catch_unwind(|| bezier.offset(distance)); + + // Only include valid results + if let Ok(subpath) = offset_result { + if subpath.len() >= 2 { + subpaths.push(subpath); + } + } + } + + // If we couldn't offset any segments, just return the original + if subpaths.is_empty() { + return self.clone(); + } let mut drop_common_point = vec![true; self.len()]; + // Rest of the function remains the same // Clip or join consecutive Subpaths for i in 0..subpaths.len() - 1 { let j = i + 1; @@ -424,7 +437,7 @@ impl Subpath { } // Clip any overlap in the last segment - if self.closed { + if self.closed && !subpaths.is_empty() { let out_tangent = self.get_segment(self.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.)); let in_tangent = self.get_segment(0).unwrap().tangent(TValue::Parametric(0.)); let angle = out_tangent.angle_to(in_tangent); @@ -462,6 +475,11 @@ impl Subpath { } } + // Ensure we have subpaths before merging + if subpaths.is_empty() { + return self.clone(); + } + // Merge the subpaths. Drop points which overlap with one another. let mut manipulator_groups = subpaths[0].manipulator_groups.clone(); for i in 1..subpaths.len() { @@ -475,7 +493,7 @@ impl Subpath { manipulator_groups.append(&mut subpaths[i].manipulator_groups.clone()); } } - if self.closed && drop_common_point[0] { + if self.closed && !subpaths.is_empty() && drop_common_point[0] { let last_group = manipulator_groups.pop().unwrap(); manipulator_groups[0].in_handle = last_group.in_handle; } diff --git a/node-graph/gcore/src/logic.rs b/node-graph/gcore/src/logic.rs index cefb4617e6..8de3400621 100644 --- a/node-graph/gcore/src/logic.rs +++ b/node-graph/gcore/src/logic.rs @@ -33,6 +33,22 @@ fn string_slice(_: impl Ctx, #[implementations(String)] string: String, start: f string.char_indices().skip(start).take(n).map(|(_, c)| c).collect() } +#[node_macro::node(category("Text"))] +fn split_string_by_index( + _: impl Ctx, + #[implementations(String)] + /// The comma-separated string to split. + input: String, + /// The zero-based index of the item to retrieve. + #[default(0.0)] + #[min(0.0)] + index: f64, +) -> String { + let parts: Vec<&str> = input.split(',').map(|s| s.trim()).collect(); + let floored_index = index.floor() as usize; + if floored_index < parts.len() { parts[floored_index].to_string() } else { String::new() } +} + #[node_macro::node(category("Text"))] fn string_length(_: impl Ctx, #[implementations(String)] string: String) -> usize { string.len() diff --git a/node-graph/gcore/src/vector/algorithms/instance.rs b/node-graph/gcore/src/vector/algorithms/instance.rs index 89a08d5613..dbec8f24ce 100644 --- a/node-graph/gcore/src/vector/algorithms/instance.rs +++ b/node-graph/gcore/src/vector/algorithms/instance.rs @@ -1,6 +1,6 @@ use crate::instances::Instance; use crate::vector::{VectorData, VectorDataTable}; -use crate::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractIndex, ExtractVarArgs, OwnedContextImpl}; +use crate::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractIndex, ExtractVarArgs, GraphicGroupTable, OwnedContextImpl}; use glam::{DAffine2, DVec2}; #[node_macro::node(name("Instance on Points"), category("Vector: Shape"), path(graphene_core::vector))] @@ -33,6 +33,36 @@ async fn instance_on_points( result } +#[node_macro::node(name("Group Instance on Points"), category("Vector: Shape"), path(graphene_core::vector))] +async fn group_instance_on_points( + ctx: impl ExtractAll + CloneVarArgs + Ctx, + points: VectorDataTable, + #[implementations(Context -> GraphicGroupTable)] instance_node: impl Node<'n, Context<'static>, Output = GraphicGroupTable>, +) -> GraphicGroupTable { + let mut result = GraphicGroupTable::empty(); + + for Instance { instance: points, transform, .. } in points.instances() { + for (index, &point) in points.point_domain.positions().iter().enumerate() { + let transformed_point = transform.transform_point2(point); + + let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_vararg(Box::new(transformed_point)); + let instanced = instance_node.eval(new_ctx.into_context()).await; + + for instanced_element in instanced.instances() { + let new_instance = result.push_instance(instanced_element); + *new_instance.transform *= DAffine2::from_translation(transformed_point); + } + } + } + + // TODO: Remove once we support empty tables, currently this is here to avoid crashing + if result.is_empty() { + return GraphicGroupTable::empty(); + } + + result +} + #[node_macro::node(category("Attributes"), path(graphene_core::vector))] async fn instance_position(ctx: impl Ctx + ExtractVarArgs) -> DVec2 { match ctx.vararg(0).map(|dynamic| dynamic.downcast_ref::()) { diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index d5661fb5d1..72be09a03c 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -806,6 +806,98 @@ where result_table } +#[node_macro::node(category("Vector"), path(graphene_core::vector))] +async fn box_pack( + _: impl Ctx, + #[implementations(GraphicGroupTable)] instances: GraphicGroupTable, + #[expose] + #[implementations(VectorDataTable)] + container_shape: VectorDataTable, + #[default(10.)] padding: f64, + #[default(false)] sort_by_area: bool, +) -> GraphicGroupTable { + let mut result = GraphicGroupTable::empty(); + + // Get the container's bounding box + let container_transform = container_shape.transform(); + let container_bbox = container_shape + .one_instance() + .instance + .bounding_box_with_transform(container_transform) + .unwrap_or_else(|| [DVec2::ZERO, DVec2::ZERO]); + + let container_size = container_bbox[1] - container_bbox[0]; + let container_origin = container_bbox[0]; + + // Extract bounding boxes and original instances + let mut items = Vec::new(); + for instance in instances.instances() { + // Get the bounding box directly from the instance transform + if let Some(bbox) = instance.instance.bounding_box(*instance.transform) { + let size = bbox[1] - bbox[0]; + items.push((size, *instance.transform, instance.instance.clone())); + } + } + + // Sort instances by area if requested (larger first for better packing) + if sort_by_area { + items.sort_by(|a, b| { + let area_a = a.0.x * a.0.y; + let area_b = b.0.x * b.0.y; + area_b.partial_cmp(&area_a).unwrap_or(std::cmp::Ordering::Equal) + }); + } + + // Simple bin packing algorithm + let mut current_x = container_origin.x; + let mut current_y = container_origin.y; + let mut row_height = 0.0; + + for (size, _, instance) in items { + // Add padding to the item size + let item_width = size.x + padding; + let item_height = size.y + padding; + + // Check if we need to move to the next row + if current_x + item_width > container_origin.x + container_size.x { + current_x = container_origin.x; + current_y += row_height; + row_height = 0.0; + } + + // Check if we've exceeded the container height + if current_y + item_height > container_origin.y + container_size.y { + // We could implement multi-page packing here + break; + } + + // Calculate the position for this item + let position = DVec2::new(current_x, current_y); + + // Extract the current center of the instance + let instance_center = { + if let Some(bbox) = instance.bounding_box(DAffine2::IDENTITY) { + (bbox[0] + bbox[1]) * 0.5 + } else { + DVec2::ZERO + } + }; + + // Create a new transform that positions the instance correctly + let new_transform = DAffine2::from_translation(position + instance_center); + + // Add to the result + let pushed = result.push(instance); + *pushed.transform = new_transform; + + // Update position tracking + current_x += item_width; + row_height = row_height.max(item_height); + } + + result +} + #[node_macro::node(category("Vector"), path(graphene_core::vector))] async fn center_instances( _: impl Ctx,