mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 02:08:12 +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,
|
_ => *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| {
|
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 {
|
if should_flip_direction {
|
||||||
direction_unit_vector *= -1.;
|
direction_unit_vector *= -1.;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,25 +351,38 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
|
|||||||
rotated_subpath
|
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> {
|
pub fn offset(&self, distance: f64, join: Join) -> Subpath<PointId> {
|
||||||
// An offset at a distance 0 from the curve is simply the same curve
|
// Early returns - same as before
|
||||||
// An offset of a single point is not defined
|
|
||||||
if distance == 0. || self.len() <= 1 || self.len_segments() < 1 {
|
if distance == 0. || self.len() <= 1 || self.len_segments() < 1 {
|
||||||
return self.clone();
|
return self.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut subpaths = self
|
// Collect valid offset subpaths
|
||||||
.iter()
|
let mut subpaths = Vec::new();
|
||||||
.filter(|bezier| !bezier.is_point())
|
for bezier in self.iter() {
|
||||||
.map(|bezier| bezier.offset(distance))
|
if bezier.is_point() {
|
||||||
.filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty).
|
continue;
|
||||||
.collect::<Vec<Subpath<PointId>>>();
|
}
|
||||||
|
|
||||||
|
// 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()];
|
let mut drop_common_point = vec![true; self.len()];
|
||||||
|
|
||||||
|
// Rest of the function remains the same
|
||||||
// Clip or join consecutive Subpaths
|
// Clip or join consecutive Subpaths
|
||||||
for i in 0..subpaths.len() - 1 {
|
for i in 0..subpaths.len() - 1 {
|
||||||
let j = i + 1;
|
let j = i + 1;
|
||||||
@@ -424,7 +437,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clip any overlap in the last segment
|
// 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 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 in_tangent = self.get_segment(0).unwrap().tangent(TValue::Parametric(0.));
|
||||||
let angle = out_tangent.angle_to(in_tangent);
|
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.
|
// Merge the subpaths. Drop points which overlap with one another.
|
||||||
let mut manipulator_groups = subpaths[0].manipulator_groups.clone();
|
let mut manipulator_groups = subpaths[0].manipulator_groups.clone();
|
||||||
for i in 1..subpaths.len() {
|
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());
|
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();
|
let last_group = manipulator_groups.pop().unwrap();
|
||||||
manipulator_groups[0].in_handle = last_group.in_handle;
|
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()
|
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"))]
|
#[node_macro::node(category("Text"))]
|
||||||
fn string_length(_: impl Ctx, #[implementations(String)] string: String) -> usize {
|
fn string_length(_: impl Ctx, #[implementations(String)] string: String) -> usize {
|
||||||
string.len()
|
string.len()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::instances::Instance;
|
use crate::instances::Instance;
|
||||||
use crate::vector::{VectorData, VectorDataTable};
|
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};
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
||||||
#[node_macro::node(name("Instance on Points"), category("Vector: Shape"), path(graphene_core::vector))]
|
#[node_macro::node(name("Instance on Points"), category("Vector: Shape"), path(graphene_core::vector))]
|
||||||
@@ -33,6 +33,36 @@ async fn instance_on_points(
|
|||||||
result
|
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))]
|
#[node_macro::node(category("Attributes"), path(graphene_core::vector))]
|
||||||
async fn instance_position(ctx: impl Ctx + ExtractVarArgs) -> DVec2 {
|
async fn instance_position(ctx: impl Ctx + ExtractVarArgs) -> DVec2 {
|
||||||
match ctx.vararg(0).map(|dynamic| dynamic.downcast_ref::<DVec2>()) {
|
match ctx.vararg(0).map(|dynamic| dynamic.downcast_ref::<DVec2>()) {
|
||||||
|
|||||||
@@ -806,6 +806,98 @@ where
|
|||||||
result_table
|
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))]
|
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||||
async fn center_instances(
|
async fn center_instances(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
|
|||||||
Reference in New Issue
Block a user