.ai import: a variable-width stroke whose look draws nothing comes in as itself

An object with a variable-width stroke whose drawn look holds nothing that draws, apart from
copies read from %_ lines, has no outline to keep. It now comes in as the object with its width
profile instead of being dropped.
This commit is contained in:
dotCooCoo
2026-11-01 19:09:27 -08:00
parent 7f843e1e4a
commit aecff872a5
3 changed files with 59 additions and 5 deletions
+32 -1
View File
@@ -531,6 +531,24 @@ impl<'a> Reader<'a> {
{
return;
}
// A variable-width stroke's look that draws nothing but copies read from `%_` lines: an
// empty group, which [`styled`] takes the stroke for.
let drawn;
let look = if !hidden
&& slot_of(object.name.as_deref()).is_none()
&& matches!(look.kind, NodeKind::Group { .. })
&& self.styles.get(&style).is_some_and(|st| st.uneven)
&& !draws(look, &self.hidden_ids)
{
let mut copy = look.clone();
if let Some(children) = copy.children_mut() {
children.clear();
}
drawn = copy;
&drawn
} else {
look
};
// A style read here: the object, painted as it says.
if let Some(appearance) = self
.styles
@@ -658,7 +676,8 @@ fn styled(object: &Node, look: &Node, st: &crate::import::ai::style::ArtStyle, p
// A text object's slot (an empty group standing for it) takes the style as the type will.
let slot = slot_of(object.name.as_deref()).is_some();
if !st.plain
|| (!slot && !st.simple)
// A look with no outline in it to keep takes a variable-width stroke as the object's own.
|| (!slot && !st.simple && (!st.uneven || draws(look, &HashSet::new())))
|| (object.is_container() && !slot && !matches!(object.kind, NodeKind::Compound { .. }) && !st.items.is_empty())
{
return None;
@@ -733,3 +752,15 @@ fn strip(n: &mut Node, hidden: &HashSet<NodeId>) {
}
}
}
/// Whether `n` holds anything that draws, apart from the children read from `%_` lines (`hidden`):
/// a node other than a group or a compound path.
fn draws(n: &Node, hidden: &HashSet<NodeId>) -> bool {
if hidden.contains(&n.id) {
return false;
}
match n.children() {
Some(children) if matches!(n.kind, NodeKind::Group { .. } | NodeKind::Compound { .. }) => children.iter().any(|c| draws(c, hidden)),
_ => true,
}
}
+9 -4
View File
@@ -98,6 +98,9 @@ pub(super) struct ArtStyle {
/// Shadow on the object: what any object is read with. Type takes the other plain styles too (its drawn look
/// can't be retyped); other objects keep their drawn look, which is the app's own rendering.
pub simple: bool,
/// It would be [`Self::simple`] but for a variable-width stroke: an object whose drawn look
/// is empty takes it with its profile.
pub uneven: bool,
/// Its fills and strokes, bottom first.
pub items: Vec<StyleItem>,
/// Each item's own visibility, transparency and effects, in the order of [`Self::items`].
@@ -652,14 +655,16 @@ fn style(filters: &[Filter]) -> ArtStyle {
}
st.plain = b.plain;
let count = |stroke: bool| st.items.iter().filter(|i| matches!(i, StyleItem::Stroke { .. }) == stroke).count();
st.simple = st.plain
let simple = st.plain
&& count(true) <= 1
&& count(false) <= 1
// A variable-width stroke curves between its width points, and `WidthProfile` is straight
// between them: the drawn look holds the outline as drawn.
&& st.items.iter().all(|i| matches!(i, StyleItem::Fill(_) | StyleItem::Stroke { profile: None, .. } | StyleItem::Gradient { .. }))
&& st.items.iter().all(|i| matches!(i, StyleItem::Fill(_) | StyleItem::Stroke { .. } | StyleItem::Gradient { .. }))
&& st.looks.iter().all(|l| l.visible && l.opacity >= 1.0 && l.blend == BlendMode::Normal && l.effects.is_empty())
&& st.effects.iter().all(|e| matches!(e.id.as_str(), "stylize.outerGlow" | "stylize.feather" | "stylize.dropShadow"));
// A variable-width stroke curves between its width points, and `WidthProfile` is straight
// between them: the drawn look holds the outline as drawn.
let even = st.items.iter().all(|i| !matches!(i, StyleItem::Stroke { profile: Some(_), .. }));
(st.simple, st.uneven) = (simple && even, simple && !even);
st
}
+18
View File
@@ -1916,6 +1916,24 @@ fn a_variable_width_stroke_comes_in_as_its_drawn_outline() {
assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{:?}", r.warnings);
}
#[test]
fn a_variable_width_stroke_with_an_empty_look_comes_in_as_itself() {
// GUARDS: with no outline in its look to keep, the object was dropped.
let hidden_line = "0 5 m\n10 5 L\n".replace('\n', "\n%_");
let hidden_fill = format!("{}f\n", rect_path(0.0, 3.0, 10.0, 7.0)).replace('\n', "\n%_");
let lens = width_profile_style(&[(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)]);
// An empty look, and ones whose group or compound path holds only a copy read from `%_` lines.
for look in ["u\nU\n".to_string(), format!("u\nu\n%_{hidden_fill}\nU\nU\n"), format!("u\n*u\n%_{hidden_fill}\n*U\nU\n")] {
let art_ = format!("{look}%_{hidden_line}S\n1 (Tapered) XW\n");
let r = read_ok(&format!("{lens}{}", layer(&art_)));
let a = art(&r.doc);
let [object] = a.as_slice() else { panic!("{look:?}: {a:#?}") };
assert!(matches!(object.kind, NodeKind::Path { .. }), "{look:?}: {object:#?}");
assert_eq!(object.appearance.stroke().map(|s| (s.width, s.profile.is_some())), Some((4.0, true)), "{look:?}: {object:#?}");
assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{look:?}: {:?}", r.warnings);
}
}
#[test]
fn a_bare_comment_line_in_a_prolog_leaves_its_end_alone() {
// GUARDS: a bare `%` line's empty marker matched the empty begin marker of a section that has none.