mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
Integrate shared filter parameters with current main
This commit is contained in:
@@ -318,7 +318,8 @@ pub(crate) fn run_filter(s: &mut Session, id: &str, p: &Value) -> Result<Value>
|
||||
_ => 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())),
|
||||
|
||||
@@ -1120,9 +1120,10 @@ fn set_filter_params(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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
|
||||
|
||||
@@ -187,7 +187,7 @@ pub fn embed_map(params: &J, map: &Image) -> Option<J> {
|
||||
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))
|
||||
|
||||
@@ -149,7 +149,7 @@ fn range_entry(i: usize, o: &J) -> Result<Option<Value>, String> {
|
||||
|
||||
/// The `filterFXList` item for an `image.adjustments.hueSaturation` smart filter.
|
||||
pub(super) fn export_item(f: &SmartFilter) -> Result<Descriptor, String> {
|
||||
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});
|
||||
|
||||
@@ -108,7 +108,7 @@ pub(super) fn import_params(item: &Descriptor) -> Option<J> {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
+18
-18
@@ -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"}));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user