fix(selection): 線分の選択と削除をアンカーから分ける (#1278)

This commit is contained in:
Ikki Nishihara
2026-10-12 07:25:36 +07:00
committed by bflatastic
parent aa15cb52bd
commit 1ac797685f
25 changed files with 384 additions and 54 deletions
+1
View File
@@ -781,6 +781,7 @@ fn remove_anchors_of(d: &mut Document, sel: &mut Selection, id: NodeId, anchors:
sel.remove(id);
} else {
sel.anchors.remove(&id);
sel.segments.remove(&id);
}
Ok(gone)
}
+1 -1
View File
@@ -300,7 +300,7 @@ fn cut(s: &mut Session, p: &Value) -> Result<Value> {
fn clear(s: &mut Session, p: &Value) -> Result<Value> {
let explicit = if p.get("ids").is_some() { Some(checked_ids_param(s, p, "ids", "edit.clear")?) } else { None };
// Direct-selected anchors: delete those anchors instead of whole objects.
// Direct-selected anchors or segments: remove those components instead of whole objects.
if explicit.is_none() && !s.doc()?.selection.anchors.is_empty() {
return super::path::delete_anchors(s, p);
}
+64 -12
View File
@@ -456,21 +456,35 @@ fn convert_anchors(s: &mut Session, p: &Value) -> Result<Value> {
pub(crate) fn delete_anchors(s: &mut Session, _: &Value) -> Result<Value> {
let st = s.doc()?;
let sel = st.selection.anchors.clone();
let segments = st.selection.segments.clone();
s.edit("Clear", |d, selection| {
let mut survivors = vec![];
for (id, set) in &sel {
let remove_node = {
let path = path_mut(d, *id)?;
// Deleting an anchor from a closed path opens it there (Illustrator behaviour on Delete).
for &(si, ai) in set.iter().rev() {
if let Some(sp) = path.subpaths.get_mut(si)
&& ai < sp.anchors.len()
{
if sp.closed {
sp.anchors.rotate_left(ai + 1);
sp.anchors.pop();
sp.closed = false;
} else {
sp.anchors.remove(ai);
if let Some(refs) = segments.get(id) {
// A segment pick cuts its connection, keeping every neighboring cubic.
path.subpaths = std::mem::take(&mut path.subpaths)
.into_iter()
.enumerate()
.flat_map(|(si, sp)| {
let cuts = refs.iter().filter_map(|&(s, seg)| (s == si).then_some(seg)).collect();
without_segments(sp, &cuts)
})
.collect();
} else {
// Deleting an anchor from a closed path opens it there (Illustrator behaviour on Delete).
for &(si, ai) in set.iter().rev() {
if let Some(sp) = path.subpaths.get_mut(si)
&& ai < sp.anchors.len()
{
if sp.closed {
sp.anchors.rotate_left(ai + 1);
sp.anchors.pop();
sp.closed = false;
} else {
sp.anchors.remove(ai);
}
}
}
}
@@ -479,14 +493,52 @@ pub(crate) fn delete_anchors(s: &mut Session, _: &Value) -> Result<Value> {
};
if remove_node {
d.remove(*id)?;
} else if segments.contains_key(id) {
survivors.push(*id);
}
}
selection.clear();
// Another Delete removes the remaining path after a segment cut. Anchor Delete keeps
// its existing deselection behavior.
selection.set(survivors);
Ok(())
})?;
ok()
}
/// Remove connections, splitting open/closed contours into their surviving open runs.
fn without_segments(sp: SubPath, removed: &BTreeSet<usize>) -> Vec<SubPath> {
let cuts: BTreeSet<usize> = removed.iter().copied().filter(|&i| i < sp.segment_count()).collect();
let Some(&first) = cuts.first() else { return vec![sp] };
let n = sp.anchors.len();
let start = if sp.closed { (first + 1) % n } else { 0 };
let mut result = vec![];
let mut run = vec![];
let mut previous = None;
for (index, anchor) in sp.anchors.iter().enumerate().cycle().skip(start).take(n) {
if previous.is_some_and(|i| cuts.contains(&i)) {
result.extend(open_run(std::mem::take(&mut run)));
}
run.push(*anchor);
previous = Some(index);
}
result.extend(open_run(run));
result
}
fn open_run(mut anchors: Vec<vectorcraft_geom::Anchor>) -> Option<SubPath> {
if anchors.len() < 2 {
return None;
}
// The cut edge's incoming/outgoing controls are no longer part of an open endpoint.
if let Some(first) = anchors.first_mut() {
first.h_in = first.p;
}
if let Some(last) = anchors.last_mut() {
last.h_out = last.p;
}
Some(SubPath::new(anchors, false))
}
fn reshape(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("path.reshape", "missing id"))?;
let at = Point::new(f64_req(p, "x", "path.reshape")?, f64_req(p, "y", "path.reshape")?);
+32
View File
@@ -70,6 +70,15 @@ pub fn specs() -> Vec<CommandSpec> {
key
),
cmd!("select.anchors", "Select Anchors", [], None, "{id, anchors: [[subpath, anchor]…], mode: \"set\"|\"add\"|\"toggle\"}", has_doc, anchors),
cmd!(
"select.segments",
"Select Segments",
[],
None,
"{id, segments: [[subpath, segment]…]} select path segments; their endpoints move with them, Delete removes only their connections",
has_doc,
segments
),
cmd!(
"select.anchorsMany",
"Select Anchors",
@@ -330,6 +339,27 @@ pub(crate) fn checked_refs(v: Option<&Value>, cmd: &str) -> Result<Vec<(usize, u
.collect()
}
fn segments(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "select.segments";
let id = checked_id(s, p.get("id").ok_or_else(|| bad(C, "missing id"))?, C)?;
let refs: BTreeSet<_> = checked_refs(p.get("segments"), C)?.into_iter().collect();
let path = s.doc()?.doc.node(id).and_then(|n| n.path_data()).ok_or_else(|| bad(C, "object is not a path"))?;
let mut endpoints = BTreeSet::new();
for &(si, seg) in &refs {
let sp = path.subpaths.get(si).filter(|sp| seg < sp.segment_count()).ok_or_else(|| bad(C, "segment does not exist"))?;
endpoints.extend([(si, seg), (si, (seg + 1) % sp.anchors.len())]);
}
s.select(|_, sel| {
sel.clear();
if !refs.is_empty() {
sel.add(id);
sel.anchors.insert(id, endpoints);
sel.segments.insert(id, refs);
}
})?;
ok()
}
fn anchors(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "select.anchors";
let id = checked_id(s, p.get("id").ok_or_else(|| bad(C, "missing id"))?, C)?;
@@ -347,6 +377,7 @@ fn anchors(s: &mut Session, p: &Value) -> Result<Value> {
// A path selected as a whole has every anchor selected: toggle and add start from them all.
let whole = sel.contains(id) && sel.partial(id).is_none();
sel.add(id);
sel.segments.remove(&id);
let e = sel.anchors.entry(id).or_default();
if whole && let Some(path) = d.node(id).and_then(|n| n.path_data()) {
e.extend(path.anchors().map(|(s, i, _)| (s, i)));
@@ -385,6 +416,7 @@ fn anchors_many(s: &mut Session, p: &Value) -> Result<Value> {
sel.clear();
}
for (id, refs) in items {
sel.segments.remove(&id);
let all: BTreeSet<(usize, usize)> =
d.node(id).and_then(|n| n.path_data()).map(|p| p.anchors().map(|(s, i, _)| (s, i)).collect()).unwrap_or_default();
// Anything but a path is selected, toggled or deselected as a whole.