From 8028f468083cbfd5978fa2e4907b435853fed60c Mon Sep 17 00:00:00 2001 From: Valentin Klein Date: Fri, 13 Nov 2026 00:15:49 +0100 Subject: [PATCH] .aep import: a test pins the Orientation expression import (#3087) (#3313) * .aep import: Photoshop layers, placeholder effects, shape direction and Orientation expressions (#3087) - A footage item showing one Photoshop layer shows that layer (record index, bounds and name from its opti) and is named /; an adjustment layer shows white where it applies (opt-in through SourceLayer::adjustment_white, the direct Photoshop import is unchanged). - Effects without a counterpart arrive as placeholder effects with their parameters, so expressions reading them keep working; pseudo effects are no loss, a Dropdown Menu Control becomes EffectCraft's. Slider ranges are checked, sizes are bounded. - The loss prompt shows every detail line the report keeps and how many more there are. - Shape direction and gradient highlight records in the default state are no loss. - An expression on Orientation is imported with its value or keys. * .aep import: report placeholder records and popup defaults that borrowed definitions leave open (#3087) A value record the borrowed definitions do not name is reported instead of dropped, and a borrowed popup without a record reports that its default item is not known (the first is used). * Merge upstream/main into aep-import-bugs-3087 --- crates/aep/tests/it/import.rs | 1 + crates/aep/tests/it/import/issue_3087.rs | 90 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 crates/aep/tests/it/import/issue_3087.rs diff --git a/crates/aep/tests/it/import.rs b/crates/aep/tests/it/import.rs index 240c3ea0f..55f81aed1 100644 --- a/crates/aep/tests/it/import.rs +++ b/crates/aep/tests/it/import.rs @@ -18,6 +18,7 @@ mod effects_round2; mod effects_round3; mod effects_thirdparty; mod fx_inject; +mod issue_3087; mod lumetri; mod markers; mod masks; diff --git a/crates/aep/tests/it/import/issue_3087.rs b/crates/aep/tests/it/import/issue_3087.rs new file mode 100644 index 000000000..bf8b575f4 --- /dev/null +++ b/crates/aep/tests/it/import/issue_3087.rs @@ -0,0 +1,90 @@ +//! #3087: an expression on Orientation, built into the committed `synthetic.aep` (no external +//! files; the values are made up, the layouts are docs/aep-format.md's). The Photoshop layers and +//! placeholders of the issue are tested in `synthetic.rs` and `placeholders.rs`. + +use effectcraft_aep::import::{ImportOptions, ImportResult, LossCategory, import}; +use effectcraft_aep::{Body, Chunk, parse, write}; +use effectcraft_keyframe::Value; +use effectcraft_project::Project; + +const SYNTHETIC: &[u8] = include_bytes!("../../fixtures/synthetic.aep"); + +fn visit(chunks: &mut [Chunk<'_>], f: &mut dyn FnMut(&mut Chunk<'_>)) { + for c in chunks { + f(c); + visit(c.children_mut(), f); + } +} + +fn import_all(bytes: &[u8]) -> (Project, ImportResult) { + let mut project = Project::default(); + let none = |_: &str| false; + let r = import(&mut project, bytes, &ImportOptions { allow_losses: true, ..ImportOptions::new(&none) }).unwrap(); + (project, r) +} + +/// `synthetic.aep` with every Orientation set to `[10, 20, 30]` and the expression `text`. +fn with_orientation_expression(text: &str) -> Vec { + let mut file = parse(SYNTHETIC).unwrap(); + let mut patched = 0; + visit(&mut file.chunks, &mut |c| { + if !c.is_list(b"otst") { + return; + } + for k in c.children_mut() { + if k.is_list(b"tdbs") { + let kids = k.children_mut(); + for r in kids.iter_mut() { + if let Body::Data(d) = &mut r.body { + let d = d.to_mut(); + match &r.id { + b"tdsb" => d.copy_from_slice(&1u32.to_be_bytes()), + b"tdb4" => { + d[119] = 0; + d[120] = 1; + } + _ => {} + } + } + } + let Body::List { children, .. } = &mut k.body else { continue }; + children.push(Chunk::data(*b"Utf8", text.as_bytes().to_vec())); + } else if k.is_list(b"otky") { + let mut otda = Vec::new(); + for v in [10.0f64, 20.0, 30.0] { + otda.extend(v.to_be_bytes()); + } + *k = Chunk::list(*b"otky", vec![Chunk::data(*b"otda", otda)]); + } + } + patched += 1; + }); + assert!(patched > 0, "the synthetic project has an Orientation"); + write(&file).unwrap() +} + +/// An expression on Orientation arrives with the value (both were dropped and reported "not +/// decoded"). +#[test] +fn an_expression_on_orientation_is_imported() { + let (p, r) = import_all(&with_orientation_expression("[1, 2, 3]")); + let mut seen = 0; + for (_, c) in p.comps() { + for l in &c.layers { + let Some(o) = l.props.prop("transform/orientation") else { continue }; + assert_eq!(o.value, Value::Vec3([10.0, 20.0, 30.0]), "{}", l.name); + assert_eq!(o.expr.as_ref().map(|e| (e.text.as_str(), e.enabled)), Some(("[1, 2, 3]", true)), "{}", l.name); + seen += 1; + } + } + assert!(seen > 0); + let orientation: Vec = r + .report + .losses + .iter() + .filter(|l| l.category == LossCategory::TransformProperties) + .flat_map(|l| l.details.clone()) + .filter(|d| d.contains("Orientation")) + .collect(); + assert!(orientation.is_empty(), "{orientation:?}"); +}