Skip to content

Commit 20e12ed

Browse files
otdaviesKeavon
andauthored
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>
1 parent 5b92901 commit 20e12ed

5 files changed

Lines changed: 134 additions & 2 deletions

File tree

editor/src/messages/portfolio/document/node_graph/node_properties.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use graphene_std::table::{Table, TableRow};
2626
use graphene_std::text::{Font, TextAlign};
2727
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
2828
use graphene_std::vector::QRCodeErrorCorrectionLevel;
29-
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType};
29+
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
3030
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
3131

3232
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
@@ -220,6 +220,7 @@ pub(crate) fn property_from_type(
220220
Some(x) if x == TypeId::of::<StrokeAlign>() => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
221221
Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
222222
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
223+
Some(x) if x == TypeId::of::<RowsOrColumns>() => enum_choice::<RowsOrColumns>().for_socket(default_info).property_row(),
223224
Some(x) if x == TypeId::of::<TextAlign>() => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
224225
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
225226
Some(x) if x == TypeId::of::<ExtrudeJoiningAlgorithm>() => enum_choice::<ExtrudeJoiningAlgorithm>().for_socket(default_info).property_row(),

node-graph/graph-craft/src/document/value.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ tagged_value! {
252252
SelectiveColorChoice(raster_nodes::adjustments::SelectiveColorChoice),
253253
GridType(vector::misc::GridType),
254254
ArcType(vector::misc::ArcType),
255+
RowsOrColumns(vector::misc::RowsOrColumns),
255256
MergeByDistanceAlgorithm(vector::misc::MergeByDistanceAlgorithm),
256257
ExtrudeJoiningAlgorithm(vector::misc::ExtrudeJoiningAlgorithm),
257258
PointSpacingType(vector::misc::PointSpacingType),

node-graph/interpreted-executor/src/node_registry.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
126126
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::SelectiveColorChoice]),
127127
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]),
128128
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]),
129+
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::RowsOrColumns]),
129130
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]),
130131
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]),
131132
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
@@ -214,6 +215,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
214215
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::SelectiveColorChoice]),
215216
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]),
216217
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]),
218+
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::RowsOrColumns]),
217219
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]),
218220
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]),
219221
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),

node-graph/libraries/vector-types/src/vector/misc.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ pub enum CentroidType {
1818
Length,
1919
}
2020

21+
#[repr(C)]
22+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
23+
#[widget(Radio)]
24+
pub enum RowsOrColumns {
25+
#[default]
26+
Rows = 0,
27+
Columns,
28+
}
29+
2130
pub trait AsU64 {
2231
fn as_u64(&self) -> u64;
2332
}

node-graph/nodes/vector/src/vector_nodes.rs

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use core::cmp::Ordering;
12
use core::f64::consts::PI;
23
use core::hash::{Hash, Hasher};
34
use core_types::bounds::{BoundingBox, RenderBoundingBox};
@@ -21,7 +22,7 @@ use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, evaluat
2122
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
2223
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
2324
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
24-
use vector_types::vector::misc::{CentroidType, ExtrudeJoiningAlgorithm, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2};
25+
use vector_types::vector::misc::{CentroidType, ExtrudeJoiningAlgorithm, RowsOrColumns, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2};
2526
use vector_types::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType, is_linear};
2627
use vector_types::vector::misc::{handles_to_segment, segment_to_handles};
2728
use vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke};
@@ -871,6 +872,124 @@ fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 {
871872
tl * (1. - t.x) * (1. - t.y) + tr * t.x * (1. - t.y) + br * t.x * t.y + bl * (1. - t.x) * t.y
872873
}
873874

875+
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
876+
async fn pack_strips<T: 'n + Send + Clone>(
877+
_: impl Ctx,
878+
#[implementations(
879+
Table<Graphic>,
880+
Table<Vector>,
881+
Table<Raster<CPU>>,
882+
Table<Raster<GPU>>,
883+
)]
884+
elements: Table<T>,
885+
#[default(0.)]
886+
#[unit(" px")]
887+
separation: f64,
888+
#[default(1000.)]
889+
#[unit(" px")]
890+
strip_max_length: f64,
891+
strip_direction: RowsOrColumns,
892+
) -> Table<T>
893+
where
894+
Graphic: From<Table<T>>,
895+
Table<T>: BoundingBox,
896+
{
897+
// Packs shapes using bounds with Best-Fit Decreasing Height (BFDH) algorithm:
898+
// - Sort shapes by cross-axis size (tallest first for rows, widest first for columns)
899+
// - For each shape, find the existing strip with minimum remaining space that fits
900+
// - Create new strip only if no existing strip can accommodate the shape
901+
902+
struct Strip {
903+
along_position: f64,
904+
cross_position: f64,
905+
cross_extent: f64,
906+
}
907+
908+
// Prepare the items to be sorted
909+
let mut items: Vec<(f64, f64, DVec2, TableRow<T>)> = elements
910+
.into_iter()
911+
.map(|row| {
912+
// Single-element table to query its bounding box
913+
let single = Table::new_from_row(row.clone());
914+
let (w, h, top_left) = match single.bounding_box(DAffine2::IDENTITY, false) {
915+
RenderBoundingBox::Rectangle([min, max]) => {
916+
let size = max - min;
917+
(size.x.max(0.), size.y.max(0.), min)
918+
}
919+
_ => (0., 0., DVec2::ZERO),
920+
};
921+
let (along, cross) = match strip_direction {
922+
RowsOrColumns::Rows => (w, h),
923+
RowsOrColumns::Columns => (h, w),
924+
};
925+
(along, cross, top_left, row)
926+
})
927+
.collect();
928+
929+
// Sort by cross-axis size, largest first
930+
items.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
931+
932+
let mut result = Table::new();
933+
let mut strips: Vec<Strip> = Vec::new();
934+
935+
// This looks n^2 but it is just n*k where k is the number of strips, which is generally much smaller than n
936+
for (along, cross, top_left, mut row) in items {
937+
if along <= 0. {
938+
result.push(row);
939+
continue;
940+
}
941+
942+
// Find a good strip, minimum remaining space that can fit this item ideally
943+
let mut best_strip_index = None;
944+
let mut min_remaining_space = f64::INFINITY;
945+
946+
for (index, strip) in strips.iter().enumerate() {
947+
let remaining_space = strip_max_length - strip.along_position;
948+
if remaining_space >= along && remaining_space < min_remaining_space {
949+
min_remaining_space = remaining_space;
950+
best_strip_index = Some(index);
951+
}
952+
}
953+
954+
if let Some(strip_index) = best_strip_index {
955+
// Place on existing strip
956+
let strip = &mut strips[strip_index];
957+
958+
// Update strip cross extent if needed
959+
if cross > strip.cross_extent {
960+
strip.cross_extent = cross;
961+
}
962+
963+
let target_position = match strip_direction {
964+
RowsOrColumns::Rows => DVec2::new(strip.along_position, strip.cross_position),
965+
RowsOrColumns::Columns => DVec2::new(strip.cross_position, strip.along_position),
966+
};
967+
row.transform = DAffine2::from_translation(target_position - top_left) * row.transform;
968+
969+
strip.along_position += along + separation;
970+
} else {
971+
// Create new strip
972+
let new_cross = strips.last().map_or(0., |last| last.cross_position + last.cross_extent + separation);
973+
974+
let target_position = match strip_direction {
975+
RowsOrColumns::Rows => DVec2::new(0., new_cross),
976+
RowsOrColumns::Columns => DVec2::new(new_cross, 0.),
977+
};
978+
row.transform = DAffine2::from_translation(target_position - top_left) * row.transform;
979+
980+
strips.push(Strip {
981+
along_position: along + separation,
982+
cross_position: new_cross,
983+
cross_extent: cross,
984+
});
985+
}
986+
987+
result.push(row);
988+
}
989+
990+
result
991+
}
992+
874993
/// Automatically constructs tangents (Bézier handles) for anchor points in a vector path.
875994
#[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(core_types::vector))]
876995
async fn auto_tangents(

0 commit comments

Comments
 (0)