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:
parent
5b92901715
commit
20e12edd45
|
|
@ -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<LayoutGroup> {
|
||||
|
|
@ -220,6 +220,7 @@ pub(crate) fn property_from_type(
|
|||
Some(x) if x == TypeId::of::<StrokeAlign>() => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<RowsOrColumns>() => enum_choice::<RowsOrColumns>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<TextAlign>() => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<ExtrudeJoiningAlgorithm>() => enum_choice::<ExtrudeJoiningAlgorithm>().for_socket(default_info).property_row(),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, 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<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
async_node!(graphene_core::memo::MemoNode<_, _>, 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]),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue