From 2b7b12df33334a51d81a131be07bbe363b24e7b9 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 01:17:33 -0700 Subject: [PATCH] Symbols: the edited symbol takes the text wrap its area type gets after a wrap object moves Symbol editing copies the editing layer's art into the symbol before Text Wrap, list numbering and threaded re-flow update that layer, so moving a wrap object in the edit left the symbol, its other instances and the saved file with the old wrap. The edit now syncs the symbol again after those passes, and when that changes the symbol, inline graphics of it are refreshed and the threads holding them re-flow. --- crates/engine/src/session/edit.rs | 27 ++++++++++++++++- crates/engine/src/tests/symboledit.rs | 42 +++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/session/edit.rs b/crates/engine/src/session/edit.rs index fd2de6ec5..71f70d25a 100644 --- a/crates/engine/src/session/edit.rs +++ b/crates/engine/src/session/edit.rs @@ -39,10 +39,13 @@ impl Session { } // Symbol editing: the symbol follows its art on the editing layer, before // inline graphics follow their symbols. - let symbol_synced = + let mut symbol_synced = if st.doc.symbol_edit.is_some() { cmd::symboledit::sync(&before, Arc::make_mut(&mut st.doc)) } else { Ok(()) }; // Inline graphics in text follow their symbols. cmd::inline::refresh(&before, Arc::make_mut(&mut st.doc)); + // The document the passes below start from, for the symbol to follow what they + // change on its editing layer. + let synced = st.doc.symbol_edit.is_some().then(|| st.doc.clone()); // Text Wrap: area type follows its wrap objects; then threads re-flow. cmd::textwrap::refresh(&before, Arc::make_mut(&mut st.doc)); // Type in no thread numbers its list paragraphs from 1 (before the mask copies @@ -56,6 +59,28 @@ impl Session { if !st.doc.text_threads.is_empty() { cmd::threads::reflow(&before, Arc::make_mut(&mut st.doc)); } + // Symbol editing: the symbol also takes the text wrap, list numbering and + // re-flow those passes gave its editing layer; when that changes the symbol, its + // inline graphics follow it and the threads they are in re-flow. + let editing = st.doc.symbol_edit.as_ref().map(|e| e.layer); + if let Some(synced) = &synced + && symbol_synced.is_ok() + && editing.is_some() + && before.symbol_edit.as_ref().map(|e| e.layer) == editing + { + let art = + |d: &Document| d.symbol_edit.as_ref().and_then(|e| d.symbols.iter().find(|x| x.name == e.symbol)).map(|x| x.art.clone()); + let old = art(&st.doc); + symbol_synced = cmd::symboledit::sync(synced, Arc::make_mut(&mut st.doc)); + let new = art(&st.doc); + if symbol_synced.is_ok() && !matches!((&old, &new), (Some(a), Some(b)) if Arc::ptr_eq(a, b)) { + let resynced = st.doc.clone(); + cmd::inline::refresh(synced, Arc::make_mut(&mut st.doc)); + if !st.doc.text_threads.is_empty() { + cmd::threads::reflow(&resynced, Arc::make_mut(&mut st.doc)); + } + } + } // Asset Export: assets let go of deleted art. if !st.doc.assets.is_empty() { Arc::make_mut(&mut st.doc).prune_assets(); diff --git a/crates/engine/src/tests/symboledit.rs b/crates/engine/src/tests/symboledit.rs index 8cd11aaa2..450572b73 100644 --- a/crates/engine/src/tests/symboledit.rs +++ b/crates/engine/src/tests/symboledit.rs @@ -676,3 +676,45 @@ fn undoing_entry_with_a_group_isolated_inside_the_art_goes_back_to_before() { assert_eq!(st.isolation, None); assert_eq!(st.active_layer, before); } + +/// The text wrap of the area type in `n`. +fn wrap_in(n: &Node) -> Vec { + let mut wrap = None; + n.walk(&mut |n| { + if let NodeKind::Text(t) = &n.kind { + wrap = Some(t.wrap.clone()); + } + }); + wrap.expect("area type") +} + +#[test] +fn moving_a_wrap_object_in_the_edit_gives_the_symbol_the_new_text_wrap() { + let mut s = Session::new(); + run(&mut s, "file.new", json!({"width": 600, "height": 600})); + let text = "word ".repeat(200); + let t = id(&run(&mut s, "text.create", json!({"x": 0, "y": 0, "text": text, "size": 12, "area": {"width": 400, "height": 400}}))); + let r = id(&run(&mut s, "shape.rectangle", json!({"x": 0, "y": 50, "width": 200, "height": 100}))); + run(&mut s, "select.set", json!({"ids": [r.0]})); + run(&mut s, "object.textWrap.make", json!({})); + run(&mut s, "select.set", json!({"ids": [t.0, r.0]})); + let instance = id(&run(&mut s, "symbol.new", json!({"name": "Wrapped"}))); + let symbol_wrap = |s: &Session| wrap_in(&s.doc().unwrap().doc.symbols.iter().find(|x| x.name == "Wrapped").expect("the symbol").art); + let entered = symbol_wrap(&s); + assert!(!entered.is_empty()); + run(&mut s, "symbol.edit", json!({"id": instance.0})); + let (layer, rect) = { + let st = s.doc().unwrap(); + let layer = st.doc.symbol_edit.as_ref().unwrap().layer; + let rect = layer_art(&s).into_iter().find(|i| st.doc.node(*i).is_some_and(|n| n.extras().wrap.is_some())).expect("the wrap object"); + (layer, rect) + }; + run(&mut s, "select.set", json!({"ids": [rect.0]})); + run(&mut s, "object.move", json!({"dx": 150, "dy": 0})); + let on_layer = wrap_in(s.doc().unwrap().doc.node(layer).unwrap()); + assert_ne!(on_layer, entered, "the editing layer's text wraps around the moved object"); + assert_eq!(symbol_wrap(&s), on_layer); + // Leaving keeps it. + run(&mut s, "symbol.exitEditing", json!({})); + assert_eq!(symbol_wrap(&s), on_layer); +}