avenger-wgpu 0.0.8

WGPU rendering engine for the Avenger visualization framework
Documentation
use avenger_wgpu::register_font_directory;
use std::path::Path;
use std::sync::Once;

static INIT: Once = Once::new();

pub fn initialize() {
    INIT.call_once(|| {
        let root_path = Path::new(env!("CARGO_MANIFEST_DIR"));
        let fonts_dir = root_path
            .join("..")
            .join("avenger-vega-test-data")
            .join("fonts");
        register_font_directory(fonts_dir.to_str().unwrap());
    });
}

#[cfg(test)]
mod test_image_baselines {
    use crate::initialize;
    use avenger::scene_graph::SceneGraph;
    use avenger_vega::scene_graph::VegaSceneGraph;
    use avenger_wgpu::canvas::{Canvas, CanvasDimensions, PngCanvas};
    use dssim::Dssim;
    use rstest::rstest;
    use std::fs;
    use std::path::Path;

    #[rstest(
        category,
        spec_name,
        tolerance,
        case("rect", "stacked_bar", 0.001),
        case("rect", "stacked_bar_stroke", 0.001),
        case("rect", "stacked_bar_rounded", 0.001),
        case("rect", "stacked_bar_rounded_stroke", 0.001),
        case("rect", "stacked_bar_rounded_stroke_opacity", 0.009),
        case("rect", "heatmap", 0.006),
        case("symbol", "binned_scatter_diamonds", 0.001),
        case("symbol", "binned_scatter_square", 0.001),
        case("symbol", "binned_scatter_triangle-down", 0.001),
        case("symbol", "binned_scatter_triangle-up", 0.001),
        case("symbol", "binned_scatter_triangle-left", 0.001),
        case("symbol", "binned_scatter_triangle-right", 0.001),
        case("symbol", "binned_scatter_triangle", 0.001),
        case("symbol", "binned_scatter_wedge", 0.001),
        case("symbol", "binned_scatter_arrow", 0.001),
        case("symbol", "binned_scatter_cross", 0.001),
        case("symbol", "binned_scatter_circle", 0.001),
        case("symbol", "binned_scatter_path", 0.001),
        case("symbol", "binned_scatter_path_star", 0.001),
        case("symbol", "binned_scatter_cross_stroke", 0.001),
        case("symbol", "binned_scatter_circle_stroke", 0.001),
        case("symbol", "binned_scatter_circle_stroke_no_fill", 0.001),
        case("symbol", "binned_scatter_path_star_stroke_no_fill", 0.001),
        case("symbol", "scatter_transparent_stroke", 0.005),
        case("symbol", "scatter_transparent_stroke_star", 0.006),
        case("symbol", "wind_vector", 0.0015),
        case("symbol", "wedge_angle", 0.001),
        case("symbol", "wedge_stroke_angle", 0.001),
        case("symbol", "zindex_circles", 0.001),
        case("symbol", "mixed_symbols", 0.001),
        case("rule", "wide_rule_axes", 0.0001),

        // lyon seems to omit closing square cap, need to investigate
        case("rule", "wide_transparent_caps", 0.08),
        case("rule", "dashed_rules", 0.004),

        case("text", "bar_axis_labels", 0.033),
        case("text", "text_alignment", 0.015),
        case("text", "text_rotation", 0.015),
        case("text", "letter_scatter", 0.03),
        case("text", "lasagna_plot", 0.02),
        case("text", "arc_radial", 0.01),

        // vl-convert doesn't support emoji at all
        case("text", "emoji", 2.0),
        case("arc", "single_arc_no_inner", 0.0005),
        case("arc", "single_arc_with_inner_radius", 0.0005),
        case("arc", "single_arc_with_inner_radius_wrap", 0.0005),
        case("arc", "single_arc_with_inner_radius_wrap_stroke", 0.0005),
        case("arc", "arcs_with_variable_outer_radius", 0.0005),
        case("arc", "arcs_with_variable_outer_radius_stroke", 0.0005),
        case("arc", "arc_with_stroke", 0.0005),
        case("path", "single_path_no_stroke", 0.0005),
        case("path", "multi_path_no_stroke", 0.0005),

        // vl-convert/resvg messes up the path_with_stroke examples because it scales the path
        // width. The Vega editor renderers don't do this.
        case("path", "single_path_with_stroke", 0.8),
        case("path", "single_path_with_stroke_no_fill", 0.8),
        case("path", "multi_path_with_stroke", 0.8),
        case("path", "multi_path_with_stroke_no_fill", 0.8),

        // us-counties is a bit off due to how anti-aliasing results in light border between
        // adjacent shapes. The wgpu implementation doesn't have this border
        case("shape", "us-counties", 0.003),
        case("shape", "us-map", 0.0006),
        case("shape", "world-natural-earth-projection", 0.0006),
        case("shape", "london_tubes", 0.0002),

        case("line", "simple_line_round_cap", 0.0001),
        case("line", "simple_line_butt_cap_miter_join", 0.0001),
        // lyon seems to omit closing square cap, need to investigate
        case("line", "simple_line_square_cap_bevel_join", 0.002),
        case("line", "connected_scatter", 0.0008),
        case("line", "lines_with_open_symbols", 0.0004),
        case("line", "stocks", 0.0005),
        case("line", "stocks-legend", 0.003),
        case("line", "simple_dashed", 0.0005),
        case("line", "stocks_dashed", 0.002),
        case("line", "line_dashed_round_undefined", 0.0005),

        // lyon's square end cap doesn't seem to work
        case("line", "line_dashed_square_undefined", 0.007),
        case("line", "line_dashed_butt_undefined", 0.0005),

        // case("area", "100_percent_stacked_area", 0.005),
        case("area", "simple_unemployment", 0.0005),
        case("area", "simple_unemployment_stroke", 0.0005),
        case("area", "stacked_area", 0.005),
        case("area", "streamgraph_area", 0.005),
        case("area", "with_undefined", 0.0005),
        case("area", "with_undefined_horizontal", 0.0005),

        case("trail", "trail_stocks", 0.0005),
        case("trail", "trail_stocks_opacity", 0.0005),

        case("image", "logos", 0.001),
        case("image", "logos_sized_aspect_false", 0.001),
        case("image", "logos_sized_aspect_false_align_baseline", 0.001),
        case("image", "logos_sized_aspect_true_align_baseline", 0.001),
        case("image", "smooth_false", 0.03),  // vl-convert/resvg doesn't support smooth=false
        case("image", "smooth_true", 0.001),
        case("image", "many_images", 0.001),
        case("image", "large_images", 0.001),

        case("gradients", "heatmap_with_colorbar", 0.001),
        case("gradients", "diagonal_gradient_bars_rounded", 0.001),
        case("gradients", "default_gradient_bars_rounded_stroke", 0.0015),
        case("gradients", "residuals_colorscale", 0.0015),
        case("gradients", "stroke_rect_gradient", 0.002),
        case("gradients", "area_with_gradient", 0.001),
        case("gradients", "area_line_with_gradient", 0.001),
        case("gradients", "trail_gradient", 0.001),

        // vl-convert/resvg messes up scaled paths with strokes
        case("gradients", "path_with_stroke_gradients", 0.5),
        case("gradients", "rules_with_gradients", 0.01), // Lyon square caps issue
        case("gradients", "symbol_cross_gradient", 0.001),
        case("gradients", "symbol_circles_gradient_stroke", 0.001),

        // Our gradient bounding box for arc marks is the full circle, not the bounding box around the arc wedge
        case("gradients", "arc_gradient", 0.1),

        // vl-convert/resvg doesn't handle focus radius properly
        case("gradients", "radial_concentric_gradient_bars", 0.03),
        case("gradients", "radial_offset_gradient_bars", 0.02),
        case("gradients", "symbol_radial_gradient", 0.002),
        case("vl-convert", "bar_chart_trellis_compact", 0.02),
        case("vl-convert", "circle_binned", 0.03),
        case("vl-convert", "circle_binned_base_url", 0.03),
        case("vl-convert", "custom_projection", 0.001),
        case("vl-convert", "float_font_size", 0.01),
        case("vl-convert", "font_with_quotes", 0.02),
        case("vl-convert", "line_with_log_scale", 0.02),
        case("vl-convert", "long_legend_label", 0.01),
        case("vl-convert", "lookup_urls", 0.01),
        case("vl-convert", "numeric_font_weight", 0.02),
        case("vl-convert", "quakes_initial_selection", 0.01),
        case("vl-convert", "remote_images", 0.01),
        case("vl-convert", "seattle-weather", 0.01),
        case("vl-convert", "stocks_locale", 0.01),
        case("vl-convert", "table_heatmap", 0.04),
        case("vl-convert", "stacked_bar_h", 0.02),
        case("vl-convert", "geoScale", 0.01),
        case("vl-convert", "maptile_background", 0.01),
        case("vl-convert", "no_text_in_font_metrics", 0.03),
        case("clip", "clip_mixed_marks", 0.0001),
        case("clip", "text_clip", 0.02),
        case("clip", "clip_rounded", 0.0001),
        case("clip", "text_clip_rounded", 0.02),
        case("clip", "bar_rounded", 0.02),
    )]
    fn test_image_baseline(category: &str, spec_name: &str, tolerance: f64) {
        initialize();

        println!("{spec_name}");
        let specs_dir = format!(
            "{}/../avenger-vega-test-data/vega-scenegraphs/{category}",
            env!("CARGO_MANIFEST_DIR")
        );
        let output_dir = format!("{}/tests/output", env!("CARGO_MANIFEST_DIR"));
        fs::create_dir_all(Path::new(&output_dir)).unwrap();

        // Read scene graph spec
        let scene_spec_str =
            fs::read_to_string(format!("{specs_dir}/{spec_name}.sg.json")).unwrap();
        let scene_spec: VegaSceneGraph = serde_json::from_str(&scene_spec_str).unwrap();
        // println!("{scene_spec:#?}");

        // Read expected png
        let expected_dssim = dssim::load_image(
            &Dssim::new(),
            Path::new(&format!("{specs_dir}/{spec_name}.png")),
        )
        .ok()
        .unwrap();

        // Build scene graph
        let scene_graph: SceneGraph = scene_spec
            .to_scene_graph()
            .expect("Failed to parse scene graph");

        // println!("{}", serde_json::to_string_pretty(&scene_graph).unwrap());

        let mut png_canvas = pollster::block_on(PngCanvas::new(
            CanvasDimensions {
                size: [scene_graph.width, scene_graph.height],
                scale: 2.0,
            },
            Default::default(),
        ))
        .unwrap();
        png_canvas.set_scene(&scene_graph).unwrap();
        let img = pollster::block_on(png_canvas.render()).expect("Failed to render PNG image");
        let result_path = format!("{output_dir}/{category}-{spec_name}.png");
        img.save(&result_path).unwrap();
        let result_dssim = dssim::load_image(&Dssim::new(), result_path).unwrap();

        // Compare images
        let attr = Dssim::new();
        let (diff, _) = attr.compare(&expected_dssim, result_dssim);
        println!("{diff}");
        assert!(diff < tolerance);
    }

    #[test]
    fn test_marker() {} // Help IDE detect test module
}