mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 04:46:50 -06:00
118 lines
5.7 KiB
Rust
118 lines
5.7 KiB
Rust
use super::*;
|
|
use figcraft_color::{Color, Paint};
|
|
use figcraft_doc::{Document, Effect, NodeKind};
|
|
|
|
#[test]
|
|
fn export_basic_shapes() {
|
|
let mut d = Document::new("t");
|
|
let p = d.first_page().unwrap();
|
|
let f = d.create(p, NodeKind::Frame, "Card", 0.0, 0.0, 200.0, 100.0).unwrap();
|
|
let r = d.create(f, NodeKind::Rectangle, "R", 10.0, 10.0, 50.0, 50.0).unwrap();
|
|
d.get_mut(r).unwrap().fills = vec![Paint::solid(Color::from_hex("FF0000").unwrap())];
|
|
d.get_mut(r).unwrap().effects.push(Effect::drop_shadow());
|
|
let svg = export_svg(&d, &[f], &SvgOptions::default()).unwrap();
|
|
assert!(svg.starts_with("<svg width=\"200\" height=\"100\""));
|
|
assert!(svg.contains("#FF0000"));
|
|
assert!(svg.contains("<filter"));
|
|
assert!(svg.contains("clip-path"));
|
|
// Round-trip through import.
|
|
let mut d2 = Document::new("t2");
|
|
let p2 = d2.first_page().unwrap();
|
|
let frame = import_svg(&mut d2, svg.as_bytes(), p2, 0.0, 0.0, "Imported").unwrap();
|
|
assert!(!d2.subtree(frame).is_empty());
|
|
assert!(d2.validate().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn import_rejects_garbage() {
|
|
let mut d = Document::new("t");
|
|
let p = d.first_page().unwrap();
|
|
assert!(import_svg(&mut d, b"not svg", p, 0.0, 0.0, "x").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn import_gradient_and_groups() {
|
|
let svg = br##"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50">
|
|
<defs><linearGradient id="g"><stop offset="0" stop-color="#f00"/><stop offset="1" stop-color="#00f"/></linearGradient></defs>
|
|
<g id="grp"><rect x="10" y="10" width="30" height="30" fill="url(#g)" stroke="#000" stroke-width="2"/></g>
|
|
<circle cx="70" cy="25" r="20" fill="#0f0" fill-opacity="0.5"/></svg>"##;
|
|
let mut d = Document::new("t");
|
|
let p = d.first_page().unwrap();
|
|
let f = import_svg(&mut d, svg, p, 0.0, 0.0, "x").unwrap();
|
|
let kinds: Vec<&str> = d.subtree(f).iter().map(|i| d.node(*i).unwrap().kind.type_name()).collect();
|
|
assert!(kinds.contains(&"GROUP"));
|
|
assert!(kinds.contains(&"RECTANGLE") && kinds.contains(&"ELLIPSE"), "{kinds:?}");
|
|
}
|
|
|
|
/// #325: a frame with children inside a clipping frame keeps its own paint inside the clip. The
|
|
/// clip path is in page coordinates, so it must sit on a group around the transformed path, never
|
|
/// on the path itself (where it would be read after the path's transform, offset by it).
|
|
#[test]
|
|
fn nested_frame_paint_is_clipped_in_page_space() {
|
|
let mut d = Document::new("t");
|
|
let p = d.first_page().unwrap();
|
|
let page = d.create(p, NodeKind::Frame, "Page", 0.0, 0.0, 400.0, 300.0).unwrap();
|
|
d.get_mut(page).unwrap().fills = vec![Paint::solid(Color::from_hex("FFFFFF").unwrap())];
|
|
let card = d.create(page, NodeKind::Frame, "Card", 40.0, 60.0, 300.0, 120.0).unwrap();
|
|
let c = d.get_mut(card).unwrap();
|
|
c.fills = vec![Paint::solid(Color::from_hex("DDDDDD").unwrap())];
|
|
c.corners.radii = [12.0; 4];
|
|
c.clip = true;
|
|
let badge = d.create(card, NodeKind::Frame, "Badge", 200.0, 40.0, 80.0, 40.0).unwrap();
|
|
let b = d.get_mut(badge).unwrap();
|
|
b.fills = vec![Paint::solid(Color::from_hex("1E6F8E").unwrap())];
|
|
b.strokes = vec![Paint::solid(Color::from_hex("AA0000").unwrap())];
|
|
b.stroke.weight = 2.0;
|
|
let dot = d.create(badge, NodeKind::Rectangle, "Dot", 10.0, 10.0, 20.0, 20.0).unwrap();
|
|
d.get_mut(dot).unwrap().fills = vec![Paint::solid(Color::from_hex("FFFFFF").unwrap())];
|
|
for simplify_stroke in [true, false] {
|
|
let svg = export_svg(&d, &[page], &SvgOptions { simplify_stroke, ..SvgOptions::default() }).unwrap();
|
|
for tag in svg.split('<').filter(|t| t.contains(" clip-path=")) {
|
|
assert!(!tag.contains(" transform="), "clip-path on a transformed element: <{tag}\n{svg}");
|
|
}
|
|
// The card's clip: its rounded outline starts after the top-left radius.
|
|
let def = svg.find("<path d=\"M52 60").unwrap();
|
|
let open = svg[..def].rfind("<clipPath id=\"").unwrap() + "<clipPath id=\"".len();
|
|
let card_clip = &svg[open..open + svg[open..].find('"').unwrap()];
|
|
let wrapped = format!("<g clip-path=\"url(#{card_clip})\">");
|
|
for color in ["#1E6F8E", "#AA0000"] {
|
|
let at = svg.find(color).unwrap_or_else(|| panic!("{color} missing\n{svg}"));
|
|
let path = svg[..at].rfind("<path").unwrap();
|
|
assert!(svg[..path].ends_with(&wrapped), "{color} not in the card's clip\n{svg}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// "ab" in black with a green second character, as outlines and as live text.
|
|
fn two_colour_text() -> (Document, figcraft_doc::NodeId) {
|
|
use figcraft_doc::{StyleRun, TextData};
|
|
figcraft_text::scan_system_fonts();
|
|
let mut d = Document::new("t");
|
|
let page = d.first_page().unwrap();
|
|
let mut text = TextData::new("ab");
|
|
text.style.font_size = 24.0;
|
|
let mut style = text.style.clone();
|
|
style.fills = Some(vec![Paint::solid(Color::from_hex("00FF00").unwrap())]);
|
|
text.runs.push(StyleRun { start: 1, end: 2, style });
|
|
text.normalize();
|
|
let id = d.create(page, NodeKind::Text(text), "Text", 0.0, 0.0, 100.0, 40.0).unwrap();
|
|
d.get_mut(id).unwrap().fills = vec![Paint::solid(Color::BLACK)];
|
|
(d, id)
|
|
}
|
|
|
|
#[test]
|
|
fn text_run_fill_survives_outlined_export() {
|
|
let (d, id) = two_colour_text();
|
|
let svg = export_svg(&d, &[id], &SvgOptions { outline_text: true, ..SvgOptions::default() }).unwrap();
|
|
assert!(svg.contains("#000000") && svg.contains("#00FF00"), "{svg}");
|
|
assert_eq!(svg.matches("<path ").count(), 2, "{svg}");
|
|
}
|
|
|
|
#[test]
|
|
fn text_run_fill_survives_live_text_export() {
|
|
let (d, id) = two_colour_text();
|
|
let svg = export_svg(&d, &[id], &SvgOptions { outline_text: false, ..SvgOptions::default() }).unwrap();
|
|
assert!(svg.contains("#000000"), "{svg}");
|
|
assert!(svg.contains("fill=\"#00FF00\"") && svg.contains(">b</tspan>") && svg.contains(">a</tspan>"), "{svg}");
|
|
}
|