Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions node-graph/gbrush/src/brush.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,23 @@ pub fn blend_with_mode(background: TableRow<Raster<CPU>>, foreground: TableRow<R
}
}

/// Generates the brush strokes painted with the Brush tool as a raster image.
/// If an input image is supplied, strokes are drawn on top of it, expanding bounds as needed.
#[node_macro::node(category("Raster"))]
async fn brush(_: impl Ctx, mut image_frame_table: Table<Raster<CPU>>, strokes: Vec<BrushStroke>, cache: BrushCache) -> Table<Raster<CPU>> {
if image_frame_table.is_empty() {
image_frame_table.push(TableRow::default());
async fn brush(
_: impl Ctx,
/// Optional raster content that may be drawn onto.
mut image: Table<Raster<CPU>>,
/// The list of brush stroke paths drawn by the Brush tool, with each including both its coordinates and styles.
strokes: Vec<BrushStroke>,
/// Internal cache data used to accelerate rendering of the brush content.
cache: BrushCache,
) -> Table<Raster<CPU>> {
if image.is_empty() {
image.push(TableRow::default());
}
// TODO: Find a way to handle more than one row
let table_row = image_frame_table.iter().next().expect("Expected the one row we just pushed").into_cloned();
let table_row = image.iter().next().expect("Expected the one row we just pushed").into_cloned();

let bounds = Table::new_from_row(table_row.clone()).bounding_box(DAffine2::IDENTITY, false);
let [start, end] = if let RenderBoundingBox::Rectangle(rect) = bounds { rect } else { [DVec2::ZERO, DVec2::ZERO] };
Expand Down Expand Up @@ -296,13 +306,13 @@ async fn brush(_: impl Ctx, mut image_frame_table: Table<Raster<CPU>>, strokes:
actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_params.eval((a, b)));
}

let first_row = image_frame_table.iter_mut().next().unwrap();
let first_row = image.iter_mut().next().unwrap();
*first_row.element = actual_image.element;
*first_row.transform = actual_image.transform;
*first_row.alpha_blending = actual_image.alpha_blending;
*first_row.source_node_id = actual_image.source_node_id;

image_frame_table
image
}

pub fn blend_image_closure(foreground: TableRow<Raster<CPU>>, mut background: TableRow<Raster<CPU>>, map_fn: impl Fn(Color, Color) -> Color) -> TableRow<Raster<CPU>> {
Expand Down
18 changes: 11 additions & 7 deletions node-graph/gcore/src/animation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,19 @@ pub enum AnimationTimeMode {

/// Produces a chosen representation of the current real time and date (in UTC) based on the system clock.
#[node_macro::node(category("Animation"))]
fn real_time(ctx: impl Ctx + ExtractRealTime, _primary: (), mode: RealTimeMode) -> f64 {
fn real_time(
ctx: impl Ctx + ExtractRealTime,
_primary: (),
/// The time and date component to be produced as a number.
component: RealTimeMode,
) -> f64 {
let real_time = ctx.try_real_time().unwrap_or_default();
// TODO: Implement proper conversion using and existing time implementation
match mode {
match component {
RealTimeMode::Utc => real_time,
RealTimeMode::Year => (real_time / DAY / 365.25).floor() + 1970.,
RealTimeMode::Hour => (real_time / 1000. / 3600.).floor() % 24., // TODO: Factor in a chosen timezone
RealTimeMode::Minute => (real_time / 1000. / 60.).floor() % 60., // TODO: Factor in a chosen timezone
RealTimeMode::Year => (real_time / DAY / 365.25).floor() + 1970., // TODO: Factor in a chosen timezone
RealTimeMode::Hour => (real_time / 1000. / 3600.).floor() % 24., // TODO: Factor in a chosen timezone
RealTimeMode::Minute => (real_time / 1000. / 60.).floor() % 60., // TODO: Factor in a chosen timezone

RealTimeMode::Second => (real_time / 1000.).floor() % 60.,
RealTimeMode::Millisecond => real_time % 1000.,
Expand All @@ -42,8 +47,7 @@ fn animation_time(ctx: impl Ctx + ExtractAnimationTime) -> f64 {
ctx.try_animation_time().unwrap_or_default()
}

// These nodes require more sophisticated algorithms for giving the correct result

// TODO: These nodes require more sophisticated algorithms for giving the correct result
// #[node_macro::node(category("Animation"))]
// fn month(ctx: impl Ctx + ExtractRealTime) -> f64 {
// ((ctx.try_real_time().unwrap_or_default() / DAY / 365.25 % 1.) * 12.).floor()
Expand Down
6 changes: 6 additions & 0 deletions node-graph/gcore/src/artboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re
#[node_macro::node(category(""))]
async fn create_artboard<T: Into<Table<Graphic>> + 'n>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
/// Graphics to include within the artboard.
#[implementations(
Context -> Table<Graphic>,
Context -> Table<Vector>,
Expand All @@ -124,10 +125,15 @@ async fn create_artboard<T: Into<Table<Graphic>> + 'n>(
Context -> DAffine2,
)]
content: impl Node<Context<'static>, Output = T>,
/// Name of the artboard, shown in parts of the editor.
label: String,
/// Coordinate of the top-left corner of the artboard within the document.
location: DVec2,
/// Width and height of the artboard within the document. Only integers are valid.
dimensions: DVec2,
/// Color of the artboard background. Only positive integers are valid.
background: Table<Color>,
/// Whether to cut off the contained content that extends outside the artboard, or keep it visible.
clip: bool,
) -> Table<Artboard> {
let location = location.as_ivec2();
Expand Down
53 changes: 35 additions & 18 deletions node-graph/gcore/src/blending_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,61 +178,78 @@ impl SetClip for Table<GradientStops> {
#[node_macro::node(category("Style"))]
fn blend_mode<T: SetBlendMode>(
_: impl Ctx,
/// The layer stack that will be composited when rendering.
#[implementations(
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
)]
mut value: T,
mut content: T,
/// The choice of equation that controls how brightness and color blends between overlapping pixels.
blend_mode: BlendMode,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or TableRow<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value
content.set_blend_mode(blend_mode);
content
}

/// Modifies the opacity of the input graphics by multiplying the existing opacity by this percentage. This affects the transparency of the content (together with any above which is clipped to it).
/// Modifies the opacity of the input graphics by multiplying the existing opacity by this percentage.
/// This affects the transparency of the content (together with anything above which is clipped to it).
#[node_macro::node(category("Style"))]
fn opacity<T: MultiplyAlpha>(
_: impl Ctx,
/// The layer stack that will be composited when rendering.
#[implementations(
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
)]
mut value: T,
#[default(100.)] opacity: Percentage,
mut content: T,
/// How visible the content should be, including any content clipped to it.
/// Ranges from the default of 100% (fully opaque) to 0% (fully transparent).
#[default(100.)]
opacity: Percentage,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or TableRow<T>) rather than applying to each row in its own table, which produces the undesired result
value.multiply_alpha(opacity / 100.);
value
content.multiply_alpha(opacity / 100.);
content
}

/// Sets each of the blending properties at once. The blend mode determines how overlapping content is composited together. The opacity affects the transparency of the content (together with any above which is clipped to it). The fill affects the transparency of the content itself, without affecting that of content clipped to it. The clip property determines whether the content inherits the alpha of the content beneath it.
/// Sets each of the blending properties at once. The blend mode determines how overlapping content is composited together. The opacity affects the transparency of the content (together with anything above which is clipped to it). The fill affects the transparency of the content itself, without affecting that of content clipped to it. The clip property determines whether the content inherits the alpha of the content beneath it.
#[node_macro::node(category("Style"))]
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
_: impl Ctx,
/// The layer stack that will be composited when rendering.
#[implementations(
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
)]
mut value: T,
mut content: T,
/// The choice of equation that controls how brightness and color blends between overlapping pixels.
blend_mode: BlendMode,
#[default(100.)] opacity: Percentage,
#[default(100.)] fill: Percentage,
#[default(false)] clip: bool,
/// How visible the content should be, including any content clipped to it.
/// Ranges from the default of 100% (fully opaque) to 0% (fully transparent).
#[default(100.)]
opacity: Percentage,
/// How visible the content should be, independent of any content clipped to it.
/// Ranges from 0% (fully transparent) to 100% (fully opaque).
#[default(100.)]
fill: Percentage,
/// Whether the content inherits the alpha of the content beneath it.
#[default(false)]
clip: bool,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or TableRow<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value.multiply_alpha(opacity / 100.);
value.multiply_fill(fill / 100.);
value.set_clip(clip);
value
content.set_blend_mode(blend_mode);
content.multiply_alpha(opacity / 100.);
content.multiply_fill(fill / 100.);
content.set_clip(clip);
content
}
4 changes: 3 additions & 1 deletion node-graph/gcore/src/context_modification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ use core::f64;
use glam::{DAffine2, DVec2};
use graphene_core_shaders::color::Color;

/// Node for filtering components of the context based on the specified requirements.
/// Filters out what should be unused components of the context based on the specified requirements.
/// This node is inserted by the compiler to "zero out" unused context components.
#[node_macro::node(category("Internal"))]
async fn context_modification<T>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
/// The data to pass through, evaluated with the stripped down context.
#[implementations(
Context -> (),
Context -> bool,
Expand All @@ -42,6 +43,7 @@ async fn context_modification<T>(
Context -> GradientStops,
)]
value: impl Node<Context<'static>, Output = T>,
/// The parts of the context to keep when evaluating the input value. All other parts are nullified.
features_to_keep: ContextFeatures,
) -> T {
let new_context = OwnedContextImpl::from_flags(ctx, features_to_keep);
Expand Down
23 changes: 19 additions & 4 deletions node-graph/gcore/src/graphic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,14 +260,26 @@ impl BoundingBox for Graphic {
}
}

/// Performs internal editor record-keeping that enables tools to target this network's layer.
/// This node associates the ID of the network's parent layer to every element of output data.
/// This technical detail may be ignored by users, and will be phased out in the future.
#[node_macro::node(category(""))]
async fn source_node_id<I: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(Table<Artboard>, Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)] content: Table<I>,
#[implementations(
Table<Artboard>,
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
)]
content: Table<I>,
node_path: Vec<NodeId>,
) -> Table<I> {
// Get the penultimate element of the node path, or None if the path is too short
// This is used to get the ID of the user-facing parent layer-style node (which encapsulates this internal node).
// This is used to get the ID of the user-facing parent layer node (whose network contains this internal node).
let source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();

let mut content = content;
Expand Down Expand Up @@ -297,6 +309,8 @@ async fn extend<I: 'n + Send + Clone>(
}

// TODO: Eventually remove this document upgrade code
/// Performs an obsolete function as part of a migration from an older document format.
/// Users are advised to delete this node and replace it with a new one.
#[node_macro::node(category(""))]
async fn legacy_layer_extend<I: 'n + Send + Clone>(
_: impl Ctx,
Expand All @@ -318,7 +332,8 @@ async fn legacy_layer_extend<I: 'n + Send + Clone>(
base
}

/// Places a table of graphical content into an element of a new wrapper graphic table.
/// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input.
/// The inverse of this node is 'Flatten Graphic'.
#[node_macro::node(category("General"))]
async fn wrap_graphic<T: Into<Graphic> + 'n>(
_: impl Ctx,
Expand Down Expand Up @@ -441,7 +456,7 @@ async fn flatten_vector(_: impl Ctx, content: Table<Graphic>) -> Table<Vector> {
}

/// Returns the value at the specified index in the collection.
/// If that index has no value, the type's default value is returned.
/// If no value exists at that index, the type's default value is returned.
#[node_macro::node(category("General"))]
fn index_elements<T: AtIndex + Clone + Default>(
_: impl Ctx,
Expand Down
24 changes: 16 additions & 8 deletions node-graph/gcore/src/logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::vector::Vector;
use crate::{Context, Ctx};
use glam::{DAffine2, DVec2};

/// Type-asserts a value to be a string, so the automatic type conversion system can convert another type to a string.
/// Type-asserts a value to be a string.
#[node_macro::node(category("Debug"))]
fn to_string(_: impl Ctx, value: String) -> String {
value
Expand Down Expand Up @@ -69,8 +69,21 @@ fn string_length(_: impl Ctx, string: String) -> f64 {
string.chars().count() as f64
}

/// Splits a string into a list of substrings based on the specified delimeter.
/// For example, the delimeter "," will split "a,b,c" into the strings "a", "b", and "c".
#[node_macro::node(category("Text"))]
fn string_split(_: impl Ctx, string: String, #[default("\\n")] delimeter: String, #[default(true)] delimeter_escaping: bool) -> Vec<String> {
fn string_split(
_: impl Ctx,
/// The string to split into substrings.
string: String,
/// The character(s) that separate the substrings. These are not included in the outputs.
#[default("\\n")]
delimeter: String,
/// Whether to convert escape sequences found in the delimeter into their corresponding characters:
/// "\n" (newline), "\r" (carriage return), "\t" (tab), "\0" (null), and "\\" (backslash)
#[default(true)]
delimeter_escaping: bool,
) -> Vec<String> {
let delimeter = if delimeter_escaping {
delimeter.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\0", "\0").replace("\\\\", "\\")
} else {
Expand Down Expand Up @@ -124,10 +137,5 @@ async fn switch<T, C: Send + 'n + Clone>(
)]
if_false: impl Node<C, Output = T>,
) -> T {
if condition {
// We can't remove these calls because we only want to evaluate the branch that we actually need
if_true.eval(ctx).await
} else {
if_false.eval(ctx).await
}
if condition { if_true.eval(ctx).await } else { if_false.eval(ctx).await }
}
28 changes: 9 additions & 19 deletions node-graph/gcore/src/memo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,13 @@ use std::ops::Deref;
use std::sync::Arc;
use std::sync::Mutex;

/// Caches the output of a given Node and acts as a proxy
/// Caches the output of a given node called with a specific input.
///
/// ```text
/// ┌───────────────┐ ┌───────────────┐
/// │ │◄───┤ │◄─── EVAL (START)
/// │ CacheNode │ │ F │
/// │ ├───►│ │───► RESULT (END)
/// ┌───────────────┐ ├───────────────┤ └───────────────┘
/// │ │◄───┤ │
/// │ G │ │ Cached Data │
/// │ ├───►│ │
/// └───────────────┘ └───────────────┘
/// ```
/// A cache miss occurs when the Option is None. In this case, the node evaluates the inner node and memoizes (stores) the result.
///
/// The call from `F` directly reaches the `CacheNode` and the `CacheNode` can decide whether to call `G.eval(input_from_f)`
/// in the event of a cache miss or just return the cached data in the event of a cache hit.
/// A cache hit occurs when the Option is Some and has a stored hash matching the hash of the call argument. In this case, the node returns the cached value without re-evaluating the inner node.
///
/// Currently, only one input-output pair is cached. Subsequent calls with different inputs will overwrite the previous cache.
#[derive(Default)]
pub struct MemoNode<T, CachedNode> {
cache: Arc<Mutex<Option<(u64, T)>>>,
Expand Down Expand Up @@ -71,9 +62,8 @@ pub mod memo {
}

/// Caches the output of a given Node and acts as a proxy.
/// In contrast to the regular `MemoNode`. This node ignores all input.
/// Using this node might result in the document not updating properly,
/// use with caution.
/// In contrast to the regular `MemoNode`, this variant ignores all input.
/// This node might result in the document not updating properly. Use with caution!
#[derive(Default)]
pub struct ImpureMemoNode<I, T, CachedNode> {
cache: Arc<Mutex<Option<T>>>,
Expand All @@ -86,8 +76,8 @@ where
CachedNode: for<'any_input> Node<'any_input, I>,
for<'a> <CachedNode as Node<'a, I>>::Output: Future<Output = T> + WasmNotSend,
{
// TODO: This should return a reference to the cached cached_value
// but that requires a lot of lifetime magic <- This was suggested by copilot but is pretty accurate xD
// TODO: This should return a reference to the cached cached_value but that requires a lot of lifetime magic
// TODO: (This was suggested by copilot but is pretty accurate xD)
type Output = DynFuture<'i, T>;
fn eval(&'i self, input: I) -> Self::Output {
if let Some(cached_value) = self.cache.lock().as_ref().unwrap().deref() {
Expand Down
3 changes: 2 additions & 1 deletion node-graph/gcore/src/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use crate::{ExtractFootprint, Node, transform::Footprint};
use std::marker::PhantomData;

// TODO: Rename to "Passthrough"
/// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes.
/// Passes-through the input value without changing it.
/// This is useful for rerouting wires for organization purposes.
#[node_macro::node(skip_impl)]
fn identity<'i, T: 'i + Send>(value: T) -> T {
value
Expand Down
Loading
Loading