Direct Selection: select anchor points with a marquee across paths and move them together

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
moppeeer
2026-10-20 22:32:46 +02:00
co-authored by Claude Opus 5.5
parent 8dff7ecdb0
commit 061c4faaf0
7 changed files with 307 additions and 41 deletions
+1
View File
@@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **2026-10-20** · Direct Selection selects anchor points: a marquee selects the points inside it on every path it reaches, a click selects one point and Shift+click adds or removes one, selected points show solid, and dragging one (Shift: at 45° steps) or pressing the arrow keys moves them all in one step (`selection.set {anchors}`, `path.moveAnchors {points}`). No readiness percentage change.
- **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change.
- **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change.
- **2026-10-17** · File › Package writes the document's and the IDML's links relative to the package (`Links/logo.png`, `file:Links/logo.png`), so a package holds no path from the machine that made it; opening a document resolves them to the files beside it. Links › Copy Link(s) To still links by full path. (#340). No readiness percentage change.
+29 -3
View File
@@ -8,6 +8,21 @@ use serde_json::{Value, json};
use super::{CommandSpec, bool_or, cmd, has_doc, has_selection, ids_param, ok};
use crate::{HistoryEntry, Result, Session};
/// `anchors` of `selection.set`: the points that exist, once each.
fn anchors_param(doc: &designcraft_doc::Document, p: &Value) -> Vec<(ItemId, usize, usize)> {
let mut out: Vec<(ItemId, usize, usize)> = Vec::new();
for v in p.get("anchors").and_then(Value::as_array).into_iter().flatten() {
let n = |i: usize| v.get(i).and_then(Value::as_u64);
let (Some(id), Some(si), Some(ai)) = (n(0), n(1), n(2)) else { continue };
let (Ok(si), Ok(ai)) = (usize::try_from(si), usize::try_from(ai)) else { continue };
let a = (ItemId(id), si, ai);
if doc.item(a.0).is_some_and(|it| it.path.subpaths.get(si).is_some_and(|sp| ai < sp.anchors.len())) && !out.contains(&a) {
out.push(a);
}
}
out
}
fn can_undo(s: &Session) -> std::result::Result<(), String> {
s.active().filter(|d| !d.history.undo.is_empty()).map(|_| ()).ok_or_else(|| "nothing to undo".into())
}
@@ -193,11 +208,17 @@ pub fn specs() -> Vec<CommandSpec> {
s.typing_format = None;
Ok(json!({"redone": e.label}))
}),
cmd!(noundo "selection.set", "Select", [], None, "{ids: [id], add?: bool, content?: bool}", has_doc, |s, p| {
let ids = ids_param(p, "ids").unwrap_or_default();
cmd!(noundo "selection.set", "Select", [], None, "{ids: [id], add?: bool, content?: bool, anchors?: [[id, subpath, index]] (Direct Selection: the selected anchor points; their paths are selected too)}", has_doc, |s, p| {
let mut ids = ids_param(p, "ids").unwrap_or_default();
let add = bool_or(p, "add", false);
let content = bool_or(p, "content", false);
let st = s.doc_mut()?;
let anchors = anchors_param(&st.doc, p);
for (id, _, _) in &anchors {
if !ids.contains(id) {
ids.push(*id);
}
}
let ids: Vec<ItemId> = ids.into_iter().filter(|i| st.doc.item(*i).is_some()).collect();
if add {
for i in ids {
@@ -205,8 +226,13 @@ pub fn specs() -> Vec<CommandSpec> {
st.selection.items.push(i);
}
}
for a in anchors {
if !st.selection.anchors.contains(&a) {
st.selection.anchors.push(a);
}
}
} else {
st.selection = Selection { items: ids, content, ..Default::default() };
st.selection = Selection { items: ids, content, anchors, ..Default::default() };
}
st.selection.text = None;
st.selection.cells = None;
+58 -29
View File
@@ -680,7 +680,15 @@ pub fn specs() -> Vec<CommandSpec> {
path_reverse
),
cmd!("path.close", "Close Path", ["Object", "Paths"], None, "{id, subpath?: index (default every subpath)}", has_doc, path_close),
cmd!("path.moveAnchors", "Move Anchors", [], None, "{id, anchors: [[subpath, index]], dx, dy, handle?: in|out}", has_doc, path_move_anchors),
cmd!(
"path.moveAnchors",
"Move Anchors",
[],
None,
"{id, anchors: [[subpath, index]], dx, dy, handle?: in|out} | {points: [[id, subpath, index]], dx, dy} (anchors of several paths, as Direct Selection drags them: one step)",
has_doc,
path_move_anchors
),
cmd!(
"object.align",
"Align",
@@ -3338,38 +3346,59 @@ fn path_close(s: &mut Session, p: &Value) -> Result<Value> {
}
fn path_move_anchors(s: &mut Session, p: &Value) -> Result<Value> {
let id = super::id_param(p, "id").ok_or_else(|| bad("path.moveAnchors", "missing id"))?;
let which: Vec<(usize, usize)> = p.get("anchors").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
// The anchors to move, by path: `points` (several paths) or `id` with `anchors`.
let mut targets: Vec<(ItemId, Vec<(usize, usize)>)> = Vec::new();
if let Some(points) = p.get("points").and_then(Value::as_array) {
for v in points {
let n = |i: usize| v.get(i).and_then(Value::as_u64).and_then(|x| usize::try_from(x).ok());
let (Some(id), Some(si), Some(ai)) = (v.get(0).and_then(Value::as_u64), n(1), n(2)) else {
return Err(bad("path.moveAnchors", "each point is [id, subpath, index]"));
};
match targets.iter_mut().find(|(i, _)| i.0 == id) {
Some((_, which)) => which.push((si, ai)),
None => targets.push((ItemId(id), vec![(si, ai)])),
}
}
} else {
let id = super::id_param(p, "id").ok_or_else(|| bad("path.moveAnchors", "missing id"))?;
let which: Vec<(usize, usize)> = p.get("anchors").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
targets.push((id, which));
}
let d0 = Vec2::new(f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0));
let handle = str_param(p, "handle").map(str::to_string);
s.edit(|d, _| {
path_editable(d, id, "path.moveAnchors")?;
let loc = d.find(id).ok_or(designcraft_doc::DocError::NoItem(id))?;
let m = d.parent_xf(&loc) * d.item_at(&loc).map(|i| i.xf).unwrap_or_default();
// Spread-space delta → inner-space delta (linear part only).
let inv = m.inverse();
let dv = (inv * Point::new(d0.x, d0.y)) - (inv * Point::ZERO);
let it = d.item_mut(id).ok_or(designcraft_doc::DocError::NoItem(id))?;
for (si, ai) in &which {
let Some(a) = it.path.anchor_mut(*si, *ai) else { continue };
match handle.as_deref() {
Some("in") => {
a.h_in += dv;
if a.kind == designcraft_geom::AnchorKind::Smooth {
a.h_out = a.p - (a.h_in - a.p);
}
}
Some("out") => {
a.h_out += dv;
if a.kind == designcraft_geom::AnchorKind::Smooth {
a.h_in = a.p - (a.h_out - a.p);
}
}
_ => a.translate(dv),
}
for (id, _) in &targets {
path_editable(d, *id, "path.moveAnchors")?;
}
if it.shape == Shape::Rectangle || it.shape == Shape::Oval || it.shape == Shape::Polygon {
it.shape = Shape::Path;
for (id, which) in &targets {
let id = *id;
let loc = d.find(id).ok_or(designcraft_doc::DocError::NoItem(id))?;
let m = d.parent_xf(&loc) * d.item_at(&loc).map(|i| i.xf).unwrap_or_default();
// Spread-space delta → inner-space delta (linear part only).
let inv = m.inverse();
let dv = (inv * Point::new(d0.x, d0.y)) - (inv * Point::ZERO);
let it = d.item_mut(id).ok_or(designcraft_doc::DocError::NoItem(id))?;
for (si, ai) in which {
let Some(a) = it.path.anchor_mut(*si, *ai) else { continue };
match handle.as_deref() {
Some("in") => {
a.h_in += dv;
if a.kind == designcraft_geom::AnchorKind::Smooth {
a.h_out = a.p - (a.h_in - a.p);
}
}
Some("out") => {
a.h_out += dv;
if a.kind == designcraft_geom::AnchorKind::Smooth {
a.h_in = a.p - (a.h_out - a.p);
}
}
_ => a.translate(dv),
}
}
if it.shape == Shape::Rectangle || it.shape == Shape::Oval || it.shape == Shape::Polygon {
it.shape = Shape::Path;
}
}
ok()
})
+35
View File
@@ -1650,3 +1650,38 @@ fn a_line_fits_when_its_baseline_does() {
assert!(!overset(&mut s, 78.5), "the line is set; its descenders hang below the frame");
assert!(overset(&mut s, 50.0), "a baseline below the frame still oversets the line");
}
/// Direct Selection: a marquee over the top edges of two frames selects their top points, and a
/// drag on one of them slants both frames in one undo step (InDesign's way to make a row of
/// parallelograms).
#[test]
fn direct_selection_moves_the_points_of_several_frames_at_once() {
use designcraft_tools::{Mods, PointerEvent, PointerKind};
let mut s = session();
let v = ViewInfo::at_zoom(1.0);
let a = s.execute("frame.create", &json!({"rect": [100, 300, 200, 400]})).unwrap()["id"].as_u64().unwrap();
let b = s.execute("frame.create", &json!({"rect": [200, 300, 300, 400]})).unwrap()["id"].as_u64().unwrap();
s.execute("selection.set", &json!({"ids": []})).unwrap();
s.set_tool("directSelection");
s.pointer(&PointerEvent::new(PointerKind::Down, 80.0, 280.0), v).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Drag, 320.0, 320.0), v).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Up, 320.0, 320.0), v).unwrap();
let sel = s.doc().unwrap().selection.clone();
assert_eq!(sel.anchors.len(), 4, "{:?}", sel.anchors);
assert!(sel.items.contains(&designcraft_doc::ItemId(a)) && sel.items.contains(&designcraft_doc::ItemId(b)));
let shift = Mods { shift: true, ..Default::default() };
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 300.0), v).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Drag, 136.0, 302.0).with_mods(shift), v).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Up, 136.0, 302.0).with_mods(shift), v).unwrap();
let corners = |s: &Session, id: u64| -> Vec<(f64, f64)> {
let st = s.doc().unwrap();
let it = st.doc.item(designcraft_doc::ItemId(id)).unwrap();
it.path.subpaths[0].anchors.iter().map(|an| ((it.xf * an.p).x.round(), (it.xf * an.p).y.round())).collect()
};
// Both frames: top points 36 pt to the right, bottom points where they were.
assert_eq!(corners(&s, a), vec![(136.0, 300.0), (236.0, 300.0), (200.0, 400.0), (100.0, 400.0)]);
assert_eq!(corners(&s, b), vec![(236.0, 300.0), (336.0, 300.0), (300.0, 400.0), (200.0, 400.0)]);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(corners(&s, a), vec![(100.0, 300.0), (200.0, 300.0), (200.0, 400.0), (100.0, 400.0)]);
assert_eq!(corners(&s, b), vec![(200.0, 300.0), (300.0, 300.0), (300.0, 400.0), (200.0, 400.0)]);
}
+123 -5
View File
@@ -90,6 +90,8 @@ enum Drag {
/// None when that point could not be read. The live document during a drag is the
/// previous preview, so the snap must not read the anchor from it again.
at: Option<(SpreadRef, Point)>,
/// The other selected anchor points, which move with it: (item, subpath, anchor).
others: Vec<(u64, usize, usize)>,
},
}
@@ -179,6 +181,60 @@ fn spread_delta_to_canvas(xf: Affine, d: Vec2) -> Vec2 {
(xf * Point::new(d.x, d.y)) - (xf * Point::ORIGIN)
}
/// The anchor points inside `r` (canvas) of the unlocked, visible paths, groups' members included.
fn anchors_in(cx: &ToolContext, r: Rect) -> Vec<(ItemId, usize, usize)> {
fn walk(cx: &ToolContext, items: &[std::sync::Arc<Item>], r: Rect, out: &mut Vec<(ItemId, usize, usize)>) {
for it in items {
let locked = it.locked || cx.doc.layer(it.layer).is_some_and(|l| l.locked || !l.visible);
if locked || it.hidden {
continue;
}
if let Content::Group { items } = &it.content {
walk(cx, items, r, out);
continue;
}
let Some(xf) = cx.item_canvas_xf(it.id) else { continue };
let m = xf * it.xf;
for (si, ai, a) in it.path.anchors() {
let q = m * a.p;
if q.x >= r.x0 && q.x <= r.x1 && q.y >= r.y0 && q.y <= r.y1 {
out.push((it.id, si, ai));
}
}
}
}
let mut out = Vec::new();
for slot in &cx.layout.slots {
if let Some(sp) = cx.doc.spread(slot.spread) {
walk(cx, &sp.items, r, &mut out);
}
}
out
}
/// `selection.set` params selecting `points`: their paths, and those points.
fn anchor_selection(cx: &ToolContext, points: &[(ItemId, usize, usize)]) -> Value {
let mut ids: Vec<u64> = cx.selection.items.iter().map(|i| i.0).collect();
for (i, _, _) in points {
if !ids.contains(&i.0) {
ids.push(i.0);
}
}
let anchors: Vec<Value> = points.iter().map(|(i, s, a)| json!([i.0, s, a])).collect();
json!({"ids": ids, "anchors": anchors})
}
/// `d` held to the nearest multiple of 45°, as Shift does.
fn constrain_45(d: Vec2) -> Vec2 {
if d.hypot() < 1e-9 {
return d;
}
let step = std::f64::consts::FRAC_PI_4;
let a = (d.y.atan2(d.x) / step).round() * step;
let proj = d.x * a.cos() + d.y * a.sin();
Vec2::new(proj * a.cos(), proj * a.sin())
}
pub struct SelectionTool {
direct: bool,
drag: Drag,
@@ -709,8 +765,40 @@ impl Tool for SelectionTool {
&& let Some((id, si, ai, handle)) = anchor_at(cx, p)
{
let at = anchor_spread_point(cx, id, si, ai);
self.drag = Drag::Anchor { id, si, ai, handle, start: p, at };
return vec![Action::Begin(if handle.is_some() { "Move Direction Handle".into() } else { "Move Anchor".into() })];
let mut out = vec![];
let mut others = vec![];
if handle.is_none() {
// InDesign's anchor point selection: a click selects the point, Shift+click
// adds it or takes it away, and dragging a selected point moves all of them.
let me = (ItemId(id), si, ai);
let mut points = cx.selection.anchors.clone();
let was = points.contains(&me);
if ev.mods.shift && was {
points.retain(|a| *a != me);
} else if ev.mods.shift {
points.push(me);
} else if !was {
points = vec![me];
}
if points != cx.selection.anchors {
out.push(Action::Exec("selection.set".into(), anchor_selection(cx, &points)));
}
if ev.mods.shift && was {
self.drag = Drag::None;
return out;
}
others = points.iter().filter(|a| **a != me).map(|(i, s, a)| (i.0, *s, *a)).collect();
}
let label = if handle.is_some() {
"Move Direction Handle"
} else if others.is_empty() {
"Move Anchor"
} else {
"Move Anchors"
};
self.drag = Drag::Anchor { id, si, ai, handle, start: p, at, others };
out.push(Action::Begin(label.into()));
return out;
}
// A selected placed graphic's own handles resize it inside its frame (the
// frame's anchors, under Direct Selection, come first).
@@ -1035,8 +1123,12 @@ impl Tool for SelectionTool {
// Screen y points down: a positive screen angle is clockwise.
vec![Action::Preview("transform.rotate".into(), json!({"angle": -a}))]
}
Drag::Anchor { id, si, ai, handle, start, at } => {
let d = p - start;
Drag::Anchor { id, si, ai, handle, start, at, others } => {
let mut d = p - start;
// Shift: horizontal, vertical or at 45°.
if ev.mods.shift {
d = constrain_45(d);
}
let mut dx = d.x;
let mut dy = d.y;
self.guides.clear();
@@ -1075,6 +1167,9 @@ impl Tool for SelectionTool {
let mut params = json!({"id": id, "anchors": [[si, ai]], "dx": dx, "dy": dy});
if let Some(h) = handle {
params["handle"] = json!(h);
} else if !others.is_empty() {
let points: Vec<Value> = std::iter::once((id, si, ai)).chain(others).map(|(i, s, a)| json!([i, s, a])).collect();
params = json!({"points": points, "dx": dx, "dy": dy});
}
vec![Action::Preview("path.moveAnchors".into(), params)]
}
@@ -1100,6 +1195,21 @@ impl Tool for SelectionTool {
},
Drag::Marquee { start, cur } => {
let r = Rect::from_points(start, cur);
// Direct Selection: the anchor points inside the marquee, on every path it
// reaches (InDesign's way to select points of several frames at once).
if self.direct {
let mut points = anchors_in(cx, r);
if !points.is_empty() {
if ev.mods.shift {
for a in &cx.selection.anchors {
if !points.contains(a) {
points.push(*a);
}
}
}
return vec![Action::Exec("selection.set".into(), anchor_selection(cx, &points))];
}
}
// Items whose bounds intersect the marquee.
let mut ids: Vec<Value> = Vec::new();
for slot in &cx.layout.slots {
@@ -1131,7 +1241,15 @@ impl Tool for SelectionTool {
if key == ToolKey::Escape && self.live.take().is_some_and(|id| cx.selection.items == [id]) {
return vec![Action::Handled];
}
let mv = |dx: f64, dy: f64| vec![Action::Exec("transform.move".into(), json!({"dx": dx, "dy": dy, "copy": mods.alt}))];
let points: Vec<Value> = cx.selection.anchors.iter().map(|(i, s, a)| json!([i.0, s, a])).collect();
let direct_points = self.direct && !points.is_empty();
let mv = |dx: f64, dy: f64| {
// Direct Selection with anchor points selected: the arrow keys move the points.
if direct_points {
return vec![Action::Exec("path.moveAnchors".into(), json!({"points": points.clone(), "dx": dx, "dy": dy}))];
}
vec![Action::Exec("transform.move".into(), json!({"dx": dx, "dy": dy, "copy": mods.alt}))]
};
if cx.selection.items.is_empty() {
return vec![];
}
+56 -1
View File
@@ -1792,7 +1792,13 @@ fn direct_selection_drags_the_end_anchors_of_type_on_a_path() {
let p = Point::new(x + off.x, 300.0 + off.y);
let mut direct = create("directSelection");
assert_ne!(direct.cursor(&cx, p, Mods::default()), Cursor::PathBracket, "anchor at {x}");
assert_eq!(direct.pointer(&cx, &PointerEvent::new(PointerKind::Down, p.x, p.y)), vec![Action::Begin("Move Anchor".into())]);
// The press selects that anchor point and starts moving it.
let a = direct.pointer(&cx, &PointerEvent::new(PointerKind::Down, p.x, p.y));
assert!(
matches!(a.first(), Some(Action::Exec(cmd, p)) if cmd == "selection.set" && p["anchors"].as_array().map(Vec::len) == Some(1)),
"{a:?}"
);
assert_eq!(a.last(), Some(&Action::Begin("Move Anchor".into())));
}
}
@@ -1936,3 +1942,52 @@ fn loaded_text_cursor_threads_into_a_frame_inside_a_group() {
assert_eq!(cx2.hit_text_frame(q).map(|(_, i)| i), None, "no text frame in that group");
assert_eq!(t2.cursor(&cx2, q, Mods::default()), Cursor::NotAllowed);
}
/// Direct Selection selects anchor points with a marquee across several frames, and dragging one of
/// them moves them all, as InDesign does when the top points of a row of frames are dragged to slant
/// them (Zero2Graphic's page layout tutorial, 1:15).
#[test]
fn direct_selection_marquee_selects_points_of_several_frames_and_drags_them_together() {
let mut d = Document::new(&NewDocument::default());
let a = add_rect(&mut d, Rect::new(100.0, 100.0, 200.0, 200.0));
let b = add_rect(&mut d, Rect::new(200.0, 100.0, 300.0, 200.0));
let (c, l) = (Cache::new(), CanvasLayout::new(&d, false));
let none = Selection::default();
let mut t = create("directSelection");
// A marquee around the top edges: the four top points, not the frames' bottom ones.
let (p0, p1) = (spread_pt(&l, 80.0, 90.0), spread_pt(&l, 320.0, 110.0));
t.pointer(&ctx(&d, &none, &c, &l), &PointerEvent::new(PointerKind::Down, p0.x, p0.y));
t.pointer(&ctx(&d, &none, &c, &l), &PointerEvent::new(PointerKind::Drag, p1.x, p1.y));
let up = t.pointer(&ctx(&d, &none, &c, &l), &PointerEvent::new(PointerKind::Up, p1.x, p1.y));
let Some(Action::Exec(cmd, p)) = up.last() else { panic!("{up:?}") };
assert_eq!(cmd, "selection.set");
assert_eq!(p["ids"], serde_json::json!([a.0, b.0]));
let mut got: Vec<(u64, u64, u64)> =
p["anchors"].as_array().unwrap().iter().map(|v| (v[0].as_u64().unwrap(), v[1].as_u64().unwrap(), v[2].as_u64().unwrap())).collect();
got.sort();
assert_eq!(got, vec![(a.0, 0, 0), (a.0, 0, 1), (b.0, 0, 0), (b.0, 0, 1)]);
// Those points selected: dragging one moves all four, in one preview.
let sel = Selection { items: vec![a, b], anchors: vec![(a, 0, 0), (a, 0, 1), (b, 0, 0), (b, 0, 1)], ..Default::default() };
let cx = ctx(&d, &sel, &c, &l);
let at = spread_pt(&l, 200.0, 100.0);
let down = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, at.x, at.y));
assert_eq!(down, vec![Action::Begin("Move Anchors".into())]);
let shift = Mods { shift: true, ..Default::default() };
let drag = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, at.x + 36.0, at.y + 3.0).with_mods(shift));
let Some(Action::Preview(cmd, p)) = drag.last() else { panic!("{drag:?}") };
assert_eq!(cmd, "path.moveAnchors");
assert_eq!(p["points"].as_array().map(Vec::len), Some(4), "{p}");
// Shift keeps the move horizontal.
assert_eq!((p["dx"].as_f64(), p["dy"].as_f64()), (Some(36.0), Some(0.0)));
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, at.x + 36.0, at.y)), vec![Action::Commit]);
// Shift+click on a selected point takes it out of the selection; the arrow keys move the points.
let out = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, at.x, at.y).with_mods(shift));
let Some(Action::Exec(cmd, p)) = out.first() else { panic!("{out:?}") };
assert_eq!(cmd, "selection.set");
assert_eq!(p["anchors"].as_array().map(Vec::len), Some(3), "{p}");
let keys = t.key(&cx, ToolKey::Right, Mods::default());
assert!(
matches!(keys.as_slice(), [Action::Exec(cmd, p)] if cmd == "path.moveAnchors" && p["points"].as_array().map(Vec::len) == Some(4)),
"{keys:?}"
);
}
+5 -3
View File
@@ -1229,9 +1229,11 @@ fn draw_selection(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Docum
let (Some(it), Some((a, _))) = (doc.item(*id), item_canvas_xf(doc, layout, *id)) else { continue };
let m = a * it.xf;
let col = layer_color(doc, it);
for sp in &it.path.subpaths {
for an in &sp.anchors {
for (si, sp) in it.path.subpaths.iter().enumerate() {
for (ai, an) in sp.anchors.iter().enumerate() {
let p = xf.to_screen(m * an.p);
// A selected anchor point is solid, the others hollow (InDesign).
let chosen = sel.anchors.contains(&(*id, si, ai));
for (h, has) in [(an.h_in, an.has_in()), (an.h_out, an.has_out())] {
if has {
let hp = xf.to_screen(m * h);
@@ -1240,7 +1242,7 @@ fn draw_selection(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Docum
}
}
let r = Rect::from_center_size(p, vec2(5.0, 5.0));
painter.rect_filled(r, 0.0, Color32::WHITE);
painter.rect_filled(r, 0.0, if chosen { col } else { Color32::WHITE });
painter.rect_stroke(r, 0.0, Stroke::new(1.0, col), StrokeKind::Inside);
}
}