All files / utils directions.ts

100% Statements 64/64
100% Branches 27/27
100% Functions 8/8
100% Lines 56/56

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262        94x 94x                       94x 94x                       94x       328x 202x     126x 26x     100x 39x   61x                                     94x       474x   474x 37x 437x 61x 376x 126x 250x 61x 189x 120x     69x                                           94x     41x 2x     39x                                                         94x     27x           21x 2x     19x 19x 31x 31x         17x       19x 4x 16x 6x         19x               94x   82x             94x 7x             94x 157x             94x     13x           94x                     94x           94x                   94x                 94x                    
/**
 * @module Utils
 */
 
import * as MC from "@lisn/globals/minification-constants";
import * as MH from "@lisn/globals/minification-helpers";
 
import {
  Direction,
  XYDirection,
  ZDirection,
  NoDirection,
  AmbiguousDirection,
  CommaSeparatedStr,
  Vector,
} from "@lisn/globals/types";
 
import { maxAbs, areParallel } from "@lisn/utils/math";
import { isValidStrList, validateStrList } from "@lisn/utils/validation";
 
/**
 * Returns the cardinal direction in the XY plane for the larger of the two
 * deltas (horizontal vs vertical).
 *
 * If both deltas are 0, returns "none".
 *
 * If both deltas are equal and non-0, returns "ambiguous".
 *
 * @category Directions
 */
export const getMaxDeltaDirection = (
  deltaX: number,
  deltaY: number,
): XYDirection | NoDirection | AmbiguousDirection => {
  if (!MH.abs(deltaX) && !MH.abs(deltaY)) {
    return MC.S_NONE;
  }
 
  if (MH.abs(deltaX) === MH.abs(deltaY)) {
    return MC.S_AMBIGUOUS;
  }
 
  if (MH.abs(deltaX) > MH.abs(deltaY)) {
    return deltaX < 0 ? MC.S_LEFT : MC.S_RIGHT;
  }
  return deltaY < 0 ? MC.S_UP : MC.S_DOWN;
};
 
/**
 * Returns the approximate direction of the given 2D vector as one of the
 * cardinal (XY plane) ones: "up", "down", "left" or "right"; or "ambiguous".
 *
 * @param angleDiffThreshold See {@link areParallel} or
 *                           {@link Utils.areAntiParallel | areAntiParallel}.
 *                           This determines whether the inferred direction is
 *                           ambiguous. For it to _not_ be ambiguous it must
 *                           align with one of the four cardinal directions to
 *                           within `angleDiffThreshold`. It doesn't make
 *                           sense for this value to be < 0 or >= 45 degrees.
 *                           If it is, it's forced to be positive (absolute)
 *                           and <= 44.99.
 *
 * @category Directions
 */
export const getVectorDirection = (
  vector: Vector,
  angleDiffThreshold = 0,
): XYDirection | AmbiguousDirection | NoDirection => {
  angleDiffThreshold = MH.min(44.99, MH.abs(angleDiffThreshold));
 
  if (!maxAbs(...vector)) {
    return MC.S_NONE;
  } else if (areParallel(vector, [1, 0], angleDiffThreshold)) {
    return MC.S_RIGHT;
  } else if (areParallel(vector, [0, 1], angleDiffThreshold)) {
    return MC.S_DOWN;
  } else if (areParallel(vector, [-1, 0], angleDiffThreshold)) {
    return MC.S_LEFT;
  } else if (areParallel(vector, [0, -1], angleDiffThreshold)) {
    return MC.S_UP;
  }
 
  return MC.S_AMBIGUOUS;
};
 
/**
 * Returns the opposite direction to the given direction or null if the given
 * direction has no opposite.
 *
 * @example
 * ```javascript
 * getOppositeDirection("up"); // -> "down"
 * getOppositeDirection("down"); // -> "up"
 * getOppositeDirection("left"); // -> "right"
 * getOppositeDirection("right"); // -> "left"
 * getOppositeDirection("none"); // -> null
 * getOppositeDirection("ambiguous"); // -> null
 * ```
 *
 * @category Directions
 *
 * @throws {@link Errors.LisnUsageError | LisnUsageError}
 *                If the given view is not valid.
 */
export const getOppositeDirection = (
  direction: Direction,
): Direction | null => {
  if (!(direction in OPPOSITE_DIRECTIONS)) {
    throw MH.usageError("Invalid 'direction'");
  }
 
  return OPPOSITE_DIRECTIONS[direction];
};
 
/**
 * Returns the set of directions which are opposite to the given set of directions.
 *
 * There are two sets of opposite pairs ("up"/"down" and "left"/"right") and at
 * least one of the two opposing directions of a pair must be present for the
 * other one to be included. If both directions that constitute a pair of
 * opposites is given, then the other pair is returned instead (minus any that
 * are present in the input). See examples below for clarification.
 *
 * @example
 * ```javascript
 * getOppositeXYDirections("up"); // -> ["down"]
 * getOppositeXYDirections("left"); // -> ["right"]
 * getOppositeXYDirections("up,down"); // -> ["left","right"]
 * getOppositeXYDirections("up,left"); // -> ["down","right"]
 * getOppositeXYDirections("up,left,right"); // -> ["down"]
 * getOppositeXYDirections("none"); // -> throws
 * getOppositeXYDirections("ambiguous"); // -> throws
 * getOppositeXYDirections("in"); // -> throws
 * ```
 *
 * @category Directions
 *
 * @throws {@link Errors.LisnUsageError | LisnUsageError}
 *                If the given view is not valid.
 */
export const getOppositeXYDirections = (
  directions: CommaSeparatedStr<XYDirection> | XYDirection[],
): XYDirection[] => {
  const directionList = validateStrList(
    "directions",
    directions,
    isValidXYDirection,
  );
 
  if (!directionList) {
    throw MH.usageError("'directions' is required");
  }
 
  const opposites: XYDirection[] = [];
  for (const direction of directionList) {
    const opposite = getOppositeDirection(direction);
    if (
      opposite &&
      isValidXYDirection(opposite) &&
      !MH.includes(directionList, opposite)
    ) {
      opposites.push(opposite);
    }
  }
 
  if (!MH.lengthOf(opposites)) {
    for (const direction of XY_DIRECTIONS) {
      if (!MH.includes(directionList, direction)) {
        opposites.push(direction);
      }
    }
  }
 
  return opposites;
};
 
/**
 * Returns true if the given direction is one of the known XY ones.
 *
 * @category Validation
 */
export const isValidXYDirection = (
  direction: string,
): direction is XYDirection => MH.includes(XY_DIRECTIONS, direction);
 
/**
 * Returns true if the given direction is one of the known Z ones.
 *
 * @category Validation
 */
export const isValidZDirection = (direction: string): direction is ZDirection =>
  MH.includes(Z_DIRECTIONS, direction);
 
/**
 * Returns true if the given string is a valid direction.
 *
 * @category Validation
 */
export const isValidDirection = (direction: string): direction is Direction =>
  MH.includes(DIRECTIONS, direction);
 
/**
 * Returns true if the given string or array is a list of valid directions.
 *
 * @category Validation
 */
export const isValidDirectionList = (
  directions: string | string[],
): directions is CommaSeparatedStr<Direction> | Direction[] =>
  isValidStrList(directions, isValidDirection, false);
 
/**
 * @ignore
 * @internal
 */
export const XY_DIRECTIONS = [
  MC.S_UP,
  MC.S_DOWN,
  MC.S_LEFT,
  MC.S_RIGHT,
] as const;
 
/**
 * @ignore
 * @internal
 */
export const Z_DIRECTIONS = [MC.S_IN, MC.S_OUT] as const;
 
/**
 * @ignore
 * @internal
 */
export const SCROLL_DIRECTIONS = [
  ...XY_DIRECTIONS,
  MC.S_NONE,
  MC.S_AMBIGUOUS,
] as const;
 
/**
 * @ignore
 * @internal
 */
export const DIRECTIONS = [
  ...XY_DIRECTIONS,
  ...Z_DIRECTIONS,
  MC.S_NONE,
  MC.S_AMBIGUOUS,
] as const;
 
// --------------------
 
const OPPOSITE_DIRECTIONS = {
  [MC.S_UP]: MC.S_DOWN,
  [MC.S_DOWN]: MC.S_UP,
  [MC.S_LEFT]: MC.S_RIGHT,
  [MC.S_RIGHT]: MC.S_LEFT,
  [MC.S_IN]: MC.S_OUT,
  [MC.S_OUT]: MC.S_IN,
  [MC.S_NONE]: null,
  [MC.S_AMBIGUOUS]: null,
} as const;