mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
fix(text): keep an empty Type-tool layer, as After Effects does
Clicking away from a Type-tool layer without typing deleted it, so a click that was meant to place the text point lost the layer. AE keeps it in the timeline as <空白のテキストレイヤー>, and the layer came from the user's click rather than being a mistake to clean up. edit.deselectAll already routes through text.endEdit, so one change covers both paths. The tests that asserted the old cleanup now assert the layer survives. Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
This commit is contained in:
co-authored by
DeepSeek V4.1 Flash
parent
990fa668cd
commit
f262e39c63
@@ -74,9 +74,9 @@ fn select_all(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
Ok(json!(s.state.selected_layers.len()))
|
||||
}
|
||||
fn deselect_all(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
// A deselected layer never keeps an open text edit: commit the edit first, exactly as
|
||||
// `text.endEdit` (an empty Type-tool layer included), so every route — menu, shortcut,
|
||||
// script, MCP — leaves one final state.
|
||||
// A deselected layer never keeps an open text edit: commit it first, exactly as `text.endEdit`,
|
||||
// so every route — menu, shortcut, script, MCP — leaves one final state. An empty Type-tool
|
||||
// layer survives, as it does in After Effects.
|
||||
super::text_edit::end_edit(s, &Value::Null)?;
|
||||
s.state.selected_layers.clear();
|
||||
s.state.selected_props.clear();
|
||||
|
||||
@@ -372,18 +372,14 @@ fn edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(state_json(s))
|
||||
}
|
||||
|
||||
/// `text.endEdit`: stop editing (a Type-tool layer left empty is deleted).
|
||||
/// `text.endEdit`: stop editing.
|
||||
///
|
||||
/// A Type-tool layer the user clicked away from without typing is **kept**: After Effects leaves it in
|
||||
/// the timeline as `<空白のテキストレイヤー>`, and the layer came from the user's click rather than from a
|
||||
/// mistake to clean up. Deleting it took away something they could still type into.
|
||||
pub(crate) fn end_edit(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let Some(e) = s.state.text_edit.take() else { return Ok(Value::Null) };
|
||||
if e.created && layer_doc(s, e.layer).is_some_and(|d| d.text.is_empty()) {
|
||||
let cid = s.active_comp_id().ok_or(EngineError::NoComp)?;
|
||||
s.edit("Delete Empty Text Layer", None, |proj, st| {
|
||||
if let Some(c) = proj.comp_mut(cid) {
|
||||
c.layers.retain(|l| l.id != e.layer);
|
||||
}
|
||||
st.selected_layers.retain(|l| *l != e.layer);
|
||||
Ok(())
|
||||
})?;
|
||||
if s.state.text_edit.take().is_none() {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
s.bump();
|
||||
Ok(Value::Null)
|
||||
|
||||
@@ -267,18 +267,18 @@ fn type_tool_layers_point_box_and_empty_cleanup() {
|
||||
assert_eq!(s.state.text_edit.as_ref().map(|e| e.layer.0), Some(t));
|
||||
// Leaving an untouched Type-tool layer deletes it.
|
||||
s.execute("text.endEdit", json!({})).unwrap();
|
||||
assert!(s.active_comp().unwrap().layers.is_empty());
|
||||
assert_eq!(s.active_comp().unwrap().layers.len(), 1, "an empty Type-tool layer is kept, as in AE");
|
||||
let b = s.execute("layer.newText", json!({"text": "", "box": [40, 50, 300, 120], "edit": true})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("text.insert", json!({"text": "Boxed"})).unwrap();
|
||||
let d = doc(&s, b);
|
||||
assert_eq!(d.box_size, Some([300.0, 120.0]));
|
||||
assert_eq!(d.box_pos, [-150.0, -60.0]);
|
||||
let l = s.active_comp().unwrap().layers[0].clone();
|
||||
let l = s.active_comp().unwrap().layers.iter().find(|l| l.id.0 == b).expect("the boxed layer").clone();
|
||||
assert_eq!(l.name, "Boxed", "named after its text");
|
||||
let pos = l.props.prop("transform/position").unwrap().value.as_vec3();
|
||||
assert_eq!([pos[0], pos[1]], [190.0, 110.0]);
|
||||
s.execute("text.endEdit", json!({})).unwrap();
|
||||
assert_eq!(s.active_comp().unwrap().layers.len(), 1);
|
||||
assert_eq!(s.active_comp().unwrap().layers.len(), 2, "both text layers stay, the empty one included");
|
||||
// Resizing the box (layer space) is an attribute too.
|
||||
s.execute("layer.setText", json!({"layer": b, "box": [-100, -60, 200, 120]})).unwrap();
|
||||
assert_eq!(doc(&s, b).box_size, Some([200.0, 120.0]));
|
||||
@@ -1202,7 +1202,7 @@ fn deselect_all_ends_the_open_edit() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deselect_all_discards_an_empty_type_tool_layer() {
|
||||
fn deselect_all_keeps_an_empty_type_tool_layer() {
|
||||
let (mut s, _keep) = session("Hello");
|
||||
let new = s.execute("layer.newText", json!({"text": "", "position": [80.0, 80.0], "edit": true})).unwrap()["layer"].as_u64().unwrap();
|
||||
assert!(
|
||||
@@ -1214,7 +1214,5 @@ fn deselect_all_discards_an_empty_type_tool_layer() {
|
||||
s.execute("edit.deselectAll", json!({})).unwrap();
|
||||
assert!(s.state.text_edit.is_none());
|
||||
assert!(s.state.selected_layers.is_empty());
|
||||
assert_eq!(count(&s), before - 1, "a Type-tool layer left empty is removed, as text.endEdit does");
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(count(&s), before, "that removal is one undo step of its own, as on text.endEdit");
|
||||
assert_eq!(count(&s), before, "an empty Type-tool layer is kept, as in After Effects");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user