@jarenjs/core 0.9.2 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +229 -26
- package/README.md +64 -10
- package/dist/types/array.d.ts +13 -0
- package/dist/types/cache.d.ts +166 -0
- package/dist/types/chunk.d.ts +102 -0
- package/dist/types/color.d.ts +64 -0
- package/dist/types/convert/convert.d.ts +33 -0
- package/dist/types/convert/currency.d.ts +47 -0
- package/dist/types/convert/index.d.ts +9 -0
- package/dist/types/convert/registry.d.ts +55 -0
- package/dist/types/dates/civil.d.ts +146 -0
- package/dist/types/dates/duration.d.ts +83 -0
- package/dist/types/dates/format.d.ts +59 -0
- package/dist/types/dates/index.d.ts +4 -0
- package/dist/types/{dates.d.ts → dates/rfc3339.d.ts} +57 -3
- package/dist/types/errors.d.ts +77 -0
- package/dist/types/finance/amortization.d.ts +51 -0
- package/dist/types/finance/bond.d.ts +45 -0
- package/dist/types/finance/cashflow.d.ts +44 -0
- package/dist/types/finance/depreciation.d.ts +26 -0
- package/dist/types/finance/index.d.ts +16 -0
- package/dist/types/finance/indicators.d.ts +76 -0
- package/dist/types/finance/interest.d.ts +44 -0
- package/dist/types/finance/returns.d.ts +43 -0
- package/dist/types/finance/tvm.d.ts +50 -0
- package/dist/types/geo/angle.d.ts +4 -0
- package/dist/types/geo/bbox.d.ts +32 -0
- package/dist/types/geo/distance.d.ts +67 -0
- package/dist/types/geo/geohash.d.ts +63 -0
- package/dist/types/geo/geojson.d.ts +108 -0
- package/dist/types/geo/index-tree.d.ts +52 -0
- package/dist/types/geo/index.d.ts +11 -0
- package/dist/types/geo/mercator.d.ts +57 -0
- package/dist/types/geo/predicates.d.ts +44 -0
- package/dist/types/geo/ring.d.ts +80 -0
- package/dist/types/geo/simplify.d.ts +31 -0
- package/dist/types/geo/valid.d.ts +24 -0
- package/dist/types/geo/wkt.d.ts +16 -0
- package/dist/types/math/float64.d.ts +126 -3
- package/dist/types/math/format.d.ts +58 -0
- package/dist/types/math/index.d.ts +5 -0
- package/dist/types/math/mat4.d.ts +60 -0
- package/dist/types/math/project.d.ts +54 -0
- package/dist/types/math/solve.d.ts +67 -0
- package/dist/types/math/word.d.ts +82 -0
- package/dist/types/message.d.ts +52 -0
- package/dist/types/object.d.ts +151 -0
- package/dist/types/scan.d.ts +30 -0
- package/dist/types/schema.d.ts +24 -0
- package/dist/types/string.d.ts +96 -0
- package/dist/types/text/base64.d.ts +6 -4
- package/dist/types/text/email.d.ts +0 -1
- package/dist/types/text/host.d.ts +39 -8
- package/dist/types/text/i18n.d.ts +13 -1
- package/dist/types/text/index.d.ts +1 -0
- package/dist/types/text/misc.d.ts +15 -1
- package/dist/types/text/punycode.d.ts +45 -85
- package/docs/CONVERT.md +45 -0
- package/docs/DATES.md +78 -0
- package/docs/FINANCE.md +59 -0
- package/docs/GEO.md +114 -0
- package/docs/MATH.md +75 -0
- package/package.json +57 -4
- package/src/array.js +16 -0
- package/src/cache.js +206 -0
- package/src/chunk.js +159 -0
- package/src/color.js +125 -0
- package/src/convert/convert.js +59 -0
- package/src/convert/currency.js +74 -0
- package/src/convert/index.js +11 -0
- package/src/convert/registry.js +213 -0
- package/src/dates/civil.js +360 -0
- package/src/dates/duration.js +225 -0
- package/src/dates/format.js +238 -0
- package/src/dates/index.js +30 -0
- package/src/dates/rfc3339.js +621 -0
- package/src/errors.js +102 -0
- package/src/finance/amortization.js +50 -0
- package/src/finance/bond.js +100 -0
- package/src/finance/cashflow.js +122 -0
- package/src/finance/depreciation.js +54 -0
- package/src/finance/index.js +27 -0
- package/src/finance/indicators.js +207 -0
- package/src/finance/interest.js +63 -0
- package/src/finance/returns.js +90 -0
- package/src/finance/tvm.js +94 -0
- package/src/function.js +0 -2
- package/src/geo/angle.js +12 -0
- package/src/geo/bbox.js +80 -0
- package/src/geo/distance.js +155 -0
- package/src/geo/geohash.js +224 -0
- package/src/geo/geojson.js +361 -0
- package/src/geo/index-tree.js +294 -0
- package/src/geo/index.js +52 -0
- package/src/geo/mercator.js +124 -0
- package/src/geo/predicates.js +313 -0
- package/src/geo/ring.js +182 -0
- package/src/geo/simplify.js +130 -0
- package/src/geo/valid.js +142 -0
- package/src/geo/wkt.js +262 -0
- package/src/math/float64.js +228 -11
- package/src/math/format.js +157 -0
- package/src/math/index.js +5 -0
- package/src/math/mat4.js +131 -0
- package/src/math/project.js +49 -0
- package/src/math/solve.js +112 -0
- package/src/math/word.js +177 -0
- package/src/message.js +119 -0
- package/src/object.js +329 -0
- package/src/scan.js +42 -0
- package/src/schema.js +37 -0
- package/src/string.js +189 -0
- package/src/text/base64.js +15 -52
- package/src/text/email.js +0 -5
- package/src/text/host.js +515 -140
- package/src/text/i18n.js +13 -6
- package/src/text/identifiers.js +1 -1
- package/src/text/index.js +1 -0
- package/src/text/misc.js +81 -5
- package/src/text/punycode.js +235 -323
- package/src/dates.js +0 -371
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Number formatting and parsing. The
|
|
4
|
+
* one place the suite formats a number for display: notation control
|
|
5
|
+
* (auto/fixed/sci/eng), thousands grouping and radix output, plus a
|
|
6
|
+
* radix-aware `parseNumber`. Radix I/O delegates to `math/word.js` so
|
|
7
|
+
* base conversion is never re-implemented.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { toBase, fromBase } from './word.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} FormatOptions
|
|
14
|
+
* @property {'auto'|'fixed'|'sci'|'eng'} [notation] default 'auto'
|
|
15
|
+
* @property {number} [precision] significant/decimal digits (notation-dependent)
|
|
16
|
+
* @property {boolean|string} [group] thousands separator: true → ',', or a custom string
|
|
17
|
+
* @property {number} [radix] 2..36; when not 10, `value` is formatted as an integer word
|
|
18
|
+
* @property {string} [decimal] decimal mark, default '.'
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Insert a grouping separator every three digits of an integer string
|
|
23
|
+
* (sign-aware).
|
|
24
|
+
* @param {string} intPart
|
|
25
|
+
* @param {string} sep
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function groupInteger(intPart, sep) {
|
|
29
|
+
let neg = '';
|
|
30
|
+
let s = intPart;
|
|
31
|
+
if (s.startsWith('-')) { neg = '-'; s = s.slice(1); }
|
|
32
|
+
const parts = [];
|
|
33
|
+
for (let i = s.length; i > 0; i -= 3) {
|
|
34
|
+
parts.unshift(s.slice(Math.max(0, i - 3), i));
|
|
35
|
+
}
|
|
36
|
+
return neg + parts.join(sep);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Engineering notation: mantissa in [1, 1000) times 10^(3k).
|
|
41
|
+
* @param {number} value
|
|
42
|
+
* @param {number} precision
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
function toEngineering(value, precision) {
|
|
46
|
+
if (value === 0) return '0e+0';
|
|
47
|
+
const exp = Math.floor(Math.log10(Math.abs(value)));
|
|
48
|
+
const engExp = Math.floor(exp / 3) * 3;
|
|
49
|
+
const mantissa = value / Math.pow(10, engExp);
|
|
50
|
+
const m = precision >= 0 ? mantissa.toFixed(precision) : String(mantissa);
|
|
51
|
+
return `${m}e${engExp >= 0 ? '+' : ''}${engExp}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Format a number for display.
|
|
56
|
+
*
|
|
57
|
+
* `notation: 'auto'` (the default) is NOT a drop-in for a fixed-unit
|
|
58
|
+
* readout: the auto branch escapes to exponential once
|
|
59
|
+
* `abs(value) >= 1e21` or `abs(value) < 1e-6`, so
|
|
60
|
+
* `formatNumber(v, { precision: 3 })` is not interchangeable with a
|
|
61
|
+
* plain `Number(v.toPrecision(3))`. A caller that appends its own unit
|
|
62
|
+
* - `"0.0509 ms"` - wants the value never to become `5.09e-5`, and must
|
|
63
|
+
* either pass `notation: 'fixed'` or keep its own rounder. See
|
|
64
|
+
* packages/core/docs/MATH.md.
|
|
65
|
+
* @param {number} value
|
|
66
|
+
* @param {FormatOptions} [opts]
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
export function formatNumber(value, opts = {}) {
|
|
70
|
+
value = +value;
|
|
71
|
+
if (Number.isNaN(value)) return 'NaN';
|
|
72
|
+
if (value === Infinity) return '∞';
|
|
73
|
+
if (value === -Infinity) return '-∞';
|
|
74
|
+
|
|
75
|
+
const notation = opts.notation ?? 'auto';
|
|
76
|
+
const sep = opts.group === true ? ',' : (typeof opts.group === 'string' ? opts.group : '');
|
|
77
|
+
const decimal = opts.decimal ?? '.';
|
|
78
|
+
|
|
79
|
+
// Radix output: integer word in the requested base.
|
|
80
|
+
if (opts.radix !== undefined && opts.radix !== 10) {
|
|
81
|
+
let str = toBase(Math.trunc(value), opts.radix, { upper: true });
|
|
82
|
+
if (sep) str = groupInteger(str, sep);
|
|
83
|
+
return str;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let out;
|
|
87
|
+
if (notation === 'fixed') {
|
|
88
|
+
out = value.toFixed(opts.precision ?? 2);
|
|
89
|
+
}
|
|
90
|
+
else if (notation === 'sci') {
|
|
91
|
+
out = value.toExponential(opts.precision ?? undefined);
|
|
92
|
+
}
|
|
93
|
+
else if (notation === 'eng') {
|
|
94
|
+
out = toEngineering(value, opts.precision ?? 3);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// auto: plain decimal for human-scale magnitudes, else scientific.
|
|
98
|
+
const abs = Math.abs(value);
|
|
99
|
+
if (value !== 0 && (abs >= 1e21 || abs < 1e-6)) {
|
|
100
|
+
out = opts.precision !== undefined ? value.toExponential(opts.precision) : value.toExponential();
|
|
101
|
+
}
|
|
102
|
+
else if (opts.precision !== undefined) {
|
|
103
|
+
out = String(Number(value.toPrecision(opts.precision)));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
out = String(value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (sep || decimal !== '.') {
|
|
111
|
+
const eIdx = out.search(/[eE]/);
|
|
112
|
+
const exp = eIdx >= 0 ? out.slice(eIdx) : '';
|
|
113
|
+
const mant = eIdx >= 0 ? out.slice(0, eIdx) : out;
|
|
114
|
+
const dot = mant.indexOf('.');
|
|
115
|
+
const intPart = dot >= 0 ? mant.slice(0, dot) : mant;
|
|
116
|
+
const frac = dot >= 0 ? mant.slice(dot + 1) : '';
|
|
117
|
+
out = (sep ? groupInteger(intPart, sep) : intPart)
|
|
118
|
+
+ (frac ? decimal + frac : '')
|
|
119
|
+
+ exp;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse a numeric string, radix-aware. Recognizes `0x`/`0o`/`0b`
|
|
126
|
+
* prefixes, an explicit `radix` option, grouping separators (`,`/`_`/
|
|
127
|
+
* space) and base-10 scientific notation. Returns `NaN` on failure
|
|
128
|
+
* rather than throwing.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} str
|
|
131
|
+
* @param {{ radix?: number }} [opts]
|
|
132
|
+
* @returns {number}
|
|
133
|
+
*/
|
|
134
|
+
export function parseNumber(str, opts = {}) {
|
|
135
|
+
if (typeof str !== 'number') str = String(str);
|
|
136
|
+
else return +str;
|
|
137
|
+
let s = str.trim();
|
|
138
|
+
if (s === '') return NaN;
|
|
139
|
+
const lower = s.toLowerCase();
|
|
140
|
+
if (lower === 'nan') return NaN;
|
|
141
|
+
if (lower === '∞' || lower === 'infinity' || lower === '+infinity') return Infinity;
|
|
142
|
+
if (lower === '-∞' || lower === '-infinity') return -Infinity;
|
|
143
|
+
|
|
144
|
+
const prefixed = /^[+-]?0[xob]/i.test(s);
|
|
145
|
+
if (prefixed || (opts.radix !== undefined && opts.radix !== 10)) {
|
|
146
|
+
try {
|
|
147
|
+
return Number(fromBase(s, opts.radix ?? 10));
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return NaN;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Base 10: strip grouping, keep the numeric grammar.
|
|
154
|
+
const cleaned = s.replace(/[,_ ]/g, '');
|
|
155
|
+
if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(cleaned)) return NaN;
|
|
156
|
+
return Number(cleaned);
|
|
157
|
+
}
|
package/src/math/index.js
CHANGED
|
@@ -3,3 +3,8 @@ export * from './vec2i32.js';
|
|
|
3
3
|
export * from './float64.js';
|
|
4
4
|
export * from './vec2f64.js';
|
|
5
5
|
export * from './vec3f64.js';
|
|
6
|
+
export * from './word.js';
|
|
7
|
+
export * from './solve.js';
|
|
8
|
+
export * from './mat4.js';
|
|
9
|
+
export * from './project.js';
|
|
10
|
+
export * from './format.js';
|
package/src/math/mat4.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file A minimal 4×4 matrix kernel for the x·y·z plotter and
|
|
4
|
+
* any future 3D consumer (Mermaid 3D is a candidate — see ROADMAP).
|
|
5
|
+
*
|
|
6
|
+
* Matrices are `Float64Array(16)` in **column-major** order (the WebGL /
|
|
7
|
+
* glMatrix convention): element `m[col * 4 + row]`. Every factory returns
|
|
8
|
+
* a fresh array; `multiply` composes right-to-left like linear algebra
|
|
9
|
+
* (`multiply(a, b)` applies `b` then `a` to a column vector).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** @typedef {Float64Array} Mat4Array */
|
|
13
|
+
|
|
14
|
+
export class Mat4 {
|
|
15
|
+
/** @returns {Mat4Array} the 4×4 identity. */
|
|
16
|
+
static identity() {
|
|
17
|
+
const m = new Float64Array(16);
|
|
18
|
+
m[0] = 1; m[5] = 1; m[10] = 1; m[15] = 1;
|
|
19
|
+
return m;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `a · b` (column-major). Neither operand is mutated.
|
|
24
|
+
* @param {Mat4Array} a @param {Mat4Array} b
|
|
25
|
+
* @returns {Mat4Array}
|
|
26
|
+
*/
|
|
27
|
+
static multiply(a, b) {
|
|
28
|
+
const out = new Float64Array(16);
|
|
29
|
+
for (let col = 0; col < 4; col++) {
|
|
30
|
+
for (let row = 0; row < 4; row++) {
|
|
31
|
+
let s = 0;
|
|
32
|
+
for (let k = 0; k < 4; k++) {
|
|
33
|
+
s += a[k * 4 + row] * b[col * 4 + k];
|
|
34
|
+
}
|
|
35
|
+
out[col * 4 + row] = s;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {number} rad @returns {Mat4Array} */
|
|
42
|
+
static rotationX(rad) {
|
|
43
|
+
const c = Math.cos(rad); const s = Math.sin(rad);
|
|
44
|
+
const m = Mat4.identity();
|
|
45
|
+
m[5] = c; m[6] = s; m[9] = -s; m[10] = c;
|
|
46
|
+
return m;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @param {number} rad @returns {Mat4Array} */
|
|
50
|
+
static rotationY(rad) {
|
|
51
|
+
const c = Math.cos(rad); const s = Math.sin(rad);
|
|
52
|
+
const m = Mat4.identity();
|
|
53
|
+
m[0] = c; m[2] = -s; m[8] = s; m[10] = c;
|
|
54
|
+
return m;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @param {number} rad @returns {Mat4Array} */
|
|
58
|
+
static rotationZ(rad) {
|
|
59
|
+
const c = Math.cos(rad); const s = Math.sin(rad);
|
|
60
|
+
const m = Mat4.identity();
|
|
61
|
+
m[0] = c; m[1] = s; m[4] = -s; m[5] = c;
|
|
62
|
+
return m;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @param {number} tx @param {number} ty @param {number} tz @returns {Mat4Array} */
|
|
66
|
+
static translation(tx, ty, tz) {
|
|
67
|
+
const m = Mat4.identity();
|
|
68
|
+
m[12] = tx; m[13] = ty; m[14] = tz;
|
|
69
|
+
return m;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @param {number} sx @param {number} sy @param {number} sz @returns {Mat4Array} */
|
|
73
|
+
static scaling(sx, sy, sz) {
|
|
74
|
+
const m = new Float64Array(16);
|
|
75
|
+
m[0] = sx; m[5] = sy; m[10] = sz; m[15] = 1;
|
|
76
|
+
return m;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Orthographic projection.
|
|
81
|
+
* @param {number} l @param {number} r @param {number} b @param {number} t
|
|
82
|
+
* @param {number} n @param {number} f
|
|
83
|
+
* @returns {Mat4Array}
|
|
84
|
+
*/
|
|
85
|
+
static ortho(l, r, b, t, n, f) {
|
|
86
|
+
const m = new Float64Array(16);
|
|
87
|
+
m[0] = 2 / (r - l);
|
|
88
|
+
m[5] = 2 / (t - b);
|
|
89
|
+
m[10] = -2 / (f - n);
|
|
90
|
+
m[12] = -(r + l) / (r - l);
|
|
91
|
+
m[13] = -(t + b) / (t - b);
|
|
92
|
+
m[14] = -(f + n) / (f - n);
|
|
93
|
+
m[15] = 1;
|
|
94
|
+
return m;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Perspective projection (right-handed, clip space z in [-1, 1]).
|
|
99
|
+
* @param {number} fovy vertical field of view, radians
|
|
100
|
+
* @param {number} aspect width / height
|
|
101
|
+
* @param {number} n near plane (> 0)
|
|
102
|
+
* @param {number} f far plane
|
|
103
|
+
* @returns {Mat4Array}
|
|
104
|
+
*/
|
|
105
|
+
static perspective(fovy, aspect, n, f) {
|
|
106
|
+
const t = 1 / Math.tan(fovy / 2);
|
|
107
|
+
const m = new Float64Array(16);
|
|
108
|
+
m[0] = t / aspect;
|
|
109
|
+
m[5] = t;
|
|
110
|
+
m[10] = (f + n) / (n - f);
|
|
111
|
+
m[11] = -1;
|
|
112
|
+
m[14] = (2 * f * n) / (n - f);
|
|
113
|
+
return m;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Transform a 3D point (implicit w = 1) by `m`, returning the
|
|
118
|
+
* perspective-divided `{ x, y, z, w }` (w is the pre-divide clip w, so
|
|
119
|
+
* callers can reject points behind the camera).
|
|
120
|
+
* @param {Mat4Array} m @param {number} x @param {number} y @param {number} z
|
|
121
|
+
* @returns {{ x: number, y: number, z: number, w: number }}
|
|
122
|
+
*/
|
|
123
|
+
static transformPoint(m, x, y, z) {
|
|
124
|
+
const cx = m[0] * x + m[4] * y + m[8] * z + m[12];
|
|
125
|
+
const cy = m[1] * x + m[5] * y + m[9] * z + m[13];
|
|
126
|
+
const cz = m[2] * x + m[6] * y + m[10] * z + m[14];
|
|
127
|
+
const cw = m[3] * x + m[7] * y + m[11] * z + m[15];
|
|
128
|
+
const w = cw === 0 ? 1 : cw;
|
|
129
|
+
return { x: cx / w, y: cy / w, z: cz / w, w: cw };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file 3D → 2D projection helpers, built on `Mat4` and the
|
|
4
|
+
* existing `Vec3f64`. Used by the x·y·z plotter to turn a rotated grid
|
|
5
|
+
* of surface points into screen coordinates and to shade faces.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Mat4 } from './mat4.js';
|
|
9
|
+
import { Vec3f64 } from './vec3f64.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} Viewport
|
|
13
|
+
* @property {number} x left edge in pixels
|
|
14
|
+
* @property {number} y top edge in pixels
|
|
15
|
+
* @property {number} width
|
|
16
|
+
* @property {number} height
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Project a 3D point through `mat` (model·view·projection) and map the
|
|
21
|
+
* resulting normalized device coordinates into `viewport` pixels. The Y
|
|
22
|
+
* axis is flipped so that +Y is up in world space but down in screen
|
|
23
|
+
* space, as SVG expects.
|
|
24
|
+
*
|
|
25
|
+
* @param {{ x: number, y: number, z: number } | import('./vec3f64.js').Vec3f64} point3
|
|
26
|
+
* @param {import('./mat4.js').Mat4Array} mat
|
|
27
|
+
* @param {Viewport} viewport
|
|
28
|
+
* @returns {{ x: number, y: number, z: number, w: number }}
|
|
29
|
+
*/
|
|
30
|
+
export function project3dTo2d(point3, mat, viewport) {
|
|
31
|
+
const clip = Mat4.transformPoint(mat, +point3.x, +point3.y, +point3.z);
|
|
32
|
+
const sx = viewport.x + (clip.x * 0.5 + 0.5) * viewport.width;
|
|
33
|
+
const sy = viewport.y + (0.5 - clip.y * 0.5) * viewport.height;
|
|
34
|
+
return { x: sx, y: sy, z: clip.z, w: clip.w };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The (un-normalized) surface normal of triangle `a→b→c`, right-hand
|
|
39
|
+
* winding. Returns a `Vec3f64`.
|
|
40
|
+
* @param {import('./vec3f64.js').Vec3f64} a
|
|
41
|
+
* @param {import('./vec3f64.js').Vec3f64} b
|
|
42
|
+
* @param {import('./vec3f64.js').Vec3f64} c
|
|
43
|
+
* @returns {import('./vec3f64.js').Vec3f64}
|
|
44
|
+
*/
|
|
45
|
+
export function surfaceNormal(a, b, c) {
|
|
46
|
+
const ab = Vec3f64.sub(b, a);
|
|
47
|
+
const ac = Vec3f64.sub(c, a);
|
|
48
|
+
return Vec3f64.cross(ab, ac);
|
|
49
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Generic, domain-free numeric root finders. No finance
|
|
4
|
+
* or geometry lives here; `@jarenjs/core/finance` builds NPV/IRR/rate on
|
|
5
|
+
* top of these, the calculator's equation helpers on top of them too.
|
|
6
|
+
*
|
|
7
|
+
* Each solver returns a `{ root, iterations, converged }` record instead
|
|
8
|
+
* of throwing on non-convergence — callers decide how to react (finance
|
|
9
|
+
* falls back from Newton to bisection, for example).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} SolveResult
|
|
14
|
+
* @property {number} root the best estimate found
|
|
15
|
+
* @property {number} iterations iterations actually run
|
|
16
|
+
* @property {boolean} converged whether `tol` was reached
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const DEFAULT_TOL = 1e-10;
|
|
20
|
+
const DEFAULT_MAX_ITER = 100;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Newton–Raphson using an explicit derivative. Falls back to reporting
|
|
24
|
+
* non-convergence (rather than diverging silently) when the derivative
|
|
25
|
+
* vanishes or the step explodes.
|
|
26
|
+
*
|
|
27
|
+
* @param {(x: number) => number} f
|
|
28
|
+
* @param {(x: number) => number} df
|
|
29
|
+
* @param {number} x0
|
|
30
|
+
* @param {{ tol?: number, maxIter?: number }} [opts]
|
|
31
|
+
* @returns {SolveResult}
|
|
32
|
+
*/
|
|
33
|
+
export function newtonRaphson(f, df, x0, opts = {}) {
|
|
34
|
+
const tol = opts.tol ?? DEFAULT_TOL;
|
|
35
|
+
const maxIter = opts.maxIter ?? DEFAULT_MAX_ITER;
|
|
36
|
+
let x = +x0;
|
|
37
|
+
for (let i = 1; i <= maxIter; i++) {
|
|
38
|
+
const fx = +f(x);
|
|
39
|
+
if (Math.abs(fx) <= tol) return { root: x, iterations: i, converged: true };
|
|
40
|
+
const dfx = +df(x);
|
|
41
|
+
if (dfx === 0 || !isFinite(dfx)) return { root: x, iterations: i, converged: false };
|
|
42
|
+
const next = x - fx / dfx;
|
|
43
|
+
if (!isFinite(next)) return { root: x, iterations: i, converged: false };
|
|
44
|
+
if (Math.abs(next - x) <= tol) return { root: next, iterations: i, converged: true };
|
|
45
|
+
x = next;
|
|
46
|
+
}
|
|
47
|
+
return { root: x, iterations: maxIter, converged: false };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Bisection on a sign-changing bracket `[a, b]`. Guaranteed to converge
|
|
52
|
+
* when `f(a)` and `f(b)` straddle a root; returns `converged: false` if
|
|
53
|
+
* the bracket does not (rather than throwing).
|
|
54
|
+
*
|
|
55
|
+
* @param {(x: number) => number} f
|
|
56
|
+
* @param {number} a
|
|
57
|
+
* @param {number} b
|
|
58
|
+
* @param {{ tol?: number, maxIter?: number }} [opts]
|
|
59
|
+
* @returns {SolveResult}
|
|
60
|
+
*/
|
|
61
|
+
export function bisect(f, a, b, opts = {}) {
|
|
62
|
+
const tol = opts.tol ?? DEFAULT_TOL;
|
|
63
|
+
const maxIter = opts.maxIter ?? DEFAULT_MAX_ITER;
|
|
64
|
+
let lo = +a;
|
|
65
|
+
let hi = +b;
|
|
66
|
+
let flo = +f(lo);
|
|
67
|
+
let fhi = +f(hi);
|
|
68
|
+
if (flo === 0) return { root: lo, iterations: 0, converged: true };
|
|
69
|
+
if (fhi === 0) return { root: hi, iterations: 0, converged: true };
|
|
70
|
+
if (flo * fhi > 0) return { root: (lo + hi) / 2, iterations: 0, converged: false };
|
|
71
|
+
let mid = (lo + hi) / 2;
|
|
72
|
+
for (let i = 1; i <= maxIter; i++) {
|
|
73
|
+
mid = (lo + hi) / 2;
|
|
74
|
+
const fmid = +f(mid);
|
|
75
|
+
if (Math.abs(fmid) <= tol || (hi - lo) / 2 <= tol) {
|
|
76
|
+
return { root: mid, iterations: i, converged: true };
|
|
77
|
+
}
|
|
78
|
+
if (flo * fmid < 0) { hi = mid; fhi = fmid; }
|
|
79
|
+
else { lo = mid; flo = fmid; }
|
|
80
|
+
}
|
|
81
|
+
return { root: mid, iterations: maxIter, converged: false };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The secant method (Newton without an explicit derivative). Needs two
|
|
86
|
+
* starting points.
|
|
87
|
+
*
|
|
88
|
+
* @param {(x: number) => number} f
|
|
89
|
+
* @param {number} x0
|
|
90
|
+
* @param {number} x1
|
|
91
|
+
* @param {{ tol?: number, maxIter?: number }} [opts]
|
|
92
|
+
* @returns {SolveResult}
|
|
93
|
+
*/
|
|
94
|
+
export function secant(f, x0, x1, opts = {}) {
|
|
95
|
+
const tol = opts.tol ?? DEFAULT_TOL;
|
|
96
|
+
const maxIter = opts.maxIter ?? DEFAULT_MAX_ITER;
|
|
97
|
+
let a = +x0;
|
|
98
|
+
let b = +x1;
|
|
99
|
+
let fa = +f(a);
|
|
100
|
+
let fb = +f(b);
|
|
101
|
+
for (let i = 1; i <= maxIter; i++) {
|
|
102
|
+
if (Math.abs(fb) <= tol) return { root: b, iterations: i, converged: true };
|
|
103
|
+
const denom = fb - fa;
|
|
104
|
+
if (denom === 0 || !isFinite(denom)) return { root: b, iterations: i, converged: false };
|
|
105
|
+
const next = b - fb * (b - a) / denom;
|
|
106
|
+
if (!isFinite(next)) return { root: b, iterations: i, converged: false };
|
|
107
|
+
if (Math.abs(next - b) <= tol) return { root: next, iterations: i, converged: true };
|
|
108
|
+
a = b; fa = fb;
|
|
109
|
+
b = next; fb = +f(b);
|
|
110
|
+
}
|
|
111
|
+
return { root: b, iterations: maxIter, converged: false };
|
|
112
|
+
}
|
package/src/math/word.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file BigInt fixed-width word math. The programmer-calculator
|
|
4
|
+
* kernel: two's-complement integers of a fixed bit width (8/16/32/64),
|
|
5
|
+
* signed or unsigned, with the bitwise/shift/rotate operators and
|
|
6
|
+
* radix-string I/O a HEX/DEC/OCT/BIN calculator needs.
|
|
7
|
+
*
|
|
8
|
+
* Everything is native `BigInt` — zero dependencies, exact at any width.
|
|
9
|
+
* Values crossing the module boundary are plain `BigInt`s already reduced
|
|
10
|
+
* to the requested word (mask + sign-extend), so operators compose
|
|
11
|
+
* without re-normalizing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Supported word sizes, in bits. */
|
|
15
|
+
export const WORD_BITS = Object.freeze([8, 16, 32, 64]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The unsigned mask for a word of `bits` bits: `2**bits - 1`.
|
|
19
|
+
* @param {number} bits
|
|
20
|
+
* @returns {bigint}
|
|
21
|
+
*/
|
|
22
|
+
export function wordMask(bits) {
|
|
23
|
+
return (1n << BigInt(bits | 0)) - 1n;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reduce any integer to a fixed-width word. Unsigned words wrap into
|
|
28
|
+
* `[0, 2**bits)`; signed words are the two's-complement interpretation,
|
|
29
|
+
* so the high bit becomes the sign and the result lands in
|
|
30
|
+
* `[-2**(bits-1), 2**(bits-1))`.
|
|
31
|
+
*
|
|
32
|
+
* @param {bigint|number|string} value
|
|
33
|
+
* @param {number} [bits]
|
|
34
|
+
* @param {boolean} [signed]
|
|
35
|
+
* @returns {bigint}
|
|
36
|
+
*/
|
|
37
|
+
export function toWord(value, bits = 32, signed = false) {
|
|
38
|
+
const b = BigInt(bits | 0);
|
|
39
|
+
const mask = (1n << b) - 1n;
|
|
40
|
+
let v = (typeof value === 'bigint' ? value : BigInt(Math.trunc(Number(value)))) & mask;
|
|
41
|
+
if (signed) {
|
|
42
|
+
const signBit = 1n << (b - 1n);
|
|
43
|
+
if ((v & signBit) !== 0n) v -= (1n << b);
|
|
44
|
+
}
|
|
45
|
+
return v;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {bigint} a @param {bigint} b @param {number} [bits] @param {boolean} [signed] */
|
|
49
|
+
export function wAnd(a, b, bits = 32, signed = false) {
|
|
50
|
+
return toWord(toWord(a, bits, false) & toWord(b, bits, false), bits, signed);
|
|
51
|
+
}
|
|
52
|
+
/** @param {bigint} a @param {bigint} b @param {number} [bits] @param {boolean} [signed] */
|
|
53
|
+
export function wOr(a, b, bits = 32, signed = false) {
|
|
54
|
+
return toWord(toWord(a, bits, false) | toWord(b, bits, false), bits, signed);
|
|
55
|
+
}
|
|
56
|
+
/** @param {bigint} a @param {bigint} b @param {number} [bits] @param {boolean} [signed] */
|
|
57
|
+
export function wXor(a, b, bits = 32, signed = false) {
|
|
58
|
+
return toWord(toWord(a, bits, false) ^ toWord(b, bits, false), bits, signed);
|
|
59
|
+
}
|
|
60
|
+
/** @param {bigint} a @param {number} [bits] @param {boolean} [signed] */
|
|
61
|
+
export function wNot(a, bits = 32, signed = false) {
|
|
62
|
+
return toWord(~toWord(a, bits, false), bits, signed);
|
|
63
|
+
}
|
|
64
|
+
/** @param {bigint} a @param {bigint} n @param {number} [bits] @param {boolean} [signed] */
|
|
65
|
+
export function wShl(a, n, bits = 32, signed = false) {
|
|
66
|
+
return toWord(toWord(a, bits, false) << (BigInt(n) % BigInt(bits)), bits, signed);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Logical right shift (zero fill) over the unsigned representation, then
|
|
70
|
+
* reinterpreted per `signed`.
|
|
71
|
+
* @param {bigint} a @param {bigint} n @param {number} [bits] @param {boolean} [signed]
|
|
72
|
+
*/
|
|
73
|
+
export function wShr(a, n, bits = 32, signed = false) {
|
|
74
|
+
return toWord(toWord(a, bits, false) >> (BigInt(n) % BigInt(bits)), bits, signed);
|
|
75
|
+
}
|
|
76
|
+
/** Rotate left. @param {bigint} a @param {bigint} n @param {number} [bits] @param {boolean} [signed] */
|
|
77
|
+
export function wRol(a, n, bits = 32, signed = false) {
|
|
78
|
+
const b = BigInt(bits | 0);
|
|
79
|
+
const u = toWord(a, bits, false);
|
|
80
|
+
const s = ((BigInt(n) % b) + b) % b;
|
|
81
|
+
const mask = (1n << b) - 1n;
|
|
82
|
+
return toWord(((u << s) | (u >> (b - s))) & mask, bits, signed);
|
|
83
|
+
}
|
|
84
|
+
/** Rotate right. @param {bigint} a @param {bigint} n @param {number} [bits] @param {boolean} [signed] */
|
|
85
|
+
export function wRor(a, n, bits = 32, signed = false) {
|
|
86
|
+
const b = BigInt(bits | 0);
|
|
87
|
+
const u = toWord(a, bits, false);
|
|
88
|
+
const s = ((BigInt(n) % b) + b) % b;
|
|
89
|
+
const mask = (1n << b) - 1n;
|
|
90
|
+
return toWord(((u >> s) | (u << (b - s))) & mask, bits, signed);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Euclidean-flavoured modulo that follows the sign of the divisor,
|
|
94
|
+
* reduced back into the word.
|
|
95
|
+
* @param {bigint} a @param {bigint} b @param {number} [bits] @param {boolean} [signed]
|
|
96
|
+
*/
|
|
97
|
+
export function wMod(a, b, bits = 32, signed = false) {
|
|
98
|
+
const x = toWord(a, bits, signed);
|
|
99
|
+
const y = toWord(b, bits, signed);
|
|
100
|
+
if (y === 0n) throw new RangeError('wMod: division by zero');
|
|
101
|
+
return toWord(x % y, bits, signed);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** @param {number} radix @returns {string} */
|
|
105
|
+
function radixDigits(radix) {
|
|
106
|
+
return '0123456789abcdefghijklmnopqrstuvwxyz'.slice(0, radix);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Format an integer as a radix string (no prefix). Groups digits with an
|
|
111
|
+
* optional separator and left-pads to `pad` digits.
|
|
112
|
+
*
|
|
113
|
+
* @param {bigint|number} int
|
|
114
|
+
* @param {number} [radix] 2..36
|
|
115
|
+
* @param {{ group?: number, sep?: string, pad?: number, upper?: boolean }} [opts]
|
|
116
|
+
* @returns {string}
|
|
117
|
+
*/
|
|
118
|
+
export function toBase(int, radix = 10, opts = {}) {
|
|
119
|
+
const r = radix | 0;
|
|
120
|
+
if (r < 2 || r > 36) throw new RangeError(`toBase: radix ${radix} out of range 2..36`);
|
|
121
|
+
let v = typeof int === 'bigint' ? int : BigInt(Math.trunc(Number(int)));
|
|
122
|
+
const neg = v < 0n;
|
|
123
|
+
if (neg) v = -v;
|
|
124
|
+
const R = BigInt(r);
|
|
125
|
+
const digits = radixDigits(r);
|
|
126
|
+
let out = v === 0n ? '0' : '';
|
|
127
|
+
while (v > 0n) {
|
|
128
|
+
out = digits[Number(v % R)] + out;
|
|
129
|
+
v /= R;
|
|
130
|
+
}
|
|
131
|
+
if (opts.pad && out.length < opts.pad) out = out.padStart(opts.pad, '0');
|
|
132
|
+
if (opts.group && opts.group > 0) {
|
|
133
|
+
const sep = opts.sep ?? ' ';
|
|
134
|
+
const parts = [];
|
|
135
|
+
for (let i = out.length; i > 0; i -= opts.group) {
|
|
136
|
+
parts.unshift(out.slice(Math.max(0, i - opts.group), i));
|
|
137
|
+
}
|
|
138
|
+
out = parts.join(sep);
|
|
139
|
+
}
|
|
140
|
+
if (opts.upper) out = out.toUpperCase();
|
|
141
|
+
return neg ? '-' + out : out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parse a radix string back to a BigInt. Tolerates group separators
|
|
146
|
+
* (space/underscore), a leading sign, and an optional `0x`/`0o`/`0b`
|
|
147
|
+
* prefix (which must agree with `radix` when both are given).
|
|
148
|
+
*
|
|
149
|
+
* @param {string} str
|
|
150
|
+
* @param {number} [radix] 2..36
|
|
151
|
+
* @returns {bigint}
|
|
152
|
+
*/
|
|
153
|
+
export function fromBase(str, radix = 10) {
|
|
154
|
+
let s = String(str).trim().replace(/[ _]/g, '');
|
|
155
|
+
let neg = false;
|
|
156
|
+
if (s.startsWith('-')) { neg = true; s = s.slice(1); }
|
|
157
|
+
else if (s.startsWith('+')) s = s.slice(1);
|
|
158
|
+
const m = /^0([xob])/i.exec(s);
|
|
159
|
+
if (m) {
|
|
160
|
+
const pfx = m[1].toLowerCase();
|
|
161
|
+
const pfxRadix = pfx === 'x' ? 16 : pfx === 'o' ? 8 : 2;
|
|
162
|
+
radix = pfxRadix;
|
|
163
|
+
s = s.slice(2);
|
|
164
|
+
}
|
|
165
|
+
const r = radix | 0;
|
|
166
|
+
if (r < 2 || r > 36) throw new RangeError(`fromBase: radix ${radix} out of range 2..36`);
|
|
167
|
+
if (s.length === 0) throw new SyntaxError('fromBase: empty digit string');
|
|
168
|
+
const R = BigInt(r);
|
|
169
|
+
const digits = radixDigits(r);
|
|
170
|
+
let acc = 0n;
|
|
171
|
+
for (const ch of s.toLowerCase()) {
|
|
172
|
+
const d = digits.indexOf(ch);
|
|
173
|
+
if (d < 0) throw new SyntaxError(`fromBase: '${ch}' is not a base-${r} digit`);
|
|
174
|
+
acc = acc * R + BigInt(d);
|
|
175
|
+
}
|
|
176
|
+
return neg ? -acc : acc;
|
|
177
|
+
}
|