Shaper: an arrow widget switches a Shaper Group between Face Selection and Construction mode

A Shaper Group had no arrow widget for switching between Face Selection
and Construction mode.

With the Shaper or Selection tool active, a selected Shaper Group, or a
group with one of its faces selected, now shows an arrow beside the
middle of the right side of its source shapes' bounds. A click on it
isolates the source shapes (object.isolate). A second click leaves
isolation with the group selected (object.exitIsolation). The arrow
points right in Face Selection mode and left in Construction mode.

In Construction mode a Shaper click selects an original shape of the
isolated group. Before, it selected the whole group outside the
isolation, and shaper.select with source could pick an original of
another Shaper Group lying above it.

Docs: docs/shaper.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-16 15:00:07 +07:00
committed by bflatastic
parent 6dfdf643d9
commit 21b4e70150
9 changed files with 364 additions and 7 deletions
+9 -3
View File
@@ -31,7 +31,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Select Shaper Face",
[],
None,
"{point: [x,y], source?: bool} click selects a group, then a face; source enters construction isolation and selects an original → {id}",
"{point: [x,y], source?: bool} click selects a group, then a face; source enters construction isolation and selects an original; while a group's originals are isolated, it selects one of that group's originals → {id}",
has_doc,
select
),
@@ -687,10 +687,13 @@ fn scribble(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"id": id.0, "regions": touched.len(), "strokes": strokes}))
}
fn group_at(d: &Document, point: Point, construction: bool) -> Option<NodeId> {
/// The topmost editable Shaper Group at `point`, measured by its source shapes in `construction`.
/// When `within` is given, only that group is considered.
fn group_at(d: &Document, point: Point, construction: bool, within: Option<NodeId>) -> Option<NodeId> {
let mut groups = vec![];
d.walk(|n| {
if n.extras().shaper.is_some()
&& within.is_none_or(|g| g == n.id)
&& d.is_editable(n.id)
&& (if construction { source_group(n).and_then(Node::geometric_bounds) } else { n.geometric_bounds() })
.is_some_and(|b| b.inflate(6.0, 6.0).contains(point))
@@ -704,7 +707,10 @@ fn select(s: &mut Session, p: &Value) -> Result<Value> {
let point = point_param(p, "point").filter(|p| finite(*p)).ok_or_else(|| bad("shaper.select", "missing point"))?;
let st = s.doc()?;
let source = bool_or(p, "source", false);
let Some(group) = group_at(&st.doc, point, source) else { return Ok(json!({"id": null})) };
// In Construction mode a source pick stays in the group whose source shapes are isolated.
let isolated =
st.isolation.and_then(|i| st.doc.parent_of(i)).filter(|g| st.doc.node(*g).and_then(source_group).is_some_and(|s| st.isolation == Some(s.id)));
let Some(group) = group_at(&st.doc, point, source, isolated.filter(|_| source)) else { return Ok(json!({"id": null})) };
let node = st.doc.node(group).ok_or(EngineError::NoNode(group))?;
let target = if source {
sources(node)