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:
parent
117ce301b0
commit
eb0f019b15
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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, .. } => {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
// (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) => {}
|
||||
}
|
||||
}
|
||||
data
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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))]
|
||||
|
|
|
|||
Loading…
Reference in New Issue