From 77c3fad6c5800ec951359cd031c4ec215f8b15e8 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sun, 8 Nov 2026 02:32:11 +0100 Subject: [PATCH] #3233: an empty Layer Settings name follows the text again; document the name rule on layer.newText, layer.setText and layer.settings --- crates/engine/src/cmd/layer.rs | 6 +++--- crates/engine/src/cmd/layer/transform.rs | 7 ++++++- crates/engine/src/tests/text_edit.rs | 5 +++++ docs/agents.md | 5 ++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/cmd/layer.rs b/crates/engine/src/cmd/layer.rs index b015b72b4..f48fa26f8 100644 --- a/crates/engine/src/cmd/layer.rs +++ b/crates/engine/src/cmd/layer.rs @@ -49,7 +49,7 @@ pub fn specs() -> Vec { "Text", ["Layer", "New"], Some("Cmd+Alt+Shift+T"), - "{text?, name? (layer name; default the text), position? [x,y], box? [x,y,w,h] (paragraph text, comp space), vertical?, edit? (start editing), font?, style?, size?, fill?, stroke?, applyFill?, applyStroke?, strokeWidth?, tracking?, leading?, baselineShift?, hScale?, vScale?, tsume?, fauxBold?, fauxItalic?, allCaps?, smallCaps?, baseline?, superscript?, subscript?, kerning?, ligatures?, justify?, indentLeft?, indentRight?, indentFirst?, spaceBefore?, spaceAfter?, direction?, composer?, hangingPunctuation?, strokeOverFill?, lineJoin?} (attributes as layer.setText)", + "{text?, name? (layer name; default the text, which the name then follows; a given name stays), position? [x,y], box? [x,y,w,h] (paragraph text, comp space), vertical?, edit? (start editing), font?, style?, size?, fill?, stroke?, applyFill?, applyStroke?, strokeWidth?, tracking?, leading?, baselineShift?, hScale?, vScale?, tsume?, fauxBold?, fauxItalic?, allCaps?, smallCaps?, baseline?, superscript?, subscript?, kerning?, ligatures?, justify?, indentLeft?, indentRight?, indentFirst?, spaceBefore?, spaceAfter?, direction?, composer?, hangingPunctuation?, strokeOverFill?, lineJoin?} (attributes as layer.setText)", has_comp, new_text ), @@ -70,7 +70,7 @@ pub fn specs() -> Vec { "Layer Settings...", ["Layer"], Some("Cmd+Shift+Y"), - "{layer?, name?, color?, width?, height?, pixelAspect?, affectAll?: bool (default true; false gives this layer its own copy of a solid other layers use)} → {layer, solid?}", + "{layer?, name? (an empty name makes a text layer's name follow its text again), color?, width?, height?, pixelAspect?, affectAll?: bool (default true; false gives this layer its own copy of a solid other layers use)} → {layer, solid?}", has_layer_settings, layer_settings ), @@ -218,7 +218,7 @@ pub fn specs() -> Vec { "Edit Text", [], None, - "{layer?, range?: [start, end] (characters; default all), text?, font?, style?, size?, fill?, stroke?, applyFill?, applyStroke?, strokeWidth?, tracking?, leading?: px|\"auto\", baselineShift? px, hScale? %, vScale? %, tsume? %, fauxBold?, fauxItalic?, allCaps?, smallCaps?, baseline?: normal|superscript|subscript, superscript?, subscript?, kerning?: metrics|optical|number, ligatures?, discretionaryLigatures? (OpenType), contextualAlternates?, stylisticAlternates?, stylisticSets?: [1–20], ss01?, ss02?, ss03?, ss04?, ss05?, ss06?, ss07?, ss08?, ss09?, ss10?, ss11?, ss12?, ss13?, ss14?, ss15?, ss16?, ss17?, ss18?, ss19?, ss20?, swash?, titling?, ordinals?, fractions?, allSmallCaps?, figureStyle?: default|lining|oldStyle, figureWidth?: default|proportional|tabular, figures?, variations?: {tag: value} (variable font axes; null resets), justify?: left|center|right|justifyLeft|justifyCenter|justifyRight|justifyAll, indentLeft?, indentRight?, indentFirst?, spaceBefore?, spaceAfter?, direction?: ltr|rtl, composer?: everyLine|singleLine, hangingPunctuation?, strokeOverFill?, lineJoin?: miter|round|bevel, box?: [x,y,w,h]|null (layer space), vertical?}", + "{layer?, range?: [start, end] (characters; default all), text? (also renames a text layer whose name still follows its text), font?, style?, size?, fill?, stroke?, applyFill?, applyStroke?, strokeWidth?, tracking?, leading?: px|\"auto\", baselineShift? px, hScale? %, vScale? %, tsume? %, fauxBold?, fauxItalic?, allCaps?, smallCaps?, baseline?: normal|superscript|subscript, superscript?, subscript?, kerning?: metrics|optical|number, ligatures?, discretionaryLigatures? (OpenType), contextualAlternates?, stylisticAlternates?, stylisticSets?: [1–20], ss01?, ss02?, ss03?, ss04?, ss05?, ss06?, ss07?, ss08?, ss09?, ss10?, ss11?, ss12?, ss13?, ss14?, ss15?, ss16?, ss17?, ss18?, ss19?, ss20?, swash?, titling?, ordinals?, fractions?, allSmallCaps?, figureStyle?: default|lining|oldStyle, figureWidth?: default|proportional|tabular, figures?, variations?: {tag: value} (variable font axes; null resets), justify?: left|center|right|justifyLeft|justifyCenter|justifyRight|justifyAll, indentLeft?, indentRight?, indentFirst?, spaceBefore?, spaceAfter?, direction?: ltr|rtl, composer?: everyLine|singleLine, hangingPunctuation?, strokeOverFill?, lineJoin?: miter|round|bevel, box?: [x,y,w,h]|null (layer space), vertical?}", has_layers, set_text ), diff --git a/crates/engine/src/cmd/layer/transform.rs b/crates/engine/src/cmd/layer/transform.rs index 87403fb26..acccfc7df 100644 --- a/crates/engine/src/cmd/layer/transform.rs +++ b/crates/engine/src/cmd/layer/transform.rs @@ -157,7 +157,7 @@ pub(super) fn transform_op(s: &mut Session, p: &Value) -> Result { /// layers, which are solids) edit their Solid Settings. With `affectAll: false`, a solid other /// layers also use is copied into a new solid for this layer instead of being changed for all of /// them (After Effects' "Affect all layers that use this solid"). `name` renames the layer and -/// its solid. +/// its solid; an empty one makes a text layer's name follow its text again. pub(super) fn layer_settings(s: &mut Session, p: &Value) -> Result { let (cid, lid) = layer_p(s, p, "layer.settings")?; // Cameras and lights open their own settings (Camera Settings / Light Settings). @@ -171,6 +171,9 @@ pub(super) fn layer_settings(s: &mut Session, p: &Value) -> Result { let h = p.get("height").and_then(Value::as_u64).map(limits::size); let pixel_aspect = pixel_aspect_p(p)?; let name = str_p(p, "name").map(str::trim).filter(|n| !n.is_empty()).map(str::to_string); + // An empty name gives a text layer its text's name again, as `layer.rename` does (#3233); + // other layers keep theirs. + let follow_text = str_p(p, "name").is_some_and(|n| n.trim().is_empty()); let affect_all = p.get("affectAll").and_then(Value::as_bool).unwrap_or(true); let t = s.time(); let solid = s.edit("Layer Settings", None, |proj, _| { @@ -178,6 +181,8 @@ pub(super) fn layer_settings(s: &mut Session, p: &Value) -> Result { if let Some(n) = &name { // A user name: a text layer's name stops following its text (#3233). super::switches::set_layer_name(layer_mut(proj, cid, lid)?, n, t); + } else if follow_text && matches!(src, LayerSource::Text) { + super::switches::set_layer_name(layer_mut(proj, cid, lid)?, "", t); } let LayerSource::Solid { item } = src else { return Ok(None) }; let shared = proj.comps().flat_map(|(_, c)| &c.layers).filter(|l| l.source == src).count() > 1; diff --git a/crates/engine/src/tests/text_edit.rs b/crates/engine/src/tests/text_edit.rs index bcb4f0559..db8d4679a 100644 --- a/crates/engine/src/tests/text_edit.rs +++ b/crates/engine/src/tests/text_edit.rs @@ -1465,6 +1465,11 @@ fn a_text_layer_name_follows_its_text_until_renamed() { s.execute("layer.settings", json!({"layer": t, "name": "Settings Name"})).unwrap(); s.execute("layer.setText", json!({"layer": t, "text": "Later"})).unwrap(); assert_eq!(layer_name(&s, t), "Settings Name", "Layer Settings names it for good"); + // An empty Layer Settings name follows the text again, as `layer.rename` does. + s.execute("layer.settings", json!({"layer": t, "name": ""})).unwrap(); + assert_eq!(layer_name(&s, t), "Later", "an empty Layer Settings name shows the text"); + s.execute("layer.setText", json!({"layer": t, "text": "Follows"})).unwrap(); + assert_eq!(layer_name(&s, t), "Follows", "and follows it again"); // A layer given a name when it is made does not follow. let named = s.execute("layer.newText", json!({"text": "Body", "name": "Caption"})).unwrap()["layer"].as_u64().unwrap(); s.execute("layer.setText", json!({"layer": named, "text": "Other"})).unwrap(); diff --git a/docs/agents.md b/docs/agents.md index 27664cb73..ebf158029 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -714,7 +714,10 @@ in the Plug-in Library: [publishing-plugins.md](publishing-plugins.md). * Rename a layer with Enter (or double-click a name): the whole name is selected, Enter or a click elsewhere commits, Escape cancels; `layer.rename {layer, name}` does it directly. A text layer's name follows its text until it is renamed; renaming it to an empty name makes - it follow the text again (After Effects). + it follow the text again (After Effects). `layer.setText` and typing on canvas rename a text + layer whose name still follows its text; `layer.newText {name}` and a name set by + `layer.settings` or a script's `layer.name = …` (which runs `layer.rename`) stay, and an empty + `layer.settings` name follows the text again too. * Ctrl/Cmd-click on the current-time display (Timeline or Composition panel) toggles the Time Display Style between Timecode and Frames: `file.projectSettings {"timeDisplay": "frames"}`. * `prop.pickWhip {layer, prop, target: {layer, prop, dimension?}}` links a property as its