mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 02:48:12 +08:00
Add multi-output nodes with struct returns destructured by #[node_macro::destructure]
A #[node_macro::node] function returning a struct tagged with
field is a named output connector (title-cased from the field name,
renamed with #[name("...")], described by its doc comment). By default
the node has a hidden primary output carrying the whole struct with the
fields as secondary outputs; marking at most one field #[primary] makes
that field the primary output instead.
The macro generates one hidden extractor proto node per field plus a
registration keyed by the struct's TypeId. The Graphene preprocessor
recognizes nodes returning a registered struct and substitutes them, in
the transient runtime copy of the network only, with a generated network
exporting each field through its extractor. The destructuring machinery
therefore never appears when drilling into a node, in copied clipboard
content, or in saved documents. When a Memoize implementation is
registered for the struct type, the struct is computed once and shared
across all outputs rather than re-evaluated per output.
The editor derives output counts, names, and types for such nodes from
the registry. The old hand-authored "Split Vec2" and "Split Channels"
wrapper-network definitions are replaced by multi-output split_vec2 and
split_channels proto nodes, with document migrations that keep existing
wires valid since the output indices are unchanged.
The "Position on Path" and "Tangent on Path" nodes are combined into a
single multi-output "Evaluate Path" node whose primary output is the
position and whose secondary output is the tangent angle. A migration
converts old instances, forwarding the shared inputs and remapping the
tangent nodes' downstream connections to the new tangent output index.
The now-redundant "Extract XY" node is removed (its role is subsumed by
Split Vec2's destructuring), and "Extract Channel" becomes a plain helper
used by Split Channels rather than a standalone node.
This commit is contained in:
@@ -1,23 +1,7 @@
|
||||
use core_types::list::Item;
|
||||
use core_types::{CacheHash, Ctx};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DVec2, IVec2, UVec2};
|
||||
|
||||
/// Obtains the X or Y component of a vec2.
|
||||
///
|
||||
/// The inverse of this node is **Combine Vec2**, which composes a vec2 from its X and Y components.
|
||||
#[node_macro::node(name("Extract XY"), category("Math: Vec2"))]
|
||||
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: Item<T>, axis: Item<XY>) -> Item<f64> {
|
||||
let vector = vector.into_element();
|
||||
let axis = axis.into_element();
|
||||
|
||||
let result = match axis {
|
||||
XY::X => vector.into().x,
|
||||
XY::Y => vector.into().y,
|
||||
};
|
||||
|
||||
Item::new_from_element(result)
|
||||
}
|
||||
use glam::DVec2;
|
||||
|
||||
/// The X or Y component of a vec2.
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
@@ -29,3 +13,23 @@ pub enum XY {
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
|
||||
/// The X and Y components of a vec2, split into separate node outputs.
|
||||
#[node_macro::destructure]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, DynAny)]
|
||||
pub struct Vec2Components {
|
||||
/// The X component of the vec2.
|
||||
pub x: f64,
|
||||
/// The Y component of the vec2.
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
/// Decomposes the X and Y components of a vec2.
|
||||
///
|
||||
/// The inverse of this node is **Combine Vec2**, which composes a vec2 from its X and Y components.
|
||||
#[node_macro::node(name("Split Vec2"), category("Math: Vec2"))]
|
||||
fn split_vec2(_: impl Ctx, #[name("Vec2")] vec2: Item<DVec2>) -> Item<Vec2Components> {
|
||||
let vec2 = vec2.into_element();
|
||||
|
||||
Item::new_from_element(Vec2Components { x: vec2.x, y: vec2.y })
|
||||
}
|
||||
|
||||
@@ -105,22 +105,10 @@ fn gamma_correction<T: Adjust<Color>>(
|
||||
input
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster: Channels"), shader_node(PerPixelAdjust))]
|
||||
fn extract_channel<T: Adjust<Color>>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
Gradient,
|
||||
)]
|
||||
#[gpu_image]
|
||||
input: Item<T>,
|
||||
channel: Item<RedGreenBlueAlpha>,
|
||||
) -> Item<T> {
|
||||
let mut input = input;
|
||||
let channel = channel.into_element();
|
||||
|
||||
input.element_mut().adjust(|color| {
|
||||
/// Extracts one color channel as a grayscale image. Used internally by the `split_channels` node.
|
||||
#[cfg(feature = "std")]
|
||||
fn extract_channel<T: Adjust<Color>>(mut input: T, channel: RedGreenBlueAlpha) -> T {
|
||||
input.adjust(|color| {
|
||||
let extracted_value = match channel {
|
||||
RedGreenBlueAlpha::Red => color.r(),
|
||||
RedGreenBlueAlpha::Green => color.g(),
|
||||
@@ -132,6 +120,37 @@ fn extract_channel<T: Adjust<Color>>(
|
||||
input
|
||||
}
|
||||
|
||||
/// The red, green, blue, and alpha channels of an image, split into separate node outputs.
|
||||
#[cfg(feature = "std")]
|
||||
#[node_macro::destructure]
|
||||
#[derive(Debug, Clone, dyn_any::DynAny)]
|
||||
pub struct ImageChannels {
|
||||
/// The red channel of the image, as a grayscale image.
|
||||
pub red: Raster<CPU>,
|
||||
/// The green channel of the image, as a grayscale image.
|
||||
pub green: Raster<CPU>,
|
||||
/// The blue channel of the image, as a grayscale image.
|
||||
pub blue: Raster<CPU>,
|
||||
/// The alpha channel of the image, as a grayscale image.
|
||||
pub alpha: Raster<CPU>,
|
||||
}
|
||||
|
||||
/// Separates an image into its red, green, blue, and alpha channels, each provided as a grayscale image.
|
||||
#[cfg(feature = "std")]
|
||||
#[node_macro::node(name("Split Channels"), category("Raster: Channels"))]
|
||||
fn split_channels(_: impl Ctx, image: Item<Raster<CPU>>) -> Item<ImageChannels> {
|
||||
let (image, attributes) = image.into_parts();
|
||||
|
||||
let channels = ImageChannels {
|
||||
red: extract_channel(image.clone(), RedGreenBlueAlpha::Red),
|
||||
green: extract_channel(image.clone(), RedGreenBlueAlpha::Green),
|
||||
blue: extract_channel(image.clone(), RedGreenBlueAlpha::Blue),
|
||||
alpha: extract_channel(image, RedGreenBlueAlpha::Alpha),
|
||||
};
|
||||
|
||||
Item::from_parts(channels, attributes)
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster: Channels"), shader_node(PerPixelAdjust))]
|
||||
fn make_opaque<T: Adjust<Color>>(
|
||||
_: impl Ctx,
|
||||
|
||||
@@ -238,7 +238,6 @@ mod test {
|
||||
use core_types::transform::Footprint;
|
||||
use glam::DVec2;
|
||||
use graphene_core::ReadPositionNode;
|
||||
use graphene_core::extract_xy::{ExtractXyNode, XY};
|
||||
use graphic_types::Vector;
|
||||
use kurbo::Shape;
|
||||
use kurbo::{BezPath, DEFAULT_ACCURACY, Rect};
|
||||
@@ -278,15 +277,27 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
/// Test helper that extracts the Y component of an upstream node's `Item<DVec2>` output.
|
||||
#[derive(Clone)]
|
||||
struct ExtractYNode<Position>(Position);
|
||||
|
||||
impl<'i, I: Ctx, Position> Node<'i, I> for ExtractYNode<Position>
|
||||
where
|
||||
Position: Node<'i, I, Output = Pin<Box<dyn Future<Output = Item<DVec2>> + 'i + Send>>>,
|
||||
{
|
||||
type Output = Pin<Box<dyn Future<Output = Item<f64>> + 'i + Send>>;
|
||||
fn eval(&'i self, input: I) -> Self::Output {
|
||||
let position = self.0.eval(input);
|
||||
Box::pin(async move { Item::new_from_element(position.await.element().y) })
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn repeat_on_points_test() {
|
||||
let context = OwnedContextImpl::default().into_context();
|
||||
let rect = RectangleNode::new(
|
||||
FutureWrapperNode(()),
|
||||
ExtractXyNode::new(
|
||||
ReadPositionNode::new(FutureWrapperNode(()), FutureWrapperNode(Item::new_from_element(0_u32))),
|
||||
FutureWrapperNode(Item::new_from_element(XY::Y)),
|
||||
),
|
||||
ExtractYNode(ReadPositionNode::new(FutureWrapperNode(()), FutureWrapperNode(Item::new_from_element(0_u32)))),
|
||||
FutureWrapperNode(Item::new_from_element(2_f64)),
|
||||
FutureWrapperNode(Item::new_from_element(BoxCorners::default())),
|
||||
FutureWrapperNode(Item::new_from_element(false)),
|
||||
|
||||
@@ -1974,48 +1974,22 @@ async fn cut_segments(_: impl Ctx, content: Item<Vector>) -> Item<Vector> {
|
||||
content
|
||||
}
|
||||
|
||||
/// Determines the position of a point on the path, given by its progression from 0 to 1 along the path.
|
||||
///
|
||||
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
|
||||
#[node_macro::node(name("Position on Path"), category("Vector: Measure"), path(graphene_core::vector))]
|
||||
async fn position_on_path(
|
||||
_: impl Ctx,
|
||||
/// The path to traverse.
|
||||
content: Item<Vector>,
|
||||
/// The factor from the start to the end of the path, 0–1 for one subpath, 1–2 for a second subpath, and so on.
|
||||
progression: Item<Progression>,
|
||||
/// Swap the direction of the path.
|
||||
reverse: Item<bool>,
|
||||
/// Traverse the path using each segment's Bézier curve parameterization instead of the Euclidean distance. Faster to compute but doesn't respect actual distances.
|
||||
parameterized_distance: Item<bool>,
|
||||
) -> Item<DVec2> {
|
||||
let (progression, reverse, parameterized_distance) = (progression.into_element(), reverse.into_element(), parameterized_distance.into_element());
|
||||
let euclidian = !parameterized_distance;
|
||||
|
||||
let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||
let mut bezpaths: Vec<_> = content.element().stroke_bezpath_iter().map(|bezpath| (bezpath, transform)).collect();
|
||||
let bezpath_count = bezpaths.len() as f64;
|
||||
let progression = progression.clamp(0., bezpath_count);
|
||||
let progression = if reverse { bezpath_count - progression } else { progression };
|
||||
let index = if progression >= bezpath_count { (bezpath_count - 1.) as usize } else { progression as usize };
|
||||
|
||||
let position = bezpaths.get_mut(index).map_or(DVec2::ZERO, |(bezpath, transform)| {
|
||||
let t = if progression == bezpath_count { 1. } else { progression.fract() };
|
||||
let t = if euclidian { TValue::Euclidean(t) } else { TValue::Parametric(t) };
|
||||
|
||||
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
|
||||
|
||||
point_to_dvec2(evaluate_bezpath(bezpath, t, None))
|
||||
});
|
||||
|
||||
Item::new_from_element(position)
|
||||
/// The position and tangent angle at a point along a path, split into separate node outputs.
|
||||
#[node_macro::destructure]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, dyn_any::DynAny)]
|
||||
pub struct PathEvaluation {
|
||||
/// The position of the point on the path.
|
||||
#[primary]
|
||||
position: DVec2,
|
||||
/// The angle of the tangent at the point on the path.
|
||||
tangent: f64,
|
||||
}
|
||||
|
||||
/// Determines the angle of the tangent at a point on the path, given by its progression from 0 to 1 along the path.
|
||||
/// Determines the position and tangent angle at a point on the path, given by its progression from 0 to 1 along the path.
|
||||
///
|
||||
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
|
||||
#[node_macro::node(name("Tangent on Path"), category("Vector: Measure"), path(graphene_core::vector))]
|
||||
async fn tangent_on_path(
|
||||
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
|
||||
async fn evaluate_path(
|
||||
_: impl Ctx,
|
||||
/// The path to traverse.
|
||||
content: Item<Vector>,
|
||||
@@ -2025,9 +1999,9 @@ async fn tangent_on_path(
|
||||
reverse: Item<bool>,
|
||||
/// Traverse the path using each segment's Bézier curve parameterization instead of the Euclidean distance. Faster to compute but doesn't respect actual distances.
|
||||
parameterized_distance: Item<bool>,
|
||||
/// Whether the resulting angle should be given in as radians instead of degrees.
|
||||
/// Whether the resulting tangent angle should be given in radians instead of degrees.
|
||||
radians: Item<bool>,
|
||||
) -> Item<f64> {
|
||||
) -> Item<PathEvaluation> {
|
||||
let (progression, reverse, parameterized_distance, radians) = (progression.into_element(), reverse.into_element(), parameterized_distance.into_element(), radians.into_element());
|
||||
let euclidian = !parameterized_distance;
|
||||
|
||||
@@ -2038,25 +2012,31 @@ async fn tangent_on_path(
|
||||
let progression = if reverse { bezpath_count - progression } else { progression };
|
||||
let index = if progression >= bezpath_count { (bezpath_count - 1.) as usize } else { progression as usize };
|
||||
|
||||
let angle = bezpaths.get_mut(index).map_or(0., |(bezpath, transform)| {
|
||||
let t = if progression == bezpath_count { 1. } else { progression.fract() };
|
||||
let t_value = |t: f64| if euclidian { TValue::Euclidean(t) } else { TValue::Parametric(t) };
|
||||
let Some((bezpath, transform)) = bezpaths.get_mut(index) else {
|
||||
return Item::new_from_element(PathEvaluation { position: DVec2::ZERO, tangent: 0. });
|
||||
};
|
||||
|
||||
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
|
||||
let t = if progression == bezpath_count { 1. } else { progression.fract() };
|
||||
let t_value = |t: f64| if euclidian { TValue::Euclidean(t) } else { TValue::Parametric(t) };
|
||||
|
||||
let mut tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t_value(t), None));
|
||||
if tangent == DVec2::ZERO {
|
||||
let t = t + if t > 0.5 { -0.001 } else { 0.001 };
|
||||
tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t_value(t), None));
|
||||
}
|
||||
if tangent == DVec2::ZERO {
|
||||
return 0.;
|
||||
}
|
||||
// Apply the transform once so both the position and tangent are computed on the transformed path
|
||||
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
|
||||
|
||||
let position = point_to_dvec2(evaluate_bezpath(bezpath, t_value(t), None));
|
||||
|
||||
let mut tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t_value(t), None));
|
||||
if tangent == DVec2::ZERO {
|
||||
let t = t + if t > 0.5 { -0.001 } else { 0.001 };
|
||||
tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t_value(t), None));
|
||||
}
|
||||
let angle = if tangent == DVec2::ZERO {
|
||||
0.
|
||||
} else {
|
||||
-tangent.angle_to(if reverse { -DVec2::X } else { DVec2::X })
|
||||
});
|
||||
};
|
||||
let tangent = if radians { angle } else { angle.to_degrees() };
|
||||
|
||||
Item::new_from_element(if radians { angle } else { angle.to_degrees() })
|
||||
Item::new_from_element(PathEvaluation { position, tangent })
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), memoize)]
|
||||
@@ -2527,7 +2507,7 @@ async fn morph<I: IntoGraphicList>(
|
||||
if paths.is_empty() { default_polyline() } else { paths }
|
||||
};
|
||||
|
||||
// Select which subpath to use based on the integer part of progression (like the 'Position on Path' node)
|
||||
// Select which subpath to use based on the integer part of progression (like the 'Evaluate Path' node)
|
||||
let progression = progression.max(0.);
|
||||
let subpath_count = control_bezpaths.len() as f64;
|
||||
let progression = if reverse { subpath_count - progression } else { progression };
|
||||
|
||||
Reference in New Issue
Block a user