Merge pull request #118 from storytold/claude/system-fonts-rmnw4m
G2: font menus list the installed fonts
This commit is contained in:
commit
c750b299cf
|
|
@ -97,8 +97,8 @@ Paragraph, Align, Info, Preview, Audio and the Render Queue, docked the way you
|
|||
- **Shapes and masks:** shape layers with trim paths, repeaters, round corners, offset, zig zag,
|
||||
twist, wiggle, merge paths and gradient strokes; masks drawn with the pen tool, with modes,
|
||||
feather, expansion and vertex editing in the viewer.
|
||||
- **Text:** point and paragraph text with real shaping, the Character and Paragraph panels, and
|
||||
text animators with range selectors.
|
||||
- **Text:** point and paragraph text with real shaping in any font installed on your machine, the
|
||||
Character and Paragraph panels, and text animators with range selectors.
|
||||
- **Expressions:** JavaScript with the After Effects object model (`wiggle`, `loopOut`,
|
||||
`thisComp.layer("…")`, vector maths on arrays), an inline editor and the pick-whip.
|
||||
- **Timeline like you know it:** twirl layers open to Transform, masks, effects and the rest; drag
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ in [docs/gaps.md](docs/gaps.md).
|
|||
- **After Effects projects can't be opened.** EffectCraft can't read `.aep` / `.aepx` files,
|
||||
and third-party After Effects plug-ins can't run.
|
||||
- **Reliability is uneven across platforms.** Early Linux users hit basic problems: viewer
|
||||
panning, panel docking, drag-and-drop import.
|
||||
panning, panel docking, drag-and-drop import. A Windows user found the font menus offered only
|
||||
the bundled fonts (fixed 6 October: they list every installed font).
|
||||
- **AI tools are new.** Roto Brush's trained model (MobileSAM, M13.35) and face tracking's
|
||||
(MediaPipe Face Landmarker, M13.36) are optional downloads, not yet compared with After
|
||||
Effects.
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ fn main() -> eframe::Result {
|
|||
_ => files.push(a),
|
||||
}
|
||||
}
|
||||
// The font menus list the installed fonts: read their names while the window opens.
|
||||
effectcraft_engine::text::fonts::scan_system_in_background();
|
||||
// Wayland shows the window's icon from its desktop entry: an AppImage brings its own.
|
||||
#[cfg(target_os = "linux")]
|
||||
appimage::integrate_from_env(ICON_PNG);
|
||||
|
|
|
|||
|
|
@ -394,6 +394,18 @@ fn high_level_tools_add_effect_and_report_state() {
|
|||
assert!(err && c[0]["text"].as_str().unwrap().contains("unknown effect"), "{c:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_fonts_lists_families_with_styles() {
|
||||
let mut s = server();
|
||||
let all = call_json(&mut s, "list_fonts", json!({}));
|
||||
let fams = all["families"].as_array().unwrap();
|
||||
assert!(fams.iter().any(|f| f["family"] == "Inter" && f["origin"] == "bundled"), "{all}");
|
||||
let one = call_json(&mut s, "list_fonts", json!({"query": "jetbrains"}));
|
||||
assert_eq!(one["count"], 1, "{one}");
|
||||
assert_eq!(one["families"][0]["family"], "JetBrains Mono");
|
||||
assert!(one["families"][0]["styles"].as_array().unwrap().contains(&json!("Regular")), "{one}");
|
||||
}
|
||||
|
||||
/// Trained models over MCP (Roto Brush and face tracking): listed with their authors and
|
||||
/// licence, chosen, refused when unknown.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -150,6 +150,10 @@ fn list_effects(b: &mut Backend, a: &Value) -> Result<Reply> {
|
|||
json_reply(b.exec("effect.list", obj(&[("filter", get(a, "filter"))]))?)
|
||||
}
|
||||
|
||||
fn list_fonts(b: &mut Backend, a: &Value) -> Result<Reply> {
|
||||
json_reply(b.exec("text.fonts", obj(&[("query", get(a, "query")), ("rescan", get(a, "rescan"))]))?)
|
||||
}
|
||||
|
||||
/// `effect.apply` on one layer, then set parameters on the new instance by param id.
|
||||
fn add_effect(b: &mut Backend, a: &Value) -> Result<Reply> {
|
||||
let (layer, effect, comp) = (need(a, "layer")?, need(a, "effect")?, get(a, "comp"));
|
||||
|
|
@ -607,6 +611,21 @@ static TOOLS: &[ToolDef] = &[
|
|||
schema: || schema(json!({"filter": {"type": "string", "description": "Only effects whose id, name or category contains this text."}}), &[]),
|
||||
run: list_effects,
|
||||
},
|
||||
ToolDef {
|
||||
name: "list_fonts",
|
||||
description: "The font families text layers can use, bundled and installed on this machine: family, styles, origin (bundled / system / user) and the name in the font's own language. Narrow with `query`; `rescan` picks up fonts installed since launch. Set one with execute_command `layer.setText {\"layer\", \"font\", \"style\"}`.",
|
||||
bridge_only: false,
|
||||
schema: || {
|
||||
schema(
|
||||
json!({
|
||||
"query": {"type": "string", "description": "Only families whose name (English or own-language) contains this text."},
|
||||
"rescan": {"type": "boolean", "description": "Look for fonts installed since launch first."}
|
||||
}),
|
||||
&[],
|
||||
)
|
||||
},
|
||||
run: list_fonts,
|
||||
},
|
||||
ToolDef {
|
||||
name: "get_state",
|
||||
description: "Where things stand: the editor state (active comp, current time, selected layers/keys/items, open comps, tool, work area) plus what this build can do (`app`: version, command and effect counts, GPU effects, import/export formats, After Effects parity summary). Cheap; call it to re-orient after a series of edits.",
|
||||
|
|
|
|||
|
|
@ -498,6 +498,33 @@ pub fn font_features(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}))
|
||||
}
|
||||
|
||||
/// The installed font families (bundled and system), with their styles, where each comes from
|
||||
/// and its name in the font's own language. `query` keeps the families whose English or native
|
||||
/// name contains it (case-insensitive); `rescan` first looks for fonts installed since the app
|
||||
/// started.
|
||||
pub fn fonts(_s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use effectcraft_text::fonts;
|
||||
let added = if p.get("rescan").and_then(Value::as_bool).unwrap_or(false) { fonts::rescan_system() } else { 0 };
|
||||
let query = p.get("query").and_then(Value::as_str).map(str::to_lowercase).unwrap_or_default();
|
||||
let native = fonts::native_families();
|
||||
let mut origin: std::collections::BTreeMap<String, &'static str> = Default::default();
|
||||
for f in fonts::all_faces() {
|
||||
origin.entry(f.info.family.clone()).or_insert(f.info.origin);
|
||||
}
|
||||
let list: Vec<Value> = effectcraft_text::families()
|
||||
.into_iter()
|
||||
.filter(|(f, _)| query.is_empty() || f.to_lowercase().contains(&query) || native.get(f).is_some_and(|n| n.to_lowercase().contains(&query)))
|
||||
.map(|(f, styles)| {
|
||||
let mut v = json!({"family": f, "styles": styles, "origin": origin.get(&f).copied().unwrap_or("system")});
|
||||
if let Some(n) = native.get(&f) {
|
||||
v["nativeName"] = json!(n);
|
||||
}
|
||||
v
|
||||
})
|
||||
.collect();
|
||||
Ok(json!({"count": list.len(), "families": list, "added": added}))
|
||||
}
|
||||
|
||||
fn always_ok(_: &Session) -> std::result::Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -527,6 +554,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
always_ok,
|
||||
font_features
|
||||
),
|
||||
cmd!(
|
||||
"text.fonts",
|
||||
"List Fonts",
|
||||
[],
|
||||
None,
|
||||
"{query?, rescan?} → {count, families: [{family, styles, origin: bundled|system|user, nativeName?}], added} (rescan picks up fonts installed since launch)",
|
||||
always_ok,
|
||||
fonts
|
||||
),
|
||||
cmd!(
|
||||
"text.setSelection",
|
||||
"Set Text Selection",
|
||||
|
|
|
|||
|
|
@ -1018,6 +1018,17 @@ pub fn text_families() -> Vec<String> {
|
|||
effectcraft_text::families().into_iter().map(|(f, _)| f).collect()
|
||||
}
|
||||
|
||||
/// The styles a family offers (its own faces, in weight order), for the style menus. A family
|
||||
/// that isn't installed offers the usual styles, which resolve to Inter's.
|
||||
pub fn font_styles(family: &str) -> Vec<String> {
|
||||
effectcraft_text::families()
|
||||
.into_iter()
|
||||
.find(|(f, _)| f.eq_ignore_ascii_case(family))
|
||||
.map(|(_, st)| st)
|
||||
.filter(|st| !st.is_empty())
|
||||
.unwrap_or_else(|| ["Regular", "Medium", "SemiBold", "Bold", "Italic"].iter().map(|s| s.to_string()).collect())
|
||||
}
|
||||
|
||||
/// The OpenType feature tags (GSUB / GPOS) of the face a family + style resolves to.
|
||||
pub fn font_features(family: &str, style: &str) -> Vec<String> {
|
||||
effectcraft_text::fonts::face(effectcraft_text::resolve(family, style).face).features()
|
||||
|
|
|
|||
|
|
@ -227,3 +227,27 @@ fn opentype_features_per_range_render_and_query() {
|
|||
assert!(f["options"]["stylisticSets"].as_array().unwrap().contains(&json!(1)));
|
||||
assert_eq!(f["options"]["smallCaps"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fonts_lists_bundled_and_installed_families() {
|
||||
let mut s = Session::default();
|
||||
let r = s.execute("text.fonts", json!({})).unwrap();
|
||||
let fams = r["families"].as_array().unwrap();
|
||||
assert_eq!(r["count"].as_u64().unwrap() as usize, fams.len());
|
||||
let inter = fams.iter().find(|f| f["family"] == "Inter").unwrap();
|
||||
assert_eq!(inter["origin"], "bundled");
|
||||
assert!(inter["styles"].as_array().unwrap().contains(&json!("Bold")));
|
||||
// Installed fonts are listed without any layer having asked for one.
|
||||
use effectcraft_text::fonts::FontSource as _;
|
||||
for face in effectcraft_text::fonts::DirectorySource::system().faces() {
|
||||
assert!(fams.iter().any(|f| f["family"].as_str().unwrap().eq_ignore_ascii_case(&face.family)), "{} not listed", face.family);
|
||||
}
|
||||
// Filtered by name; rescanning keeps what was there.
|
||||
let r = s.execute("text.fonts", json!({"query": "noto ser", "rescan": true})).unwrap();
|
||||
assert_eq!(r["families"][0]["family"], "Noto Serif");
|
||||
assert_eq!(r["families"][0]["styles"], json!(["Regular"]));
|
||||
assert!(r["families"].as_array().unwrap().iter().all(|f| f["family"].as_str().unwrap().to_lowercase().contains("noto ser")));
|
||||
// The style menus offer a family's own styles.
|
||||
assert_eq!(crate::font_styles("noto serif"), vec!["Regular".to_string()]);
|
||||
assert_eq!(crate::font_styles("No Such Family").len(), 5);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -306,12 +306,16 @@ pub fn add_font_data(data: Vec<u8>) -> Vec<FaceId> {
|
|||
names.into_iter().map(|n| push(&mut d, info_from(n, FaceData::Shared(data.clone()), "user"))).collect()
|
||||
}
|
||||
|
||||
/// Held for a whole system scan: a concurrent caller waits for the complete list instead of
|
||||
/// returning early and resolving against a half-filled database.
|
||||
static SCAN: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
/// Scan the system font folders once (native only; a no-op on wasm). Returns the number of faces
|
||||
/// added by this call.
|
||||
pub fn scan_system() -> usize {
|
||||
// Held for the whole scan: a concurrent caller waits for the complete list instead of
|
||||
// returning early and resolving against a half-filled database.
|
||||
static SCAN: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
if system_scanned() {
|
||||
return 0;
|
||||
}
|
||||
let _scan = SCAN.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if system_scanned() {
|
||||
return 0;
|
||||
|
|
@ -321,6 +325,31 @@ pub fn scan_system() -> usize {
|
|||
n
|
||||
}
|
||||
|
||||
/// Scan the system font folders again, adding fonts installed since the last scan (faces already
|
||||
/// known are kept, so font ids stay valid). Returns the number of faces added.
|
||||
pub fn rescan_system() -> usize {
|
||||
let _scan = SCAN.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let n = if cfg!(target_arch = "wasm32") { 0 } else { add_source(&DirectorySource::system()) };
|
||||
db().write().unwrap_or_else(|e| e.into_inner()).scanned = true;
|
||||
n
|
||||
}
|
||||
|
||||
/// Start the system font scan on a background thread, so the font menus are complete by the
|
||||
/// time someone opens them without the first open waiting for the scan. Native only; on wasm
|
||||
/// (no threads, no font folders) this does nothing.
|
||||
pub fn scan_system_in_background() {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
if system_scanned() {
|
||||
return;
|
||||
}
|
||||
// A failed spawn only means the scan happens on first use instead.
|
||||
let _ = std::thread::Builder::new().name("font-scan".into()).spawn(|| {
|
||||
scan_system();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn system_scanned() -> bool {
|
||||
db().read().unwrap_or_else(|e| e.into_inner()).scanned
|
||||
}
|
||||
|
|
@ -340,13 +369,18 @@ pub fn all_faces() -> Vec<Arc<Face>> {
|
|||
}
|
||||
|
||||
/// Family names in the fonts' own languages: English family → native family (only families
|
||||
/// that have one).
|
||||
/// that have one). Scans the system fonts first if that hasn't happened yet.
|
||||
pub fn native_families() -> std::collections::BTreeMap<String, String> {
|
||||
scan_system();
|
||||
all_faces().into_iter().filter_map(|f| Some((f.info.family.clone(), f.info.native_family.clone()?))).collect()
|
||||
}
|
||||
|
||||
/// Families with their style names, sorted by family; styles in weight order.
|
||||
/// Families with their style names, sorted by family; styles in weight order. This is what font
|
||||
/// menus list, so it scans the system fonts first if that hasn't happened yet (before, only a
|
||||
/// project asking for a font the bundled ones lack started the scan, and the menus offered the
|
||||
/// three bundled families alone).
|
||||
pub fn families() -> Vec<(String, Vec<String>)> {
|
||||
scan_system();
|
||||
let mut m: std::collections::BTreeMap<String, Vec<(bool, u16, String)>> = Default::default();
|
||||
for f in all_faces() {
|
||||
let v = m.entry(f.info.family.clone()).or_default();
|
||||
|
|
@ -649,9 +683,15 @@ mod tests {
|
|||
let s = resolve("Noto Serif", "Bold Italic");
|
||||
assert_eq!(face(s.face).info.family, "Noto Serif");
|
||||
assert!(s.synth_bold && s.synth_italic);
|
||||
// Inter Bold Italic: nearest is Italic (400) → synth bold
|
||||
// Inter Bold Italic: the bundled cuts' nearest is Italic (400) → synth bold. Listing the
|
||||
// families scanned the system fonts, so an installed Inter's real Bold Italic wins.
|
||||
let bi = resolve("Inter", "Bold Italic");
|
||||
assert!(face(bi.face).info.italic && bi.synth_bold && !bi.synth_italic);
|
||||
let f = face(bi.face);
|
||||
if f.info.origin == "bundled" {
|
||||
assert!(f.info.italic && bi.synth_bold && !bi.synth_italic);
|
||||
} else {
|
||||
assert!(f.info.style.eq_ignore_ascii_case("Bold Italic") && !bi.synth_bold && !bi.synth_italic);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
//! The font menus list the installed fonts (issue: on Windows the Properties panel's font menu
|
||||
//! offered only Inter, JetBrains Mono and Noto Serif). Its own test binary, so the system font
|
||||
//! scan has not run yet when it starts.
|
||||
|
||||
use effectcraft_text::fonts::{DirectorySource, FontSource, families, system_scanned};
|
||||
|
||||
#[test]
|
||||
fn families_include_installed_fonts_without_a_prior_scan() {
|
||||
assert!(!system_scanned(), "nothing has scanned yet in this process");
|
||||
let listed: Vec<String> = families().into_iter().map(|(f, _)| f).collect();
|
||||
assert!(system_scanned() || cfg!(target_arch = "wasm32"), "listing the families scans the system fonts");
|
||||
for b in ["Inter", "JetBrains Mono", "Noto Serif"] {
|
||||
assert!(listed.iter().any(|f| f == b), "bundled {b} listed");
|
||||
}
|
||||
// Every family found in the platform's font folders is in the menu.
|
||||
for face in DirectorySource::system().faces() {
|
||||
assert!(listed.iter().any(|f| f.eq_ignore_ascii_case(&face.family)), "installed family {} missing from {listed:?}", face.family);
|
||||
}
|
||||
}
|
||||
|
|
@ -345,9 +345,10 @@ fn text_section(
|
|||
widgets::open_popup(ui, pop);
|
||||
}
|
||||
app.auto.add("properties.text.font", fr, "Font family");
|
||||
let fams: Vec<String> = effectcraft_engine::text_families();
|
||||
if let Some(i) = widgets::popup_menu(ui, pop, fr.left_bottom(), &fams, fams.iter().position(|f| *f == doc.font)) {
|
||||
set(actions, json!({"font": fams[i]}));
|
||||
// Every installed family (built only while the menu is open: it can be long).
|
||||
let fams: Vec<String> = if widgets::popup_is_open(ui, pop) { effectcraft_engine::text_families() } else { vec![] };
|
||||
if let Some(f) = widgets::popup_menu(ui, pop, fr.left_bottom(), &fams, fams.iter().position(|f| *f == doc.font)).and_then(|i| fams.get(i)) {
|
||||
set(actions, json!({"font": f}));
|
||||
}
|
||||
y += 28.0;
|
||||
let sr = Rect::from_min_size(pos2(x0, y), vec2(w, 22.0));
|
||||
|
|
@ -356,9 +357,10 @@ fn text_section(
|
|||
widgets::open_popup(ui, spop);
|
||||
}
|
||||
app.auto.add("properties.text.style", sr, "Font style");
|
||||
let styles: Vec<String> = ["Regular", "Medium", "SemiBold", "Bold", "Italic"].iter().map(|s| s.to_string()).collect();
|
||||
if let Some(i) = widgets::popup_menu(ui, spop, sr.left_bottom(), &styles, styles.iter().position(|s| *s == doc.style)) {
|
||||
set(actions, json!({"style": styles[i]}));
|
||||
// The family's own styles.
|
||||
let styles: Vec<String> = if widgets::popup_is_open(ui, spop) { effectcraft_engine::font_styles(&doc.font) } else { vec![] };
|
||||
if let Some(st) = widgets::popup_menu(ui, spop, sr.left_bottom(), &styles, styles.iter().position(|s| *s == doc.style)).and_then(|i| styles.get(i)) {
|
||||
set(actions, json!({"style": st}));
|
||||
}
|
||||
y += 32.0;
|
||||
// Size / leading, tracking / stroke width.
|
||||
|
|
|
|||
|
|
@ -107,9 +107,10 @@ pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
widgets::open_popup(ui, spop);
|
||||
}
|
||||
app.auto.add("character.style", sr, "Font style");
|
||||
let styles: Vec<String> = ["Regular", "Medium", "SemiBold", "Bold", "Italic"].iter().map(|s| s.to_string()).collect();
|
||||
if let Some(i) = widgets::popup_menu(ui, spop, sr.left_bottom(), &styles, styles.iter().position(|s| *s == doc.style)) {
|
||||
actions.push(json!({"style": styles[i]}));
|
||||
// The family's own styles.
|
||||
let styles: Vec<String> = if widgets::popup_is_open(ui, spop) { effectcraft_engine::font_styles(&doc.font) } else { vec![] };
|
||||
if let Some(st) = widgets::popup_menu(ui, spop, sr.left_bottom(), &styles, styles.iter().position(|s| *s == doc.style)).and_then(|i| styles.get(i)) {
|
||||
actions.push(json!({"style": st}));
|
||||
}
|
||||
y += 34.0;
|
||||
// Numeric fields in AE's two-column grid (Kerning is a popup, at row 1 right).
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ The server speaks JSON-RPC 2.0 over stdio, one message per line, and supports MC
|
|||
| `set_property {layer, path, value?, time?, expression?, comp?}` | Sets a static value. With `time` it sets a keyframe; with `expression` it sets an expression. |
|
||||
| `add_keyframe {layer, path, time+value \| keys:[...], interpolation?}` | Adds keys, then optionally applies linear/bezier/hold/easyEase. |
|
||||
| `list_effects {filter?}` | Effect ids, names, categories, GPU / 32-bpc support and parameters. |
|
||||
| `list_fonts {query?, rescan?}` | Font families text layers can use, bundled and installed, with their styles, origin and own-language name; `rescan` picks up fonts installed since launch. |
|
||||
| `add_effect {layer, effect, values?, comp?}` | Apply an effect and set its parameters in one call; returns the instance path (`effects/#n`) and its parameter paths. |
|
||||
| `render_frame {comp?, time?, max_side?, path?, inline?, transparent?}` | Returns a PNG image of a frame; `transparent: true` keeps the alpha (as an RGB + Alpha render writes it) instead of compositing over the comp background. |
|
||||
| `open_project {path \| demo \| new}` / `save_project {path?}` | Open and save files. |
|
||||
|
|
@ -350,6 +351,12 @@ whole layer, or only characters `range: [start, end]` (character indices): chara
|
|||
4. `edit.copy`, `edit.paste`, `edit.pasteTextMatchFormatting` and `edit.pasteTextFormattingOnly`
|
||||
work on the selected text; `text.endEdit` commits (an empty Type-tool layer is removed).
|
||||
|
||||
Fonts: `text.fonts` (MCP `list_fonts`) lists every family a text layer can use, bundled and installed, with its
|
||||
styles, origin (`bundled`, `system`, `user`) and own-language name (`{"query":"gothic"}` filters
|
||||
by name; `{"rescan":true}` first picks up fonts installed since launch). Pass a listed `family` and
|
||||
one of its `styles` to `layer.setText {"font", "style"}`; `text.fontFeatures` says which OpenType
|
||||
options a font draws with its own glyphs.
|
||||
|
||||
In expressions, `text.sourceText.style` / `getStyleAt(i, t)` read styles and the setters
|
||||
(`setFontSize(v, start?, count?)`, `setFillColor`, `setText`, `setJustification`…) return a
|
||||
styled document.
|
||||
|
|
|
|||
|
|
@ -551,8 +551,9 @@ What keeps it fast:
|
|||
|
||||
- **Lazy open.** A project file carries its footage metadata, so File ▸ Open is read + parse +
|
||||
swap. Nothing is decoded until a frame needs it, expressions compile on first evaluation
|
||||
(cached by text), and system fonts are scanned only when a text layer asks for a family that
|
||||
isn't bundled. The footage files are checked afterwards in the background (`footage.check`, a
|
||||
(cached by text), and system fonts are scanned on first need (a text layer asking for a family
|
||||
that isn't bundled, or a font menu or `text.fonts` listing the families); the desktop app starts
|
||||
that scan on a background thread at launch, reading name tables only. The footage files are checked afterwards in the background (`footage.check`, a
|
||||
"Checking footage" job in the Progress panel): missing items are flagged, items saved without
|
||||
metadata are probed, and the project is not marked modified. The desktop app does this after
|
||||
every open (`Session::check_footage_on_open`); headless sessions run `footage.check {wait:true}`.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ and the work that closes the gap. This document is meant for contributors and ag
|
|||
what to work on. The [ROADMAP](../ROADMAP.md) summarises it; [parity.md](parity.md) is the
|
||||
feature-by-feature checklist it builds on.
|
||||
|
||||
*Assessed 5 October 2026. Estimates marked "≈" are judgements from the evidence listed, not
|
||||
*Assessed 5 October 2026, user issues updated 6 October. Estimates marked "≈" are judgements from the evidence listed, not
|
||||
measurements. Update this file when the evidence changes.*
|
||||
|
||||
## Two different questions
|
||||
|
|
@ -34,7 +34,7 @@ of them have been reconciled.
|
|||
| Behaves like After Effects | largely unmeasured, ≈ 60–80% | One outside contributor found four bugs in features marked done within a day (PRs #6–#10): Hold keyframes eased the motion into them, `keyInSpatialTangent` / `keyOutSpatialTangent` had the wrong names, a zero frame rate crashed, Find and Enter Full Screen shared Ctrl+F off macOS. There are about 10 After Effects reference captures in total. The first live G1 comparison now checks eight scalar Rotation/ease cases (168 samples) against AE 26.3x87 via AEsync 2.0.4: all pass after fixing overlapping temporal influences, with maximum error below 3e-11 degrees ([scores and reproduction](fidelity/README.md)). Other animation behavior and rendered frames remain unmeasured |
|
||||
| Opening existing After Effects work | ≈ 0% | `.aep` / `.aepx` projects cannot be opened. Third-party After Effects plug-ins cannot run. Expressions and the scripting object model are strong, so scripts and expressions carry over |
|
||||
| Stability | improving | 23 never-crash PRs landed on 4 October; [AGENTS.md](../AGENTS.md) "Never crash" now binds every crate. On 5 October `cargo xtask ci` failed on main under Rust 1.99's clippy (a fix is in progress) |
|
||||
| Real-user experience | ≈ 70%, uneven by platform | Issues #41–#47 (all from the Linux AppImage 0.1.1, 5 October): panning the viewer snaps back, panels can't be resized or rearranged, drag-and-drop and double-click import don't work, layer rename gets stuck, the Layer Settings arrow does nothing, the Project panel clips, the Wayland window icon is generic. Issues #63–#68 (macOS, 5 October): scaling a layer by its handles goes wrong and can stick at 0, two 3D compasses, the viewer lags while a layer is dragged (a frame renders in 20 ms but showed after ≈ 100 ms, behind RAM preview prefetch), the Discord and other links do nothing, Delete doesn't delete in the Project panel, hidden layers can be selected in the viewer. All but the last two were fixed or confirmed fixed with a test on 5–6 October (those two have community PRs); file drops still can't work on Wayland (winit has no support). The panning, rename and arrow bugs had been fixed in v0.2.0 already; nobody had told the reporter. Most checking happens on macOS. No localisation, no accessibility work |
|
||||
| Real-user experience | ≈ 70%, uneven by platform | Issues #41–#47 (all from the Linux AppImage 0.1.1, 5 October): panning the viewer snaps back, panels can't be resized or rearranged, drag-and-drop and double-click import don't work, layer rename gets stuck, the Layer Settings arrow does nothing, the Project panel clips, the Wayland window icon is generic. Issues #63–#68 (macOS, 5 October): scaling a layer by its handles goes wrong and can stick at 0, two 3D compasses, the viewer lags while a layer is dragged (a frame renders in 20 ms but showed after ≈ 100 ms, behind RAM preview prefetch), the Discord and other links do nothing, Delete doesn't delete in the Project panel, hidden layers can be selected in the viewer. All but the last two were fixed or confirmed fixed with a test on 5–6 October (those two have community PRs); file drops still can't work on Wayland (winit has no support). The panning, rename and arrow bugs had been fixed in v0.2.0 already; nobody had told the reporter. A Windows user (6 October, on Discord) found the font menus listed only the three bundled fonts: installed fonts were read only when a project asked for one, on every platform. Fixed with a regression test on 6 October; the menus now list every installed family and its own styles, and agents get `text.fonts` / `list_fonts`. Most checking happens on macOS. No localisation, no accessibility work |
|
||||
| Performance | unknown against After Effects | Internal numbers only (e.g. Advanced 3D 290 ms/frame at 1080p on the GPU, an M4 Pro under load). Nothing benchmarked against After Effects; no large real projects (4K footage, hundreds of layers) tested |
|
||||
| Media formats | ≈ 80% | H.264, ProRes, HEVC, AV1, image sequences and audio exist. The new HEVC / AV1 encoders have no B-frames, multi-reference or SAO / CDEF, so files are larger than from mature encoders. Camera formats (BRAW, R3D, ProRes RAW, variable-frame-rate phone video) are unverified |
|
||||
| AI-assisted tools | ≈ 50% | Both tools can use trained models (pure-Rust inference, optional downloads), but nothing compares them with After Effects yet. Roto Brush 2.0 / 3.0 with MobileSAM (M13.35) scores IoU 0.989 on the base frame of our synthetic moving-disc test and ≥ 0.980 over 20 propagated frames (classic: 0.973). Face tracking with MediaPipe Face Landmarker (M13.36) matches Google's own pipeline to 0.85 px on average on a test portrait; on our synthetic clip the eyes and chin stay within 4% of the face height |
|
||||
|
|
@ -64,7 +64,8 @@ The most important missing piece: it turns every other estimate here into a meas
|
|||
### G2. Real-user reliability on every platform
|
||||
|
||||
- Fix user issues as they come in, each with a regression test, and answer the reporter (#41–#47
|
||||
and #63–#68 are handled; Wayland file drops wait on winit).
|
||||
and #63–#68 are handled, as is the Windows report that the font menus missed installed fonts;
|
||||
Wayland file drops wait on winit).
|
||||
- Run the headless snapshot and control-channel checks on Linux (X11 and Wayland) and Windows, not
|
||||
only macOS. Interactions that only fail with real input (docking drags, viewer pan, drag-and-drop
|
||||
import, inline rename) need scripted input tests.
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
|||
| Shapes | ≈ 80% | 1.5 | Lottie can't carry stroke taper/wave (stroke Taper and Wave, Dash 2/Gap 2/Dash 3/Gap 3 and radial-gradient Highlight Length/Angle landed in M13.5; pen tool for shape paths and vertex editing in M6.5) |
|
||||
| Compositions | ≈ 81% | 3.3 | Mocha-style planar tracks for templates; nested comp markers on the precomp layer bar (Preserve frame rate / resolution when nested and nested comps showing nothing past their end landed in M4.9–M4.10; Pre-compose keeping the comp's settings and New Comp from Selection's dialog in M3.12; CMP-7: Essential Graphics mirrored and linked properties landed in M13.11; Font and uniform Scale controls, Composition ▸ Open in Essential Graphics, Save Frame As ▸ Photoshop Layers / ProEXR and the VR Comp Editor landed in M13.7; the marker dialog, Composition Flowchart, Essential Graphics with master properties, `.ectemplate` templates and Responsive Design — Time landed: CMP-6, CMP-7) |
|
||||
| Animation | 70% | 9.0 | puppet depth beyond pins, recording, rigging and follow-through (puppet pin recording with Record Options landed in M13.1; pin selection, rotate/scale handles, nulls for pins and Follow-Through in M13.14–M13.16), Wiggler/Smoother/Motion Sketch (motion-path handles and the graph editor transform box landed in M5.8; keyframe colour labels and Select Keyframe Label Group, Graph Editor snapping to markers / layer ends in M13.5) |
|
||||
| Text | ≈ 96% | 0.3 | no extruded strokes (M13.12: the Variable Font Axes animator re-spaces the text — advances follow the animated axes; M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
|
||||
| Text | ≈ 96% | 0.3 | no extruded strokes (G2: the font menus list every installed family with its own styles — before, only a project asking for a font read the system fonts — and `text.fonts` / MCP `list_fonts` list them for agents; M13.12: the Variable Font Axes animator re-spaces the text — advances follow the animated axes; M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
|
||||
| Web | 98% | 0.1 | No shared-memory threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`) (M13.30: Render Queue, analyses and Content-Aware Fill render on each job worker's own WebGPU device in passes, particles and Advanced 3D read back under keys, layer buffers in the browser's disk cache; browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10; WEB-1 in M13.24: GPU effects and the GPU compositor in the frame workers on their own WebGPU devices with deferred readbacks (frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.info` / `storage.persist` / `storage.clear`), and a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) fixed) |
|
||||
| 3D | 88% | 5.5 | multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 |
|
||||
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (280 run on the GPU since M13.29, EFF-5: M13.28 added the remaining pixel effects and the geometry and audio generators (Lightning, Advanced Lightning, Beam, Lens Flare, Radio Waves, Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC Threads, Audio Spectrum / Waveform, Basic / Path Text); M13.29 added the particle effects (CC Particle World / Systems II, Particle Playground, CC Ball Action, CC Pixel Polly, CC Scatterize, Curl Noise) and removed the fallbacks of Shatter's wireframe views, Foam's extras, Timewarp's Matte Layer, strong Warp Fisheye / Twist bends and custom `.ocio` configs; M13.23 added the 3D Channel and Immersive Video families, Apply Color LUT, the OCIO effects, Color Profile Converter and the simulations' render passes (Card Wipe included); M13.22 added the CC light family, the CC transitions, Block Dissolve, Radial Shadow, CC Bender / Blobbylize / Cylinder / Sphere / Spotlight / Environment, 3D Glasses, Numbers, Timecode and the time effects joined the 166 of M13.13; Fractal and the analysis / tool effects still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
|
|
|
|||
Loading…
Reference in New Issue