|
| 1 | +import _ from "underscore"; |
| 2 | +import mathjs from "mathjs"; |
| 3 | + |
| 4 | +import { tolerance as TOLERANCE } from "./constants"; |
| 5 | + |
| 6 | +/** |
| 7 | + * @summary Zero threshold. Numbers below it are put to zero exactly. |
| 8 | + * Used to avoid math.js bug in treating zero as X.XXe-16. |
| 9 | + */ |
| 10 | +const EPSILON = 1e-8; |
| 11 | +/** |
| 12 | + * @summary Returns scalar product of vectors |
| 13 | + * @param v1 {Number[]} Vector 1 |
| 14 | + * @param v2 {Number[]} Vector 2 |
| 15 | + * @return {Number} |
| 16 | + */ |
| 17 | + |
| 18 | +const product = (v1, v2) => { |
| 19 | + return math.multiply(v1, math.transpose(v2)); |
| 20 | +}; |
| 21 | + |
| 22 | +/** |
| 23 | + * @summary Returns length of a vector. |
| 24 | + * @param v {Number[]} Vector |
| 25 | + * @return {Number} |
| 26 | + */ |
| 27 | +const vlen = (v) => { |
| 28 | + return math.sqrt(product(v, v)); |
| 29 | +}; |
| 30 | + |
| 31 | +/** |
| 32 | + * @summary Returns angle between `a` and `b` vectors. |
| 33 | + * @param a {Number[]} Vector a |
| 34 | + * @param b {Number[]} Vector b |
| 35 | + * @param [unit] {String} `rad`, `deg` |
| 36 | + * @return {Number} |
| 37 | + */ |
| 38 | +const angle = (a, b, unit) => { |
| 39 | + const lenA = vlen(a); |
| 40 | + const lenB = vlen(b); |
| 41 | + return math.unit(math.acos(product(a, b) / (lenA * lenB)), "rad").toNumber(unit || "deg"); |
| 42 | +}; |
| 43 | + |
| 44 | +const angleUpTo90 = (...args) => { |
| 45 | + const angleUpTo180 = angle(...args); |
| 46 | + return angleUpTo180 < 90 ? angleUpTo180 : 180 - angleUpTo180; |
| 47 | +}; |
| 48 | + |
| 49 | +/** |
| 50 | + * @summary Returns distance between 2 vectors. |
| 51 | + * @param v1 {Number[]} Vector |
| 52 | + * @param v2 {Number[]} Vector |
| 53 | + * @return {Number} |
| 54 | + */ |
| 55 | +const vDist = (v1, v2) => { |
| 56 | + if (v1.length !== v2.length) { |
| 57 | + console.error( |
| 58 | + "Attempting to calculate distance between vectors of different dimensionality", |
| 59 | + ); |
| 60 | + return; |
| 61 | + } |
| 62 | + return vlen(v1.map((coordinate, index) => coordinate - v2[index])); |
| 63 | +}; |
| 64 | + |
| 65 | +/** |
| 66 | + * @summary Returns checks whether 2 vector are equal within tolerance. |
| 67 | + * @param vec1 {Number[]} Vector |
| 68 | + * @param vec2 {Number[]} Vector |
| 69 | + * @param tolerance {Number} Tolerance |
| 70 | + * @return {Number} |
| 71 | + */ |
| 72 | +const vEqualWithTolerance = (vec1, vec2, tolerance = TOLERANCE.pointsDistance) => |
| 73 | + vDist(vec1, vec2) <= tolerance; |
| 74 | + |
| 75 | +/** |
| 76 | + * @summary Returns 0 if passed number is less than Made.math.EPSILON. |
| 77 | + * @param n {Number} |
| 78 | + * @return {Number} |
| 79 | + */ |
| 80 | +const roundToZero = (n) => { |
| 81 | + return Math.abs(n) < EPSILON ? 0 : n; |
| 82 | +}; |
| 83 | + |
| 84 | +/** |
| 85 | + * @summary Returns number with specified precision. |
| 86 | + * @param x {Number} |
| 87 | + * @param n {Number} |
| 88 | + * @return {Number} |
| 89 | + */ |
| 90 | +const precise = (x, n = 7) => { |
| 91 | + return Number(x.toPrecision(n)); |
| 92 | +}; |
| 93 | + |
| 94 | +/** |
| 95 | + * @summary Returns mod of the passed value with the specified tolerance. |
| 96 | + * @param num {Number} |
| 97 | + * @param tolerance {Number} |
| 98 | + * @return {Number} |
| 99 | + */ |
| 100 | +const mod = (num, tolerance = 0.001) => { |
| 101 | + const m = num % 1; |
| 102 | + const x = num >= 0 ? m : 1 + m; |
| 103 | + |
| 104 | + if (math.smallerEq(Math.abs(x - 1), tolerance) || math.smallerEq(Math.abs(x), tolerance)) { |
| 105 | + return 0; |
| 106 | + } |
| 107 | + return x; |
| 108 | +}; |
| 109 | + |
| 110 | +/** |
| 111 | + * @summary Returns cartesian of passed arrays. |
| 112 | + * @example combinations([1,2], [4,5], [6]) = [[1,4,6], [1,5,6], [2,4,6], [2,5,6]]; |
| 113 | + */ |
| 114 | +const cartesianProduct = (...arg) => { |
| 115 | + const r = []; |
| 116 | + const max = arg.length - 1; |
| 117 | + |
| 118 | + const helper = (arr, i) => { |
| 119 | + for (let j = 0, l = arg[i].length; j < l; j++) { |
| 120 | + const a = arr.slice(0); // clone arr |
| 121 | + a.push(arg[i][j]); |
| 122 | + if (i === max) { |
| 123 | + r.push(a); |
| 124 | + } else { |
| 125 | + helper(a, i + 1); |
| 126 | + } |
| 127 | + } |
| 128 | + }; |
| 129 | + |
| 130 | + helper([], 0); |
| 131 | + return r; |
| 132 | +}; |
| 133 | + |
| 134 | +/** |
| 135 | + * @summary Returns all possible positive integer combinations where each value changes from 0 to a, b, c. |
| 136 | + * @param a {Number} |
| 137 | + * @param b {Number} |
| 138 | + * @param tolerance {Number} |
| 139 | + */ |
| 140 | +const almostEqual = (a, b, tolerance = TOLERANCE.pointsDistance) => { |
| 141 | + return Math.abs(a - b) < tolerance; |
| 142 | +}; |
| 143 | + |
| 144 | +/** |
| 145 | + * @summary Returns true if number is 0 <= x < 1, inclusive, otherwise false. |
| 146 | + * Helper to deal with JS arithmetic artifacts. |
| 147 | + * @number number {Number} |
| 148 | + */ |
| 149 | +const isBetweenZeroInclusiveAndOne = (number, tolerance = TOLERANCE.length) => { |
| 150 | + return roundToZero(number) >= 0 && !almostEqual(number, 1, tolerance) && number < 1; |
| 151 | +}; |
| 152 | + |
| 153 | +/** |
| 154 | + * @summary Returns all possible positive integer combinations where each value changes from 0 to a, b, c. |
| 155 | + * @example |
| 156 | + * var comb = combinations(1, 2, 0); |
| 157 | + * // [[0, 0, 0], [0, 1, 0], [0, 2, 0], [1, 0, 0], [1, 1, 0], [1, 2, 0]] |
| 158 | + * @param a |
| 159 | + * @param b |
| 160 | + * @param c |
| 161 | + */ |
| 162 | +const combinations = (a, b, c) => { |
| 163 | + const combs = []; |
| 164 | + for (let i = 0; i <= a; i++) { |
| 165 | + for (let j = 0; j <= b; j++) { |
| 166 | + for (let k = 0; k <= c; k++) { |
| 167 | + combs.push([i, j, k]); |
| 168 | + } |
| 169 | + } |
| 170 | + } |
| 171 | + return combs; |
| 172 | +}; |
| 173 | + |
| 174 | +/* |
| 175 | + * @summary Same as `combinations` but accepting intervals (tuples) of integers: eg. [-3, 4] |
| 176 | + */ |
| 177 | +const combinationsFromIntervals = (arrA, arrB, arrC) => { |
| 178 | + const combs = []; |
| 179 | + for (let i = arrA[0]; i <= arrA[1]; i++) { |
| 180 | + for (let j = arrB[0]; j <= arrB[1]; j++) { |
| 181 | + for (let k = arrC[0]; k <= arrC[1]; k++) { |
| 182 | + combs.push([i, j, k]); |
| 183 | + } |
| 184 | + } |
| 185 | + } |
| 186 | + return combs; |
| 187 | +}; |
| 188 | + |
| 189 | +const roundValueToNDecimals = (value, decimals = 3) => { |
| 190 | + return parseFloat(value.toFixed(decimals)); |
| 191 | +} |
| 192 | + |
| 193 | +/** |
| 194 | + * @summary Returns n splits of the passed segment. |
| 195 | + * @param point1 {Number[]} |
| 196 | + * @param point2 {Number[]} |
| 197 | + * @param n {Number} |
| 198 | + */ |
| 199 | +const calculateSegmentsBetweenPoints3D = (point1, point2, n) => { |
| 200 | + // safely parse if passed strings |
| 201 | + const point1_ = point1.map(x => parseFloat(x)); |
| 202 | + const point2_ = point2.map(x => parseFloat(x)); |
| 203 | + const n_ = parseInt(n) |
| 204 | + |
| 205 | + const result = []; |
| 206 | + for (let i = 1; i < n_; i++) { |
| 207 | + const lambda = i / (n_ - i); |
| 208 | + result.push([ |
| 209 | + (point1_[0] + lambda * point2_[0]) / (1 + lambda), |
| 210 | + (point1_[1] + lambda * point2_[1]) / (1 + lambda), |
| 211 | + (point1_[2] + lambda * point2_[2]) / (1 + lambda) |
| 212 | + ]); |
| 213 | + } |
| 214 | + return result; |
| 215 | +} |
| 216 | + |
| 217 | +/** |
| 218 | + * @summary Wrapper for native [Number.toPrecision](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_objects/Number/toPrecision) method. |
| 219 | + * Returns a string representing the Number object to the specified precision. |
| 220 | + * @memberOf Helpers |
| 221 | + * @package exabyte:core |
| 222 | + * @locus Client |
| 223 | + * @method |
| 224 | + * @name toPrecision |
| 225 | + * @param {Number} number |
| 226 | + * @param {Number} precision Optional. An integer specifying the number of significant digits. |
| 227 | + */ |
| 228 | +export function numberToPrecision(number, precision) { |
| 229 | + |
| 230 | + if (_.isNumber(number)) { |
| 231 | + return number.toPrecision(precision); |
| 232 | + } |
| 233 | + return number; |
| 234 | +} |
| 235 | + |
| 236 | +export const math = { |
| 237 | + ...mathjs, |
| 238 | + PI: Math.PI, |
| 239 | + trunc: Math.trunc, |
| 240 | + product, |
| 241 | + vlen, |
| 242 | + angle, |
| 243 | + angleUpTo90, |
| 244 | + vDist, |
| 245 | + vEqualWithTolerance, |
| 246 | + roundToZero, |
| 247 | + precise, |
| 248 | + mod, |
| 249 | + isBetweenZeroInclusiveAndOne, |
| 250 | + cartesianProduct, |
| 251 | + almostEqual, |
| 252 | + combinations, |
| 253 | + combinationsFromIntervals, |
| 254 | + calculateSegmentsBetweenPoints3D, |
| 255 | + roundValueToNDecimals, |
| 256 | + numberToPrecision, |
| 257 | +}; |
0 commit comments