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:
Keavon Chambers
2026-07-07 03:14:51 -07:00
parent 3d7e85c054
commit 0fb36b68a4
17 changed files with 923 additions and 327 deletions

View File

@@ -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,