Merge pull request #4842 from dotCooCoo/fix/assemble-keeps-visibility

Outline Stroke, Expand: hidden and locked objects stay hidden and locked
This commit is contained in:
BFlat
2026-10-31 20:09:38 +07:00
committed by GitHub
3 changed files with 63 additions and 1 deletions
+4 -1
View File
@@ -888,11 +888,13 @@ pub(crate) fn outline_strokes(d: &mut Document, brushes: &[vectorcraft_brush::Br
}
/// A plain copy of path `l` (new ids, no transparency, name or mask) painted with `items`.
/// The copy is shown and unlocked.
pub(crate) fn styled_copy(d: &mut Document, l: &Node, items: Vec<AppearanceItem>) -> Node {
let mut f = d.reid(l);
f.appearance = Appearance { items, ..Default::default() }.into();
f.opacity = 1.0;
f.blend = BlendMode::Normal;
(f.visible, f.locked, f.knockout_shape) = (true, false, false);
f.name = None;
f.extras_mut().mask = None;
if let NodeKind::Path { live, .. } = &mut f.kind {
@@ -903,7 +905,7 @@ pub(crate) fn styled_copy(d: &mut Document, l: &Node, items: Vec<AppearanceItem>
/// What stands for object `l` once its appearance became `parts` (paint order): the one part (in a
/// group when it has effects of its own) or a group of them, with `l`'s transparency, name, opacity
/// mask and effects. `None` without parts.
/// mask, effects, and hidden, locked and knockout-shape states. `None` without parts.
pub(crate) fn assemble(d: &mut Document, l: &Node, mut parts: Vec<Node>) -> Option<Node> {
let root = match parts.len() {
0 => return None,
@@ -911,6 +913,7 @@ pub(crate) fn assemble(d: &mut Document, l: &Node, mut parts: Vec<Node>) -> Opti
_ => Node::group(d.alloc_id(), parts.into_iter().map(Arc::new).collect()),
};
let mut root = with_transparency(d, root, l.opacity, l.blend);
(root.visible, root.locked, root.knockout_shape) = (l.visible, l.locked, l.knockout_shape);
root.name = l.name.clone();
root.extras_mut().mask = l.extras().mask.clone();
root.appearance.effects = l.appearance.effects.clone();
+29
View File
@@ -159,6 +159,35 @@ fn eight_strips_match_the_gradient_samples() {
assert!(matches!(selected(&s).appearance.fill_paint(), Paint::Gradient(_)));
}
#[test]
fn expanding_a_group_keeps_its_hidden_and_locked_members_hidden_and_locked() {
// A hidden stroked square with a gradient fill, a locked square with a pattern fill and a
// plain square, grouped.
let mut s = session();
let pat = pattern_rect(&mut s, Rect::new(20.0, 100.0, 80.0, 160.0), false);
let grad = gradient_rect(&mut s, Rect::new(20.0, 20.0, 80.0, 80.0), white_black(), true);
let plain = NodeId(run(&mut s, "shape.rectangle", json!({"x": 100, "y": 20, "width": 40, "height": 40}))["id"].as_u64().unwrap());
run(&mut s, "select.set", json!({"ids": [pat.0, grad.0, plain.0]}));
run(&mut s, "object.group", json!({}));
s.edit("t", |d, _| {
d.node_mut(grad).unwrap().visible = false;
d.node_mut(pat).unwrap().locked = true;
Ok(())
})
.unwrap();
run(&mut s, "object.expand", json!({}));
let g = selected(&s);
let ch = g.children().unwrap();
assert_eq!(ch.len(), 3);
assert!(!clip_parts(&ch[0]).1.is_empty() && ch[0].locked && ch[0].visible, "the pattern's tiles stay locked: {:?}", ch[0]);
assert!(!ch[1].visible && !ch[1].locked, "the gradient square's art stays hidden: {:?}", ch[1]);
assert!(ch[2].visible && !ch[2].locked);
// Showing or unlocking the new art shows or unlocks all of it.
for c in ch[..2].iter().flat_map(|c| c.children().unwrap().iter()) {
c.walk(&mut |n| assert!(n.visible && !n.locked, "{n:?}"));
}
}
#[test]
fn a_radial_gradient_becomes_concentric_ellipses() {
let mut s = session();
+30
View File
@@ -122,6 +122,36 @@ fn every_stroke_is_outlined_in_paint_order() {
assert_eq!(d.layers[0].children().unwrap()[0].appearance.items.len(), 3);
}
/// The art that stands for an outlined path keeps the path's hidden, locked and knockout-shape
/// states, so a hidden member of a selected group stays hidden.
#[test]
fn outlined_art_keeps_the_hidden_locked_and_knockout_shape_states() {
let mut s = session();
// A filled and stroked square (a fill copy and an outline) and a stroked line (one outline).
let sq = NodeId(s.execute("shape.rectangle", &json!({"x": 0, "y": 100, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap());
s.execute("paint.setFill", &json!({"color": "#ff0000"})).unwrap();
s.execute("paint.setStroke", &json!({"color": "#000000"})).unwrap();
let ln = line(&mut s, json!({"weight": 4}));
s.execute("select.set", &json!({"ids": [sq.0, ln.0]})).unwrap();
s.execute("object.group", &json!({})).unwrap();
s.edit("t", |d, _| {
d.node_mut(sq).unwrap().visible = false;
let l = d.node_mut(ln).unwrap();
l.locked = true;
l.knockout_shape = true;
Ok(())
})
.unwrap();
let g = outline(&mut s);
let ch = g.children().unwrap();
assert_eq!(ch.len(), 2);
let (sq, ln) = (&ch[0], &ch[1]);
assert!(!sq.visible && !sq.locked, "the square's art stays hidden: {sq:?}");
assert_eq!(sq.children().map(|c| c.len()), Some(2), "a fill copy and the outline");
assert!(sq.children().unwrap().iter().all(|c| c.visible && !c.locked), "showing it shows all of it: {sq:?}");
assert!(ln.visible && ln.locked && ln.knockout_shape, "the line's outline stays locked: {ln:?}");
}
/// The live effect on a stroke item outlines that stroke (its own weight), not the top stroke.
#[test]
fn the_live_effect_outlines_the_stroke_it_sits_on() {