mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
M3.24: basic appearance, Layers target dots, Mixed Appearances and Clear resets opacity
An appearance is basic with at most one fill and one stroke, none hidden or with its own opacity, blend mode or effects, and no object effects; Reduce to Basic keeps the topmost visible fill and stroke. The Layers panel fills an object's target circle for any non-default transparency (blend mode, isolation, knockout or an opacity mask: Node::has_default_transparency). A multi-selection whose objects differ shows "Mixed Appearances" with no rows. The Appearance panel's object row and the Control bar name the selection from one source, and type objects are "Type". Expand Appearance is only in the Object menu. Clear Appearance also resets the object's opacity and blend mode. Tests cover addFill/addStroke, setItem/removeItem, clear, reduceToBasic and graphicStyle.apply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b6b87ee9b6
commit
3692b9d233
@@ -326,15 +326,14 @@ impl Appearance {
|
||||
pub fn stroke_width(&self) -> f64 {
|
||||
self.stroke().filter(|s| !s.paint.is_none()).map(|s| s.width).unwrap_or(0.0)
|
||||
}
|
||||
/// Is this the basic one-fill-one-stroke appearance without effects?
|
||||
/// Is this a basic appearance: at most one fill and one stroke, none hidden or with its own
|
||||
/// opacity, blend mode or effects, and no object effects?
|
||||
pub fn is_basic(&self) -> bool {
|
||||
let count = |fill: bool| self.items.iter().filter(|i| i.is_fill() == fill).count();
|
||||
self.effects.is_empty()
|
||||
&& self.items.len() <= 2
|
||||
&& self.items.iter().filter(|i| matches!(i, AppearanceItem::Fill(_))).count() <= 1
|
||||
&& self.items.iter().all(|i| match i {
|
||||
AppearanceItem::Fill(f) => f.effects.is_empty() && f.opacity == 1.0 && f.blend == BlendMode::Normal,
|
||||
AppearanceItem::Stroke(s) => s.effects.is_empty() && s.opacity == 1.0 && s.blend == BlendMode::Normal,
|
||||
})
|
||||
&& count(true) <= 1
|
||||
&& count(false) <= 1
|
||||
&& self.items.iter().all(|i| i.visible() && i.effects().is_empty() && i.opacity() == 1.0 && i.blend() == BlendMode::Normal)
|
||||
}
|
||||
/// Fill item `index`, or the topmost fill for `None`. `None` when that item is not a fill.
|
||||
pub fn fill_at(&self, index: Option<usize>) -> Option<&FillLayer> {
|
||||
@@ -488,6 +487,27 @@ mod tests {
|
||||
assert_eq!((a.items[1].kind_name(), a.items[2].kind_name()), ("stroke", "fill"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_means_one_plain_fill_and_stroke() {
|
||||
let stroke = || AppearanceItem::Stroke(StrokeLayer::new(Paint::solid(Color::BLACK), 1.0));
|
||||
let mut a = Appearance { items: vec![stroke(), stroke()], effects: vec![] };
|
||||
assert!(!a.is_basic(), "two strokes");
|
||||
a.items.pop();
|
||||
assert!(a.is_basic() && Appearance::default().is_basic());
|
||||
let mut b = Appearance::default_art();
|
||||
b.stroke_mut().unwrap().visible = false;
|
||||
assert!(!b.is_basic(), "a hidden stroke");
|
||||
let mut c = Appearance::default_art();
|
||||
c.fill_mut().unwrap().opacity = 0.5;
|
||||
assert!(!c.is_basic(), "fill opacity");
|
||||
let mut d = Appearance::default_art();
|
||||
d.items.push(AppearanceItem::Fill(FillLayer::new(Paint::None)));
|
||||
assert!(!d.is_basic(), "two fills");
|
||||
let mut e = Appearance::default_art();
|
||||
e.effects.push(Effect { id: "distort.roughen".into(), params: serde_json::Value::Null, visible: true });
|
||||
assert!(!e.is_basic(), "an effect");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_creates_missing() {
|
||||
let mut a = Appearance::default();
|
||||
|
||||
+24
-1
@@ -357,6 +357,11 @@ impl Node {
|
||||
pub fn is_container(&self) -> bool {
|
||||
self.children().is_some()
|
||||
}
|
||||
/// Full opacity, Normal blending, no isolation, knockout or opacity mask (the Layers panel
|
||||
/// fills an object's target circle otherwise).
|
||||
pub fn has_default_transparency(&self) -> bool {
|
||||
self.opacity >= 1.0 && self.blend == BlendMode::Normal && !self.isolate && !self.knockout && self.mask.is_none()
|
||||
}
|
||||
/// Kind name as the Layers panel / Properties panel shows it.
|
||||
pub fn kind_label(&self) -> &'static str {
|
||||
match &self.kind {
|
||||
@@ -367,7 +372,7 @@ impl Node {
|
||||
NodeKind::Path { guide: true, .. } => "Guide",
|
||||
NodeKind::Path { .. } => "Path",
|
||||
NodeKind::Compound { .. } => "Compound Path",
|
||||
NodeKind::Text(_) => "Text",
|
||||
NodeKind::Text(_) => "Type",
|
||||
NodeKind::Image(_) => "Image",
|
||||
NodeKind::SymbolInstance { .. } => "Symbol",
|
||||
NodeKind::Blend { .. } => "Blend",
|
||||
@@ -545,6 +550,24 @@ mod tests {
|
||||
assert_eq!(l.label(), "Rectangle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_transparency() {
|
||||
let p = || Node::path(NodeId(1), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art());
|
||||
assert!(p().has_default_transparency());
|
||||
let tweaks: [fn(&mut Node); 5] = [
|
||||
|n| n.opacity = 0.5,
|
||||
|n| n.blend = BlendMode::Multiply,
|
||||
|n| n.isolate = true,
|
||||
|n| n.knockout = true,
|
||||
|n| n.mask = Some(Box::new(OpacityMask::new(n.clone(), true))),
|
||||
];
|
||||
for tweak in tweaks {
|
||||
let mut n = p();
|
||||
tweak(&mut n);
|
||||
assert!(!n.has_default_transparency(), "{n:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_group_recurses() {
|
||||
let p = Node::path(NodeId(2), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art());
|
||||
|
||||
Reference in New Issue
Block a user