mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Mostly working instancing + box packing
This commit is contained in:
committed by
Keavon Chambers
parent
c9192307c7
commit
fa3e6eeca9
@@ -271,9 +271,9 @@ impl Bezier {
|
||||
_ => *self,
|
||||
};
|
||||
|
||||
let should_flip_direction = (self.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE);
|
||||
let should_flip_direction = (self.start - intersection).normalize_or_zero().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE);
|
||||
intermediate.apply_transformation(|point| {
|
||||
let mut direction_unit_vector = (intersection - point).normalize();
|
||||
let mut direction_unit_vector = (intersection - point).normalize_or_zero();
|
||||
if should_flip_direction {
|
||||
direction_unit_vector *= -1.;
|
||||
}
|
||||
|
||||
@@ -351,25 +351,38 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
|
||||
rotated_subpath
|
||||
}
|
||||
|
||||
/// Reduces the segments of the subpath into simple subcurves, then scales each subcurve a set `distance` away.
|
||||
/// The intersections of segments of the subpath are joined using the method specified by the `join` argument.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/libraries/bezier-rs#subpath/offset/solo" title="Offset Demo"></iframe>
|
||||
pub fn offset(&self, distance: f64, join: Join) -> Subpath<PointId> {
|
||||
// An offset at a distance 0 from the curve is simply the same curve
|
||||
// An offset of a single point is not defined
|
||||
// Early returns - same as before
|
||||
if distance == 0. || self.len() <= 1 || self.len_segments() < 1 {
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
let mut subpaths = self
|
||||
.iter()
|
||||
.filter(|bezier| !bezier.is_point())
|
||||
.map(|bezier| bezier.offset(distance))
|
||||
.filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty).
|
||||
.collect::<Vec<Subpath<PointId>>>();
|
||||
// Collect valid offset subpaths
|
||||
let mut subpaths = Vec::new();
|
||||
for bezier in self.iter() {
|
||||
if bezier.is_point() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to offset the bezier and handle potential failures
|
||||
let offset_result = std::panic::catch_unwind(|| bezier.offset(distance));
|
||||
|
||||
// Only include valid results
|
||||
if let Ok(subpath) = offset_result {
|
||||
if subpath.len() >= 2 {
|
||||
subpaths.push(subpath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't offset any segments, just return the original
|
||||
if subpaths.is_empty() {
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
let mut drop_common_point = vec![true; self.len()];
|
||||
|
||||
// Rest of the function remains the same
|
||||
// Clip or join consecutive Subpaths
|
||||
for i in 0..subpaths.len() - 1 {
|
||||
let j = i + 1;
|
||||
@@ -424,7 +437,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
|
||||
}
|
||||
|
||||
// Clip any overlap in the last segment
|
||||
if self.closed {
|
||||
if self.closed && !subpaths.is_empty() {
|
||||
let out_tangent = self.get_segment(self.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.));
|
||||
let in_tangent = self.get_segment(0).unwrap().tangent(TValue::Parametric(0.));
|
||||
let angle = out_tangent.angle_to(in_tangent);
|
||||
@@ -462,6 +475,11 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we have subpaths before merging
|
||||
if subpaths.is_empty() {
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
// Merge the subpaths. Drop points which overlap with one another.
|
||||
let mut manipulator_groups = subpaths[0].manipulator_groups.clone();
|
||||
for i in 1..subpaths.len() {
|
||||
@@ -475,7 +493,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
|
||||
manipulator_groups.append(&mut subpaths[i].manipulator_groups.clone());
|
||||
}
|
||||
}
|
||||
if self.closed && drop_common_point[0] {
|
||||
if self.closed && !subpaths.is_empty() && drop_common_point[0] {
|
||||
let last_group = manipulator_groups.pop().unwrap();
|
||||
manipulator_groups[0].in_handle = last_group.in_handle;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,22 @@ fn string_slice(_: impl Ctx, #[implementations(String)] string: String, start: f
|
||||
string.char_indices().skip(start).take(n).map(|(_, c)| c).collect()
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Text"))]
|
||||
fn split_string_by_index(
|
||||
_: impl Ctx,
|
||||
#[implementations(String)]
|
||||
/// The comma-separated string to split.
|
||||
input: String,
|
||||
/// The zero-based index of the item to retrieve.
|
||||
#[default(0.0)]
|
||||
#[min(0.0)]
|
||||
index: f64,
|
||||
) -> String {
|
||||
let parts: Vec<&str> = input.split(',').map(|s| s.trim()).collect();
|
||||
let floored_index = index.floor() as usize;
|
||||
if floored_index < parts.len() { parts[floored_index].to_string() } else { String::new() }
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Text"))]
|
||||
fn string_length(_: impl Ctx, #[implementations(String)] string: String) -> usize {
|
||||
string.len()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::instances::Instance;
|
||||
use crate::vector::{VectorData, VectorDataTable};
|
||||
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractIndex, ExtractVarArgs, OwnedContextImpl};
|
||||
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractIndex, ExtractVarArgs, GraphicGroupTable, OwnedContextImpl};
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[node_macro::node(name("Instance on Points"), category("Vector: Shape"), path(graphene_core::vector))]
|
||||
@@ -33,6 +33,36 @@ async fn instance_on_points(
|
||||
result
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Group Instance on Points"), category("Vector: Shape"), path(graphene_core::vector))]
|
||||
async fn group_instance_on_points(
|
||||
ctx: impl ExtractAll + CloneVarArgs + Ctx,
|
||||
points: VectorDataTable,
|
||||
#[implementations(Context -> GraphicGroupTable)] instance_node: impl Node<'n, Context<'static>, Output = GraphicGroupTable>,
|
||||
) -> GraphicGroupTable {
|
||||
let mut result = GraphicGroupTable::empty();
|
||||
|
||||
for Instance { instance: points, transform, .. } in points.instances() {
|
||||
for (index, &point) in points.point_domain.positions().iter().enumerate() {
|
||||
let transformed_point = transform.transform_point2(point);
|
||||
|
||||
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_vararg(Box::new(transformed_point));
|
||||
let instanced = instance_node.eval(new_ctx.into_context()).await;
|
||||
|
||||
for instanced_element in instanced.instances() {
|
||||
let new_instance = result.push_instance(instanced_element);
|
||||
*new_instance.transform *= DAffine2::from_translation(transformed_point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove once we support empty tables, currently this is here to avoid crashing
|
||||
if result.is_empty() {
|
||||
return GraphicGroupTable::empty();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Attributes"), path(graphene_core::vector))]
|
||||
async fn instance_position(ctx: impl Ctx + ExtractVarArgs) -> DVec2 {
|
||||
match ctx.vararg(0).map(|dynamic| dynamic.downcast_ref::<DVec2>()) {
|
||||
|
||||
@@ -806,6 +806,98 @@ where
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn box_pack(
|
||||
_: impl Ctx,
|
||||
#[implementations(GraphicGroupTable)] instances: GraphicGroupTable,
|
||||
#[expose]
|
||||
#[implementations(VectorDataTable)]
|
||||
container_shape: VectorDataTable,
|
||||
#[default(10.)] padding: f64,
|
||||
#[default(false)] sort_by_area: bool,
|
||||
) -> GraphicGroupTable {
|
||||
let mut result = GraphicGroupTable::empty();
|
||||
|
||||
// Get the container's bounding box
|
||||
let container_transform = container_shape.transform();
|
||||
let container_bbox = container_shape
|
||||
.one_instance()
|
||||
.instance
|
||||
.bounding_box_with_transform(container_transform)
|
||||
.unwrap_or_else(|| [DVec2::ZERO, DVec2::ZERO]);
|
||||
|
||||
let container_size = container_bbox[1] - container_bbox[0];
|
||||
let container_origin = container_bbox[0];
|
||||
|
||||
// Extract bounding boxes and original instances
|
||||
let mut items = Vec::new();
|
||||
for instance in instances.instances() {
|
||||
// Get the bounding box directly from the instance transform
|
||||
if let Some(bbox) = instance.instance.bounding_box(*instance.transform) {
|
||||
let size = bbox[1] - bbox[0];
|
||||
items.push((size, *instance.transform, instance.instance.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort instances by area if requested (larger first for better packing)
|
||||
if sort_by_area {
|
||||
items.sort_by(|a, b| {
|
||||
let area_a = a.0.x * a.0.y;
|
||||
let area_b = b.0.x * b.0.y;
|
||||
area_b.partial_cmp(&area_a).unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
}
|
||||
|
||||
// Simple bin packing algorithm
|
||||
let mut current_x = container_origin.x;
|
||||
let mut current_y = container_origin.y;
|
||||
let mut row_height = 0.0;
|
||||
|
||||
for (size, _, instance) in items {
|
||||
// Add padding to the item size
|
||||
let item_width = size.x + padding;
|
||||
let item_height = size.y + padding;
|
||||
|
||||
// Check if we need to move to the next row
|
||||
if current_x + item_width > container_origin.x + container_size.x {
|
||||
current_x = container_origin.x;
|
||||
current_y += row_height;
|
||||
row_height = 0.0;
|
||||
}
|
||||
|
||||
// Check if we've exceeded the container height
|
||||
if current_y + item_height > container_origin.y + container_size.y {
|
||||
// We could implement multi-page packing here
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate the position for this item
|
||||
let position = DVec2::new(current_x, current_y);
|
||||
|
||||
// Extract the current center of the instance
|
||||
let instance_center = {
|
||||
if let Some(bbox) = instance.bounding_box(DAffine2::IDENTITY) {
|
||||
(bbox[0] + bbox[1]) * 0.5
|
||||
} else {
|
||||
DVec2::ZERO
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new transform that positions the instance correctly
|
||||
let new_transform = DAffine2::from_translation(position + instance_center);
|
||||
|
||||
// Add to the result
|
||||
let pushed = result.push(instance);
|
||||
*pushed.transform = new_transform;
|
||||
|
||||
// Update position tracking
|
||||
current_x += item_width;
|
||||
row_height = row_height.max(item_height);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn center_instances(
|
||||
_: impl Ctx,
|
||||
|
||||
Reference in New Issue
Block a user