mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Revamp the syntax to #[hard(a..b)] and #[soft(a..b)] bounds on node definitions (#4307)
* Revamp the syntax to #[hard(a..b)] and #[soft(a..b)] bounds on node definitions * Address review feedback: error on empty bounds ranges and remove redundant count clamps * Fix NaN transform from Repeat Array with a count of 1 * Remove redundant manual clamps already enforced by #[hard(...)] bounds
This commit is contained in:
@@ -156,7 +156,9 @@ async fn mirror<T: 'n + Send + Clone>(
|
||||
content: List<T>,
|
||||
#[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint,
|
||||
#[unit(" px")] offset: f64,
|
||||
#[range((-90., 90.))] angle: Angle,
|
||||
#[range]
|
||||
#[soft(-90..90)]
|
||||
angle: Angle,
|
||||
#[default(true)] keep_original: bool,
|
||||
) -> List<T>
|
||||
where
|
||||
|
||||
@@ -22,13 +22,13 @@ fn text(
|
||||
/// The font size used to draw the text.
|
||||
#[unit(" px")]
|
||||
#[default(24.)]
|
||||
#[hard_min(1.)]
|
||||
#[hard(1..)]
|
||||
size: f64,
|
||||
/// The line height ratio, relative to the font size. Each line is drawn lower than its previous line by the distance of *Size* × *Line Height*.
|
||||
///
|
||||
/// 0 means all lines overlap. 1 means all lines are spaced by just the font size. 1.2 is a common default for readable text. 2 means double-spaced text.
|
||||
#[unit("x")]
|
||||
#[hard_min(0.)]
|
||||
#[hard(0..)]
|
||||
#[step(0.1)]
|
||||
#[default(1.2)]
|
||||
line_height: f64,
|
||||
@@ -38,15 +38,14 @@ fn text(
|
||||
letter_spacing: f64,
|
||||
/// The angle of faux italic slant applied to each glyph.
|
||||
#[unit("°")]
|
||||
#[hard_min(-85.)]
|
||||
#[hard_max(85.)]
|
||||
#[hard(-85..85)]
|
||||
letter_tilt: f64,
|
||||
/// Enables the maximum width constraint so lines can wrap.
|
||||
#[widget(ParsedWidgetOverride::Hidden)]
|
||||
has_max_width: bool,
|
||||
/// The maximum width that the text block can occupy before wrapping to a new line. Otherwise, lines do not wrap.
|
||||
#[unit(" px")]
|
||||
#[hard_min(1.)]
|
||||
#[hard(1..)]
|
||||
#[widget(ParsedWidgetOverride::Custom = "optional_f64")]
|
||||
max_width: f64,
|
||||
/// Whether the *Max Height* property is enabled so that lines beyond it are not drawn.
|
||||
@@ -54,7 +53,7 @@ fn text(
|
||||
has_max_height: bool,
|
||||
/// The maximum height that the text block can occupy. Excess lines are not drawn.
|
||||
#[unit(" px")]
|
||||
#[hard_min(1.)]
|
||||
#[hard(1..)]
|
||||
#[widget(ParsedWidgetOverride::Custom = "optional_f64")]
|
||||
max_height: f64,
|
||||
/// The horizontal alignment of each line of text within its surrounding box. To have an effect on a single line of text, *Max Width* must be set.
|
||||
|
||||
@@ -85,8 +85,9 @@ fn gamma_correction<T: Adjust<Color>>(
|
||||
#[gpu_image]
|
||||
mut input: T,
|
||||
#[default(2.2)]
|
||||
#[range((0.01, 10.))]
|
||||
#[hard_min(0.0001)]
|
||||
#[range]
|
||||
#[hard(0.0001..)]
|
||||
#[soft(0.01..10)]
|
||||
gamma: f32,
|
||||
inverse: bool,
|
||||
) -> T {
|
||||
@@ -344,22 +345,28 @@ fn black_and_white<T: Adjust<Color>>(
|
||||
mut image: T,
|
||||
#[default(Color::BLACK)] tint: Color,
|
||||
#[default(40.)]
|
||||
#[range((-200., 300.))]
|
||||
#[range]
|
||||
#[soft(-200..300)]
|
||||
reds: PercentageF32,
|
||||
#[default(60.)]
|
||||
#[range((-200., 300.))]
|
||||
#[range]
|
||||
#[soft(-200..300)]
|
||||
yellows: PercentageF32,
|
||||
#[default(40.)]
|
||||
#[range((-200., 300.))]
|
||||
#[range]
|
||||
#[soft(-200..300)]
|
||||
greens: PercentageF32,
|
||||
#[default(60.)]
|
||||
#[range((-200., 300.))]
|
||||
#[range]
|
||||
#[soft(-200..300)]
|
||||
cyans: PercentageF32,
|
||||
#[default(20.)]
|
||||
#[range((-200., 300.))]
|
||||
#[range]
|
||||
#[soft(-200..300)]
|
||||
blues: PercentageF32,
|
||||
#[default(80.)]
|
||||
#[range((-200., 300.))]
|
||||
#[range]
|
||||
#[soft(-200..300)]
|
||||
magentas: PercentageF32,
|
||||
) -> T {
|
||||
image.adjust(|color| {
|
||||
@@ -997,12 +1004,11 @@ fn posterize<T: Adjust<Color>>(
|
||||
#[gpu_image]
|
||||
mut input: T,
|
||||
#[default(4)]
|
||||
#[hard_min(2)]
|
||||
#[hard(2..)]
|
||||
levels: u32,
|
||||
) -> T {
|
||||
let levels = levels as f32;
|
||||
input.adjust(|color| {
|
||||
// `hard_min(2)` constrains the widget but doesn't bind the data-flow input (a saved doc or upstream node could still feed 0 or 1, producing inf/NaN below).
|
||||
let levels = (levels as f32).max(2.);
|
||||
let number_of_areas = levels.recip();
|
||||
let size_of_areas = (levels - 1.).recip();
|
||||
color.map_gamma_rgb(|c| (c / number_of_areas).floor() * size_of_areas)
|
||||
@@ -1029,8 +1035,9 @@ fn exposure<T: Adjust<Color>>(
|
||||
exposure: f32,
|
||||
offset: f32,
|
||||
#[default(1.)]
|
||||
#[range((0.01, 10.))]
|
||||
#[hard_min(0.0001)]
|
||||
#[range]
|
||||
#[hard(0.0001..)]
|
||||
#[soft(0.01..10)]
|
||||
gamma_correction: f32,
|
||||
) -> T {
|
||||
input.adjust(|color| {
|
||||
|
||||
@@ -92,8 +92,9 @@ async fn blur(
|
||||
/// The image to be blurred.
|
||||
image_frame: List<Raster<CPU>>,
|
||||
/// The radius of the blur kernel.
|
||||
#[range((0., 100.))]
|
||||
#[hard_min(0.)]
|
||||
#[range]
|
||||
#[hard(0..)]
|
||||
#[soft(..100)]
|
||||
radius: PixelLength,
|
||||
/// Use a lower-quality box kernel instead of a circular Gaussian kernel. This is faster but produces boxy artifacts.
|
||||
box_blur: bool,
|
||||
@@ -128,8 +129,9 @@ async fn median_filter(
|
||||
/// The image to be filtered.
|
||||
image_frame: List<Raster<CPU>>,
|
||||
/// The radius of the filter kernel. Larger values remove more noise but may blur fine details.
|
||||
#[range((0., 50.))]
|
||||
#[hard_min(0.)]
|
||||
#[range]
|
||||
#[hard(0..)]
|
||||
#[soft(..50)]
|
||||
radius: PixelLength,
|
||||
) -> List<Raster<CPU>> {
|
||||
image_frame
|
||||
|
||||
@@ -8,7 +8,7 @@ async fn image_color_palette(
|
||||
_: impl Ctx,
|
||||
image: List<Raster<CPU>>,
|
||||
#[default(4)]
|
||||
#[hard_min(1)]
|
||||
#[hard(1..)]
|
||||
count: u32,
|
||||
) -> List<Color> {
|
||||
const GRID: f32 = 3.;
|
||||
|
||||
@@ -20,13 +20,13 @@ async fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
)]
|
||||
content: impl Node<'n, Context<'static>, Output = List<T>>,
|
||||
#[default(1)]
|
||||
#[hard_min(1)]
|
||||
#[hard(1..)]
|
||||
count: u32,
|
||||
reverse: bool,
|
||||
) -> List<T> {
|
||||
// Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`).
|
||||
|
||||
let count = count.max(1) as usize;
|
||||
let count = count as usize;
|
||||
|
||||
let mut result_list = List::new();
|
||||
|
||||
@@ -60,12 +60,12 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
direction: PixelSize,
|
||||
angle: Angle,
|
||||
#[default(5)]
|
||||
#[hard_min(1)]
|
||||
#[hard(1..)]
|
||||
count: u32,
|
||||
) -> List<T> {
|
||||
let angle = angle.to_radians();
|
||||
let count = count.max(1);
|
||||
let total = (count - 1) as f64;
|
||||
// A single copy has no steps between copies, so the denominator is kept at 1 to avoid `0. / 0.` producing a NaN transform
|
||||
let total = (count - 1).max(1) as f64;
|
||||
|
||||
let mut result_list = List::new();
|
||||
|
||||
@@ -108,11 +108,9 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
#[default(5)]
|
||||
radius: f64,
|
||||
#[default(5)]
|
||||
#[hard_min(1)]
|
||||
#[hard(1..)]
|
||||
count: u32,
|
||||
) -> List<T> {
|
||||
let count = count.max(1);
|
||||
|
||||
let mut result_list = List::new();
|
||||
|
||||
for index in 0..count {
|
||||
@@ -265,6 +263,26 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn repeat_single_copy() {
|
||||
let context = OwnedContextImpl::default().into_context();
|
||||
let repeated = super::repeat_array(
|
||||
context,
|
||||
&FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))),
|
||||
DVec2::new(12., 10.),
|
||||
45.,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
let vector_list = vector_nodes::flatten_path(Footprint::default(), repeated).await;
|
||||
let vector = vector_list.element(0).unwrap();
|
||||
assert_eq!(vector.region_manipulator_groups().count(), 1);
|
||||
|
||||
let (_, manipulator_groups) = vector.region_manipulator_groups().next().unwrap();
|
||||
let anchor = manipulator_groups[0].anchor;
|
||||
assert!(anchor.length() < 1e-5, "Expected the single copy to be untransformed, found anchor {anchor}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn repeat_transform_position() {
|
||||
let direction = DVec2::new(12., 10.);
|
||||
|
||||
@@ -424,7 +424,7 @@ fn string_repeat(
|
||||
string: String,
|
||||
/// The number of times the string should appear in the output.
|
||||
#[default(2)]
|
||||
#[hard_min(1)]
|
||||
#[hard(1..)]
|
||||
count: u32,
|
||||
/// The string placed between each repetition.
|
||||
#[default("\\n")]
|
||||
@@ -436,7 +436,7 @@ fn string_repeat(
|
||||
) -> String {
|
||||
let separator = if separator_escaping { unescape_string(separator) } else { separator };
|
||||
|
||||
let count = count.max(1) as usize;
|
||||
let count = count as usize;
|
||||
|
||||
let mut result = String::with_capacity((string.len() + separator.len()) * count);
|
||||
for i in 0..count {
|
||||
|
||||
@@ -77,7 +77,8 @@ fn arc(
|
||||
radius: f64,
|
||||
start_angle: Angle,
|
||||
#[default(270.)]
|
||||
#[range((0., 360.))]
|
||||
#[range]
|
||||
#[soft(0..360)]
|
||||
sweep_angle: Angle,
|
||||
arc_type: ArcType,
|
||||
) -> List<Vector> {
|
||||
@@ -167,7 +168,7 @@ fn regular_polygon<T: AsU64>(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
#[default(6)]
|
||||
#[hard_min(3.)]
|
||||
#[hard(3..)]
|
||||
#[implementations(u32, u64, f64)]
|
||||
sides: T,
|
||||
#[unit(" px")]
|
||||
@@ -185,7 +186,7 @@ fn star<T: AsU64>(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
#[default(5)]
|
||||
#[hard_min(2.)]
|
||||
#[hard(2..)]
|
||||
#[implementations(u32, u64, f64)]
|
||||
sides: T,
|
||||
#[unit(" px")]
|
||||
@@ -228,7 +229,7 @@ fn qr_code(
|
||||
text: String,
|
||||
#[widget(ParsedWidgetOverride::Hidden)] has_size: bool,
|
||||
#[unit(" px")]
|
||||
#[hard_min(1.)]
|
||||
#[hard(1..)]
|
||||
#[widget(ParsedWidgetOverride::Custom = "optional_f64")]
|
||||
size: f64,
|
||||
error_correction: QRCodeErrorCorrectionLevel,
|
||||
@@ -267,7 +268,7 @@ fn qr_code(
|
||||
};
|
||||
|
||||
if has_size {
|
||||
vector.transform(glam::DAffine2::from_scale(DVec2::splat(size.max(1.) / qr_code.size() as f64)));
|
||||
vector.transform(glam::DAffine2::from_scale(DVec2::splat(size / qr_code.size() as f64)));
|
||||
}
|
||||
|
||||
List::new_from_element(vector)
|
||||
@@ -312,7 +313,7 @@ fn grid<T: GridSpacing>(
|
||||
_primary: (),
|
||||
grid_type: GridType,
|
||||
#[unit(" px")]
|
||||
#[hard_min(0.)]
|
||||
#[hard(0..)]
|
||||
#[default(10)]
|
||||
#[implementations(f64, DVec2)]
|
||||
spacing: T,
|
||||
|
||||
@@ -263,21 +263,25 @@ async fn copy_to_points<I: 'n + Send + Clone>(
|
||||
content: List<I>,
|
||||
/// Minimum range of randomized sizes given to each placed copy.
|
||||
#[default(1)]
|
||||
#[range((0., 2.))]
|
||||
#[range]
|
||||
#[soft(0..2)]
|
||||
#[unit("x")]
|
||||
random_scale_min: Multiplier,
|
||||
/// Maximum range of randomized sizes given to each placed copy.
|
||||
#[default(1)]
|
||||
#[range((0., 2.))]
|
||||
#[range]
|
||||
#[soft(0..2)]
|
||||
#[unit("x")]
|
||||
random_scale_max: Multiplier,
|
||||
/// Bias for the probability distribution of randomized sizes (0 is uniform, negatives favor more of small sizes, positives favor more of large sizes).
|
||||
#[range((-50., 50.))]
|
||||
#[range]
|
||||
#[soft(-50..50)]
|
||||
random_scale_bias: f64,
|
||||
/// Seed to determine unique variations on all the randomized copy sizes.
|
||||
random_scale_seed: SeedValue,
|
||||
/// Range of randomized angles given to each placed copy, in degrees ranging from furthest clockwise to counterclockwise.
|
||||
#[range((0., 360.))]
|
||||
#[range]
|
||||
#[soft(0..360)]
|
||||
random_rotation: Angle,
|
||||
/// Seed to determine unique variations on all the randomized copy angles.
|
||||
random_rotation_seed: SeedValue,
|
||||
@@ -337,18 +341,16 @@ async fn copy_to_points<I: 'n + Send + Clone>(
|
||||
async fn round_corners(
|
||||
_: impl Ctx,
|
||||
source: List<Vector>,
|
||||
#[hard_min(0.)]
|
||||
#[hard(0..)]
|
||||
#[default(10.)]
|
||||
radius: PixelLength,
|
||||
#[range((0., 1.))]
|
||||
#[hard_min(0.)]
|
||||
#[hard_max(1.)]
|
||||
#[range]
|
||||
#[hard(0..1)]
|
||||
#[default(0.5)]
|
||||
roundness: f64,
|
||||
#[default(100.)] edge_length_limit: Percentage,
|
||||
#[range((0., 180.))]
|
||||
#[hard_min(0.)]
|
||||
#[hard_max(180.)]
|
||||
#[range]
|
||||
#[hard(0..180)]
|
||||
#[default(5.)]
|
||||
min_angle_threshold: Angle,
|
||||
) -> List<Vector> {
|
||||
@@ -452,7 +454,7 @@ pub fn merge_by_distance(
|
||||
_: impl Ctx,
|
||||
content: List<Vector>,
|
||||
#[default(0.1)]
|
||||
#[hard_min(0.0001)]
|
||||
#[hard(0.0001..)]
|
||||
distance: PixelLength,
|
||||
algorithm: MergeByDistanceAlgorithm,
|
||||
) -> List<Vector> {
|
||||
@@ -892,8 +894,8 @@ async fn auto_tangents(
|
||||
source: List<Vector>,
|
||||
/// The amount of spread for the auto-tangents, from 0 (sharp corner) to 1 (full spread).
|
||||
#[default(0.5)]
|
||||
// TODO: Make this a soft range to allow any value to be typed in outside the slider range of 0 to 1
|
||||
#[range((0., 1.))]
|
||||
#[range]
|
||||
#[soft(0..1)]
|
||||
spread: f64,
|
||||
/// If active, existing non-zero handles won't be affected.
|
||||
#[default(true)]
|
||||
@@ -1376,16 +1378,16 @@ async fn sample_polyline(
|
||||
content: List<Vector>,
|
||||
spacing: PointSpacingType,
|
||||
#[default(100.)]
|
||||
#[hard_min(0.)]
|
||||
#[hard(0..)]
|
||||
#[unit(" px")]
|
||||
separation: f64,
|
||||
#[default(100)]
|
||||
#[hard_min(2)]
|
||||
#[hard(2..)]
|
||||
quantity: u32,
|
||||
#[hard_min(0.)]
|
||||
#[hard(0..)]
|
||||
#[unit(" px")]
|
||||
start_offset: f64,
|
||||
#[hard_min(0.)]
|
||||
#[hard(0..)]
|
||||
#[unit(" px")]
|
||||
stop_offset: f64,
|
||||
adaptive_spacing: bool,
|
||||
@@ -1830,8 +1832,9 @@ async fn scatter_points(
|
||||
content: List<Vector>,
|
||||
#[unit(" px")]
|
||||
#[default(10.)]
|
||||
#[hard_min(0.01)]
|
||||
#[range((1., 100.))]
|
||||
#[range]
|
||||
#[hard(0.01..)]
|
||||
#[soft(1..100)]
|
||||
separation: f64,
|
||||
seed: SeedValue,
|
||||
) -> List<Vector> {
|
||||
|
||||
Reference in New Issue
Block a user