diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 625117cd..b3ed309d 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -26,7 +26,7 @@ use graphene_std::table::{Table, TableRow}; use graphene_std::text::{Font, TextAlign}; use graphene_std::transform::{Footprint, ReferencePoint, Transform}; use graphene_std::vector::QRCodeErrorCorrectionLevel; -use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType}; +use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType}; use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; pub(crate) fn string_properties(text: &str) -> Vec { @@ -220,6 +220,7 @@ pub(crate) fn property_from_type( Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 15279f56..fb483a01 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -252,6 +252,7 @@ tagged_value! { SelectiveColorChoice(raster_nodes::adjustments::SelectiveColorChoice), GridType(vector::misc::GridType), ArcType(vector::misc::ArcType), + RowsOrColumns(vector::misc::RowsOrColumns), MergeByDistanceAlgorithm(vector::misc::MergeByDistanceAlgorithm), ExtrudeJoiningAlgorithm(vector::misc::ExtrudeJoiningAlgorithm), PointSpacingType(vector::misc::PointSpacingType), diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index a9a28a85..8dadda45 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -126,6 +126,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => graphene_std::raster::adjustments::SelectiveColorChoice]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::RowsOrColumns]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), @@ -214,6 +215,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => graphene_std::raster::SelectiveColorChoice]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::RowsOrColumns]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), diff --git a/node-graph/libraries/vector-types/src/vector/misc.rs b/node-graph/libraries/vector-types/src/vector/misc.rs index 088e5daa..17902b60 100644 --- a/node-graph/libraries/vector-types/src/vector/misc.rs +++ b/node-graph/libraries/vector-types/src/vector/misc.rs @@ -18,6 +18,15 @@ pub enum CentroidType { Length, } +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Radio)] +pub enum RowsOrColumns { + #[default] + Rows = 0, + Columns, +} + pub trait AsU64 { fn as_u64(&self) -> u64; } diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index dfa2cfd4..a7b45cf4 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -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( + _: impl Ctx, + #[implementations( + Table, + Table, + Table>, + Table>, + )] + elements: Table, + #[default(0.)] + #[unit(" px")] + separation: f64, + #[default(1000.)] + #[unit(" px")] + strip_max_length: f64, + strip_direction: RowsOrColumns, +) -> Table +where + Graphic: From>, + Table: 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)> = 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 = 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(