diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index ef649f0d12..9b002585dc 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -643,6 +643,116 @@ 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 } +/// Packs shapes using bounds with Best Fit Decreasing Height (BFDH) algorithm +/// Algorithm: +/// - Sort shapes by height (tallest first) +/// - For each shape, find the existing shelf with minimum remaining space that fits +/// - Create new shelf only if no existing shelf can accommodate the shape +/// Works as a reasonable approximation for classic box packing problem +#[node_macro::node(category("Vector"), path(graphene_core::vector))] +async fn pack_by_bounds( + _: impl Ctx, + #[implementations( + Table, + Table, + Table>, + Table>, + )] + elements: Table, + #[unit(" px")] + #[default(10.)] + spacing: f64, + #[unit(" px")] + #[default(1000.)] + max_width: f64, +) -> Table +where + Graphic: From>, + Table: BoundingBox, +{ + use core::cmp::Ordering; + + // Helper structure for shelves + #[derive(Clone)] + struct Shelf { + y: f64, + height: f64, + current_x: f64, + } + + // Prep the rows 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), + }; + (w, h, top_left, row) + }) + .collect(); + + // Sort by height, tallest first + items.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal)); + + let mut result = Table::new(); + let mut shelves: Vec = Vec::new(); + + for (w, h, top_left, mut row) in items { + if w <= 0. { + result.push(row); + continue; + } + + // Find a good shelf, minimum remaining space that can fit this item ideally + let mut best_shelf_idx = None; + let mut min_remaining_space = f64::INFINITY; + + for (idx, shelf) in shelves.iter().enumerate() { + let remaining_space = max_width - shelf.current_x; + if remaining_space >= w && remaining_space < min_remaining_space { + min_remaining_space = remaining_space; + best_shelf_idx = Some(idx); + } + } + + if let Some(shelf_idx) = best_shelf_idx { + // Place on existing shelf + let shelf = &mut shelves[shelf_idx]; + + // Update shelf height if needed + if h > shelf.height { + shelf.height = h; + } + + let target_pos = DVec2::new(shelf.current_x, shelf.y); + row.transform = DAffine2::from_translation(target_pos - top_left) * row.transform; + + shelf.current_x += w + spacing; + } else { + // Create new shelf + let new_y = shelves.last().map_or(0., |last| last.y + last.height + spacing); + let target_pos = DVec2::new(0., new_y); + row.transform = DAffine2::from_translation(target_pos - top_left) * row.transform; + + shelves.push(Shelf { + y: new_y, + height: h, + current_x: w + spacing, + }); + } + + 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(graphene_core::vector))] async fn auto_tangents( diff --git a/node-graph/gmath-nodes/src/lib.rs b/node-graph/gmath-nodes/src/lib.rs index 88ced629e2..58fba55c6b 100644 --- a/node-graph/gmath-nodes/src/lib.rs +++ b/node-graph/gmath-nodes/src/lib.rs @@ -220,6 +220,97 @@ fn logarithm( } } +/// The Remap function (remap) linearly maps a number from one range to another. If the input range is zero, the output will be the output minimum. +#[node_macro::node(category("Math: Numeric"))] +fn remap( + _: impl Ctx, + #[implementations(f64, f32)] value: U, + #[implementations(f64, f32)] + #[default(-1.)] + input_min: U, + #[implementations(f64, f32)] + #[default(1.)] + input_max: U, + #[implementations(f64, f32)] + #[default(0.)] + output_min: U, + #[implementations(f64, f32)] + #[default(1.)] + output_max: U, + #[default(false)] clamped: bool, +) -> U { + let input_range = input_max - input_min; + + // Handle division by zero + if input_range.abs() < U::epsilon() { + return output_min; + } + + let normalized = (value - input_min) / input_range; + let output_range = output_max - output_min; + + let result = output_min + normalized * output_range; + + if clamped { + // Handle both normal and inverted ranges, since we want to allow the user to use this node to also reverse a range. + if output_min <= output_max { + result.clamp(output_min, output_max) + } else { + result.clamp(output_max, output_min) + } + } else { + result + } +} + +/// Compute pascal triangle coefficients for use in generalized smoothstep +fn pascal_triangle(a: T, b: T) -> T { + let mut result = T::one(); + let b_int = b.to_usize().unwrap_or(0); + for i in 1..=b_int { + let i_t = T::from(i).unwrap(); + result = result * (a - (i_t - T::one())) / i_t; + } + result +} + +/// The smoothstep function creates a smooth interpolation curve between 0 and 1 +/// Order 1 is linear, order 2 is the standard smoothstep (3x² - 2x³), etc +#[node_macro::node(category("Math: Numeric"))] +fn smoothstep( + _: impl Ctx, + /// The input value which will be smoothly interpolated, values are automatically clamped to the 0-1 range + #[implementations(f64, f32)] + value: T, + /// Higher values create smoother transitions, minimum value is 1 e.g. linear, maximum is 8 e.g. very smooth + #[default(2.)] + #[implementations(f64, f32)] + #[hard_min(1.)] + #[hard_max(8.)] + order: T, +) -> T { + // Clamp input + let value = value.clamp(T::zero(), T::one()); + + // For order 1, return linear interpolation + let order_int = order.to_usize().unwrap_or(1).max(1); + if order_int == 1 { + return value; + } + + // Compute generalized smoothstep using Pascal triangle + let order_t = T::from(order_int).unwrap(); + let mut result = T::zero(); + for n in 0..order_int { + let n_t = T::from(n).unwrap(); + let coeff1 = pascal_triangle(-order_t, n_t); + let coeff2 = pascal_triangle(T::from(2 * order_int - 1).unwrap(), order_t - n_t - T::one()); + let power = value.powf(order_t + n_t); + result = result + coeff1 * coeff2 * power; + } + result +} + /// The sine trigonometric function (sin) calculates the ratio of the angle's opposite side length to its hypotenuse length. #[node_macro::node(category("Math: Trig"))] fn sine(