mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-10 18:13:35 +00:00
Integration: transform anchor codes, Close save:false, Batch Save As + Close, opacity and thumbnail-click regressions (#10702)
- FTcs reference points used a 3x3 grid (Qcs0..Qcs8). The reference application has
Qcsa centre, Qcs0..Qcs3 corners clockwise from top-left and Qcs4..Qcs7 the sides
top/right/bottom/left (measured: 50 % scale, Qcs6 keeps the bottom centre, Qcs1 the
top-right corner). Qcs8 and Qcsi stay kept steps; nothing writes Qcs8.
- file.close {"save": false} (used by ui-egui tests) became an error; it discards again
and saves as Svng N.
- Batch never performs the action's Save As, so the scratch document stayed dirty and the
recorded Close (no Svng -> "save":"ask") failed every file of "...; Save As; Close".
A Save / Save As save point now marks the document saved.
- Adjustment-layer opacity is quantised to 8 bits on load; a recorded 70 % saved back as
70.196 %. The whole percent that quantises to the same level is saved again.
- Cmd-clicking a group's thumbnail errored; it loads the whole canvas again as before.
A recorded Cmd-click on another layer's thumbnail kept the numeric id and failed strict
Save Actions; it records the layer by name (or as the Background).
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:
co-authored by
yoriswind
Claude Opus 5.5
parent
0ae7356064
commit
78cf0cf91f
@@ -311,7 +311,7 @@ fn build() -> Vec<CommandSpec> {
|
||||
"Close",
|
||||
["File"],
|
||||
Some("Cmd+W"),
|
||||
r##"{"document":index?,"save":"discard|ask"="discard"} (ask refuses to close a changed document in headless playback)"##,
|
||||
r##"{"document":index?,"save":"discard|ask"|false="discard"} (false means discard; ask refuses to close a changed document in headless playback)"##,
|
||||
has_doc,
|
||||
|s, p| {
|
||||
let i = p.get("document").and_then(Value::as_u64).map(|v| v as usize).or(s.active_index()).ok_or(EngineError::NoDocument)?;
|
||||
@@ -321,7 +321,9 @@ fn build() -> Vec<CommandSpec> {
|
||||
return Err(bad("file.close", "the document has unsaved changes; save it or explicitly discard them"));
|
||||
}
|
||||
}
|
||||
Some(v) if v != "discard" => return Err(bad("file.close", "`save` must be ask or discard")),
|
||||
// `false` (don't save) is the older spelling of discard.
|
||||
Some(Value::Bool(false)) => {}
|
||||
Some(v) if v != "discard" => return Err(bad("file.close", "`save` must be ask, discard or false")),
|
||||
_ => {}
|
||||
}
|
||||
s.close(i).ok_or(EngineError::NoDocument)?;
|
||||
|
||||
@@ -114,6 +114,48 @@ fn recorded_eye_visibility_replays_on_another_document_and_duplicate_names() {
|
||||
assert!(s.active().unwrap().doc.layer(photocraft_doc::LayerId(target)).unwrap().visible);
|
||||
}
|
||||
|
||||
/// ⌘-clicking a layer's thumbnail that is not the selected layer records the layer by name (the
|
||||
/// Background as the Background), so the step saves in strict Save Actions and replays on the
|
||||
/// same-named layer of another document.
|
||||
#[test]
|
||||
fn recorded_thumbnail_click_on_another_layer_saves_and_replays_by_name() {
|
||||
use photocraft_geom::Rect;
|
||||
let fixture = |s: &mut Session| {
|
||||
s.execute("file.new", json!({"width": 4, "height": 1})).unwrap();
|
||||
let source = s.execute("layer.new.layer", json!({"name": "Source"})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.edit("Pixels", |doc, _| {
|
||||
doc.layer_mut(photocraft_doc::LayerId(source)).unwrap().surface_mut().unwrap().fill_rect(Rect::new(0, 0, 1, 1), &[1.0, 0.0, 0.0, 1.0]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("layer.new.layer", json!({"name": "Active"})).unwrap();
|
||||
source
|
||||
};
|
||||
let mut s = Session::new();
|
||||
let source = fixture(&mut s);
|
||||
let background = s.active().unwrap().doc.background().unwrap().id.0;
|
||||
s.execute("actions.record", json!({"name": "Load"})).unwrap();
|
||||
s.execute("select.loadSelection", json!({"channel": "transparency", "layer": source, "operation": "new"})).unwrap();
|
||||
s.execute("select.loadSelection", json!({"channel": "transparency", "layer": background, "operation": "new"})).unwrap();
|
||||
s.execute("actions.stop", json!({})).unwrap();
|
||||
let action = s.actions.list.iter().find(|a| a.name == "Load").unwrap().clone();
|
||||
assert_eq!(
|
||||
action.steps,
|
||||
vec![
|
||||
("select.loadSelection".into(), json!({"channel": "transparency", "layer": {"name": "Source"}, "operation": "new"})),
|
||||
("select.loadSelection".into(), json!({"channel": "transparency", "layer": {"background": true}, "operation": "new"})),
|
||||
]
|
||||
);
|
||||
crate::atn_cmds::export("Set", &[&action], &[], false).unwrap();
|
||||
// Replayed in another document, the first step loads its Source layer.
|
||||
let mut other = Session::new();
|
||||
fixture(&mut other);
|
||||
let (id, params) = &action.steps[0];
|
||||
other.execute(id, params.clone()).unwrap();
|
||||
let doc = &other.active().unwrap().doc;
|
||||
assert_eq!(doc.selection.as_ref().unwrap().read_region(Rect::new(0, 0, 4, 1)), vec![1.0, 0.0, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_keeps_calls_and_function_key_and_rejects_bad_names() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -131,6 +131,7 @@ pub(super) fn close_to(_: &str, p: &Value) -> Option<Descriptor> {
|
||||
}
|
||||
match p.get("save").filter(|v| !v.is_null()) {
|
||||
Some(v) if v == "ask" => Some(desc("null")),
|
||||
Some(Value::Bool(false)) => Some(desc("null").with("Svng", enm("YsN ", "N "))),
|
||||
Some(v) if v != "discard" => None,
|
||||
_ => Some(desc("null").with("Svng", enm("YsN ", "N "))),
|
||||
}
|
||||
|
||||
@@ -7,18 +7,19 @@ use super::fields::{F, Kind, Mapped, fields_from, fields_to, step};
|
||||
use super::tables::{CHANNELS, INTERPOLATION, RADIAL_METHOD, RADIAL_QUALITY, channel_of, lookup, lookup_back};
|
||||
use super::{D, Descriptor, Map, ReferenceItem, UnicodeString, Value, json};
|
||||
|
||||
/// Photoshop's nine frame anchors and the average/centre anchor.
|
||||
/// The reference point of a Free Transform (`QCSt`): `Qcsa` the centre (average), `Qcs0`..`Qcs3`
|
||||
/// the corners clockwise from the top left, `Qcs4`..`Qcs7` the sides top, right, bottom, left.
|
||||
/// `Qcsi` (independent) and unknown codes have no mapping and stay kept steps.
|
||||
const TRANSFORM_REFERENCE: &[(&str, &str)] = &[
|
||||
("Qcsa", "center"),
|
||||
("Qcs0", "topLeft"),
|
||||
("Qcs1", "top"),
|
||||
("Qcs2", "topRight"),
|
||||
("Qcs3", "left"),
|
||||
("Qcs4", "center"),
|
||||
("Qcs4", "top"),
|
||||
("Qcs1", "topRight"),
|
||||
("Qcs7", "left"),
|
||||
("Qcs5", "right"),
|
||||
("Qcs6", "bottomLeft"),
|
||||
("Qcs7", "bottom"),
|
||||
("Qcs8", "bottomRight"),
|
||||
("Qcs3", "bottomLeft"),
|
||||
("Qcs6", "bottom"),
|
||||
("Qcs2", "bottomRight"),
|
||||
];
|
||||
|
||||
/// `transform`: Photoshop records a Free Transform relative to the frame (offset of its centre,
|
||||
|
||||
@@ -279,6 +279,16 @@ pub(super) fn make_from(d: &Descriptor) -> Mapped {
|
||||
}
|
||||
}
|
||||
|
||||
/// The percent to save for an adjustment layer's opacity. Loading quantises a recorded percent
|
||||
/// to 8 bits (70 % becomes 179/255); a whole percent that quantises to that same level is the
|
||||
/// one that was recorded, so it is saved back (load then save keeps `70`, not `70.196`).
|
||||
fn recorded_percent(opacity: f64) -> f64 {
|
||||
let level = opacity * 255.0;
|
||||
let percent = opacity * 100.0;
|
||||
let whole = percent.round();
|
||||
if (level - level.round()).abs() < 1e-6 && (whole * 255.0 / 100.0).round() == level.round() { whole } else { percent }
|
||||
}
|
||||
|
||||
/// Write a creation step, refusing native options the target descriptor cannot carry.
|
||||
pub(super) fn make_to(id: &str, p: &Value) -> Option<Descriptor> {
|
||||
let with_name = |cls: &str| {
|
||||
@@ -425,7 +435,7 @@ pub(super) fn make_to(id: &str, p: &Value) -> Option<Descriptor> {
|
||||
};
|
||||
let mut using = desc("AdjL").with("Type", D::Descriptor(t));
|
||||
if let Some(opacity) = opacity {
|
||||
using = using.with("Opct", pct(opacity * 100.0));
|
||||
using = using.with("Opct", pct(recorded_percent(opacity)));
|
||||
}
|
||||
desc("null").with("null", class_ref("AdjL")).with("Usng", D::Descriptor(using))
|
||||
}
|
||||
|
||||
@@ -3237,14 +3237,13 @@ fn mapped_transform_reference_points_round_trip() {
|
||||
for (code, reference) in [
|
||||
("Qcsa", "center"),
|
||||
("Qcs0", "topLeft"),
|
||||
("Qcs1", "top"),
|
||||
("Qcs2", "topRight"),
|
||||
("Qcs3", "left"),
|
||||
("Qcs4", "center"),
|
||||
("Qcs4", "top"),
|
||||
("Qcs1", "topRight"),
|
||||
("Qcs7", "left"),
|
||||
("Qcs5", "right"),
|
||||
("Qcs6", "bottomLeft"),
|
||||
("Qcs7", "bottom"),
|
||||
("Qcs8", "bottomRight"),
|
||||
("Qcs3", "bottomLeft"),
|
||||
("Qcs6", "bottom"),
|
||||
("Qcs2", "bottomRight"),
|
||||
] {
|
||||
for target in [target_ref("Lyr "), D::Reference(vec![ReferenceItem::Property { class: class("Chnl"), key: Id::new("fsel") }])] {
|
||||
let d = desc("null")
|
||||
@@ -3268,8 +3267,19 @@ fn mapped_transform_reference_points_round_trip() {
|
||||
}
|
||||
}
|
||||
}
|
||||
let d = desc("null").with("null", target_ref("Lyr ")).with("FTcs", enm("QCSt", "unknown")).with("Wdth", pct(50.0));
|
||||
assert_eq!(mapped_recording("Trnf", d).actions[0].steps[0].0, PHOTOSHOP_STEP);
|
||||
// The reference application never records `Qcs8` or `Qcsi` (independent): kept steps.
|
||||
for code in ["unknown", "Qcs8", "Qcsi"] {
|
||||
let d = desc("null").with("null", target_ref("Lyr ")).with("FTcs", enm("QCSt", code)).with("Wdth", pct(50.0));
|
||||
assert_eq!(mapped_recording("Trnf", d).actions[0].steps[0].0, PHOTOSHOP_STEP, "{code}");
|
||||
}
|
||||
// A native anchor saves as the reference application's code; nothing writes `Qcs8`.
|
||||
for (reference, code) in [("top", "Qcs4"), ("topRight", "Qcs1"), ("bottom", "Qcs6"), ("bottomRight", "Qcs2")] {
|
||||
let native = Action::new("Native", vec![("edit.transform".into(), json!({"scaleX":50,"scaleY":50,"reference":reference}))]);
|
||||
let (bytes, _) = export("Anchors", &[&native], &[], false).unwrap();
|
||||
let saved = atn::parse(&bytes).unwrap();
|
||||
let d = saved.actions[0].items[0].descriptor.as_ref().unwrap();
|
||||
assert!(matches!(d.get("FTcs"), Some(D::Enumerated { value, .. }) if value.is(code)), "{reference}: {:?}", d.get("FTcs"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Alt-clicking an eye twice must isolate and then restore visibility, not just show a layer.
|
||||
@@ -3450,6 +3460,25 @@ fn mapped_close_keeps_ask_and_discard_distinct() {
|
||||
assert!(s.documents().is_empty());
|
||||
}
|
||||
|
||||
/// `"save": false` is the older spelling of discard: it closes a changed document and saves as
|
||||
/// "don't save" (`Svng N`); `true` and other values stay refused.
|
||||
#[test]
|
||||
fn close_save_false_discards_and_saves_as_dont_save() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", json!({"width":4,"height":4})).unwrap();
|
||||
s.execute("layer.new.layer", json!({})).unwrap();
|
||||
for refused in [json!(true), json!("yes"), json!(0)] {
|
||||
assert!(s.execute("file.close", json!({"save": refused})).is_err(), "{refused}");
|
||||
assert_eq!(s.documents().len(), 1);
|
||||
}
|
||||
s.execute("file.close", json!({"save": false})).unwrap();
|
||||
assert!(s.documents().is_empty());
|
||||
let native = Action::new("Close", vec![("file.close".into(), json!({"save": false}))]);
|
||||
let (bytes, _) = export("Close", &[&native], &[], false).unwrap();
|
||||
let d = atn::parse(&bytes).unwrap().actions[0].items[0].descriptor.clone().unwrap();
|
||||
assert!(matches!(d.get("Svng"), Some(D::Enumerated { type_id, value }) if type_id.is("YsN ") && value.is("N ")), "{:?}", d.get("Svng"));
|
||||
}
|
||||
|
||||
/// Lab Photo Filter colours use the CMS conversion and remain mapped after saving as RGB.
|
||||
#[test]
|
||||
fn mapped_lab_photo_filter_imports_and_round_trips() {
|
||||
|
||||
@@ -171,7 +171,6 @@ fn ref_planes(doc: &Document, layer: Option<LayerId>, active: Option<LayerId>, r
|
||||
.map(|p| p[3])
|
||||
.collect(),
|
||||
],
|
||||
LayerContent::Group(_) => return Err(EngineError::Other("group transparency sources are not supported".into())),
|
||||
// Adjustment and fill layers cover the whole canvas.
|
||||
_ => vec![vec![1.0; area.width() as usize * area.height() as usize]],
|
||||
},
|
||||
|
||||
@@ -307,7 +307,15 @@ pub(super) fn batch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut saved: Option<Arc<Document>> = None;
|
||||
for (id, params) in &steps {
|
||||
if batch_save_point(id) {
|
||||
saved = Some(scratch.active().ok_or(EngineError::NoDocument)?.doc.clone());
|
||||
let st = scratch.active_mut().ok_or(EngineError::NoDocument)?;
|
||||
saved = Some(st.doc.clone());
|
||||
// Batch performs the action's Save / Save As (to its destination), so the
|
||||
// document is saved there as far as the steps after it see: a Close recorded
|
||||
// without a save policy (`"save":"ask"`) then closes it. A Save a Copy leaves
|
||||
// the document as changed as it was.
|
||||
if id != "file.saveACopy" {
|
||||
st.saved_revision = st.revision;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let r = scratch.execute(id, params.clone())?;
|
||||
|
||||
@@ -1828,6 +1828,28 @@ fn batch_saves_at_the_recorded_save_so_a_close_after_it_keeps_the_result() {
|
||||
assert!(r["errors"][0]["error"].as_str().unwrap().contains("no active document"), "{r}");
|
||||
}
|
||||
|
||||
/// "…; Save As; Close" with the Close recorded on a clean document (no `Svng`, `"save":"ask"`):
|
||||
/// Batch performs the Save As, so the document is saved when the Close asks, and every file is
|
||||
/// written. The Close used to refuse the still-changed scratch document and fail each file.
|
||||
#[test]
|
||||
fn batch_save_as_then_close_that_asks_processes_every_file() {
|
||||
let dir = tmp("batch-save-close-ask");
|
||||
let input = vec![png(&dir, "red.png", 8, 8, "#ff0000"), png(&dir, "blue.png", 8, 8, "#0000ff")];
|
||||
let out = join(&dir, "out");
|
||||
let steps = json!([["image.adjustments.invert", {}], ["file.saveAs", {}], ["file.close", {"save": "ask"}]]);
|
||||
let r = Session::new().execute("file.automate.batch", json!({"input": input, "output": out, "format": "png", "steps": steps})).unwrap();
|
||||
assert_eq!(r["errors"], json!([]), "{r}");
|
||||
assert_eq!(r["files"].as_array().unwrap().len(), 2, "{r}");
|
||||
let red = photocraft_io::import("red.png", &std::fs::read(join(&out, "red.png")).unwrap()).unwrap().document;
|
||||
assert_eq!(photocraft_compose::render(&red, Rect::new(1, 1, 2, 2)).px[0], [0.0, 1.0, 1.0, 1.0], "the inverted red");
|
||||
// A Save a Copy doesn't save the document itself: the Close that asks still refuses it.
|
||||
let out = join(&dir, "copy");
|
||||
let steps = json!([["image.adjustments.invert", {}], ["file.saveACopy", {}], ["file.close", {"save": "ask"}]]);
|
||||
let r = Session::new().execute("file.automate.batch", json!({"input": input, "output": out, "format": "png", "steps": steps})).unwrap();
|
||||
assert_eq!(r["files"], json!([]), "{r}");
|
||||
assert_eq!(r["errors"].as_array().unwrap().len(), 2, "{r}");
|
||||
}
|
||||
|
||||
/// The document a recorded save marked is the result, not the one left active after it closed.
|
||||
/// Here the action saves and closes a second document it made, then inverts the input; the output
|
||||
/// used to be the inverted input, written in place of the result without an error.
|
||||
|
||||
@@ -159,6 +159,29 @@ pub(crate) fn recorded_step(s: &Session, id: &str, p: Value, pre_selected: Optio
|
||||
}
|
||||
return p;
|
||||
}
|
||||
if id == "select.loadSelection" {
|
||||
return recorded_source_layer(s, p, layer_id);
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// ⌘-clicking another layer's thumbnail loads that layer's transparency. The step refers to the
|
||||
/// layer the way the reference application records it, by name (or as the Background), so it
|
||||
/// replays in another document and saves to `.atn`. A layer that the name would not find (a
|
||||
/// same-named layer above it) keeps its id.
|
||||
fn recorded_source_layer(s: &Session, mut p: Value, id: LayerId) -> Value {
|
||||
let Some(doc) = s.active().map(|st| &st.doc) else { return p };
|
||||
let Some(layer) = doc.layer(id) else { return p };
|
||||
let reference = if doc.background().is_some_and(|b| b.id == id) {
|
||||
json!({"background": true})
|
||||
} else if doc.walk().into_iter().rev().find(|(_, _, l)| l.name == layer.name).is_some_and(|(_, _, l)| l.id == id) {
|
||||
json!({"name": layer.name})
|
||||
} else {
|
||||
return p;
|
||||
};
|
||||
if let Some(fields) = p.as_object_mut() {
|
||||
fields.insert("layer".into(), reference);
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ fn review_copy_cut_opacity_is_validated_before_any_step_runs() {
|
||||
fn review_layer_transform_uses_the_recorded_anchor() {
|
||||
for depth in [8, 16, 32] {
|
||||
for (code, fx, fy) in
|
||||
[("Qcs0", 0, 0), ("Qcs1", 1, 0), ("Qcs2", 2, 0), ("Qcs3", 0, 1), ("Qcs4", 1, 1), ("Qcs5", 2, 1), ("Qcs6", 0, 2), ("Qcs7", 1, 2), ("Qcs8", 2, 2)]
|
||||
[("Qcs0", 0, 0), ("Qcs4", 1, 0), ("Qcs1", 2, 0), ("Qcs7", 0, 1), ("Qcsa", 1, 1), ("Qcs5", 2, 1), ("Qcs3", 0, 2), ("Qcs6", 1, 2), ("Qcs2", 2, 2)]
|
||||
{
|
||||
let d = Descriptor::new("null")
|
||||
.with("FTcs", enm("QCSt", code))
|
||||
@@ -176,11 +176,15 @@ fn review_layer_transform_uses_the_recorded_anchor() {
|
||||
.unwrap();
|
||||
replay_saved(&mut s, &action);
|
||||
let state = s.active().unwrap();
|
||||
assert_eq!(
|
||||
state.doc.layer(state.active_layer.unwrap()).unwrap().surface().unwrap().content_bounds(),
|
||||
photocraft_geom::Rect::new(20 + 5 * fx, 20 + 5 * fy, 30 + 5 * fx, 30 + 5 * fy),
|
||||
"{code}, depth {depth}"
|
||||
);
|
||||
let bounds = state.doc.layer(state.active_layer.unwrap()).unwrap().surface().unwrap().content_bounds();
|
||||
assert_eq!(bounds, photocraft_geom::Rect::new(20 + 5 * fx, 20 + 5 * fy, 30 + 5 * fx, 30 + 5 * fy), "{code}, depth {depth}");
|
||||
// Measured in the reference application: Qcs6 keeps the bottom centre fixed, Qcs1 the
|
||||
// top-right corner.
|
||||
match code {
|
||||
"Qcs6" => assert_eq!(bounds, photocraft_geom::Rect::new(25, 30, 35, 40), "depth {depth}"),
|
||||
"Qcs1" => assert_eq!(bounds, photocraft_geom::Rect::new(30, 20, 40, 30), "depth {depth}"),
|
||||
_ => {}
|
||||
}
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
if code == "Qcs0" {
|
||||
s.edit("Mark rotation", |doc, active| {
|
||||
@@ -377,9 +381,10 @@ fn review_10702_transparency_names_resolve_at_playback() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A group has no supported transparency plane and must not overwrite a real selection.
|
||||
/// A group's transparency loads the whole canvas, as ⌘-clicking its thumbnail always did (the
|
||||
/// group's combined transparency is not composited yet), rather than failing.
|
||||
#[test]
|
||||
fn group_transparency_sources_fail_without_changing_selection() {
|
||||
fn group_transparency_sources_load_the_canvas() {
|
||||
use photocraft_doc::LayerId;
|
||||
use photocraft_geom::Rect;
|
||||
|
||||
@@ -409,19 +414,21 @@ fn group_transparency_sources_fail_without_changing_selection() {
|
||||
]),
|
||||
);
|
||||
let action = recorded(AtnEvent::Code(*b"setd"), descriptor.clone());
|
||||
let before = s.active().unwrap().doc.clone();
|
||||
let revision = s.active().unwrap().revision;
|
||||
let history = s.active().unwrap().history.past_len();
|
||||
let (id, params) = &action.steps[0];
|
||||
let result = s.execute(id, params.clone());
|
||||
let alpha = s.active().unwrap().doc.selection.as_ref().map(|mask| mask.read_region(Rect::from_xywh(0, 0, 2, 1)));
|
||||
assert!(result.is_err(), "group {name:?} returned {result:?} with selection {alpha:?}");
|
||||
assert_eq!(s.active().unwrap().doc, before);
|
||||
assert_eq!(s.active().unwrap().revision, revision);
|
||||
assert_eq!(s.active().unwrap().history.past_len(), history);
|
||||
let doc = &s.active().unwrap().doc;
|
||||
let alpha = doc.selection.as_ref().map(|mask| mask.read_region(doc.bounds())).unwrap_or(vec![1.0; 2]);
|
||||
assert!(result.is_ok(), "group {name:?} returned {result:?}");
|
||||
assert_eq!(alpha, vec![1.0, 1.0], "group {name:?}");
|
||||
// ⌘-click on the group's thumbnail (by id) loads the same.
|
||||
s.execute("select.deselect", json!({})).unwrap();
|
||||
s.execute("select.loadSelection", json!({"channel":"transparency","layer":group.0})).unwrap();
|
||||
let doc = &s.active().unwrap().doc;
|
||||
assert_eq!(doc.selection.as_ref().map(|mask| mask.read_region(doc.bounds())).unwrap_or(vec![1.0; 2]), vec![1.0, 1.0]);
|
||||
assert_eq!(saved(&action).descriptor, Some(descriptor));
|
||||
|
||||
// The group's explicit mask remains a valid source.
|
||||
s.execute("select.loadSelection", json!({"channel":"transparency","layer":"Child"})).unwrap();
|
||||
s.execute("layer.layerMask.revealSelection", json!({"layer":group.0})).unwrap();
|
||||
assert!(s.active().unwrap().doc.selection.is_none());
|
||||
s.execute("select.loadSelection", json!({"channel":"mask","layer":params["layer"]})).unwrap();
|
||||
@@ -557,7 +564,8 @@ fn recorded_adjustment_opacity_keeps_gradient_settings_and_defaults() {
|
||||
}
|
||||
let written = saved(&action).descriptor.unwrap();
|
||||
let Some(D::Descriptor(using)) = written.get("Usng") else { panic!("missing adjustment metadata") };
|
||||
assert!(matches!(using.get("Opct"),Some(D::UnitFloat{unit,value}) if unit==b"#Prc" && (*value-179.0/255.0*100.0).abs()<1e-10));
|
||||
// The recorded 70 % saves back as 70 %, not as the 8-bit level 179/255 (70.196 %).
|
||||
assert!(matches!(using.get("Opct"), Some(D::UnitFloat { unit, value }) if unit == b"#Prc" && *value == 70.0), "{:?}", using.get("Opct"));
|
||||
for depth in [8, 16, 32] {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", json!({"width":4,"height":3,"depth":depth})).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user