mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -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
@@ -38,8 +38,24 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_selection,
|
||||
|s, p| add_item(s, p, false)
|
||||
),
|
||||
cmd!("appearance.clear", "Clear Appearance", ["Window", "Appearance"], None, "{ids?}", has_selection, clear_appearance),
|
||||
cmd!("appearance.reduceToBasic", "Reduce to Basic Appearance", ["Window", "Appearance"], None, "{ids?}", has_selection, reduce_basic),
|
||||
cmd!(
|
||||
"appearance.clear",
|
||||
"Clear Appearance",
|
||||
["Window", "Appearance"],
|
||||
None,
|
||||
"{ids?} leave each object one empty fill and stroke (None) and reset its opacity and blend mode",
|
||||
has_selection,
|
||||
clear_appearance
|
||||
),
|
||||
cmd!(
|
||||
"appearance.reduceToBasic",
|
||||
"Reduce to Basic Appearance",
|
||||
["Window", "Appearance"],
|
||||
None,
|
||||
"{ids?} keep only the topmost visible fill and stroke, without their own opacity, blend mode or effects, and drop the object's effects",
|
||||
has_selection,
|
||||
reduce_basic
|
||||
),
|
||||
cmd!(
|
||||
"appearance.setItem",
|
||||
"Appearance Item",
|
||||
@@ -261,7 +277,10 @@ fn clear_appearance(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.edit("Clear Appearance", |d, _| {
|
||||
for id in &ids {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
// No fill, no stroke, and the object's Opacity row back to Default.
|
||||
n.appearance = Appearance::basic(Paint::None, Paint::None, 1.0);
|
||||
n.opacity = 1.0;
|
||||
n.blend = BlendMode::Normal;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -275,10 +294,12 @@ fn reduce_basic(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.edit("Reduce to Basic Appearance", |d, _| {
|
||||
for id in &ids {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
let f = n.appearance.fill_paint();
|
||||
let st = n.appearance.stroke().cloned();
|
||||
n.appearance = Appearance { items: vec![AppearanceItem::Fill(FillLayer::new(f))], effects: vec![] };
|
||||
if let Some(mut st) = st {
|
||||
// The topmost visible fill and stroke stay, without their own transparency or effects.
|
||||
let top = |fill: bool| n.appearance.items.iter().rev().find(|i| i.visible() && i.is_fill() == fill).cloned();
|
||||
let fill = top(true).map_or(Paint::None, |f| f.paint().clone());
|
||||
let stroke = top(false);
|
||||
n.appearance = Appearance { items: vec![AppearanceItem::Fill(FillLayer::new(fill))], effects: vec![] };
|
||||
if let Some(AppearanceItem::Stroke(mut st)) = stroke {
|
||||
st.effects.clear();
|
||||
st.opacity = 1.0;
|
||||
st.blend = BlendMode::Normal;
|
||||
|
||||
@@ -267,3 +267,77 @@ fn text_fill_items_composite_with_their_opacity() {
|
||||
let px = render(&s).pixel(x, y);
|
||||
assert!((110..=145).contains(&px[0]) && px[1] < 10, "{px:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_fill_and_stroke_copy_the_top_ones() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 100.0);
|
||||
run(&mut s, "paint.setFill", json!({"color": "#ff0000"}));
|
||||
run(&mut s, "stroke.set", json!({"weight": 4}));
|
||||
run(&mut s, "appearance.addFill", json!({}));
|
||||
run(&mut s, "appearance.addStroke", json!({})); // [Fill, Stroke, Fill, Stroke]
|
||||
let n = node(&s, id);
|
||||
assert_eq!(n.appearance.items.iter().map(AppearanceItem::kind_name).collect::<Vec<_>>(), ["fill", "stroke", "fill", "stroke"]);
|
||||
assert_eq!(fill_paint(&n, 2).color().unwrap().to_hex(), "#ff0000");
|
||||
assert_eq!(stroke(&n, 3).width, 4.0);
|
||||
assert!(!n.appearance.is_basic());
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(node(&s, id).appearance.items.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_item_and_remove_item() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 100.0);
|
||||
run(&mut s, "appearance.setItem", json!({"index": 1, "opacity": 50, "blend": "screen", "weight": 3, "color": "#0000ff"}));
|
||||
let n = node(&s, id);
|
||||
let st = stroke(&n, 1);
|
||||
assert_eq!((st.opacity, st.blend, st.width), (0.5, BlendMode::Screen, 3.0));
|
||||
assert_eq!(st.paint.color().unwrap().to_hex(), "#0000ff");
|
||||
run(&mut s, "appearance.setItem", json!({"index": 0, "visible": false, "opacity": 0.25}));
|
||||
let n = node(&s, id);
|
||||
assert!(!n.appearance.items[0].visible() && n.appearance.items[0].opacity() == 0.25);
|
||||
assert!(s.execute("appearance.setItem", &json!({})).is_err());
|
||||
run(&mut s, "appearance.removeItem", json!({"index": 0}));
|
||||
let n = node(&s, id);
|
||||
assert_eq!(n.appearance.items.len(), 1);
|
||||
assert!(n.appearance.fill().is_none() && n.appearance.stroke().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_and_reduce_to_basic() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 100.0);
|
||||
run(&mut s, "appearance.addStroke", json!({}));
|
||||
run(&mut s, "appearance.setItem", json!({"index": 2, "visible": false, "color": "#00ff00"}));
|
||||
run(&mut s, "appearance.setItem", json!({"index": 1, "opacity": 0.5}));
|
||||
run(&mut s, "effect.apply", json!({"effect": "distort.roughen", "item": 1}));
|
||||
run(&mut s, "effect.apply", json!({"effect": "stylize.dropShadow", "item": null}));
|
||||
run(&mut s, "transparency.set", json!({"opacity": 40, "blend": "multiply"}));
|
||||
// Reduce keeps the topmost visible fill and stroke, plain, and the object's transparency.
|
||||
run(&mut s, "appearance.reduceToBasic", json!({}));
|
||||
let n = node(&s, id);
|
||||
assert!(n.appearance.is_basic(), "{:?}", n.appearance);
|
||||
assert_eq!(stroke(&n, 1).paint, Paint::solid(Color::BLACK));
|
||||
assert_eq!((n.opacity, n.blend), (0.4, BlendMode::Multiply));
|
||||
// Clear leaves one None fill and stroke, and resets the object's Opacity row.
|
||||
run(&mut s, "appearance.clear", json!({}));
|
||||
let n = node(&s, id);
|
||||
assert_eq!(n.appearance, vectorcraft_doc::Appearance::basic(Paint::None, Paint::None, 1.0));
|
||||
assert_eq!((n.opacity, n.blend), (1.0, BlendMode::Normal));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(node(&s, id).opacity, 0.4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphic_style_apply_replaces_the_stack() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 100.0);
|
||||
run(&mut s, "appearance.addFill", json!({}));
|
||||
run(&mut s, "effect.apply", json!({"effect": "distort.roughen"}));
|
||||
let name = run(&mut s, "graphicStyle.new", json!({"name": "Rough"}))["name"].clone();
|
||||
let b = rect(&mut s, 250.0);
|
||||
run(&mut s, "graphicStyle.apply", json!({ "name": name }));
|
||||
assert_eq!(node(&s, b).appearance, node(&s, a).appearance);
|
||||
assert!(s.execute("graphicStyle.apply", &json!({"name": "nope"})).is_err());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user