From fcb987a8d1e687e396b3a5944c0cc9008d0a2216 Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Thu, 22 Oct 2026 08:30:51 -0600 Subject: [PATCH] Integrate shared filter parameters with current main --- crates/engine/src/filters.rs | 3 +- crates/engine/src/smart_cmds.rs | 5 +-- crates/io/src/displace_map.rs | 2 +- crates/io/src/hue_sat_filter_map.rs | 18 +++++------ crates/io/src/perspective_warp_map.rs | 14 ++++---- crates/io/src/smart_map.rs | 36 ++++++++++----------- crates/ops/src/lib.rs | 6 ++-- crates/ops/tests/displace_history.rs | 24 ++++++++++++++ crates/ui-egui/src/filter_dialog.rs | 2 +- crates/ui-egui/src/smart_ui.rs | 2 +- crates/ui-egui/src/smart_ui/editor_tests.rs | 2 +- 11 files changed, 70 insertions(+), 44 deletions(-) diff --git a/crates/engine/src/filters.rs b/crates/engine/src/filters.rs index c3ae72026b..99b14796f8 100644 --- a/crates/engine/src/filters.rs +++ b/crates/engine/src/filters.rs @@ -318,7 +318,8 @@ pub(crate) fn run_filter(s: &mut Session, id: &str, p: &Value) -> Result _ => params.clone(), }; // Non-destructive: record the filter and re-render the smart object from its source. - let sf = SmartFilter { command: id.clone(), params: params.into(), blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true }; + let sf = + SmartFilter { command: id.clone(), params: params.into(), blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true }; return crate::smart_cmds::add_smart_filter(doc, layer, sf, selection.as_ref()).map(|()| fp); } _ => return Err(EngineError::Other("not a pixel layer".into())), diff --git a/crates/engine/src/smart_cmds.rs b/crates/engine/src/smart_cmds.rs index b0dad3e3f3..d390cb4740 100644 --- a/crates/engine/src/smart_cmds.rs +++ b/crates/engine/src/smart_cmds.rs @@ -1120,9 +1120,10 @@ fn set_filter_params(s: &mut Session, p: &Value) -> Result { if f.command == DISPLACE { if let Some(map) = &new_map { f.params = photocraft_io::smart_map::displace_embed_map(&f.params, map) - .ok_or_else(|| bad(CMD, "the displacement map is too large to keep in a Smart Object's filter"))?; + .ok_or_else(|| bad(CMD, "the displacement map is too large to keep in a Smart Object's filter"))? + .into(); } else if new_path { - f.params = photocraft_io::smart_map::displace_without_map(&f.params); + f.params = photocraft_io::smart_map::displace_without_map(&f.params).into(); } } // Authorize the underlying command with the complete merged params, including paths diff --git a/crates/io/src/displace_map.rs b/crates/io/src/displace_map.rs index 27faf7d66e..154f3ba348 100644 --- a/crates/io/src/displace_map.rs +++ b/crates/io/src/displace_map.rs @@ -187,7 +187,7 @@ pub fn embed_map(params: &J, map: &Image) -> Option { let enumerated = |t: &str, v: &str| Value::Enumerated { type_id: Id::new(t), value: Id::new(v) }; // The settings here are placeholders: the params' own replace them when the filter renders // and when it is written to PSD (`export_item`). - let neutral = SmartFilter { command: String::new(), params: J::Null, blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true }; + let neutral = SmartFilter { command: String::new(), params: J::Null.into(), blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true }; let item = Descriptor::new("filterFX") .with("Nm ", Value::Text(photocraft_psd::descriptor::UnicodeString::new_nul("Displace..."))) .with("blendOptions", blend_options(&neutral)) diff --git a/crates/io/src/hue_sat_filter_map.rs b/crates/io/src/hue_sat_filter_map.rs index d7ce04938d..e59c2b4cc8 100644 --- a/crates/io/src/hue_sat_filter_map.rs +++ b/crates/io/src/hue_sat_filter_map.rs @@ -149,7 +149,7 @@ fn range_entry(i: usize, o: &J) -> Result, String> { /// The `filterFXList` item for an `image.adjustments.hueSaturation` smart filter. pub(super) fn export_item(f: &SmartFilter) -> Result { - let p = &f.params; + let p = f.params.as_ref(); let obj = match p { J::Object(o) => o, J::Null => &Map::new(), @@ -230,7 +230,7 @@ mod tests { let c = |r: f64, g: f64, b: f64| { Value::Descriptor(Descriptor::new("RGBC").with("Rd ", Value::Double(r)).with("Grn ", Value::Double(g)).with("Bl ", Value::Double(b))) }; - let neutral = SmartFilter { command: String::new(), params: J::Null, blend: BlendMode::Normal, opacity: 1.0, visible: true }; + let neutral = SmartFilter { command: String::new(), params: J::Null.into(), blend: BlendMode::Normal, opacity: 1.0, visible: true }; Descriptor::new("filterFX") .with("Nm ", Value::Text(UnicodeString::new_nul("Farbton/Sättigung..."))) .with("blendOptions", blend_options(&neutral)) @@ -255,7 +255,7 @@ mod tests { } fn filter(params: J) -> SmartFilter { - SmartFilter { command: COMMAND.into(), params, blend: BlendMode::Normal, opacity: 1.0, visible: true } + SmartFilter { command: COMMAND.into(), params: params.into(), blend: BlendMode::Normal, opacity: 1.0, visible: true } } #[test] @@ -268,13 +268,13 @@ mod tests { let f = filter_from_item(&observed()); assert_eq!(f.command, COMMAND); assert_eq!( - f.params, + *f.params, json!({"colorize": false, "hue": 12, "saturation": -20, "lightness": 5, "yellows": {"hue": -16, "saturation": 10, "lightness": -5, "range": [15, 45, 75, 105]}}) ); let c = filter_from_item(&item(fltr(true, vec![hst2(None, 200, 40, -10)]))); assert_eq!(c.command, COMMAND); - assert_eq!(c.params, json!({"colorize": true, "hue": 200, "saturation": 40, "lightness": -10})); + assert_eq!(*c.params, json!({"colorize": true, "hue": 200, "saturation": 40, "lightness": -10})); } #[test] @@ -300,11 +300,11 @@ mod tests { fn command_params_are_written_like_the_dialog() { let export = |p: J| filter_from_item(&item_for_filter(&filter(p)).unwrap()).params; // Partial params: the command's defaults, numbers rounded and clamped. - assert_eq!(export(json!({"hue": 12.4})), json!({"colorize": false, "hue": 12, "saturation": 0, "lightness": 0})); - assert_eq!(export(json!({"hue": -400, "lightness": 101, "layer": 3})), json!({"colorize": false, "hue": -180, "saturation": 0, "lightness": 100})); - assert_eq!(export(J::Null), json!({"colorize": false, "hue": 0, "saturation": 0, "lightness": 0})); + assert_eq!(*export(json!({"hue": 12.4})), json!({"colorize": false, "hue": 12, "saturation": 0, "lightness": 0})); + assert_eq!(*export(json!({"hue": -400, "lightness": 101, "layer": 3})), json!({"colorize": false, "hue": -180, "saturation": 0, "lightness": 100})); + assert_eq!(*export(J::Null), json!({"colorize": false, "hue": 0, "saturation": 0, "lightness": 0})); // Colorize starts at 25 % saturation and wraps the hue into 0..360. - assert_eq!(export(json!({"colorize": true, "hue": -30})), json!({"colorize": true, "hue": 330, "saturation": 25, "lightness": 0})); + assert_eq!(*export(json!({"colorize": true, "hue": -30})), json!({"colorize": true, "hue": 330, "saturation": 25, "lightness": 0})); // The full set (`to_params`): neutral ranges at their default extent are left out, reds // are written within one turn. let mut full = json!({"hue": 0, "saturation": 0, "lightness": 0, "colorize": false}); diff --git a/crates/io/src/perspective_warp_map.rs b/crates/io/src/perspective_warp_map.rs index 654de23b32..71e29df8d0 100644 --- a/crates/io/src/perspective_warp_map.rs +++ b/crates/io/src/perspective_warp_map.rs @@ -108,7 +108,7 @@ pub(super) fn import_params(item: &Descriptor) -> Option { let planes = planes_of(f)?; // Neutral blending in the template: the model's own fields replace it on export. let mut template = item.clone(); - let neutral = SmartFilter { command: String::new(), params: J::Null, blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true }; + let neutral = SmartFilter { command: String::new(), params: J::Null.into(), blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true }; set(&mut template, "blendOptions", blend_options(&neutral)); set(&mut template, "enab", Value::Boolean(true)); // Export refuses a larger template; such an item stays opaque, so it is still written back. @@ -275,7 +275,7 @@ mod tests { "blendOptions", blend_options(&SmartFilter { command: String::new(), - params: J::Null, + params: J::Null.into(), blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true, @@ -331,7 +331,7 @@ mod tests { fn edited_planes_round_trip_and_keep_the_rest_of_the_template() { let src = two_planes(708); let mut f = filter_from_item(&src); - f.params["planes"][1]["dst"][2] = json!([155.5, 118.0]); + std::sync::Arc::make_mut(&mut f.params)["planes"][1]["dst"][2] = json!([155.5, 118.0]); f.opacity = 0.5; f.visible = false; let item = item_for_filter(&f).unwrap(); @@ -351,7 +351,7 @@ mod tests { let planes = vec![Plane { src: V, dst: W }]; let f = SmartFilter { command: COMMAND.into(), - params: json!({"planes": planes}), + params: json!({"planes": planes}).into(), blend: photocraft_color::BlendMode::Multiply, opacity: 0.25, visible: true, @@ -372,7 +372,7 @@ mod tests { // Edit › Perspective Warp on a smart object stores its own id; it is written too, with a // straighten baked in. - let f = SmartFilter { command: EDIT_COMMAND.into(), params: json!({"planes": planes, "straighten": "auto", "interpolation": "bilinear"}), ..f }; + let f = SmartFilter { command: EDIT_COMMAND.into(), params: json!({"planes": planes, "straighten": "auto", "interpolation": "bilinear"}).into(), ..f }; assert!(item_for_filter(&f).is_ok()); } @@ -429,7 +429,7 @@ mod tests { fn bad_exports_report_errors() { let ok = filter_from_item(&one_plane()).params; let with = |k: &str, v: J| { - let mut p = ok.clone(); + let mut p = ok.as_ref().clone(); p[k] = v; p }; @@ -448,7 +448,7 @@ mod tests { with(TEMPLATE_KEY, json!(to_hex(&Descriptor::new("filterFX").to_bytes()))), with(TEMPLATE_KEY, json!("0".repeat(MAX_TEMPLATE_BYTES * 2 + 2))), ] { - let f = SmartFilter { command: COMMAND.into(), params: p.clone(), blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true }; + let f = SmartFilter { command: COMMAND.into(), params: p.clone().into(), blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true }; assert!(export_item(&f).is_err(), "{p}"); } } diff --git a/crates/io/src/smart_map.rs b/crates/io/src/smart_map.rs index e5c87d8c6e..d3431f85e8 100644 --- a/crates/io/src/smart_map.rs +++ b/crates/io/src/smart_map.rs @@ -1806,7 +1806,7 @@ mod tests { let item = photoshop_no_options_item("Weichzeichnen", Value::Integer(id)); let f = filter_from_item(&Descriptor::from_bytes(&item.to_bytes()).unwrap()); assert_eq!(f.command, command); - assert_eq!(f.params, json!({})); + assert_eq!(*f.params, json!({})); assert_eq!((f.visible, f.blend), (false, BlendMode::Multiply)); assert!((f.opacity - 0.6).abs() < 1e-6); } @@ -1869,7 +1869,7 @@ mod tests { let item = ps_item("Smart Sharpen...", fltr.clone(), 1131); let f = filter_from_item(&Descriptor::from_bytes(&item.to_bytes()).unwrap()); assert_eq!(f.command, "filter.sharpen.smartSharpen"); - assert_eq!(f.params, json!({"amount": 120.0, "radius": 2.5, "reduceNoise": 5.0})); + assert_eq!(*f.params, json!({"amount": 120.0, "radius": 2.5, "reduceNoise": 5.0})); assert_eq!(written_fltr(&f), fltr); let d = item_for_filter(&f).unwrap(); assert_eq!(int(&d, "filterID"), Some(1131)); @@ -1890,7 +1890,7 @@ mod tests { let keys: Vec<&str> = f.params.as_object().unwrap().keys().map(String::as_str).collect(); assert_eq!(keys.len(), 3, "{keys:?}"); assert!(keys.iter().all(|k| ["amount", "radius", "reduceNoise"].contains(k)), "{keys:?}"); - assert_eq!(f.params, json!({"amount": 200.0, "radius": 1.0, "reduceNoise": 10.0})); + assert_eq!(*f.params, json!({"amount": 200.0, "radius": 1.0, "reduceNoise": 10.0})); assert_eq!(written_fltr(&f), smart_sharpen(200.0, 1.0, 10.0, "GsnB")); } @@ -1973,7 +1973,7 @@ mod tests { let item = ps_item("Dust & Scratches...", fltr.clone(), 1_148_417_107); let f = filter_from_item(&Descriptor::from_bytes(&item.to_bytes()).unwrap()); assert_eq!(f.command, "filter.noise.dustAndScratches"); - assert_eq!(f.params, json!({"radius": radius, "threshold": threshold})); + assert_eq!(*f.params, json!({"radius": radius, "threshold": threshold})); assert_eq!(written_fltr(&f), fltr); assert_eq!(int(&item_for_filter(&f).unwrap(), "filterID"), Some(1_148_417_107)); } @@ -2040,7 +2040,7 @@ mod tests { ("filter.noise.dustAndScratches", json!([]), json!({"radius": 1, "threshold": 0})), ] { let back = filter_from_item(&item_for_filter(&sf(command, p.clone())).unwrap()); - assert_eq!((back.command.as_str(), &back.params), (command, &want), "{p}"); + assert_eq!((back.command.as_str(), back.params.as_ref()), (command, &want), "{p}"); } } @@ -2162,7 +2162,7 @@ mod tests { let item = measured_item(fltr, 51.0, 255.0); // Through bytes, as read from a file. let f = filter_from_item(&Descriptor::from_bytes(&item.to_bytes()).unwrap()); - assert_eq!((f.command.as_str(), &f.params), (command, ¶ms)); + assert_eq!((f.command.as_str(), f.params.as_ref()), (command, ¶ms)); // Written back in the measured shape: same class, keys and their order, value types // (long, bool, enum with its type), filterID; Tiles also its colours. let back = item_for_filter(&f).unwrap(); @@ -2181,12 +2181,12 @@ mod tests { assert_eq!(filter_from_item(&bare).command, UNSUPPORTED_FILTER); // Only Tiles reads the entry's colours; Wind and Extrude write the defaults. let f = filter_from_item(&measured_item(wind("Wnd ", "Left"), 51.0, 0.0)); - assert_eq!(f.params, json!({"method": "wind", "direction": "fromRight"})); + assert_eq!(*f.params, json!({"method": "wind", "direction": "fromRight"})); let back = item_for_filter(&f).unwrap(); assert_eq!((back.get("FrgC"), back.get("BckC")), (Some(&rgbc(0.0)), Some(&rgbc(255.0)))); // Tiles without colours in its params (or the entry) writes the engine's black and white. let f = filter_from_item(&Descriptor::new("filterFX").with("Fltr", Value::Descriptor(tiles(long(4), long(5), "FlBc")))); - assert_eq!(f.params, json!({"count": 4, "maxOffset": 5, "fill": "background"})); + assert_eq!(*f.params, json!({"count": 4, "maxOffset": 5, "fill": "background"})); let back = item_for_filter(&f).unwrap(); assert_eq!((back.get("FrgC"), back.get("BckC")), (Some(&rgbc(0.0)), Some(&rgbc(255.0)))); } @@ -2339,7 +2339,7 @@ mod tests { let item = measured_named_item(b"Ofst", "Offset...", offset_fltr(Value::Integer(h), Value::Integer(v), enumv("FlMd", fill))); let f = filter_from_item(&item); assert_eq!(f.command, "filter.other.offset"); - assert_eq!(f.params, json!({"horizontal": h, "vertical": v, "undefinedAreas": ours})); + assert_eq!(*f.params, json!({"horizontal": h, "vertical": v, "undefinedAreas": ours})); // Written back exactly as measured. assert_eq!(item_for_filter(&f).unwrap(), item); } @@ -2409,7 +2409,7 @@ mod tests { let item = measured_named_item(b"Cstm", "Custom...", custom_fltr(Value::Integer(scale), Value::Integer(offset), longs(&kernel))); let f = filter_from_item(&item); assert_eq!(f.command, "filter.other.custom"); - assert_eq!(f.params, json!({"kernel": kernel, "scale": scale, "offset": offset})); + assert_eq!(*f.params, json!({"kernel": kernel, "scale": scale, "offset": offset})); assert_eq!(item_for_filter(&f).unwrap(), item); } } @@ -2488,7 +2488,7 @@ mod tests { for (code, ty) in [("CrsD", "coarseDots"), ("LngS", "longStrokes")] { let item = pixelate_item("Mezzotint...", Descriptor::new("Mztn").with("MztT", enumv("MztT", code)), 1_299_870_830); let f = filter_from_item(&item); - assert_eq!((f.command.as_str(), &f.params), ("filter.pixelate.mezzotint", &json!({"type": ty}))); + assert_eq!((f.command.as_str(), f.params.as_ref()), ("filter.pixelate.mezzotint", &json!({"type": ty}))); } // No type or an unknown type is Fine Dots, like the engine; the seed // (PhotoCraft only) is not saved. @@ -2527,7 +2527,7 @@ mod tests { let item = pixelate_item("Color Halftone...", measured.clone(), 1_131_180_616); let f = filter_from_item(&item); assert_eq!(f.command, "filter.pixelate.colorHalftone"); - assert_eq!(f.params, json!({"maxRadius": 6, "channel1": 10, "channel2": 20, "channel3": 30, "channel4": 40})); + assert_eq!(*f.params, json!({"maxRadius": 6, "channel1": 10, "channel2": 20, "channel3": 30, "channel4": 40})); assert_eq!(get_desc(&item_for_filter(&f).unwrap(), "Fltr"), Some(&measured)); // Defaults and hostile values write longs within the dialog's ranges. let d = item_for_filter(&sf("filter.pixelate.colorHalftone", json!({"maxRadius": f64::NAN, "channel1": 1e12, "channel2": -1e12, "channel3": "x"}))) @@ -2664,7 +2664,7 @@ mod tests { for (fltr, id, command, params) in cases { let item = ps_item("x", fltr.clone(), id); let f = filter_from_item(&Descriptor::from_bytes(&item.to_bytes()).unwrap()); - assert_eq!((f.command.as_str(), &f.params), (command, ¶ms), "{fltr:?}"); + assert_eq!((f.command.as_str(), f.params.as_ref()), (command, ¶ms), "{fltr:?}"); let out = item_for_filter(&f).unwrap(); assert_eq!(out.get("filterID"), Some(&Value::Integer(id))); assert_eq!(out.get("hasoptions"), Some(&Value::Boolean(true))); @@ -2839,7 +2839,7 @@ mod tests { let item = photoshop_item(id, fltr); // Through bytes, as read from a file. let f = filter_from_item(&Descriptor::from_bytes(&item.to_bytes()).unwrap()); - assert_eq!((f.command.as_str(), &f.params), (command, ¶ms)); + assert_eq!((f.command.as_str(), f.params.as_ref()), (command, ¶ms)); // Written back in the measured shape: same class, keys, value types and filterID. let back = item_for_filter(&f).unwrap(); for key in ["Fltr", "filterID", "BckC"] { @@ -2858,7 +2858,7 @@ mod tests { assert_eq!(item_for_filter(&f).unwrap().get("BckC"), Some(&rgbc(51.0))); let mut item = photoshop_item(b"Crst", Descriptor::new("Crst").with("ClSz", Value::Integer(7))); set(&mut item, "BckC", rgbc(51.0)); - assert_eq!(filter_from_item(&item).params, json!({"cellSize": 7})); + assert_eq!(*filter_from_item(&item).params, json!({"cellSize": 7})); // Our random seed has no PSD key: it is not written. let fltr = |c: &str, p: J| item_for_filter(&sf(c, p)).unwrap().get("Fltr").cloned(); assert_eq!( @@ -3551,7 +3551,7 @@ mod tests { for (hex, command, params) in cases { let item = photoshop(hex); let f = filter_from_item(&item); - assert_eq!((f.command.as_str(), &f.params), (command, ¶ms)); + assert_eq!((f.command.as_str(), f.params.as_ref()), (command, ¶ms)); // Written back, it is Photoshop's item again: name, colours, settings and key order. assert_eq!(item_for_filter(&f).unwrap().items, item.items, "{command}"); } @@ -3638,12 +3638,12 @@ mod tests { fn render_filters_made_in_photocraft_are_written() { // Without colours (an older document or a script), Photoshop's default black and white. let back = filter_from_item(&item_for_filter(&sf("filter.render.clouds", json!({"seed": 7}))).unwrap()); - assert_eq!(back.params, json!({"seed": 7, "foreground": [0.0, 0.0, 0.0, 1.0], "background": [1.0, 1.0, 1.0, 1.0]})); + assert_eq!(*back.params, json!({"seed": 7, "foreground": [0.0, 0.0, 0.0, 1.0], "background": [1.0, 1.0, 1.0, 1.0]})); // A seed above i32::MAX keeps its bit pattern, and colours may be hex. let p = json!({"variance": 30, "strength": 10, "seed": 4_000_000_000u32, "foreground": "#ff0000", "background": [0.0, 0.0, 1.0, 1.0]}); let back = filter_from_item(&item_for_filter(&sf("filter.render.fibers", p)).unwrap()); assert_eq!( - back.params, + *back.params, json!({"variance": 30, "strength": 10, "seed": 4_000_000_000u32, "foreground": [1.0, 0.0, 0.0, 1.0], "background": [0.0, 0.0, 1.0, 1.0]}) ); let flare = sf("filter.render.lensFlare", json!({"brightness": 120, "centerX": 0.25, "centerY": 0.75, "lens": "moviePrime"})); diff --git a/crates/ops/src/lib.rs b/crates/ops/src/lib.rs index 6b28d26eb1..148fb19133 100644 --- a/crates/ops/src/lib.rs +++ b/crates/ops/src/lib.rs @@ -387,10 +387,10 @@ impl History { self.held_states().fold(0, |n, s| n.saturating_add(tile_bytes(&s.doc, &mut seen))) } - /// Keep pixel memory within [`History::max_bytes`]: the current document's tiles plus the - /// tiles only history holds (newest states first). The oldest undo states that don't fit are + /// Keep retained tile/filter-parameter memory within [`History::max_bytes`]: the current + /// document plus allocations only history holds (newest states first). The oldest undo states that don't fit are /// dropped; the most recent one is always kept so the last step can be undone. The History - /// Brush's source state ([`History::set_brush_source`]) is never dropped: its unique tiles are + /// Brush's source state ([`History::set_brush_source`]) is never dropped: its unique allocations are /// counted first, and the newest states fill what remains. Returns how many states were /// dropped. pub fn trim(&mut self, current: &Document) -> usize { diff --git a/crates/ops/tests/displace_history.rs b/crates/ops/tests/displace_history.rs index 372990de08..2a976f88c9 100644 --- a/crates/ops/tests/displace_history.rs +++ b/crates/ops/tests/displace_history.rs @@ -122,3 +122,27 @@ fn aliased_filters_and_snapshots_are_counted_once() { let empty = Document::new("empty", Size::new(8, 8), ColorMode::Rgb, SampleType::U8); assert_eq!(history.extra_bytes(&empty, &[&pinned, &pinned]), expected); } + +#[test] +fn cleared_history_retains_filter_payloads_until_the_next_edit() { + let mut current = Arc::new(document()); + let mut history = History::new(20); + edit(&mut history, &mut current, |d| d.name = "renamed".into()); + edit(&mut history, &mut current, |d| { + let LayerContent::Smart(smart) = &mut d.layers[0].content else { panic!("fixture") }; + smart.smart_filters[0].params = json!({"__displacePsd": "cd".repeat(4096)}).into(); + }); + let retained = history.pixel_bytes(¤t); + let unique = history.unique_bytes(¤t); + assert!(unique >= 16384, "old filter payload is retained by undo"); + assert!(history.clear_states()); + assert_eq!(history.pixel_bytes(¤t), retained); + assert_eq!(history.unique_bytes(¤t), unique, "undoable Clear History still owns the old payload"); + let (restored, _) = history.undo(current.clone()).unwrap(); + current = restored; + assert_eq!(history.pixel_bytes(¤t), retained); + assert!(history.clear_states()); + edit(&mut history, &mut current, |d| d.name = "after clear".into()); + assert_eq!(history.unique_bytes(¤t), 0); + assert_eq!(history.pixel_bytes(¤t), History::default().pixel_bytes(¤t)); +} diff --git a/crates/ui-egui/src/filter_dialog.rs b/crates/ui-egui/src/filter_dialog.rs index 7880579b01..b55aa070fd 100644 --- a/crates/ui-egui/src/filter_dialog.rs +++ b/crates/ui-egui/src/filter_dialog.rs @@ -1111,7 +1111,7 @@ pub(crate) fn preview_document_for_target( mask.feather /= run_k as f32; } for f in &mut sm.smart_filters { - scale_preview_params(&f.command, &mut f.params, run_k); + scale_preview_params(&f.command, std::sync::Arc::make_mut(&mut f.params), run_k); } if command == "layer.smartFilter.setParams" { let f = sm.smart_filters.get(index)?; diff --git a/crates/ui-egui/src/smart_ui.rs b/crates/ui-egui/src/smart_ui.rs index edb4e84b29..41486571e9 100644 --- a/crates/ui-egui/src/smart_ui.rs +++ b/crates/ui-egui/src/smart_ui.rs @@ -391,7 +391,7 @@ pub fn open_editor(app: &mut PhotocraftApp, ctx: &egui::Context, layer: u64, sm: }; if let Some(d) = app.ui.dialog_mut(id) { d.fields.insert("__smartFilter".into(), target); - if let Value::Object(p) = &f.params { + if let Value::Object(p) = f.params.as_ref() { for (k, v) in p { if !k.starts_with("__") { d.fields.insert(k.clone(), v.clone()); diff --git a/crates/ui-egui/src/smart_ui/editor_tests.rs b/crates/ui-egui/src/smart_ui/editor_tests.rs index 6e0b9d14f8..790f690c79 100644 --- a/crates/ui-egui/src/smart_ui/editor_tests.rs +++ b/crates/ui-egui/src/smart_ui/editor_tests.rs @@ -172,7 +172,7 @@ fn filter_editor_starts_missing_parameters_from_defaults_not_remembered_values() let ctx = egui::Context::default(); let mut sm = smart(&app, layer).clone(); let index = sm.smart_filters.iter().position(|f| f.command == command).unwrap(); - sm.smart_filters[index].params = json!({"amount": 125, "radius": 2}); + sm.smart_filters[index].params = json!({"amount": 125, "radius": 2}).into(); super::open_editor(&mut app, &ctx, layer.0, &sm, index); let fields = &app.ui.dialogs.last().unwrap().fields;