collective

Browse / github.com/storytold/vectorcraft / patch

Keep Group Selection drags on the selected outlined component

Keep Group Selection drags on the selected outlined component ## What and why Outline Stroke keeps every original fill and replaces live strokes with filled outline geometry. Group Selection drags an already selected fill or outlined stroke independently, for open and closed paths. Repeated completed clicks still advance through enclosing groups; beginning a drag does not prematurely promote selection. Existing compound-member editing and Undo/Redo remain. This update incorporates upstream main `f792c63597f289d0bfb914fd06255cd1e4c8852d` and retains its behavior. The production fix is in Group Selection; fill preservation is existing upstream be

Status
open
Score
0.00
Adopters
0 other operators
Author
FractalEncrypt (imported from pull request #3272, open)
Base
f792c63597f289d0bfb914fd06255cd1e4c8852d
Size
+396 −3 in 7 files
Review
pending
Shared
2026-10-28
Id
541983ebff0a68319e47c806

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/cmd/pathops.rs crates/engine/src/tests_pathops.rs crates/tools/src/direct.rs docs/gaps.md docs/roadmap.md docs/target-app-parity.md docs/ui-parity.md

diff --git a/crates/engine/src/cmd/pathops.rs b/crates/engine/src/cmd/pathops.rs--- a/crates/engine/src/cmd/pathops.rs+++ b/crates/engine/src/cmd/pathops.rs@@ -721,6 +721,8 @@ pub(crate) fn outline_strokes(d: &mut Document, brushes: &[vectorcraft_brush::Br         return None;     }     let (path, rule) = node_path(l)?;+    // A fill belongs to the source path, whether open or closed. Converting its stroke+    // preserves that fill on a separate copy (including compound member geometry).     let mut parts: Vec<Node> = vec![];     let mut fills: Vec<AppearanceItem> = vec![];     for item in &l.appearance.items {diff --git a/crates/engine/src/tests_pathops.rs b/crates/engine/src/tests_pathops.rs--- a/crates/engine/src/tests_pathops.rs+++ b/crates/engine/src/tests_pathops.rs@@ -448,6 +448,26 @@ fn outline_stroke_of_line() {     assert!((area_of(&s, r[0]) - 1000.0).abs() < 1.0); } +#[test]+fn outline_stroke_preserves_a_zero_area_fill_without_retaining_a_live_stroke() {+    let mut s = session();+    s.execute("shape.line", &json!({"x1": 10, "y1": 20, "x2": 60, "y2": 90})).unwrap();+    s.execute("paint.setFill", &json!({"color":"#ffffff"})).unwrap();+    let before = s.doc().unwrap().doc.clone();+    let result = ids(&s.execute("object.path.outlineStroke", &json!({})).unwrap());+    let n = node(&s, result[0]);+    let children = n.children().expect("the original fill and the outlined stroke");+    assert_eq!(children.len(), 2);+    assert!(children.iter().all(|n| n.appearance.stroke().is_none()));+    assert!(children.iter().any(|n| n.appearance.fill_paint() == Paint::solid(Color::WHITE)), "an invisible fill is not a live stroke");+    assert!(n.appearance.stroke().is_none());+    assert!(children.iter().any(|n| n.appearance.fill_paint() == Paint::solid(Color::BLACK)));+    s.execute("edit.undo", &json!({})).unwrap();+    assert_eq!(s.doc().unwrap().doc.layers, before.layers);+    s.execute("edit.redo", &json!({})).unwrap();+    assert!(node(&s, result[0]).appearance.stroke().is_none());+}+ #[test] fn outline_stroke_with_fill_makes_group() {     let mut s = session();@@ -1065,6 +1085,206 @@ fn trim_merge_and_crop_remove_strokes() {     } } +#[test]+fn outline_stroke_preserves_tiny_and_signed_area_canceling_closed_fills() {+    for points in [vec![[0.0, 0.0], [100.0, 100.0], [0.0, 100.0], [100.0, 0.0]], vec![[0.0, 0.0], [1e-8, 0.0], [1e-8, 1e-8]]] {+        let mut s = session();+        let anchors: Vec<Value> = points.into_iter().map(|[x, y]| json!({"x":x,"y":y})).collect();+        s.execute("path.create", &json!({"anchors":anchors,"closed":true})).unwrap();+        let result = ids(&s.execute("object.path.outlineStroke", &json!({})).unwrap());+        let n = node(&s, result[0]);+        let retains_fill = n.children().is_some_and(|ch| ch.iter().any(|c| c.appearance.fill_paint() == Paint::solid(Color::WHITE)))+            || n.appearance.fill_paint() == Paint::solid(Color::WHITE);+        assert!(retains_fill, "a real fill survives independently of its signed area or scale");+    }+}++#[test]+fn outline_stroke_preserves_bent_open_fill_and_history() {+    let mut s = session();+    s.execute("path.create", &json!({"anchors":[{"x":0,"y":0},{"x":60,"y":0},{"x":75,"y":-15},{"x":110,"y":-15}],"closed":false})).unwrap();+    let before = s.doc().unwrap().doc.clone();+    let result = ids(&s.execute("object.path.outlineStroke", &json!({})).unwrap());+    let n = node(&s, result[0]);+    let children = n.children().expect("original fill beside the outlined stroke");+    assert_eq!(children.len(), 2);+    let fill = children.iter().find(|n| n.appearance.fill_paint() == Paint::solid(Color::WHITE)).unwrap();+    assert!(!fill.path_data().unwrap().subpaths[0].closed);+    assert!(children.iter().all(|n| n.appearance.stroke().is_none()));+    let after = s.doc().unwrap().doc.layers.clone();+    s.execute("edit.undo", &json!({})).unwrap();+    assert_eq!(s.doc().unwrap().doc.layers, before.layers);+    s.execute("edit.redo", &json!({})).unwrap();+    assert_eq!(s.doc().unwrap().doc.layers, after);+}++#[test]+fn outline_stroke_mixed_contours_preserve_all_fill() {+    use vectorcraft_geom::{Anchor, Point, SubPath};+    let mut s = session();+    let id = rect(&mut s, 0.0, 0.0, 20.0, 20.0);+    s.edit("mixed contours", |d, _| {+        let n = d.node_mut(id).unwrap();+        if let NodeKind::Path { path, .. } = &mut n.kind {+            path.subpaths.push(SubPath::new(+                vec![Anchor::corner(Point::new(50.0, 0.0)), Anchor::corner(Point::new(80.0, 0.0)), Anchor::corner(Point::new(80.0, 20.0))],+                false,+            ));+        }+        Ok(())+    })+    .unwrap();+    let result = ids(&s.execute("object.path.outlineStroke", &json!({})).unwrap());+    let outlined = node(&s, result[0]);+    let children = outlined.children().unwrap();+    let fill = children.iter().find(|n| n.appearance.fill_paint() == Paint::solid(Color::WHITE)).unwrap();+    let NodeKind::Path { path, .. } = &fill.kind else { panic!("fill is a path") };+    assert_eq!(path.subpaths.len(), 2);+    assert!(path.subpaths[0].closed);+    assert!(!path.subpaths[1].closed);+}++#[test]+fn outline_stroke_keeps_closed_compound_members_editable() {+    check_outlined_compound_members(false);+}++#[test]+fn outline_stroke_keeps_open_compound_contours_and_members() {+    check_outlined_compound_members(true);+}++fn check_outlined_compound_members(mixed: bool) {+    use vectorcraft_geom::{Anchor, Point, SubPath};+    let mut s = session();+    let outer = rect(&mut s, 10.0, 10.0, 100.0, 100.0);+    let inner = rect(&mut s, 35.0, 35.0, 30.0, 30.0);+    let mut members = vec![outer.0, inner.0];+    if mixed {+        let open = s.execute("path.create", &json!({"anchors":[{"x":140,"y":10},{"x":170,"y":30},{"x":140,"y":50}]})).unwrap();+        members.push(open["id"].as_u64().unwrap());+        s.edit("Mixed compound member", |d, _| {+            let NodeKind::Path { path, .. } = &mut d.node_mut(inner).unwrap().kind else { panic!("path") };+            path.subpaths.push(SubPath::new(vec![Anchor::corner(Point::new(45.0, 80.0)), Anchor::corner(Point::new(70.0, 90.0))], false));+            Ok(())+        })+        .unwrap();+    }+    s.execute("select.set", &json!({"ids":members})).unwrap();+    let compound = NodeId(s.execute("object.compoundPath.make", &json!({})).unwrap()["id"].as_u64().unwrap());+    s.edit("Compound member names and rule", |d, _| {+        d.node_mut(outer).unwrap().name = Some("outer-member".into());+        d.node_mut(inner).unwrap().name = Some("inner-member".into());+        let NodeKind::Compound { rule, .. } = &mut d.node_mut(compound).unwrap().kind else { panic!("compound") };+        *rule = FillRule::EvenOdd;+        Ok(())+    })+    .unwrap();+    let original = node(&s, compound);+    let expected: Vec<_> = original+        .children()+        .unwrap()+        .iter()+        .map(|child| {+            let path = child.path_data().unwrap().clone();+            (child.name.clone(), path)+        })+        .collect();+    let before = s.doc().unwrap().doc.layers.clone();+    let result = ids(&s.execute("object.path.outlineStroke", &json!({})).unwrap())[0];+    let group = node(&s, result);+    let fill = group.children().unwrap().iter().find(|child| child.appearance.fill_paint() == Paint::solid(Color::WHITE)).unwrap();+    let NodeKind::Compound { children, rule } = &fill.kind else { panic!("closed fill must retain editable compound members") };+    assert_eq!(*rule, FillRule::EvenOdd);+    assert_eq!(children.len(), if mixed { 3 } else { 2 });+    for (child, (name, path)) in children.iter().zip(expected) {+        assert_eq!(child.name, name);+        assert_eq!(child.path_data().unwrap(), &path);+        assert_ne!(child.id, outer);+        assert_ne!(child.id, inner);+    }+    let member = children[0].id;+    let sibling = children[1].id;+    let member_bounds = children[0].geometric_bounds().unwrap();+    let sibling_before = children[1].clone();+    let after = s.doc().unwrap().doc.layers.clone();+    s.execute("edit.undo", &json!({})).unwrap();+    assert_eq!(s.doc().unwrap().doc.layers, before);+    s.execute("edit.redo", &json!({})).unwrap();+    assert_eq!(s.doc().unwrap().doc.layers, after);+    // Group Selection can still address one retained child without moving its sibling.+    s.execute("select.set", &json!({"ids":[member.0]})).unwrap();+    s.execute("object.move", &json!({"dx":7,"dy":3})).unwrap();+    assert!(close(+        node(&s, member).geometric_bounds().unwrap(),+        Rect::new(member_bounds.x0 + 7.0, member_bounds.y0 + 3.0, member_bounds.x1 + 7.0, member_bounds.y1 + 3.0),+        1e-6+    ));+    assert_eq!(node(&s, sibling), *sibling_before);+}++/// Exercise the real Group Selection pointer route, not select.set by id.+#[test]+fn outline_fill_and_stroke_move_independently_with_group_selection() {+    use vectorcraft_tools::{PointerEvent, PointerKind};+    for closed in [true, false] {+        for move_fill in [false, true] {+            for click_first in [false, true] {+                let mut s = session();+                s.execute(+                    "path.create",+                    &json!({"anchors":[{"x":100,"y":100},{"x":200,"y":100},{"x":200,"y":200},{"x":100,"y":200}],"closed":closed}),+                )+                .unwrap();+                s.execute("paint.setFill", &json!({"color":"#ff0000"})).unwrap();+                s.execute("stroke.set", &json!({"color":"#000000","weight":10})).unwrap();+                let root = ids(&s.execute("object.path.outlineStroke", &json!({})).unwrap())[0];+                let group = node(&s, root);+                let children = group.children().expect("fill and outlined stroke stay separate");+                assert_eq!(children.len(), 2);+                let fill = children.iter().find(|c| c.appearance.fill_paint().color().is_some_and(|c| c.to_hex() == "#ff0000")).unwrap().id;+                let stroke = children.iter().find(|c| c.id != fill).unwrap().id;+                group.walk(&mut |n| assert!(n.appearance.stroke().is_none(), "no retained live stroke"));+                let moved = if move_fill { fill } else { stroke };+                let other = if move_fill { stroke } else { fill };+                let other_before = node(&s, other);+                let bounds_before = bounds(&s, moved);+                let layers_before_move = s.doc().unwrap().doc.layers.clone();+                let v = ViewInfo { smart_guides: false, snap_to_point: false, ..ViewInfo::default() };+                s.select_tool("groupSelection", v).unwrap();+                // Start empty, as clicking the art with Group Selection does after deselection.+                s.execute("select.none", &json!({})).unwrap();+                let (x, y) = if move_fill { (150.0, 150.0) } else { (150.0, 100.0) };+                let compound_stroke = !move_fill && matches!(node(&s, stroke).kind, NodeKind::Compound { .. });+                for _ in 0..if compound_stroke {+                    2+                } else if click_first {+                    1+                } else {+                    0+                } {+                    s.pointer(&PointerEvent::new(PointerKind::Down, x, y), v).unwrap();+                    s.pointer(&PointerEvent::new(PointerKind::Up, x, y), v).unwrap();+                }+                for (kind, x, y) in [(PointerKind::Down, x, y), (PointerKind::Drag, x + 30.0, y + 15.0), (PointerKind::Up, x + 30.0, y + 15.0)] {+                    s.pointer(&PointerEvent::new(kind, x, y), v).unwrap();+                }+                assert_eq!(node(&s, other), other_before, "sibling unchanged: closed={closed}, fill={move_fill}, click_first={click_first}");+                let b = bounds(&s, moved);+                assert!(+                    close(b, Rect::new(bounds_before.x0 + 30.0, bounds_before.y0 + 15.0, bounds_before.x1 + 30.0, bounds_before.y1 + 15.0), 1e-6),+                    "moved leaf: {b:?}"+                );+                let layers_after_move = s.doc().unwrap().doc.layers.clone();+                s.execute("edit.undo", &json!({})).unwrap();+                assert_eq!(s.doc().unwrap().doc.layers, layers_before_move);+                s.execute("edit.redo", &json!({})).unwrap();+                assert_eq!(s.doc().unwrap().doc.layers, layers_after_move);+            }+        }+    }+}+ /// #1590: an inset larger than the shape leaves nothing, and the error says so rather than asking /// for paths that are selected. #[test]diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs--- a/crates/tools/src/direct.rs+++ b/crates/tools/src/direct.rs@@ -168,6 +168,8 @@ pub struct DirectSelectionTool {     click: Option<(NodeId, usize, usize)>,     /// A click on a whole path picks the segment on release; a drag still moves the whole path.     click_segment: Option<(NodeId, usize, usize)>,+    /// Group Selection advances to an enclosing group only on a completed click, not a drag.+    group_promote: Option<NodeId>, }  impl DirectSelectionTool {@@ -187,6 +189,7 @@ impl DirectSelectionTool {             guide: GuideEdit::default(),             click: None,             click_segment: None,+            group_promote: None,         }     } @@ -474,6 +477,7 @@ impl Tool for DirectSelectionTool {         self.guides.clear();         (self.anchor_snap, self.copy_snap, self.move_snap) = (None, None, None);         (self.click, self.click_segment) = (None, None);+        self.group_promote = None;         vec![Action::Cancel]     }     fn claims_key(&self, _cx: &ToolContext, key: ToolKey) -> bool {@@ -488,6 +492,7 @@ impl Tool for DirectSelectionTool {         let (tol, point) = (cx.pick_tol(), cx.point_tol());         match (ev.kind, self.state.clone()) {             (PointerKind::Down, _) if self.group => {+                self.group_promote = None;                 if let Some(out) = self.guide.press(cx, ev) {                     self.state = State::Idle;                     return out;@@ -521,7 +526,7 @@ impl Tool for DirectSelectionTool {                     }                 } else if !cx.selection.contains(target) {                     if cx.selection.contains(h.leaf) {-                        out.push(Action::Exec("select.add".into(), json!({"ids": [target.0]})));+                        self.group_promote = Some(target);                     } else {                         out.push(Action::Exec("select.set".into(), json!({"ids": [target.0]})));                     }@@ -854,6 +859,10 @@ impl Tool for DirectSelectionTool {                 self.state = State::Idle;                 self.guides.clear();                 (self.anchor_snap, self.copy_snap, self.move_snap) = (None, None, None);+                let promote = self.group_promote.take();+                if !began && let Some(id) = promote {+                    return vec![Action::Exec("select.add".into(), json!({"ids": [id.0]}))];+                }                 match (began, deselect, self.click.take(), self.click_segment.take()) {                     (true, ..) => vec![Action::Commit],                     (false, Some(id), _, _) => vec![Action::Exec("select.toggle".into(), json!({"id": id.0}))],@@ -1515,11 +1524,21 @@ mod tests {         d.insert(Some(l), 0, c).unwrap();         let p = paint();         let none = Selection::default();-        let click = |s: &Selection, x, y| DirectSelectionTool::new(true).pointer(&cx(&d, s, &p), &PointerEvent::new(PointerKind::Down, x, y));+        let click = |s: &Selection, x, y| {+            let mut t = DirectSelectionTool::new(true);+            let context = cx(&d, s, &p);+            let mut actions = t.pointer(&context, &PointerEvent::new(PointerKind::Down, x, y));+            actions.extend(t.pointer(&context, &PointerEvent::new(PointerKind::Up, x, y)));+            actions+        };         let set = |id: NodeId| vec![Action::Exec("select.set".into(), json!({"ids": [id.0]}))];         assert_eq!(click(&none, 25.0, 100.0), set(outer), "the fill between the outline and the hole");         assert_eq!(click(&none, 50.0, 100.0), set(hole), "the hole's outline");-        assert!(click(&none, 100.0, 100.0).is_empty(), "inside the hole nothing is hit");+        assert_eq!(+            click(&none, 100.0, 100.0),+            vec![Action::Exec("select.none".into(), json!({}))],+            "a completed click inside the hole clears selection"+        );         let mut s = Selection::default();         s.set([outer]);         assert_eq!(click(&s, 25.0, 100.0), vec![Action::Exec("select.add".into(), json!({"ids": [compound.0]}))]);@@ -1573,6 +1592,31 @@ mod tests {         (d, inside, outside, inner, outer)     } +    /// A second press on a selected leaf must not promote its drag to the enclosing group.+    /// A completed click still advances one group, and Escape cancels that pending click.+    #[test]+    fn group_selection_distinguishes_selected_leaf_drags_from_clicks() {+        let (d, inside, _, inner, _) = nested_group_doc();+        let paint = paint();+        let mut selected = Selection::default();+        selected.set([inside]);+        let context = cx(&d, &selected, &paint);+        let mut t = DirectSelectionTool::new(true);+        let down = PointerEvent::new(PointerKind::Down, 150.0, 150.0);+        assert!(t.pointer(&context, &down).is_empty());+        let actions = t.pointer(&context, &PointerEvent::new(PointerKind::Drag, 180.0, 165.0));+        assert!(matches!(actions.as_slice(), [Action::Begin(_), Action::Preview(command, _)] if command=="object.transform"));+        assert_eq!(t.pointer(&context, &PointerEvent::new(PointerKind::Up, 180.0, 165.0)), vec![Action::Commit]);+        assert!(t.pointer(&context, &down).is_empty());+        assert_eq!(+            t.pointer(&context, &PointerEvent::new(PointerKind::Up, 150.0, 150.0)),+            vec![Action::Exec("select.add".into(), json!({"ids":[inner.0]}))]+        );+        assert!(t.pointer(&context, &down).is_empty());+        assert_eq!(t.key(&context, ToolKey::Escape, Mods::default()), vec![Action::Cancel]);+        assert!(t.pointer(&context, &PointerEvent::new(PointerKind::Up, 150.0, 150.0)).is_empty());+    }+     /// Isolation mode: a marquee and an anchor click take the path inside the isolated group and     /// leave the path beside it. Group Selection stops at the isolated group instead of adding     /// the group around it.diff --git a/docs/gaps.md b/docs/gaps.md--- a/docs/gaps.md+++ b/docs/gaps.md@@ -42,6 +42,27 @@ one agent, estimated unless marked; calibration in [target-app-parity.md](target-app-parity.md#calibration). **Beta** marks the gaps that must close before the beta stage ([ROADMAP.md](../ROADMAP.md)). +## Outline Stroke submission evidence++**Feature evidence last updated:** 2026-10-23.++The shared assessment status line and revision table retain the feature-base snapshot; merging retains upstream's current shared assessment and history. This dated section records the submission's feature evidence and does not rescore upstream or claim a new combined build or runtime measurement.++- Current-main conflict reconciliation retains upstream and feature behavior; scores and estimates unchanged+- Outline Stroke preserves open- and closed-contour fills as separate editable objects alongside filled stroke outlines; Group Selection distinguishes clicking up the group hierarchy from dragging a selected object. Validation of this correction is recorded separately from prior deletion behavior.++Public behavior references: [Outline Stroke](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/convert-strokes-to-compound-paths.html), [fills on open paths](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/about-fills-and-strokes.html), and [selecting objects within groups](https://helpx.adobe.com/ca/illustrator/desktop/manage-objects/select-objects/select-object-groups-and-nested-object-groups.html).++### Feature revisions++| Date | Change | Summary |+|---|---|---|+| 2026-10-23 | correction | Supersede the proposed open-fill deletion: preserve original fills and outline strokes separately. Cover independent Group Selection drags without promoting the selection on press; repeated clicks still select enclosing groups. No score or hours change; fresh review required. |+| 2026-10-21 | minor | Reconcile feature status and revision evidence in a dated section; retain shared baseline metadata for independent upstream updates. Scores and estimates are not reassessed. |+| 2026-10-21 | minor | Reconcile this feature with current main, preserving both sets of behavior and progress evidence; scores and estimates unchanged. |+| 2026-10-20 | minor | Historical proposal (superseded 2026-10-23): Outline Stroke follow-up: preserve closed compound fill members and their individual editing while filtering open contours; scores and hours unchanged. |+| 2026-10-20 | minor | Historical proposal (superseded 2026-10-23): Outline Stroke consumes open-contour fills and preserves closed-contour fills, including mixed paths; scores and hours unchanged. |+ ## Ranked  | Rank | Gap | Hours | Beta | Doc |@@ -227,6 +248,8 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall  - **Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views:** `live_trace_result_bounds_exclude_the_retained_bitmap_at_any_angle` checks every TraceView, geometric and visual bounds and rotated result bounds. `live_trace_selection_highlight_excludes_the_retained_source_rectangle` checks result highlights and the existing ordinary-group policy. Shared editing-child traversal also preserves Shaper handling. Broader scope and estimates unchanged. +- **Outline Stroke fill preservation:** original open, closed and mixed-contour fills remain editable alongside converted stroke geometry. Tests cover paint order, compound members, independent movement through Group Selection pointer events, and Undo/Redo. No live stroke remains. Fresh correction evidence is required; prior open-fill-deletion evidence is historical and superseded.+ - **Implemented, Windows native canvas and regression checks passed in PR #2643 (#2544):** Anchor Point edits selected blend spine points and handles with path-compatible selection/stacking, handle visibility and 45-degree constraints; #2425 perpendicular segment reshaping is preserved. Broader G11 scope and estimates unchanged.  - **Done (#1968):** native saves retain transparency grid visibility without adding an edit or undo step; older files default to hidden. Broader G11 scope and estimates unchanged.@@ -790,3 +813,12 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | 2026-10-22 | minor | Merge current main and complete the Turkish tab-drag labels and validation messages requested by the maintainer; all 24 catalogs covered; scores and estimates unchanged. | | 2026-10-22 | minor | Tab-drag follow-up: reconcile upstream progress updates and document drag lifecycle, insertion behavior and regression contracts; behavior, scores and estimates unchanged. | | 2026-10-22 | minor | Artwork dragged with Selection switches visible document tabs after a 400 ms hover, then copies editable artwork on canvas release; source and clipboard stay intact; scores and hours unchanged |+++### Outline Group Selection current-main follow-up — 2026-10-25++This private candidate retains original open and closed fills while replacing live strokes with+filled outlines. Group Selection defers ancestor promotion until a click is released, allowing+the selected fill or outlined stroke to drag independently. Current upstream changes are retained;+fresh combined-source checks and native evidence are required. No score, render-budget, platform+or publication acceptance is implied by a text merge or by historical review.diff --git a/docs/roadmap.md b/docs/roadmap.md--- a/docs/roadmap.md+++ b/docs/roadmap.md@@ -76,6 +76,27 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V   (~170–270 h one agent, ~50–80 h with 4–6 agents). - **1.0:** feature parity plus the fidelity pass, hardening and packaging for all three desktop OSes and the web. +## Outline Stroke submission evidence++**Feature evidence last updated:** 2026-10-23.++The shared assessment status line and revision table retain the feature-base snapshot; merging retains upstream's current shared assessment and history. This dated section records the submission's feature evidence and does not rescore upstream or claim a new combined build or runtime measurement.++- Current-main conflict reconciliation retains upstream and feature behavior; scores and estimates unchanged+- Outline Stroke preserves open- and closed-contour fills as separate editable objects alongside filled stroke outlines; Group Selection distinguishes clicking up the group hierarchy from dragging a selected object. Validation of this correction is recorded separately from prior deletion behavior.++Public behavior references: [Outline Stroke](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/convert-strokes-to-compound-paths.html), [fills on open paths](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/about-fills-and-strokes.html), and [selecting objects within groups](https://helpx.adobe.com/ca/illustrator/desktop/manage-objects/select-objects/select-object-groups-and-nested-object-groups.html).++### Feature revisions++| Date | Change | Summary |+|---|---|---|+| 2026-10-23 | correction | Supersede the proposed open-fill deletion: preserve original fills and outline strokes separately. Cover independent Group Selection drags without promoting the selection on press; repeated clicks still select enclosing groups. No score or hours change; fresh review required. |+| 2026-10-21 | minor | Reconcile feature status and revision evidence in a dated section; retain shared baseline metadata for independent upstream updates. Scores and estimates are not reassessed. |+| 2026-10-21 | minor | Reconcile this feature with current main, preserving both sets of behavior and progress evidence; scores and estimates unchanged. |+| 2026-10-20 | minor | Historical proposal (superseded 2026-10-23): Outline Stroke follow-up: preserve closed compound fill members and their individual editing while filtering open contours; scores and hours unchanged. |+| 2026-10-20 | minor | Historical proposal (superseded 2026-10-23): Outline Stroke consumes open-contour fills and preserves closed-contour fills, including mixed paths; scores and hours unchanged. |+ ## Milestones  | # | Milestone | Status | Est. remaining, one agent (h) |@@ -284,3 +305,12 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-10-22 | minor | Merge current main and complete the Turkish tab-drag labels and validation messages requested by the maintainer; all 24 catalogs covered; scores and estimates unchanged. | | 2026-10-22 | minor | Tab-drag follow-up: reconcile upstream progress updates and document drag lifecycle, insertion behavior and regression contracts; behavior, scores and estimates unchanged. | | 2026-10-22 | minor | Artwork dragged with Selection switches visible document tabs after a 400 ms hover, then copies editable artwork on canvas release; source and clipboard stay intact; scores and hours unchanged |+++### Outline Group Selection current-main follow-up — 2026-10-25++This private candidate retains original open and closed fills while replacing live strokes with+filled outlines. Group Selection defers ancestor promotion until a click is released, allowing+the selected fill or outlined stroke to drag independently. Current upstream changes are retained;+fresh combined-source checks and native evidence are required. No score, render-budget, platform+or publication acceptance is implied by a text merge or by historical review.diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md--- a/docs/target-app-parity.md+++ b/docs/target-app-parity.md@@ -38,6 +38,31 @@ the work list made from it. Deep areas have their own checklists: [UI and intera [file formats](file-format-parity.md), [hardware](hardware-parity.md), [languages](localization-parity.md), [effects](effects-parity.md) and [type](type-parity.md). +## Outline Stroke submission evidence++**Feature evidence last updated:** 2026-10-23.++The shared assessment status line and revision table retain the feature-base snapshot; merging retains upstream's current shared assessment and history. This dated section records the submission's feature evidence and does not rescore upstream or claim a new combined build or runtime measurement.++- Current-main conflict reconciliation retains upstream and feature behavior; scores and estimates unchanged+- Outline Stroke preserves open- and closed-contour fills as separate editable objects alongside filled stroke outlines; Group Selection distinguishes clicking up the group hierarchy from dragging a selected object. Validation of this correction is recorded separately from prior deletion behavior.++- **Outline Stroke fill preservation:** original open, closed and mixed-contour fills remain editable alongside converted stroke geometry. Tests cover paint order, compound members, independent movement through Group Selection pointer events, and Undo/Redo. No live stroke remains. Fresh correction evidence is required; prior open-fill-deletion evidence is historical and superseded.++Compound fills keep all original member geometry, names and fill rules, including open members. Independently moving a retained member must not move its siblings or the outlined stroke.++Public behavior references: [Outline Stroke](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/convert-strokes-to-compound-paths.html), [fills on open paths](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/about-fills-and-strokes.html), and [selecting objects within groups](https://helpx.adobe.com/ca/illustrator/desktop/manage-objects/select-objects/select-object-groups-and-nested-object-groups.html).++### Feature revisions++| Date | Change | Summary |+|---|---|---|+| 2026-10-23 | correction | Supersede the proposed open-fill deletion: preserve original fills and outline strokes separately. Cover independent Group Selection drags without promoting the selection on press; repeated clicks still select enclosing groups. No score or hours change; fresh review required. |+| 2026-10-21 | minor | Reconcile feature status and revision evidence in a dated section; retain shared baseline metadata for independent upstream updates. Scores and estimates are not reassessed. |+| 2026-10-21 | minor | Reconcile this feature with current main, preserving both sets of behavior and progress evidence; scores and estimates unchanged. |+| 2026-10-20 | minor | Historical proposal (superseded 2026-10-23): Outline Stroke follow-up: preserve closed compound fill members and their individual editing while filtering open contours; scores and hours unchanged. |+| 2026-10-20 | minor | Historical proposal (superseded 2026-10-23): Outline Stroke consumes open-contour fills and preserves closed-contour fills, including mixed paths; scores and hours unchanged. |+ ## Headline  | Number | Value | Kind |@@ -1351,3 +1376,12 @@ applies to fills). Tests: `color::gradient::tests` (perceptual midpoint, expansi | 2026-10-22 | minor | Merge current main and complete the Turkish tab-drag labels and validation messages requested by the maintainer; all 24 catalogs covered; scores and estimates unchanged. | | 2026-10-22 | minor | Tab-drag follow-up: reconcile upstream progress updates and document drag lifecycle, insertion behavior and regression contracts; behavior, scores and estimates unchanged. | | 2026-10-22 | minor | Artwork dragged with Selection switches visible document tabs after a 400 ms hover, then copies editable artwork on canvas release; source and clipboard stay intact; scores and hours unchanged |+++### Outline Group Selection current-main follow-up — 2026-10-25++This private candidate retains original open and closed fills while replacing live strokes with+filled outlines. Group Selection defers ancestor promotion until a click is released, allowing+the selected fill or outlined stroke to drag independently. Current upstream changes are retained;+fresh combined-source checks and native evidence are required. No score, render-budget, platform+or publication acceptance is implied by a text merge or by historical review.diff --git a/docs/ui-parity.md b/docs/ui-parity.md--- a/docs/ui-parity.md+++ b/docs/ui-parity.md@@ -45,6 +45,27 @@ forbids launching or measuring Illustrator, so there has been **no side-by-side "behaves as the documentation describes", which is weaker than "a power user can't tell". Status: **done** (documented behaviour implemented and tested), **partial**, **missing**. +## Outline Stroke submission evidence++**Feature evidence last updated:** 2026-10-23.++The shared assessment status line and revision table retain the feature-base snapshot; merging retains upstream's current shared assessment and history. This dated section records the submission's feature evidence and does not rescore upstream or claim a new combined build or runtime measurement.++- Current-main conflict reconciliation retains upstream and feature behavior; scores and estimates unchanged+- Outline Stroke preserves open- and closed-contour fills as separate editable objects alongside filled stroke outlines; Group Selection distinguishes clicking up the group hierarchy from dragging a selected object. Validation of this correction is recorded separately from prior deletion behavior.++Public behavior references: [Outline Stroke](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/convert-strokes-to-compound-paths.html), [fills on open paths](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/about-fills-and-strokes.html), and [selecting objects within groups](https://helpx.adobe.com/ca/illustrator/desktop/manage-objects/select-objects/select-object-groups-and-nested-object-groups.html).++### Feature revisions++| Date | Change | Summary |+|---|---|---|+| 2026-10-23 | correction | Supersede the proposed open-fill deletion: preserve original fills and outline strokes separately. Cover independent Group Selection drags without promoting the selection on press; repeated clicks still select enclosing groups. No score or hours change; fresh review required. |+| 2026-10-21 | minor | Reconcile feature status and revision evidence in a dated section; retain shared baseline metadata for independent upstream updates. Scores and estimates are not reassessed. |+| 2026-10-21 | minor | Reconcile this feature with current main, preserving both sets of behavior and progress evidence; scores and estimates unchanged. |+| 2026-10-20 | minor | Historical proposal (superseded 2026-10-23): Outline Stroke follow-up: preserve closed compound fill members and their individual editing while filtering open contours; scores and hours unchanged. |+| 2026-10-20 | minor | Historical proposal (superseded 2026-10-23): Outline Stroke consumes open-contour fills and preserves closed-contour fills, including mixed paths; scores and hours unchanged. |+ ## Summary  | Area | % | Remaining (h) | Kind |@@ -260,6 +281,7 @@ empty), without opening the dialog. | Effect > Warp Options: style previews, orientation choices and percent controls | done | #1760; focused input, mixed-style and history checks passed; original captures reviewed. No geometry or optional Properties changes. | | Command search: Up and Down move through the results, Enter runs the highlighted one | done | #1798 (#1757) | | Pathfinder panel: a group selected alone counts its nested shapes, so an expanded stroked object keeps the Shape Modes enabled | done | #1642 (#1635) |+| Outline Stroke preserves original fills and replaces strokes with filled geometry for open and closed paths | partial | Fresh pointer-route regressions cover independent fill/outline drags and repeated-click group selection; native checks and DS review are required for this correction. Previously saved groups are not migrated; scores and estimates unchanged. | | Selected Mirror Repeat shows a prominent axis with move and rotation handles; bounding-box handles take priority over the axis line | done | #2877, `tools/src/mirror.rs`; [public behavior reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-mirror-repeat-options.html) | | Object › Expand on radial, grid and mirror repeats makes groups of their instances | done | #1765 (#1756) | | Select › Same matches type on its characters' fill and stroke | done | #1746 (#1737) |@@ -471,3 +493,12 @@ separate. Scores and hours unchanged. [Behavior and command details](development | 2026-10-22 | minor | Merge current main and complete the Turkish tab-drag labels and validation messages requested by the maintainer; all 24 catalogs covered; scores and estimates unchanged. | | 2026-10-22 | minor | Tab-drag follow-up: reconcile upstream progress updates and document drag lifecycle, insertion behavior and regression contracts; behavior, scores and estimates unchanged. | | 2026-10-22 | minor | Artwork dragged with Selection switches visible document tabs after a 400 ms hover, then copies editable artwork on canvas release; source and clipboard stay intact; scores and hours unchanged |+++### Outline Group Selection current-main follow-up — 2026-10-25++This private candidate retains original open and closed fills while replacing live strokes with+filled outlines. Group Selection defers ancestor promotion until a click is released, allowing+the selected fill or outlined stroke to drag independently. Current upstream changes are retained;+fresh combined-source checks and native evidence are required. No score, render-budget, platform+or publication acceptance is implied by a text merge or by historical review.