diff --git a/crates/engine/src/cmd/paths/closed.rs b/crates/engine/src/cmd/paths/closed.rs index d3cd8c1ef..268e68843 100644 --- a/crates/engine/src/cmd/paths/closed.rs +++ b/crates/engine/src/cmd/paths/closed.rs @@ -38,19 +38,18 @@ fn targets(s: &Session) -> Result> { props: s.state.selected_props.iter().filter(|(lid, _)| s.state.selected_layers.contains(lid)).copied().collect(), time: s.time(), }; + let whole_layers = selection.vertices.is_empty() && selection.props.is_empty(); let mut targets = Vec::new(); for layer in comp.layers.iter() { - let selected_layer = if !selection.vertices.is_empty() { - selection.vertices.iter().any(|v| v.layer == layer.id) - } else if !selection.props.is_empty() { - selection.props.iter().any(|(lid, _)| *lid == layer.id) - } else { + let selected_layer = if whole_layers { s.state.selected_layers.contains(&layer.id) + } else { + selection.vertices.iter().any(|v| v.layer == layer.id) || selection.props.iter().any(|(lid, _)| *lid == layer.id) }; if !selected_layer || layer.switches.locked { continue; } - collect(&selection, layer, &layer.props, &mut Vec::new(), selection.props.is_empty(), &mut targets)?; + collect(&selection, layer, &layer.props, &mut Vec::new(), whole_layers, &mut targets)?; } if targets.is_empty() { return Err(bad("path.toggleClosed", "select an unlocked mask or Bezier shape path")); @@ -70,11 +69,10 @@ fn collect(selection: &Selection, layer: &Layer, group: &PropGroup, route: &mut && let Some(prop) = group.get("path") && let KV::Path(path) = prop.value_at(layer.layer_time(selection.time)) { - let selected = if selection.vertices.is_empty() { - selected || selection.props.contains(&(layer.id, prop.uid)) - } else { - selection.vertices.iter().any(|v| v.layer == layer.id && v.mask == group.uid && v.index < path.vertices.len()) - }; + // Property selection also selects its points; both describe the selected paths. + let selected = selected + || selection.props.contains(&(layer.id, prop.uid)) + || selection.vertices.iter().any(|v| v.layer == layer.id && v.mask == group.uid && v.index < path.vertices.len()); if selected { targets.push(Target { layer: layer.id, route: route.clone(), closed: path.closed }); } diff --git a/crates/engine/src/tests/viewer.rs b/crates/engine/src/tests/viewer.rs index 36e3b0537..a9cd5c646 100644 --- a/crates/engine/src/tests/viewer.rs +++ b/crates/engine/src/tests/viewer.rs @@ -302,7 +302,7 @@ fn toggle_closed_respects_path_and_vertex_selection() { s.state.selected_props = vec![(LayerId(lid), b)]; s.state.selected_vertices = vec![crate::VertexRef { layer: LayerId(lid), mask: a, index: 1 }]; s.execute("path.toggleClosed", json!({})).unwrap(); - assert!(shape_path(&s, lid, a).closed, "vertices take precedence over property selection"); + assert!(shape_path(&s, lid, a).closed, "selected points and properties both describe target paths"); assert!(shape_path(&s, lid, b).closed); s.state.selected_vertices.clear(); s.state.selected_props = vec![(LayerId(lid), a), (LayerId(lid), leaf), (LayerId(lid), b)]; @@ -323,6 +323,31 @@ fn toggle_closed_respects_path_and_vertex_selection() { assert!(!shape_path(&s, lid, b).closed, "layer selection addresses all editable paths"); } +#[test] +fn toggle_closed_combines_property_and_shape_group_selections() { + let mut s = comp(); + let a = s.execute("shape.newPath", json!({"vertices": [[0, 0], [100, 0], [100, 100]], "closed": true})).unwrap(); + let (lid, a_path) = (a["layer"].as_u64().unwrap(), a["path"].as_u64().unwrap()); + let b = s.execute("shape.newPath", json!({"layer": lid, "vertices": [[10, 10], [90, 10], [90, 90]], "closed": true})).unwrap(); + let (b_path, b_group) = (b["path"].as_u64().unwrap(), b["group"].as_u64().unwrap()); + let c_path = + s.execute("shape.newPath", json!({"layer": lid, "vertices": [[20, 20], [80, 20], [80, 80]], "closed": true})).unwrap()["path"].as_u64().unwrap(); + s.execute("prop.select", json!({"layer": lid, "prop": a_path})).unwrap(); + s.execute("prop.select", json!({"layer": lid, "prop": b_group, "add": true})).unwrap(); + assert!(s.state.selected_vertices.iter().all(|v| v.mask == a_path), "only the path property implies selected vertices"); + assert!(!s.state.selected_vertices.is_empty()); + assert!(s.state.selected_props.contains(&(LayerId(lid), b_group))); + let result = s.execute("path.toggleClosed", json!({})).unwrap(); + assert_eq!(result["paths"], json!(2)); + assert!(!shape_path(&s, lid, a_path).closed); + assert!(!shape_path(&s, lid, b_path).closed, "the added shape group is included alongside implied vertices"); + assert!(shape_path(&s, lid, c_path).closed, "unselected sibling stays unchanged"); + s.execute("edit.undo", json!({})).unwrap(); + assert!(shape_path(&s, lid, a_path).closed); + assert!(shape_path(&s, lid, b_path).closed, "both selected paths undo together"); + assert!(shape_path(&s, lid, c_path).closed); +} + #[test] fn toggle_closed_ignores_selections_retained_on_other_layers() { let mut s = comp();