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
2 changes: 1 addition & 1 deletion editor/src/messages/portfolio/document_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1673,7 +1673,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
// async fn morph(_: impl Ctx, source: Table<Vector>, #[expose] target: Table<Vector>, #[default(0.5)] time: Fraction) -> Table<Vector> { ... }
//
// 4 inputs - even older signature (commit 80b8df8d4298b6669f124b929ce61bfabfc44e41):
// async fn morph(_: impl Ctx, source: Table<Vector>, #[expose] target: Table<Vector>, #[default(0.5)] time: Fraction, #[min(0.)] start_index: IntegerCount) -> Table<Vector> { ... }
// async fn morph(_: impl Ctx, source: Table<Vector>, #[expose] target: Table<Vector>, #[default(0.5)] time: Fraction, start_index: u32) -> Table<Vector> { ... }
//
// v2 signature:
// async fn morph<I: IntoGraphicTable>(_: impl Ctx, #[implementations(Table<Graphic>, Table<Vector>)] content: I, progression: Progression) -> Table<Vector> { ... }
Expand Down
2 changes: 0 additions & 2 deletions node-graph/libraries/no-std-types/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ pub mod types {
pub type Progression = 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
pub type SeedValue = u32;
/// DVec2 with px unit
Expand Down
96 changes: 91 additions & 5 deletions node-graph/node-macro/src/parsing.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use convert_case::{Case, Casing};
use indoc::{formatdoc, indoc};
use proc_macro2::TokenStream as TokenStream2;
use quote::{ToTokens, format_ident};
use quote::{ToTokens, format_ident, quote};
use syn::parse::{Parse, ParseStream, Parser};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
Expand Down Expand Up @@ -123,17 +123,71 @@ pub enum ParsedFieldType {
Node(NodeParsedField),
}

/// A numeric bound value accepted by attributes like `#[soft_min]`, `#[hard_min]`, `#[soft_max]`, and `#[hard_max]`.
/// Accepts both integer literals (e.g. `1`, `-1`) and float literals (e.g. `1.`, `-500.`).
#[derive(Clone, Debug)]
pub struct NumberBound {
is_negative: bool,
literal: NumberBoundLiteral,
}

#[derive(Clone, Debug)]
enum NumberBoundLiteral {
Float(LitFloat),
Int(LitInt),
}

impl NumberBound {
pub fn to_f64(&self) -> f64 {
let magnitude = match &self.literal {
NumberBoundLiteral::Float(lit) => lit.base10_parse::<f64>().unwrap_or_default(),
NumberBoundLiteral::Int(lit) => lit.base10_parse::<u64>().unwrap_or_default() as f64,
Comment thread
Keavon marked this conversation as resolved.
};
if self.is_negative { -magnitude } else { magnitude }
}
Comment thread
Keavon marked this conversation as resolved.
}

impl Parse for NumberBound {
fn parse(input: ParseStream) -> syn::Result<Self> {
let is_negative = input.peek(syn::Token![-]);
if is_negative {
let _: syn::Token![-] = input.parse()?;
}

let literal = if input.peek(LitFloat) {
NumberBoundLiteral::Float(input.parse()?)
} else if input.peek(LitInt) {
NumberBoundLiteral::Int(input.parse()?)
} else {
return Err(input.error("expected a numeric literal (integer or float)"));
};

Ok(NumberBound { is_negative, literal })
}
}

impl ToTokens for NumberBound {
fn to_tokens(&self, stream: &mut TokenStream2) {
match (&self.literal, self.is_negative) {
(NumberBoundLiteral::Float(lit), false) => lit.to_tokens(stream),
(NumberBoundLiteral::Float(lit), true) => stream.extend(quote!(-#lit)),
(NumberBoundLiteral::Int(lit), false) => stream.extend(quote!(#lit as f64)),
(NumberBoundLiteral::Int(lit), true) => stream.extend(quote!(-(#lit as f64))),
}
}
Comment thread
Keavon marked this conversation as resolved.
}

/// a param of any kind, either a concrete type or a generic type with a set of possible types specified via
/// `#[implementation(type)]`
#[derive(Clone, Debug)]
pub struct RegularParsedField {
pub ty: Type,
pub exposed: bool,
pub value_source: ParsedValueSource,
pub number_soft_min: Option<LitFloat>,
pub number_soft_max: Option<LitFloat>,
pub number_hard_min: Option<LitFloat>,
pub number_hard_max: Option<LitFloat>,
pub number_soft_min: Option<NumberBound>,
pub number_soft_max: Option<NumberBound>,
pub number_hard_min: Option<NumberBound>,
pub number_hard_max: Option<NumberBound>,
pub number_mode_range: Option<ExprTuple>,
pub implementations: Punctuated<Type, Comma>,
pub gpu_image: bool,
Expand Down Expand Up @@ -722,6 +776,29 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
.map(|attr| parse_implementations(attr, ident))
.transpose()?
.unwrap_or_default();

// Error if a float literal is given for a bound attribute on an integer-typed field
if is_integer_type(&ty) {
let bound_attrs = [
(&number_soft_min, "soft_min"),
(&number_hard_min, "hard_min"),
(&number_soft_max, "soft_max"),
(&number_hard_max, "hard_max"),
];
for (bound, attr_name) in bound_attrs {
if let Some(NumberBound {
literal: NumberBoundLiteral::Float(_),
..
}) = bound
{
return Err(Error::new_spanned(
&pat_ident,
format!("Attribute `#[{attr_name}]` on `{ident}` has a float literal, but `{ident}` is an integer type. Use an integer literal without a decimal point."),
));
}
}
}

Ok(ParsedField {
pat_ident,
ty: ParsedFieldType::Regular(RegularParsedField {
Expand Down Expand Up @@ -769,6 +846,15 @@ fn parse_node_type(ty: &Type) -> (bool, Option<Type>, Option<Type>) {
(false, None, None)
}

fn is_integer_type(ty: &Type) -> bool {
let Type::Path(type_path) = ty else { return false };
let Some(segment) = type_path.path.segments.last() else { return false };
matches!(
segment.ident.to_string().as_str(),
"u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64" | "i128" | "isize"
)
}

fn parse_output(output: &ReturnType) -> syn::Result<Type> {
match output {
ReturnType::Default => Ok(syn::parse_quote!(())),
Expand Down
8 changes: 4 additions & 4 deletions node-graph/node-macro/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ fn validate_min_max(parsed: &ParsedNodeFn) {
} = field
{
if let (Some(soft_min), Some(hard_min)) = (number_soft_min, number_hard_min) {
let soft_min_value: f64 = soft_min.base10_parse().unwrap_or_default();
let hard_min_value: f64 = hard_min.base10_parse().unwrap_or_default();
let soft_min_value: f64 = soft_min.to_f64();
let hard_min_value: f64 = hard_min.to_f64();
if soft_min_value == hard_min_value {
emit_error!(
pat_ident.span(),
Expand All @@ -56,8 +56,8 @@ fn validate_min_max(parsed: &ParsedNodeFn) {
}

if let (Some(soft_max), Some(hard_max)) = (number_soft_max, number_hard_max) {
let soft_max_value: f64 = soft_max.base10_parse().unwrap_or_default();
let hard_max_value: f64 = hard_max.base10_parse().unwrap_or_default();
let soft_max_value: f64 = soft_max.to_f64();
let hard_max_value: f64 = hard_max.to_f64();
if soft_max_value == hard_max_value {
emit_error!(
pat_ident.span(),
Expand Down
2 changes: 1 addition & 1 deletion node-graph/nodes/raster/src/adjustments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,7 @@ fn posterize<T: Adjust<Color>>(
#[gpu_image]
mut input: T,
#[default(4)]
#[hard_min(2.)]
#[hard_min(2)]
levels: u32,
) -> T {
input.adjust(|color| {
Expand Down
9 changes: 7 additions & 2 deletions node-graph/nodes/raster/src/image_color_palette.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
use core_types::color::Color;
use core_types::context::Ctx;
use core_types::registry::types::IntegerCount;
use core_types::table::{Table, TableRow};
use raster_types::{CPU, Raster};

#[node_macro::node(category("Color"))]
async fn image_color_palette(_: impl Ctx, image: Table<Raster<CPU>>, #[default(4)] count: IntegerCount) -> Table<Color> {
async fn image_color_palette(
_: impl Ctx,
image: Table<Raster<CPU>>,
#[default(4)]
#[hard_min(1)]
count: u32,
) -> Table<Color> {
const GRID: f32 = 3.;

let bins = GRID * GRID * GRID;
Expand Down
14 changes: 10 additions & 4 deletions node-graph/nodes/repeat/src/repeat_nodes.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::gcore::Context;
use core::f64::consts::TAU;
use core_types::registry::types::{Angle, IntegerCount, PixelSize};
use core_types::registry::types::{Angle, PixelSize};
use core_types::table::{Table, TableRowRef};
use core_types::{CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl};
use glam::{DAffine2, DVec2};
Expand All @@ -19,7 +19,9 @@ async fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
Context -> Table<GradientStops>,
)]
instance: impl Node<'n, Context<'static>, Output = Table<T>>,
#[default(1)] count: u64,
#[default(1)]
#[hard_min(1)]
count: u32,
reverse: bool,
) -> Table<T> {
// Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`).
Expand Down Expand Up @@ -57,7 +59,9 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
// 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(5)] count: IntegerCount,
#[default(5)]
#[hard_min(1)]
count: u32,
) -> Table<T> {
let angle = angle.to_radians();
let count = count.max(1);
Expand Down Expand Up @@ -102,7 +106,9 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
#[unit(" px")]
#[default(5)]
radius: f64,
#[default(5)] count: IntegerCount,
#[default(5)]
#[hard_min(1)]
count: u32,
) -> Table<T> {
let count = count.max(1);

Expand Down
Loading