Object styles: Clear Overrides restores the paragraph style; feather choke kept to 0..100

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
paaspaas00andClaude Opus 5.5 committed 2026-10-21 14:47:44 +02:00
1 parent ef4bbdb708
commit 88403c5319
3 files changed
+65 -13

No files matched your search

+2
View File
@@ -1254,6 +1254,8 @@ impl Effects {
ds.size = len("dropShadow.size", ds.size)?;
ds.spread = pct("dropShadow.spread", ds.spread)?;
self.feather = len("feather", self.feather)?;
// As IDML import reads it, so a round trip keeps it.
self.feather_options.choke = pct("featherOptions.choke", self.feather_options.choke)?;
let e = &mut self.inner_shadow;
e.opacity = opacity("innerShadow.opacity", e.opacity)?;
e.angle = finite("innerShadow.angle", e.angle)?;
+34 -13
View File
@@ -377,7 +377,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Clear Object Style Overrides",
[],
None,
"{ids?} — each item takes its object style's fill, stroke, effects and text frame options again → {changed}",
"{ids?} — each item takes its object style's fill, stroke, effects and text frame options again, and a text frame's story its paragraph style → {changed}",
super::has_selection,
clear_object_overrides
),
@@ -1430,18 +1430,33 @@ fn apply_object(s: &mut Session, p: &Value) -> Result<Value> {
fn clear_object_overrides(s: &mut Session, p: &Value) -> Result<Value> {
let ids = super::targets(s, p)?;
// Each target with its style's attributes applied, kept only where that changes the item.
// Each target with its style's attributes applied, kept only where that changes the item;
// and a text frame's story back in the style's paragraph style, as applying the style does.
let d = &s.doc()?.doc;
let changed: Vec<Item> = ids
.iter()
.filter_map(|id| {
let it = d.item(*id)?;
let mut next = it.clone();
d.styles.object_style(&it.object_style)?.apply_to(&mut next);
(next != *it).then_some(next)
})
.collect();
if changed.is_empty() {
let mut changed: Vec<Item> = Vec::new();
let mut restyled: Vec<(designcraft_doc::StoryId, String)> = Vec::new();
let mut n = 0;
for id in &ids {
let Some(it) = d.item(*id) else { continue };
let Some(os) = d.styles.object_style(&it.object_style) else { continue };
let mut next = it.clone();
os.apply_to(&mut next);
let item_changed = next != *it;
if item_changed {
changed.push(next);
}
let mut story_changed = false;
if let (Some(ps), Some(tf)) = (&os.paragraph_style, it.text_frame())
&& d.story(tf.story).is_some_and(|st| st.paras.iter().any(|f| f.style != *ps))
{
story_changed = true;
if !restyled.iter().any(|(sid, _)| *sid == tf.story) {
restyled.push((tf.story, ps.clone()));
}
}
n += usize::from(item_changed || story_changed);
}
if n == 0 {
return Ok(json!({"changed": 0}));
}
s.edit(|d, _| {
@@ -1450,7 +1465,13 @@ fn clear_object_overrides(s: &mut Session, p: &Value) -> Result<Value> {
*it = next.clone();
}
}
Ok(json!({"changed": changed.len()}))
for (sid, ps) in &restyled {
if let Some(st) = d.story_mut(*sid) {
let len = st.len();
st.format_paras(0..len, |f| f.style = ps.clone());
}
}
Ok(json!({"changed": n}))
})
}
+29
View File
@@ -1710,3 +1710,32 @@ fn a_line_fits_when_its_baseline_does() {
assert!(!overset(&mut s, 78.5), "the line is set; its descenders hang below the frame");
assert!(overset(&mut s, 50.0), "a baseline below the frame still oversets the line");
}
/// Clear Overrides puts a text frame's story back in its object style's paragraph style, as
/// applying the style does; and a style's feather choke is kept to 0–100 %, as IDML reads it.
#[test]
fn object_style_clear_overrides_restores_the_paragraph_style_and_choke_is_clamped() {
let mut s = session();
s.execute("style.paragraph.create", &json!({"name": "Caption"})).unwrap();
s.execute("style.object.create", &json!({"name": "Captioned", "fromSelection": false, "paragraphStyle": "Caption"})).unwrap();
let f = s.execute("frame.create", &json!({"rect": [36, 36, 300, 200], "content": "text", "text": "One\nTwo"})).unwrap();
let (id, sid) = (f["id"].as_u64().unwrap(), designcraft_doc::StoryId(f["story"].as_u64().unwrap()));
s.execute("style.object.apply", &json!({"name": "Captioned", "ids": [id]})).unwrap();
let styles = |s: &Session| s.doc().unwrap().doc.story(sid).unwrap().paras.iter().map(|p| p.style.clone()).collect::<Vec<_>>();
assert_eq!(styles(&s), ["Caption", "Caption"]);
{
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
let st = std::sync::Arc::make_mut(doc.stories.get_mut(&sid).unwrap());
st.paras[1].style = designcraft_doc::BASIC_PARAGRAPH.into();
}
let r = s.execute("style.object.clearOverrides", &json!({"ids": [id]})).unwrap();
assert_eq!(r["changed"], 1);
assert_eq!(styles(&s), ["Caption", "Caption"]);
let r = s.execute("style.object.clearOverrides", &json!({"ids": [id]})).unwrap();
assert_eq!(r["changed"], 0, "nothing left to clear");
s.execute("style.object.create", &json!({"name": "Soft", "fromSelection": false, "effects": {"feather": 4, "featherOptions": {"choke": 150}}}))
.unwrap();
let fx = s.doc().unwrap().doc.styles.object_style("Soft").unwrap().effects.clone().unwrap();
assert_eq!(fx.feather_options.choke, 100.0);
}