Filter › Other › Minimum / Maximum: Preserve Roundness (#27)
Photoshop's Minimum and Maximum have Preserve: Squareness | Roundness; only the square element existed. Roundness takes the extreme over a disc (dx² + dy² ≤ r²), one van Herk / Gil-Werman running min/max per disc row, so cost stays O(r) per pixel. filter.other.minimum/maximum take "preserve" (default squareness; older params deserialize as Squareness). Co-authored-by: storybored379 <storybored379@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6bdee62e87
commit
7ec0b4aa62
|
|
@ -117,6 +117,17 @@ pub enum UndefinedAreas {
|
|||
Transparent,
|
||||
}
|
||||
|
||||
/// Minimum / Maximum › Preserve: the shape the filter grows or shrinks by.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Preserve {
|
||||
/// A square of `2·radius + 1` pixels (keeps corners square).
|
||||
#[default]
|
||||
Squareness,
|
||||
/// A disc of `radius` pixels (rounds corners off evenly).
|
||||
Roundness,
|
||||
}
|
||||
|
||||
/// Spherize mode.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -179,8 +190,17 @@ pub enum FilterParams {
|
|||
AddNoise { amount: f32, distribution: Distribution, monochromatic: bool, seed: u32 },
|
||||
Median { radius: f32 },
|
||||
DustAndScratches { radius: f32, threshold: f32 },
|
||||
Minimum { radius: f32 },
|
||||
Maximum { radius: f32 },
|
||||
/// Radius px; `preserve` is the dialog's Preserve (older params default to Squareness).
|
||||
Minimum {
|
||||
radius: f32,
|
||||
#[serde(default)]
|
||||
preserve: Preserve,
|
||||
},
|
||||
Maximum {
|
||||
radius: f32,
|
||||
#[serde(default)]
|
||||
preserve: Preserve,
|
||||
},
|
||||
Offset { horizontal: i32, vertical: i32, undefined: UndefinedAreas },
|
||||
Mosaic { cell_size: f32 },
|
||||
/// Angle degrees, height px, amount %.
|
||||
|
|
@ -319,8 +339,8 @@ impl FilterParams {
|
|||
| FilterParams::SurfaceBlur { radius, .. }
|
||||
| FilterParams::Median { radius }
|
||||
| FilterParams::DustAndScratches { radius, .. }
|
||||
| FilterParams::Minimum { radius }
|
||||
| FilterParams::Maximum { radius } => Halo::Radius(radius.max(0.0).ceil() as i32 + 1),
|
||||
| FilterParams::Minimum { radius, .. }
|
||||
| FilterParams::Maximum { radius, .. } => Halo::Radius(radius.max(0.0).ceil() as i32 + 1),
|
||||
FilterParams::MotionBlur { distance, .. } => Halo::Radius((distance.abs() / 2.0).ceil() as i32 + 2),
|
||||
FilterParams::Mosaic { cell_size } => Halo::Radius(cell_size.max(1.0).ceil() as i32 + 1),
|
||||
FilterParams::Emboss { height, .. } => Halo::Radius(height.abs().ceil() as i32 + 2),
|
||||
|
|
@ -474,8 +494,8 @@ pub fn kernel(params: &FilterParams, src: &Image, out: Rect, ctx: &Ctx) -> Vec<f
|
|||
FilterParams::AddNoise { amount, distribution, monochromatic, seed } => noise::add(src, out, ctx, *amount, *distribution, *monochromatic, *seed),
|
||||
FilterParams::Median { radius } => noise::median(src, out, *radius, None),
|
||||
FilterParams::DustAndScratches { radius, threshold } => noise::median(src, out, *radius, Some(*threshold)),
|
||||
FilterParams::Minimum { radius } => other::min_max(src, out, *radius, false),
|
||||
FilterParams::Maximum { radius } => other::min_max(src, out, *radius, true),
|
||||
FilterParams::Minimum { radius, preserve } => other::min_max_preserve(src, out, *radius, false, *preserve),
|
||||
FilterParams::Maximum { radius, preserve } => other::min_max_preserve(src, out, *radius, true, *preserve),
|
||||
FilterParams::Offset { horizontal, vertical, undefined } => other::offset(src, out, ctx, *horizontal, *vertical, *undefined),
|
||||
FilterParams::Mosaic { cell_size } => stylize::mosaic(src, out, ctx, *cell_size),
|
||||
FilterParams::Emboss { angle, height, amount } => stylize::emboss(src, out, ctx, *angle, *height, *amount),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,84 @@
|
|||
use photocraft_geom::Rect;
|
||||
|
||||
use crate::image::{Edge, Image};
|
||||
use crate::{Ctx, UndefinedAreas};
|
||||
use crate::{Ctx, Preserve, UndefinedAreas};
|
||||
|
||||
/// Minimum or Maximum with the dialog's Preserve option: a square (`min_max`) or a disc
|
||||
/// (`min_max_round`).
|
||||
pub(crate) fn min_max_preserve(
|
||||
src: &Image,
|
||||
out: Rect,
|
||||
radius: f32,
|
||||
max: bool,
|
||||
preserve: Preserve,
|
||||
) -> Vec<f32> {
|
||||
match preserve {
|
||||
Preserve::Squareness => min_max(src, out, radius, max),
|
||||
Preserve::Roundness => min_max_round(src, out, radius, max),
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimum or Maximum over a disc (Preserve: Roundness): every offset with
|
||||
/// `dx² + dy² ≤ radius²`, so corners and straight edges move by the same distance. Each row of
|
||||
/// the disc is a horizontal window, taken with the van Herk / Gil-Werman running extreme
|
||||
/// (constant work per pixel whatever the window), so the cost is O(radius) per pixel.
|
||||
pub(crate) fn min_max_round(src: &Image, out: Rect, radius: f32, max: bool) -> Vec<f32> {
|
||||
let n = src.ch;
|
||||
let r = radius.max(0.0);
|
||||
let ri = r.floor() as i32;
|
||||
if ri == 0 {
|
||||
return src.crop(out);
|
||||
}
|
||||
let w = out.width() as usize;
|
||||
let pick = |a: f32, b: f32| if max { a.max(b) } else { a.min(b) };
|
||||
let mut res = vec![if max { f32::MIN } else { f32::MAX }; w * out.height() as usize * n];
|
||||
let (mut row, mut ext) = (Vec::new(), Vec::new());
|
||||
for dy in -ri..=ri {
|
||||
// Half-width of the disc on this row (the epsilon keeps exact squares, e.g. r 5, dy 3 → 4).
|
||||
let hw = ((r * r - (dy * dy) as f32).max(0.0).sqrt() + 1e-4).floor() as i32;
|
||||
for y in out.y0..out.y1 {
|
||||
let o = (y - out.y0) as usize * w * n;
|
||||
for c in 0..n {
|
||||
row.clear();
|
||||
row.extend((out.x0 - hw..out.x1 + hw).map(|x| src.get(x, y + dy, c)));
|
||||
running_extreme(&row, (2 * hw + 1) as usize, max, &mut ext);
|
||||
for (x, v) in ext.iter().enumerate() {
|
||||
let i = o + x * n + c;
|
||||
res[i] = pick(res[i], *v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// `out[i]` = extreme of `v[i..i + k]` for every full window (van Herk / Gil-Werman: prefix
|
||||
/// extremes within blocks of `k` plus suffix extremes within blocks, one lookup of each).
|
||||
pub(crate) fn running_extreme(v: &[f32], k: usize, max: bool, out: &mut Vec<f32>) {
|
||||
out.clear();
|
||||
let n = v.len();
|
||||
if k <= 1 {
|
||||
out.extend_from_slice(v);
|
||||
return;
|
||||
}
|
||||
if n < k {
|
||||
return;
|
||||
}
|
||||
let pick = |a: f32, b: f32| if max { a.max(b) } else { a.min(b) };
|
||||
let mut pre = v.to_vec();
|
||||
let mut suf = v.to_vec();
|
||||
for i in 1..n {
|
||||
if i % k != 0 {
|
||||
pre[i] = pick(pre[i - 1], v[i]);
|
||||
}
|
||||
}
|
||||
for i in (0..n - 1).rev() {
|
||||
if (i + 1) % k != 0 {
|
||||
suf[i] = pick(suf[i + 1], v[i]);
|
||||
}
|
||||
}
|
||||
out.extend((0..=n - k).map(|i| pick(suf[i], pre[i + k - 1])));
|
||||
}
|
||||
|
||||
/// Minimum (spreads dark/transparent areas) or Maximum over a square.
|
||||
pub(crate) fn min_max(src: &Image, out: Rect, radius: f32, max: bool) -> Vec<f32> {
|
||||
|
|
|
|||
|
|
@ -45,8 +45,10 @@ fn all_filters() -> Vec<FilterParams> {
|
|||
FilterParams::AddNoise { amount: 25.0, distribution: Distribution::Gaussian, monochromatic: false, seed: 3 },
|
||||
FilterParams::Median { radius: 2.0 },
|
||||
FilterParams::DustAndScratches { radius: 2.0, threshold: 10.0 },
|
||||
FilterParams::Minimum { radius: 1.0 },
|
||||
FilterParams::Maximum { radius: 2.0 },
|
||||
FilterParams::Minimum { radius: 1.0, preserve: Preserve::Squareness },
|
||||
FilterParams::Maximum { radius: 2.0, preserve: Preserve::Squareness },
|
||||
FilterParams::Minimum { radius: 2.5, preserve: Preserve::Roundness },
|
||||
FilterParams::Maximum { radius: 3.0, preserve: Preserve::Roundness },
|
||||
FilterParams::Offset { horizontal: 7, vertical: -3, undefined: UndefinedAreas::Wrap },
|
||||
FilterParams::Mosaic { cell_size: 6.0 },
|
||||
FilterParams::Emboss { angle: 135.0, height: 2.0, amount: 100.0 },
|
||||
|
|
@ -224,13 +226,158 @@ fn median_and_dust_remove_a_speck() {
|
|||
fn minimum_maximum_spread() {
|
||||
let mut s = flat(SampleType::F32, R, [0.5, 0.5, 0.5, 1.0]);
|
||||
s.write_pixel(10, 10, &[1.0, 1.0, 1.0, 1.0]);
|
||||
let mx = run(&s, &FilterParams::Maximum { radius: 1.0 });
|
||||
let mx = run(&s, &FilterParams::Maximum { radius: 1.0, preserve: Preserve::Squareness });
|
||||
assert!((mx.pixel(11, 11)[0] - 1.0).abs() < 1e-6);
|
||||
assert!((mx.pixel(12, 10)[0] - 0.5).abs() < 1e-6);
|
||||
let mn = run(&s, &FilterParams::Minimum { radius: 1.0 });
|
||||
let mn = run(&s, &FilterParams::Minimum { radius: 1.0, preserve: Preserve::Squareness });
|
||||
assert!((mn.pixel(10, 10)[0] - 0.5).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximum_roundness_grows_a_point_into_a_disc() {
|
||||
// One white pixel on black: Maximum spreads it over the structuring element itself.
|
||||
for depth in [SampleType::U8, SampleType::U16, SampleType::F32] {
|
||||
let mut s = flat(depth, R, [0.0, 0.0, 0.0, 1.0]);
|
||||
s.write_pixel(20, 15, &[1.0, 1.0, 1.0, 1.0]);
|
||||
for radius in [1.0f32, 2.0, 3.5, 6.0] {
|
||||
let round = run(
|
||||
&s,
|
||||
&FilterParams::Maximum {
|
||||
radius,
|
||||
preserve: Preserve::Roundness,
|
||||
},
|
||||
);
|
||||
let square = run(
|
||||
&s,
|
||||
&FilterParams::Maximum {
|
||||
radius,
|
||||
preserve: Preserve::Squareness,
|
||||
},
|
||||
);
|
||||
let rs = radius.round() as i32;
|
||||
for y in R.y0..R.y1 {
|
||||
for x in R.x0..R.x1 {
|
||||
let (dx, dy) = (x - 20, y - 15);
|
||||
let in_disc = (dx * dx + dy * dy) as f32 <= radius * radius;
|
||||
assert_eq!(
|
||||
round.pixel(x, y)[0] > 0.5,
|
||||
in_disc,
|
||||
"{depth:?} r {radius} at {dx},{dy}"
|
||||
);
|
||||
assert_eq!(
|
||||
square.pixel(x, y)[0] > 0.5,
|
||||
dx.abs() <= rs && dy.abs() <= rs,
|
||||
"{depth:?} r {radius} at {dx},{dy}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Radius 1: the four neighbours, not the diagonals (a 3×3 square would take them).
|
||||
let mut s = flat(SampleType::U8, R, [0.0, 0.0, 0.0, 1.0]);
|
||||
s.write_pixel(20, 15, &[1.0, 1.0, 1.0, 1.0]);
|
||||
let r1 = run(
|
||||
&s,
|
||||
&FilterParams::Maximum {
|
||||
radius: 1.0,
|
||||
preserve: Preserve::Roundness,
|
||||
},
|
||||
);
|
||||
assert_eq!(r1.pixel(21, 15)[0], 1.0);
|
||||
assert_eq!(r1.pixel(21, 16)[0], 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimum_maximum_roundness_match_brute_force() {
|
||||
// Exhaustive disc search on a textured image, both operators, a few radii (incl. fractional).
|
||||
let s = pattern(SampleType::F32, R);
|
||||
let img = s.read_region(R);
|
||||
let at = |x: i32, y: i32, c: usize| -> Option<f32> {
|
||||
R.contains(x, y)
|
||||
.then(|| img[((y * 40 + x) * 4) as usize + c])
|
||||
};
|
||||
for radius in [1.0f32, 1.5, 2.9, 4.0] {
|
||||
let ri = radius.floor() as i32;
|
||||
for max in [false, true] {
|
||||
let p = if max {
|
||||
FilterParams::Maximum {
|
||||
radius,
|
||||
preserve: Preserve::Roundness,
|
||||
}
|
||||
} else {
|
||||
FilterParams::Minimum {
|
||||
radius,
|
||||
preserve: Preserve::Roundness,
|
||||
}
|
||||
};
|
||||
let got = run(&s, &p);
|
||||
for y in R.y0 + ri..R.y1 - ri {
|
||||
for x in R.x0 + ri..R.x1 - ri {
|
||||
for c in 0..3 {
|
||||
let mut want = if max { f32::MIN } else { f32::MAX };
|
||||
for dy in -ri..=ri {
|
||||
for dx in -ri..=ri {
|
||||
if (dx * dx + dy * dy) as f32 <= radius * radius
|
||||
&& let Some(v) = at(x + dx, y + dy, c)
|
||||
{
|
||||
want = if max { want.max(v) } else { want.min(v) };
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
(got.pixel(x, y)[c] - want).abs() < 1e-6,
|
||||
"r {radius} max {max} at {x},{y} c{c}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn running_extreme_windows() {
|
||||
let v = [3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0];
|
||||
let mut out = Vec::new();
|
||||
for k in 1..=v.len() {
|
||||
for max in [false, true] {
|
||||
crate::other::running_extreme(&v, k, max, &mut out);
|
||||
let want: Vec<f32> = v
|
||||
.windows(k)
|
||||
.map(|w| {
|
||||
w.iter()
|
||||
.copied()
|
||||
.fold(if max { f32::MIN } else { f32::MAX }, |a, b| {
|
||||
if max { a.max(b) } else { a.min(b) }
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(out, want, "k {k} max {max}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimum_maximum_old_params_default_to_squareness() {
|
||||
let p: FilterParams = serde_json::from_str(r#"{"filter":"minimum","radius":3}"#).unwrap();
|
||||
assert_eq!(
|
||||
p,
|
||||
FilterParams::Minimum {
|
||||
radius: 3.0,
|
||||
preserve: Preserve::Squareness
|
||||
}
|
||||
);
|
||||
let p: FilterParams =
|
||||
serde_json::from_str(r#"{"filter":"maximum","radius":2,"preserve":"roundness"}"#).unwrap();
|
||||
assert_eq!(
|
||||
p,
|
||||
FilterParams::Maximum {
|
||||
radius: 2.0,
|
||||
preserve: Preserve::Roundness
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_wrap_repeat_transparent() {
|
||||
let s = pattern(SampleType::U8, R);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//! smart object, recording a smart filter), respects the selection, and is
|
||||
//! undoable. `filter.lastFilter` re-runs the most recent filter command.
|
||||
|
||||
use photocraft_algo::{self as algo, Distribution, FilterParams, PolarMode, RadialMethod, RippleSize, SpherizeMode, UndefinedAreas, WaveType};
|
||||
use photocraft_algo::{self as algo, Distribution, FilterParams, PolarMode, Preserve, RadialMethod, RippleSize, SpherizeMode, UndefinedAreas, WaveType};
|
||||
use photocraft_doc::{LayerContent, SmartFilter};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
|
|
@ -31,6 +31,15 @@ fn undefined(p: &Value) -> UndefinedAreas {
|
|||
}
|
||||
}
|
||||
|
||||
/// Minimum / Maximum › Preserve (Photoshop's default is Squareness).
|
||||
fn preserve(p: &Value) -> Preserve {
|
||||
if s(p, "preserve", "squareness") == "roundness" {
|
||||
Preserve::Roundness
|
||||
} else {
|
||||
Preserve::Squareness
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the algorithm parameters for a filter command id from JSON params
|
||||
/// (Photoshop dialog units).
|
||||
pub fn params_for(id: &str, p: &Value) -> Option<FilterParams> {
|
||||
|
|
@ -71,8 +80,8 @@ pub fn params_for(id: &str, p: &Value) -> Option<FilterParams> {
|
|||
},
|
||||
"filter.noise.median" => FilterParams::Median { radius: f(p, "radius", 1.0).clamp(1.0, 500.0) },
|
||||
"filter.noise.dustAndScratches" => FilterParams::DustAndScratches { radius: f(p, "radius", 1.0).clamp(1.0, 500.0), threshold: f(p, "threshold", 0.0).clamp(0.0, 255.0) },
|
||||
"filter.other.minimum" => FilterParams::Minimum { radius: f(p, "radius", 1.0).clamp(0.2, 500.0) },
|
||||
"filter.other.maximum" => FilterParams::Maximum { radius: f(p, "radius", 1.0).clamp(0.2, 500.0) },
|
||||
"filter.other.minimum" => FilterParams::Minimum { radius: f(p, "radius", 1.0).clamp(0.2, 500.0), preserve: preserve(p) },
|
||||
"filter.other.maximum" => FilterParams::Maximum { radius: f(p, "radius", 1.0).clamp(0.2, 500.0), preserve: preserve(p) },
|
||||
"filter.other.offset" => FilterParams::Offset { horizontal: i(p, "horizontal", 0), vertical: i(p, "vertical", 0), undefined: undefined(p) },
|
||||
"filter.pixelate.mosaic" => FilterParams::Mosaic { cell_size: f(p, "cellSize", 10.0).clamp(2.0, 200.0) },
|
||||
"filter.stylize.emboss" => FilterParams::Emboss { angle: f(p, "angle", 135.0), height: f(p, "height", 3.0).clamp(1.0, 100.0), amount: f(p, "amount", 100.0).clamp(1.0, 500.0) },
|
||||
|
|
@ -253,8 +262,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
filter_cmd!("filter.distort.ripple", "Ripple…", ["Filter", "Distort"], r##"{"amount":-999..999=100,"size":"small|medium|large"}"##),
|
||||
filter_cmd!("filter.distort.polarCoordinates", "Polar Coordinates…", ["Filter", "Distort"], r##"{"mode":"rectangularToPolar|polarToRectangular"}"##),
|
||||
filter_cmd!("filter.other.highPass", "High Pass…", ["Filter", "Other"], r##"{"radius":0.1..1000=10}"##),
|
||||
filter_cmd!("filter.other.minimum", "Minimum…", ["Filter", "Other"], r##"{"radius":0.2..500=1}"##),
|
||||
filter_cmd!("filter.other.maximum", "Maximum…", ["Filter", "Other"], r##"{"radius":0.2..500=1}"##),
|
||||
filter_cmd!("filter.other.minimum", "Minimum…", ["Filter", "Other"], r##"{"radius":0.2..500=1,"preserve":"squareness|roundness"}"##),
|
||||
filter_cmd!("filter.other.maximum", "Maximum…", ["Filter", "Other"], r##"{"radius":0.2..500=1,"preserve":"squareness|roundness"}"##),
|
||||
filter_cmd!("filter.other.offset", "Offset…", ["Filter", "Other"], r##"{"horizontal":px=0,"vertical":px=0,"undefinedAreas":"wrap|repeat|transparent"}"##),
|
||||
CommandSpec {
|
||||
id: "filter.lastFilter",
|
||||
|
|
@ -426,6 +435,74 @@ mod tests {
|
|||
assert!(params_for("filter.nope", &json!({})).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimum_maximum_preserve_roundness() {
|
||||
for depth in [8, 16, 32] {
|
||||
let mut s = Session::new();
|
||||
s.execute(
|
||||
"file.new",
|
||||
json!({"width": 48, "height": 32, "depth": depth}),
|
||||
)
|
||||
.unwrap();
|
||||
s.execute("layer.new.layer", json!({})).unwrap();
|
||||
// A white dot on black.
|
||||
s.edit("dot", |doc, active| {
|
||||
let surf = doc
|
||||
.layer_mut(active.unwrap())
|
||||
.unwrap()
|
||||
.surface_mut()
|
||||
.unwrap();
|
||||
surf.fill_rect(
|
||||
photocraft_geom::Rect::new(0, 0, 48, 32),
|
||||
&[0.0, 0.0, 0.0, 1.0],
|
||||
);
|
||||
surf.write_pixel(24, 16, &[1.0, 1.0, 1.0, 1.0]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let before = active_pixels(&s);
|
||||
let lit = |s: &Session, x: usize, y: usize| active_pixels(s)[(y * 48 + x) * 4] > 0.5;
|
||||
s.execute(
|
||||
"filter.other.maximum",
|
||||
json!({"radius": 4, "preserve": "roundness"}),
|
||||
)
|
||||
.unwrap();
|
||||
// Grown into a disc of radius 4: (4, 0) and (2, 3) are in, the square's corner isn't.
|
||||
assert!(lit(&s, 28, 16) && lit(&s, 26, 19), "depth {depth}");
|
||||
assert!(!lit(&s, 28, 20) && !lit(&s, 27, 19), "depth {depth}");
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(active_pixels(&s), before);
|
||||
// The default is Squareness: the corner is taken.
|
||||
s.execute("filter.other.maximum", json!({"radius": 4}))
|
||||
.unwrap();
|
||||
assert!(lit(&s, 28, 20), "depth {depth}");
|
||||
// Minimum with roundness: only the centre of that 9×9 square holds a whole disc.
|
||||
s.execute(
|
||||
"filter.other.minimum",
|
||||
json!({"radius": 4, "preserve": "roundness"}),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!lit(&s, 20, 12) && lit(&s, 24, 16), "depth {depth}");
|
||||
}
|
||||
assert_eq!(
|
||||
params_for(
|
||||
"filter.other.minimum",
|
||||
&json!({"radius": 2, "preserve": "roundness"})
|
||||
),
|
||||
Some(FilterParams::Minimum {
|
||||
radius: 2.0,
|
||||
preserve: Preserve::Roundness
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
params_for("filter.other.maximum", &json!({})),
|
||||
Some(FilterParams::Maximum {
|
||||
radius: 1.0,
|
||||
preserve: Preserve::Squareness
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smart_object_records_smart_filter() {
|
||||
use photocraft_doc::{Layer, SmartObject, SmartSource};
|
||||
|
|
|
|||
Loading…
Reference in New Issue