From 6b7731d05a9a1161bef809f56b164887c46dcc4c Mon Sep 17 00:00:00 2001 From: arnav-hiwarkar Date: Wed, 11 Nov 2026 16:15:54 +0530 Subject: [PATCH] #3365: the losses prompt shows every detail line and how many have none MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prompt showed 3 detail lines per category under a bare count, which read as the project's total. The headline now says how many occurrences are left out or changed, every line the report keeps shows, and "… and N more" counts the rest. The import report records that number as `unlisted` (an unmapped effect's line covers all its instances); unmapped effects are listed most used first. Co-Authored-By: Claude Opus 5.5 --- crates/aep/src/import/report.rs | 45 ++++++++++++++++++++++--- crates/ui-egui/src/i18n/ui.rs | 22 ++++++++++++ crates/ui-egui/src/panels/aep_losses.rs | 39 ++++++++++++++++----- docs/aep-import.md | 10 ++++-- 4 files changed, 99 insertions(+), 17 deletions(-) diff --git a/crates/aep/src/import/report.rs b/crates/aep/src/import/report.rs index 2825b61b3..aa50f4dff 100644 --- a/crates/aep/src/import/report.rs +++ b/crates/aep/src/import/report.rs @@ -98,8 +98,12 @@ pub struct Loss { pub category: LossCategory, /// How many occurrences (exact). pub count: usize, - /// Up to [`MAX_DETAILS`] example lines. + /// Example lines: up to [`MAX_DETAILS`], and for effects with no counterpart one more line per + /// effect (with its number of instances). pub details: Vec, + /// How many of the `count` occurrences no line of `details` describes. + #[serde(default)] + pub unlisted: usize, } /// What arrived in the project. @@ -281,7 +285,7 @@ const MAX_UNMAPPED_EFFECTS: usize = 100; impl Collector { /// Records one loss. pub fn lose(&mut self, category: LossCategory, detail: impl Into) { - let loss = self.losses.entry(category).or_insert_with(|| Loss { category, count: 0, details: Vec::new() }); + let loss = self.losses.entry(category).or_insert_with(|| Loss { category, count: 0, details: Vec::new(), unlisted: 0 }); loss.count = loss.count.saturating_add(1); if loss.details.len() < MAX_DETAILS { loss.details.push(detail.into()); @@ -292,7 +296,8 @@ impl Collector { /// made in [`Collector::finish`], one per effect with the number of instances, so that many /// other losses cannot hide which effects were dropped. pub fn lose_unmapped_effect(&mut self, match_name: &str, detail: String) { - let loss = self.losses.entry(LossCategory::Effects).or_insert_with(|| Loss { category: LossCategory::Effects, count: 0, details: Vec::new() }); + let loss = + self.losses.entry(LossCategory::Effects).or_insert_with(|| Loss { category: LossCategory::Effects, count: 0, details: Vec::new(), unlisted: 0 }); loss.count = loss.count.saturating_add(1); if let Some(seen) = self.unmapped_effects.get_mut(match_name) { seen.1 = seen.1.saturating_add(1); @@ -303,12 +308,22 @@ impl Collector { } pub fn finish(mut self) -> ImportReport { + // Every line describes one occurrence, except an unmapped effect's, which describes all + // its instances. + for loss in self.losses.values_mut() { + loss.unlisted = loss.count.saturating_sub(loss.details.len()); + } if let Some(loss) = self.losses.get_mut(&LossCategory::Effects) { - let named = std::mem::take(&mut self.unmapped_effects).into_values().map(|(first, n)| match n { + // The most used effects first. + let mut named: Vec<(String, usize)> = std::mem::take(&mut self.unmapped_effects).into_values().collect(); + named.sort_by(|a, b| b.1.cmp(&a.1)); + let described = named.iter().fold(0usize, |sum, (_, n)| sum.saturating_add(*n)); + loss.unlisted = loss.unlisted.saturating_sub(described); + let lines = named.into_iter().map(|(first, n)| match n { 1 => first, _ => format!("{first}, and {} more like it", n - 1), }); - loss.details.splice(0..0, named); + loss.details.splice(0..0, lines); } ImportReport { imported: self.imported, @@ -355,6 +370,25 @@ mod tests { // One instance stays the single line it was. assert_eq!(named("(Pseudo/)"), ["C: 'D' has the effect 'Dropdown' (Pseudo/)"]); assert!(effects.details.len() <= MAX_DETAILS + 2, "the examples stay bounded"); + // 20 parameter lines, the cylinders' line and the dropdown's describe 46; 10 parameters + // have no line. + assert_eq!(effects.unlisted, 10); + // The most used effect comes first. + assert!(effects.details[0].contains("(CC Cylinder)"), "{:?}", effects.details); + } + + /// #3365: each category says how many of its occurrences have no example line. + #[test] + fn a_loss_counts_the_occurrences_without_a_line() { + let mut col = Collector::default(); + for i in 0..27 { + col.lose(LossCategory::Masks, format!("mask {i}")); + } + col.lose(LossCategory::ShapeLayers, "one"); + let report = col.finish(); + let unlisted = |c| report.losses.iter().find(|l| l.category == c).map(|l| (l.details.len(), l.unlisted)); + assert_eq!(unlisted(LossCategory::Masks), Some((MAX_DETAILS, 7))); + assert_eq!(unlisted(LossCategory::ShapeLayers), Some((1, 0))); } /// A file with more than [`MAX_UNMAPPED_EFFECTS`] different unmapped effects names that many @@ -369,5 +403,6 @@ mod tests { let effects = report.losses.iter().find(|l| l.category == LossCategory::Effects).unwrap(); assert_eq!(effects.count, MAX_UNMAPPED_EFFECTS + 1, "every instance is counted"); assert_eq!(effects.details.len(), MAX_UNMAPPED_EFFECTS, "{} lines", effects.details.len()); + assert_eq!(effects.unlisted, 1, "the effect past the limit has no line"); } } diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs index 1a162dab3..b2e0b3631 100644 --- a/crates/ui-egui/src/i18n/ui.rs +++ b/crates/ui-egui/src/i18n/ui.rs @@ -245,6 +245,8 @@ const JAPANESE: &[(&str, &str)] = &[ ("Import Adobe After Effects Project", "Adobe After Effectsプロジェクトを読み込み"), ("Some parts of this project cannot be imported exactly (left out or changed):", "このプロジェクトの一部は正確に読み込めません(省略または変更されます):"), ("Import Anyway", "このまま読み込み"), + ("{}: {} left out or changed", "{}:{} 件が省略または変更"), + ("… and {} more", "…ほか {} 件"), ("Casts Shadows", "シャドウを落とす"), ("Change Renderer…", "レンダラーを変更…"), ( @@ -1386,6 +1388,8 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Import Adobe After Effects Project", "导入 Adobe After Effects 项目"), ("Some parts of this project cannot be imported exactly (left out or changed):", "此项目的某些部分无法准确导入(将被省略或更改):"), ("Import Anyway", "仍然导入"), + ("{}: {} left out or changed", "{}:{} 项被省略或更改"), + ("… and {} more", "…以及另外 {} 项"), ("Casts Shadows", "投射阴影"), ("Change Renderer…", "更改渲染器…"), ("Changing a shortcut creates a custom preset; the default stays as it is.", "修改快捷键会创建自定义预设;默认预设保持不变。"), @@ -2505,6 +2509,8 @@ const SWEDISH: &[(&str, &str)] = &[ "Vissa delar av projektet kan inte importeras exakt (utelämnas eller ändras):", ), ("Import Anyway", "Importera ändå"), + ("{}: {} left out or changed", "{}: {} utelämnas eller ändras"), + ("… and {} more", "… och {} till"), ("Casts Shadows", "Kastar skuggor"), ("Change Renderer…", "Byt renderare…"), ( @@ -3778,6 +3784,8 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Import Adobe After Effects Project", "匯入 Adobe After Effects 專案"), ("Some parts of this project cannot be imported exactly (left out or changed):", "此專案的部分內容無法準確匯入(將被省略或變更):"), ("Import Anyway", "仍要匯入"), + ("{}: {} left out or changed", "{}:{} 項被省略或變更"), + ("… and {} more", "…以及另外 {} 項"), ("Casts Shadows", "投射陰影"), ("Change Renderer…", "更改算圖引擎…"), ("Changing a shortcut creates a custom preset; the default stays as it is.", "修改快速鍵會創建自定義預設集;預設預設集保持不變。"), @@ -4847,6 +4855,8 @@ const UKRAINIAN: &[(&str, &str)] = &[ "Деякі частини цього проєкту неможливо імпортувати точно (їх пропущено або змінено):", ), ("Import Anyway", "Усе одно імпортувати"), + ("{}: {} left out or changed", "{}: {} пропущено або змінено"), + ("… and {} more", "… і ще {}"), ("Casts Shadows", "Відкидає тіні"), ("Change Renderer…", "Змінити рушій рендерингу…"), ( @@ -5428,6 +5438,8 @@ const PORTUGUESE: &[(&str, &str)] = &[ "Algumas partes deste projeto não podem ser importadas exatamente (omitidas ou alteradas):", ), ("Import Anyway", "Importar mesmo assim"), + ("{}: {} left out or changed", "{}: {} omitidos ou alterados"), + ("… and {} more", "… e mais {}"), ("Casts Shadows", "Projeta sombras"), ("Change Renderer…", "Alterar renderizador…"), ( @@ -6201,6 +6213,8 @@ const POLISH: &[(&str, &str)] = &[ "Niektórych części tego projektu nie można zaimportować dokładnie (zostaną pominięte lub zmienione):", ), ("Import Anyway", "Importuj mimo to"), + ("{}: {} left out or changed", "{}: {} pominięto lub zmieniono"), + ("… and {} more", "… i {} więcej"), ("Casts Shadows", "Rzuca cienie"), ("Change Renderer…", "Zmień renderer…"), ( @@ -6957,6 +6971,8 @@ const CZECH: &[(&str, &str)] = &[ "Některé části tohoto projektu nelze importovat přesně (budou vynechány nebo změněny):", ), ("Import Anyway", "Přesto importovat"), + ("{}: {} left out or changed", "{}: {} vynecháno nebo změněno"), + ("… and {} more", "… a {} dalších"), ("Casts Shadows", "Vrhat stíny"), ("Change Renderer…", "Změnit vykreslovací modul…"), ( @@ -7532,6 +7548,8 @@ const SPANISH: &[(&str, &str)] = &[ "Algunas partes de este proyecto no se pueden importar con exactitud (se omiten o cambian):", ), ("Import Anyway", "Importar de todos modos"), + ("{}: {} left out or changed", "{}: {} omitidos o cambiados"), + ("… and {} more", "… y {} más"), ("Casts Shadows", "Proyecta sombras"), ("Change Renderer…", "Cambiar motor de procesamiento…"), ( @@ -8098,6 +8116,8 @@ const GEORGIAN: &[(&str, &str)] = &[ "ამ პროექტის ზოგიერთი ნაწილის ზუსტად იმპორტი შეუძლებელია (გამოტოვებული ან შეცვლილი):", ), ("Import Anyway", "მაინც იმპორტი"), + ("{}: {} left out or changed", "{}: {} გამოტოვებული ან შეცვლილი"), + ("… and {} more", "… და კიდევ {}"), ("Casts Shadows", "ჩრდილს აგდებს"), ("Change Renderer…", "რენდერერის შეცვლა…"), ("Changing a shortcut creates a custom preset; the default stays as it is.", "მალსახმობის შეცვლა ქმნის მორგებულ პრესეტს; ნაგულისხმევი უცვლელი რჩება."), @@ -9070,6 +9090,8 @@ const FRENCH: &[(&str, &str)] = &[ "Certaines parties de ce projet ne peuvent pas être importées à l’identique (omises ou modifiées) :", ), ("Import Anyway", "Importer quand même"), + ("{}: {} left out or changed", "{} : {} omis ou modifiés"), + ("… and {} more", "… et {} de plus"), ("Casts Shadows", "Projette des ombres"), ("Change Renderer…", "Changer de moteur de rendu…"), ( diff --git a/crates/ui-egui/src/panels/aep_losses.rs b/crates/ui-egui/src/panels/aep_losses.rs index 4eb71104d..4674c369a 100644 --- a/crates/ui-egui/src/panels/aep_losses.rs +++ b/crates/ui-egui/src/panels/aep_losses.rs @@ -8,7 +8,7 @@ //! //! Automation ids: `dialog.aepLosses.continue`, `dialog.aepLosses.cancel`. -use crate::i18n::tr; +use crate::i18n::{tr, tr_args}; use egui::vec2; use serde_json::{Value, json}; @@ -23,9 +23,6 @@ pub struct Pending { pub queue: Vec, } -/// How many detail lines of a loss category the prompt shows. -const DETAILS: usize = 3; - /// Whether the prompt is showing with a conversion waiting on it. pub fn asking(app: &EffectcraftApp) -> bool { app.dialog == Some(Dialog::AepLosses) && app.dialog_state.aep_losses.command.is_some() @@ -77,14 +74,24 @@ fn category(c: &str) -> String { chars.next().map(|f| f.to_uppercase().chain(chars).collect()).unwrap_or_default() } -/// The report's losses as lines: one per category with its count, and a few of its details. +/// The report's losses as lines: one per category with how many occurrences are left out or +/// changed, then every detail line the report keeps and "… and N more" for the occurrences none +/// describes (#3365). pub fn lines(report: &Value) -> Vec<(String, Vec)> { let Some(losses) = report["losses"].as_array() else { return vec![] }; losses .iter() .map(|l| { - let head = format!("{} ({})", category(l["category"].as_str().unwrap_or("other")), l["count"].as_u64().unwrap_or(0)); - let details = l["details"].as_array().map(|d| d.iter().filter_map(Value::as_str).take(DETAILS).map(str::to_string).collect()).unwrap_or_default(); + let count = l["count"].as_u64().unwrap_or(0); + let head = tr_args("{}: {} left out or changed", &[&category(l["category"].as_str().unwrap_or("other")), &count]); + let mut details: Vec = + l["details"].as_array().map(|d| d.iter().filter_map(Value::as_str).map(str::to_string).collect()).unwrap_or_default(); + // An export report has no `unlisted`: each of its lines is one occurrence. + let listed = u64::try_from(details.len()).unwrap_or(u64::MAX); + let unlisted = l["unlisted"].as_u64().unwrap_or_else(|| count.saturating_sub(listed)); + if unlisted > 0 { + details.push(tr_args("… and {} more", &[&unlisted])); + } (head, details) }) .collect() @@ -179,11 +186,25 @@ mod tests { fn the_report_reads_as_categories_with_counts_and_details() { let report = json!({"losses": [{"category": "layer_styles", "count": 4, "details": ["a", "b", "c", "d"]}, {"category": "effects", "count": 1}]}); let l = lines(&report); - assert_eq!(l[0], ("Layer styles (4)".to_string(), vec!["a".to_string(), "b".into(), "c".into()])); - assert_eq!(l[1], ("Effects (1)".to_string(), vec![])); + assert_eq!(l[0], ("Layer styles: 4 left out or changed".to_string(), vec!["a".to_string(), "b".into(), "c".into(), "d".into()])); + assert_eq!(l[1], ("Effects: 1 left out or changed".to_string(), vec!["… and 1 more".to_string()])); assert!(lines(&json!({})).is_empty()); } + /// #3365: every detail line shows, then how many occurrences have none: the report's + /// `unlisted` (an effect line can describe many instances), else the lines past the count. + #[test] + fn every_detail_shows_and_the_rest_is_counted() { + let details: Vec = (0..20).map(|i| format!("d{i}")).collect(); + let l = lines(&json!({"losses": [{"category": "masks", "count": 27, "details": details}]})); + assert_eq!(l[0].1.len(), 21); + assert_eq!(l[0].1[20], "… and 7 more"); + let l = lines(&json!({"losses": [{"category": "effects", "count": 50, "details": ["X, and 44 more like it"], "unlisted": 5}]})); + assert_eq!(l[0].1, ["X, and 44 more like it", "… and 5 more"]); + let l = lines(&json!({"losses": [{"category": "effects", "count": 45, "details": ["X, and 44 more like it"], "unlisted": 0}]})); + assert_eq!(l[0].1, ["X, and 44 more like it"]); + } + /// Projects dropped or picked together wait only behind a prompt that shows; with none /// showing they are tried at once (a missing file is an error naming it). #[test] diff --git a/docs/aep-import.md b/docs/aep-import.md index c704e0a89..3713ca0fe 100644 --- a/docs/aep-import.md +++ b/docs/aep-import.md @@ -1,6 +1,6 @@ # After Effects project import -> **Last reviewed:** 2026-11-05 · **Last updated:** 2026-11-10 · **Change:** dropped projects and projects picked in File ▸ Import ▸ File… import as projects, several asked about one at a time; `file.import` hands them to `file.importAep`; the menu entries are named Adobe After Effects Project… (#3103); before that: After Effects' Puppet arrives as EffectCraft's Puppet without its meshes and pins, which are reported mesh by mesh (#3093; the storage is not sampled yet, aep-format 3.7); before that: Lumetri Color's master RGB curve, Hue vs Sat curve and colour wheels are imported (1200 presets made by hand); before that: Lumetri Color is imported (1200: every slider, checkbox and popup with a counterpart, by match name, also from records in the default state; switched-off sections, LUTs, looks, undecoded curves and wheels are reported); before that: the report names the After Effects version a file is written for and the one that wrote it (`aeVersion`); copies saved as 25.x and 24.x import like the 2026 save; before that: Curves and Mesh Warp are imported (961-965, 985; FORMAT S of the corpus); before that: a shape item's eye switch is imported (bit 0 of its group flags, 986 / 987: a switched-off Fill under Trim Paths is no longer drawn, render equal to After Effects), and a layer style's (switch group `tdsb` 0, 988); before that: a negative time stretch (Time Reverse Layer) is imported: in and out point in order, the layer plays backwards (981-983: render equal to After Effects at 7 points); before that: After Effects' None on Compound Blur, Displacement Map and Set Matte arrives as an explicit None (`NO_LAYER`), not as the owning layer; before that: layer styles are imported (the nine styles and Blending Options, 951-979; Global Light becomes the composition's); before that: the 27 effects of round 3 are in the table (`effectcraft_aep::effect_map`); before that: a layer of a clip, sequence or sound ends at its source's end (After Effects ends it there when it opens a project, read-back test f1), unless it is remapped or looped or runs less than a frame past; before that: keys of shape colours are read (the reader took their 152-byte spatial records for plain ones: `tdb4` byte 7 is 0 there, 854); before that: the effect table moved to `effectcraft_aep::effect_map`; point defaults in percent and a popup default slot of 0 (After Effects' save of a project it opened, e1 / e2 re-saves) are read; before that: the justification of right-to-left paragraphs is read relative to the direction (t7); before that: 24 more layer blend modes (906, 907); before that: 27 more effects are imported (batch 6: 903-905), with layer parameters; before that: a Rec. 709 working space also sets the output space (After Effects renders in the working space, 902); before that: a mask whose path was never set arrives as the source rectangle (900, 901); before that: Offset Paths' Copy Offset arrives as EffectCraft's (After Effects' value minus 1, 828); before that: project settings, image sequences, conformed clips and effect instance names are imported, next to the mattes, shapes, text settings and animators, effects, markers, time remapping, cameras and lights); before that: parallel and ambient lights (701, 702) and keyed Orientation (703) are imported; before that: masks on text and shape layers (705, 706: AE vs EffectCraft render 12 and 0 of 5184 points, glyph edges) and Merge Paths, Offset Paths, Repeater (707-710, 731; merge renders equal but for edges; Repeater copy opacity and Offset Paths' outline differ in EffectCraft's renderer) are imported; before that: keyframes: the ease of Anchor Point, effect point, effect colour and mask path keys is imported (597-599, 700, 730: within 0.5 px of After Effects' read-backs); before that: effects: Directional Blur, Radial Blur, CC Radial Blur, Venetian Blinds, Channel Blur, Black & White, Tritone, Luma Key, Simple Choker, Matte Choker and Transform are imported (711-723; render checks: CC Radial Blur and Channel Blur equal, Directional Blur within 5/255; Venetian Blinds, Radial Blur, Black & White (8/255), Tritone (19/255), Luma Key, Matte Choker and Transform differ in EffectCraft's renderer, see STATUS); before that: text: character and paragraph settings (stroke, scales, baseline shift, kerning, ligatures, tsume, indents, spacing, justified text, composer, direction, vertical text; 538-578) and text animators with range selectors (579-594, 114) are imported; render checks 594 (keyed range selector at 0 / 0.5 / 1 / 1.5 / 2.5 s), 595, 596 equal but for glyph edges; before that: cameras, spot and point lights, X / Y Rotation and Orientation, Timewarp and expressions on effect parameters are imported (render checks 486, 535-537 equal but for edges; 488 differs by 5.25 %, cause in EffectCraft's renderer not isolated); before that: text: superscript and subscript are imported (526-528); before that: effects: Invert, Fast Box Blur, Gaussian Blur, Drop Shadow, Glow, Fill, Hue/Saturation, Levels and five expression controls are imported, popups are mapped by item name, checkbox defaults come from the file; before that: shape layers are imported (groups, paths, paints, trim paths, round corners; 451-484, 519 added); before that: markers and Time Remapping are imported (509, 510 added); before that: track mattes are imported (402-408); before that: text: automatic leading is verified (400); effects: Exposure is imported (Channels, all twelve sliders, Bypass, keys), next to Tint and Gradient Ramp; before that: text: U+0003 is a paragraph break (386, 387); before that: a stored text Anchor Point is pixels (385, now V); before that: effects: Tint and Gradient Ramp are imported (all parameters and keys), other effects stay reported; `.aepx` is accepted; text layers are imported (Source Text with style runs, paragraphs and box text; fonts by PostScript name, missing fonts listed); layer styles are reported; before that: layer masks · **Target:** Adobe After Effects 2026 +> **Last reviewed:** 2026-11-05 · **Last updated:** 2026-11-11 · **Change:** the losses prompt shows every detail line and how many have none (`unlisted`); unmapped effects are listed most used first (#3365); before that: dropped projects and projects picked in File ▸ Import ▸ File… import as projects, several asked about one at a time; `file.import` hands them to `file.importAep`; the menu entries are named Adobe After Effects Project… (#3103); before that: After Effects' Puppet arrives as EffectCraft's Puppet without its meshes and pins, which are reported mesh by mesh (#3093; the storage is not sampled yet, aep-format 3.7); before that: Lumetri Color's master RGB curve, Hue vs Sat curve and colour wheels are imported (1200 presets made by hand); before that: Lumetri Color is imported (1200: every slider, checkbox and popup with a counterpart, by match name, also from records in the default state; switched-off sections, LUTs, looks, undecoded curves and wheels are reported); before that: the report names the After Effects version a file is written for and the one that wrote it (`aeVersion`); copies saved as 25.x and 24.x import like the 2026 save; before that: Curves and Mesh Warp are imported (961-965, 985; FORMAT S of the corpus); before that: a shape item's eye switch is imported (bit 0 of its group flags, 986 / 987: a switched-off Fill under Trim Paths is no longer drawn, render equal to After Effects), and a layer style's (switch group `tdsb` 0, 988); before that: a negative time stretch (Time Reverse Layer) is imported: in and out point in order, the layer plays backwards (981-983: render equal to After Effects at 7 points); before that: After Effects' None on Compound Blur, Displacement Map and Set Matte arrives as an explicit None (`NO_LAYER`), not as the owning layer; before that: layer styles are imported (the nine styles and Blending Options, 951-979; Global Light becomes the composition's); before that: the 27 effects of round 3 are in the table (`effectcraft_aep::effect_map`); before that: a layer of a clip, sequence or sound ends at its source's end (After Effects ends it there when it opens a project, read-back test f1), unless it is remapped or looped or runs less than a frame past; before that: keys of shape colours are read (the reader took their 152-byte spatial records for plain ones: `tdb4` byte 7 is 0 there, 854); before that: the effect table moved to `effectcraft_aep::effect_map`; point defaults in percent and a popup default slot of 0 (After Effects' save of a project it opened, e1 / e2 re-saves) are read; before that: the justification of right-to-left paragraphs is read relative to the direction (t7); before that: 24 more layer blend modes (906, 907); before that: 27 more effects are imported (batch 6: 903-905), with layer parameters; before that: a Rec. 709 working space also sets the output space (After Effects renders in the working space, 902); before that: a mask whose path was never set arrives as the source rectangle (900, 901); before that: Offset Paths' Copy Offset arrives as EffectCraft's (After Effects' value minus 1, 828); before that: project settings, image sequences, conformed clips and effect instance names are imported, next to the mattes, shapes, text settings and animators, effects, markers, time remapping, cameras and lights); before that: parallel and ambient lights (701, 702) and keyed Orientation (703) are imported; before that: masks on text and shape layers (705, 706: AE vs EffectCraft render 12 and 0 of 5184 points, glyph edges) and Merge Paths, Offset Paths, Repeater (707-710, 731; merge renders equal but for edges; Repeater copy opacity and Offset Paths' outline differ in EffectCraft's renderer) are imported; before that: keyframes: the ease of Anchor Point, effect point, effect colour and mask path keys is imported (597-599, 700, 730: within 0.5 px of After Effects' read-backs); before that: effects: Directional Blur, Radial Blur, CC Radial Blur, Venetian Blinds, Channel Blur, Black & White, Tritone, Luma Key, Simple Choker, Matte Choker and Transform are imported (711-723; render checks: CC Radial Blur and Channel Blur equal, Directional Blur within 5/255; Venetian Blinds, Radial Blur, Black & White (8/255), Tritone (19/255), Luma Key, Matte Choker and Transform differ in EffectCraft's renderer, see STATUS); before that: text: character and paragraph settings (stroke, scales, baseline shift, kerning, ligatures, tsume, indents, spacing, justified text, composer, direction, vertical text; 538-578) and text animators with range selectors (579-594, 114) are imported; render checks 594 (keyed range selector at 0 / 0.5 / 1 / 1.5 / 2.5 s), 595, 596 equal but for glyph edges; before that: cameras, spot and point lights, X / Y Rotation and Orientation, Timewarp and expressions on effect parameters are imported (render checks 486, 535-537 equal but for edges; 488 differs by 5.25 %, cause in EffectCraft's renderer not isolated); before that: text: superscript and subscript are imported (526-528); before that: effects: Invert, Fast Box Blur, Gaussian Blur, Drop Shadow, Glow, Fill, Hue/Saturation, Levels and five expression controls are imported, popups are mapped by item name, checkbox defaults come from the file; before that: shape layers are imported (groups, paths, paints, trim paths, round corners; 451-484, 519 added); before that: markers and Time Remapping are imported (509, 510 added); before that: track mattes are imported (402-408); before that: text: automatic leading is verified (400); effects: Exposure is imported (Channels, all twelve sliders, Bypass, keys), next to Tint and Gradient Ramp; before that: text: U+0003 is a paragraph break (386, 387); before that: a stored text Anchor Point is pixels (385, now V); before that: effects: Tint and Gradient Ramp are imported (all parameters and keys), other effects stay reported; `.aepx` is accepted; text layers are imported (Source Text with style runs, paragraphs and box text; fonts by PostScript name, missing fonts listed); layer styles are reported; before that: layer masks · **Target:** Adobe After Effects 2026 File ▸ Import ▸ Adobe After Effects Project… (`file.importAep`) reads an `.aep` or its XML form `.aepx` and adds its items to the open project, in a folder named after the file (as After Effects does when it imports a project). @@ -322,8 +322,12 @@ parameter gets the file's value: ## What is reported `ImportReport` (`serde`, returned as JSON) has `imported` (counts, including `textLayers`), -`losses` (per category: `count` exact, up to 20 `details`), `missingFootage`, `missingFonts` -(PostScript names; not losses), `limitations` and `aeVersion`. The categories: +`losses` (per category: `count` exact; up to 20 `details`, plus one line per unmapped effect with its +number of instances, the most used first; `unlisted`, how many of the `count` no line describes), +`missingFootage`, `missingFonts` +(PostScript names; not losses), `limitations` and `aeVersion`. The losses prompt shows each +category with how many occurrences are left out or changed, every detail line, and "… and N more" +for the `unlisted` ones. The categories: | Category | Counted | |---|---|