mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 17:08:11 +08:00
Initial versions of remap, smoothstep and pack by bounds
This commit is contained in:
@@ -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
|
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<I: 'n + Send + Clone>(
|
||||||
|
_: impl Ctx,
|
||||||
|
#[implementations(
|
||||||
|
Table<Graphic>,
|
||||||
|
Table<Vector>,
|
||||||
|
Table<Raster<CPU>>,
|
||||||
|
Table<Raster<GPU>>,
|
||||||
|
)]
|
||||||
|
elements: Table<I>,
|
||||||
|
#[unit(" px")]
|
||||||
|
#[default(10.)]
|
||||||
|
spacing: f64,
|
||||||
|
#[unit(" px")]
|
||||||
|
#[default(1000.)]
|
||||||
|
max_width: f64,
|
||||||
|
) -> Table<I>
|
||||||
|
where
|
||||||
|
Graphic: From<Table<I>>,
|
||||||
|
Table<I>: 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<I>)> = 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<Shelf> = 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.
|
/// 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))]
|
#[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(graphene_core::vector))]
|
||||||
async fn auto_tangents(
|
async fn auto_tangents(
|
||||||
|
|||||||
@@ -220,6 +220,97 @@ fn logarithm<T: num_traits::float::Float>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<U: num_traits::float::Float>(
|
||||||
|
_: 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<T: num_traits::float::Float>(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<T: num_traits::float::Float>(
|
||||||
|
_: 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.
|
/// 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"))]
|
#[node_macro::node(category("Math: Trig"))]
|
||||||
fn sine<T: num_traits::float::Float>(
|
fn sine<T: num_traits::float::Float>(
|
||||||
|
|||||||
Reference in New Issue
Block a user