mod common;
use common::*;
use zenith_core::default_provider;
use zenith_scene::compile;
use zenith_scene::ir::SceneCommand;
fn doc_with_node(node_kdl: &str) -> String {
format!(
r##"zenith version=1 {{
project id="proj.pat" name="Pattern"
tokens format="zenith-token-v1" {{}}
styles {{}}
document id="doc.pat" title="Pattern" {{
page id="page.pat" w=(px)400 h=(px)300 {{
{node_kdl}
}}
}}
}}"##
)
}
fn fill_ellipses(result: &CompileResult) -> Vec<(f64, f64, f64, f64)> {
result
.scene
.commands
.iter()
.filter_map(|c| match c {
SceneCommand::FillEllipse { x, y, w, h, .. } => Some((*x, *y, *w, *h)),
_ => None,
})
.collect()
}
fn close(a: (f64, f64, f64, f64), b: (f64, f64, f64, f64)) -> bool {
(a.0 - b.0).abs() < 1e-6
&& (a.1 - b.1).abs() < 1e-6
&& (a.2 - b.2).abs() < 1e-6
&& (a.3 - b.3).abs() < 1e-6
}
#[test]
fn grid_exact_coordinates_no_jitter() {
let src = doc_with_node(
r##"pattern id="pat.grid" kind="grid" x=(px)0 y=(px)0 w=(px)100 h=(px)100 spacing=(px)50 {
ellipse id="dot" x=(px)0 y=(px)0 w=(px)10 h=(px)10 fill="#ff0000"
}"##,
);
let doc = parse(&src);
let result = compile(&doc, &default_provider());
let ellipses = fill_ellipses(&result);
assert_eq!(
ellipses.len(),
4,
"expected 4 grid instances; got {ellipses:?}"
);
for expected in [
(0.0, 0.0, 10.0, 10.0),
(50.0, 0.0, 10.0, 10.0),
(0.0, 50.0, 10.0, 10.0),
(50.0, 50.0, 10.0, 10.0),
] {
assert!(
ellipses.iter().any(|&e| close(e, expected)),
"expected an ellipse at {expected:?}; got {ellipses:?}"
);
}
}
#[test]
fn grid_bounds_origin_offset() {
let src = doc_with_node(
r##"pattern id="pat.off" kind="grid" x=(px)20 y=(px)30 w=(px)100 h=(px)100 spacing=(px)50 {
ellipse id="dot" x=(px)0 y=(px)0 w=(px)10 h=(px)10 fill="#00ff00"
}"##,
);
let doc = parse(&src);
let result = compile(&doc, &default_provider());
let ellipses = fill_ellipses(&result);
assert_eq!(ellipses.len(), 4, "expected 4 instances; got {ellipses:?}");
for expected in [
(20.0, 30.0, 10.0, 10.0),
(70.0, 30.0, 10.0, 10.0),
(20.0, 80.0, 10.0, 10.0),
(70.0, 80.0, 10.0, 10.0),
] {
assert!(
ellipses.iter().any(|&e| close(e, expected)),
"expected an ellipse at {expected:?}; got {ellipses:?}"
);
}
}
#[test]
fn scatter_count_and_determinism() {
let src = doc_with_node(
r##"pattern id="pat.sc" kind="scatter" x=(px)0 y=(px)0 w=(px)200 h=(px)200 count=5 seed=7 {
ellipse id="dot" x=(px)0 y=(px)0 w=(px)8 h=(px)8 fill="#0000ff"
}"##,
);
let doc = parse(&src);
let r1 = compile(&doc, &default_provider());
let r2 = compile(&doc, &default_provider());
let e1 = fill_ellipses(&r1);
assert_eq!(e1.len(), 5, "expected 5 scatter instances; got {e1:?}");
for (x, y, _, _) in &e1 {
assert!(
*x >= 0.0 && *x < 200.0 && *y >= 0.0 && *y < 200.0,
"scatter instance ({x},{y}) escaped bounds"
);
}
assert_eq!(
r1.scene.commands, r2.scene.commands,
"scatter must be deterministic across compiles"
);
}
#[test]
fn grid_deterministic() {
let src = doc_with_node(
r##"pattern id="pat.det" kind="grid" x=(px)5 y=(px)5 w=(px)120 h=(px)80 spacing=(px)25 jitter=0.4 seed=11 {
ellipse id="dot" x=(px)0 y=(px)0 w=(px)6 h=(px)6 fill="#222222"
}"##,
);
let doc = parse(&src);
let r1 = compile(&doc, &default_provider());
let r2 = compile(&doc, &default_provider());
assert_eq!(
r1.scene.commands, r2.scene.commands,
"jittered grid must be byte-identical across compiles"
);
assert!(!fill_ellipses(&r1).is_empty());
}
#[test]
fn clip_brackets_instances() {
let src = doc_with_node(
r##"pattern id="pat.clip" kind="grid" x=(px)10 y=(px)20 w=(px)100 h=(px)50 spacing=(px)50 {
ellipse id="dot" x=(px)0 y=(px)0 w=(px)10 h=(px)10 fill="#ff00ff"
}"##,
);
let doc = parse(&src);
let result = compile(&doc, &default_provider());
let cmds = &result.scene.commands;
let push_idx = cmds.iter().position(|c| {
matches!(
c,
SceneCommand::PushClip { x, y, w, h }
if (*x - 10.0).abs() < 1e-6
&& (*y - 20.0).abs() < 1e-6
&& (*w - 100.0).abs() < 1e-6
&& (*h - 50.0).abs() < 1e-6
)
});
let push_idx = push_idx.expect("expected a PushClip of the pattern bounds box");
let pop_idx = cmds[push_idx + 1..]
.iter()
.position(|c| matches!(c, SceneCommand::PopClip))
.map(|p| push_idx + 1 + p)
.expect("expected a PopClip after the bounds PushClip");
let inner = &cmds[push_idx + 1..pop_idx];
assert!(
inner
.iter()
.any(|c| matches!(c, SceneCommand::FillEllipse { .. })),
"expected motif instances inside the bounds clip"
);
}
#[test]
fn background_fill_paints_before_clip() {
let src = doc_with_node(
r##"pattern id="pat.bg" kind="grid" x=(px)10 y=(px)20 w=(px)100 h=(px)50 spacing=(px)50 fill="#102030" {
ellipse id="dot" x=(px)0 y=(px)0 w=(px)10 h=(px)10 fill="#ffffff"
}"##,
);
let doc = parse(&src);
let result = compile(&doc, &default_provider());
let cmds = &result.scene.commands;
let push_idx = cmds
.iter()
.position(|c| {
matches!(
c,
SceneCommand::PushClip { x, y, w, h }
if (*x - 10.0).abs() < 1e-6
&& (*y - 20.0).abs() < 1e-6
&& (*w - 100.0).abs() < 1e-6
&& (*h - 50.0).abs() < 1e-6
)
})
.expect("expected a PushClip of the pattern bounds box");
let fill_idx = cmds
.iter()
.position(|c| {
matches!(
c,
SceneCommand::FillRect { x, y, w, h, .. }
if (*x - 10.0).abs() < 1e-6
&& (*y - 20.0).abs() < 1e-6
&& (*w - 100.0).abs() < 1e-6
&& (*h - 50.0).abs() < 1e-6
)
})
.expect("expected a FillRect of the pattern bounds box (background panel)");
assert!(
fill_idx < push_idx,
"background fill must be emitted before the bounds clip"
);
}
#[test]
fn no_background_without_fill_or_stroke() {
let src = doc_with_node(
r##"pattern id="pat.plain" kind="grid" x=(px)10 y=(px)20 w=(px)100 h=(px)50 spacing=(px)50 {
ellipse id="dot" x=(px)0 y=(px)0 w=(px)10 h=(px)10 fill="#ffffff"
}"##,
);
let doc = parse(&src);
let result = compile(&doc, &default_provider());
let cmds = &result.scene.commands;
let push_idx = cmds
.iter()
.position(|c| {
matches!(
c,
SceneCommand::PushClip { x, y, w, h }
if (*x - 10.0).abs() < 1e-6
&& (*y - 20.0).abs() < 1e-6
&& (*w - 100.0).abs() < 1e-6
&& (*h - 50.0).abs() < 1e-6
)
})
.expect("expected a PushClip of the pattern bounds box");
let has_panel = cmds[..push_idx].iter().any(|c| {
matches!(
c,
SceneCommand::FillRect { x, y, w, h, .. }
| SceneCommand::FillRoundedRect { x, y, w, h, .. }
| SceneCommand::StrokeRect { x, y, w, h, .. }
| SceneCommand::StrokeRoundedRect { x, y, w, h, .. }
if (*x - 10.0).abs() < 1e-6
&& (*y - 20.0).abs() < 1e-6
&& (*w - 100.0).abs() < 1e-6
&& (*h - 50.0).abs() < 1e-6
)
});
assert!(
!has_panel,
"a pattern without fill/stroke must emit no background panel"
);
}
#[test]
fn absent_pattern_unaffected() {
let src =
doc_with_node(r##"ellipse id="plain" x=(px)10 y=(px)10 w=(px)20 h=(px)20 fill="#abcdef""##);
let doc = parse(&src);
let r1 = compile(&doc, &default_provider());
let r2 = compile(&doc, &default_provider());
assert_eq!(r1.scene.commands, r2.scene.commands);
let ellipses = fill_ellipses(&r1);
assert_eq!(ellipses.len(), 1);
assert!(close(ellipses[0], (10.0, 10.0, 20.0, 20.0)));
}