@gkucmierz/utils 5.1.0 → 5.4.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/main.mjs +23 -3
- package/package.json +2 -3
- package/src/developer-utils/grid-navigation.mjs +175 -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
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module geometry
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const attachBounds = (tuple, leftClosed = true, rightClosed = true) => {
|
|
6
|
+
const lc = Boolean(leftClosed);
|
|
7
|
+
const rc = Boolean(rightClosed);
|
|
8
|
+
Object.defineProperties(tuple, {
|
|
9
|
+
leftClosed: { value: lc, enumerable: false, writable: false, configurable: true },
|
|
10
|
+
rightClosed: { value: rc, enumerable: false, writable: false, configurable: true },
|
|
11
|
+
});
|
|
12
|
+
return tuple;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a 1D range tuple `[min, max]` annotated with endpoint inclusivity flags
|
|
17
|
+
* (`leftClosed`, `rightClosed`, both defaulting to `true` for closed `<min, max>`).
|
|
18
|
+
* @param {number} min - Lower bound
|
|
19
|
+
* @param {number} max - Upper bound
|
|
20
|
+
* @param {boolean} [leftClosed=true] - Whether `min` is included (`<min` / `[min` vs `(min`)
|
|
21
|
+
* @param {boolean} [rightClosed=true] - Whether `max` is included (`max>` / `max]` vs `max)`)
|
|
22
|
+
* @returns {Array<number>} Annotated `[min, max]` interval tuple
|
|
23
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4JgQwHYHMCmCC+CAZjFGAgEQACWA1gK4DGYIOMAXgPT1wgA2AZwoBuYMHTYcooA|▶ Try it live in Instacode}
|
|
24
|
+
*/
|
|
25
|
+
export const range = (min, max, leftClosed = true, rightClosed = true) => {
|
|
26
|
+
const a = Number(min);
|
|
27
|
+
const b = Number(max);
|
|
28
|
+
if (a <= b) {
|
|
29
|
+
return attachBounds([a, b], leftClosed, rightClosed);
|
|
30
|
+
}
|
|
31
|
+
return attachBounds([b, a], rightClosed, leftClosed);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parses mathematical or Polish interval notation (`"[4, 6)"`, `"<4, 6)"`, `"(4, 6]"`, `"(4, 6>"`, `"[4, 6]"`, `"(4, 6)"`)
|
|
36
|
+
* into an annotated `[min, max]` interval tuple with `.leftClosed` and `.rightClosed`.
|
|
37
|
+
* @param {string|Array} input - Interval string or tuple
|
|
38
|
+
* @param {boolean} [defaultLeftClosed=true] - Fallback left closure when `input` is a 2-tuple
|
|
39
|
+
* @param {boolean} [defaultRightClosed=true] - Fallback right closure when `input` is a 2-tuple
|
|
40
|
+
* @returns {Array<number>|null} Annotated `[min, max]` tuple or `null` if invalid/empty
|
|
41
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IgQxgZwKYCU0B2A5tggL4IBmMUYCARAAJEDWArgMZgjYwBeAejZwQAG0z0A3MGDoseQiWlA|▶ Try it live in Instacode}
|
|
42
|
+
*/
|
|
43
|
+
export const parseRange = (input, defaultLeftClosed = true, defaultRightClosed = true) => {
|
|
44
|
+
if (typeof input === 'string') {
|
|
45
|
+
const trimmed = input.trim();
|
|
46
|
+
const match = /^([([<])\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)\s*,\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)\s*([)\]>])$/i.exec(
|
|
47
|
+
trimmed
|
|
48
|
+
);
|
|
49
|
+
if (!match) return null;
|
|
50
|
+
const leftChar = match[1];
|
|
51
|
+
const a = Number(match[2]);
|
|
52
|
+
const b = Number(match[3]);
|
|
53
|
+
const rightChar = match[4];
|
|
54
|
+
const lc = leftChar === '[' || leftChar === '<';
|
|
55
|
+
const rc = rightChar === ']' || rightChar === '>';
|
|
56
|
+
return normalizeInterval1D(attachBounds([a, b], lc, rc), lc, rc);
|
|
57
|
+
}
|
|
58
|
+
return normalizeInterval1D(input, defaultLeftClosed, defaultRightClosed);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Formats an interval tuple into mathematical (`"[4, 6)"`) or angle (`"<4, 6)"`) notation.
|
|
63
|
+
* @param {Array<number>} r - Interval tuple
|
|
64
|
+
* @param {'bracket'|'angle'} [style='bracket'] - Notation style for closed endpoints
|
|
65
|
+
* @returns {string} Formatted interval string
|
|
66
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IGazAQzgJUwOwHMBTBAXzRijAQCIABQgawFcBjMEYmALwHoWcEABsAzrQDcwYOhhZcBElKA|▶ Try it live in Instacode}
|
|
67
|
+
*/
|
|
68
|
+
export const formatRange = (r, style = 'bracket') => {
|
|
69
|
+
const iv = normalizeInterval1D(r);
|
|
70
|
+
if (!iv) return '∅';
|
|
71
|
+
const lc = iv.leftClosed ?? true;
|
|
72
|
+
const rc = iv.rightClosed ?? true;
|
|
73
|
+
const openL = lc ? (style === 'angle' ? '<' : '[') : '(';
|
|
74
|
+
const closeR = rc ? (style === 'angle' ? '>' : ']') : ')';
|
|
75
|
+
return `${openL}${iv[0]}, ${iv[1]}${closeR}`;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const isNonEmptyInterval = (a, b, lc, rc) => a < b || (a === b && lc && rc);
|
|
79
|
+
|
|
80
|
+
const normalizeInterval1D = (r, defaultLeftClosed = true, defaultRightClosed = true) => {
|
|
81
|
+
if (typeof r === 'string') {
|
|
82
|
+
return parseRange(r, defaultLeftClosed, defaultRightClosed);
|
|
83
|
+
}
|
|
84
|
+
if (r && typeof r === 'object' && !Array.isArray(r)) {
|
|
85
|
+
const a = Number(r.min ?? r.from ?? r[0]);
|
|
86
|
+
const b = Number(r.max ?? r.to ?? r[1]);
|
|
87
|
+
if (!Number.isFinite(a) || !Number.isFinite(b)) return null;
|
|
88
|
+
const lc = typeof r.leftClosed === 'boolean' ? r.leftClosed : Boolean(defaultLeftClosed);
|
|
89
|
+
const rc = typeof r.rightClosed === 'boolean' ? r.rightClosed : Boolean(defaultRightClosed);
|
|
90
|
+
const [lo, hi, fLo, fHi] = a <= b ? [a, b, lc, rc] : [b, a, rc, lc];
|
|
91
|
+
return isNonEmptyInterval(lo, hi, fLo, fHi) ? attachBounds([lo, hi], fLo, fHi) : null;
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(r) || r.length < 2) return null;
|
|
94
|
+
const a = Number(r[0]);
|
|
95
|
+
const b = Number(r[1]);
|
|
96
|
+
if (!Number.isFinite(a) || !Number.isFinite(b)) return null;
|
|
97
|
+
|
|
98
|
+
const rawLc =
|
|
99
|
+
typeof r[2] === 'boolean'
|
|
100
|
+
? r[2]
|
|
101
|
+
: typeof r.leftClosed === 'boolean'
|
|
102
|
+
? r.leftClosed
|
|
103
|
+
: Boolean(defaultLeftClosed);
|
|
104
|
+
const rawRc =
|
|
105
|
+
typeof r[3] === 'boolean'
|
|
106
|
+
? r[3]
|
|
107
|
+
: typeof r.rightClosed === 'boolean'
|
|
108
|
+
? r.rightClosed
|
|
109
|
+
: Boolean(defaultRightClosed);
|
|
110
|
+
|
|
111
|
+
const [lo, hi, lc, rc] = a <= b ? [a, b, rawLc, rawRc] : [b, a, rawRc, rawLc];
|
|
112
|
+
if (!isNonEmptyInterval(lo, hi, lc, rc)) return null;
|
|
113
|
+
return attachBounds([lo, hi], lc, rc);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const isSingleIntervalInput1D = (input) =>
|
|
117
|
+
typeof input === 'string' ||
|
|
118
|
+
(Array.isArray(input) && input.length >= 2 && typeof input[0] === 'number' && typeof input[1] === 'number');
|
|
119
|
+
|
|
120
|
+
const mergeIntervals1D = (intervals) => {
|
|
121
|
+
if (!Array.isArray(intervals) || intervals.length === 0) return [];
|
|
122
|
+
const valid = [];
|
|
123
|
+
for (let i = 0; i < intervals.length; i += 1) {
|
|
124
|
+
const iv = intervals[i];
|
|
125
|
+
if (Array.isArray(iv) && isNonEmptyInterval(iv[0], iv[1], iv.leftClosed ?? true, iv.rightClosed ?? true)) {
|
|
126
|
+
valid.push(attachBounds([iv[0], iv[1]], iv.leftClosed ?? true, iv.rightClosed ?? true));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (valid.length === 0) return [];
|
|
130
|
+
|
|
131
|
+
valid.sort((x, y) => {
|
|
132
|
+
if (x[0] !== y[0]) return x[0] - y[0];
|
|
133
|
+
// At identical start point, closed left endpoint `[a` comes before open `(a`
|
|
134
|
+
if (x.leftClosed !== y.leftClosed) return x.leftClosed ? -1 : 1;
|
|
135
|
+
if (x[1] !== y[1]) return x[1] - y[1];
|
|
136
|
+
return (y.rightClosed ? 1 : 0) - (x.rightClosed ? 1 : 0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const merged = [valid[0]];
|
|
140
|
+
for (let i = 1; i < valid.length; i += 1) {
|
|
141
|
+
const prev = merged[merged.length - 1];
|
|
142
|
+
const curr = valid[i];
|
|
143
|
+
|
|
144
|
+
const canMerge =
|
|
145
|
+
curr[0] < prev[1] || (curr[0] === prev[1] && (prev.rightClosed || curr.leftClosed));
|
|
146
|
+
|
|
147
|
+
if (canMerge) {
|
|
148
|
+
const nextLo = prev[0];
|
|
149
|
+
const nextLc = prev[0] === curr[0] ? prev.leftClosed || curr.leftClosed : prev.leftClosed;
|
|
150
|
+
let nextHi = prev[1];
|
|
151
|
+
let nextRc = prev.rightClosed;
|
|
152
|
+
if (curr[1] > prev[1]) {
|
|
153
|
+
nextHi = curr[1];
|
|
154
|
+
nextRc = curr.rightClosed;
|
|
155
|
+
} else if (curr[1] === prev[1]) {
|
|
156
|
+
nextRc = prev.rightClosed || curr.rightClosed;
|
|
157
|
+
}
|
|
158
|
+
merged[merged.length - 1] = attachBounds([nextLo, nextHi], nextLc, nextRc);
|
|
159
|
+
} else {
|
|
160
|
+
merged.push(curr);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return merged;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const toIntervalList1D = (input, defaultLeftClosed = true, defaultRightClosed = true) => {
|
|
167
|
+
if (input === null || input === undefined) return [];
|
|
168
|
+
if (isSingleIntervalInput1D(input)) {
|
|
169
|
+
const single = normalizeInterval1D(input, defaultLeftClosed, defaultRightClosed);
|
|
170
|
+
return single ? [single] : [];
|
|
171
|
+
}
|
|
172
|
+
if (!Array.isArray(input) || input.length === 0) return [];
|
|
173
|
+
const list = [];
|
|
174
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
175
|
+
const norm = normalizeInterval1D(input[i], defaultLeftClosed, defaultRightClosed);
|
|
176
|
+
if (norm) list.push(norm);
|
|
177
|
+
}
|
|
178
|
+
return mergeIntervals1D(list);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const intersectTwoIntervals1D = (a, b) => {
|
|
182
|
+
let lo;
|
|
183
|
+
let lc;
|
|
184
|
+
if (a[0] > b[0]) {
|
|
185
|
+
lo = a[0];
|
|
186
|
+
lc = a.leftClosed;
|
|
187
|
+
} else if (b[0] > a[0]) {
|
|
188
|
+
lo = b[0];
|
|
189
|
+
lc = b.leftClosed;
|
|
190
|
+
} else {
|
|
191
|
+
lo = a[0];
|
|
192
|
+
lc = a.leftClosed && b.leftClosed;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let hi;
|
|
196
|
+
let rc;
|
|
197
|
+
if (a[1] < b[1]) {
|
|
198
|
+
hi = a[1];
|
|
199
|
+
rc = a.rightClosed;
|
|
200
|
+
} else if (b[1] < a[1]) {
|
|
201
|
+
hi = b[1];
|
|
202
|
+
rc = b.rightClosed;
|
|
203
|
+
} else {
|
|
204
|
+
hi = a[1];
|
|
205
|
+
rc = a.rightClosed && b.rightClosed;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!isNonEmptyInterval(lo, hi, lc, rc)) return null;
|
|
209
|
+
return attachBounds([lo, hi], lc, rc);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const subtractTwoIntervals1D = (iv, cut) => {
|
|
213
|
+
const inter = intersectTwoIntervals1D(iv, cut);
|
|
214
|
+
if (!inter) return [iv];
|
|
215
|
+
|
|
216
|
+
const out = [];
|
|
217
|
+
// Left remainder: [iv[0], inter[0]] with right closure = !inter.leftClosed
|
|
218
|
+
const leftLo = iv[0];
|
|
219
|
+
const leftHi = inter[0];
|
|
220
|
+
const leftLc = iv.leftClosed;
|
|
221
|
+
const leftRc = !inter.leftClosed;
|
|
222
|
+
if (isNonEmptyInterval(leftLo, leftHi, leftLc, leftRc)) {
|
|
223
|
+
out.push(attachBounds([leftLo, leftHi], leftLc, leftRc));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Right remainder: [inter[1], iv[1]] with left closure = !inter.rightClosed
|
|
227
|
+
const rightLo = inter[1];
|
|
228
|
+
const rightHi = iv[1];
|
|
229
|
+
const rightLc = !inter.rightClosed;
|
|
230
|
+
const rightRc = iv.rightClosed;
|
|
231
|
+
if (isNonEmptyInterval(rightLo, rightHi, rightLc, rightRc)) {
|
|
232
|
+
out.push(attachBounds([rightLo, rightHi], rightLc, rightRc));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return out;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Computes the intersection (AND / mutual part) of two 1D ranges or range sets.
|
|
240
|
+
* Supports closed `<a, b>`, half-open `<a, b)` / `(a, b>`, and open `(a, b)` intervals via
|
|
241
|
+
* optional `leftClosed` and `rightClosed` booleans (both defaulting to `true`).
|
|
242
|
+
* @param {Array|string} rangeA - First 1D range (`[min, max]` or `[min, max, leftClosed, rightClosed]`) or array of ranges
|
|
243
|
+
* @param {Array|string} rangeB - Second 1D range or array of ranges
|
|
244
|
+
* @param {boolean} [leftClosed=true] - Default left-endpoint inclusivity (`true` = `<a`, `false` = `(a`)
|
|
245
|
+
* @param {boolean} [rightClosed=true] - Default right-endpoint inclusivity (`true` = `b>`, `false` = `b)`)
|
|
246
|
+
* @returns {Array<Array<number>>} Disjoint sorted array of intersecting `[min, max]` ranges
|
|
247
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4LAVzmghgGwEpYB2A5gKYIC+CAZjFGAgEQACxA1mgMZgikwBeAegwgcAZ0YBuYMHSZcBEqWlA|▶ Try it live in Instacode}
|
|
248
|
+
*/
|
|
249
|
+
export const mutualRange = (rangeA, rangeB, leftClosed = true, rightClosed = true) => {
|
|
250
|
+
const listA = toIntervalList1D(rangeA, leftClosed, rightClosed);
|
|
251
|
+
const listB = toIntervalList1D(rangeB, leftClosed, rightClosed);
|
|
252
|
+
if (listA.length === 0 || listB.length === 0) return [];
|
|
253
|
+
|
|
254
|
+
const result = [];
|
|
255
|
+
let i = 0;
|
|
256
|
+
let j = 0;
|
|
257
|
+
while (i < listA.length && j < listB.length) {
|
|
258
|
+
const inter = intersectTwoIntervals1D(listA[i], listB[j]);
|
|
259
|
+
if (inter) {
|
|
260
|
+
result.push(inter);
|
|
261
|
+
}
|
|
262
|
+
if (listA[i][1] < listB[j][1] || (listA[i][1] === listB[j][1] && !listA[i].rightClosed && listB[j].rightClosed)) {
|
|
263
|
+
i += 1;
|
|
264
|
+
} else {
|
|
265
|
+
j += 1;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return mergeIntervals1D(result);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Computes the union (OR / sum) of 1D ranges or range sets.
|
|
273
|
+
* Touching intervals at boundary `p` coalesce if and only if at least one interval includes `p` (`rightClosed || leftClosed`).
|
|
274
|
+
* Can be called as `sumRange(listOfRanges, leftClosed, rightClosed)` or `sumRange(rangeA, rangeB, leftClosed, rightClosed)`.
|
|
275
|
+
* @param {Array|string} rangeA - First 1D range or array of 1D ranges
|
|
276
|
+
* @param {Array|string|boolean} [rangeB] - Second 1D range / array of ranges, or `leftClosed` boolean when 1st arg is a list
|
|
277
|
+
* @param {boolean} [leftClosed=true] - Default left-endpoint inclusivity
|
|
278
|
+
* @param {boolean} [rightClosed=true] - Default right-endpoint inclusivity
|
|
279
|
+
* @returns {Array<Array<number>>} Disjoint sorted array of `[min, max]` ranges
|
|
280
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IM4FcwCUCGA7AcwFMEBfBAMxijAQCIABAga3QGMwQiYAvAenRwQAG1R0A3MGAZs+YpKA|▶ Try it live in Instacode}
|
|
281
|
+
*/
|
|
282
|
+
export const sumRange = (rangeA, rangeB = undefined, leftClosed = true, rightClosed = true) => {
|
|
283
|
+
if (typeof rangeB === 'boolean' || rangeB === undefined) {
|
|
284
|
+
const lc = typeof rangeB === 'boolean' ? rangeB : leftClosed;
|
|
285
|
+
const rc = typeof rangeB === 'boolean' ? Boolean(leftClosed) : rightClosed;
|
|
286
|
+
return toIntervalList1D(rangeA, lc, rc);
|
|
287
|
+
}
|
|
288
|
+
const listA = toIntervalList1D(rangeA, leftClosed, rightClosed);
|
|
289
|
+
const listB = toIntervalList1D(rangeB, leftClosed, rightClosed);
|
|
290
|
+
return mergeIntervals1D([...listA, ...listB]);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Computes the set difference (A \ B) of two 1D ranges or range sets, flipping boundary closures accurately.
|
|
295
|
+
* @param {Array|string} rangeA - Base 1D range or array of 1D ranges
|
|
296
|
+
* @param {Array|string} rangeB - Subtracted 1D range or array of 1D ranges
|
|
297
|
+
* @param {boolean} [leftClosed=true] - Default left-endpoint inclusivity
|
|
298
|
+
* @param {boolean} [rightClosed=true] - Default right-endpoint inclusivity
|
|
299
|
+
* @returns {Array<Array<number>>} Disjoint sorted array of `[min, max]` ranges in A but not in B
|
|
300
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4ICYgGYYEoEMB2A5gKYIC+CGMUYCARAAKEDWArgMZgjEwBeA9KzggANgGc6AbmDB0WPEWLSgA|▶ Try it live in Instacode}
|
|
301
|
+
*/
|
|
302
|
+
export const diffRange = (rangeA, rangeB, leftClosed = true, rightClosed = true) => {
|
|
303
|
+
let current = toIntervalList1D(rangeA, leftClosed, rightClosed);
|
|
304
|
+
const subList = toIntervalList1D(rangeB, leftClosed, rightClosed);
|
|
305
|
+
if (current.length === 0 || subList.length === 0) return current;
|
|
306
|
+
|
|
307
|
+
for (let s = 0; s < subList.length; s += 1) {
|
|
308
|
+
const cut = subList[s];
|
|
309
|
+
const next = [];
|
|
310
|
+
for (let i = 0; i < current.length; i += 1) {
|
|
311
|
+
next.push(...subtractTwoIntervals1D(current[i], cut));
|
|
312
|
+
}
|
|
313
|
+
current = next;
|
|
314
|
+
if (current.length === 0) break;
|
|
315
|
+
}
|
|
316
|
+
return current;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Computes the symmetric difference (XOR / A Δ B) of two 1D ranges or range sets.
|
|
321
|
+
* @param {Array|string} rangeA - First 1D range or array of 1D ranges
|
|
322
|
+
* @param {Array|string} rangeB - Second 1D range or array of 1D ranges
|
|
323
|
+
* @param {boolean} [leftClosed=true] - Default left-endpoint inclusivity
|
|
324
|
+
* @param {boolean} [rightClosed=true] - Default right-endpoint inclusivity
|
|
325
|
+
* @returns {Array<Array<number>>} Disjoint sorted array of `[min, max]` ranges in exactly one of A or B
|
|
326
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IB6wEoEMB2BzAUwQF8EAzGKMBAIgAE8BrAVwGMwQCYAvAembggANgGcaAbmDB0MbPgKSgA|▶ Try it live in Instacode}
|
|
327
|
+
*/
|
|
328
|
+
export const xorRange = (rangeA, rangeB, leftClosed = true, rightClosed = true) => {
|
|
329
|
+
const onlyA = diffRange(rangeA, rangeB, leftClosed, rightClosed);
|
|
330
|
+
const onlyB = diffRange(rangeB, rangeA, leftClosed, rightClosed);
|
|
331
|
+
return mergeIntervals1D([...onlyA, ...onlyB]);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Checks whether a 1D point `x` is contained in `rangeOrRanges`, respecting open vs closed endpoints.
|
|
336
|
+
* @param {Array|string} rangeOrRanges - 1D range or array of 1D ranges
|
|
337
|
+
* @param {number} x - Point coordinate to test
|
|
338
|
+
* @param {boolean} [leftClosed=true] - Default left-endpoint inclusivity
|
|
339
|
+
* @param {boolean} [rightClosed=true] - Default right-endpoint inclusivity
|
|
340
|
+
* @returns {boolean} `true` if `x` belongs to the range set
|
|
341
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IMZQHZwIYgwZwAUo84BGAEQQF8EAzGKMBAIgAEBzAawFdUwQAUxgAvAPQ84IADb4WAbmDB0WXAWKlKioA|▶ Try it live in Instacode}
|
|
342
|
+
*/
|
|
343
|
+
export const containsPoint1D = (rangeOrRanges, x, leftClosed = true, rightClosed = true) => {
|
|
344
|
+
const val = Number(x);
|
|
345
|
+
if (!Number.isFinite(val)) return false;
|
|
346
|
+
const list = toIntervalList1D(rangeOrRanges, leftClosed, rightClosed);
|
|
347
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
348
|
+
const iv = list[i];
|
|
349
|
+
const aboveMin = iv.leftClosed ? val >= iv[0] : val > iv[0];
|
|
350
|
+
const belowMax = iv.rightClosed ? val <= iv[1] : val < iv[1];
|
|
351
|
+
if (aboveMin && belowMax) return true;
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Computes the total 1D Lebesgue measure (length) of a single range `[min, max]` or array of ranges.
|
|
358
|
+
* Single points `[a, a]` have length `0`.
|
|
359
|
+
* @param {Array|string} rangeOrRanges - 1D range or array of 1D ranges
|
|
360
|
+
* @returns {number} Total 1D measure (length >= 0)
|
|
361
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4LAUwIYGcCuM0BKGAdgOZoIC+CAZjFGAgEQACpA1jgMZghowAvAPQ44IADZYmAbmDB02PIRLlZQA|▶ Try it live in Instacode}
|
|
362
|
+
*/
|
|
363
|
+
export const measureRange = (rangeOrRanges) => {
|
|
364
|
+
const list = toIntervalList1D(rangeOrRanges, true, true);
|
|
365
|
+
let total = 0;
|
|
366
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
367
|
+
total += list[i][1] - list[i][0];
|
|
368
|
+
}
|
|
369
|
+
return total;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const normalizeBoxNd = (box, defaultLeftClosed = true, defaultRightClosed = true) => {
|
|
373
|
+
if (!Array.isArray(box) || box.length === 0) return null;
|
|
374
|
+
const axes = [];
|
|
375
|
+
for (let d = 0; d < box.length; d += 1) {
|
|
376
|
+
const iv = normalizeInterval1D(box[d], defaultLeftClosed, defaultRightClosed);
|
|
377
|
+
if (!iv) return null;
|
|
378
|
+
axes.push(iv);
|
|
379
|
+
}
|
|
380
|
+
return axes;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const isSingleBoxNd = (input) =>
|
|
384
|
+
Array.isArray(input) &&
|
|
385
|
+
input.length > 0 &&
|
|
386
|
+
(typeof input[0] === 'string' ||
|
|
387
|
+
(Array.isArray(input[0]) && typeof input[0][0] === 'number' && typeof input[0][1] === 'number'));
|
|
388
|
+
|
|
389
|
+
const toBoxListNd = (input, defaultLeftClosed = true, defaultRightClosed = true) => {
|
|
390
|
+
if (!Array.isArray(input) || input.length === 0) return [];
|
|
391
|
+
if (isSingleBoxNd(input)) {
|
|
392
|
+
const b = normalizeBoxNd(input, defaultLeftClosed, defaultRightClosed);
|
|
393
|
+
return b ? [b] : [];
|
|
394
|
+
}
|
|
395
|
+
const boxes = [];
|
|
396
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
397
|
+
const b = normalizeBoxNd(input[i], defaultLeftClosed, defaultRightClosed);
|
|
398
|
+
if (b) boxes.push(b);
|
|
399
|
+
}
|
|
400
|
+
return boxes;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const cloneBoxNd = (box) =>
|
|
404
|
+
box.map((iv) => attachBounds([iv[0], iv[1]], iv.leftClosed ?? true, iv.rightClosed ?? true));
|
|
405
|
+
|
|
406
|
+
const intersectTwoBoxesNd = (boxA, boxB) => {
|
|
407
|
+
if (!boxA || !boxB || boxA.length !== boxB.length) return null;
|
|
408
|
+
const dim = boxA.length;
|
|
409
|
+
const out = new Array(dim);
|
|
410
|
+
for (let d = 0; d < dim; d += 1) {
|
|
411
|
+
const inter1D = intersectTwoIntervals1D(boxA[d], boxB[d]);
|
|
412
|
+
if (!inter1D) return null;
|
|
413
|
+
out[d] = inter1D;
|
|
414
|
+
}
|
|
415
|
+
return out;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const subtractSingleBoxNd = (boxA, boxB) => {
|
|
419
|
+
const inter = intersectTwoBoxesNd(boxA, boxB);
|
|
420
|
+
if (!inter) return [cloneBoxNd(boxA)];
|
|
421
|
+
|
|
422
|
+
const dim = boxA.length;
|
|
423
|
+
const shards = [];
|
|
424
|
+
const core = cloneBoxNd(boxA);
|
|
425
|
+
|
|
426
|
+
for (let d = 0; d < dim; d += 1) {
|
|
427
|
+
const remainders = subtractTwoIntervals1D(core[d], inter[d]);
|
|
428
|
+
for (let r = 0; r < remainders.length; r += 1) {
|
|
429
|
+
const remIv = remainders[r];
|
|
430
|
+
const shard = core.map((iv, idx) =>
|
|
431
|
+
idx === d
|
|
432
|
+
? attachBounds([remIv[0], remIv[1]], remIv.leftClosed, remIv.rightClosed)
|
|
433
|
+
: attachBounds([iv[0], iv[1]], iv.leftClosed, iv.rightClosed)
|
|
434
|
+
);
|
|
435
|
+
shards.push(shard);
|
|
436
|
+
}
|
|
437
|
+
core[d] = attachBounds([inter[d][0], inter[d][1]], inter[d].leftClosed, inter[d].rightClosed);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return shards;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const tryMergeTwoBoxesNd = (boxA, boxB) => {
|
|
444
|
+
if (!boxA || !boxB || boxA.length !== boxB.length) return null;
|
|
445
|
+
const dim = boxA.length;
|
|
446
|
+
let diffAxis = -1;
|
|
447
|
+
|
|
448
|
+
for (let d = 0; d < dim; d += 1) {
|
|
449
|
+
const sameAxis =
|
|
450
|
+
boxA[d][0] === boxB[d][0] &&
|
|
451
|
+
boxA[d][1] === boxB[d][1] &&
|
|
452
|
+
boxA[d].leftClosed === boxB[d].leftClosed &&
|
|
453
|
+
boxA[d].rightClosed === boxB[d].rightClosed;
|
|
454
|
+
if (!sameAxis) {
|
|
455
|
+
if (diffAxis !== -1) return null;
|
|
456
|
+
diffAxis = d;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (diffAxis === -1) return cloneBoxNd(boxA);
|
|
461
|
+
const mergedAxis = mergeIntervals1D([boxA[diffAxis], boxB[diffAxis]]);
|
|
462
|
+
if (mergedAxis.length !== 1) return null;
|
|
463
|
+
|
|
464
|
+
return boxA.map((iv, idx) =>
|
|
465
|
+
idx === diffAxis
|
|
466
|
+
? mergedAxis[0]
|
|
467
|
+
: attachBounds([iv[0], iv[1]], iv.leftClosed, iv.rightClosed)
|
|
468
|
+
);
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const coalesceDisjointBoxesNd = (boxes) => {
|
|
472
|
+
if (boxes.length <= 1) return boxes;
|
|
473
|
+
const result = boxes.map(cloneBoxNd);
|
|
474
|
+
let changed = true;
|
|
475
|
+
while (changed) {
|
|
476
|
+
changed = false;
|
|
477
|
+
outer: for (let i = 0; i < result.length; i += 1) {
|
|
478
|
+
for (let j = i + 1; j < result.length; j += 1) {
|
|
479
|
+
const merged = tryMergeTwoBoxesNd(result[i], result[j]);
|
|
480
|
+
if (merged) {
|
|
481
|
+
result[i] = merged;
|
|
482
|
+
result.splice(j, 1);
|
|
483
|
+
changed = true;
|
|
484
|
+
break outer;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const disjointizeBoxesNd = (boxes) => {
|
|
493
|
+
const disjoint = [];
|
|
494
|
+
for (let i = 0; i < boxes.length; i += 1) {
|
|
495
|
+
let candidates = [cloneBoxNd(boxes[i])];
|
|
496
|
+
for (let j = 0; j < disjoint.length && candidates.length > 0; j += 1) {
|
|
497
|
+
const existing = disjoint[j];
|
|
498
|
+
const nextCandidates = [];
|
|
499
|
+
for (let c = 0; c < candidates.length; c += 1) {
|
|
500
|
+
nextCandidates.push(...subtractSingleBoxNd(candidates[c], existing));
|
|
501
|
+
}
|
|
502
|
+
candidates = nextCandidates;
|
|
503
|
+
}
|
|
504
|
+
disjoint.push(...candidates);
|
|
505
|
+
}
|
|
506
|
+
return coalesceDisjointBoxesNd(disjoint);
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Computes the N-dimensional intersection (AND) of two N-orthotopes (hyperrectangles) or sets of N-orthotopes.
|
|
511
|
+
* Each N-box is represented as `[[min_1, max_1], [min_2, max_2], ..., [min_N, max_N]]`.
|
|
512
|
+
* @param {Array} boxA - First N-box or array of N-boxes
|
|
513
|
+
* @param {Array} boxB - Second N-box or array of N-boxes
|
|
514
|
+
* @param {boolean} [leftClosed=true] - Default left-endpoint inclusivity on each axis
|
|
515
|
+
* @param {boolean} [rightClosed=true] - Default right-endpoint inclusivity on each axis
|
|
516
|
+
* @returns {Array<Array<Array<number>>>} Array of disjoint intersecting N-boxes
|
|
517
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4LAVzmghgGwEJQAeCAvggGYxRgIBEAAgOYDWaAxmCAKYwBeA9BhA4AzrQDcwYOky4ChSUA|▶ Try it live in Instacode}
|
|
518
|
+
*/
|
|
519
|
+
export const mutualBox = (boxA, boxB, leftClosed = true, rightClosed = true) => {
|
|
520
|
+
const listA = disjointizeBoxesNd(toBoxListNd(boxA, leftClosed, rightClosed));
|
|
521
|
+
const listB = disjointizeBoxesNd(toBoxListNd(boxB, leftClosed, rightClosed));
|
|
522
|
+
if (listA.length === 0 || listB.length === 0) return [];
|
|
523
|
+
|
|
524
|
+
const out = [];
|
|
525
|
+
for (let i = 0; i < listA.length; i += 1) {
|
|
526
|
+
for (let j = 0; j < listB.length; j += 1) {
|
|
527
|
+
const inter = intersectTwoBoxesNd(listA[i], listB[j]);
|
|
528
|
+
if (inter) out.push(inter);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return coalesceDisjointBoxesNd(out);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Computes the N-dimensional set difference (A \ B) decomposed into pairwise-disjoint N-orthotopes.
|
|
536
|
+
* @param {Array} boxA - Base N-box or array of N-boxes
|
|
537
|
+
* @param {Array} boxB - Subtracted N-box or array of N-boxes
|
|
538
|
+
* @param {boolean} [leftClosed=true] - Default left-endpoint inclusivity on each axis
|
|
539
|
+
* @param {boolean} [rightClosed=true] - Default right-endpoint inclusivity on each axis
|
|
540
|
+
* @returns {Array<Array<Array<number>>>} Pairwise-disjoint N-boxes covering A \ B
|
|
541
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4ICYgGYYEJQB4IC+CGMUYCARAAIDmA1gK4DGYIApjAF4D0jcIADYBnSgG5gwdFlx4JQA|▶ Try it live in Instacode}
|
|
542
|
+
*/
|
|
543
|
+
export const diffBox = (boxA, boxB, leftClosed = true, rightClosed = true) => {
|
|
544
|
+
let current = disjointizeBoxesNd(toBoxListNd(boxA, leftClosed, rightClosed));
|
|
545
|
+
const subList = toBoxListNd(boxB, leftClosed, rightClosed);
|
|
546
|
+
if (current.length === 0 || subList.length === 0) return current;
|
|
547
|
+
|
|
548
|
+
for (let s = 0; s < subList.length; s += 1) {
|
|
549
|
+
const cut = subList[s];
|
|
550
|
+
const next = [];
|
|
551
|
+
for (let i = 0; i < current.length; i += 1) {
|
|
552
|
+
next.push(...subtractSingleBoxNd(current[i], cut));
|
|
553
|
+
}
|
|
554
|
+
current = next;
|
|
555
|
+
if (current.length === 0) break;
|
|
556
|
+
}
|
|
557
|
+
return coalesceDisjointBoxesNd(current);
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Computes the N-dimensional union (OR) decomposed into pairwise-disjoint N-orthotopes.
|
|
562
|
+
* @param {Array} boxA - First N-box or array of N-boxes
|
|
563
|
+
* @param {Array} [boxB] - Second N-box or array of N-boxes
|
|
564
|
+
* @param {boolean} [leftClosed=true] - Default left-endpoint inclusivity on each axis
|
|
565
|
+
* @param {boolean} [rightClosed=true] - Default right-endpoint inclusivity on each axis
|
|
566
|
+
* @returns {Array<Array<Array<number>>>} Pairwise-disjoint N-boxes covering A U B
|
|
567
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IM4FcwCEoA8EC+CAZjFGAgEQACA5gNboDGYIApjAF4D06cIAG1SUA3MGAZseMUA|▶ Try it live in Instacode}
|
|
568
|
+
*/
|
|
569
|
+
export const sumBox = (boxA, boxB = undefined, leftClosed = true, rightClosed = true) => {
|
|
570
|
+
if (typeof boxB === 'boolean' || boxB === undefined) {
|
|
571
|
+
const lc = typeof boxB === 'boolean' ? boxB : leftClosed;
|
|
572
|
+
const rc = typeof boxB === 'boolean' ? Boolean(leftClosed) : rightClosed;
|
|
573
|
+
return disjointizeBoxesNd(toBoxListNd(boxA, lc, rc));
|
|
574
|
+
}
|
|
575
|
+
const listA = disjointizeBoxesNd(toBoxListNd(boxA, leftClosed, rightClosed));
|
|
576
|
+
const listB = toBoxListNd(boxB, leftClosed, rightClosed);
|
|
577
|
+
return disjointizeBoxesNd([...listA, ...listB]);
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Computes the N-dimensional symmetric difference (XOR / A Δ B) as pairwise-disjoint N-orthotopes.
|
|
582
|
+
* @param {Array} boxA - First N-box or array of N-boxes
|
|
583
|
+
* @param {Array} boxB - Second N-box or array of N-boxes
|
|
584
|
+
* @param {boolean} [leftClosed=true] - Default left-endpoint inclusivity on each axis
|
|
585
|
+
* @param {boolean} [rightClosed=true] - Default right-endpoint inclusivity on each axis
|
|
586
|
+
* @returns {Array<Array<Array<number>>>} Pairwise-disjoint N-boxes covering A Δ B
|
|
587
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IB6wEJVQgvggMxijAQCIABAcwGsBXAYzBAFMYAvAejrhABsAzmQDcwYOhhZUooA|▶ Try it live in Instacode}
|
|
588
|
+
*/
|
|
589
|
+
export const xorBox = (boxA, boxB, leftClosed = true, rightClosed = true) => {
|
|
590
|
+
const onlyA = diffBox(boxA, boxB, leftClosed, rightClosed);
|
|
591
|
+
const onlyB = diffBox(boxB, boxA, leftClosed, rightClosed);
|
|
592
|
+
return coalesceDisjointBoxesNd([...onlyA, ...onlyB]);
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Checks whether an N-dimensional point `[x_1, x_2, ..., x_N]` belongs to `boxOrBoxes`,
|
|
597
|
+
* respecting open vs closed boundaries on every axis.
|
|
598
|
+
* @param {Array} boxOrBoxes - Single N-box or array of N-boxes
|
|
599
|
+
* @param {Array<number>} point - N-dimensional coordinate vector
|
|
600
|
+
* @param {boolean} [leftClosed=true] - Default left-endpoint inclusivity
|
|
601
|
+
* @param {boolean} [rightClosed=true] - Default right-endpoint inclusivity
|
|
602
|
+
* @returns {boolean} `true` if `point` lies inside the N-orthotope set
|
|
603
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IMZQHZwIYgwZwAUo84A5AEwQF8EAzGKMBAIgAEBzAawFdUwQAUxgAvAPQ84IADb4WAbmDB0WXAWKlKioA|▶ Try it live in Instacode}
|
|
604
|
+
*/
|
|
605
|
+
export const containsPointNd = (boxOrBoxes, point, leftClosed = true, rightClosed = true) => {
|
|
606
|
+
if (!Array.isArray(point) || point.length === 0) return false;
|
|
607
|
+
const boxes = toBoxListNd(boxOrBoxes, leftClosed, rightClosed);
|
|
608
|
+
for (let i = 0; i < boxes.length; i += 1) {
|
|
609
|
+
const b = boxes[i];
|
|
610
|
+
if (b.length !== point.length) continue;
|
|
611
|
+
let insideAllAxes = true;
|
|
612
|
+
for (let d = 0; d < b.length; d += 1) {
|
|
613
|
+
const val = Number(point[d]);
|
|
614
|
+
const iv = b[d];
|
|
615
|
+
const aboveMin = iv.leftClosed ? val >= iv[0] : val > iv[0];
|
|
616
|
+
const belowMax = iv.rightClosed ? val <= iv[1] : val < iv[1];
|
|
617
|
+
if (!aboveMin || !belowMax) {
|
|
618
|
+
insideAllAxes = false;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (insideAllAxes) return true;
|
|
623
|
+
}
|
|
624
|
+
return false;
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Computes the Lebesgue measure (1D length, 2D area, 3D volume, 4D hypervolume, N-D measure)
|
|
629
|
+
* of a single N-box `[[min_1, max_1], ..., [min_N, max_N]]` or an array of N-boxes.
|
|
630
|
+
* Automatically decomposes overlapping N-boxes into disjoint shards so overlapping regions are never double-counted.
|
|
631
|
+
* @param {Array} boxOrBoxes - Single N-box or array of N-boxes
|
|
632
|
+
* @returns {number} Exact N-dimensional measure
|
|
633
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4LAUwIYGcCuM0BCUAHggL4IBmMUYCARAAIDmA1jgMZghowBeAehxwQAGyz0A3MGDpseQiWlA|▶ Try it live in Instacode}
|
|
634
|
+
*/
|
|
635
|
+
export const measureBox = (boxOrBoxes) => {
|
|
636
|
+
const disjoint = disjointizeBoxesNd(toBoxListNd(boxOrBoxes, true, true));
|
|
637
|
+
let total = 0;
|
|
638
|
+
for (let i = 0; i < disjoint.length; i += 1) {
|
|
639
|
+
const b = disjoint[i];
|
|
640
|
+
let prod = 1;
|
|
641
|
+
for (let d = 0; d < b.length; d += 1) {
|
|
642
|
+
prod *= b[d][1] - b[d][0];
|
|
643
|
+
}
|
|
644
|
+
total += prod;
|
|
645
|
+
}
|
|
646
|
+
return total;
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Fast O(N) calculation of the shared N-dimensional measure (intersection length/area/volume/hypervolume)
|
|
651
|
+
* of two single N-orthotopes without allocating shard arrays.
|
|
652
|
+
* @param {Array<Array<number>>} boxA - First N-box `[[a1, b1], ..., [aN, bN]]`
|
|
653
|
+
* @param {Array<Array<number>>} boxB - Second N-box `[[c1, d1], ..., [cN, dN]]`
|
|
654
|
+
* @returns {number} Shared N-dimensional measure (>= 0)
|
|
655
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IM4AsCGMCmATAWR01QFdcA5PBAXwQDMYowEAiAAQHMBrUgYzAgcMAF4B6UnBAAbVKwDcwYBmz4iJcjiqKgA|▶ Try it live in Instacode}
|
|
656
|
+
*/
|
|
657
|
+
export const sharedMeasureNd = (boxA, boxB) => {
|
|
658
|
+
if (!Array.isArray(boxA) || !Array.isArray(boxB) || boxA.length === 0 || boxA.length !== boxB.length) {
|
|
659
|
+
return 0;
|
|
660
|
+
}
|
|
661
|
+
let measure = 1;
|
|
662
|
+
for (let d = 0; d < boxA.length; d += 1) {
|
|
663
|
+
const a0 = Math.min(boxA[d][0], boxA[d][1]);
|
|
664
|
+
const a1 = Math.max(boxA[d][0], boxA[d][1]);
|
|
665
|
+
const b0 = Math.min(boxB[d][0], boxB[d][1]);
|
|
666
|
+
const b1 = Math.max(boxB[d][0], boxB[d][1]);
|
|
667
|
+
const overlap = Math.min(a1, b1) - Math.max(a0, b0);
|
|
668
|
+
if (overlap <= 0) return 0;
|
|
669
|
+
measure *= overlap;
|
|
670
|
+
}
|
|
671
|
+
return measure;
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Chainable N-dimensional Orthotope (Hyperrectangle) Set supporting Boolean algebra
|
|
676
|
+
* (`and`, `or`, `sub`, `xor`), point membership (`contains`), and Lebesgue measure (`measure`)
|
|
677
|
+
* in any dimension N >= 1, with full open/closed boundary awareness.
|
|
678
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IPLwBZQMoFNEC+CAZjFGAgEQACA5gNYCuAxmCLjAF4D0jcIAGwDOlANzBg6OFjxxxQA|▶ Try it live in Instacode}
|
|
679
|
+
*/
|
|
680
|
+
export class OrthoSet {
|
|
681
|
+
/**
|
|
682
|
+
* @param {Array} [boxes=[]] - Single N-box or array of N-boxes (or 1D interval)
|
|
683
|
+
* @param {boolean} [leftClosed=true] - Default left-endpoint inclusivity
|
|
684
|
+
* @param {boolean} [rightClosed=true] - Default right-endpoint inclusivity
|
|
685
|
+
*/
|
|
686
|
+
constructor(boxes = [], leftClosed = true, rightClosed = true) {
|
|
687
|
+
this.leftClosed = Boolean(leftClosed);
|
|
688
|
+
this.rightClosed = Boolean(rightClosed);
|
|
689
|
+
if (Array.isArray(boxes) && boxes.length >= 2 && typeof boxes[0] === 'number' && typeof boxes[1] === 'number') {
|
|
690
|
+
const iv = normalizeInterval1D(boxes, this.leftClosed, this.rightClosed);
|
|
691
|
+
this.boxes = iv ? [[iv]] : [];
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
this.boxes = disjointizeBoxesNd(toBoxListNd(boxes, this.leftClosed, this.rightClosed));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
static fromRect2D(rect, leftClosed = true, rightClosed = true) {
|
|
698
|
+
if (!rect || typeof rect !== 'object') return new OrthoSet([], leftClosed, rightClosed);
|
|
699
|
+
const left = Number(rect.left ?? rect.x ?? 0);
|
|
700
|
+
const top = Number(rect.top ?? rect.y ?? 0);
|
|
701
|
+
const right = left + Number(rect.width ?? 0);
|
|
702
|
+
const bottom = top + Number(rect.height ?? 0);
|
|
703
|
+
return new OrthoSet(
|
|
704
|
+
[
|
|
705
|
+
[left, right],
|
|
706
|
+
[top, bottom],
|
|
707
|
+
],
|
|
708
|
+
leftClosed,
|
|
709
|
+
rightClosed
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
and(other, leftClosed = this.leftClosed, rightClosed = this.rightClosed) {
|
|
714
|
+
const rhs = other instanceof OrthoSet ? other.boxes : other;
|
|
715
|
+
return new OrthoSet(mutualBox(this.boxes, rhs, leftClosed, rightClosed), leftClosed, rightClosed);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
or(other, leftClosed = this.leftClosed, rightClosed = this.rightClosed) {
|
|
719
|
+
const rhs = other instanceof OrthoSet ? other.boxes : other;
|
|
720
|
+
return new OrthoSet(sumBox(this.boxes, rhs, leftClosed, rightClosed), leftClosed, rightClosed);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
sub(other, leftClosed = this.leftClosed, rightClosed = this.rightClosed) {
|
|
724
|
+
const rhs = other instanceof OrthoSet ? other.boxes : other;
|
|
725
|
+
return new OrthoSet(diffBox(this.boxes, rhs, leftClosed, rightClosed), leftClosed, rightClosed);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
xor(other, leftClosed = this.leftClosed, rightClosed = this.rightClosed) {
|
|
729
|
+
const rhs = other instanceof OrthoSet ? other.boxes : other;
|
|
730
|
+
return new OrthoSet(xorBox(this.boxes, rhs, leftClosed, rightClosed), leftClosed, rightClosed);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
contains(point) {
|
|
734
|
+
return containsPointNd(this.boxes, point, this.leftClosed, this.rightClosed);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
measure() {
|
|
738
|
+
return measureBox(this.boxes);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
isEmpty() {
|
|
742
|
+
return this.boxes.length === 0;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
toArray() {
|
|
746
|
+
return this.boxes.map((b) => b.map((iv) => attachBounds([iv[0], iv[1]], iv.leftClosed, iv.rightClosed)));
|
|
747
|
+
}
|
|
748
|
+
}
|