Merge pull request #4379 from dotCooCoo/fix/shape-builder-merge-off-ui-thread

Shape Builder: a large selection merges on another thread
This commit is contained in:
BFlat
2026-11-09 13:10:54 +08:00
committed by GitHub
40 changed files with 2215 additions and 154 deletions
@@ -1,5 +1,5 @@
use super::{
Beside, BuilderMap, EngineError, FillRule, Node, NodeId, PathData, Point, Rect, Result, Session, SubPath, Touched, Value, b, bad, check_numbers,
Arc, Beside, BuilderMap, EngineError, FillRule, Node, NodeId, PathData, Point, Rect, Result, Session, Touched, Value, b, bad, check_numbers,
f64_or, fill_param, finite, gap_param, ids_json, json, marquee_param, po, points_param, remnant, replace_leaves, replace_with_pieces, root_ids,
shape_node, switch_or,
};
@@ -41,6 +41,7 @@ pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
let gap = gap_param(p);
// The map the Shape Builder tool already has, which a click or drag of it is made over.
let shared = s.tool.shape_builder_map().filter(|(_, g)| *g == gap);
let prepared = s.tool.shape_builder_merge();
let label = if erase { "Shape Builder Delete" } else { "Shape Builder" };
let (ids, merged, touched, split) = s.edit(label, |d, sel| {
let (shapes, leaves) = b::shape_builder_shapes(d, &roots, open_filled_as_closed);
@@ -89,16 +90,28 @@ pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
touched.add(map.hit(q, Some(tol)));
return Ok((ids, None, touched, Some(split)));
}
let mut touched = Touched { regions: marquee.map(|(a, b)| map.regions_in_marquee(a, b)).unwrap_or_default(), ..Default::default() };
touched.extend(&map.builder_hits_along(&pts, piece_tol));
touched.edges = map.erased_edges(&touched);
let touched = map.merge_touched(&pts, marquee, piece_tol);
if touched.is_empty() {
return Err(EngineError::Other("Shape Builder: no region under the pointer".into()));
}
let cuts: Vec<&SubPath> = touched.edges.iter().filter_map(|&k| map.edges.get(k)?.0.path.subpaths.first()).collect();
let inputs = b::merge_inputs(&leaves.iter().collect::<Vec<_>>(), open_filled_as_closed);
// The tool computed this merge on another thread when it would come out the same.
let ready = prepared.as_ref().filter(|m| {
shared.as_ref().is_some_and(|(s, _)| Arc::ptr_eq(s, &m.map) && std::ptr::eq(s.as_ref(), map))
&& m.gap == gap
&& m.touched == touched
&& m.inputs == inputs
});
let computed;
let geometry = match ready {
Some(m) => &m.geometry,
None => {
computed = b::merge_geometry(map, &touched, gap, &inputs);
&computed
}
};
let picked: Vec<&po::Region> = touched.regions.iter().filter_map(|&i| map.regions.get(i).map(|r| &r.0)).collect();
let faces = po::FaceMerger::with_gap(&map.shapes, gap_param(p));
let union = if picked.is_empty() { PathData::default() } else { faces.merge(&picked) };
let union = &geometry.union;
// Pick Color From Artwork: the merged shape takes the look of the front-most object under the
// drag's first point, else under its last point, else of the front-most object. Color
// Swatches: the current fill, with the rest of the look of the front-most object over the
@@ -115,7 +128,7 @@ pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
// What goes in place of each object the map was made from (none: it stays as it is).
let mut outs: Vec<Option<Vec<Node>>> = vec![];
let mut merged_id = None;
for (i, (sh, leaf)) in map.shapes.iter().zip(&leaves).enumerate() {
for (i, leaf) in leaves.iter().take(map.shapes.len()).enumerate() {
let mut out = None;
let pieces: Vec<usize> = map.lines.iter().enumerate().filter(|(_, (l, _))| l.key == i as u64).map(|(k, _)| k).collect();
if !pieces.is_empty() {
@@ -131,41 +144,15 @@ pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
}
out = Some(kept);
}
} else {
} else if let Some(Some(rest)) = geometry.rests.get(i) {
// A shape loses the regions deleted or merged, then the edges deleted (each one on
// its outline, shared edges from every shape along them).
let in_union = picked.iter().any(|r| r.sources.contains(&i));
let mut rest = if in_union {
// What is left of it: its faces not merged or deleted, united. They share their
// edges with the merged faces exactly, where subtracting the merged shape left
// slivers of the outline along them (#569). A sliver goes in only where the
// shape covers it and the merged shape does not take it in.
let kept: Vec<&po::Region> = map
.regions
.iter()
.enumerate()
.filter(|(k, (r, ..))| r.sources.contains(&i) && !touched.regions.contains(k))
.map(|(_, (r, ..))| r)
.collect();
if kept.is_empty() { PathData::default() } else { faces.merge_rest(i, &kept, &picked) }
// its outline, shared edges from every shape along them). What is left of its own
// outline keeps its fill rule; united faces fill with non-zero.
out = Some(if rest.path.is_empty() {
vec![]
} else {
b::node_outline(leaf).map_or_else(|| sh.path.clone(), |o| o.0)
};
let fill_closes = open_filled_as_closed && !leaf.appearance.fill_paint().is_none();
// Edges come back from the planar map within a few hundredths of the outlines they
// follow, whatever else is selected.
let cut_tol = b::outline_snap(&sh.path);
let mut cut = false;
for piece in &cuts {
if let Some(r) = po::cut_out(&rest, piece, fill_closes, cut_tol) {
rest = r;
cut = true;
}
}
if in_union || cut {
// What is left of its own outline keeps its fill rule; united faces fill with non-zero.
out = Some(if rest.is_empty() { vec![] } else { vec![remnant(d, leaf, rest, in_union.then_some(FillRule::NonZero))] });
}
vec![remnant(d, leaf, rest.path.clone(), rest.united.then_some(FillRule::NonZero))]
});
}
// The merged shape goes in place of the object it takes its look from, so it stays in
// that object's group (a clip group too).
+2
View File
@@ -752,6 +752,8 @@ pub(super) fn encode_ai(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Res
/// file becomes the document's own; the live document gets them in [`SaveJob::finish`]). Bad
/// options fail here, before anything is encoded.
pub fn save_job(s: &mut Session, plan: SavePlan) -> Result<SaveJob> {
// A Shape Builder merge still being computed for a click is saved with the document.
s.finish_tool_edit_later()?;
// The web build, without a clock, leaves the dates unless given one.
let at = if plan.path.is_some() && plan.retargets() { clock_date(s, "modified", plan.modified) } else { None };
job_for_stamped(s, s.doc()?, plan, at)
+6
View File
@@ -678,6 +678,12 @@ impl Session {
for e in self.prefs.keep_folders(&mut p) {
log::warn!("{e}");
}
// The edit the active tool computes on another thread is applied first (the Preferences
// dialog applies its settings here, outside a command). A failure to apply it was the
// edit's own, and the preferences apply all the same.
if self.depth == 0 {
let _ = self.finish_tool_edit_later();
}
let grid_changed = p.gridline_every != self.prefs.gridline_every || p.grid_subdivisions != self.prefs.grid_subdivisions;
let history_changed = p.history_states != self.prefs.history_states;
let tile_edge_changed = p.pattern_tile_edge_color != self.prefs.pattern_tile_edge_color;