diff --git a/crates/engine/src/cmd/shaper.rs b/crates/engine/src/cmd/shaper.rs index 49f59e7bf..9a952082a 100644 --- a/crates/engine/src/cmd/shaper.rs +++ b/crates/engine/src/cmd/shaper.rs @@ -4,7 +4,10 @@ use super::pathops::Beside; use super::{CommandSpec, EngineError, Result, Session, bad, bool_or, checked_ids_param, cmd, f64_or, has_doc, id_param, point_param}; use serde_json::{Value, json}; -use std::{collections::BTreeMap, sync::Arc}; +use std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, +}; use vectorcraft_doc::shaper::{EdgeEdit, FILL_PREFIX, FaceEdit, FaceSelector, SOURCES, STROKE_PREFIX, ShaperSpec}; use vectorcraft_doc::{Appearance, AppearanceItem, Document, Node, NodeId, NodeKind}; use vectorcraft_geom::{FillRule, PathData, Point, Rect, Shape as _}; @@ -386,17 +389,37 @@ fn generated(d: &mut Document, src: &[Arc], spec: &ShaperSpec, map: &Build Ok(out) } -/// Re-evaluate only groups whose original art changed. Errors reach Session::edit's rollback. -pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> { - let mut ids = vec![]; - d.walk(|n| { - if n.extras().shaper.is_some() { - ids.push(n.id) +fn changed_shaper_groups(before: &[Arc], after: &[Arc], out: &mut Vec) { + let mut by_id: Option>> = None; + for (i, a) in after.iter().enumerate() { + let b = match before.get(i) { + Some(b) if b.id == a.id => Some(b), + _ => by_id.get_or_insert_with(|| before.iter().map(|b| (b.id, b)).collect()).get(&a.id).copied(), + }; + if b.is_some_and(|b| Arc::ptr_eq(b, a)) { + continue; } - }); + if a.extras().shaper.is_some() { + out.push(a.id); + } + if let Some(children) = a.children() { + changed_shaper_groups(b.and_then(|b| b.children()).map_or(&[], Vec::as_slice), children, out); + } + } +} + +/// Re-evaluate only groups whose original art changed. Errors reach Session::edit's rollback. +/// Returns whether any Shaper group's artwork was rebuilt or modified. +pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result { + let mut ids = vec![]; + changed_shaper_groups(&before.layers, &d.layers, &mut ids); + if ids.is_empty() { + return Ok(false); + } // Groups whose source art is all gone are removed through `emptied`, which is made when the // first one is found. let mut emptied: Option = None; + let mut rebuilt_any = false; for &id in &ids { let Some(current) = d.node(id).cloned() else { continue }; let Some(old) = before.node(id) else { continue }; @@ -413,6 +436,7 @@ pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> { } if src.is_empty() { emptied.get_or_insert_with(|| Beside::replacing(d, &ids)).put(d, id, vec![])?; + rebuilt_any = true; continue; } let map = map_of(&src)?; @@ -510,8 +534,10 @@ pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> { if let Some(ch) = n.children_mut() { *ch = children; } + rebuilt_any = true; } - emptied.map_or(Ok(()), |e| e.finish(d)) + emptied.map_or(Ok(()), |e| e.finish(d))?; + Ok(rebuilt_any) } fn candidates(s: &Session, p: &Value, points: &[Point], tolerance: f64) -> Result> { diff --git a/crates/engine/src/cmd/textwrap.rs b/crates/engine/src/cmd/textwrap.rs index 1315f6da6..b90a67105 100644 --- a/crates/engine/src/cmd/textwrap.rs +++ b/crates/engine/src/cmd/textwrap.rs @@ -4,6 +4,8 @@ //! resolves, for each area text object, the outlines of the wrap objects above it into //! `TextObject::wrap` (text space); the layout engine subtracts them from each line's span. +use std::sync::Arc; + use serde_json::{Value, json}; use vectorcraft_doc::{Document, Node, NodeId, NodeKind, TextKind, TextWrap, WrapShape}; use vectorcraft_geom::{PathData, shapes}; @@ -149,21 +151,18 @@ fn collect<'a>(n: &'a Node, out: &mut Vec>) { } } -fn any_wrap_state(n: &Node) -> bool { - n.extras().wrap.is_some() - || matches!(&n.kind, NodeKind::Text(t) if !t.wrap.is_empty()) - || n.children().is_some_and(|ch| ch.iter().any(|c| any_wrap_state(c))) -} - /// Re-resolve the wrap shapes of every area text object (called after each edit). -pub(crate) fn refresh(doc: &mut Document) { - if !doc.layers.iter().any(|l| any_wrap_state(l)) { - return; - } +pub(crate) fn refresh(before: &Document, doc: &mut Document) { let mut updates: Vec<(NodeId, Vec)> = vec![]; - for layer in &doc.layers { + for (idx, layer) in doc.layers.iter().enumerate() { + if before.layers.get(idx).is_some_and(|b| Arc::ptr_eq(b, layer)) || before.layers.iter().any(|b| b.id == layer.id && Arc::ptr_eq(b, layer)) { + continue; + } let mut items = vec![]; collect(layer, &mut items); + if items.is_empty() { + continue; + } for (i, it) in items.iter().enumerate() { let Item::Text(id) = it else { continue }; let Some(NodeKind::Text(t)) = doc.node(*id).map(|n| &n.kind) else { continue }; diff --git a/crates/engine/src/session/edit.rs b/crates/engine/src/session/edit.rs index 0f3056522..a15bc5422 100644 --- a/crates/engine/src/session/edit.rs +++ b/crates/engine/src/session/edit.rs @@ -31,13 +31,15 @@ impl Session { }) { Ok(v) => match cmd::shaper::refresh(&before, Arc::make_mut(&mut st.doc)) { Err(e) => Err(e), - Ok(()) => { + Ok(shaper_changed) => { // Attached Shaper groups whose art was rebuilt go back on their path. - cmd::objects_on_path::refresh(&before, Arc::make_mut(&mut st.doc)); + if shaper_changed { + cmd::objects_on_path::refresh(&before, Arc::make_mut(&mut st.doc)); + } // Inline graphics in text follow their symbols. cmd::inline::refresh(&before, Arc::make_mut(&mut st.doc)); // Text Wrap: area type follows its wrap objects; then threads re-flow. - cmd::textwrap::refresh(Arc::make_mut(&mut st.doc)); + cmd::textwrap::refresh(&before, Arc::make_mut(&mut st.doc)); // Type in no thread numbers its list paragraphs from 1 (before the mask copies // its editing layer's art). cmd::threads::restart_unthreaded_counts(&mut st.doc);