@gkucmierz/utils 5.1.0 → 5.4.1
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/main.mjs +23 -3
- package/package.json +2 -3
- package/src/developer-utils/grid-navigation.mjs +291 -0
- package/src/developer-utils/quorum.mjs +345 -0
- package/src/developer-utils/type.mjs +24 -0
- package/src/geometry/math3d.mjs +164 -0
- package/src/geometry/range-set.mjs +748 -0
- package/src/string-arrays/parse-heading.mjs +38 -0
- package/src/string-arrays/slugify.mjs +74 -0
package/main.mjs
CHANGED
|
@@ -41,6 +41,9 @@ import {
|
|
|
41
41
|
import {
|
|
42
42
|
|
|
43
43
|
} from './src/developer-utils/get-type.mjs'
|
|
44
|
+
import {
|
|
45
|
+
detectGridColumns, navigateGrid
|
|
46
|
+
} from './src/developer-utils/grid-navigation.mjs'
|
|
44
47
|
import {
|
|
45
48
|
measurePerformance
|
|
46
49
|
} from './src/developer-utils/measure-performance.mjs'
|
|
@@ -50,6 +53,9 @@ import {
|
|
|
50
53
|
import {
|
|
51
54
|
naturalSearch
|
|
52
55
|
} from './src/developer-utils/natural-search.mjs'
|
|
56
|
+
import {
|
|
57
|
+
QUORUM_CONFIG, QUORUM_DEFAULT_K, QUORUM_DEFAULT_N, QUORUM_MAX_RELATIVE_ERROR, QUORUM_MAX_RELATIVE_ERROR_PCT, QUORUM_ZERO_FLOOR_EPSILON, computeRelativeError, evaluateQuorum, evaluateQuorum4of5, isQuorumEqual
|
|
58
|
+
} from './src/developer-utils/quorum.mjs'
|
|
53
59
|
import {
|
|
54
60
|
randNormal
|
|
55
61
|
} from './src/developer-utils/rand-normal.mjs'
|
|
@@ -60,17 +66,20 @@ import {
|
|
|
60
66
|
setSafeInterval
|
|
61
67
|
} from './src/developer-utils/set-safe-interval.mjs'
|
|
62
68
|
import {
|
|
63
|
-
Type, Types, is, of, same
|
|
69
|
+
Type, Types, is, isCallable, of, same
|
|
64
70
|
} from './src/developer-utils/type.mjs'
|
|
65
71
|
import {
|
|
66
72
|
barycentricCoordinates
|
|
67
73
|
} from './src/geometry/barycentric.mjs'
|
|
68
74
|
import {
|
|
69
|
-
axisAngleToMatrix4, crossProduct, dotProduct, getRotationMatrixFromVectors, multiplyMatrix4, normalize, projectToTrackball
|
|
75
|
+
applyScreenDragQuat, axisAngleToMatrix4, axisAngleToQuat, crossProduct, dotProduct, eulerYXZToQuat, getRotationMatrixFromVectors, matToEulerYXZ, matToQuat, multiplyMatrix4, multiplyQuat, normalize, normalizeQuat, projectToTrackball, quatToMat, rotateVecByQuat, slerp
|
|
70
76
|
} from './src/geometry/math3d.mjs'
|
|
71
77
|
import {
|
|
72
78
|
matrixAsArray
|
|
73
79
|
} from './src/geometry/matrix.mjs'
|
|
80
|
+
import {
|
|
81
|
+
OrthoSet, containsPoint1D, containsPointNd, diffBox, diffRange, formatRange, measureBox, measureRange, mutualBox, mutualRange, parseRange, range, sharedMeasureNd, sumBox, sumRange, xorBox, xorRange
|
|
82
|
+
} from './src/geometry/range-set.mjs'
|
|
74
83
|
import {
|
|
75
84
|
egcd
|
|
76
85
|
} from './src/number-theory/egcd.mjs'
|
|
@@ -140,6 +149,12 @@ import {
|
|
|
140
149
|
import {
|
|
141
150
|
formatBigNumber, formatBigNumberBI, wrapFn
|
|
142
151
|
} from './src/string-arrays/format-big-number.mjs'
|
|
152
|
+
import {
|
|
153
|
+
parseHeading
|
|
154
|
+
} from './src/string-arrays/parse-heading.mjs'
|
|
155
|
+
import {
|
|
156
|
+
slugify
|
|
157
|
+
} from './src/string-arrays/slugify.mjs'
|
|
143
158
|
|
|
144
159
|
export * from './src/automata/langtons-ant.mjs';
|
|
145
160
|
export * from './src/combinatorics/combinations.mjs';
|
|
@@ -155,9 +170,11 @@ export * from './src/data-structures/list-node.mjs';
|
|
|
155
170
|
export * from './src/developer-utils/binary-search.mjs';
|
|
156
171
|
export * from './src/developer-utils/consume-iterator.mjs';
|
|
157
172
|
export * from './src/developer-utils/get-type.mjs';
|
|
173
|
+
export * from './src/developer-utils/grid-navigation.mjs';
|
|
158
174
|
export * from './src/developer-utils/measure-performance.mjs';
|
|
159
175
|
export * from './src/developer-utils/memoize.mjs';
|
|
160
176
|
export * from './src/developer-utils/natural-search.mjs';
|
|
177
|
+
export * from './src/developer-utils/quorum.mjs';
|
|
161
178
|
export * from './src/developer-utils/rand-normal.mjs';
|
|
162
179
|
export * from './src/developer-utils/range-array.mjs';
|
|
163
180
|
export * from './src/developer-utils/set-safe-interval.mjs';
|
|
@@ -165,6 +182,7 @@ export * from './src/developer-utils/type.mjs';
|
|
|
165
182
|
export * from './src/geometry/barycentric.mjs';
|
|
166
183
|
export * from './src/geometry/math3d.mjs';
|
|
167
184
|
export * from './src/geometry/matrix.mjs';
|
|
185
|
+
export * from './src/geometry/range-set.mjs';
|
|
168
186
|
export * from './src/number-theory/egcd.mjs';
|
|
169
187
|
export * from './src/number-theory/factors.mjs';
|
|
170
188
|
export * from './src/number-theory/gcd.mjs';
|
|
@@ -188,7 +206,9 @@ export * from './src/string-arrays/bijective-numeration.mjs';
|
|
|
188
206
|
export * from './src/string-arrays/chunks.mjs';
|
|
189
207
|
export * from './src/string-arrays/copy-case.mjs';
|
|
190
208
|
export * from './src/string-arrays/format-big-number.mjs';
|
|
209
|
+
export * from './src/string-arrays/parse-heading.mjs';
|
|
210
|
+
export * from './src/string-arrays/slugify.mjs';
|
|
191
211
|
|
|
192
212
|
export default [
|
|
193
|
-
createLangtonsAnt, createUnlimitedGrid, combinations, combinationsIterator, bin2gray, gray2bin, nChooseK, permutations, permutationsIterator, ConflatingQueue, SetCnt, Stack, Trie, Heap, ListNode, binarySearchArr, binarySearchGE, binarySearchLE, binarySearchRangeIncl, consumeIteratorNonBlocking, measurePerformance, memoize, naturalSearch, randNormal, array2range, range2array, setSafeInterval, Type, Types, is, of, same, barycentricCoordinates, axisAngleToMatrix4, crossProduct, dotProduct, getRotationMatrixFromVectors, multiplyMatrix4, normalize, projectToTrackball, matrixAsArray, egcd, factors, factorsBI, gcd, gcdBI, lcm, lcmBI, lucasLehmerBI, mobius, mobiusBI, mod, modBI, phi, phiBI, powMod, powModBI, tonelliShanksBI, nelderMead, particleSwarmOptimization, simulatedAnnealing, goldenRatio, goldenRatioBI, goldenRatioStr, gpn, gpnBI, heronsFormula, heronsFormulaBI, squareRoot, squareRootBI, arrayHistogram, fromBase64, fromBase64Url, toBase64, toBase64Url, bijective2num, bijective2numBI, num2bijective, num2bijectiveBI, chunks, chunksAsyncIterator, chunksIterator, copyCase, formatBigNumber, formatBigNumberBI, wrapFn
|
|
213
|
+
createLangtonsAnt, createUnlimitedGrid, combinations, combinationsIterator, bin2gray, gray2bin, nChooseK, permutations, permutationsIterator, ConflatingQueue, SetCnt, Stack, Trie, Heap, ListNode, binarySearchArr, binarySearchGE, binarySearchLE, binarySearchRangeIncl, consumeIteratorNonBlocking, detectGridColumns, navigateGrid, measurePerformance, memoize, naturalSearch, QUORUM_CONFIG, QUORUM_DEFAULT_K, QUORUM_DEFAULT_N, QUORUM_MAX_RELATIVE_ERROR, QUORUM_MAX_RELATIVE_ERROR_PCT, QUORUM_ZERO_FLOOR_EPSILON, computeRelativeError, evaluateQuorum, evaluateQuorum4of5, isQuorumEqual, randNormal, array2range, range2array, setSafeInterval, Type, Types, is, isCallable, of, same, barycentricCoordinates, applyScreenDragQuat, axisAngleToMatrix4, axisAngleToQuat, crossProduct, dotProduct, eulerYXZToQuat, getRotationMatrixFromVectors, matToEulerYXZ, matToQuat, multiplyMatrix4, multiplyQuat, normalize, normalizeQuat, projectToTrackball, quatToMat, rotateVecByQuat, slerp, matrixAsArray, OrthoSet, containsPoint1D, containsPointNd, diffBox, diffRange, formatRange, measureBox, measureRange, mutualBox, mutualRange, parseRange, range, sharedMeasureNd, sumBox, sumRange, xorBox, xorRange, egcd, factors, factorsBI, gcd, gcdBI, lcm, lcmBI, lucasLehmerBI, mobius, mobiusBI, mod, modBI, phi, phiBI, powMod, powModBI, tonelliShanksBI, nelderMead, particleSwarmOptimization, simulatedAnnealing, goldenRatio, goldenRatioBI, goldenRatioStr, gpn, gpnBI, heronsFormula, heronsFormulaBI, squareRoot, squareRootBI, arrayHistogram, fromBase64, fromBase64Url, toBase64, toBase64Url, bijective2num, bijective2numBI, num2bijective, num2bijectiveBI, chunks, chunksAsyncIterator, chunksIterator, copyCase, formatBigNumber, formatBigNumberBI, wrapFn, parseHeading, slugify
|
|
194
214
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gkucmierz/utils",
|
|
3
|
-
"version": "5.1
|
|
3
|
+
"version": "5.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Usefull functions for solving programming tasks",
|
|
6
6
|
"keywords": [
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"docs:build": "jsdoc -c jsdoc.json && cp -r docs/@gkucmierz/utils/*/* docs/ && rm -rf docs/@gkucmierz",
|
|
75
75
|
"docs:open": "open docs/index.html",
|
|
76
76
|
"lint": "eslint .",
|
|
77
|
-
"prepare": "
|
|
77
|
+
"prepare": "git config core.hooksPath .githooks || true"
|
|
78
78
|
},
|
|
79
79
|
"repository": {
|
|
80
80
|
"type": "git",
|
|
@@ -88,7 +88,6 @@
|
|
|
88
88
|
"docdash": "^2.0.2",
|
|
89
89
|
"eslint": "^10.1.0",
|
|
90
90
|
"globals": "^17.4.0",
|
|
91
|
-
"husky": "^8.0.3",
|
|
92
91
|
"jasmine": "^4.4.0",
|
|
93
92
|
"jsdoc": "^4.0.4",
|
|
94
93
|
"lz-string": "^1.5.0",
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module developer-utils
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a 2D occupancy matrix `rows[r][c] = itemIndex` (or `-1` for empty gap)
|
|
7
|
+
* when `spanLastRow` or `colSpans` is enabled.
|
|
8
|
+
*
|
|
9
|
+
* @param {number} totalItems
|
|
10
|
+
* @param {number} cols
|
|
11
|
+
* @param {Object} opts
|
|
12
|
+
* @returns {{ matrix: number[][], itemPos: Array<{ row: number, cols: number[] }> }}
|
|
13
|
+
*/
|
|
14
|
+
const buildSpannedGridMatrix = (totalItems, cols, opts = {}) => {
|
|
15
|
+
const spanLastRow = Boolean(opts.spanLastRow);
|
|
16
|
+
const colSpans = opts.colSpans || null;
|
|
17
|
+
const matrix = [];
|
|
18
|
+
const itemPos = Array.from({ length: totalItems }, () => ({ row: 0, cols: [] }));
|
|
19
|
+
|
|
20
|
+
let curRow = new Array(cols).fill(-1);
|
|
21
|
+
let c = 0;
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < totalItems; i += 1) {
|
|
24
|
+
let span = 1;
|
|
25
|
+
if (colSpans) {
|
|
26
|
+
const rawSpan = Array.isArray(colSpans) ? colSpans[i] : colSpans[i];
|
|
27
|
+
if (Number.isFinite(Number(rawSpan)) && Number(rawSpan) >= 1) {
|
|
28
|
+
span = Math.min(cols, Math.floor(Number(rawSpan)));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (c + span > cols) {
|
|
32
|
+
matrix.push(curRow);
|
|
33
|
+
curRow = new Array(cols).fill(-1);
|
|
34
|
+
c = 0;
|
|
35
|
+
}
|
|
36
|
+
if (spanLastRow && i === totalItems - 1 && c < cols) {
|
|
37
|
+
span = cols - c;
|
|
38
|
+
}
|
|
39
|
+
const rowIdx = matrix.length;
|
|
40
|
+
itemPos[i].row = rowIdx;
|
|
41
|
+
for (let k = 0; k < span && c < cols; k += 1) {
|
|
42
|
+
curRow[c] = i;
|
|
43
|
+
itemPos[i].cols.push(c);
|
|
44
|
+
c += 1;
|
|
45
|
+
}
|
|
46
|
+
if (c >= cols) {
|
|
47
|
+
matrix.push(curRow);
|
|
48
|
+
curRow = new Array(cols).fill(-1);
|
|
49
|
+
c = 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (c > 0) {
|
|
54
|
+
matrix.push(curRow);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { matrix, itemPos };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolves the next 0-based index when navigating a 2D grid (matrix) that may contain
|
|
62
|
+
* trailing gaps in its last row (`totalItems` not divisible by `columns`) or spanned cells
|
|
63
|
+
* (`spanLastRow: true` / `colSpans` / `preferredCol`).
|
|
64
|
+
*
|
|
65
|
+
* Pure mathematical/index function — completely independent of the DOM.
|
|
66
|
+
*
|
|
67
|
+
* Wrapping configuration hierarchy:
|
|
68
|
+
* - `wrap` (boolean, default `true`): master default for both axes.
|
|
69
|
+
* - `wrapHoriz` / `wrapHorizontal` (boolean, optional): overrides `wrap` for horizontal movement (`left` / `right`).
|
|
70
|
+
* - `wrapVert` / `wrapVertical` (boolean, optional): overrides `wrap` for vertical movement (`up` / `down`).
|
|
71
|
+
* - `rowStrict` (boolean, default `false`): when `true`, horizontal movement wraps/clamps strictly within the current row instead of reading order.
|
|
72
|
+
* - `spanLastRow` (boolean, default `false`): when `true`, the final item (`totalItems - 1`) spans all remaining columns in the last row (e.g. a 2x3 grid where row 2 has a full-width card `[4, 4]`).
|
|
73
|
+
* - `colSpans` (number[]|Object, optional): explicit column span per item index (`>= 1`).
|
|
74
|
+
* - `preferredCol` (number, optional): preferred column index (`0 <= preferredCol < columns`) preserved when stepping vertically out of a multi-column spanned item.
|
|
75
|
+
*
|
|
76
|
+
* When `columns === 1` (single-column layout), `right` behaves identically to `down`
|
|
77
|
+
* and `left` behaves identically to `up`.
|
|
78
|
+
*
|
|
79
|
+
* @param {number} currentIndex - Current active 0-based index (`0 <= currentIndex < totalItems`).
|
|
80
|
+
* @param {number} totalItems - Total number of items in the grid ($N \ge 0$).
|
|
81
|
+
* @param {'left'|'right'|'up'|'down'|'ArrowLeft'|'ArrowRight'|'ArrowUp'|'ArrowDown'} direction - Movement direction.
|
|
82
|
+
* @param {number|Object} [columnsOrOptions=1] - Column count ($C \ge 1$) or options object.
|
|
83
|
+
* @param {Object} [maybeOptions={}] - Optional configuration when 4th argument is a column count.
|
|
84
|
+
* @returns {number} Next 0-based index, or `-1` if `totalItems <= 0`.
|
|
85
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IHYEMBuIDm64CmA4jCACYIC+CAZjFGAgEQACOA1gK4DGYIBMAF4B6TnBAAbAM5MA3MGAZseQiXLygA|▶ Try it live in Instacode}
|
|
86
|
+
*/
|
|
87
|
+
export const navigateGrid = (
|
|
88
|
+
currentIndex,
|
|
89
|
+
totalItems,
|
|
90
|
+
direction,
|
|
91
|
+
columnsOrOptions = 1,
|
|
92
|
+
maybeOptions = {}
|
|
93
|
+
) => {
|
|
94
|
+
if (!Number.isFinite(totalItems) || totalItems <= 0) {
|
|
95
|
+
return -1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const isObj = columnsOrOptions !== null && typeof columnsOrOptions === 'object';
|
|
99
|
+
const opts = isObj ? columnsOrOptions : (maybeOptions || {});
|
|
100
|
+
const rawCols = isObj ? opts.columns : columnsOrOptions;
|
|
101
|
+
const cols = Math.max(1, Math.floor(Number(rawCols) || 1));
|
|
102
|
+
|
|
103
|
+
const cur =
|
|
104
|
+
Number.isFinite(currentIndex) && currentIndex >= 0 && currentIndex < totalItems
|
|
105
|
+
? Math.floor(currentIndex)
|
|
106
|
+
: 0;
|
|
107
|
+
|
|
108
|
+
const baseWrap = typeof opts.wrap === 'boolean' ? opts.wrap : true;
|
|
109
|
+
const wrapHoriz =
|
|
110
|
+
typeof opts.wrapHorizontal === 'boolean'
|
|
111
|
+
? opts.wrapHorizontal
|
|
112
|
+
: typeof opts.wrapHoriz === 'boolean'
|
|
113
|
+
? opts.wrapHoriz
|
|
114
|
+
: baseWrap;
|
|
115
|
+
const wrapVert =
|
|
116
|
+
typeof opts.wrapVertical === 'boolean'
|
|
117
|
+
? opts.wrapVertical
|
|
118
|
+
: typeof opts.wrapVert === 'boolean'
|
|
119
|
+
? opts.wrapVert
|
|
120
|
+
: baseWrap;
|
|
121
|
+
const rowStrict = Boolean(opts.rowStrict);
|
|
122
|
+
const hasSpans = Boolean(opts.spanLastRow || opts.colSpans);
|
|
123
|
+
|
|
124
|
+
const row = Math.floor(cur / cols);
|
|
125
|
+
const col = cur % cols;
|
|
126
|
+
|
|
127
|
+
let deltaH = 0;
|
|
128
|
+
let deltaV = 0;
|
|
129
|
+
|
|
130
|
+
switch (direction) {
|
|
131
|
+
case 'right':
|
|
132
|
+
case 'ArrowRight':
|
|
133
|
+
deltaH = 1;
|
|
134
|
+
break;
|
|
135
|
+
case 'left':
|
|
136
|
+
case 'ArrowLeft':
|
|
137
|
+
deltaH = -1;
|
|
138
|
+
break;
|
|
139
|
+
case 'down':
|
|
140
|
+
case 'ArrowDown':
|
|
141
|
+
deltaV = 1;
|
|
142
|
+
break;
|
|
143
|
+
case 'up':
|
|
144
|
+
case 'ArrowUp':
|
|
145
|
+
deltaV = -1;
|
|
146
|
+
break;
|
|
147
|
+
default:
|
|
148
|
+
return cur;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Horizontal movement (left / right)
|
|
152
|
+
if (deltaH !== 0) {
|
|
153
|
+
if (rowStrict) {
|
|
154
|
+
if (hasSpans && cols > 1) {
|
|
155
|
+
const { matrix, itemPos } = buildSpannedGridMatrix(totalItems, cols, opts);
|
|
156
|
+
const curRowIdx = itemPos[cur]?.row ?? 0;
|
|
157
|
+
const rowItems = [];
|
|
158
|
+
for (const cellItem of matrix[curRowIdx] || []) {
|
|
159
|
+
if (cellItem >= 0 && !rowItems.includes(cellItem)) {
|
|
160
|
+
rowItems.push(cellItem);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const posInRow = rowItems.indexOf(cur);
|
|
164
|
+
if (posInRow < 0 || rowItems.length <= 0) return cur;
|
|
165
|
+
const nextPos = posInRow + deltaH;
|
|
166
|
+
if (nextPos >= 0 && nextPos < rowItems.length) {
|
|
167
|
+
return rowItems[nextPos];
|
|
168
|
+
}
|
|
169
|
+
if (wrapHoriz) {
|
|
170
|
+
return rowItems[((nextPos % rowItems.length) + rowItems.length) % rowItems.length];
|
|
171
|
+
}
|
|
172
|
+
return cur;
|
|
173
|
+
}
|
|
174
|
+
const rowStart = row * cols;
|
|
175
|
+
const rowLen = Math.min(cols, totalItems - rowStart);
|
|
176
|
+
if (rowLen <= 0) return cur;
|
|
177
|
+
const nextCol = col + deltaH;
|
|
178
|
+
if (nextCol >= 0 && nextCol < rowLen) {
|
|
179
|
+
return rowStart + nextCol;
|
|
180
|
+
}
|
|
181
|
+
if (wrapHoriz) {
|
|
182
|
+
return rowStart + (((nextCol % rowLen) + rowLen) % rowLen);
|
|
183
|
+
}
|
|
184
|
+
return cur;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const nextIdx = cur + deltaH;
|
|
188
|
+
if (nextIdx >= 0 && nextIdx < totalItems) {
|
|
189
|
+
return nextIdx;
|
|
190
|
+
}
|
|
191
|
+
if (wrapHoriz) {
|
|
192
|
+
return ((nextIdx % totalItems) + totalItems) % totalItems;
|
|
193
|
+
}
|
|
194
|
+
return cur;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Vertical movement (up / down) when spanned cells are configured
|
|
198
|
+
if (hasSpans && cols > 1) {
|
|
199
|
+
const { matrix, itemPos } = buildSpannedGridMatrix(totalItems, cols, opts);
|
|
200
|
+
const curInfo = itemPos[cur] || { row: 0, cols: [0] };
|
|
201
|
+
const occupiedCols = curInfo.cols.length > 0 ? curInfo.cols : [0];
|
|
202
|
+
let targetCol = occupiedCols[0];
|
|
203
|
+
if (occupiedCols.length > 1 && Number.isFinite(Number(opts.preferredCol))) {
|
|
204
|
+
const pref = Math.max(0, Math.min(cols - 1, Math.floor(Number(opts.preferredCol))));
|
|
205
|
+
if (occupiedCols.includes(pref)) {
|
|
206
|
+
targetCol = pref;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const validRowsForCol = [];
|
|
211
|
+
for (let r = 0; r < matrix.length; r += 1) {
|
|
212
|
+
if (matrix[r][targetCol] >= 0) {
|
|
213
|
+
validRowsForCol.push(r);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (validRowsForCol.length === 0) return cur;
|
|
217
|
+
|
|
218
|
+
const curRowPos = validRowsForCol.indexOf(curInfo.row);
|
|
219
|
+
const baseRowPos = curRowPos >= 0 ? curRowPos : 0;
|
|
220
|
+
const nextRowPos = baseRowPos + deltaV;
|
|
221
|
+
if (nextRowPos >= 0 && nextRowPos < validRowsForCol.length) {
|
|
222
|
+
return matrix[validRowsForCol[nextRowPos]][targetCol];
|
|
223
|
+
}
|
|
224
|
+
if (wrapVert) {
|
|
225
|
+
const wrappedPos =
|
|
226
|
+
((nextRowPos % validRowsForCol.length) + validRowsForCol.length) %
|
|
227
|
+
validRowsForCol.length;
|
|
228
|
+
return matrix[validRowsForCol[wrappedPos]][targetCol];
|
|
229
|
+
}
|
|
230
|
+
return cur;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Vertical movement (up / down) along column `col` (handling incomplete last row gaps)
|
|
234
|
+
const colRows = col < totalItems ? Math.floor((totalItems - 1 - col) / cols) + 1 : 0;
|
|
235
|
+
if (colRows <= 0) return cur;
|
|
236
|
+
|
|
237
|
+
const nextRow = row + deltaV;
|
|
238
|
+
if (nextRow >= 0 && nextRow < colRows) {
|
|
239
|
+
return nextRow * cols + col;
|
|
240
|
+
}
|
|
241
|
+
if (wrapVert) {
|
|
242
|
+
const wrappedRow = ((nextRow % colRows) + colRows) % colRows;
|
|
243
|
+
return wrappedRow * cols + col;
|
|
244
|
+
}
|
|
245
|
+
return cur;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Pure higher-order geometric utility that detects the number of columns in the first row
|
|
250
|
+
* of a 2D layout from an iterable of items and a row-offset projector function.
|
|
251
|
+
*
|
|
252
|
+
* Counts how many consecutive leading items share the same row coordinate (`y` / `top`)
|
|
253
|
+
* within the given `tolerance`. Completely independent of the DOM.
|
|
254
|
+
*
|
|
255
|
+
* @param {Iterable<*>} items - Iterable of numbers (row offsets) or arbitrary items.
|
|
256
|
+
* @param {Function|number} [getRowOffset=(x) => x] - Pure projector `(item, index) => number` (or numeric tolerance if 2nd arg is a number).
|
|
257
|
+
* @param {number} [tolerance=0] - Maximum allowed absolute difference between an item's row offset and the first item's offset.
|
|
258
|
+
* @returns {number} Detected number of columns in the first row ($\ge 1$).
|
|
259
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4ICYFM7oMZwOIwioDCUANgK5gB2AzggL4IBmMUYCARAAIDmA1hWxgQ6GAC8A9BTggytTgG5gwDFlwEipSjVrKgA|▶ Try it live in Instacode}
|
|
260
|
+
*/
|
|
261
|
+
export const detectGridColumns = (items, getRowOffset = (x) => x, tolerance = 0) => {
|
|
262
|
+
if (!items || typeof items[Symbol.iterator] !== 'function') {
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const isFn = typeof getRowOffset === 'function';
|
|
267
|
+
const projector = isFn ? getRowOffset : (x) => x;
|
|
268
|
+
const tol = Math.max(0, Number(isFn ? tolerance : getRowOffset) || 0);
|
|
269
|
+
|
|
270
|
+
let firstOffset = null;
|
|
271
|
+
let firstRowCount = 0;
|
|
272
|
+
let idx = 0;
|
|
273
|
+
|
|
274
|
+
for (const item of items) {
|
|
275
|
+
const raw = projector(item, idx++);
|
|
276
|
+
const offset = typeof raw === 'number' ? raw : Number(raw);
|
|
277
|
+
if (!Number.isFinite(offset)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (firstOffset === null) {
|
|
281
|
+
firstOffset = offset;
|
|
282
|
+
firstRowCount = 1;
|
|
283
|
+
} else if (Math.abs(offset - firstOffset) <= tol) {
|
|
284
|
+
firstRowCount += 1;
|
|
285
|
+
} else {
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return Math.max(1, firstRowCount);
|
|
291
|
+
};
|