New node: Pack Strips (#3246)

* Added basic pack by bounds node

Apply suggestion from @Keavon

Co-authored-by: Keavon Chambers <keavon@keavon.com>

* Add support for choosing rows/columns strip direction

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Oliver Davies
2026-02-16 01:07:03 -08:00
committed by GitHub
parent 5b92901715
commit 20e12edd45
5 changed files with 134 additions and 2 deletions

View File

@@ -1,3 +1,4 @@
use core::cmp::Ordering;
use core::f64::consts::PI;
use core::hash::{Hash, Hasher};
use core_types::bounds::{BoundingBox, RenderBoundingBox};
@@ -21,7 +22,7 @@ use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, evaluat
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
use vector_types::vector::misc::{CentroidType, ExtrudeJoiningAlgorithm, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2};
use vector_types::vector::misc::{CentroidType, ExtrudeJoiningAlgorithm, RowsOrColumns, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2};
use vector_types::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType, is_linear};
use vector_types::vector::misc::{handles_to_segment, segment_to_handles};
use vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke};
@@ -871,6 +872,124 @@ fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 {
tl * (1. - t.x) * (1. - t.y) + tr * t.x * (1. - t.y) + br * t.x * t.y + bl * (1. - t.x) * t.y
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn pack_strips<T: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
)]
elements: Table<T>,
#[default(0.)]
#[unit(" px")]
separation: f64,
#[default(1000.)]
#[unit(" px")]
strip_max_length: f64,
strip_direction: RowsOrColumns,
) -> Table<T>
where
Graphic: From<Table<T>>,
Table<T>: BoundingBox,
{
// Packs shapes using bounds with Best-Fit Decreasing Height (BFDH) algorithm:
// - Sort shapes by cross-axis size (tallest first for rows, widest first for columns)
// - For each shape, find the existing strip with minimum remaining space that fits
// - Create new strip only if no existing strip can accommodate the shape
struct Strip {
along_position: f64,
cross_position: f64,
cross_extent: f64,
}
// Prepare the items to be sorted
let mut items: Vec<(f64, f64, DVec2, TableRow<T>)> = elements
.into_iter()
.map(|row| {
// Single-element table to query its bounding box
let single = Table::new_from_row(row.clone());
let (w, h, top_left) = match single.bounding_box(DAffine2::IDENTITY, false) {
RenderBoundingBox::Rectangle([min, max]) => {
let size = max - min;
(size.x.max(0.), size.y.max(0.), min)
}
_ => (0., 0., DVec2::ZERO),
};
let (along, cross) = match strip_direction {
RowsOrColumns::Rows => (w, h),
RowsOrColumns::Columns => (h, w),
};
(along, cross, top_left, row)
})
.collect();
// Sort by cross-axis size, largest first
items.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
let mut result = Table::new();
let mut strips: Vec<Strip> = Vec::new();
// This looks n^2 but it is just n*k where k is the number of strips, which is generally much smaller than n
for (along, cross, top_left, mut row) in items {
if along <= 0. {
result.push(row);
continue;
}
// Find a good strip, minimum remaining space that can fit this item ideally
let mut best_strip_index = None;
let mut min_remaining_space = f64::INFINITY;
for (index, strip) in strips.iter().enumerate() {
let remaining_space = strip_max_length - strip.along_position;
if remaining_space >= along && remaining_space < min_remaining_space {
min_remaining_space = remaining_space;
best_strip_index = Some(index);
}
}
if let Some(strip_index) = best_strip_index {
// Place on existing strip
let strip = &mut strips[strip_index];
// Update strip cross extent if needed
if cross > strip.cross_extent {
strip.cross_extent = cross;
}
let target_position = match strip_direction {
RowsOrColumns::Rows => DVec2::new(strip.along_position, strip.cross_position),
RowsOrColumns::Columns => DVec2::new(strip.cross_position, strip.along_position),
};
row.transform = DAffine2::from_translation(target_position - top_left) * row.transform;
strip.along_position += along + separation;
} else {
// Create new strip
let new_cross = strips.last().map_or(0., |last| last.cross_position + last.cross_extent + separation);
let target_position = match strip_direction {
RowsOrColumns::Rows => DVec2::new(0., new_cross),
RowsOrColumns::Columns => DVec2::new(new_cross, 0.),
};
row.transform = DAffine2::from_translation(target_position - top_left) * row.transform;
strips.push(Strip {
along_position: along + separation,
cross_position: new_cross,
cross_extent: cross,
});
}
result.push(row);
}
result
}
/// Automatically constructs tangents (Bézier handles) for anchor points in a vector path.
#[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(core_types::vector))]
async fn auto_tangents(