masks.addVertex: clamp oversized index on animated paths, reject empty paths instead of panicking (#543)

This commit is contained in:
MarekSpodBiedry committed 2026-10-10 09:16:34 +02:00
1 parent 7bd7621262
commit b40964db91
2 files changed
+51 -7

No files matched your search

+18 -7
View File
@@ -366,23 +366,34 @@ fn add_vertex(s: &mut Session, p: &Value) -> Result<Value> {
let after = u64_p(p, "after").ok_or_else(|| bad("masks.addVertex", "need `after`"))? as usize;
let at = p.get("at").and_then(pt).ok_or_else(|| bad("masks.addVertex", "need `at`"))?;
edit_mask(s, p, "Add Mask Vertex", move |m, t| {
let n = m.path_at(t).vertices.len();
// clamp against the length before any insertion, so the index stays valid in the grown path
let i = (after + 1).min(n);
if m.path.is_animated() && m.path.keyframes.len() > 1 {
if n == 0 {
return Err(bad("masks.addVertex", "the animated mask path has no vertices to insert after"));
}
// keep every keyframe interpolable: insert the vertex into all of them
for k in &mut m.path.keyframes {
if let ParamValue::Path(path) = &mut k.value {
let i = (after + 1).min(path.vertices.len());
let a = path.vertices[after.min(path.vertices.len() - 1)].p;
let b = path.vertices[i % path.vertices.len()].p;
path.vertices.insert(i, MaskVertex::corner(a.lerp(b, 0.5)));
let len = path.vertices.len();
if len == 0 {
continue;
}
let at_i = i.min(len);
let a = path.vertices[after.min(len - 1)].p;
let b = path.vertices[at_i % len].p;
path.vertices.insert(at_i, MaskVertex::corner(a.lerp(b, 0.5)));
}
}
}
let mut path = m.path_at(t);
let i = (after + 1).min(path.vertices.len());
if m.path.keyframes.len() > 1 {
path.vertices[i] = MaskVertex::corner(at);
if let Some(v) = path.vertices.get_mut(i) {
*v = MaskVertex::corner(at);
}
} else {
path.vertices.insert(i, MaskVertex::corner(at));
path.vertices.insert(i.min(path.vertices.len()), MaskVertex::corner(at));
}
set_path_at(m, t, path);
Ok(())
+33
View File
@@ -106,6 +106,39 @@ fn keyframed_mask_path_interpolates() {
assert_eq!(a["masks"][0]["path"]["vertices"].as_array().unwrap().len(), 4);
}
#[test]
fn add_vertex_on_animated_path_clamps_oversized_index_and_rejects_empty_path() {
let (mut s, clip) = demo();
let it = s.active_sequence().unwrap().find_item(clip).unwrap().1.clone();
let rate = s.active_sequence().unwrap().settings.frame_rate;
s.set_playhead(it.start);
s.execute("masks.add", json!({"effect": "opacity", "shape": "polygon", "center": [400, 400], "size": [200, 200]})).unwrap();
s.execute("effects.toggleAnimation", json!({"clip": clip.0, "effect": "opacity", "mask": 0, "param": "path"})).unwrap();
s.set_playhead(it.start + rate.tick_of(10));
s.execute("masks.translate", json!({"delta": [5, 0]})).unwrap();
// an index past the last vertex appends instead of panicking
s.execute("masks.addVertex", json!({"after": 100, "at": [1, 1]})).unwrap();
let l = s.execute("masks.list", json!({})).unwrap();
assert_eq!(l["masks"][0]["path"]["vertices"].as_array().unwrap().len(), 5);
// an empty animated path is a parameter error and leaves the mask alone
s.set_playhead(it.start);
s.execute("masks.add", json!({"effect": "opacity", "path": {"vertices": [], "closed": false}})).unwrap();
s.execute("effects.toggleAnimation", json!({"clip": clip.0, "effect": "opacity", "mask": 1, "param": "path"})).unwrap();
s.set_playhead(it.start + rate.tick_of(10));
s.execute("masks.translate", json!({"mask": 1, "delta": [5, 0]})).unwrap();
let e = s.execute("masks.addVertex", json!({"mask": 1, "after": 0, "at": [1, 1]})).unwrap_err().to_string();
assert!(e.contains("no vertices"), "{e}");
let l = s.execute("masks.list", json!({})).unwrap();
assert_eq!(l["masks"][1]["path"]["vertices"].as_array().unwrap().len(), 0);
// a static empty path still takes its first vertex
s.execute("masks.add", json!({"effect": "opacity", "path": {"vertices": [], "closed": false}})).unwrap();
s.execute("masks.addVertex", json!({"mask": 2, "after": 0, "at": [1, 1]})).unwrap();
let l = s.execute("masks.list", json!({})).unwrap();
assert_eq!(l["masks"][2]["path"]["vertices"].as_array().unwrap().len(), 1);
}
#[test]
fn merged_drag_is_one_undo_step_and_project_round_trips() {
let (mut s, _) = demo();