From 737fcfec043afabaa32a6bc41e1c0f1cfda08f57 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 09:53:54 -0800 Subject: [PATCH] IDML: an item without corners keeps none over its object style Object styles now export their corners, and an import gives an item without corner attributes the corners of its style chain and of [None]. An item with no corners whose style, a style it is based on, or [None] sets corners now writes None for each corner, so it keeps no corners after export and import. The lookup runs once per style. --- crates/idml/src/export.rs | 3 +++ crates/idml/src/export/item.rs | 36 +++++++++++++++++++++++-- crates/idml/src/tests/object_styles.rs | 37 ++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/crates/idml/src/export.rs b/crates/idml/src/export.rs index 717bb5d6c..0c85e8f8a 100644 --- a/crates/idml/src/export.rs +++ b/crates/idml/src/export.rs @@ -106,6 +106,8 @@ struct Ex<'a> { /// Index topic path → first serialized sort key; See / See also cross-references. index_topics: BTreeMap, String>, index_xrefs: Vec<(Vec, &'static str, String)>, + /// Object style name → whether its chain or `[None]` sets corners, found once per style. + style_corners: HashMap, } impl<'a> Ex<'a> { @@ -121,6 +123,7 @@ impl<'a> Ex<'a> { link_text_sources: Vec::new(), index_topics: BTreeMap::new(), index_xrefs: Vec::new(), + style_corners: HashMap::new(), }; ex.assign_swatch_ids(); for s in d.stories.values() { diff --git a/crates/idml/src/export/item.rs b/crates/idml/src/export/item.rs index 469053c2f..3c240deca 100644 --- a/crates/idml/src/export/item.rs +++ b/crates/idml/src/export/item.rs @@ -87,8 +87,10 @@ impl<'a> Ex<'a> { el.set("GradientStrokeAngle", num(a)); } self.stroke_attrs(&mut el, &it.stroke); - // Corner options: map path-order corners to the named corners by geometry. - if !it.corners.is_none() { + // Corner options: map path-order corners to the named corners by geometry. An item without + // corners whose object style chain sets some writes None, so an import does not give it + // the style's corners. + if !it.corners.is_none() || self.style_sets_corners(os) { let names_by_index = corner_names(&it.path); for (i, c) in it.corners.corners.iter().enumerate() { let n = names_by_index[i]; @@ -153,6 +155,36 @@ impl<'a> Ex<'a> { el } + /// Whether object style `name`, a style it is based on, or `[None]` (which an import reads + /// after every item's style chain) sets corners other than None. A built-in style the + /// document lacks counts as its default. + fn style_sets_corners(&mut self, name: &str) -> bool { + if let Some(&known) = self.style_corners.get(name) { + return known; + } + let defaults = designcraft_doc::Styles::default(); + let style = |n: &str| self.d.styles.object_style(n).or_else(|| defaults.object_style(n)); + let rounds = |os: &designcraft_doc::ObjectStyle| os.corners.is_some_and(|c| !c.is_none()); + let mut seen: Vec<&str> = Vec::new(); + let mut cur = Some(name); + let mut found = false; + while let Some(n) = cur.take() { + if seen.contains(&n) { + break; + } + let Some(os) = style(n) else { break }; + if rounds(os) { + found = true; + break; + } + seen.push(n); + cur = os.based_on.as_deref(); + } + let found = found || style(designcraft_doc::NO_OBJECT_STYLE).is_some_and(rounds); + self.style_corners.insert(name.to_string(), found); + found + } + fn append_transparency(&self, el: &mut El, it: &Item, os: &str) { // Transparency. // An item whose style sets effects writes the effects it has turned off too, so that on diff --git a/crates/idml/src/tests/object_styles.rs b/crates/idml/src/tests/object_styles.rs index 4ff0fa6bf..6226d4a74 100644 --- a/crates/idml/src/tests/object_styles.rs +++ b/crates/idml/src/tests/object_styles.rs @@ -232,6 +232,43 @@ fn exported_frames_keep_their_own_values_over_their_object_style() { assert_eq!(got.options, want); } +/// Items without corners keep none after export and import, though their object style, the style +/// it is based on, or `[None]` (read after every style chain) sets rounded corners. +#[test] +fn exported_frames_keep_no_corners_over_their_object_style() { + let mut d = small_doc(); + let lid = d.default_layer(); + let id = designcraft_doc::ItemId(d.alloc()); + let rect = designcraft_doc::Item::new(id, lid, Shape::Rectangle, shapes::rectangle(Rect::new(40.0, 420.0, 200.0, 500.0))); + d.insert_item(SpreadRef::Doc(0), rect, None).unwrap(); + let rounded = Some(designcraft_geom::corners::CornerOptions { + corners: [designcraft_geom::corners::Corner { shape: CornerShape::Rounded, size: 12.0 }; 4], + }); + let styles = std::sync::Arc::make_mut(&mut d.styles); + styles.object.push(designcraft_doc::ObjectStyle { name: "Round".into(), corners: rounded, ..Default::default() }); + styles.object.push(designcraft_doc::ObjectStyle { name: "Child".into(), based_on: Some("Round".into()), ..Default::default() }); + match styles.object.iter_mut().find(|o| o.name == designcraft_doc::NO_OBJECT_STYLE) { + Some(none) => none.corners = rounded, + None => { + styles.object.push(designcraft_doc::ObjectStyle { name: designcraft_doc::NO_OBJECT_STYLE.into(), corners: rounded, ..Default::default() }) + } + } + styles.object.push(designcraft_doc::ObjectStyle { name: "Plain".into(), ..Default::default() }); + let names = ["Round", "Child", designcraft_doc::NO_OBJECT_STYLE, "Plain"]; + let ids: Vec<_> = d.spreads.iter().flat_map(|s| s.items.iter().map(|i| i.id)).take(4).collect(); + assert_eq!(ids.len(), 4); + for (id, style) in ids.iter().zip(names) { + let it = d.item_mut(*id).unwrap(); + it.object_style = style.into(); + assert!(it.corners.is_none()); + } + let back = import_idml(&export_idml(&d)).unwrap(); + for style in names { + let it = back.spreads.iter().flat_map(|s| s.items.iter()).find(|i| i.object_style == style).unwrap(); + assert!(it.corners.is_none(), "{style}: {:?}", it.corners); + } +} + #[test] fn text_wrap_side_and_contour_come_from_the_style_for_frames_and_placed_graphics() { let styles = r#"