mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 22:22:25 +00:00
Merge main into fix/plugin-grant-module-hash
This commit is contained in:
@@ -263,7 +263,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Symbol Instance Appearance",
|
||||
[],
|
||||
None,
|
||||
"{id?|ids?: [one id] (default: the one selected object), path: [i…] (the object in the symbol's art: child indices in paint order from the art's root down through its groups; [] the root), fill?: \"#rrggbb\"|{…paint params of paint.setFill: color, none, swatch, tint, gradient, keepModel}|null (null: the symbol's own again), stroke?: as fill, opacity?: 0..100|null, blend?: name|null} change the appearance of one object of a dynamic symbol instance's art, for that instance only: it stays linked, so editing the symbol reshapes it and keeps the change; refused for a static symbol or a path naming no object → {id, overrides: how many objects the instance changes}",
|
||||
"{id?|ids?: [one id] (default: the one selected object), path: [i…] (the object in the symbol's art: child indices in paint order from the art's root down through its groups; [] the root), fill?: \"#rrggbb\"|{…paint params of paint.setFill: color, none, swatch, tint, gradient, keepModel}|null (null: the symbol's own again), stroke?: as fill, opacity?: 0..100|null, blend?: name|null} change the appearance of one object of a dynamic symbol instance's art, for that instance only: it stays linked, so editing the symbol reshapes it and keeps the change; refused for a static symbol, a path naming no object, or a fill or stroke other than null for an object that does not draw it (a guide, blend, envelope, Repeat, Intertwine group, gradient mesh or placed document; a symbol instance's stroke, and its fill unless a solid color or none, which stains its art; an image's fill or stroke without a Convert to Shape or Outline Object effect), with nothing of the call applied; opacity, blend and null are taken for every object → {id, overrides: how many objects the instance changes}",
|
||||
has_selection,
|
||||
symbol_instance_set_appearance
|
||||
),
|
||||
|
||||
@@ -129,6 +129,64 @@ fn path_param(p: &Value, art: &Node, cmd: &str) -> Result<Vec<u32>> {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// The refusal of `key` (`fill` or `stroke`) for `n`, an object of a dynamic symbol's art, when `n`
|
||||
/// does not draw that paint of its own; `None` when it does. Paths (not guides),
|
||||
/// compound paths, type, groups and layers draw them; a symbol instance draws its fill as a stain
|
||||
/// on its art ([`solid_fill_check`]) and no stroke; an image draws one only when that fill or
|
||||
/// stroke has a Convert to Shape or Outline Object effect.
|
||||
fn paint_refusal(n: &Node, key: &str, cmd: &str) -> Option<EngineError> {
|
||||
Some(match &n.kind {
|
||||
NodeKind::Path { guide: true, .. } => bad(cmd, format!("{key} cannot be set on a guide")),
|
||||
NodeKind::Path { .. } | NodeKind::Compound { .. } | NodeKind::Text(_) | NodeKind::Group { .. } | NodeKind::Layer { .. } => return None,
|
||||
NodeKind::Image(_) => {
|
||||
let effects = if key == "fill" { n.appearance.fill().map(|f| &f.effects) } else { n.appearance.stroke().map(|s| &s.effects) };
|
||||
if effects.is_some_and(|e| vectorcraft_render::effects::gives_image_shape(e)) {
|
||||
return None;
|
||||
}
|
||||
bad(cmd, format!("{key} cannot be set on an image without a Convert to Shape or Outline Object effect on it"))
|
||||
}
|
||||
NodeKind::SymbolInstance { .. } if key == "fill" => return None,
|
||||
NodeKind::SymbolInstance { .. } => bad(cmd, "stroke cannot be set on a symbol instance"),
|
||||
NodeKind::Blend { .. } => bad(cmd, format!("{key} cannot be set on a blend")),
|
||||
NodeKind::Envelope { .. } => bad(cmd, format!("{key} cannot be set on an envelope")),
|
||||
NodeKind::Mesh(_) => bad(cmd, format!("{key} cannot be set on a gradient mesh")),
|
||||
NodeKind::Repeat(_) => bad(cmd, format!("{key} cannot be set on a Repeat")),
|
||||
NodeKind::PlacedDocument(_) => bad(cmd, format!("{key} cannot be set on a placed document")),
|
||||
NodeKind::Intertwine { .. } => bad(cmd, format!("{key} cannot be set on an Intertwine group")),
|
||||
})
|
||||
}
|
||||
|
||||
/// Refuse a `fill` or `stroke` other than null that the object at `path` of `art` does not draw
|
||||
/// ([`paint_refusal`]); null, which takes a change back, is taken for every object.
|
||||
fn check_paint_kinds(p: &Value, art: &Node, path: &[u32], cmd: &str) -> Result<()> {
|
||||
let Some(n) = symbol_overrides::part(art, path) else { return Ok(()) };
|
||||
for key in ["fill", "stroke"] {
|
||||
if p.get(key).is_some_and(|v| !v.is_null())
|
||||
&& let Some(e) = paint_refusal(n, key, cmd)
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Refuse `fill`, the paint `p` asks for the object at `path` of `art`, when that object is a
|
||||
/// symbol instance and `fill` is a gradient or a pattern: an instance stains its art with a solid
|
||||
/// fill only, so such a fill draws nothing.
|
||||
fn solid_fill_check(p: &Value, art: &Node, path: &[u32], fill: Option<&vectorcraft_color::Paint>, cmd: &str) -> Result<()> {
|
||||
let asked = p.get("fill").is_some_and(|v| !v.is_null());
|
||||
let is_instance = symbol_overrides::part(art, path).is_some_and(|n| matches!(n.kind, NodeKind::SymbolInstance { .. }));
|
||||
let draws_nothing = |f: &vectorcraft_color::Paint| match f {
|
||||
vectorcraft_color::Paint::Gradient(_) => true,
|
||||
vectorcraft_color::Paint::Pattern { .. } => true,
|
||||
vectorcraft_color::Paint::None | vectorcraft_color::Paint::Solid { .. } => false,
|
||||
};
|
||||
if asked && is_instance && fill.is_some_and(draws_nothing) {
|
||||
return Err(bad(cmd, "fill on a symbol instance must be a solid color or none"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `fill` or `stroke` of `p` for override `o`: absent leaves it, null clears it, a colour string or
|
||||
/// an object of `paint.setFill`'s paint params sets it.
|
||||
fn paint_change(s: &Session, p: &Value, key: &str, cmd: &str) -> Result<Option<Option<vectorcraft_color::Paint>>> {
|
||||
@@ -170,9 +228,11 @@ pub(super) fn symbol_instance_set_appearance(s: &mut Session, p: &Value) -> Resu
|
||||
}
|
||||
let (id, art) = dynamic_instance(s, p, C)?;
|
||||
let path = path_param(p, &art, C)?;
|
||||
check_paint_kinds(p, &art, &path, C)?;
|
||||
let d = &s.doc()?.doc;
|
||||
let old = d.node(id).and_then(|n| n.extras().symbol_overrides.iter().find(|o| o.path == path).cloned());
|
||||
let new = changed(s, p, old.unwrap_or_else(|| SymbolOverride { path: path.clone(), ..Default::default() }), C)?;
|
||||
solid_fill_check(p, &art, &path, new.fill.as_ref(), C)?;
|
||||
let count = edit_part(s, id, &path, "Symbol Instance Appearance", |o| *o = new)?;
|
||||
Ok(json!({ "id": id.0, "overrides": count }))
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! Fill or Stroke changes it.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{Node, NodeKind};
|
||||
use vectorcraft_geom::Rect;
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
|
||||
@@ -107,6 +108,314 @@ fn static_symbols_and_bad_targets_are_refused() {
|
||||
assert_eq!(overrides(&s, inst), 0);
|
||||
}
|
||||
|
||||
fn undo_len(s: &Session) -> usize {
|
||||
s.doc().unwrap().history.undo.len()
|
||||
}
|
||||
|
||||
/// The frontmost object of the active layer.
|
||||
fn top(s: &Session) -> NodeId {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
d.node(d.default_layer().unwrap()).unwrap().children().unwrap().last().unwrap().id
|
||||
}
|
||||
|
||||
/// Every pixel of the page.
|
||||
fn page(s: &Session) -> Vec<[u8; 4]> {
|
||||
let r = vectorcraft_render::Renderer::new().render_region(&s.doc().unwrap().doc, Rect::new(0.0, 0.0, 200.0, 100.0), 1.0, true);
|
||||
(0..r.height).flat_map(|y| (0..r.width).map(move |x| (x, y))).map(|(x, y)| r.pixel(x, y)).collect()
|
||||
}
|
||||
|
||||
/// A dynamic symbol "Art" of the frontmost object, made in place: its instance.
|
||||
fn art_of_top(s: &mut Session) -> NodeId {
|
||||
let t = top(s);
|
||||
id(&s.execute("symbol.new", &json!({"name": "Art", "ids": [t.0], "type": "dynamic"})).unwrap())
|
||||
}
|
||||
|
||||
/// Put a red fill and stroke on the root of instance `inst`'s art directly, past the command.
|
||||
fn paint_root_red(s: &mut Session, inst: NodeId) {
|
||||
let red = Paint::solid(Color::rgb(1.0, 0.0, 0.0));
|
||||
s.edit("paint", |d, _| {
|
||||
let o = vectorcraft_doc::symbol_overrides::SymbolOverride { path: vec![], fill: Some(red.clone()), stroke: Some(red), ..Default::default() };
|
||||
d.node_mut(inst).ok_or(EngineError::NoNode(inst))?.extras_mut().symbol_overrides.push(o);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// A 30×30 red image placed with its frame from (50, 20) to (80, 50).
|
||||
fn place_image(s: &mut Session) {
|
||||
let png = crate::tests_links::png(30, 30, [255, 0, 0]);
|
||||
let data = vectorcraft_format::base64_encode(&png);
|
||||
s.execute("file.place", &json!({"name": "red.png", "dataBase64": data, "at": [65, 35], "link": false})).unwrap();
|
||||
}
|
||||
|
||||
fn select_top(s: &mut Session, n: usize) {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let kids = d.node(d.default_layer().unwrap()).unwrap().children().unwrap();
|
||||
let ids: Vec<u64> = kids[kids.len() - n..].iter().map(|c| c.id.0).collect();
|
||||
s.execute("select.set", &json!({ "ids": ids })).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objects_that_draw_no_fill_or_stroke_of_their_own_refuse_one() {
|
||||
let folder = crate::tests_links::Folder::new("dynamic-symbol-kinds");
|
||||
let source = folder.file("placed.vectorcraft");
|
||||
let mut doc = vectorcraft_doc::Document::new(40.0, 40.0);
|
||||
let layer = doc.layers[0].id;
|
||||
let look = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0);
|
||||
let rect = vectorcraft_doc::Node::path(doc.alloc_id(), vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 40.0, 40.0)), look);
|
||||
doc.insert(Some(layer), 0, rect).unwrap();
|
||||
crate::tests_links::write(&source, &vectorcraft_format::save_file(&doc));
|
||||
type Make = Box<dyn Fn(&mut Session)>;
|
||||
let cases: Vec<(&str, &str, Make)> = vec![
|
||||
(
|
||||
"guide",
|
||||
"a guide",
|
||||
Box::new(|s| {
|
||||
square(s, 10.0);
|
||||
select_top(s, 1);
|
||||
s.execute("view.guides.make", &json!({})).unwrap();
|
||||
}),
|
||||
),
|
||||
(
|
||||
"blend",
|
||||
"a blend",
|
||||
Box::new(|s| {
|
||||
let (a, b) = (square(s, 10.0), square(s, 60.0));
|
||||
s.execute("object.blend.make", &json!({"ids": [a.0, b.0], "steps": 1})).unwrap();
|
||||
}),
|
||||
),
|
||||
(
|
||||
"envelope",
|
||||
"an envelope",
|
||||
Box::new(|s| {
|
||||
square(s, 10.0);
|
||||
select_top(s, 1);
|
||||
s.execute("object.envelope.makeWithWarp", &json!({"style": "arch", "bend": 50})).unwrap();
|
||||
}),
|
||||
),
|
||||
("radial Repeat", "a Repeat", Box::new(|s| repeat(s, "object.repeat.radial"))),
|
||||
("grid Repeat", "a Repeat", Box::new(|s| repeat(s, "object.repeat.grid"))),
|
||||
("mirror Repeat", "a Repeat", Box::new(|s| repeat(s, "object.repeat.mirror"))),
|
||||
(
|
||||
"Intertwine",
|
||||
"an Intertwine group",
|
||||
Box::new(|s| {
|
||||
square(s, 10.0);
|
||||
square(s, 30.0);
|
||||
select_top(s, 2);
|
||||
s.execute("object.intertwine.make", &json!({})).unwrap();
|
||||
}),
|
||||
),
|
||||
(
|
||||
"gradient mesh",
|
||||
"a gradient mesh",
|
||||
Box::new(|s| {
|
||||
let r = square(s, 10.0);
|
||||
s.execute("object.mesh.create", &json!({"ids": [r.0], "rows": 1, "cols": 1})).unwrap();
|
||||
}),
|
||||
),
|
||||
("image", "an image", Box::new(place_image)),
|
||||
(
|
||||
"placed document",
|
||||
"a placed document",
|
||||
Box::new(move |s| {
|
||||
let r = s.execute("file.place", &json!({"path": source, "at": [40, 40]})).unwrap();
|
||||
assert_eq!(r["linked"], true, "{r}");
|
||||
}),
|
||||
),
|
||||
];
|
||||
// Every case runs, and the failure lists each object that fails.
|
||||
let mut failures = Vec::new();
|
||||
for (label, kind, make) in cases {
|
||||
let mut s = session();
|
||||
make(&mut s);
|
||||
let inst = art_of_top(&mut s);
|
||||
if let Err(why) = refuses_own_paint(&mut s, inst, kind) {
|
||||
failures.push(format!("{label}: {why}"));
|
||||
}
|
||||
}
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
/// Check that the root of instance `inst`'s art, an object of `kind` that draws no fill or stroke
|
||||
/// of its own, refuses both, takes null, opacity and blend, and draws no stored fill or stroke.
|
||||
fn refuses_own_paint(s: &mut Session, inst: NodeId, kind: &str) -> std::result::Result<(), String> {
|
||||
let set = |s: &mut Session, p: &Value| s.execute("symbol.instance.setAppearance", p);
|
||||
let undo = undo_len(s);
|
||||
for key in ["fill", "stroke"] {
|
||||
for paint in [json!("#ff0000"), json!({"none": true})] {
|
||||
// With a valid opacity too: nothing of the call is applied.
|
||||
let p = json!({"id": inst.0, "path": [], key: paint, "opacity": 50});
|
||||
match set(s, &p) {
|
||||
Ok(_) => return Err(format!("{p} was taken")),
|
||||
Err(e) if !e.to_string().contains(&format!("{key} cannot be set on {kind}")) => return Err(format!("{p}: {e}")),
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (overrides(s, inst), undo_len(s)) != (0, undo) {
|
||||
return Err("a refused call changed the instance or added an undo step".into());
|
||||
}
|
||||
// Null (taking a change back), opacity and blend are taken.
|
||||
let clear = json!({"id": inst.0, "path": [], "fill": null, "stroke": null});
|
||||
for p in [clear, json!({"id": inst.0, "path": [], "opacity": 50, "blend": "multiply"})] {
|
||||
set(s, &p).map_err(|e| format!("{p}: {e}"))?;
|
||||
}
|
||||
if overrides(s, inst) != 1 {
|
||||
return Err("opacity and blend were not stored".into());
|
||||
}
|
||||
// The renderer draws no fill or stroke of the object's own: a stored one changes nothing.
|
||||
let before = page(s);
|
||||
paint_root_red(s, inst);
|
||||
if page(s) != before {
|
||||
return Err("a stored fill and stroke changed the canvas".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repeat(s: &mut Session, cmd: &str) {
|
||||
square(s, 10.0);
|
||||
select_top(s, 1);
|
||||
s.execute(cmd, &json!({})).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_symbol_instance_in_the_art_takes_a_fill_that_stains_it_and_no_stroke() {
|
||||
let mut s = session();
|
||||
pair(&mut s, "static");
|
||||
let inst = art_of_top(&mut s);
|
||||
let undo = undo_len(&s);
|
||||
let e = s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [], "stroke": "#ff0000"})).unwrap_err().to_string();
|
||||
assert!(e.contains("stroke cannot be set on a symbol instance"), "{e}");
|
||||
assert_eq!((overrides(&s, inst), undo_len(&s)), (0, undo));
|
||||
// A stroke stored past the command draws nothing.
|
||||
let before = page(&s);
|
||||
let red = Paint::solid(Color::rgb(1.0, 0.0, 0.0));
|
||||
s.edit("paint", |d, _| {
|
||||
let o = vectorcraft_doc::symbol_overrides::SymbolOverride { path: vec![], stroke: Some(red), ..Default::default() };
|
||||
d.node_mut(inst).ok_or(EngineError::NoNode(inst))?.extras_mut().symbol_overrides.push(o);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert!(page(&s) == before, "a stroke on a symbol instance draws nothing");
|
||||
// The fill stains the nested instance's art.
|
||||
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [], "fill": "#ff0000"})).unwrap();
|
||||
assert_eq!(pixel(&s, 30, 30), RED, "the fill stains the art");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_symbol_instance_in_the_art_refuses_a_gradient_fill() {
|
||||
let mut s = session();
|
||||
pair(&mut s, "static");
|
||||
let inst = art_of_top(&mut s);
|
||||
let undo = undo_len(&s);
|
||||
let gradient = json!({"stops": [{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#00ff00"}]});
|
||||
let p = json!({"id": inst.0, "path": [], "fill": {"gradient": gradient}, "opacity": 50});
|
||||
let e = s.execute("symbol.instance.setAppearance", &p).unwrap_err().to_string();
|
||||
assert!(e.contains("fill on a symbol instance must be a solid color or none"), "{e}");
|
||||
assert_eq!((overrides(&s, inst), undo_len(&s)), (0, undo), "nothing changes and no undo step");
|
||||
// A gradient fill stored past the command draws nothing.
|
||||
let before = page(&s);
|
||||
let paint = Paint::Gradient(Box::new(vectorcraft_color::GradientPaint::new(vectorcraft_color::Gradient::default())));
|
||||
s.edit("paint", |d, _| {
|
||||
let o = vectorcraft_doc::symbol_overrides::SymbolOverride { path: vec![], fill: Some(paint), ..Default::default() };
|
||||
d.node_mut(inst).ok_or(EngineError::NoNode(inst))?.extras_mut().symbol_overrides.push(o);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert!(page(&s) == before, "a gradient fill on a symbol instance draws nothing");
|
||||
// A solid fill and none are taken.
|
||||
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [], "fill": {"none": true}})).unwrap();
|
||||
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [], "fill": "#ff0000"})).unwrap();
|
||||
assert_eq!(pixel(&s, 30, 30), RED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_symbol_instance_in_the_art_refuses_a_pattern_fill() {
|
||||
let mut s = session();
|
||||
let tile = id(&s.execute("shape.rectangle", &json!({"x": 150, "y": 60, "width": 10, "height": 10})).unwrap());
|
||||
s.execute("object.pattern.make", &json!({"ids": [tile.0], "name": "Dots", "edit": false})).unwrap();
|
||||
pair(&mut s, "static");
|
||||
let inst = art_of_top(&mut s);
|
||||
let undo = undo_len(&s);
|
||||
let p = json!({"id": inst.0, "path": [], "fill": {"swatch": "Dots"}, "opacity": 50});
|
||||
let e = s.execute("symbol.instance.setAppearance", &p).unwrap_err().to_string();
|
||||
assert!(e.contains("fill on a symbol instance must be a solid color or none"), "{e}");
|
||||
assert_eq!((overrides(&s, inst), undo_len(&s)), (0, undo), "nothing changes and no undo step");
|
||||
// The pattern draws on a path.
|
||||
let probe = square(&mut s, 110.0);
|
||||
let before = page(&s);
|
||||
s.execute("paint.setFill", &json!({"ids": [probe.0], "swatch": "Dots"})).unwrap();
|
||||
assert!(page(&s) != before, "the pattern draws on a path");
|
||||
// A pattern fill stored on the nested instance past the command draws nothing.
|
||||
let before = page(&s);
|
||||
let paint = s.doc().unwrap().doc.swatch("Dots").unwrap().paint.clone();
|
||||
assert!(matches!(paint, Paint::Pattern { .. }), "{paint:?}");
|
||||
s.edit("paint", |d, _| {
|
||||
let o = vectorcraft_doc::symbol_overrides::SymbolOverride { path: vec![], fill: Some(paint), ..Default::default() };
|
||||
d.node_mut(inst).ok_or(EngineError::NoNode(inst))?.extras_mut().symbol_overrides.push(o);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert!(page(&s) == before, "a pattern fill on a symbol instance draws nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_takes_back_an_old_change_an_object_does_not_draw() {
|
||||
let mut s = session();
|
||||
let (a, b) = (square(&mut s, 10.0), square(&mut s, 60.0));
|
||||
s.execute("object.blend.make", &json!({"ids": [a.0, b.0], "steps": 1})).unwrap();
|
||||
let inst = art_of_top(&mut s);
|
||||
// A change stored before the command refused one, as an older document may hold.
|
||||
paint_root_red(&mut s, inst);
|
||||
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [], "fill": null})).unwrap();
|
||||
assert!(node(&s, inst).extras().symbol_overrides[0].fill.is_none());
|
||||
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [], "stroke": null})).unwrap();
|
||||
assert_eq!(overrides(&s, inst), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paths_type_groups_and_shaped_image_paint_take_a_fill_and_stroke() {
|
||||
let set = |s: &mut Session, inst: NodeId, path: Value| {
|
||||
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": path, "fill": "#ff0000", "stroke": "#ff0000"})).unwrap()
|
||||
};
|
||||
// A group's own fill paints over its members.
|
||||
let mut s = session();
|
||||
let inst = pair(&mut s, "dynamic");
|
||||
assert_eq!(set(&mut s, inst, json!([]))["overrides"], 1);
|
||||
assert_eq!(pixel(&s, 30, 30), RED, "the group's own fill");
|
||||
// A compound path, type and a clipping path.
|
||||
let mut s = session();
|
||||
let (a, b) = (square(&mut s, 10.0), square(&mut s, 60.0));
|
||||
s.execute("object.compoundPath.make", &json!({"ids": [a.0, b.0]})).unwrap();
|
||||
let inst = art_of_top(&mut s);
|
||||
set(&mut s, inst, json!([]));
|
||||
assert_eq!(pixel(&s, 30, 30), RED, "the compound path");
|
||||
let mut s = session();
|
||||
s.execute("text.create", &json!({"x": 10, "y": 40, "text": "Hi"})).unwrap();
|
||||
let inst = art_of_top(&mut s);
|
||||
set(&mut s, inst, json!([]));
|
||||
let mut s = session();
|
||||
let (a, b) = (square(&mut s, 10.0), square(&mut s, 20.0));
|
||||
s.execute("object.clippingMask.make", &json!({"ids": [a.0, b.0]})).unwrap();
|
||||
let inst = art_of_top(&mut s);
|
||||
assert!(matches!(&node(&s, inst).kind, NodeKind::SymbolInstance { .. }));
|
||||
set(&mut s, inst, json!([0]));
|
||||
set(&mut s, inst, json!([]));
|
||||
// An image whose fill has Convert to Shape: the fill paints 9 pt past its frame.
|
||||
let mut s = session();
|
||||
place_image(&mut s);
|
||||
s.execute("appearance.addFill", &json!({})).unwrap();
|
||||
s.execute("paint.setFill", &json!({"color": "#00ff00", "item": 0})).unwrap();
|
||||
s.execute("effect.apply", &json!({"effect": "convertToShape.rectangle", "item": 0})).unwrap();
|
||||
assert_eq!(pixel(&s, 44, 35), [0, 255, 0, 255]);
|
||||
let inst = art_of_top(&mut s);
|
||||
let e = s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [], "stroke": "#ff0000"})).unwrap_err().to_string();
|
||||
assert!(e.contains("stroke cannot be set on an image without a Convert to Shape or Outline Object effect on it"), "{e}");
|
||||
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [], "fill": "#ff0000"})).unwrap();
|
||||
assert_eq!(pixel(&s, 44, 35), RED, "the shaped fill takes the instance's color");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redefining_the_symbol_keeps_the_changes_whose_objects_remain() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -461,6 +461,25 @@ fn svg_export_writes_the_filter_when_it_is_the_last_effect() {
|
||||
assert!(!out.contains("<feOffset dx=\"3\""), "{out}");
|
||||
}
|
||||
|
||||
/// A filter at the size limit whose written form, with the id SVG export gives it, would be longer
|
||||
/// than SVG import reads is exported as an image, which opens again as the canvas draws it.
|
||||
#[test]
|
||||
fn svg_export_draws_a_filter_too_long_to_read_back_as_an_image() {
|
||||
let (mut s, id) = blue_square();
|
||||
let (open, close) =
|
||||
(r##"<filter id="r"><feFlood flood-color="#ff0000" result=""##, r#""/><feComposite in2="SourceGraphic" operator="in"/></filter>"#);
|
||||
let xml = format!("{open}{}{close}", "a".repeat(vectorcraft_doc::SvgFilter::MAX_XML - open.len() - close.len()));
|
||||
run(&mut s, "effect.apply", json!({"effect": "svg.filter", "ids": [id.0], "params": {"xml": xml}}));
|
||||
assert_eq!(&render(&s).pixel(100, 100)[..3], &[255, 0, 0]);
|
||||
let out = svg(&mut s);
|
||||
assert!(out.contains("<image") && !out.contains("<filter"), "{} bytes", out.len());
|
||||
let (back, warnings) = vectorcraft_svg::import_with_report(&out).unwrap();
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
let again = vectorcraft_render::Renderer::new().render_region(&back, vectorcraft_geom::Rect::new(0.0, 0.0, 200.0, 200.0), 1.0, true);
|
||||
let px = again.pixel(100, 100);
|
||||
assert!(px[0] > 240 && px[1] < 15 && px[2] < 15, "{px:?}");
|
||||
}
|
||||
|
||||
/// A filter SVG 1.1 can't hold (a blend mode SVG 1.1 lacks) is exported as an image, as the canvas
|
||||
/// draws it; an SVG 1.1 one stays a filter.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user