diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 18919edd68..68a18fbe5b 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1673,7 +1673,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], // async fn morph(_: impl Ctx, source: Table, #[expose] target: Table, #[default(0.5)] time: Fraction) -> Table { ... } // // 4 inputs - even older signature (commit 80b8df8d4298b6669f124b929ce61bfabfc44e41): - // async fn morph(_: impl Ctx, source: Table, #[expose] target: Table, #[default(0.5)] time: Fraction, #[min(0.)] start_index: IntegerCount) -> Table { ... } + // async fn morph(_: impl Ctx, source: Table, #[expose] target: Table, #[default(0.5)] time: Fraction, start_index: u32) -> Table { ... } // // v2 signature: // async fn morph(_: impl Ctx, #[implementations(Table, Table)] content: I, progression: Progression) -> Table { ... } diff --git a/node-graph/libraries/no-std-types/src/registry.rs b/node-graph/libraries/no-std-types/src/registry.rs index 9fde16ff77..1f12a428be 100644 --- a/node-graph/libraries/no-std-types/src/registry.rs +++ b/node-graph/libraries/no-std-types/src/registry.rs @@ -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 diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index e612ca575a..ac140b85da 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -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; @@ -123,6 +123,60 @@ 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::().unwrap_or_default(), + NumberBoundLiteral::Int(lit) => lit.base10_parse::().unwrap_or_default() as f64, + }; + if self.is_negative { -magnitude } else { magnitude } + } +} + +impl Parse for NumberBound { + fn parse(input: ParseStream) -> syn::Result { + 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))), + } + } +} + /// 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)] @@ -130,10 +184,10 @@ pub struct RegularParsedField { pub ty: Type, pub exposed: bool, pub value_source: ParsedValueSource, - pub number_soft_min: Option, - pub number_soft_max: Option, - pub number_hard_min: Option, - pub number_hard_max: Option, + pub number_soft_min: Option, + pub number_soft_max: Option, + pub number_hard_min: Option, + pub number_hard_max: Option, pub number_mode_range: Option, pub implementations: Punctuated, pub gpu_image: bool, @@ -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 { @@ -769,6 +846,15 @@ fn parse_node_type(ty: &Type) -> (bool, Option, Option) { (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 { match output { ReturnType::Default => Ok(syn::parse_quote!(())), diff --git a/node-graph/node-macro/src/validation.rs b/node-graph/node-macro/src/validation.rs index 8f8a3e3716..79ad327910 100644 --- a/node-graph/node-macro/src/validation.rs +++ b/node-graph/node-macro/src/validation.rs @@ -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(), @@ -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(), diff --git a/node-graph/nodes/raster/src/adjustments.rs b/node-graph/nodes/raster/src/adjustments.rs index ea729094b7..2dd182c06a 100644 --- a/node-graph/nodes/raster/src/adjustments.rs +++ b/node-graph/nodes/raster/src/adjustments.rs @@ -962,7 +962,7 @@ fn posterize>( #[gpu_image] mut input: T, #[default(4)] - #[hard_min(2.)] + #[hard_min(2)] levels: u32, ) -> T { input.adjust(|color| { diff --git a/node-graph/nodes/raster/src/image_color_palette.rs b/node-graph/nodes/raster/src/image_color_palette.rs index ef20170591..8a75d5d915 100644 --- a/node-graph/nodes/raster/src/image_color_palette.rs +++ b/node-graph/nodes/raster/src/image_color_palette.rs @@ -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>, #[default(4)] count: IntegerCount) -> Table { +async fn image_color_palette( + _: impl Ctx, + image: Table>, + #[default(4)] + #[hard_min(1)] + count: u32, +) -> Table { const GRID: f32 = 3.; let bins = GRID * GRID * GRID; diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index 466f66152b..c06e5aa716 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -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}; @@ -19,7 +19,9 @@ async fn repeat + Default + Send + Clone + 'static>( Context -> Table, )] instance: impl Node<'n, Context<'static>, Output = Table>, - #[default(1)] count: u64, + #[default(1)] + #[hard_min(1)] + count: u32, reverse: bool, ) -> Table { // Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`). @@ -57,7 +59,9 @@ pub async fn repeat_array + 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 { let angle = angle.to_radians(); let count = count.max(1); @@ -102,7 +106,9 @@ async fn repeat_radial + Default + Send + Clone + 'static>( #[unit(" px")] #[default(5)] radius: f64, - #[default(5)] count: IntegerCount, + #[default(5)] + #[hard_min(1)] + count: u32, ) -> Table { let count = count.max(1);