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(""##;
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("");
for color in ["#1E6F8E", "#AA0000"] {
let at = svg.find(color).unwrap_or_else(|| panic!("{color} missing\n{svg}"));
let path = svg[..at].rfind(" (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("b") && svg.contains(">a"), "{svg}");
}
#[test]
fn title_case_does_not_restart_words_across_fill_runs_in_live_text() {
use figcraft_doc::{StyleRun, TextCase, TextData};
figcraft_text::scan_system_fonts();
let mut d = Document::new("t");
let page = d.first_page().unwrap();
let mut text = TextData::new("hello");
text.style.font_size = 24.0;
text.style.case = TextCase::Title;
let mut style = text.style.clone();
style.fills = Some(vec![Paint::solid(Color::from_hex("00FF00").unwrap())]);
text.runs.push(StyleRun { start: 2, end: 3, style });
text.normalize();
let id = d.create(page, NodeKind::Text(text), "Text", 0.0, 0.0, 200.0, 40.0).unwrap();
d.get_mut(id).unwrap().fills = vec![Paint::solid(Color::BLACK)];
let svg = export_svg(&d, &[id], &SvgOptions { outline_text: false, ..SvgOptions::default() }).unwrap();
assert!(svg.contains(">He") && svg.contains(">l") && svg.contains(">lo"), "{svg}");
}
/// Control characters in text (a pasted vertical-tab soft break, U+0001) are not allowed in XML:
/// live-text export must leave them out so the SVG still parses, for plain and per-run spans.
#[test]
fn live_text_export_drops_xml_forbidden_control_characters() {
use figcraft_doc::{StyleRun, TextData};
figcraft_text::scan_system_fonts();
for styled in [false, true] {
let mut d = Document::new("t");
let page = d.first_page().unwrap();
let mut text = TextData::new("line one\u{b}line two\u{1}");
text.style.font_size = 24.0;
if styled {
let mut style = text.style.clone();
style.fills = Some(vec![Paint::solid(Color::from_hex("00FF00").unwrap())]);
text.runs.push(StyleRun { start: 5, end: 12, style });
text.normalize();
}
let id = d.create(page, NodeKind::Text(text), "Text", 0.0, 0.0, 600.0, 40.0).unwrap();
d.get_mut(id).unwrap().fills = vec![Paint::solid(Color::BLACK)];
let svg = export_svg(&d, &[id], &SvgOptions { outline_text: false, ..SvgOptions::default() }).unwrap();
assert!(!svg.contains('\u{b}') && !svg.contains('\u{1}'), "{svg:?}");
assert!(svg.contains("line") && svg.contains("two"), "{svg}");
let mut d2 = Document::new("t2");
let p2 = d2.first_page().unwrap();
assert!(import_svg(&mut d2, svg.as_bytes(), p2, 0.0, 0.0, "x").is_ok(), "{svg:?}");
}
}
#[test]
fn side_strokes_on_rounded_corners_export_as_their_area() {
// border-l-4 rounded-r-lg: the stroke path is the 4 px bar (60 px tall), drawn even-odd.
use figcraft_geom::Shape;
let mut d = Document::new("t");
let p = d.first_page().unwrap();
let f = d.create(p, NodeKind::Frame, "F", 0.0, 0.0, 120.0, 60.0).unwrap();
let n = d.get_mut(f).unwrap();
n.corners.radii = [0.0, 8.0, 8.0, 0.0];
n.strokes = vec![Paint::solid(Color::from_hex("FF0000").unwrap())];
n.stroke.align = figcraft_doc::StrokeAlign::Inside;
n.stroke.sides = Some([0.0, 0.0, 0.0, 4.0]);
let svg = export_svg(&d, &[f], &SvgOptions::default()).unwrap();
let stroke = svg.split(""##;
import_svg(&mut d, svg.as_bytes(), p, 0.0, 0.0, "x").unwrap();
let loops = |n: &figcraft_doc::Node| match &n.kind {
NodeKind::Vector { network } => network.regions.iter().map(|r| r.loops.len()).sum::(),
_ => 0,
};
assert_eq!(d.nodes().map(loops).sum::(), 1);
}