R/delaunay.R

Defines functions surface sandwichedFacet volume vertexNeighborFacets plotDelaunay3D plotDelaunay2D getDelaunaySimplicies getDelaunaySimplices delaunay volume_under_triangle exteriorDelaunayEdges

Documented in delaunay getDelaunaySimplices getDelaunaySimplicies plotDelaunay2D plotDelaunay3D surface volume

#' @importFrom cxhull cxhull
#' @importFrom hash keys
#' @noRd
exteriorDelaunayEdges <- function(tessellation){
  tilefacets <- tessellation[["tilefacets"]]
  points <- attr(tessellation, "points")
  exteriorFacets <- Filter(Negate(sandwichedFacet), tilefacets)
  edges <- NULL
  prec <- sqrt(.Machine[["double.eps"]])
  for(tilefacet in exteriorFacets){
    normal <- tilefacet[["normal"]]
    offset <- tilefacet[["offset"]]
    # center <- tilefacet[["subsimplex"]][["circumcenter"]]
    # if(abs(crossprod(center, normal) + offset) >= sqrt(.Machine$double.eps)){
    #   normal <- -normal
    #   cat("orientation:\n")
    #
    #   tileparent <- tess$tiles[[tilefacet[["facetOf"]]]]
    #
    #   print(tileparent[["orientation"]])
    # }else{
    #   cat("nochaneg\n")
    # }
    ids <- sort(as.integer(keys(tilefacet[["subsimplex"]][["vertices"]])))
    pt1 <- points[ids[1L], ]
    pt2 <- points[ids[2L], ]
    if(
      abs(crossprod(pt1, normal) + offset) < prec
      && abs(crossprod(pt2, normal) + offset) < prec
    ){
      edges <- rbind(
        edges,
        c(ids[1L], ids[2L]),
        c(ids[2L], ids[3L]),
        c(ids[1L], ids[3L])
      )
    }
  }
  edges <- unique(edges)
  hullfacets <- cxhull(points)[["facets"]]
  edges3 <- list()
  vertices <- matrix(nrow = 0L, ncol = 3L)
  rnames <- integer(0L)
  for(i in 1L:nrow(edges)){
    edge <- edges[i, ]
    idA <- edge[1L]
    A <- points[idA, ]
    x <- vapply(hullfacets, function(f){
      c(crossprod(f[["normal"]], A)) + f[["offset"]]
    }, numeric(1L))
    Abelongs <- which(abs(x) < prec)
    idB <- edge[2L]
    B <- points[idB, ]
    if(length(Abelongs)){
      x <- vapply(Abelongs, function(j){
        f <- hullfacets[[j]]
        c(crossprod(f[["normal"]], B)) + f[["offset"]]
      }, numeric(1L))
      if(any(abs(x) < prec)){
        edges3 <-
          append(edges3, list(Edge3$new(A = A, B = B, idA = idA, idB = idB)))
        vertices <- rbind(vertices, A, B)
        rnames <- c(rnames, c(idA, idB))
      }
    }
  }
  # edges <- unique(do.call(rbind, lapply(exteriorFacets, function(f){
  #   ids <- sort(as.integer(keys(f[["subsimplex"]][["vertices"]])))
  #   rbind(
  #     c(ids[1L], ids[2L]),
  #     c(ids[2L], ids[3L]),
  #     c(ids[1L], ids[3L])
  #   )
  # })))
  # A_Bs <- apply(edges, 1L, function(edge){
  #   paste0(edge[1L], "-", edge[2L])
  # })
  # print(A_Bs)
  # unique_edges <- edges[which(table(A_Bs) == 1L), ]
  # print(unique_edges)
  #  edges < unique(edges)
  o <- order(rnames)
  vertices <- vertices[o, ]
  rownames(vertices) <- as.character(rnames[o])
  # vertices <- points[unique(c(edges)), ]
  # nedges <- nrow(edges)
  # edges3 <- vector("list", length = nedges)
  # for(i in 1L:nedges){
  #   edge <- edges[i,]
  #   edges3[[i]] <-
  #     Edge3$new(A = points[edge[1L], ], B = points[edge[2L], ])
  # }
  attr(edges3, "vertices") <- unique(vertices)
  edges3
}

volume_under_triangle <- function(x, y, z){
  sum(z) *
    (x[1L]*y[2L] - x[2L]*y[1L] + x[2L]*y[3L] -
       x[3L]*y[2L] + x[3L]*y[1L] - x[1L]*y[3L]) / 6
}

#' @title Delaunay triangulation
#' @description Delaunay triangulation (or tessellation) of a set of points.
#'
#' @param points the points given as a matrix, one point per row
#' @param atinfinity Boolean, whether to include a point at infinity;
#'   ignored if \code{elevation=TRUE}
#' @param degenerate Boolean, whether to include degenerate tiles;
#'   ignored if \code{elevation=TRUE}
#' @param exteriorEdges Boolean, for dimension 3 only, whether to return
#'   the exterior edges (see below)
#' @param elevation Boolean, only for three-dimensional points; if \code{TRUE},
#'   the function performs an elevated Delaunay triangulation (also called 2.5D
#'   Delaunay triangulation), using the third coordinate of a point as its
#'   elevation; see the example
#'
#' @return If the function performs an elevated Delaunay tessellation, then
#'   the returned value is a list with four fields: \code{mesh}, \code{edges},
#'   \code{volume}, and \code{surface}. The \code{mesh} field is an object of
#'   class \code{mesh3d}, ready for plotting with the \strong{rgl} package. The
#'   \code{edges} field is an integer matrix which provides the indices of the
#'   vertices of the edges, and an indicator of whether an edge is a border
#'   edge; this matrix is obtained with \code{\link[Rvcg]{vcgGetEdge}}.
#'   The \code{volume} field provides the sum of the
#'   volumes under the Delaunay triangles, that is to say the total volume
#'   under the triangulated surface. Finally, the \code{surface} field provides
#'   the sum of the areas of the Delaunay triangles, thus this is an approximate
#'   value of the area of the surface that is triangulated.
#'   The elevated Delaunay tessellation is built with the help of the
#'   \strong{interp} package.
#'
#' Otherwise, the function returns the Delaunay tessellation with many details,
#'   in a list. This list contains five fields:
#' \describe{
#'   \item{\emph{vertices}}{the vertices (or sites) of the tessellation; these
#'   are the points passed to the function}
#'   \item{\emph{tiles}}{the tiles of the tessellation (triangles in dimension
#'   2, tetrahedra in dimension 3)}
#'   \item{\emph{tilefacets}}{the facets of the tiles of the tessellation}
#'   \item{\emph{mesh}}{a 'rgl' mesh (\code{\link[rgl]{mesh3d}} object)}
#'   \item{\emph{edges}}{a two-columns integer matrix representing the edges,
#'   each row represents an edge; the two integers of a row are the indices of
#'   the two points which form the edge.}
#' }
#' In dimension 3, the list contains an additional field \emph{exteriorEdges}
#'   if you set \code{exteriorEdges = TRUE}. This is the list of the exterior
#'   edges, represented as \code{\link{Edge3}} objects. This field is involved
#'   in the function \code{\link{plotDelaunay3D}}.
#'
#' The \strong{vertices} field is a list with the following fields:
#' \describe{
#'   \item{\emph{id}}{the id of the vertex; this is nothing but the index of
#'   the corresponding point passed to the function}
#'   \item{\emph{neighvertices}}{the ids of the vertices of the tessellation
#'   connected to this vertex by an edge}
#'   \item{\emph{neightilefacets}}{the ids of the tile facets this vertex
#'   belongs to}
#'   \item{\emph{neightiles}}{the ids of the tiles this vertex belongs to}
#' }
#' The \strong{tiles} field is a list with the following fields:
#' \describe{
#'   \item{\emph{id}}{the id of the tile}
#'   \item{\emph{simplex}}{a list describing the simplex (that is, the tile);
#'   this list contains four fields: \emph{vertices}, a
#'   \code{\link[hash]{hash}} giving the simplex vertices and their id,
#'   \emph{circumcenter}, the circumcenter of the simplex, \emph{circumradius},
#'   the circumradius of the simplex, and \emph{volume}, the volume of the
#'   simplex}
#'   \item{\emph{facets}}{the ids of the facets of this tile}
#'   \item{\emph{neighbors}}{the ids of the tiles adjacent to this tile}
#'   \item{\emph{family}}{two tiles have the same family if they share the
#'   same circumcenter; in this case the family is an integer, and the family is
#'   \code{NA} for tiles which do not share their circumcenter with any other
#'   tile}
#'   \item{\emph{orientation}}{\code{1} or \code{-1}, an indicator of the
#'   orientation of the tile}
#' }
#' The \strong{tilefacets} field is a list with the following fields:
#' \describe{
#'   \item{\emph{id}}{the id of this tile facet}
#'   \item{\emph{subsimplex}}{a list describing the subsimplex (that is, the
#'   tile facet); this list is similar to the \emph{simplex} list of
#'   \strong{tiles}}
#'   \item{\emph{facetOf}}{one or two ids, the id(s) of the tile this facet
#'   belongs to}
#'   \item{\emph{normal}}{a vector, the normal of the tile facet}
#'   \item{\emph{offset}}{a number, the offset of the tile facet}
#' }
#'
#' @export
#' @useDynLib tessellation, .registration = TRUE
#' @importFrom hash hash keys
#' @importFrom rgl tmesh3d
#' @importFrom Rvcg vcgGetEdge
#'
#' @note The package provides the functions \code{\link{plotDelaunay2D}} to
#'   plot a 2D Delaunay tessellation and \code{\link{plotDelaunay3D}} to
#'   plot a 3D Delaunay tessellation. But there is no function to plot an
#'   elevated Delaunay tessellation; the examples show how to plot such a
#'   Delaunay tessellation.
#'
#' @seealso \code{\link{getDelaunaySimplices}}
#'
#' @examples library(tessellation)
#' points <- rbind(
#'  c(0.5,0.5,0.5),
#'  c(0,0,0),
#'  c(0,0,1),
#'  c(0,1,0),
#'  c(0,1,1),
#'  c(1,0,0),
#'  c(1,0,1),
#'  c(1,1,0),
#'  c(1,1,1)
#' )
#' del <- delaunay(points)
#' del$vertices[[1]]
#' del$tiles[[1]]
#' del$tilefacets[[1]]
#'
#' # an elevated Delaunay tessellation ####
#' f <- function(x, y){
#'   dnorm(x) * dnorm(y)
#' }
#' x <- y <- seq(-5, 5, length.out = 50)
#' grd <- expand.grid(x = x, y = y) # grid on the xy-plane
#' points <- as.matrix(transform( # data (x_i, y_i, z_i)
#'   grd, z = f(x, y)
#' ))
#' del <- delaunay(points, elevation = TRUE)
#' del[["volume"]] # close to 1, as expected
#' # plotting
#' library(rgl)
#' mesh <- del[["mesh"]]
#' open3d(windowRect = c(100, 100, 612, 356), zoom = 0.6)
#' aspect3d(1, 1, 20)
#' shade3d(mesh, color = "limegreen", polygon_offset = 1)
#' wire3d(mesh)
#'
#' # another elevated Delaunay triangulation, to check the correctness
#' #   of the calculated surface and the calculated volume ####
#' library(Rvcg)
#' library(rgl)
#' cap <- vcgSphericalCap(angleRad = pi/2, subdivision = 3)
#' open3d(windowRect = c(100, 100, 612, 356), zoom = 0.6)
#' shade3d(cap, color = "lawngreen", polygon_offset = 1)
#' wire3d(cap)
#' # exact value of the surface of the spherical cap:
#' R <- 1
#' h <- R * (1 - sin(pi/2/2))
#' 2 * pi * R * h
#' # our approximation:
#' points <- t(cap$vb[-4, ]) # the points on the spherical cap
#' del <- delaunay(points, elevation = TRUE)
#' del[["surface"]]
#' # try to increase `subdivision` in `vcgSphericalCap` to get a
#' #   better approximation of the true value
#' # note that 'Rvcg' returns the same result as ours:
#' vcgArea(cap)
#' # let's check the volume as well:
#' pi * h^2 * (R - h/3) # true value
#' del[["volume"]]
#' # there's a warning with 'Rvcg':
#' tryCatch(vcgVolume(cap), warning = function(w) message(w))
#' suppressWarnings({vcgVolume(cap)})
delaunay <- function(
  points, atinfinity = FALSE, degenerate = FALSE, exteriorEdges = FALSE,
  elevation = FALSE
){
  stopifnot(isBoolean(atinfinity))
  stopifnot(isBoolean(degenerate))
  stopifnot(isBoolean(exteriorEdges))
  stopifnot(isBoolean(elevation))
  if(!is.matrix(points) || !is.numeric(points)){
    stop("The `points` argument must be a numeric matrix.", call. = TRUE)
  }
  dimension <- ncol(points)
  if(dimension < 2L){
    stop("The dimension must be at least 2.", call. = TRUE)
  }
  if(nrow(points) <= dimension){
    stop("Insufficient number of points.", call. = TRUE)
  }
  if(any(is.na(points))){
    stop("Points with missing values are not allowed.", call. = TRUE)
  }
  if(elevation){
    if(dimension != 3L){
      stop(
        "To get an elevated Delaunay tessellation (`elevation=TRUE`), ",
        "you have to provide three-dimensional points.",
        call. = TRUE
      )
    }
    del <- delaunay(
      points[, c(1L, 2L)], atinfinity = FALSE, degenerate = FALSE,
      exteriorEdges = FALSE, elevation = FALSE
    )

    # x <- points[, 1L]
    # y <- points[, 2L]
    # x <- (x - min(x)) / diff(range(x))
    # y <- (y - min(y)) / diff(range(y))
    # #xy <- points[, c(1L, 2L)]
    # o <- order(round(x+y, 6L), y-x)
    # xy <- cbind(x, y)[o, ]
    # if(anyDuplicated(xy)){
    #   stop("There are some duplicated points.", call. = TRUE)
    # }
    # points <- points[o, ]
    # # xy <- round(points[, c(1L, 2L)], 6)
    # Triangles <- triangles(tri.mesh(xy[, 1L], xy[, 2L]))

    delVertices <- del[["vertices"]]
    ids <- vapply(delVertices, `[[`, integer(1L), "id")

    #vertices <- do.call(cbind, lapply(delVertices, `[[`, "point"))

    vertices <- points[ids, ]

    Triangles <- do.call(rbind, lapply(del[["tiles"]], function(tile){
      indices <- tile[["vertices"]]
      if(tile[["orientation"]] == -1L){
        indices <- indices[c(2L, 1L, 3L)]
      }
      indices
    }))
    # Triangles <- Triangles[, 1L:3L]
    # vertices <- points
    mesh <- tmesh3d(
      vertices = t(vertices),
      indices = t(Triangles),
      homogeneous = FALSE
    )
    # edges <- t(vapply(del[["tilefacets"]], function(x){
    #   as.integer(keys(x[["subsimplex"]][["vertices"]]))
    # }, numeric(2L)))
    # volumes <- apply(Triangles, 1L, function(trgl){
    #   trgl <- vertices[trgl, ]
    #   volume_under_triangle(trgl[, 1L], trgl[, 2L], trgl[, 3L])
    # })
    # areas <- apply(Triangles, 1L, function(trgl){
    #   trgl <- vertices[trgl, ]
    #   triangleArea(trgl[1L, ], trgl[2L, ], trgl[3L, ])
    # })
    volumes_and_areas <- apply(Triangles, 1L, function(trgl){
      trgl <- vertices[trgl, ]
      c(
        volume_under_triangle(trgl[, 1L], trgl[, 2L], trgl[, 3L]),
        triangleArea(trgl[1L, ], trgl[2L, ], trgl[3L, ])
      )
    })
    out <- list(
      "mesh"    = mesh,
      "edges"   = `colnames<-`(
        as.matrix(vcgGetEdge(mesh))[, -3L], c("v1", "v2", "border")
      ),
      "volume"  = sum(volumes_and_areas[1L, ]),
      "surface" = sum(volumes_and_areas[2L, ])
    )
    attr(out, "elevation") <- TRUE
    class(out) <- "delaunay"
    return(out)
  }
  if(anyDuplicated(points)){
    stop("There are some duplicated points.", call. = TRUE)
  }
  storage.mode(points) <- "double"
  errfile <- tempfile(fileext = ".txt")
  tess <- tryCatch({
    .Call(
      "delaunay_",
      points,
      as.integer(atinfinity),
      as.integer(degenerate),
      0,
      errfile
    )
  }, error = function(e){
    try(cat(readLines(errfile), sep="\n"), silent = TRUE)
    stop(e)
  })
  pointsAsList <- lapply(1L:nrow(points), function(i) points[i, ])
  tiles <- tess[["tiles"]]
  for(i in seq_along(tiles)){
    tile <- tiles[[i]]
    simplex <- tile[["simplex"]]
    vertices <- simplex[["vertices"]]
    tess[["tiles"]][[i]] <- c(
      list("vertices" = vertices),
      tile
    )
    tess[["tiles"]][[i]][["simplex"]][["vertices"]] <-
      hash(as.character(vertices), pointsAsList[vertices])
  }
  tilefacets <- tess[["tilefacets"]]
  if(dimension == 3L){
    nTriangles <- length(tilefacets)
    Triangles <- matrix(NA_integer_, nrow = nTriangles, ncol = 3L)
    for(i in 1L:nTriangles){
      subsimplex <- tilefacets[[i]][["subsimplex"]]
      vertices <- Triangles[i, ] <- subsimplex[["vertices"]]
      tess[["tilefacets"]][[i]][["subsimplex"]][["vertices"]] <-
        hash(as.character(vertices), pointsAsList[vertices])
    }
  }else{
    for(i in seq_along(tilefacets)){
      subsimplex <- tilefacets[[i]][["subsimplex"]]
      vertices <- subsimplex[["vertices"]]
      tess[["tilefacets"]][[i]][["subsimplex"]][["vertices"]] <-
        hash(as.character(vertices), pointsAsList[vertices])
    }
  }
  attr(tess, "points") <- points
  if(dimension == 3L && exteriorEdges){
    tess[["exteriorEdges"]] <- exteriorDelaunayEdges(tess)
  }
  if(dimension == 2L){
    attr(tess[["tiles"]], "info") <-
      "Dimension 2. Tiles are triangles. A simplex volume is a triangle area."
    attr(tess[["tilefacets"]], "info") <- paste0(
      "Dimension 2. Tile facets are the edges of the triangles. ",
      "A subsimplex volume is nothing but the length of an edge."
    )
  }else if(dimension == 3L){
    tess[["mesh"]] <- mesh <- tmesh3d(
      vertices = t(points),
      indices = t(Triangles)
    )
    tess[["edges"]] <- `colnames<-`(
      as.matrix(vcgGetEdge(mesh))[, c(1L, 2L)],
      c("v1", "v2")
    )
    attr(tess[["tiles"]], "info") <-
      "Dimension 3. Tiles are tetrahedra."
    attr(tess[["tilefacets"]], "info") <- paste0(
      "Dimension 3. Tile facets are the triangles. ",
      "A subsimplex volume is nothing but the area of a triangle."
    )
  }
  class(tess) <- "delaunay"
  tess
}

#' @title Delaunay simplices
#' @description Get Delaunay simplices (tiles).
#'
#' @param tessellation the output of \code{\link{delaunay}}
#' @param hashes Boolean, whether to return the simplices as hash maps
#'
#' @return The list of simplices of the Delaunay tessellation.
#' @export
#' @importFrom hash values
#' @name getDelaunaySimplices
#' @rdname getDelaunaySimplices
#'
#' @examples library(tessellation)
#' pts <- rbind(
#'   c(-5, -5,  16),
#'   c(-5,  8,   3),
#'   c(4,  -1,   3),
#'   c(4,  -5,   7),
#'   c(4,  -1, -10),
#'   c(4,  -5, -10),
#'   c(-5,  8, -10),
#'   c(-5, -5, -10)
#' )
#' tess <- delaunay(pts)
#' getDelaunaySimplices(tess)
getDelaunaySimplices <- function(tessellation, hashes = FALSE){
  stopifnot(isBoolean(hashes))
  if(!inherits(tessellation, "delaunay")){
    stop(
      "The argument `tessellation` must be an output of the `delaunay` function.",
      call. = TRUE
    )
  }
  if(isTRUE(attr(tessellation, "elevation"))){
    stop(
      "This function is not conceived for elevated Delaunay tessellations.",
      call. = TRUE
    )
  }
  simplices <-
    lapply(lapply(tessellation[["tiles"]], `[[`, "simplex"), `[[`, "vertices")
  if(!hashes){
    simplices <- lapply(simplices, function(simplex) t(values(simplex)))
  }
  simplices
}

#' @export
#' @name getDelaunaySimplices
#' @rdname getDelaunaySimplices
getDelaunaySimplicies <- function(tessellation, hashes = FALSE) {
  getDelaunaySimplices(tessellation, hashes)
}


#' @title Plot 2D Delaunay tessellation
#' @description Plot a 2D Delaunay tessellation.
#'
#' @param tessellation the output of \code{\link{delaunay}}
#' @param border the color of the borders of the triangles; \code{NULL} for
#'   no borders
#' @param color controls the filling colors of the triangles, either
#'   \code{FALSE} for no color, \code{"random"} to use
#'   \code{\link[colorsGen]{randomColor}}, or \code{"distinct"} to use
#'   \code{\link[Polychrome]{createPalette}}
#' @param distinctArgs if \code{color = "distinct"}, a list of arguments
#'   passed to \code{\link[Polychrome]{createPalette}}
#' @param randomArgs if \code{color = "random"}, a list of arguments passed
#'   to \code{\link[colorsGen]{randomColor}}
#' @param lty,lwd graphical parameters
#' @param ... arguments passed to \code{\link{plot}}
#'
#' @return No value, just renders a 2D plot.
#' @export
#' @importFrom hash keys values
#' @importFrom graphics plot polygon par segments
#'
#' @examples # random points in a square
#' set.seed(314)
#' library(tessellation)
#' library(uniformly)
#' square <- rbind(
#'   c(-1, 1), c(1, 1), c(1, -1), c(-1, -1)
#' )
#' ptsin <- runif_in_cube(10L, d = 2L)
#' pts <- rbind(square, ptsin)
#' d <- delaunay(pts)
#' opar <- par(mar = c(0, 0, 0, 0))
#' plotDelaunay2D(
#'   d, xlab = NA, ylab = NA, asp = 1, color = "random",
#'   randomArgs = list(hue = "random", luminosity = "dark")
#' )
#' par(opar)
plotDelaunay2D <- function(
  tessellation, border = "black", color = "distinct",
  distinctArgs = list(seedcolors = c("#ff0000", "#00ff00", "#0000ff")),
  randomArgs = list(hue = "random", luminosity = "bright"),
  lty = par("lty"), lwd = par("lwd"), ...
){
  if(!inherits(tessellation, "delaunay")){
    stop(
      "The argument `tessellation` must be an output of the `delaunay` function.",
      call. = TRUE
    )
  }
  if(isTRUE(attr(tessellation, "elevation"))){
    stop(
      "This function is not conceived for elevated Delaunay tessellations.",
      call. = TRUE
    )
  }
  vertices <- attr(tessellation, "points")
  if(ncol(vertices) != 2L){
    stop(
      sprintf("Invalid dimension (%d instead of 2).", ncol(vertices)),
      call. = TRUE
    )
  }
  plot(vertices, type = "n", ...)
  if(!isFALSE(color)){
    color <- match.arg(color, c("random", "distinct"))
    simplices <- getDelaunaySimplices(tessellation, hashes = TRUE)
    nsimplices <- length(simplices)
    if(color == "random") {
      colors <- rcolors(nsimplices, randomArgs)
    } else {
      colors <- distinctColors(nsimplices, distinctArgs)
    }
    for(i in 1L:nsimplices){
      triangle <- t(values(simplices[[i]]))
      polygon(triangle, border = NA, col = colors[i])
    }
  }
  if(!is.null(border)){
    edges <- lapply(tessellation[["tilefacets"]], function(tilefacet){
      as.integer(keys(tilefacet[["subsimplex"]][["vertices"]]))
    })
    # cat("nedges:\n")
    # print(length(edges))
    # edges <- uniqueWith(edges, sameSegments)
    # print(length(edges))
    for(edge in edges){
      p0 <- vertices[edge[1L], ]
      p1 <- vertices[edge[2L], ]
      segments(
        p0[1L], p0[2L], p1[1L], p1[2L], col = border, lty = lty, lwd = lwd
      )
    }
  }
}


#' @title Plot 3D Delaunay tessellation
#' @description Plot a 3D Delaunay tessellation with \strong{rgl}.
#'
#' @param tessellation the output of \code{\link{delaunay}}
#' @param color controls the filling colors of the tetrahedra, either
#'   \code{FALSE} for no color, \code{"random"} to use
#'   \code{\link[colorsGen]{randomColor}}, or \code{"distinct"} to use
#'   \code{\link[Polychrome]{createPalette}}
#' @param distinctArgs if \code{color = "distinct"}, a list of arguments
#'   passed to \code{\link[Polychrome]{createPalette}}
#' @param randomArgs if \code{color = "random"}, a list of arguments passed
#'   to \code{\link[colorsGen]{randomColor}}
#' @param alpha opacity, number between 0 and 1
#' @param exteriorEdgesAsTubes Boolean, whether to plot the exterior edges
#'   as tubes; in order to use this feature, you need to set
#'   \code{exteriorEdges = TRUE} in the \code{\link{delaunay}} function
#' @param tubeRadius if \code{exteriorEdgesAsTubes = TRUE}, the radius of
#'   the tubes
#' @param tubeColor if \code{exteriorEdgesAsTubes = TRUE}, the color of
#'   the tubes
#'
#' @return No value, just renders a 3D plot.
#' @export
#' @importFrom utils combn
#' @importFrom rgl triangles3d spheres3d
#' @importFrom hash keys values
#'
#' @examples library(tessellation)
#' pts <- rbind(
#'   c(-5, -5,  16),
#'   c(-5,  8,   3),
#'   c(4,  -1,   3),
#'   c(4,  -5,   7),
#'   c(4,  -1, -10),
#'   c(4,  -5, -10),
#'   c(-5,  8, -10),
#'   c(-5, -5, -10)
#' )
#' tess <- delaunay(pts)
#' library(rgl)
#' open3d(windowRect = c(50, 50, 562, 562))
#' plotDelaunay3D(tess, color = "random")
#' \donttest{open3d(windowRect = c(50, 50, 562, 562))
#' plotDelaunay3D(
#'   tess, exteriorEdgesAsTubes = TRUE, tubeRadius = 0.3, tubeColor = "yellow"
#' )}
plotDelaunay3D <- function(
  tessellation, color = "distinct",
  distinctArgs = list(seedcolors = c("#ff0000", "#00ff00", "#0000ff")),
  randomArgs = list(hue = "random", luminosity = "bright"),
  alpha = 0.3, exteriorEdgesAsTubes = FALSE, tubeRadius, tubeColor
){
  stopifnot(isBoolean(exteriorEdgesAsTubes))
  if(!inherits(tessellation, "delaunay")){
    stop(
      "The argument `tessellation` must be an output of the `delaunay` function.",
      call. = TRUE
    )
  }
  if(isTRUE(attr(tessellation, "elevation"))){
    stop(
      "This function is not conceived for elevated Delaunay tessellations.",
      call. = TRUE
    )
  }
  vertices <- attr(tessellation, "points")
  if(ncol(vertices) != 3L){
    stop(
      sprintf("Invalid dimension (%d instead of 3).", ncol(vertices)),
      call. = TRUE
    )
  }
  # simplices <- getDelaunaySimplices(tessellation, hashes = TRUE)
  # # edges <- unique(do.call(rbind, lapply(simplices, function(simplex){
  # #   t(combn(as.integer(keys(simplex)), 2L))
  # # })))
  # nsimplices <- length(simplices)
  if(!isFALSE(color)) {
    color <- match.arg(color, c("random", "distinct"))
    simplices <- getDelaunaySimplices(tessellation, hashes = TRUE)
    nsimplices <- length(simplices)
    if(color == "random") {
      colors <- rcolors(nsimplices, randomArgs)
    } else {
      colors <- distinctColors(nsimplices, distinctArgs)
    }
    triangles <- combn(4L, 3L)
    for(i in 1L:nsimplices) {
      simplex <- t(values(simplices[[i]]))
      for(j in 1L:4L) {
        triangles3d(simplex[triangles[, j], ], color = colors[i], alpha = alpha)
      }
    }
  }
  edges <- tessellation[["edges"]]
  for(i in 1L:nrow(edges)){
    edge <- edges[i, ]
    p1 <- vertices[edge[1L], ]
    p2 <- vertices[edge[2L], ]
    lines3d(rbind(p1, p2), color = "black")
  }
  if(exteriorEdgesAsTubes){
    if(!"exteriorEdges" %in% names(tessellation)){
      warning(
        "You didn't set the option `exteriorEdges=TRUE` in the `delaunay` ",
        "function, therefore the option `exteriorEdgesAsTubes` is ignored."
      )
    }
    edges3 <- tessellation[["exteriorEdges"]]
    for(edge3 in edges3){
      edge3$plot(
        edgeAsTube = TRUE, tubeRadius = tubeRadius, tubeColor = tubeColor
      )
    }
    spheres3d(
      attr(edges3, "vertices"), radius = 1.5*tubeRadius, color = tubeColor
    )
  }
}

#' tile facets a vertex belongs to
#' @noRd
vertexNeighborFacets <- function(tessellation, vertexId){
  vertex <- tessellation[["vertices"]][[vertexId]]
  neighs <- vertex[["neightilefacets"]]
  tessellation[["tilefacets"]][neighs]
}

#' @title Tessellation volume
#' @description The volume of the Delaunay tessellation, that is, the volume of
#'   the convex hull of the sites.
#'
#' @param tessellation output of \code{\link{delaunay}}
#'
#' @return A number, the volume of the Delaunay tessellation (area in 2D).
#' @seealso \code{\link{surface}}
#' @export
volume <- function(tessellation){
  if(!inherits(tessellation, "delaunay")){
    stop(
      "The argument `tessellation` must be an output of the `delaunay` function.",
      call. = TRUE
    )
  }
  if(isTRUE(attr(tessellation, "elevation"))){
    stop(
      "This function is not conceived for elevated Delaunay tessellations.",
      call. = TRUE
    )
  }
  tileVolumes <- vapply(tessellation[["tiles"]], function(tile){
    tile[["simplex"]][["volume"]]
  }, numeric(1L))
  sum(tileVolumes)
}

sandwichedFacet <- function(tilefacet){
  length(tilefacet[["facetOf"]]) == 2L
}

#' @title Tessellation surface
#' @description Exterior surface of the Delaunay tessellation.
#'
#' @param tessellation output of \code{\link{delaunay}}
#'
#' @return A number, the exterior surface of the Delaunay tessellation
#'   (perimeter in 2D).
#' @seealso \code{\link{volume}}
#' @note It is not guaranteed that this function provides the correct result
#'   for all cases. The exterior surface of the Delaunay tessellation is the
#'   exterior surface of the convex hull of the sites (the points), and you can
#'   get it with the \strong{cxhull} package (by summing the volumes of the
#'   facets). Moreover, I encountered some cases for which I got a correct
#'   result only with the option \code{degenerate=TRUE} in the
#'   \code{delaunay} function. I will probably remove this function in the
#'   next version.
#' @export
surface <- function(tessellation){
  if(!inherits(tessellation, "delaunay")){
    stop(
      "The argument `tessellation` must be an output of the `delaunay` function.",
      call. = TRUE
    )
  }
  if(isTRUE(attr(tessellation, "elevation"))){
    stop(
      "This function is not conceived for elevated Delaunay tessellations.",
      call. = TRUE
    )
  }
  exteriorFacets <-
    Filter(Negate(sandwichedFacet), tessellation[["tilefacets"]])
  ridgeSurfaces <- vapply(exteriorFacets, function(tilefacet){
    tilefacet[["subsimplex"]][["volume"]]
  }, numeric(1L))
  sum(ridgeSurfaces)
}

Try the tessellation package in your browser

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

tessellation documentation built on May 29, 2024, 11:51 a.m.