mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-12 05:02:51 +00:00
Integration: Lens Flare dialog centre and Clouds seed 0 on Save Actions (#10620)
The Lens Flare params doc said "centerX":number=0.5, which the filter
dialog reads as an integer field defaulting to 0: Filter > Render > Lens
Flare put the flare in the top-left corner and an imported 0.4 centre
rounded to 0. The doc is back to 0..1=0.5; the engine still plays
off-canvas centres unclamped (a recorded 1.4 centre now has a playback test).
A Clouds or Difference Clouds step recorded through PhotoCraft's dialog
carries {"seed":0}, and Save Actions refused it. The event has no seed
key; seed 0 is what the imported empty step plays as, so it is dropped on
export. Any other seed still plays differently and is still refused.
Co-authored-by: yoriswind <164435053+yoriswind@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
33ee38b431
commit
563666d69a
4 files changed
+40
-3
No files matched your search
@@ -99,6 +99,26 @@ fn clouds_events_map_and_refuse_unrepresentable_native_settings() {
|
||||
for p in [json!({"seed": 7}), json!({"foreground": "#ff0000"}), json!({"background": "#0000ff"}), json!({"layer": 7}), json!({"target": "mask"})] {
|
||||
assert!(export_item(id, &p, "Render").is_none(), "{id}: {p}");
|
||||
}
|
||||
// The Clouds dialog records `seed: 0`, which is what the empty imported step plays as.
|
||||
for p in [json!({"seed": 0}), json!({"seed": 0.0}), json!({"seed": 0, "target": "pixels"})] {
|
||||
assert_eq!(export_item(id, &p, "Render").map(|item| item.descriptor), Some(Some(desc("null"))), "{id}: {p}");
|
||||
}
|
||||
for p in [json!({"seed": -1}), json!({"seed": 0.5}), json!({"seed": "0"}), json!({"seed": 0, "foreground": "#ff0000"})] {
|
||||
assert!(export_item(id, &p, "Render").is_none(), "{id}: {p}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A recorded dialog step saves through the real Save Actions command.
|
||||
#[test]
|
||||
fn clouds_recorded_through_the_dialog_save() {
|
||||
for id in ["filter.render.clouds", "filter.render.differenceClouds"] {
|
||||
let mut s = Session::new();
|
||||
s.actions.list.push(Action::new("Render", vec![(id.into(), json!({"seed": 0}))]));
|
||||
let saved = s.execute("actions.save", json!({"action": "Render"})).unwrap();
|
||||
assert_eq!(saved["skipped"], json!([]), "{id}");
|
||||
let back = import(&photocraft_paint::tile::b64_decode(saved["data"].as_str().unwrap()).unwrap()).unwrap();
|
||||
assert_eq!(back.actions[0].steps, vec![(id.into(), json!({}))], "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +258,8 @@ fn render_actions_match_direct_filters_and_undo() {
|
||||
(*b"DfrC", desc("null"), "filter.render.differenceClouds", json!({})),
|
||||
(*b"Fbrs", fibers(23, 6, -7), "filter.render.fibers", json!({"variance": 23, "strength": 6, "seed": -7})),
|
||||
(*b"LnsF", flare(240, [5.0, -5.0], "Nkn1"), "filter.render.lensFlare", json!({"brightness": 240, "centerX": 5.0, "centerY": -5.0, "lens": "prime105"})),
|
||||
// Just right of the canvas: the dialog's 0..1 range does not clamp a recorded centre.
|
||||
(*b"LnsF", flare(100, [1.4, 0.25], "Zm"), "filter.render.lensFlare", json!({"brightness": 100, "centerX": 1.4, "centerY": 0.25, "lens": "zoom"})),
|
||||
];
|
||||
for mode in ["rgb", "gray"] {
|
||||
for depth in [8, 16, 32] {
|
||||
|
||||
@@ -42,9 +42,12 @@ pub(super) fn clouds_from(id: &str, d: &Descriptor) -> Mapped {
|
||||
if d.items.is_empty() { step(id, json!({})) } else { Mapped::Unsupported { harmless: false } }
|
||||
}
|
||||
|
||||
/// Explicit seeds, colors and layer references have no fields in these ATN events.
|
||||
/// Explicit colors and layer references have no fields in these ATN events. A seed has none
|
||||
/// either, but `seed: 0` (what PhotoCraft's dialog records) is what the imported empty step
|
||||
/// plays as, so it is dropped; any other seed would play differently and is refused.
|
||||
pub(super) fn clouds_to(_: &str, p: &Value) -> Option<Descriptor> {
|
||||
params(p, &[]).then(|| desc("null"))
|
||||
let seed_replays = p.get("seed").is_none_or(|s| s.as_f64() == Some(0.0));
|
||||
(params(p, &["seed"]) && seed_replays).then(|| desc("null"))
|
||||
}
|
||||
|
||||
/// Capture a missing recorded seed once; later playback and export reuse the stored bits.
|
||||
|
||||
@@ -725,7 +725,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"filter.render.lensFlare",
|
||||
"Lens Flare…",
|
||||
["Filter", "Render"],
|
||||
r##"{"brightness":10..300=100,"centerX":number=0.5,"centerY":number=0.5,"lens":"zoom|prime35|prime105|moviePrime"} (normalized center coordinates; positions outside the image are supported)"##
|
||||
r##"{"brightness":10..300=100,"centerX":0..1=0.5,"centerY":0..1=0.5,"lens":"zoom|prime35|prime105|moviePrime"} (normalized centre: 0..1 is the canvas; agents and actions may pass values outside it, which place the flare off the canvas unclamped)"##
|
||||
),
|
||||
cmd!(
|
||||
"filter.render.lightingEffects",
|
||||
|
||||
@@ -22,6 +22,18 @@ fn a_unit_after_a_range_bound_does_not_reset_the_range_to_its_fallback() {
|
||||
assert!(matches!(size.map(|p| p.kind), Some(Kind::Range { max, .. }) if max == 5000.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lens_flare_dialog_centre_is_a_slider_from_0_to_1_defaulting_to_the_middle() {
|
||||
// `"centerX":number=0.5` read as an integer field defaulting to 0: the menu's Lens Flare
|
||||
// landed in the top-left corner and an imported 0.4 centre rounded to 0.
|
||||
let spec = photocraft_engine::commands::find("filter.render.lensFlare").expect("filter.render.lensFlare is registered");
|
||||
let params = parse_spec(spec.params);
|
||||
for key in ["centerX", "centerY"] {
|
||||
let kind = params.iter().find(|p| p.key == key).map(|p| p.kind.clone());
|
||||
assert_eq!(kind, Some(Kind::Range { min: 0.0, max: 1.0, default: 0.5 }), "{key}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_settings_working_spaces_list_installed_profiles_of_their_space() {
|
||||
// #6323: a press condition installed in the system can be the CMYK working space, as in
|
||||
|
||||
Reference in new issue
Block a user