R/vector3.R

Defines functions extract_vector3 extract_normal extract_vertical extract_horizontal extract_normal_dimension extract_vertical_dimension extract_horizontal_dimension extract_dimension scale_type.dddr_vector3 pillar_shaft.dddr_vector3 format.dddr_vector3_pillar format_and_align_right vec_ptype_abbr.dddr_vector3 as_vector3.character as_vector3 as.character.dddr_vector3 format.dddr_vector3 paste_vector is.na.dddr_vector3 `$.dddr_vector3` vector3 new_vector3

Documented in as.character.dddr_vector3 as_vector3 as_vector3.character format.dddr_vector3 new_vector3 vector3

#' Internal method for creating a new vector3
#'
#' This follows the pattern recommended in the `vctrs` documentation.
#'
#' @param x,y,z Vector entries, expected to be double vectors
#'
#' @keywords internal
new_vector3 <- function(x = double(), y = double(), z = double()) {
  vctrs::vec_assert(x, ptype = double())
  vctrs::vec_assert(y, ptype = double())
  vctrs::vec_assert(z, ptype = double())
  vctrs::new_rcrd(list(x = x, y = y, z = z), class = "dddr_vector3")
}

#' Create a 3D vector
#'
#' Creates a three-dimensional vector given three vectors representing Cartesian
#' coordinates.
#'
#' @param x,y,z Numeric vectors representing the the vector's coordinates in the
#'   specified dimension.
#'
#' @export
vector3 <- function(x, y, z) {
  l <- vctrs::vec_cast_common(x, y, z, .to = double())
  l <- vctrs::vec_recycle_common(l[[1]], l[[2]], l[[3]])
  new_vector3(l[[1]], l[[2]], l[[3]])
}

#' Vector field access
#'
#' In order to access fields within each vector, the `$` operator is used.
#'
#' @param x Vector3 whose fields to access
#' @param name Field name to access. For vectors, this should be one of "x",
#'   "y", or "z". For quaternions, this can include "w" as well.
#'
#' @name field_access
#' @examples
#' vector3(x = 1:4, y = 2:5, z = 3:6)$y
NULL

#' @rdname field_access
#' @export
`$.dddr_vector3` <- function(x, name) {
  # should xyz conventions be a setting?
  # should all fields be accessible?
  if (name %in% c("x", "y", "z")) {
    return(vctrs::field(x, name))
  } else {
    return(NULL)
  }
}

#' @method is.na dddr_vector3
#' @export
is.na.dddr_vector3 <- function(x) {
  is.na(x$x) | is.na(x$y) | is.na(x$z)
}


#' Vector3 Conversions to and from Character
#'
#' @param x an object to be converted
#' @param ... arguments passed along to underlying methods `as.character` or
#'   `format`
#' @param digits number of decimal digits to produce
#' @param regex regular expression used to parse. The three groups should produce x, y, and z values in order.
#'
#' @name vec3_character
NULL


#' @keywords internal
paste_vector <- function(x, y, z) {
  paste0("(", x, ", ", y, ", ", z, ")")
}

#' @rdname vec3_character
#' @export
format.dddr_vector3 <- function(x, ..., digits = 4) {
  v <- x # don't overwrite the name

  out <- paste_vector(
    format(v$x, ..., digits = digits),
    format(v$y, ..., digits = digits),
    format(v$z, ..., digits = digits)
  )

  out
}

#' @rdname vec3_character
#' @method as.character dddr_vector3
#' @export
as.character.dddr_vector3 <- function(x, ...) {
  paste_vector(
    as.character(x$x, ...),
    as.character(x$y, ...),
    as.character(x$z, ...)
  )
}

#' @export
#' @rdname vec3_character
as_vector3 <- function(x, ...) {
  UseMethod("as_vector3", x)
}

#' @method as_vector3 character
#' @export
#' @rdname vec3_character
as_vector3.character <- function(x, ..., regex = "\\((.*),(.*),(.*)\\)") {
  # NAs are acceptable...
  regex_results <- regexec(regex, x)
  regex_matches <- regmatches(x, regex_results)
  vector3(
    x = as.numeric(sapply(regex_matches, function(m) m[[2]])),
    y = as.numeric(sapply(regex_matches, function(m) m[[3]])),
    z = as.numeric(sapply(regex_matches, function(m) m[[4]]))
  )
}


#' @importFrom vctrs vec_ptype_abbr
#' @method vec_ptype_abbr dddr_vector3
#' @export
vec_ptype_abbr.dddr_vector3 <- function(x, ...) {
  "vct3"
}

format_and_align_right <- function(x, width) {
  x <- format(x, width = width)
  extent <- pillar::get_extent(x)
  spaces <- pmax(width - extent, 0L)
  paste0(strrep(" ", spaces), x)
}

#' @method format dddr_vector3_pillar
#' @export
format.dddr_vector3_pillar <- function(x, width, ...) {
  # if width is maximum, everyone gets the width they request.
  if (width >= attr(x, "width")) {
    out <- paste0(
      "(",
      format_and_align_right(x$pillar_x, width = attr(x$pillar_x, "width")),
      ", ",
      format_and_align_right(x$pillar_y, width = attr(x$pillar_y, "width")),
      ", ",
      format_and_align_right(x$pillar_z, width = attr(x$pillar_z, "width")),
      ")"
    )
  } else {
    # Pillar guarantees width >= min_width

    l <- list(x$pillar_x, x$pillar_y, x$pillar_z)
    to_claim <- unlist(lapply(l, function(a) {
      attr(a, "width") - attr(a, "min_width")
    }))
    total_to_claim <- sum(to_claim)

    need_to_claim <- (width - attr(x, "min_width"))
    claim_rate <- need_to_claim / total_to_claim
    claimed <- claim_rate * to_claim

    claimed_int <- as.integer(floor(claimed))
    claimed_frac <- claimed - claimed_int

    remaining <- round(sum(claimed_frac))
    additions <- rank(claimed_frac, ties.method = "random") > (3 - remaining)

    min_width <- unlist(lapply(l, function(a) {
      attr(a, "min_width")
    }))

    total_spaces <- additions + claimed_int + min_width

    out <- paste0(
      "(",
      format_and_align_right(x$pillar_x, width = total_spaces[[1]]),
      ", ",
      format_and_align_right(x$pillar_y, width = total_spaces[[2]]),
      ", ",
      format_and_align_right(x$pillar_z, width = total_spaces[[3]]),
      ")"
    )
  }

  attr(out, "align") <- "right"
  out
}

#' @importFrom pillar pillar_shaft
#' @method pillar_shaft dddr_vector3
#' @export
pillar_shaft.dddr_vector3 <- function(v) {

  pillar_x <- pillar::pillar_shaft(v$x)
  pillar_y <- pillar::pillar_shaft(v$y)
  pillar_z <- pillar::pillar_shaft(v$z)

  width <- as.integer(sum(
    attr(pillar_x, "width"),
    attr(pillar_y, "width"),
    attr(pillar_z, "width"),
    6 # the 6 represents 2 parens, 2 spaces, and 2 commas.
  ))

  min_width <- as.integer(sum(
    attr(pillar_x, "min_width"),
    attr(pillar_y, "min_width"),
    attr(pillar_z, "min_width"),
    6 # ditto
  ))

  out <- list(pillar_x = pillar_x, pillar_y = pillar_y, pillar_z = pillar_z)
  class(out) <- c("dddr_vector3_pillar", class(out))
  attr(out, "width") <- width
  attr(out, "min_width") <- min_width
  out
}

#' @importFrom ggplot2 scale_type
#' @method scale_type dddr_vector3
#' @export
scale_type.dddr_vector3 <- function(x) "identity"

#' @keywords internal
extract_dimension <- function(v, direction) {
  cartesian_direction <- get_axes_semantics()[[direction]]
  axis <- substr(cartesian_direction, 2, 2)
  `$.dddr_vector3`(v, axis)
}

#' @keywords internal
extract_horizontal_dimension <- function(view) {
    # if I'm looking at the front, it means forward is pointing towards me.
    # Up is on the top, as usual.
    # I need another function to call the right extract functions.
  switch(view,
    "at front" = "left", # positive X (in ggplot space) is to the left of the object.
    "at back" = "right",
    "at top" = "left",
    "at bottom" = "left",
    "at port" = "backward",
    "at starboard" = "forward",
    #"at left", "" conventions...
    NA
  )
}

#' @keywords internal
extract_vertical_dimension <- function(view) {
  switch(view,
         "at front" = "up",
         "at back" = "up",
         "at top" = "backward",
         "at bottom" = "forward",
         "at port" = "up",
         "at starboard" = "up",
         NA
  )
}

#' @keywords internal
extract_normal_dimension <- function(view) {
  switch(view,
         "at front" = "forward",
         "at back" = "backward",
         "at top" = "up",
         "at bottom" = "down",
         "at port" = "left",
         "at starboard" = "right",
         NA
  )
}

#' @keywords internal
extract_horizontal <- function(v) {
  dimension <- extract_horizontal_dimension(attr(v, "view"))
  extract_dimension(v, dimension)
}

#' @keywords internal
extract_vertical <- function(v) {
  dimension <- extract_vertical_dimension(attr(v, "view"))
  extract_dimension(v, dimension)
}

#' @keywords internal
extract_normal <- function(v) {
  dimension <- extract_normal_dimension(attr(v, "view"))
  extract_dimension(v, dimension)
}


#' @keywords internal
extract_vector3 <- function(data) {
  if (is.null(attr(data$v, "view"))) {
    rlang::abort(
      message = "No view direction specified. Did you forget to specify a `coord_*` system?",
      class = "dddr_semantics"
    )
  }
  # TODO: This needs a better name.
  data$x <- extract_horizontal(data$v)
  data$y <- extract_vertical(data$v)
  data$depth <- extract_normal(data$v)
  data
}
MrMallIronmaker/dddr documentation built on May 11, 2022, 8:39 p.m.