mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
As the branch does for older tests: `panic!("…")` instead of
`unreachable!()` in test code (clippy.toml only allows unwrap, expect and
panic there), and the integration tests main added (CLI file I/O, render
gradient text and stroke geometry, SVG export suites) opt out of the
unwrap/expect/panic lints. No test logic changed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
61 lines
2.9 KiB
Rust
61 lines
2.9 KiB
Rust
//! Live effects in SVG export: a fill or stroke's own raster effects become filters on that item's
|
||
//! element, and geometry effects on type are baked through its outlines.
|
||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
|
||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||
|
||
use serde_json::json;
|
||
use vectorcraft_color::{Color, Paint};
|
||
use vectorcraft_doc::{Appearance, CharStyle, Document, Effect, Node, NodeId, NodeKind, TextObject};
|
||
use vectorcraft_geom::{Point, Rect, shapes};
|
||
use vectorcraft_svg::{ExportOptions, export};
|
||
|
||
fn doc_with(mut n: Node) -> Document {
|
||
let mut d = Document::new(200.0, 200.0);
|
||
n.id = d.alloc_id();
|
||
let l = d.layers[0].id;
|
||
d.insert(Some(l), 0, n).unwrap();
|
||
d
|
||
}
|
||
|
||
fn fx(id: &str) -> Effect {
|
||
Effect { id: id.into(), params: json!({}), visible: true }
|
||
}
|
||
|
||
#[test]
|
||
fn a_strokes_own_glow_filters_the_stroke_element_only() {
|
||
let mut n = Node::path(
|
||
NodeId(0),
|
||
shapes::rectangle(Rect::new(20.0, 20.0, 80.0, 80.0)),
|
||
Appearance::basic(Paint::solid(Color::WHITE), Paint::solid(Color::BLACK), 2.0),
|
||
);
|
||
n.appearance.items[1].effects_mut().push(fx("stylize.outerGlow"));
|
||
let svg = export(&doc_with(n), &ExportOptions::default());
|
||
assert_eq!(svg.matches("<filter ").count(), 1, "{svg}");
|
||
// The filter group holds the stroke's path, and the fill's path comes before it, unfiltered.
|
||
let open = svg.find("<g filter=\"url(#").expect("a filter group");
|
||
let inside = &svg[open..svg[open..].find("</g>").map(|i| open + i).unwrap()];
|
||
assert!(inside.contains("stroke=\"#000000\"") && !inside.contains("fill=\"#ffffff\""), "{inside}");
|
||
assert!(svg[..open].contains("fill=\"#ffffff\""), "{svg}");
|
||
// A hidden effect exports nothing.
|
||
let mut n = Node::path(
|
||
NodeId(0),
|
||
shapes::rectangle(Rect::new(20.0, 20.0, 80.0, 80.0)),
|
||
Appearance::basic(Paint::solid(Color::WHITE), Paint::solid(Color::BLACK), 2.0),
|
||
);
|
||
n.appearance.items[1].effects_mut().push(Effect { visible: false, ..fx("stylize.outerGlow") });
|
||
assert!(!export(&doc_with(n), &ExportOptions::default()).contains("<filter "));
|
||
}
|
||
|
||
#[test]
|
||
fn geometry_effects_on_type_export_as_its_reshaped_outlines() {
|
||
let style = CharStyle { size: 40.0, fill: Paint::solid(Color::BLACK), ..Default::default() };
|
||
let mut n = Node::new(NodeId(0), NodeKind::Text(Box::new(TextObject::point(Point::new(10.0, 60.0), "Hi", style))));
|
||
let plain = export(&doc_with(n.clone()), &ExportOptions::default());
|
||
assert!(plain.contains("<text"), "{plain}");
|
||
n.appearance.effects =
|
||
vec![Effect { id: "distort.roughen".into(), params: json!({"size": 3, "relative": false}), visible: true }, fx("stylize.dropShadow")];
|
||
let svg = export(&doc_with(n), &ExportOptions::default());
|
||
assert!(!svg.contains("<text") && svg.contains("<path"), "{svg}");
|
||
assert!(svg.contains("<filter "), "the shadow stays a filter: {svg}");
|
||
}
|