Improve type handling for ints/floats with the #[hard/soft_min/max(...)] node macro parameter attributes (#4041)
This commit is contained in:
parent
fcf9396a71
commit
f42d12da9e
|
|
@ -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> { ... }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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::<f64>().unwrap_or_default(),
|
||||
NumberBoundLiteral::Int(lit) => lit.base10_parse::<u64>().unwrap_or_default() as f64,
|
||||
};
|
||||
if self.is_negative { -magnitude } else { magnitude }
|
||||
}
|
||||
}
|
||||
|
||||
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))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<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,
|
||||
|
|
@ -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<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!(())),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<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`).
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue