tests/testthat/test-scimesh_bridge.R

# Tests for the scimesh renderer backend bridge and the shared plain-R
# geometry transform helpers.

test_that("rotation.matrix matches rgl::rotationMatrix.", {
    angles <- c(0, pi / 2, pi, 0.3, -1.2);
    axes <- list(c(1, 0, 0), c(0, 1, 0), c(0, 0, 1), c(1, 1, 1), c(0.3, -2, 0.5));
    for (a in angles) {
        for (ax in axes) {
            m1 <- rotation.matrix(a, ax[1], ax[2], ax[3]);
            m2 <- rgl::rotationMatrix(a, ax[1], ax[2], ax[3]);
            expect_equal(m1, m2, tolerance = 1e-12);
        }
    }
});


test_that("transform_renderable rotates matrices and mesh3d objects (incl. normals).", {
    M <- rotation.matrix(pi / 2, 0, 0, 1);
    pts <- rbind(c(1, 2, 3), c(4, 5, 6), c(0, 0, 1));
    expect_equal(transform_renderable(pts, M), rgl::rotate3d(pts, pi / 2, 0, 0, 1), tolerance = 1e-12);

    m <- rgl::tetrahedron3d();
    rot <- transform_renderable(m, M);
    expect_equal(rot$vb, rgl::rotate3d(m, pi / 2, 0, 0, 1)$vb, tolerance = 1e-12);

    m2 <- rgl::tetrahedron3d();
    m2$normals <- m2$vb;
    m2$normals[4, ] <- 0;
    rot2 <- transform_renderable(m2, M);
    expect_equal(rot2$normals[1:3, ], t(M[1:3, 1:3]) %*% m2$normals[1:3, ], tolerance = 1e-12);
});


test_that("color_to_rgba converts hex and named colors.", {
    expect_equal(color_to_rgba("#FF0000"), c(1, 0, 0, 1));
    expect_equal(color_to_rgba("#FF0000FF"), c(1, 0, 0, 1));
    expect_equal(color_to_rgba("white"), c(1, 1, 1, 1));
});


test_that("color_to_rgba handles NA as fully transparent.", {
    expect_equal(color_to_rgba(NA_character_), c(0, 0, 0, 0));
});


test_that("apply.style.alpha extracts alpha from style parameters.", {
    expect_equal(apply.style.alpha(list(alpha = 0.4)), 0.4);
    expect_equal(apply.style.alpha(list(shininess = 50)), 1.0);
});


test_that("view.angle.to.hemi.filter maps views to hemispheres.", {
    expect_equal(view.angle.to.hemi.filter("lateral_lh"), "lh");
    expect_equal(view.angle.to.hemi.filter("sd_medial_rh"), "rh");
    expect_equal(view.angle.to.hemi.filter("dorsal"), "both");
    expect_equal(view.angle.to.hemi.filter("caudal"), "both");
});


test_that("get.rglstyle.parameters resolves 'from_mesh' correctly.", {
    r <- structure(list(style = "glass"), class = "fs.coloredmesh");
    expect_equal(get.rglstyle.parameters(r, "from_mesh")$alpha, 0.4);

    r2 <- structure(list(), class = "fs.coloredmesh");
    expect_equal(get.rglstyle.parameters(r2, "from_mesh")$shininess, 50);
});


test_that("get.fsbrain.renderer.backend defaults to 'rgl'.", {
    old <- getOption("fsbrain.renderer_backend");
    on.exit(options(fsbrain.renderer_backend = old));
    options(fsbrain.renderer_backend = NULL);
    expect_equal(get.fsbrain.renderer.backend(), "rgl");
});


test_that("get.fsbrain.scimesh.output.dims defaults to 1920x1080.", {
    old <- getOption("fsbrain.scimesh.output_dims");
    on.exit(options(fsbrain.scimesh.output_dims = old));
    options(fsbrain.scimesh.output_dims = NULL);
    expect_equal(get.fsbrain.scimesh.output.dims(), c(1920L, 1080L));
});


test_that("coloredmesh_to_scimesh applies per-mesh alpha from 'from_mesh'.", {
    testthat::skip_if_not_installed("scimesh");
    mesh <- rgl::tetrahedron3d();
    cm <- structure(list(mesh = mesh, col = "#FF0000", render = TRUE,
                         style = list(alpha = 0.4)), class = "fs.coloredmesh");
    sm <- coloredmesh_to_scimesh(cm, "from_mesh");
    expect_equal(as.numeric(sm$colors[1, "A"]), 0.4);
});


test_that("scimesh can render a synthetic scene headlessly.", {
    testthat::skip_if_not_installed("scimesh");
    mesh <- rgl::tetrahedron3d();
    cm <- structure(list(mesh = mesh, col = "#FF0000", render = TRUE), class = "fs.coloredmesh");
    scene <- coloredmeshes_to_scimesh(cm);
    expect_equal(length(scene), 1L);

    cam <- scimesh::camera(eye = c(0, 0, 5), center = c(0, 0, 0), projection = "orthographic");
    opts <- scimesh::render_options(width = 32L, height = 32L, background_color = c(1, 1, 1, 1));
    img <- scimesh::render_scene(scene, cam, opts);
    out <- tempfile(fileext = ".png");
    scimesh::write_png(img, out);
    expect_true(file.exists(out));
    expect_gt(file.size(out), 0);
});


# --- Pure unit tests (no scimesh required) ------------------------------------

test_that("hex_colors_to_rgba_matrix produces Nx4 RGBA matrices.", {
    m <- hex_colors_to_rgba_matrix(c("#FF0000", "#00FF00", "#0000FF"));
    expect_equal(dim(m), c(3L, 4L));
    expect_equal(colnames(m), c("R", "G", "B", "A"));
    expect_equal(unname(m[1, ]), c(1, 0, 0, 1));
    expect_equal(unname(m[2, ]), c(0, 1, 0, 1));
    expect_equal(unname(m[3, ]), c(0, 0, 1, 1));
});


test_that("hex_colors_to_rgba_matrix maps NA colors to fully transparent rows.", {
    m <- hex_colors_to_rgba_matrix(c("#FF0000", NA_character_, "#00FF00"));
    expect_equal(unname(m[2, ]), c(0, 0, 0, 0));
});


test_that("hex_colors_to_rgba_matrix errors on non-character input.", {
    expect_error(hex_colors_to_rgba_matrix(c(1, 2, 3)));
});


test_that("filter_scene_by_view selects hemispheres from a hemilist scene.", {
    scene <- list(lh = "mesh_lh", rh = "mesh_rh");
    expect_equal(filter_scene_by_view(scene, "both"), list("mesh_lh", "mesh_rh"));
    expect_equal(filter_scene_by_view(scene, "lh"), list("mesh_lh"));
    expect_equal(filter_scene_by_view(scene, "rh"), list("mesh_rh"));
});


test_that("filter_scene_by_view handles empty and non-hemilist scenes.", {
    expect_equal(filter_scene_by_view(list(), "both"), list());
    expect_equal(filter_scene_by_view(list(lh = "mesh_lh"), "rh"), list());
    unnamed <- list("a", "b");
    expect_equal(filter_scene_by_view(unnamed, "both"), unnamed);
});


test_that("transform_coords applies 4x4 transforms to coordinates.", {
    M <- rotation.matrix(pi / 2, 0, 0, 1);
    pts <- rbind(c(1, 2, 3), c(4, 5, 6), c(0, 0, 1));
    expect_equal(transform_coords(pts, M), rgl::rotate3d(pts, pi / 2, 0, 0, 1), tolerance = 1e-12);

    expect_equal(transform_coords(NULL, M), NULL);
    expect_equal(transform_coords(c(1, 0, 0), M),
                 rgl::rotate3d(matrix(c(1, 0, 0), ncol = 3L), pi / 2, 0, 0, 1),
                 tolerance = 1e-12);

    expect_error(transform_coords(cbind(1, 2), M));   # not an Nx3 matrix
});


test_that("view.angle.to.hemi.filter errors on invalid angles.", {
    expect_error(view.angle.to.hemi.filter("not_a_view"));
});


# --- scimesh-gated unit tests (headless, no interactive plotting) -------------

test_that("view_angle_to_scimesh_camera maps views to cameras and hemisphere filters.", {
    testthat::skip_if_not_installed("scimesh");
    mesh <- rgl::tetrahedron3d();
    cm <- structure(list(mesh = mesh, col = "#FF0000", render = TRUE), class = "fs.coloredmesh");
    scene <- coloredmeshes_to_scimesh(list(lh = cm, rh = cm));

    expected <- c("lateral_lh" = "lh", "medial_lh" = "lh", "lateral_rh" = "rh",
                  "medial_rh" = "rh", "dorsal" = "both", "ventral" = "both",
                  "rostral" = "both", "caudal" = "both");
    for (v in names(expected)) {
        res <- view_angle_to_scimesh_camera(scene, v);
        expect_equal(res$hemi_filter, expected[[v]]);
        expect_true(all(c("eye", "center", "up", "projection", "fov") %in% names(res$camera)));
    }
});


test_that("view_angle_to_scimesh_camera errors on invalid views and empty scenes.", {
    testthat::skip_if_not_installed("scimesh");
    mesh <- rgl::tetrahedron3d();
    cm <- structure(list(mesh = mesh, col = "#FF0000", render = TRUE), class = "fs.coloredmesh");
    scene <- coloredmeshes_to_scimesh(list(lh = cm, rh = cm));
    expect_error(view_angle_to_scimesh_camera(scene, "not_a_view"));
    expect_error(view_angle_to_scimesh_camera(list(), "dorsal"));
});


test_that("fsbrain_style_to_scimesh_options maps styles to render options.", {
    testthat::skip_if_not_installed("scimesh");

    opts_default <- fsbrain_style_to_scimesh_options("default");
    expect_false(isTRUE(opts_default$wireframe));
    expect_true(isTRUE(opts_default$backface_culling));
    expect_equal(opts_default$width, 800L);
    expect_equal(opts_default$height, 600L);
    expect_equal(opts_default$background_color, c(1, 1, 1, 1));
    expect_equal(opts_default$specular_color, c(0, 0, 0, 1));
    expect_equal(opts_default$shininess, 50);

    opts_edges <- fsbrain_style_to_scimesh_options("edges");
    expect_true(isTRUE(opts_edges$wireframe));

    opts_custom <- fsbrain_style_to_scimesh_options("default", bg_rgba = c(0, 0, 0, 1), width = 100L, height = 50L);
    expect_equal(opts_custom$width, 100L);
    expect_equal(opts_custom$height, 50L);
    expect_equal(opts_custom$background_color, c(0, 0, 0, 1));
});


test_that("highlight_points_to_scimesh creates sphere meshes per point.", {
    testthat::skip_if_not_installed("scimesh");

    expect_equal(highlight_points_to_scimesh(list()), list());

    rglactions <- list("highlight_points" = list("coords" = rbind(c(0, 0, 0), c(1, 1, 1)),
                                                 "color" = "#FF0000", "radius" = 2,
                                                 "hemi" = c("lh", "lh")));
    expect_equal(length(highlight_points_to_scimesh(rglactions, "both")), 2L);
    expect_equal(length(highlight_points_to_scimesh(rglactions, "lh")), 2L);
    expect_equal(length(highlight_points_to_scimesh(rglactions, "rh")), 0L);

    # Defaults: a bare vector of coordinates gets the default color and radius.
    bare <- list("highlight_points" = list("coords" = c(0, 0, 0)));
    expect_equal(length(highlight_points_to_scimesh(bare, "both")), 1L);
});


test_that("coloredmeshes_to_scimesh handles hemilists, render flags, and unnamed lists.", {
    testthat::skip_if_not_installed("scimesh");
    mesh <- rgl::tetrahedron3d();
    cm_on <- structure(list(mesh = mesh, col = "#FF0000", render = TRUE), class = "fs.coloredmesh");
    cm_off <- structure(list(mesh = mesh, col = "#00FF00", render = FALSE), class = "fs.coloredmesh");

    # render=FALSE meshes are skipped.
    scene <- coloredmeshes_to_scimesh(list(lh = cm_off, rh = cm_on));
    expect_equal(names(scene), "rh");

    # Empty lists yield empty scenes.
    expect_equal(coloredmeshes_to_scimesh(list()), list());

    # A single coloredmesh is wrapped under 'single'.
    expect_equal(names(coloredmeshes_to_scimesh(cm_on)), "single");

    # Unnamed lists are flattened into a plain scene list.
    expect_equal(length(coloredmeshes_to_scimesh(list(cm_on, cm_on))), 2L);
});

Try the fsbrain package in your browser

Any scripts or data that you put into this service are public.

fsbrain documentation built on Aug. 23, 2026, 5:06 p.m.