mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Fix #4820: the Layers panel lists a blend's spine
An open blend shows a <Spine> row below its key objects, where Release leaves the spine. A click selects the blend (Shift/Cmd toggles it), so Direct Selection edits the spine on the canvas; a locked blend's row selects nothing, and the row is left out while searching or filtering.
This commit is contained in:
@@ -397,7 +397,7 @@ fn listed_when_opened(doc: &Document, open: &OpenRows, opts: &PanelOptions, quer
|
||||
}
|
||||
let mut rows = vec![];
|
||||
push_rows(layer, 0, false, (Color32::PLACEHOLDER, true), (&open, opts, query, filter, doc), &mut rows);
|
||||
rows.iter().any(|r| r.node.id == id && r.guide.is_none())
|
||||
rows.iter().any(|r| r.node.id == id && r.guide.is_none() && !r.spine)
|
||||
}
|
||||
|
||||
/// The active document's uid and its last selected object, the one Locate Object locates.
|
||||
|
||||
@@ -63,6 +63,8 @@ pub(super) struct Row {
|
||||
pub(super) dim: bool,
|
||||
/// A ruler guide on the layer `node` (its index in the document's guides): the guide's row.
|
||||
pub(super) guide: Option<usize>,
|
||||
/// The spine of blend `node`: its row, below the blend's key objects (#4820).
|
||||
pub(super) spine: bool,
|
||||
}
|
||||
|
||||
/// An object kind under the Filter's By Object (`ui.layersFilter`'s `objects`).
|
||||
@@ -262,10 +264,20 @@ pub(super) fn push_rows(
|
||||
let opens = opens(n, opts) || !guides.is_empty();
|
||||
let open = opens && (narrowing || open_rows.contains(n.id));
|
||||
let at = out.len();
|
||||
out.push(Row { node: n.clone(), depth, clip_path, opens, open, colour, dim: !shown || n.is_template(), guide: None });
|
||||
out.push(Row { node: n.clone(), depth, clip_path, opens, open, colour, dim: !shown || n.is_template(), guide: None, spine: false });
|
||||
if open {
|
||||
for i in guides.into_iter().rev() {
|
||||
out.push(Row { node: n.clone(), depth: depth + 1, clip_path: false, opens: false, open: false, colour, dim: !shown, guide: Some(i) });
|
||||
out.push(Row {
|
||||
node: n.clone(),
|
||||
depth: depth + 1,
|
||||
clip_path: false,
|
||||
opens: false,
|
||||
open: false,
|
||||
colour,
|
||||
dim: !shown,
|
||||
guide: Some(i),
|
||||
spine: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
if open && let Some(children) = n.children() {
|
||||
@@ -273,6 +285,21 @@ pub(super) fn push_rows(
|
||||
push_rows(c, depth + 1, i == 0 && n.clips(), (colour, shown), cx, out);
|
||||
}
|
||||
}
|
||||
// A blend's spine is a row in it, below its key objects, as Release leaves it (not while
|
||||
// searching or filtering: it is no object of its own).
|
||||
if open && !narrowing && matches!(n.kind, NodeKind::Blend { .. }) {
|
||||
out.push(Row {
|
||||
node: n.clone(),
|
||||
depth: depth + 1,
|
||||
clip_path: false,
|
||||
opens: false,
|
||||
open: false,
|
||||
colour,
|
||||
dim: !shown,
|
||||
guide: None,
|
||||
spine: true,
|
||||
});
|
||||
}
|
||||
// Searching or filtering, a row stays for a match of its own (its name holds the search text
|
||||
// and the filter keeps it) or for a row it keeps inside it.
|
||||
if narrowing && out.len() == at + 1 && !(filter.keeps(n) && (!searching || n.display_name().to_lowercase().contains(query))) {
|
||||
|
||||
@@ -156,6 +156,47 @@ fn guide_row(ui: &mut Ui, view: &View, item: &Row, i: usize, out: &mut Out) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The row of the spine of blend `item` (#4820): a path mark and `<Path>`, the selection square
|
||||
/// while the blend is selected. A click selects the blend, whose spine Direct Selection then edits
|
||||
/// on the canvas; Shift or Cmd adds it to (or takes it from) the selection. The spine of a hidden
|
||||
/// or locked blend can't be selected.
|
||||
fn spine_row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
|
||||
let (t, h) = (&view.t, view.h);
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), h), Sense::click());
|
||||
let blend = item.node.id;
|
||||
let selected = view.sel.contains(&blend);
|
||||
let editable = (view.doc.editable())(blend);
|
||||
if editable && resp.hovered() {
|
||||
ui.painter().rect_filled(r, 0.0, t.hover.gamma_multiply(0.6));
|
||||
}
|
||||
ui.painter().line_segment([r.left_bottom(), r.right_bottom()], Stroke::new(1.0, t.input_border));
|
||||
let mut x = r.left() + 2.0 * COLUMN + 2.0;
|
||||
colour_bar(ui, &item.node, r, x, item.colour);
|
||||
x += 6.0 + item.depth as f32 * INDENT + 16.0;
|
||||
// The path mark: a short diagonal between two anchor dots.
|
||||
let (a, b) = (egui::pos2(x + 2.0, r.center().y + 5.0), egui::pos2(x + 14.0, r.center().y - 5.0));
|
||||
ui.painter().line_segment([a, b], Stroke::new(1.5, t.accent));
|
||||
for p in [a, b] {
|
||||
ui.painter().rect_filled(egui::Rect::from_center_size(p, vec2(3.0, 3.0)), 0.0, t.accent);
|
||||
}
|
||||
x += 22.0;
|
||||
let name = format!("<{}>", tl!("Path"));
|
||||
let color = if item.dim { t.text_dim } else { t.text };
|
||||
ui.painter().text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, name, egui::FontId::proportional(13.0), color);
|
||||
let col_x = r.right() - 43.5;
|
||||
ui.painter().line_segment([egui::pos2(col_x, r.top()), egui::pos2(col_x, r.bottom())], Stroke::new(1.0, t.input_border));
|
||||
if selected {
|
||||
let q = egui::Rect::from_center_size(egui::pos2(r.right() - SQUARE_X, r.center().y), vec2(7.0, 7.0));
|
||||
ui.painter().rect_filled(q, 0.0, item.colour);
|
||||
ui.painter().rect_stroke(q, 0.0, Stroke::new(1.0, t.text), StrokeKind::Inside);
|
||||
}
|
||||
if editable && resp.clicked() {
|
||||
let m = ui.input(|i| i.modifiers);
|
||||
let action = if m.shift || m.command { "select.toggle" } else { "select.set" };
|
||||
out.actions.push((action.into(), json!({"ids": [blend.0]})));
|
||||
}
|
||||
}
|
||||
|
||||
/// Guide rows `guides` dragged over row `r` (`resp`) of layer `layer`: the row lights up, and the
|
||||
/// release puts them on that layer (`guide.setLayer`, one undo step).
|
||||
fn guide_drop(ui: &Ui, view: &View, guides: &[usize], layer: NodeId, r: egui::Rect, resp: &egui::Response, out: &mut Out) {
|
||||
@@ -206,6 +247,9 @@ pub(super) fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
|
||||
if let Some(i) = item.guide {
|
||||
return guide_row(ui, view, item, i, out);
|
||||
}
|
||||
if item.spine {
|
||||
return spine_row(ui, view, item, out);
|
||||
}
|
||||
let n = &*item.node;
|
||||
let (depth, clip_path, has_children, open, color) = (item.depth, item.clip_path, item.opens, item.open, item.colour);
|
||||
let doc = view.doc;
|
||||
|
||||
@@ -1591,3 +1591,38 @@ fn a_canvas_selection_reveals_its_active_layer() {
|
||||
app.session.active_mut().unwrap().active_layer = doc.layer_of(NodeId(rects[0]));
|
||||
assert!(shown(&mut app, &top) && !shown(&mut app, &bottom), "the highlighted row keeps the view");
|
||||
}
|
||||
|
||||
/// #4820: an open blend lists its spine below its key objects, and a click on the spine's row
|
||||
/// selects the blend (whose spine Direct Selection edits); a locked blend's spine row selects
|
||||
/// nothing, and searching leaves the row out.
|
||||
#[test]
|
||||
fn a_blends_spine_is_a_row_below_its_keys_that_selects_the_blend() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
let run = |app: &mut VectorcraftApp, id: &str, p: serde_json::Value| app.session.execute(id, &p).unwrap();
|
||||
run(&mut app, "file.new", json!({"width": 300, "height": 200}));
|
||||
let a = run(&mut app, "shape.rectangle", json!({"x": 0, "y": 0, "width": 20, "height": 20}))["id"].as_u64().unwrap();
|
||||
let b = run(&mut app, "shape.rectangle", json!({"x": 200, "y": 0, "width": 20, "height": 20}))["id"].as_u64().unwrap();
|
||||
run(&mut app, "select.set", json!({"ids": [a, b]}));
|
||||
let blend = NodeId(run(&mut app, "object.blend.make", json!({}))["id"].as_u64().unwrap());
|
||||
run(&mut app, "select.none", json!({}));
|
||||
expand(&mut app, &json!({})).unwrap();
|
||||
let st = app.session.active().unwrap();
|
||||
let rows = list_rows(&st.doc, &st.layers_open, &PanelOptions::default(), "", &Filter::default());
|
||||
let kinds: Vec<(u64, bool)> = rows.iter().map(|r| (r.node.id.0, r.spine)).collect();
|
||||
assert_eq!(kinds[1..], [(blend.0, false), (b, false), (a, false), (blend.0, true)], "the spine row comes last in the blend");
|
||||
let rows = list_rows(&st.doc, &st.layers_open, &PanelOptions::default(), "rect", &Filter::default());
|
||||
assert!(!rows.iter().any(|r| r.spine), "no spine row while searching");
|
||||
// Rows top down: the layer, the blend, both keys (with target circles), then the spine.
|
||||
let ctx = egui::Context::default();
|
||||
let (circles, _) = frame(&mut app, &ctx, vec![], false);
|
||||
assert_eq!(circles.len(), 4);
|
||||
let spine_row = egui::pos2(150.0, circles[3].y + (circles[3].y - circles[2].y));
|
||||
let selected = |app: &VectorcraftApp| app.session.active().unwrap().selection.objects.to_vec();
|
||||
click_with(&mut app, &ctx, spine_row, egui::Modifiers::NONE);
|
||||
assert_eq!(selected(&app), [blend], "its row selects the blend");
|
||||
click_with(&mut app, &ctx, spine_row, egui::Modifiers::SHIFT);
|
||||
assert!(selected(&app).is_empty(), "Shift-click takes it out again");
|
||||
run(&mut app, "layer.setProps", json!({"ids": [blend.0], "locked": true}));
|
||||
click_with(&mut app, &ctx, spine_row, egui::Modifiers::NONE);
|
||||
assert!(selected(&app).is_empty(), "a locked blend's spine can't be selected");
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Pen: Cmd/Ctrl drags with the lent selection tool, not hidden box handles, #4980) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Layers panel: a blend's spine row, #4820) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -90,6 +90,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Busy cursor during long operations | done | Background open, save and export show the system progress cursor (gap G1) |
|
||||
| Each anchor snaps to Smart Guides and construction angles | done | `DrawSnap` |
|
||||
| Curvature tool: click/drag points, double-click or Alt-click for a corner, temporary Direct Selection, point conversion | partial | circular point markers without direction handles or duplicate markers, Anchor Size and Hide Edges regression-tested (#1236, in part); curve geometry: each stretch of three points follows the circle through them (handles along its tangent, chord/(3·cos²(α/2)) long, shorter in a turn tighter than 30°), open ends curving in, the same rule for the rubber band, dragged, inserted and converted points (#1236, in part); neighbours of a point dragged on an existing path keep their tangents, and modifiers requested in #975 remain |
|
||||
| Layers panel lists a blend's spine (#4820) | done | an open blend shows a `<Path>` row below its key objects, where Release leaves the spine; a click selects the blend (Shift/Cmd toggles it) so Direct Selection edits the spine; a locked blend's row selects nothing; no row while searching or filtering (`a_blends_spine_is_a_row_below_its_keys_that_selects_the_blend`) |
|
||||
| Blend spine Anchor Point conversion and handle editing (#2544) | implemented | symmetric handles, independent drag/retraction, visible-handle policy and selection/stacking covered by tool and engine regressions; Windows native canvas checks and screenshots reviewed in PR #2643 |
|
||||
| Add/Delete/Anchor Point tools; Anchor Point tool drags segments to reshape | done | #2267: a click on a handle end retracts that handle and leaves the anchor a corner with the other one; Shift held at the press on an anchor moves it, handles and all (Shift kept down: at 45° steps) instead of pulling out handles |
|
||||
| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | done | Fidelity goes down to 0.01 pt in the dialog, the tool and `path.freehand` (the Smooth tool keeps 0.05); its slider runs across the Tool Options row on a logarithmic rail from 0.01 to 20 pt, the field shows hundredths, and the slider's and the field's arrow keys step hundredths up to 0.1 pt, tenths up to 1 pt and whole points above (Shift ×10) (#3328); Alt key toggles to Smooth Tool (#2001); Shift held at the press draws a straight line at 45° steps from the Constrain Angle, and Alt held at the press (with Alt key toggles to Smooth Tool off) a straight line at any angle (#3799), with a straight-line cursor while Shift or Alt is held before the press, and a path-continuation cursor over an end of a selected open path, where the line continues the path; Edit Selected Paths continues a selected path from an end and, from a stroke that starts and ends on a selected path, redraws the section between (the shorter way round a closed path, #2850); with Keep selected (Pencil and Paintbrush, on by default) off, the path a stroke draws or continues is not selected afterwards; a filled Pencil path keeps a stroke of None (#1804, part of #1679); while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start, the Pencil and the Paintbrush show the closing cursor, and a release there closes the path |
|
||||
@@ -387,6 +388,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-01 | minor | Layers panel: an open blend lists its spine as a `<Path>` row below its key objects, and a click selects the blend so Direct Selection edits the spine (#4820); scores and estimates unchanged |
|
||||
| 2026-11-01 | minor | Pen with Cmd/Ctrl held: the lent Selection tool no longer scales the path being drawn by bounding-box handles the canvas hides while its end anchor is selected (a flat path did nothing under a resize pointer); a Cmd drag moves the path instead, and with Direct Selection moves anchors and segments, one undo step each (#4980); scores and estimates unchanged |
|
||||
| 2026-11-01 | minor | Dynamic symbols (#4938): Symbol Options › Symbol Type; per-instance appearance for objects of a dynamic symbol's art; Reset in the Properties panel and the Symbols panel menu; on-canvas Direct Selection of the parts remains |
|
||||
| 2026-11-01 | minor | Export for Screens has Include Artboard Fill beside Include Bleed (on by default, remembered; `includeArtboardFill`): the files draw each artboard's background color behind its art, as exports and print now do, and the Print dialog's preview shows it (#4937); scores and estimates unchanged |
|
||||
|
||||
Reference in New Issue
Block a user