@virtual-content/core 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1395 @@
1
+ // src/fenwick-tree.ts
2
+ var EPSILON = 1e-9;
3
+ var FenwickTree = class {
4
+ constructor(values) {
5
+ this.values = new Array(values.length).fill(0);
6
+ this.tree = new Array(values.length + 1).fill(0);
7
+ for (let index = 0; index < values.length; index += 1) {
8
+ this.add(index, values[index] ?? 0);
9
+ }
10
+ }
11
+ get size() {
12
+ return this.values.length;
13
+ }
14
+ get(index) {
15
+ if (!this.hasIndex(index)) {
16
+ return 0;
17
+ }
18
+ return this.values[index] ?? 0;
19
+ }
20
+ add(index, delta) {
21
+ if (!this.hasIndex(index) || !Number.isFinite(delta) || delta === 0) {
22
+ return;
23
+ }
24
+ this.values[index] = (this.values[index] ?? 0) + delta;
25
+ let cursor = index + 1;
26
+ while (cursor < this.tree.length) {
27
+ this.tree[cursor] = (this.tree[cursor] ?? 0) + delta;
28
+ cursor += cursor & -cursor;
29
+ }
30
+ }
31
+ set(index, value) {
32
+ if (!this.hasIndex(index) || !Number.isFinite(value)) {
33
+ return 0;
34
+ }
35
+ const previous = this.values[index] ?? 0;
36
+ const delta = value - previous;
37
+ this.add(index, delta);
38
+ return delta;
39
+ }
40
+ sum(endExclusive) {
41
+ let cursor = clampInteger(endExclusive, 0, this.values.length);
42
+ let total = 0;
43
+ while (cursor > 0) {
44
+ total += this.tree[cursor] ?? 0;
45
+ cursor -= cursor & -cursor;
46
+ }
47
+ return total;
48
+ }
49
+ total() {
50
+ return this.sum(this.values.length);
51
+ }
52
+ lowerBound(offset) {
53
+ if (this.values.length === 0) {
54
+ return -1;
55
+ }
56
+ let target = clampNumber(offset, 0, Math.max(0, this.total() - EPSILON));
57
+ let index = 0;
58
+ let bit = 1 << this.values.length.toString(2).length - 1;
59
+ while (bit !== 0) {
60
+ const nextIndex = index + bit;
61
+ const nextValue = this.tree[nextIndex] ?? 0;
62
+ if (nextIndex <= this.values.length && nextValue <= target) {
63
+ target -= nextValue;
64
+ index = nextIndex;
65
+ }
66
+ bit >>= 1;
67
+ }
68
+ return Math.min(index, this.values.length - 1);
69
+ }
70
+ hasIndex(index) {
71
+ return Number.isInteger(index) && index >= 0 && index < this.values.length;
72
+ }
73
+ };
74
+ function clampInteger(value, min, max) {
75
+ if (!Number.isFinite(value)) {
76
+ return min;
77
+ }
78
+ return Math.min(max, Math.max(min, Math.trunc(value)));
79
+ }
80
+ function clampNumber(value, min, max) {
81
+ if (!Number.isFinite(value)) {
82
+ return min;
83
+ }
84
+ return Math.min(max, Math.max(min, value));
85
+ }
86
+
87
+ // src/geometry.ts
88
+ function createPoint(x = 0, y = 0) {
89
+ return { x: normalizeNumber(x), y: normalizeNumber(y) };
90
+ }
91
+ function createRect(x = 0, y = 0, width = 0, height = 0) {
92
+ return {
93
+ x: normalizeNumber(x),
94
+ y: normalizeNumber(y),
95
+ width: normalizeSize(width),
96
+ height: normalizeSize(height)
97
+ };
98
+ }
99
+ function rectEndX(rect) {
100
+ return rect.x + rect.width;
101
+ }
102
+ function rectEndY(rect) {
103
+ return rect.y + rect.height;
104
+ }
105
+ function rectsIntersect(a, b) {
106
+ return a.x < rectEndX(b) && rectEndX(a) > b.x && a.y < rectEndY(b) && rectEndY(a) > b.y;
107
+ }
108
+ function expandRect(rect, overscan, axis) {
109
+ const before = normalizeSize(overscan.before);
110
+ const after = normalizeSize(overscan.after);
111
+ if (axis === "horizontal") {
112
+ return createRect(rect.x - before, rect.y, rect.width + before + after, rect.height);
113
+ }
114
+ if (axis === "both") {
115
+ return createRect(rect.x - before, rect.y - before, rect.width + before + after, rect.height + before + after);
116
+ }
117
+ return createRect(rect.x, rect.y - before, rect.width, rect.height + before + after);
118
+ }
119
+ function rectFromScrollPoint(point, size) {
120
+ return createRect(point.x, point.y, size.width, size.height);
121
+ }
122
+ function mainStart(rect, direction) {
123
+ return direction === "horizontal" ? rect.x : rect.y;
124
+ }
125
+ function mainSize(rect, direction) {
126
+ return direction === "horizontal" ? rect.width : rect.height;
127
+ }
128
+ function crossSize(rect, direction) {
129
+ return direction === "horizontal" ? rect.height : rect.width;
130
+ }
131
+ function rectFromMainCross(params) {
132
+ const crossStart2 = normalizeNumber(params.crossStart ?? 0);
133
+ const resolvedMainStart = normalizeNumber(params.mainStart);
134
+ const resolvedMainSize = normalizeSize(params.mainSize);
135
+ const resolvedCrossSize = normalizeSize(params.crossSize ?? 0);
136
+ return params.direction === "horizontal" ? createRect(resolvedMainStart, crossStart2, resolvedMainSize, resolvedCrossSize) : createRect(crossStart2, resolvedMainStart, resolvedCrossSize, resolvedMainSize);
137
+ }
138
+ function pointFromMainCross(params) {
139
+ const main = normalizeNumber(params.main);
140
+ const cross = normalizeNumber(params.cross ?? 0);
141
+ return params.direction === "horizontal" ? { x: main, y: cross } : { x: cross, y: main };
142
+ }
143
+ function unionRect(rects) {
144
+ if (rects.length === 0) {
145
+ return createRect();
146
+ }
147
+ let minX = Number.POSITIVE_INFINITY;
148
+ let minY = Number.POSITIVE_INFINITY;
149
+ let maxX = 0;
150
+ let maxY = 0;
151
+ for (const rect of rects) {
152
+ minX = Math.min(minX, rect.x);
153
+ minY = Math.min(minY, rect.y);
154
+ maxX = Math.max(maxX, rectEndX(rect));
155
+ maxY = Math.max(maxY, rectEndY(rect));
156
+ }
157
+ return createRect(Math.min(0, minX), Math.min(0, minY), maxX - Math.min(0, minX), maxY - Math.min(0, minY));
158
+ }
159
+ function normalizeNumber(value) {
160
+ return Number.isFinite(value) ? value : 0;
161
+ }
162
+ function normalizeSize(value) {
163
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
164
+ }
165
+
166
+ // src/size-model.ts
167
+ var SizeModel = class {
168
+ constructor(options) {
169
+ this.keys = [];
170
+ this.measured = [];
171
+ this.measuredSizesByKey = /* @__PURE__ */ new Map();
172
+ this.tree = new FenwickTree([]);
173
+ this.keyToIndex = /* @__PURE__ */ new Map();
174
+ this.countValue = normalizeCount(options.count);
175
+ this.estimateSize = options.estimateSize;
176
+ this.getKey = options.getKey;
177
+ this.minSize = normalizeMinSize(options.minSize);
178
+ this.rebuild();
179
+ }
180
+ get count() {
181
+ return this.countValue;
182
+ }
183
+ setCount(count) {
184
+ const nextCount = normalizeCount(count);
185
+ if (nextCount === this.countValue) {
186
+ return;
187
+ }
188
+ this.countValue = nextCount;
189
+ this.rebuild();
190
+ }
191
+ keyOf(index) {
192
+ return this.keys[index];
193
+ }
194
+ indexOfKey(key) {
195
+ return this.keyToIndex.get(key) ?? -1;
196
+ }
197
+ sizeOf(index) {
198
+ return this.tree.get(index);
199
+ }
200
+ isMeasured(index) {
201
+ return this.measured[index] ?? false;
202
+ }
203
+ startOf(index) {
204
+ return this.tree.sum(index);
205
+ }
206
+ endOf(index) {
207
+ return this.tree.sum(index + 1);
208
+ }
209
+ spacedStartOf(index, spacing) {
210
+ return this.startOf(index) + spacingStart(spacing) + Math.max(0, index) * spacingGap(spacing);
211
+ }
212
+ spacedEndOf(index, spacing) {
213
+ return this.spacedStartOf(index, spacing) + this.sizeOf(index);
214
+ }
215
+ totalSize() {
216
+ return this.tree.total();
217
+ }
218
+ spacedTotalSize(spacing) {
219
+ const count = this.countValue;
220
+ return this.tree.total() + spacingStart(spacing) + spacingEnd(spacing) + Math.max(0, count - 1) * spacingGap(spacing);
221
+ }
222
+ lowerBound(offset) {
223
+ return this.tree.lowerBound(offset);
224
+ }
225
+ spacedLowerBound(offset, spacing) {
226
+ if (this.countValue === 0) {
227
+ return 0;
228
+ }
229
+ const gap = spacingGap(spacing);
230
+ const paddingStart = spacingStart(spacing);
231
+ const target = offset - paddingStart;
232
+ if (target <= 0 || gap === 0) {
233
+ return this.lowerBound(Math.max(0, target));
234
+ }
235
+ let low = 0;
236
+ let high = this.countValue - 1;
237
+ while (low < high) {
238
+ const mid = Math.floor((low + high) / 2);
239
+ if (this.endOf(mid) + mid * gap > target) {
240
+ high = mid;
241
+ } else {
242
+ low = mid + 1;
243
+ }
244
+ }
245
+ return low;
246
+ }
247
+ measure(key, index, size) {
248
+ if (!Number.isInteger(index) || index < 0 || index >= this.countValue) {
249
+ return { delta: 0, changed: false, stale: true, size: 0 };
250
+ }
251
+ const currentKey = this.keys[index];
252
+ if (currentKey !== key) {
253
+ return { delta: 0, changed: false, stale: true, size: this.sizeOf(index) };
254
+ }
255
+ const nextSize = this.normalizeSize(size);
256
+ this.measuredSizesByKey.set(key, nextSize);
257
+ this.measured[index] = true;
258
+ const delta = this.tree.set(index, nextSize);
259
+ return {
260
+ delta,
261
+ changed: delta !== 0,
262
+ stale: false,
263
+ size: nextSize
264
+ };
265
+ }
266
+ resetMeasurements(keys) {
267
+ if (keys === void 0) {
268
+ this.measuredSizesByKey.clear();
269
+ } else {
270
+ for (const key of keys) {
271
+ this.measuredSizesByKey.delete(key);
272
+ }
273
+ }
274
+ this.rebuild();
275
+ }
276
+ rebuild() {
277
+ const keys = [];
278
+ const sizes = [];
279
+ const measured = [];
280
+ const keyToIndex = /* @__PURE__ */ new Map();
281
+ for (let index = 0; index < this.countValue; index += 1) {
282
+ const key = this.getKey(index);
283
+ const measuredSize = this.measuredSizesByKey.get(key);
284
+ keys.push(key);
285
+ keyToIndex.set(key, index);
286
+ measured.push(measuredSize !== void 0);
287
+ sizes.push(measuredSize ?? this.normalizeSize(this.estimateSize(index)));
288
+ }
289
+ this.keys = keys;
290
+ this.measured = measured;
291
+ this.keyToIndex = keyToIndex;
292
+ this.tree = new FenwickTree(sizes);
293
+ }
294
+ normalizeSize(size) {
295
+ if (!Number.isFinite(size)) {
296
+ return this.minSize;
297
+ }
298
+ return Math.max(this.minSize, size);
299
+ }
300
+ };
301
+ function normalizeCount(count) {
302
+ if (!Number.isFinite(count)) {
303
+ return 0;
304
+ }
305
+ return Math.max(0, Math.trunc(count));
306
+ }
307
+ function normalizeMinSize(minSize) {
308
+ if (!Number.isFinite(minSize) || minSize === void 0) {
309
+ return 1;
310
+ }
311
+ return Math.max(Number.EPSILON, minSize);
312
+ }
313
+ function spacingGap(spacing) {
314
+ return normalizeSpacing(spacing?.gap ?? 0);
315
+ }
316
+ function spacingStart(spacing) {
317
+ return normalizeSpacing(spacing?.paddingStart ?? 0);
318
+ }
319
+ function spacingEnd(spacing) {
320
+ return normalizeSpacing(spacing?.paddingEnd ?? 0);
321
+ }
322
+ function normalizeSpacing(value) {
323
+ if (!Number.isFinite(value)) {
324
+ return 0;
325
+ }
326
+ return Math.max(0, value);
327
+ }
328
+
329
+ // src/range.ts
330
+ function resolveOverscan(overscan, budget) {
331
+ if (typeof overscan === "number") {
332
+ const value = normalizeOverscanValue(overscan);
333
+ return {
334
+ before: value,
335
+ after: value,
336
+ maxItems: normalizeOptionalPositiveNumber(budget?.maxItems),
337
+ maxPx: normalizeOptionalPositiveNumber(budget?.maxPx)
338
+ };
339
+ }
340
+ return {
341
+ before: normalizeOverscanValue(overscan?.before ?? 0),
342
+ after: normalizeOverscanValue(overscan?.after ?? 0),
343
+ maxItems: normalizeOptionalPositiveNumber(budget?.maxItems),
344
+ maxPx: normalizeOptionalPositiveNumber(budget?.maxPx)
345
+ };
346
+ }
347
+ function materializeRange(params) {
348
+ const { sizes, scrollOffset, viewportSize, overscan, centerIndex, centerStart, spacing } = params;
349
+ const maxItems = params.maxItems ?? overscan.maxItems ?? Number.POSITIVE_INFINITY;
350
+ const maxPx = params.maxPx ?? overscan.maxPx ?? Number.POSITIVE_INFINITY;
351
+ if (sizes.count === 0 || centerIndex < 0) {
352
+ return { rangeStart: 0, rangeEnd: 0, items: [] };
353
+ }
354
+ const totalSize = sizes.spacedTotalSize(spacing);
355
+ const renderStart = Math.max(0, scrollOffset - overscan.before);
356
+ const renderEnd = Math.min(totalSize, scrollOffset + viewportSize + overscan.after);
357
+ const gap = normalizeSpacing2(spacing?.gap ?? 0);
358
+ let rangeStart = centerIndex;
359
+ let cursorStart = centerStart;
360
+ let rangeEnd = centerIndex + 1;
361
+ let cursorEnd = sizes.spacedEndOf(centerIndex, spacing);
362
+ while (rangeStart > 0 && cursorStart > renderStart) {
363
+ const previousSize = sizes.sizeOf(rangeStart - 1);
364
+ if (rangeEndCount(rangeStart - 1, rangeEnd) > maxItems) {
365
+ break;
366
+ }
367
+ if (cursorEndSpan(cursorStart - gap - previousSize, cursorEnd) > maxPx) {
368
+ break;
369
+ }
370
+ rangeStart -= 1;
371
+ cursorStart -= gap + previousSize;
372
+ }
373
+ while (rangeEnd < sizes.count && cursorEnd < renderEnd) {
374
+ const nextSize = sizes.sizeOf(rangeEnd);
375
+ if (rangeEndCount(rangeStart, rangeEnd + 1) > maxItems) {
376
+ break;
377
+ }
378
+ if (cursorEndSpan(cursorStart, cursorEnd + gap + nextSize) > maxPx) {
379
+ break;
380
+ }
381
+ cursorEnd += gap + nextSize;
382
+ rangeEnd += 1;
383
+ }
384
+ const items = [];
385
+ let itemStart = cursorStart;
386
+ for (let index = rangeStart; index < rangeEnd; index += 1) {
387
+ const size = sizes.sizeOf(index);
388
+ const key = sizes.keyOf(index);
389
+ if (key === void 0) {
390
+ continue;
391
+ }
392
+ const offsetFromCenter = itemStart - centerStart;
393
+ items.push(params.createItem?.(index, key, itemStart, size, offsetFromCenter, sizes.isMeasured(index)) ?? {
394
+ index,
395
+ key,
396
+ rect: createRect(0, itemStart, 0, size),
397
+ start: itemStart,
398
+ size,
399
+ end: itemStart + size,
400
+ offsetFromCenter,
401
+ isMeasured: sizes.isMeasured(index)
402
+ });
403
+ itemStart += size + gap;
404
+ }
405
+ return { rangeStart, rangeEnd, items };
406
+ }
407
+ function rangeEndCount(start, end) {
408
+ return end - start;
409
+ }
410
+ function cursorEndSpan(start, end) {
411
+ return end - start;
412
+ }
413
+ function normalizeOverscanValue(value) {
414
+ if (!Number.isFinite(value)) {
415
+ return 0;
416
+ }
417
+ return Math.max(0, value);
418
+ }
419
+ function normalizeOptionalPositiveNumber(value) {
420
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) {
421
+ return void 0;
422
+ }
423
+ return value;
424
+ }
425
+ function normalizeSpacing2(value) {
426
+ if (!Number.isFinite(value)) {
427
+ return 0;
428
+ }
429
+ return Math.max(0, value);
430
+ }
431
+
432
+ // src/spacing.ts
433
+ function resolveLayoutGap(gap) {
434
+ if (typeof gap === "number") {
435
+ const value = normalizeSpacingValue(gap);
436
+ return { main: value, cross: value };
437
+ }
438
+ return {
439
+ main: normalizeSpacingValue(gap?.main ?? 0),
440
+ cross: normalizeSpacingValue(gap?.cross ?? 0)
441
+ };
442
+ }
443
+ function resolveLayoutPadding(padding) {
444
+ if (typeof padding === "number") {
445
+ const value = normalizeSpacingValue(padding);
446
+ return {
447
+ mainStart: value,
448
+ mainEnd: value,
449
+ crossStart: value,
450
+ crossEnd: value
451
+ };
452
+ }
453
+ return {
454
+ mainStart: normalizeSpacingValue(padding?.mainStart ?? 0),
455
+ mainEnd: normalizeSpacingValue(padding?.mainEnd ?? 0),
456
+ crossStart: normalizeSpacingValue(padding?.crossStart ?? 0),
457
+ crossEnd: normalizeSpacingValue(padding?.crossEnd ?? 0)
458
+ };
459
+ }
460
+ function normalizeSpacingValue(value) {
461
+ if (!Number.isFinite(value)) {
462
+ return 0;
463
+ }
464
+ return Math.max(0, value);
465
+ }
466
+
467
+ // src/linear-band-engine.ts
468
+ var LinearBandEngine = class {
469
+ constructor(options) {
470
+ this.crossSizes = [];
471
+ this.direction = options.layout.direction ?? "vertical";
472
+ this.estimateRect = resolveEstimateRect(options, this.direction);
473
+ this.configuredCrossSize = normalizeOptionalSize(options.layout.crossSize);
474
+ this.gap = resolveLayoutGap(options.layout.gap);
475
+ this.padding = resolveLayoutPadding(options.layout.padding);
476
+ this.sizes = new SizeModel({
477
+ count: options.count,
478
+ getKey: options.getKey,
479
+ minSize: options.minSize,
480
+ estimateSize: (index) => mainSize(this.estimateRect(index), this.direction)
481
+ });
482
+ this.rebuildCrossSizes();
483
+ }
484
+ get count() {
485
+ return this.sizes.count;
486
+ }
487
+ setCount(count) {
488
+ this.sizes.setCount(count);
489
+ this.rebuildCrossSizes();
490
+ }
491
+ keyOf(index) {
492
+ return this.sizes.keyOf(index);
493
+ }
494
+ indexOfKey(key) {
495
+ return this.sizes.indexOfKey(key);
496
+ }
497
+ getTotalRect() {
498
+ const totalMain = this.totalMainSize();
499
+ let totalCross = 0;
500
+ for (const size of this.crossSizes) {
501
+ totalCross = Math.max(totalCross, size);
502
+ }
503
+ totalCross += this.padding.crossStart + this.padding.crossEnd;
504
+ return this.direction === "horizontal" ? createRect(0, 0, totalMain, totalCross) : createRect(0, 0, totalCross, totalMain);
505
+ }
506
+ getVisibleItems(viewport, overscan) {
507
+ if (this.sizes.count === 0) {
508
+ return [];
509
+ }
510
+ const scrollOffset = this.direction === "horizontal" ? viewport.x : viewport.y;
511
+ const viewportSize = this.direction === "horizontal" ? viewport.width : viewport.height;
512
+ const centerIndex = this.sizes.spacedLowerBound(clampNumber2(scrollOffset + viewportSize / 2, 0, Math.max(0, this.totalMainSize() - Number.EPSILON)), this.spacing());
513
+ const centerStart = this.sizes.spacedStartOf(centerIndex, this.spacing());
514
+ return materializeRange({
515
+ sizes: this.sizes,
516
+ scrollOffset,
517
+ viewportSize,
518
+ overscan,
519
+ centerIndex,
520
+ centerStart,
521
+ spacing: this.spacing(),
522
+ createItem: (index, key, start, size, offsetFromCenter, isMeasured) => this.createItem(index, key, start, size, offsetFromCenter, isMeasured)
523
+ }).items;
524
+ }
525
+ getItemRect(index) {
526
+ if (index < 0 || index >= this.sizes.count) {
527
+ return void 0;
528
+ }
529
+ return this.rectFor(index, this.sizes.spacedStartOf(index, this.spacing()), this.sizes.sizeOf(index));
530
+ }
531
+ getItemMainStart(index) {
532
+ return this.sizes.spacedStartOf(index, this.spacing());
533
+ }
534
+ getItemMainSize(index) {
535
+ return this.sizes.sizeOf(index);
536
+ }
537
+ isMeasured(index) {
538
+ return this.sizes.isMeasured(index);
539
+ }
540
+ measureItem(index, key, rect) {
541
+ const current = this.getItemRect(index) ?? createRect();
542
+ const nextRect = { ...current, ...rect };
543
+ const measurement = this.sizes.measure(key, index, mainSize(nextRect, this.direction));
544
+ if (!measurement.stale) {
545
+ this.crossSizes[index] = Math.max(0, this.direction === "horizontal" ? nextRect.height : nextRect.width);
546
+ }
547
+ return {
548
+ delta: measurement.delta,
549
+ changed: measurement.changed || !measurement.stale && this.crossSizes[index] !== (this.direction === "horizontal" ? current.height : current.width),
550
+ stale: measurement.stale,
551
+ rect: this.getItemRect(index) ?? current
552
+ };
553
+ }
554
+ resetMeasurements(keys) {
555
+ this.sizes.resetMeasurements(keys);
556
+ this.rebuildCrossSizes();
557
+ }
558
+ getScrollTarget(index, align, viewport) {
559
+ if (this.sizes.count === 0) {
560
+ return createPoint();
561
+ }
562
+ const targetIndex = clampInteger2(index, 0, this.sizes.count - 1);
563
+ const itemStart = this.sizes.spacedStartOf(targetIndex, this.spacing());
564
+ const itemSize = this.sizes.sizeOf(targetIndex);
565
+ const itemEnd = itemStart + itemSize;
566
+ const viewportStart = this.direction === "horizontal" ? viewport.x : viewport.y;
567
+ const viewportSize = this.direction === "horizontal" ? viewport.width : viewport.height;
568
+ let main = itemStart;
569
+ if ((align === "auto" || align === "nearest") && itemStart >= viewportStart && itemEnd <= viewportStart + viewportSize) {
570
+ main = viewportStart;
571
+ } else if (align === "end" || (align === "auto" || align === "nearest") && itemEnd > viewportStart + viewportSize) {
572
+ main = itemEnd - viewportSize;
573
+ } else if (align === "center") {
574
+ main = itemStart + itemSize / 2 - viewportSize / 2;
575
+ }
576
+ const totalMain = this.totalMainSize();
577
+ main = clampNumber2(main, 0, Math.max(0, totalMain - viewportSize));
578
+ return pointFromMainCross({
579
+ direction: this.direction,
580
+ main,
581
+ cross: this.direction === "horizontal" ? viewport.y : viewport.x
582
+ });
583
+ }
584
+ locateCenter(viewportCenter, lastIndex, lastViewportCenter, recenterThreshold) {
585
+ const mainCenter = this.direction === "horizontal" ? viewportCenter.x : viewportCenter.y;
586
+ const lastMainCenter = lastViewportCenter === void 0 ? void 0 : this.direction === "horizontal" ? lastViewportCenter.x : lastViewportCenter.y;
587
+ if (lastIndex < 0 || lastMainCenter === void 0) {
588
+ return { index: this.locateAbsolute(mainCenter), mode: "initial" };
589
+ }
590
+ if (Math.abs(mainCenter - lastMainCenter) > recenterThreshold) {
591
+ return { index: this.locateAbsolute(mainCenter), mode: "recenter" };
592
+ }
593
+ return { index: this.locateTracked(mainCenter, lastIndex), mode: "tracked" };
594
+ }
595
+ locateAbsolute(mainCenter) {
596
+ return this.sizes.spacedLowerBound(clampNumber2(mainCenter, 0, Math.max(0, this.totalMainSize() - Number.EPSILON)), this.spacing());
597
+ }
598
+ locateTracked(mainCenter, lastIndex) {
599
+ const target = clampNumber2(mainCenter, 0, Math.max(0, this.totalMainSize() - Number.EPSILON));
600
+ let index = clampInteger2(lastIndex, 0, this.sizes.count - 1);
601
+ let start = this.sizes.spacedStartOf(index, this.spacing());
602
+ let size = this.sizes.sizeOf(index);
603
+ const gap = this.gap.main;
604
+ while (index > 0 && target < start) {
605
+ index -= 1;
606
+ size = this.sizes.sizeOf(index);
607
+ start -= size + gap;
608
+ }
609
+ while (index < this.sizes.count - 1 && target >= start + size) {
610
+ start += size + gap;
611
+ index += 1;
612
+ size = this.sizes.sizeOf(index);
613
+ }
614
+ return index;
615
+ }
616
+ createItem(index, key, start, size, offsetFromCenter, isMeasured) {
617
+ const rect = this.rectFor(index, start, size);
618
+ return {
619
+ index,
620
+ key,
621
+ rect,
622
+ start,
623
+ size,
624
+ end: start + size,
625
+ offsetFromCenter,
626
+ isMeasured
627
+ };
628
+ }
629
+ rectFor(index, start, size) {
630
+ return rectFromMainCross({
631
+ direction: this.direction,
632
+ mainStart: start,
633
+ mainSize: size,
634
+ crossStart: this.padding.crossStart,
635
+ crossSize: this.crossSizes[index] ?? 0
636
+ });
637
+ }
638
+ spacing() {
639
+ return {
640
+ gap: this.gap.main,
641
+ paddingStart: this.padding.mainStart,
642
+ paddingEnd: this.padding.mainEnd
643
+ };
644
+ }
645
+ totalMainSize() {
646
+ return this.sizes.spacedTotalSize(this.spacing());
647
+ }
648
+ rebuildCrossSizes() {
649
+ this.crossSizes = Array.from({ length: this.sizes.count }, (_, index) => {
650
+ if (this.configuredCrossSize !== void 0) {
651
+ return this.configuredCrossSize;
652
+ }
653
+ const rect = this.estimateRect(index);
654
+ return Math.max(0, this.direction === "horizontal" ? rect.height : rect.width);
655
+ });
656
+ }
657
+ };
658
+ function resolveEstimateRect(options, direction) {
659
+ return (index) => normalizeEstimateRect(options.estimateRect(index), direction);
660
+ }
661
+ function normalizeEstimateRect(rect, direction) {
662
+ const fallbackMain = mainSize(rect, direction);
663
+ return direction === "horizontal" ? createRect(rect.x, rect.y, fallbackMain, rect.height) : createRect(rect.x, rect.y, rect.width, fallbackMain);
664
+ }
665
+ function clampNumber2(value, min, max) {
666
+ if (!Number.isFinite(value)) {
667
+ return min;
668
+ }
669
+ return Math.min(max, Math.max(min, value));
670
+ }
671
+ function clampInteger2(value, min, max) {
672
+ if (!Number.isFinite(value)) {
673
+ return min;
674
+ }
675
+ return Math.min(max, Math.max(min, Math.trunc(value)));
676
+ }
677
+ function normalizeOptionalSize(value) {
678
+ if (typeof value !== "number" || !Number.isFinite(value)) {
679
+ return void 0;
680
+ }
681
+ return Math.max(0, value);
682
+ }
683
+
684
+ // src/spatial-rect-engine.ts
685
+ var SpatialRectEngine = class {
686
+ constructor(options) {
687
+ this.keys = [];
688
+ this.sourceRects = [];
689
+ this.rects = [];
690
+ this.measured = [];
691
+ this.measuredRectsByKey = /* @__PURE__ */ new Map();
692
+ this.keyToIndex = /* @__PURE__ */ new Map();
693
+ this.countValue = normalizeCount2(options.count);
694
+ this.getKey = options.getKey;
695
+ this.estimateRect = options.estimateRect;
696
+ this.mode = options.layout?.mode === "masonry" ? "masonry" : "free";
697
+ this.direction = options.layout?.direction ?? "vertical";
698
+ this.lanes = normalizeCount2(options.layout?.lanes ?? 1) || 1;
699
+ this.gap = resolveLayoutGap(options.layout?.gap);
700
+ this.padding = resolveLayoutPadding(options.layout?.padding);
701
+ this.minSize = normalizeSize2(options.minSize ?? 1) || Number.EPSILON;
702
+ this.rebuild();
703
+ }
704
+ get count() {
705
+ return this.countValue;
706
+ }
707
+ setCount(count) {
708
+ const next = normalizeCount2(count);
709
+ if (next === this.countValue) {
710
+ return;
711
+ }
712
+ this.countValue = next;
713
+ this.rebuild();
714
+ }
715
+ keyOf(index) {
716
+ return this.keys[index];
717
+ }
718
+ indexOfKey(key) {
719
+ return this.keyToIndex.get(key) ?? -1;
720
+ }
721
+ getTotalRect() {
722
+ const contentRect = unionRect(this.rects);
723
+ const trailingX = this.direction === "horizontal" ? this.padding.mainEnd : this.padding.crossEnd;
724
+ const trailingY = this.direction === "horizontal" ? this.padding.crossEnd : this.padding.mainEnd;
725
+ const minimumWidth = this.direction === "horizontal" ? this.padding.mainStart : this.padding.crossStart;
726
+ const minimumHeight = this.direction === "horizontal" ? this.padding.crossStart : this.padding.mainStart;
727
+ return createRect(
728
+ 0,
729
+ 0,
730
+ Math.max(minimumWidth, contentRect.width) + trailingX,
731
+ Math.max(minimumHeight, contentRect.height) + trailingY
732
+ );
733
+ }
734
+ getVisibleItems(viewport, overscan) {
735
+ const queryRect = expandRect(viewport, overscan, "both");
736
+ const items = [];
737
+ const maxItems = overscan.maxItems ?? Number.POSITIVE_INFINITY;
738
+ for (let index = 0; index < this.rects.length; index += 1) {
739
+ if (items.length >= maxItems) {
740
+ break;
741
+ }
742
+ const rect = this.rects[index];
743
+ const key = this.keys[index];
744
+ if (key === void 0 || !rectsIntersect(rect, queryRect)) {
745
+ continue;
746
+ }
747
+ items.push(this.createItem(index, key, rect));
748
+ }
749
+ return items;
750
+ }
751
+ getItemRect(index) {
752
+ return this.rects[index];
753
+ }
754
+ getItemMainStart(index) {
755
+ const rect = this.rects[index];
756
+ return rect === void 0 ? 0 : mainStart(rect, this.direction);
757
+ }
758
+ getItemMainSize(index) {
759
+ const rect = this.rects[index];
760
+ return rect === void 0 ? 0 : mainSize(rect, this.direction);
761
+ }
762
+ isMeasured(index) {
763
+ return this.measured[index] ?? false;
764
+ }
765
+ measureItem(index, key, rect) {
766
+ if (!Number.isInteger(index) || index < 0 || index >= this.countValue) {
767
+ return { delta: 0, changed: false, stale: true, rect: createRect() };
768
+ }
769
+ if (this.keys[index] !== key) {
770
+ return { delta: 0, changed: false, stale: true, rect: this.rects[index] ?? createRect() };
771
+ }
772
+ const previousSource = this.sourceRects[index] ?? normalizeRect(this.estimateRect(index), this.minSize);
773
+ const previous = this.rects[index] ?? createRect();
774
+ const nextSource = normalizeRect(mergeMeasuredSourceRect(previousSource, rect), this.minSize);
775
+ this.measuredRectsByKey.set(key, nextSource);
776
+ this.rebuild();
777
+ const current = this.rects[index] ?? this.placeFreeRect(nextSource);
778
+ return {
779
+ delta: mainSize(current, this.direction) - mainSize(previous, this.direction),
780
+ changed: !sameRect(previous, current),
781
+ stale: false,
782
+ rect: current
783
+ };
784
+ }
785
+ resetMeasurements(keys) {
786
+ if (keys === void 0) {
787
+ this.measuredRectsByKey.clear();
788
+ } else {
789
+ for (const key of keys) {
790
+ this.measuredRectsByKey.delete(key);
791
+ }
792
+ }
793
+ this.rebuild();
794
+ }
795
+ getScrollTarget(index, align, viewport) {
796
+ const rect = this.rects[clampInteger3(index, 0, Math.max(0, this.rects.length - 1))];
797
+ if (rect === void 0) {
798
+ return createPoint();
799
+ }
800
+ const total = this.getTotalRect();
801
+ let x = rect.x;
802
+ let y = rect.y;
803
+ if (align === "center") {
804
+ x = rect.x + rect.width / 2 - viewport.width / 2;
805
+ y = rect.y + rect.height / 2 - viewport.height / 2;
806
+ } else if (align === "end") {
807
+ x = rect.x + rect.width - viewport.width;
808
+ y = rect.y + rect.height - viewport.height;
809
+ } else if (align === "auto" || align === "nearest") {
810
+ x = nearestAxisTarget(rect.x, rect.x + rect.width, viewport.x, viewport.x + viewport.width);
811
+ y = nearestAxisTarget(rect.y, rect.y + rect.height, viewport.y, viewport.y + viewport.height);
812
+ }
813
+ return createPoint(
814
+ clampNumber3(x, 0, Math.max(0, total.width - viewport.width)),
815
+ clampNumber3(y, 0, Math.max(0, total.height - viewport.height))
816
+ );
817
+ }
818
+ locateCenter(viewportCenter) {
819
+ if (this.rects.length === 0) {
820
+ return { index: -1, mode: "initial" };
821
+ }
822
+ let bestIndex = 0;
823
+ let bestDistance = Number.POSITIVE_INFINITY;
824
+ for (let index = 0; index < this.rects.length; index += 1) {
825
+ const rect = this.rects[index];
826
+ const dx = rect.x + rect.width / 2 - viewportCenter.x;
827
+ const dy = rect.y + rect.height / 2 - viewportCenter.y;
828
+ const distance = dx * dx + dy * dy;
829
+ if (distance < bestDistance) {
830
+ bestDistance = distance;
831
+ bestIndex = index;
832
+ }
833
+ }
834
+ return { index: bestIndex, mode: "recenter" };
835
+ }
836
+ rebuild() {
837
+ const keys = [];
838
+ const measured = [];
839
+ const keyToIndex = /* @__PURE__ */ new Map();
840
+ const sourceRects = [];
841
+ const rects = [];
842
+ const laneMainStarts = Array.from({ length: this.lanes }, () => this.padding.mainStart);
843
+ for (let index = 0; index < this.countValue; index += 1) {
844
+ const key = this.getKey(index);
845
+ const measuredRect = this.measuredRectsByKey.get(key);
846
+ const sourceRect = normalizeRect(measuredRect ?? this.estimateRect(index), this.minSize);
847
+ const rect = this.mode === "masonry" ? this.placeMasonryRect(sourceRect, laneMainStarts) : this.placeFreeRect(sourceRect);
848
+ keys.push(key);
849
+ measured.push(measuredRect !== void 0);
850
+ keyToIndex.set(key, index);
851
+ sourceRects.push(sourceRect);
852
+ rects.push(rect);
853
+ }
854
+ this.keys = keys;
855
+ this.measured = measured;
856
+ this.keyToIndex = keyToIndex;
857
+ this.sourceRects = sourceRects;
858
+ this.rects = rects;
859
+ }
860
+ placeMasonryRect(rect, laneMainStarts) {
861
+ const lane = indexOfMin(laneMainStarts);
862
+ const itemMainSize = mainSize(rect, this.direction);
863
+ const itemCrossSize = crossSize(rect, this.direction);
864
+ const itemCrossStart = this.padding.crossStart + crossStart(rect, this.direction) + lane * (itemCrossSize + this.gap.cross);
865
+ const itemMainStart = laneMainStarts[lane];
866
+ laneMainStarts[lane] = itemMainStart + itemMainSize + this.gap.main;
867
+ return rectFromMainCross({
868
+ direction: this.direction,
869
+ mainStart: itemMainStart,
870
+ mainSize: itemMainSize,
871
+ crossStart: itemCrossStart,
872
+ crossSize: itemCrossSize
873
+ });
874
+ }
875
+ placeFreeRect(rect) {
876
+ return this.direction === "horizontal" ? createRect(rect.x + this.padding.mainStart, rect.y + this.padding.crossStart, rect.width, rect.height) : createRect(rect.x + this.padding.crossStart, rect.y + this.padding.mainStart, rect.width, rect.height);
877
+ }
878
+ createItem(index, key, rect) {
879
+ return {
880
+ index,
881
+ key,
882
+ rect,
883
+ start: mainStart(rect, this.direction),
884
+ size: mainSize(rect, this.direction),
885
+ end: mainStart(rect, this.direction) + mainSize(rect, this.direction),
886
+ offsetFromCenter: 0,
887
+ isMeasured: this.measured[index] ?? false
888
+ };
889
+ }
890
+ };
891
+ function normalizeRect(rect, minSize) {
892
+ return createRect(rect.x, rect.y, Math.max(minSize, rect.width), Math.max(minSize, rect.height));
893
+ }
894
+ function mergeMeasuredSourceRect(source, measurement) {
895
+ return createRect(
896
+ source.x,
897
+ source.y,
898
+ measurement.width ?? source.width,
899
+ measurement.height ?? source.height
900
+ );
901
+ }
902
+ function crossStart(rect, direction) {
903
+ return direction === "horizontal" ? rect.y : rect.x;
904
+ }
905
+ function sameRect(a, b) {
906
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
907
+ }
908
+ function indexOfMin(values) {
909
+ let selected = 0;
910
+ for (let index = 1; index < values.length; index += 1) {
911
+ if (values[index] < values[selected]) {
912
+ selected = index;
913
+ }
914
+ }
915
+ return selected;
916
+ }
917
+ function normalizeCount2(value) {
918
+ if (!Number.isFinite(value)) {
919
+ return 0;
920
+ }
921
+ return Math.max(0, Math.trunc(value));
922
+ }
923
+ function normalizeSize2(value) {
924
+ if (!Number.isFinite(value)) {
925
+ return 0;
926
+ }
927
+ return Math.max(0, value);
928
+ }
929
+ function clampNumber3(value, min, max) {
930
+ if (!Number.isFinite(value)) {
931
+ return min;
932
+ }
933
+ return Math.min(max, Math.max(min, value));
934
+ }
935
+ function nearestAxisTarget(itemStart, itemEnd, viewportStart, viewportEnd) {
936
+ if (itemStart >= viewportStart && itemEnd <= viewportEnd) {
937
+ return viewportStart;
938
+ }
939
+ if (itemEnd > viewportEnd) {
940
+ return itemEnd - (viewportEnd - viewportStart);
941
+ }
942
+ return itemStart;
943
+ }
944
+ function clampInteger3(value, min, max) {
945
+ if (!Number.isFinite(value)) {
946
+ return min;
947
+ }
948
+ return Math.min(max, Math.max(min, Math.trunc(value)));
949
+ }
950
+
951
+ // src/anchor.ts
952
+ function makeAnchor(snapshot) {
953
+ if (snapshot.centerIndex < 0 || snapshot.centerKey === void 0) {
954
+ return void 0;
955
+ }
956
+ return {
957
+ key: snapshot.centerKey,
958
+ index: snapshot.centerIndex,
959
+ offsetInsideItem: snapshot.viewportCenter - snapshot.centerStart,
960
+ offsetInsideRect: snapshot.centerRect === void 0 ? void 0 : {
961
+ x: snapshot.viewportCenterPoint.x - snapshot.centerRect.x,
962
+ y: snapshot.viewportCenterPoint.y - snapshot.centerRect.y
963
+ }
964
+ };
965
+ }
966
+
967
+ // src/virtualizer.ts
968
+ var Virtualizer = class {
969
+ constructor(options) {
970
+ this.viewportRect = createRect();
971
+ this.scrollPoint = createPoint();
972
+ this.scrollVelocity = 0;
973
+ this.scrollDirection = 0;
974
+ this.lastCenterIndex = -1;
975
+ this.lastLocateMode = "initial";
976
+ this.scrollAxis = options.scroll.axis;
977
+ this.layoutMode = options.layout.mode;
978
+ this.layoutDirection = options.layout.direction ?? "vertical";
979
+ this.measurementAxes = options.measurement.axes;
980
+ this.engine = this.layoutMode === "masonry" || this.layoutMode === "free" ? new SpatialRectEngine(options) : new LinearBandEngine(options);
981
+ const adaptiveOptions = typeof options.range?.adaptive === "object" ? options.range.adaptive : void 0;
982
+ this.baseOverscan = resolveOverscan(adaptiveOptions?.base ?? options.range?.overscan, {
983
+ maxItems: options.range?.maxItems,
984
+ maxPx: options.range?.maxPx
985
+ });
986
+ this.adaptiveRange = resolveAdaptiveRange(options.range?.adaptive, this.baseOverscan);
987
+ this.recenterThreshold = normalizeOptionalPositiveNumber2(options.sync?.recenterThreshold);
988
+ this.scrollPoint = this.clampScrollPoint(resolveInitialScrollPoint(options.scroll));
989
+ this.viewportRect = rectFromScrollPoint(this.scrollPoint, this.viewportRect);
990
+ }
991
+ setViewport(size) {
992
+ const normalized = normalizeNonNegativeNumber(size);
993
+ if (this.scrollAxis === "horizontal") {
994
+ this.setViewportRect(createRect(this.scrollPoint.x, this.scrollPoint.y, normalized, this.viewportRect.height));
995
+ } else {
996
+ this.setViewportRect(createRect(this.scrollPoint.x, this.scrollPoint.y, this.viewportRect.width, normalized));
997
+ }
998
+ }
999
+ setViewportRect(rect) {
1000
+ this.viewportRect = createRect(this.scrollPoint.x, this.scrollPoint.y, rect.width, rect.height);
1001
+ this.scrollPoint = this.clampScrollPoint(this.scrollPoint);
1002
+ this.viewportRect = rectFromScrollPoint(this.scrollPoint, this.viewportRect);
1003
+ }
1004
+ setScrollOffset(offset, timestamp = now()) {
1005
+ const next = this.scrollAxis === "horizontal" ? createPoint(offset, this.scrollPoint.y) : createPoint(this.scrollPoint.x, offset);
1006
+ this.setScrollPoint(next, timestamp);
1007
+ }
1008
+ setScrollPoint(point, timestamp = now()) {
1009
+ const nextPoint = this.clampScrollPoint(point);
1010
+ this.updateScrollVelocity(nextPoint, timestamp);
1011
+ this.scrollPoint = nextPoint;
1012
+ this.viewportRect = rectFromScrollPoint(this.scrollPoint, this.viewportRect);
1013
+ }
1014
+ recenter(offsetOrPoint = this.scrollPoint) {
1015
+ const point = typeof offsetOrPoint === "number" ? this.scrollAxis === "horizontal" ? createPoint(offsetOrPoint, this.scrollPoint.y) : createPoint(this.scrollPoint.x, offsetOrPoint) : offsetOrPoint;
1016
+ this.scrollPoint = this.clampScrollPoint(point);
1017
+ this.viewportRect = rectFromScrollPoint(this.scrollPoint, this.viewportRect);
1018
+ this.resetScrollVelocity();
1019
+ const located = this.locateCenter();
1020
+ this.lastCenterIndex = located.index;
1021
+ this.lastViewportCenter = this.getViewportCenterPoint();
1022
+ this.lastLocateMode = "recenter";
1023
+ }
1024
+ setCount(count) {
1025
+ this.engine.setCount(count);
1026
+ this.scrollPoint = this.clampScrollPoint(this.scrollPoint);
1027
+ this.viewportRect = rectFromScrollPoint(this.scrollPoint, this.viewportRect);
1028
+ this.lastCenterIndex = Math.min(this.lastCenterIndex, this.engine.count - 1);
1029
+ if (this.engine.count === 0) {
1030
+ this.lastCenterIndex = -1;
1031
+ this.lastViewportCenter = void 0;
1032
+ }
1033
+ }
1034
+ setMeasuredSize(key, index, size) {
1035
+ const rect = this.engine.getItemRect(index);
1036
+ const nextRect = this.isHorizontalLayoutDirection() ? { width: size, height: rect?.height } : { width: rect?.width, height: size };
1037
+ return this.setMeasuredRect(key, index, nextRect);
1038
+ }
1039
+ setMeasuredRect(key, index, rect) {
1040
+ const before = this.getSnapshot();
1041
+ const anchor = makeAnchor(before);
1042
+ const measurement = this.engine.measureItem(index, key, rect);
1043
+ this.scrollPoint = this.clampScrollPoint(this.scrollPoint);
1044
+ this.viewportRect = rectFromScrollPoint(this.scrollPoint, this.viewportRect);
1045
+ const correctedScrollPoint = anchor === void 0 ? this.scrollPoint : this.getScrollPointForAnchor(anchor);
1046
+ return {
1047
+ delta: measurement.delta,
1048
+ changed: measurement.changed,
1049
+ stale: measurement.stale,
1050
+ size: mainSize(measurement.rect, this.layoutDirection),
1051
+ rect: measurement.rect,
1052
+ correctedScrollOffset: this.projectScrollOffset(correctedScrollPoint),
1053
+ correctedScrollPoint
1054
+ };
1055
+ }
1056
+ resetMeasurements(keys) {
1057
+ this.engine.resetMeasurements(keys);
1058
+ this.scrollPoint = this.clampScrollPoint(this.scrollPoint);
1059
+ this.viewportRect = rectFromScrollPoint(this.scrollPoint, this.viewportRect);
1060
+ }
1061
+ getState() {
1062
+ const snapshot = this.getSnapshot();
1063
+ const { items: _items, ...state } = snapshot;
1064
+ return state;
1065
+ }
1066
+ getSnapshot() {
1067
+ const overscan = this.resolveCurrentOverscan();
1068
+ const totalRect = this.engine.getTotalRect();
1069
+ const viewportCenterPoint = this.getViewportCenterPoint();
1070
+ if (this.engine.count === 0) {
1071
+ return this.createSnapshot({
1072
+ overscan,
1073
+ totalRect,
1074
+ viewportCenterPoint,
1075
+ centerIndex: -1,
1076
+ centerRect: void 0,
1077
+ items: []
1078
+ });
1079
+ }
1080
+ const located = this.locateCenter();
1081
+ const centerIndex = located.index;
1082
+ const centerRect = this.engine.getItemRect(centerIndex);
1083
+ const items = this.engine.getVisibleItems(this.viewportRect, overscan);
1084
+ this.lastCenterIndex = centerIndex;
1085
+ this.lastViewportCenter = viewportCenterPoint;
1086
+ this.lastLocateMode = located.mode;
1087
+ return this.createSnapshot({
1088
+ overscan,
1089
+ totalRect,
1090
+ viewportCenterPoint,
1091
+ centerIndex,
1092
+ centerRect,
1093
+ items
1094
+ });
1095
+ }
1096
+ makeAnchor(snapshot = this.getSnapshot()) {
1097
+ return makeAnchor(snapshot);
1098
+ }
1099
+ getScrollOffsetForAnchor(anchor) {
1100
+ return this.projectScrollOffset(this.getScrollPointForAnchor(anchor));
1101
+ }
1102
+ getScrollPointForAnchor(anchor) {
1103
+ const resolvedIndex = this.resolveAnchorIndex(anchor);
1104
+ if (resolvedIndex < 0) {
1105
+ return createPoint();
1106
+ }
1107
+ const rect = this.engine.getItemRect(resolvedIndex);
1108
+ if (rect === void 0) {
1109
+ return createPoint();
1110
+ }
1111
+ const offset = anchor.offsetInsideRect ?? {
1112
+ x: 0,
1113
+ y: anchor.offsetInsideItem
1114
+ };
1115
+ return this.clampScrollPoint(createPoint(
1116
+ rect.x + offset.x - this.viewportRect.width / 2,
1117
+ rect.y + offset.y - this.viewportRect.height / 2
1118
+ ));
1119
+ }
1120
+ scrollToIndex(index, align = "auto") {
1121
+ return this.projectScrollOffset(this.getScrollTarget(index, align));
1122
+ }
1123
+ scrollToItem(index, align = "auto") {
1124
+ return this.getScrollTarget(index, align);
1125
+ }
1126
+ getScrollTarget(index, align = "auto") {
1127
+ return this.engine.getScrollTarget(index, align, this.viewportRect);
1128
+ }
1129
+ resolveScrollTarget(target) {
1130
+ if ("index" in target) {
1131
+ return this.getScrollTarget(target.index, target.align ?? "auto");
1132
+ }
1133
+ if ("key" in target) {
1134
+ const index = this.engine.indexOfKey(target.key);
1135
+ return index < 0 ? this.scrollPoint : this.getScrollTarget(index, target.align ?? "auto");
1136
+ }
1137
+ if ("offset" in target) {
1138
+ return this.scrollAxis === "horizontal" ? this.scrollToPoint(createPoint(target.offset, this.scrollPoint.y)) : this.scrollToPoint(createPoint(this.scrollPoint.x, target.offset));
1139
+ }
1140
+ if ("rect" in target) {
1141
+ return this.scrollToRect(target.rect, target.align ?? "auto");
1142
+ }
1143
+ return this.scrollToPoint(target.point);
1144
+ }
1145
+ scrollToPoint(point) {
1146
+ return this.clampScrollPoint(point);
1147
+ }
1148
+ scrollToRect(rect, align = "auto") {
1149
+ const viewport = this.viewportRect;
1150
+ let x = rect.x;
1151
+ let y = rect.y;
1152
+ if (align === "center") {
1153
+ x = rect.x + rect.width / 2 - viewport.width / 2;
1154
+ y = rect.y + rect.height / 2 - viewport.height / 2;
1155
+ } else if (align === "end") {
1156
+ x = rect.x + rect.width - viewport.width;
1157
+ y = rect.y + rect.height - viewport.height;
1158
+ } else if (align === "auto" || align === "nearest") {
1159
+ x = nearestAxisTarget2(rect.x, rect.x + rect.width, viewport.x, viewport.x + viewport.width);
1160
+ y = nearestAxisTarget2(rect.y, rect.y + rect.height, viewport.y, viewport.y + viewport.height);
1161
+ }
1162
+ return this.scrollToPoint(createPoint(x, y));
1163
+ }
1164
+ getDebugState() {
1165
+ const overscan = this.resolveCurrentOverscan();
1166
+ return {
1167
+ lastCenterIndex: this.lastCenterIndex,
1168
+ lastViewportCenter: this.projectPoint(this.lastViewportCenter),
1169
+ lastLocateMode: this.lastLocateMode,
1170
+ scrollVelocity: this.scrollVelocity,
1171
+ scrollDirection: this.scrollDirection,
1172
+ overscanBefore: overscan.before,
1173
+ overscanAfter: overscan.after,
1174
+ adaptiveRangeActive: overscan.active
1175
+ };
1176
+ }
1177
+ createSnapshot(params) {
1178
+ const centerStart = params.centerIndex >= 0 ? this.engine.getItemMainStart(params.centerIndex) : 0;
1179
+ const centerSize = params.centerIndex >= 0 ? this.engine.getItemMainSize(params.centerIndex) : 0;
1180
+ const rangeStart = params.items.length === 0 ? 0 : Math.min(...params.items.map((item) => item.index));
1181
+ const rangeEnd = params.items.length === 0 ? 0 : Math.max(...params.items.map((item) => item.index)) + 1;
1182
+ return {
1183
+ count: this.engine.count,
1184
+ scrollAxis: this.scrollAxis,
1185
+ layoutMode: this.layoutMode,
1186
+ measurementAxes: this.measurementAxes,
1187
+ viewportSize: this.projectViewportSize(),
1188
+ viewportRect: this.viewportRect,
1189
+ scrollOffset: this.projectScrollOffset(this.scrollPoint),
1190
+ scrollPoint: this.scrollPoint,
1191
+ viewportCenter: this.projectPoint(params.viewportCenterPoint) ?? 0,
1192
+ viewportCenterPoint: params.viewportCenterPoint,
1193
+ centerIndex: params.centerIndex,
1194
+ centerKey: params.centerIndex >= 0 ? this.engine.keyOf(params.centerIndex) : void 0,
1195
+ centerStart,
1196
+ centerSize,
1197
+ centerRect: params.centerRect,
1198
+ rangeStart,
1199
+ rangeEnd,
1200
+ totalSize: this.projectTotalSize(params.totalRect),
1201
+ totalRect: params.totalRect,
1202
+ overscanBefore: params.overscan.before,
1203
+ overscanAfter: params.overscan.after,
1204
+ scrollVelocity: this.scrollVelocity,
1205
+ scrollDirection: this.scrollDirection,
1206
+ adaptiveRangeActive: params.overscan.active,
1207
+ items: params.items
1208
+ };
1209
+ }
1210
+ locateCenter() {
1211
+ return this.engine.locateCenter(
1212
+ this.getViewportCenterPoint(),
1213
+ this.lastCenterIndex,
1214
+ this.lastViewportCenter,
1215
+ this.getRecenterThreshold()
1216
+ );
1217
+ }
1218
+ updateScrollVelocity(nextPoint, timestamp) {
1219
+ if (!this.adaptiveRange.enabled) {
1220
+ return;
1221
+ }
1222
+ if (this.lastScrollPoint === void 0 || this.lastScrollTime === void 0) {
1223
+ this.lastScrollPoint = nextPoint;
1224
+ this.lastScrollTime = timestamp;
1225
+ return;
1226
+ }
1227
+ const previousOffset = this.projectScrollOffset(this.lastScrollPoint);
1228
+ const nextOffset = this.projectScrollOffset(nextPoint);
1229
+ const delta = nextOffset - previousOffset;
1230
+ const elapsed = Math.max(1, timestamp - this.lastScrollTime);
1231
+ this.lastScrollPoint = nextPoint;
1232
+ this.lastScrollTime = timestamp;
1233
+ if (delta === 0) {
1234
+ if (this.lastMovementTime !== void 0 && timestamp - this.lastMovementTime > this.adaptiveRange.settleMs) {
1235
+ this.scrollVelocity = 0;
1236
+ this.scrollDirection = 0;
1237
+ }
1238
+ return;
1239
+ }
1240
+ const instantVelocity = Math.abs(delta) / elapsed;
1241
+ const smoothing = this.adaptiveRange.velocitySmoothing;
1242
+ this.scrollVelocity = this.scrollVelocity === 0 ? instantVelocity : this.scrollVelocity * smoothing + instantVelocity * (1 - smoothing);
1243
+ this.scrollDirection = delta > 0 ? 1 : -1;
1244
+ this.lastMovementTime = timestamp;
1245
+ }
1246
+ resetScrollVelocity() {
1247
+ this.lastScrollPoint = this.scrollPoint;
1248
+ this.lastScrollTime = void 0;
1249
+ this.lastMovementTime = void 0;
1250
+ this.scrollVelocity = 0;
1251
+ this.scrollDirection = 0;
1252
+ }
1253
+ resolveCurrentOverscan() {
1254
+ if (!this.adaptiveRange.enabled || this.scrollVelocity === 0 || this.scrollDirection === 0) {
1255
+ return {
1256
+ before: this.baseOverscan.before,
1257
+ after: this.baseOverscan.after,
1258
+ maxItems: this.baseOverscan.maxItems,
1259
+ maxPx: this.baseOverscan.maxPx,
1260
+ active: false
1261
+ };
1262
+ }
1263
+ const lookahead = this.scrollVelocity * this.adaptiveRange.lookaheadMs;
1264
+ const aheadExtra = lookahead * this.adaptiveRange.directionBias;
1265
+ const behindExtra = lookahead * (1 - this.adaptiveRange.directionBias);
1266
+ const before = this.scrollDirection < 0 ? this.baseOverscan.before + aheadExtra : this.baseOverscan.before + behindExtra;
1267
+ const after = this.scrollDirection > 0 ? this.baseOverscan.after + aheadExtra : this.baseOverscan.after + behindExtra;
1268
+ return {
1269
+ before: clampNumber4(before, this.adaptiveRange.min, this.adaptiveRange.max),
1270
+ after: clampNumber4(after, this.adaptiveRange.min, this.adaptiveRange.max),
1271
+ maxItems: this.baseOverscan.maxItems,
1272
+ maxPx: this.baseOverscan.maxPx,
1273
+ active: true
1274
+ };
1275
+ }
1276
+ getViewportCenterPoint() {
1277
+ return createPoint(
1278
+ this.scrollPoint.x + this.viewportRect.width / 2,
1279
+ this.scrollPoint.y + this.viewportRect.height / 2
1280
+ );
1281
+ }
1282
+ getRecenterThreshold() {
1283
+ return this.recenterThreshold ?? Math.max(this.projectViewportSize() * 4, this.baseOverscan.before + this.baseOverscan.after, 1024);
1284
+ }
1285
+ clampScrollPoint(point) {
1286
+ const total = this.engine.getTotalRect();
1287
+ const maxX = Math.max(0, total.width - this.viewportRect.width);
1288
+ const maxY = Math.max(0, total.height - this.viewportRect.height);
1289
+ return createPoint(
1290
+ this.scrollAxis === "vertical" ? 0 : clampNumber4(point.x, 0, maxX),
1291
+ this.scrollAxis === "horizontal" ? 0 : clampNumber4(point.y, 0, maxY)
1292
+ );
1293
+ }
1294
+ projectScrollOffset(point) {
1295
+ return this.scrollAxis === "horizontal" ? point.x : point.y;
1296
+ }
1297
+ projectViewportSize() {
1298
+ return this.scrollAxis === "horizontal" ? this.viewportRect.width : this.viewportRect.height;
1299
+ }
1300
+ projectTotalSize(rect) {
1301
+ return this.scrollAxis === "horizontal" ? rect.width : rect.height;
1302
+ }
1303
+ projectPoint(point) {
1304
+ if (point === void 0) {
1305
+ return void 0;
1306
+ }
1307
+ return this.scrollAxis === "horizontal" ? point.x : point.y;
1308
+ }
1309
+ isHorizontalLayoutDirection() {
1310
+ return this.layoutDirection === "horizontal";
1311
+ }
1312
+ resolveAnchorIndex(anchor) {
1313
+ if (this.engine.keyOf(anchor.index) === anchor.key) {
1314
+ return anchor.index;
1315
+ }
1316
+ return this.engine.indexOfKey(anchor.key);
1317
+ }
1318
+ };
1319
+ function resolveAdaptiveRange(value, baseOverscan) {
1320
+ const options = typeof value === "object" ? value : {};
1321
+ const enabled = value === true || options.enabled === true;
1322
+ const baseMax = Math.max(baseOverscan.before, baseOverscan.after, 1);
1323
+ return {
1324
+ enabled,
1325
+ min: normalizeNonNegativeNumber(options.min ?? baseMax),
1326
+ max: Math.max(normalizeNonNegativeNumber(options.max ?? baseMax * 6), baseMax),
1327
+ lookaheadMs: normalizePositiveNumber(options.lookaheadMs ?? 140, 140),
1328
+ velocitySmoothing: clampNumber4(options.velocitySmoothing ?? 0.65, 0, 0.95),
1329
+ directionBias: clampNumber4(options.directionBias ?? 0.8, 0.5, 1),
1330
+ settleMs: normalizePositiveNumber(options.settleMs ?? 180, 180)
1331
+ };
1332
+ }
1333
+ function normalizeNonNegativeNumber(value) {
1334
+ if (!Number.isFinite(value)) {
1335
+ return 0;
1336
+ }
1337
+ return Math.max(0, value);
1338
+ }
1339
+ function normalizeOptionalPositiveNumber2(value) {
1340
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1341
+ return void 0;
1342
+ }
1343
+ return value;
1344
+ }
1345
+ function normalizePositiveNumber(value, fallback) {
1346
+ if (!Number.isFinite(value) || value <= 0) {
1347
+ return fallback;
1348
+ }
1349
+ return value;
1350
+ }
1351
+ function clampNumber4(value, min, max) {
1352
+ if (!Number.isFinite(value)) {
1353
+ return min;
1354
+ }
1355
+ return Math.min(max, Math.max(min, value));
1356
+ }
1357
+ function resolveInitialScrollPoint(scroll) {
1358
+ if (scroll.restore?.point !== void 0) {
1359
+ return scroll.restore.point;
1360
+ }
1361
+ if (scroll.initialPoint !== void 0) {
1362
+ return scroll.initialPoint;
1363
+ }
1364
+ const offset = scroll.restore?.offset ?? scroll.initialOffset;
1365
+ if (offset === void 0) {
1366
+ return createPoint();
1367
+ }
1368
+ return scroll.axis === "horizontal" ? createPoint(offset, 0) : createPoint(0, offset);
1369
+ }
1370
+ function nearestAxisTarget2(itemStart, itemEnd, viewportStart, viewportEnd) {
1371
+ if (itemStart >= viewportStart && itemEnd <= viewportEnd) {
1372
+ return viewportStart;
1373
+ }
1374
+ if (itemEnd > viewportEnd) {
1375
+ return itemEnd - (viewportEnd - viewportStart);
1376
+ }
1377
+ return itemStart;
1378
+ }
1379
+ function now() {
1380
+ return typeof performance === "undefined" ? Date.now() : performance.now();
1381
+ }
1382
+ export {
1383
+ FenwickTree,
1384
+ LinearBandEngine,
1385
+ SizeModel,
1386
+ SpatialRectEngine,
1387
+ Virtualizer,
1388
+ createPoint,
1389
+ createRect,
1390
+ expandRect,
1391
+ rectFromScrollPoint,
1392
+ rectsIntersect,
1393
+ resolveLayoutGap,
1394
+ resolveLayoutPadding
1395
+ };