mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 17:38:12 +08:00
Improve type handling for ints/floats with the #[hard/soft_min/max(...)] node macro parameter attributes (#4041)
This commit is contained in:
@@ -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> { ... }
|
// 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):
|
// 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:
|
// v2 signature:
|
||||||
// async fn morph<I: IntoGraphicTable>(_: impl Ctx, #[implementations(Table<Graphic>, Table<Vector>)] content: I, progression: Progression) -> Table<Vector> { ... }
|
// 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;
|
pub type Progression = f64;
|
||||||
/// Signed integer that's actually a float because we don't handle type conversions very well yet
|
/// Signed integer that's actually a float because we don't handle type conversions very well yet
|
||||||
pub type SignedInteger = f64;
|
pub type SignedInteger = f64;
|
||||||
/// Unsigned integer
|
|
||||||
pub type IntegerCount = u32;
|
|
||||||
/// Unsigned integer to be used for random seeds
|
/// Unsigned integer to be used for random seeds
|
||||||
pub type SeedValue = u32;
|
pub type SeedValue = u32;
|
||||||
/// DVec2 with px unit
|
/// DVec2 with px unit
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use indoc::{formatdoc, indoc};
|
use indoc::{formatdoc, indoc};
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
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::parse::{Parse, ParseStream, Parser};
|
||||||
use syn::punctuated::Punctuated;
|
use syn::punctuated::Punctuated;
|
||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
@@ -123,6 +123,60 @@ pub enum ParsedFieldType {
|
|||||||
Node(NodeParsedField),
|
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
|
/// a param of any kind, either a concrete type or a generic type with a set of possible types specified via
|
||||||
/// `#[implementation(type)]`
|
/// `#[implementation(type)]`
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -130,10 +184,10 @@ pub struct RegularParsedField {
|
|||||||
pub ty: Type,
|
pub ty: Type,
|
||||||
pub exposed: bool,
|
pub exposed: bool,
|
||||||
pub value_source: ParsedValueSource,
|
pub value_source: ParsedValueSource,
|
||||||
pub number_soft_min: Option<LitFloat>,
|
pub number_soft_min: Option<NumberBound>,
|
||||||
pub number_soft_max: Option<LitFloat>,
|
pub number_soft_max: Option<NumberBound>,
|
||||||
pub number_hard_min: Option<LitFloat>,
|
pub number_hard_min: Option<NumberBound>,
|
||||||
pub number_hard_max: Option<LitFloat>,
|
pub number_hard_max: Option<NumberBound>,
|
||||||
pub number_mode_range: Option<ExprTuple>,
|
pub number_mode_range: Option<ExprTuple>,
|
||||||
pub implementations: Punctuated<Type, Comma>,
|
pub implementations: Punctuated<Type, Comma>,
|
||||||
pub gpu_image: bool,
|
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))
|
.map(|attr| parse_implementations(attr, ident))
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.unwrap_or_default();
|
.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 {
|
Ok(ParsedField {
|
||||||
pat_ident,
|
pat_ident,
|
||||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
ty: ParsedFieldType::Regular(RegularParsedField {
|
||||||
@@ -769,6 +846,15 @@ fn parse_node_type(ty: &Type) -> (bool, Option<Type>, Option<Type>) {
|
|||||||
(false, None, None)
|
(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> {
|
fn parse_output(output: &ReturnType) -> syn::Result<Type> {
|
||||||
match output {
|
match output {
|
||||||
ReturnType::Default => Ok(syn::parse_quote!(())),
|
ReturnType::Default => Ok(syn::parse_quote!(())),
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ fn validate_min_max(parsed: &ParsedNodeFn) {
|
|||||||
} = field
|
} = field
|
||||||
{
|
{
|
||||||
if let (Some(soft_min), Some(hard_min)) = (number_soft_min, number_hard_min) {
|
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 soft_min_value: f64 = soft_min.to_f64();
|
||||||
let hard_min_value: f64 = hard_min.base10_parse().unwrap_or_default();
|
let hard_min_value: f64 = hard_min.to_f64();
|
||||||
if soft_min_value == hard_min_value {
|
if soft_min_value == hard_min_value {
|
||||||
emit_error!(
|
emit_error!(
|
||||||
pat_ident.span(),
|
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) {
|
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 soft_max_value: f64 = soft_max.to_f64();
|
||||||
let hard_max_value: f64 = hard_max.base10_parse().unwrap_or_default();
|
let hard_max_value: f64 = hard_max.to_f64();
|
||||||
if soft_max_value == hard_max_value {
|
if soft_max_value == hard_max_value {
|
||||||
emit_error!(
|
emit_error!(
|
||||||
pat_ident.span(),
|
pat_ident.span(),
|
||||||
|
|||||||
@@ -962,7 +962,7 @@ fn posterize<T: Adjust<Color>>(
|
|||||||
#[gpu_image]
|
#[gpu_image]
|
||||||
mut input: T,
|
mut input: T,
|
||||||
#[default(4)]
|
#[default(4)]
|
||||||
#[hard_min(2.)]
|
#[hard_min(2)]
|
||||||
levels: u32,
|
levels: u32,
|
||||||
) -> T {
|
) -> T {
|
||||||
input.adjust(|color| {
|
input.adjust(|color| {
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
use core_types::color::Color;
|
use core_types::color::Color;
|
||||||
use core_types::context::Ctx;
|
use core_types::context::Ctx;
|
||||||
use core_types::registry::types::IntegerCount;
|
|
||||||
use core_types::table::{Table, TableRow};
|
use core_types::table::{Table, TableRow};
|
||||||
use raster_types::{CPU, Raster};
|
use raster_types::{CPU, Raster};
|
||||||
|
|
||||||
#[node_macro::node(category("Color"))]
|
#[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.;
|
const GRID: f32 = 3.;
|
||||||
|
|
||||||
let bins = GRID * GRID * GRID;
|
let bins = GRID * GRID * GRID;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::gcore::Context;
|
use crate::gcore::Context;
|
||||||
use core::f64::consts::TAU;
|
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::table::{Table, TableRowRef};
|
||||||
use core_types::{CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl};
|
use core_types::{CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
@@ -19,7 +19,9 @@ async fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
|||||||
Context -> Table<GradientStops>,
|
Context -> Table<GradientStops>,
|
||||||
)]
|
)]
|
||||||
instance: impl Node<'n, Context<'static>, Output = Table<T>>,
|
instance: impl Node<'n, Context<'static>, Output = Table<T>>,
|
||||||
#[default(1)] count: u64,
|
#[default(1)]
|
||||||
|
#[hard_min(1)]
|
||||||
|
count: u32,
|
||||||
reverse: bool,
|
reverse: bool,
|
||||||
) -> Table<T> {
|
) -> Table<T> {
|
||||||
// Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`).
|
// 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.
|
// 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,
|
direction: PixelSize,
|
||||||
angle: Angle,
|
angle: Angle,
|
||||||
#[default(5)] count: IntegerCount,
|
#[default(5)]
|
||||||
|
#[hard_min(1)]
|
||||||
|
count: u32,
|
||||||
) -> Table<T> {
|
) -> Table<T> {
|
||||||
let angle = angle.to_radians();
|
let angle = angle.to_radians();
|
||||||
let count = count.max(1);
|
let count = count.max(1);
|
||||||
@@ -102,7 +106,9 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
|||||||
#[unit(" px")]
|
#[unit(" px")]
|
||||||
#[default(5)]
|
#[default(5)]
|
||||||
radius: f64,
|
radius: f64,
|
||||||
#[default(5)] count: IntegerCount,
|
#[default(5)]
|
||||||
|
#[hard_min(1)]
|
||||||
|
count: u32,
|
||||||
) -> Table<T> {
|
) -> Table<T> {
|
||||||
let count = count.max(1);
|
let count = count.max(1);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user