New nodes: 'Reset Transform', 'Replace Transform', 'Count Points', 'Index Points' (#3420)

- Add the 'Reset Transform' and 'Replace Transform' nodes
- Add the 'Count Points' and 'Index Points' nodes
- Make the 'Index Elements' node support negative indexing from the end
- Make the 'Flatten Vector' node's implementation reusable
- Fix crash displaying 0x0 raster image in the Data panel
- Fix the 'Points to Polyline' node not working on two-point objects
This commit is contained in:
Keavon Chambers 2025-11-25 20:41:59 -08:00 committed by GitHub
parent 117ce301b0
commit eb0f019b15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 210 additions and 79 deletions

View File

@ -502,10 +502,17 @@ impl TableRowLayout for Raster<CPU> {
format!("Raster ({}x{})", self.width, self.height)
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let base64_string = self.data().base64_string.clone().unwrap_or_else(|| {
let raster = self.data();
if raster.width == 0 || raster.height == 0 {
let widgets = vec![TextLabel::new("Image has no area").widget_holder()];
return vec![LayoutGroup::Row { widgets }];
}
let base64_string = raster.base64_string.clone().unwrap_or_else(|| {
use base64::Engine;
let output = self.data().to_png();
let output = raster.to_png();
let preamble = "data:image/png;base64,";
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
base64_string.push_str(preamble);

View File

@ -154,6 +154,7 @@ pub(crate) fn property_from_type(
Some("PixelLength") => number_widget(default_info, number_input.min(min(0.)).unit(unit.unwrap_or(" px"))).into(),
Some("Length") => number_widget(default_info, number_input.min(min(0.))).into(),
Some("Fraction") => number_widget(default_info, number_input.mode_range().min(min(0.)).max(max(1.))).into(),
Some("SignedInteger") => number_widget(default_info, number_input.int()).into(),
Some("IntegerCount") => number_widget(default_info, number_input.int().min(min(1.))).into(),
Some("SeedValue") => number_widget(default_info, number_input.int().min(min(0.))).into(),
Some("PixelSize") => vec2_widget(default_info, "X", "Y", unit.unwrap_or(" px"), None, false),

View File

@ -848,7 +848,7 @@ impl NodeNetwork {
// If the input to self is a node, connect the corresponding output of the inner network to it
NodeInput::Node { node_id, output_index } => {
nested_node.populate_first_network_input(node_id, output_index, nested_input_index, node.original_location.inputs(*import_index), 1);
let input_node = self.nodes.get_mut(&node_id).unwrap_or_else(|| panic!("unable find input node {node_id:?}"));
let input_node = self.nodes.get_mut(&node_id).unwrap_or_else(|| panic!("Unable to find input node {node_id:?}"));
input_node.original_location.dependants[output_index].push(nested_node_id);
}
NodeInput::Import { import_index, .. } => {

View File

@ -79,6 +79,8 @@ impl Convert<DVec2, ()> for DVec2 {
}
}
// TODO: Add a DVec2 to Table<Vector> anchor point conversion implementation to replace the 'Vec2 to Point' node
/// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types.
macro_rules! impl_convert {
($from:ty, $to:ty) => {

View File

@ -119,6 +119,55 @@ impl From<Table<GradientStops>> for Graphic {
// Local trait to convert types to Table<Graphic> (avoids orphan rule issues)
pub trait IntoGraphicTable {
fn into_graphic_table(self) -> Table<Graphic>;
/// Deeply flattens any vector content within a graphic table, discarding non-vector content, and returning a table of only vector elements.
fn into_flattened_vector_table(self) -> Table<Vector>
where
Self: std::marker::Sized,
{
let content = self.into_graphic_table();
// TODO: Avoid mutable reference, instead return a new Table<Graphic>?
fn flatten_table(output_vector_table: &mut Table<Vector>, current_graphic_table: Table<Graphic>) {
for current_graphic_row in current_graphic_table.iter() {
let current_graphic = current_graphic_row.element.clone();
let source_node_id = *current_graphic_row.source_node_id;
match current_graphic {
// If we're allowed to recurse, flatten any tables we encounter
Graphic::Graphic(mut current_graphic_table) => {
// Apply the parent graphic's transform to all child elements
for graphic in current_graphic_table.iter_mut() {
*graphic.transform = *current_graphic_row.transform * *graphic.transform;
}
flatten_table(output_vector_table, current_graphic_table);
}
// Push any leaf Vector elements we encounter
Graphic::Vector(vector_table) => {
for current_vector_row in vector_table.iter() {
output_vector_table.push(TableRow {
element: current_vector_row.element.clone(),
transform: *current_graphic_row.transform * *current_vector_row.transform,
alpha_blending: AlphaBlending {
blend_mode: current_vector_row.alpha_blending.blend_mode,
opacity: current_graphic_row.alpha_blending.opacity * current_vector_row.alpha_blending.opacity,
fill: current_vector_row.alpha_blending.fill,
clip: current_vector_row.alpha_blending.clip,
},
source_node_id,
});
}
}
_ => {}
}
}
}
let mut output = Table::new();
flatten_table(&mut output, content);
output
}
}
impl IntoGraphicTable for Table<Graphic> {
@ -284,6 +333,7 @@ impl RenderComplexity for Graphic {
pub trait AtIndex {
type Output;
fn at_index(&self, index: usize) -> Option<Self::Output>;
fn at_index_from_end(&self, index: usize) -> Option<Self::Output>;
}
impl<T: Clone> AtIndex for Vec<T> {
type Output = T;
@ -291,6 +341,10 @@ impl<T: Clone> AtIndex for Vec<T> {
fn at_index(&self, index: usize) -> Option<Self::Output> {
self.get(index).cloned()
}
fn at_index_from_end(&self, index: usize) -> Option<Self::Output> {
if index == 0 || index > self.len() { None } else { self.get(self.len() - index).cloned() }
}
}
impl<T: Clone> AtIndex for Table<T> {
type Output = Table<T>;
@ -304,6 +358,18 @@ impl<T: Clone> AtIndex for Table<T> {
None
}
}
fn at_index_from_end(&self, index: usize) -> Option<Self::Output> {
let mut result_table = Self::default();
if index == 0 || index > self.len() {
None
} else if let Some(row) = self.iter().nth(self.len() - index) {
result_table.push(row.into_cloned());
Some(result_table)
} else {
None
}
}
}
// TODO: Eventually remove this migration document upgrade code

View File

@ -19,6 +19,8 @@ pub mod types {
pub type Length = f64;
/// 0 to 1
pub type Fraction = f64;
/// Signed integer that's actually a float because we don't handle type conversions very well yet
pub type SignedInteger = f64;
/// Unsigned integer
pub type IntegerCount = u32;
/// Unsigned integer to be used for random seeds

View File

@ -1,15 +1,11 @@
use core_types::Color;
use core_types::{
Ctx,
blending::AlphaBlending,
table::{Table, TableRow},
uuid::NodeId,
};
use core_types::Ctx;
use core_types::registry::types::SignedInteger;
use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId;
use glam::{DAffine2, DVec2};
use graphic_types::{
Artboard, Vector,
graphic::{Graphic, IntoGraphicTable},
};
use graphic_types::graphic::{Graphic, IntoGraphicTable};
use graphic_types::{Artboard, Vector};
use raster_types::{CPU, GPU, Raster};
use vector_types::GradientStops;
@ -164,48 +160,8 @@ pub async fn flatten_graphic(_: impl Ctx, content: Table<Graphic>, fully_flatten
/// Converts a graphic table into a vector table by deeply flattening any vector content it contains, and discarding any non-vector content.
#[node_macro::node(category("Vector"))]
pub async fn flatten_vector(_: impl Ctx, content: Table<Graphic>) -> Table<Vector> {
// TODO: Avoid mutable reference, instead return a new Table<Graphic>?
fn flatten_table(output_vector_table: &mut Table<Vector>, current_graphic_table: Table<Graphic>) {
for current_graphic_row in current_graphic_table.iter() {
let current_graphic = current_graphic_row.element.clone();
let source_node_id = *current_graphic_row.source_node_id;
match current_graphic {
// If we're allowed to recurse, flatten any tables we encounter
Graphic::Graphic(mut current_graphic_table) => {
// Apply the parent graphic's transform to all child elements
for graphic in current_graphic_table.iter_mut() {
*graphic.transform = *current_graphic_row.transform * *graphic.transform;
}
flatten_table(output_vector_table, current_graphic_table);
}
// Push any leaf Vector elements we encounter
Graphic::Vector(vector_table) => {
for current_vector_row in vector_table.iter() {
output_vector_table.push(TableRow {
element: current_vector_row.element.clone(),
transform: *current_graphic_row.transform * *current_vector_row.transform,
alpha_blending: AlphaBlending {
blend_mode: current_vector_row.alpha_blending.blend_mode,
opacity: current_graphic_row.alpha_blending.opacity * current_vector_row.alpha_blending.opacity,
fill: current_vector_row.alpha_blending.fill,
clip: current_vector_row.alpha_blending.clip,
},
source_node_id,
});
}
}
_ => {}
}
}
}
let mut output = Table::new();
flatten_table(&mut output, content);
output
pub async fn flatten_vector<I: IntoGraphicTable + 'n + Send + Clone>(_: impl Ctx, #[implementations(Table<Graphic>, Table<Vector>)] content: I) -> Table<Vector> {
content.into_flattened_vector_table()
}
/// Returns the value at the specified index in the collection.
@ -229,11 +185,18 @@ pub fn index_elements<T: graphic_types::graphic::AtIndex + Clone + Default>(
Table<GradientStops>,
)]
collection: T,
/// The index of the item to retrieve, starting from 0 for the first item.
index: u32,
/// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the collection, starting from -1 for the last item.
index: SignedInteger,
) -> T::Output
where
T::Output: Clone + Default,
{
collection.at_index(index as usize).unwrap_or_default()
let index = index as i32;
if index < 0 {
collection.at_index_from_end(-index as usize)
} else {
collection.at_index(index as usize)
}
.unwrap_or_default()
}

View File

@ -3,7 +3,7 @@ use core_types::color::Color;
use core_types::table::Table;
use core_types::transform::{ApplyTransform, Transform};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, InjectFootprint, ModifyFootprint, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Graphic;
use graphic_types::Vector;
use graphic_types::raster_types::{CPU, GPU, Raster};
@ -16,14 +16,14 @@ async fn transform<T: ApplyTransform + 'n + 'static>(
#[implementations(
Context -> DAffine2,
Context -> DVec2,
Context -> Table<Vector>,
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
)]
value: impl Node<Context<'static>, Output = T>,
content: impl Node<Context<'static>, Output = T>,
translation: DVec2,
rotation: f64,
scale: DVec2,
@ -41,24 +41,75 @@ async fn transform<T: ApplyTransform + 'n + 'static>(
ctx = ctx.with_footprint(footprint);
}
let mut transform_target = value.eval(ctx.into_context()).await;
let mut transform_target = content.eval(ctx.into_context()).await;
transform_target.left_apply_transform(&matrix);
transform_target
}
/// Overwrites the transform of each element in the input table with the specified transform.
#[node_macro::node(category(""))]
fn replace_transform<Data, TransformInput: Transform>(
_: impl Ctx + InjectFootprint,
#[implementations(Table<Vector>, Table<Raster<CPU>>, Table<Graphic>, Table<Color>, Table<GradientStops>)] mut data: Table<Data>,
#[implementations(DAffine2)] transform: TransformInput,
) -> Table<Data> {
for data_transform in data.iter_mut() {
*data_transform.transform = transform.transform();
/// Resets the desired components of the input transform to their default values. If all components are reset, the output will be set to the identity transform.
/// Shear is represented jointly by rotation and scale, so resetting both will also remove any shear.
#[node_macro::node(category("Math: Transform"))]
fn reset_transform<T>(
_: impl Ctx,
#[implementations(
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
)]
mut content: Table<T>,
#[default(true)] reset_translation: bool,
reset_rotation: bool,
reset_scale: bool,
) -> Table<T> {
for row in content.iter_mut() {
// Translation
if reset_translation {
row.transform.translation = DVec2::ZERO;
}
data
// (Rotation, Scale)
match (reset_rotation, reset_scale) {
(true, true) => {
row.transform.matrix2 = DMat2::IDENTITY;
}
(true, false) => {
let scale = row.transform.decompose_scale();
row.transform.matrix2 = DMat2::from_diagonal(scale);
}
(false, true) => {
let rotation = row.transform.decompose_rotation();
let rotation_matrix = DMat2::from_angle(rotation);
row.transform.matrix2 = rotation_matrix;
}
(false, false) => {}
}
}
content
}
/// Overwrites the transform of each element in the input table with the specified transform.
#[node_macro::node(category("Math: Transform"))]
fn replace_transform<T>(
_: impl Ctx + InjectFootprint,
#[implementations(
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
)]
mut content: Table<T>,
transform: DAffine2,
) -> Table<T> {
for row in content.iter_mut() {
*row.transform = transform.transform();
}
content
}
// TODO: Figure out how this node should behave once #2982 is implemented.
@ -74,9 +125,9 @@ async fn extract_transform<T>(
Table<Color>,
Table<GradientStops>,
)]
vector: Table<T>,
content: Table<T>,
) -> DAffine2 {
vector.iter().next().map(|row| *row.transform).unwrap_or_default()
content.iter().next().map(|row| *row.transform).unwrap_or_default()
}
/// Produces the inverse of the input transform, which is the transform that undoes the effect of the original transform.

View File

@ -234,7 +234,7 @@ async fn repeat<I: 'n + Send + Clone>(
// TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed.
direction: PixelSize,
angle: Angle,
#[default(4)] count: IntegerCount,
#[default(5)] count: IntegerCount,
) -> Table<I> {
let angle = angle.to_radians();
let count = count.max(1);
@ -825,6 +825,7 @@ async fn dimensions(_: impl Ctx, content: Table<Vector>) -> DVec2 {
.unwrap_or_default()
}
// TODO: Replace this node with an automatic type conversion implementation of the `Convert` trait
/// Converts a vec2 value into a vector path composed of a single anchor point.
///
/// This is useful in conjunction with nodes that repeat it, followed by the "Points to Polyline" node to string together a path of the points.
@ -848,12 +849,12 @@ async fn points_to_polyline(_: impl Ctx, mut points: Table<Vector>, #[default(tr
let points_count = row.element.point_domain.ids().len();
if points_count > 2 {
if points_count >= 2 {
(0..points_count - 1).for_each(|i| {
segment_domain.push(next_id.next_id(), i, i + 1, BezierHandles::Linear, StrokeId::generate());
});
if closed {
if closed && points_count != 2 {
segment_domain.push(next_id.next_id(), points_count - 1, 0, BezierHandles::Linear, StrokeId::generate());
row.element
@ -2007,9 +2008,47 @@ async fn count_elements<I: Count>(
Vec<f64>,
Vec<DVec2>,
)]
source: I,
content: I,
) -> f64 {
source.count() as f64
content.count() as f64
}
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
async fn count_points(_: impl Ctx, content: Table<Vector>) -> f64 {
content.into_iter().map(|row| row.element.point_domain.positions().len() as f64).sum()
}
/// Retrieves the vec2 position (in local space) of the anchor point at the specified index in table of vector elements.
/// If no value exists at that index, the position (0, 0) is returned.
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn index_points(
_: impl Ctx,
/// The vector element or elements containing the anchor points to be retrieved.
content: Table<Vector>,
/// The index of the points to retrieve, starting from 0 for the first point. Negative indices count backwards from the end, starting from -1 for the last item.
index: f64,
) -> DVec2 {
let points_count = content.iter().map(|row| row.element.point_domain.positions().len()).sum::<usize>();
// Clamp and allow negative indexing from the end
let index = index as isize;
let index = if index < 0 {
(points_count as isize + index).max(0) as usize
} else {
(index as usize).min(points_count - 1)
};
// Find the point at the given index across all vector elements
let mut accumulated = 0;
for row in content.iter() {
let row_point_count = row.element.point_domain.positions().len();
if index - accumulated < row_point_count {
return row.element.point_domain.positions()[index - accumulated];
}
accumulated += row_point_count;
}
DVec2::ZERO
}
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]