@aardworx/wombat.rendering 0.8.0 → 0.9.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/core/command.d.ts +7 -9
- package/dist/core/command.d.ts.map +1 -1
- package/dist/core/renderTask.d.ts +64 -1
- package/dist/core/renderTask.d.ts.map +1 -1
- package/dist/core/renderTask.js +3 -1
- package/dist/core/renderTask.js.map +1 -1
- package/dist/resources/preparedRenderObject.js +3 -3
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +17 -0
- package/dist/resources/uniformBuffer.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts +105 -0
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/dispatch.js +355 -0
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -0
- package/dist/runtime/derivedUniforms/index.d.ts +6 -0
- package/dist/runtime/derivedUniforms/index.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/index.js +26 -0
- package/dist/runtime/derivedUniforms/index.js.map +1 -0
- package/dist/runtime/derivedUniforms/recipes.d.ts +42 -0
- package/dist/runtime/derivedUniforms/recipes.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/recipes.js +134 -0
- package/dist/runtime/derivedUniforms/recipes.js.map +1 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +56 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.js +171 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -0
- package/dist/runtime/derivedUniforms/slotId.d.ts +15 -0
- package/dist/runtime/derivedUniforms/slotId.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/slotId.js +30 -0
- package/dist/runtime/derivedUniforms/slotId.js.map +1 -0
- package/dist/runtime/derivedUniforms/slots.d.ts +49 -0
- package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/slots.js +123 -0
- package/dist/runtime/derivedUniforms/slots.js.map +1 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts +13 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js +218 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js.map +1 -0
- package/dist/runtime/flattenTree.d.ts +5 -0
- package/dist/runtime/flattenTree.d.ts.map +1 -0
- package/dist/runtime/flattenTree.js +45 -0
- package/dist/runtime/flattenTree.js.map +1 -0
- package/dist/runtime/heapAdapter.d.ts +16 -0
- package/dist/runtime/heapAdapter.d.ts.map +1 -0
- package/dist/runtime/heapAdapter.js +263 -0
- package/dist/runtime/heapAdapter.js.map +1 -0
- package/dist/runtime/heapEffect.d.ts +235 -0
- package/dist/runtime/heapEffect.d.ts.map +1 -0
- package/dist/runtime/heapEffect.js +518 -0
- package/dist/runtime/heapEffect.js.map +1 -0
- package/dist/runtime/heapEffectIR.d.ts +34 -0
- package/dist/runtime/heapEffectIR.d.ts.map +1 -0
- package/dist/runtime/heapEffectIR.js +1161 -0
- package/dist/runtime/heapEffectIR.js.map +1 -0
- package/dist/runtime/heapEligibility.d.ts +9 -0
- package/dist/runtime/heapEligibility.d.ts.map +1 -0
- package/dist/runtime/heapEligibility.js +195 -0
- package/dist/runtime/heapEligibility.js.map +1 -0
- package/dist/runtime/heapScene.d.ts +240 -31
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +3334 -265
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/heapShaderFamily.d.ts +132 -0
- package/dist/runtime/heapShaderFamily.d.ts.map +1 -0
- package/dist/runtime/heapShaderFamily.js +866 -0
- package/dist/runtime/heapShaderFamily.js.map +1 -0
- package/dist/runtime/hybridScene.d.ts +133 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -0
- package/dist/runtime/hybridScene.js +159 -0
- package/dist/runtime/hybridScene.js.map +1 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +9 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.d.ts +21 -4
- package/dist/runtime/renderTask.d.ts.map +1 -1
- package/dist/runtime/renderTask.js +127 -32
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.d.ts +33 -3
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +12 -3
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/scenePass.d.ts +28 -3
- package/dist/runtime/scenePass.d.ts.map +1 -1
- package/dist/runtime/scenePass.js +37 -4
- package/dist/runtime/scenePass.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +35 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +247 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts +240 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/atlasPool.js +798 -0
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -0
- package/dist/runtime/textureAtlas/bvhTree2d.d.ts +33 -0
- package/dist/runtime/textureAtlas/bvhTree2d.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/bvhTree2d.js +501 -0
- package/dist/runtime/textureAtlas/bvhTree2d.js.map +1 -0
- package/dist/runtime/textureAtlas/packer.d.ts +50 -0
- package/dist/runtime/textureAtlas/packer.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/packer.js +390 -0
- package/dist/runtime/textureAtlas/packer.js.map +1 -0
- package/dist/window/loop.d.ts +14 -0
- package/dist/window/loop.d.ts.map +1 -1
- package/dist/window/loop.js +18 -5
- package/dist/window/loop.js.map +1 -1
- package/package.json +5 -4
- package/src/core/command.ts +9 -9
- package/src/core/renderTask.ts +55 -2
- package/src/resources/preparedRenderObject.ts +3 -3
- package/src/resources/uniformBuffer.ts +15 -0
- package/src/runtime/derivedUniforms/dispatch.ts +407 -0
- package/src/runtime/derivedUniforms/index.ts +52 -0
- package/src/runtime/derivedUniforms/recipes.ts +159 -0
- package/src/runtime/derivedUniforms/sceneIntegration.ts +239 -0
- package/src/runtime/derivedUniforms/slotId.ts +35 -0
- package/src/runtime/derivedUniforms/slots.ts +155 -0
- package/src/runtime/derivedUniforms/uberKernel.wgsl.ts +220 -0
- package/src/runtime/flattenTree.ts +46 -0
- package/src/runtime/heapAdapter.ts +314 -0
- package/src/runtime/heapEffect.ts +719 -0
- package/src/runtime/heapEffectIR.ts +1211 -0
- package/src/runtime/heapEligibility.ts +189 -0
- package/src/runtime/heapScene.ts +3981 -349
- package/src/runtime/heapShaderFamily.ts +1102 -0
- package/src/runtime/hybridScene.ts +310 -0
- package/src/runtime/index.ts +46 -0
- package/src/runtime/renderTask.ts +167 -32
- package/src/runtime/runtime.ts +37 -4
- package/src/runtime/scenePass.ts +39 -4
- package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +291 -0
- package/src/runtime/textureAtlas/atlasPool.ts +991 -0
- package/src/runtime/textureAtlas/bvhTree2d.ts +530 -0
- package/src/runtime/textureAtlas/packer.ts +429 -0
- package/src/window/loop.ts +32 -6
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
// BvhTree2d — generic immutable 2D bounding-volume hierarchy.
|
|
2
|
+
//
|
|
3
|
+
// Port of `Aardvark.Geometry.BvhTree2d<'K, 'V>` from
|
|
4
|
+
// `aardvark.base/src/Aardvark.Geometry/Bvh.fs` (the 2D variant). The
|
|
5
|
+
// algorithm and structure mirror the F# original:
|
|
6
|
+
// - Internal nodes are a discriminated union: `Leaf` (overflow
|
|
7
|
+
// count, bounds, key→(bounds,value) map) or `Node` (best cost,
|
|
8
|
+
// count, bounds, left, right).
|
|
9
|
+
// - Splitting picks the dimension+pivot with the lowest SAH-style
|
|
10
|
+
// cost across both axes; rebuilds a leaf into a node when the
|
|
11
|
+
// count exceeds the limit.
|
|
12
|
+
// - Add / remove preserve persistent semantics — every operation
|
|
13
|
+
// returns a fresh tree; existing nodes are reused where possible.
|
|
14
|
+
//
|
|
15
|
+
// One simplification vs the F# source: `HashMap<K, _>` is replaced by
|
|
16
|
+
// `ReadonlyMap<K, _>` (JS Map). The packer never needs structural
|
|
17
|
+
// HashMap operations like `union` of distinct subtrees as a free
|
|
18
|
+
// HashMap union; we replicate `HashMap.union` by copying values into
|
|
19
|
+
// a fresh Map. For the scale we expect (~thousands of free rects per
|
|
20
|
+
// page) this is fine.
|
|
21
|
+
|
|
22
|
+
import { Box2d, V2d } from "@aardworx/wombat.base";
|
|
23
|
+
|
|
24
|
+
export const SPLIT_LIMIT_DEFAULT = 24;
|
|
25
|
+
|
|
26
|
+
// Box helpers — wombat's Box2d uses min/max V2d. We define a few small
|
|
27
|
+
// utilities that mirror Aardvark's surface (`isValid`, `area`, etc.)
|
|
28
|
+
// in terms of the wombat API.
|
|
29
|
+
function boxIsValid(b: Box2d): boolean {
|
|
30
|
+
// wombat's Box2d.isValid considers min<=max on both axes.
|
|
31
|
+
return b.isValid();
|
|
32
|
+
}
|
|
33
|
+
function boxArea(b: Box2d): number {
|
|
34
|
+
if (!boxIsValid(b)) return 0;
|
|
35
|
+
const s = b.size();
|
|
36
|
+
return s.x * s.y;
|
|
37
|
+
}
|
|
38
|
+
function boxIntersects(a: Box2d, b: Box2d, eps: number = 0): boolean {
|
|
39
|
+
if (!boxIsValid(a) || !boxIsValid(b)) return false;
|
|
40
|
+
const amin = a.min, amax = a.max, bmin = b.min, bmax = b.max;
|
|
41
|
+
return (
|
|
42
|
+
amin.x - eps <= bmax.x &&
|
|
43
|
+
amax.x + eps >= bmin.x &&
|
|
44
|
+
amin.y - eps <= bmax.y &&
|
|
45
|
+
amax.y + eps >= bmin.y
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
function boxContains(outer: Box2d, inner: Box2d): boolean {
|
|
49
|
+
if (!boxIsValid(outer) || !boxIsValid(inner)) return false;
|
|
50
|
+
const omin = outer.min, omax = outer.max, imin = inner.min, imax = inner.max;
|
|
51
|
+
return omin.x <= imin.x && omin.y <= imin.y && omax.x >= imax.x && omax.y >= imax.y;
|
|
52
|
+
}
|
|
53
|
+
function boxUnion(a: Box2d, b: Box2d): Box2d {
|
|
54
|
+
if (!boxIsValid(a)) return b;
|
|
55
|
+
if (!boxIsValid(b)) return a;
|
|
56
|
+
const amin = a.min, amax = a.max, bmin = b.min, bmax = b.max;
|
|
57
|
+
return Box2d.fromMinMax(
|
|
58
|
+
new V2d(Math.min(amin.x, bmin.x), Math.min(amin.y, bmin.y)),
|
|
59
|
+
new V2d(Math.max(amax.x, bmax.x), Math.max(amax.y, bmax.y)),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
function boxIntersection(a: Box2d, b: Box2d): Box2d {
|
|
63
|
+
if (!boxIsValid(a) || !boxIsValid(b)) return Box2d.empty;
|
|
64
|
+
const amin = a.min, amax = a.max, bmin = b.min, bmax = b.max;
|
|
65
|
+
const minX = Math.max(amin.x, bmin.x);
|
|
66
|
+
const minY = Math.max(amin.y, bmin.y);
|
|
67
|
+
const maxX = Math.min(amax.x, bmax.x);
|
|
68
|
+
const maxY = Math.min(amax.y, bmax.y);
|
|
69
|
+
if (minX > maxX || minY > maxY) return Box2d.empty;
|
|
70
|
+
return Box2d.fromMinMax(new V2d(minX, minY), new V2d(maxX, maxY));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Internal node representation ──────────────────────────────────
|
|
74
|
+
// Leaf: { kind:"leaf", overflow, bounds, values: Map<K,[Box2d,V]> }
|
|
75
|
+
// Node: { kind:"node", bestCost, count, bounds, left, right }
|
|
76
|
+
//
|
|
77
|
+
// Tagged shape mirrors the F# DU; `kind` is the discriminant.
|
|
78
|
+
|
|
79
|
+
interface LeafNode<K, V> {
|
|
80
|
+
readonly kind: "leaf";
|
|
81
|
+
readonly overflow: number;
|
|
82
|
+
readonly bounds: Box2d;
|
|
83
|
+
readonly values: ReadonlyMap<K, readonly [Box2d, V]>;
|
|
84
|
+
}
|
|
85
|
+
interface InnerNode<K, V> {
|
|
86
|
+
readonly kind: "node";
|
|
87
|
+
readonly bestCost: number;
|
|
88
|
+
readonly count: number;
|
|
89
|
+
readonly bounds: Box2d;
|
|
90
|
+
readonly left: BvhNode<K, V>;
|
|
91
|
+
readonly right: BvhNode<K, V>;
|
|
92
|
+
}
|
|
93
|
+
type BvhNode<K, V> = LeafNode<K, V> | InnerNode<K, V>;
|
|
94
|
+
|
|
95
|
+
function nodeCount<K, V>(n: BvhNode<K, V>): number {
|
|
96
|
+
return n.kind === "leaf" ? n.values.size : n.count;
|
|
97
|
+
}
|
|
98
|
+
function nodeBounds<K, V>(n: BvhNode<K, V>): Box2d {
|
|
99
|
+
return n.bounds;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cost(invArea: number, lBox: Box2d, lCnt: number, rBox: Box2d, rCnt: number): number {
|
|
103
|
+
const inter = boxIntersection(lBox, rBox);
|
|
104
|
+
const iVol = boxIsValid(inter) ? boxArea(inter) * invArea : 0;
|
|
105
|
+
const lVol = boxArea(lBox) * invArea;
|
|
106
|
+
const rVol = boxArea(rBox) * invArea;
|
|
107
|
+
const cnt = lCnt + rCnt;
|
|
108
|
+
const l = lCnt / cnt;
|
|
109
|
+
const r = rCnt / cnt;
|
|
110
|
+
return (1 / cnt + l * lVol + r * rVol + iVol) / 2;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function* iterMap<K, V>(m: ReadonlyMap<K, readonly [Box2d, V]>): Iterable<[K, Box2d, V]> {
|
|
114
|
+
for (const [k, [b, v]] of m) yield [k, b, v];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function* iterAll<K, V>(n: BvhNode<K, V>): Iterable<[K, Box2d, V]> {
|
|
118
|
+
if (n.kind === "leaf") {
|
|
119
|
+
yield* iterMap(n.values);
|
|
120
|
+
} else {
|
|
121
|
+
yield* iterAll(n.left);
|
|
122
|
+
yield* iterAll(n.right);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Try to find a binary split with cost < 1.0 across both dims.
|
|
127
|
+
function trySplit<K, V>(
|
|
128
|
+
invArea: number,
|
|
129
|
+
elements: ReadonlyMap<K, readonly [Box2d, V]>,
|
|
130
|
+
):
|
|
131
|
+
| { cost: number; lBox: Box2d; lEls: Map<K, readonly [Box2d, V]>; rBox: Box2d; rEls: Map<K, readonly [Box2d, V]> }
|
|
132
|
+
| undefined {
|
|
133
|
+
if (elements.size <= 1) return undefined;
|
|
134
|
+
const arr: Array<[K, Box2d, V]> = [];
|
|
135
|
+
for (const [k, [b, v]] of elements) arr.push([k, b, v]);
|
|
136
|
+
|
|
137
|
+
let bestCost = Number.POSITIVE_INFINITY;
|
|
138
|
+
let bestPerm: number[] | null = null;
|
|
139
|
+
let bestSplit = -1;
|
|
140
|
+
let bestLBox: Box2d = Box2d.empty;
|
|
141
|
+
let bestRBox: Box2d = Box2d.empty;
|
|
142
|
+
|
|
143
|
+
const n = arr.length;
|
|
144
|
+
for (let dim = 0; dim < 2; dim++) {
|
|
145
|
+
const perm = arr.map((_, i) => i);
|
|
146
|
+
perm.sort((a, b) => {
|
|
147
|
+
const ca = arr[a]![1].center();
|
|
148
|
+
const cb = arr[b]![1].center();
|
|
149
|
+
return dim === 0 ? ca.x - cb.x : ca.y - cb.y;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const lBoxes: Box2d[] = new Array(n);
|
|
153
|
+
const rBoxes: Box2d[] = new Array(n);
|
|
154
|
+
|
|
155
|
+
let last = arr[perm[0]!]![1];
|
|
156
|
+
lBoxes[0] = last;
|
|
157
|
+
for (let i = 1; i < n; i++) {
|
|
158
|
+
const b = boxUnion(arr[perm[i]!]![1], last);
|
|
159
|
+
lBoxes[i] = b;
|
|
160
|
+
last = b;
|
|
161
|
+
}
|
|
162
|
+
last = Box2d.empty;
|
|
163
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
164
|
+
const b = boxUnion(arr[perm[i]!]![1], last);
|
|
165
|
+
rBoxes[i] = b;
|
|
166
|
+
last = b;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (let lCnt = 1; lCnt < n; lCnt++) {
|
|
170
|
+
const rCnt = n - lCnt;
|
|
171
|
+
const lBox = lBoxes[lCnt - 1]!;
|
|
172
|
+
const rBox = rBoxes[lCnt]!;
|
|
173
|
+
const c = cost(invArea, lBox, lCnt, rBox, rCnt);
|
|
174
|
+
if (c < bestCost) {
|
|
175
|
+
bestCost = c;
|
|
176
|
+
bestPerm = perm;
|
|
177
|
+
bestSplit = lCnt;
|
|
178
|
+
bestLBox = lBox;
|
|
179
|
+
bestRBox = rBox;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (bestCost < 1.0 && bestPerm !== null) {
|
|
185
|
+
const lEls = new Map<K, readonly [Box2d, V]>();
|
|
186
|
+
const rEls = new Map<K, readonly [Box2d, V]>();
|
|
187
|
+
for (let i = 0; i < bestSplit; i++) {
|
|
188
|
+
const [k, b, v] = arr[bestPerm[i]!]!;
|
|
189
|
+
lEls.set(k, [b, v]);
|
|
190
|
+
}
|
|
191
|
+
for (let i = bestSplit; i < n; i++) {
|
|
192
|
+
const [k, b, v] = arr[bestPerm[i]!]!;
|
|
193
|
+
rEls.set(k, [b, v]);
|
|
194
|
+
}
|
|
195
|
+
return { cost: bestCost, lBox: bestLBox, lEls, rBox: bestRBox, rEls };
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function build<K, V>(
|
|
201
|
+
limit: number,
|
|
202
|
+
bounds: Box2d,
|
|
203
|
+
elements: ReadonlyMap<K, readonly [Box2d, V]>,
|
|
204
|
+
): BvhNode<K, V> {
|
|
205
|
+
if (elements.size === 0) throw new Error("BvhTree2d.build: empty elements");
|
|
206
|
+
if (elements.size <= limit) {
|
|
207
|
+
return { kind: "leaf", overflow: 0, bounds, values: elements };
|
|
208
|
+
}
|
|
209
|
+
const inv = boxArea(bounds) > 0 ? 1.0 / boxArea(bounds) : 0;
|
|
210
|
+
const s = trySplit(inv, elements);
|
|
211
|
+
if (s !== undefined) {
|
|
212
|
+
const l = build(limit, s.lBox, s.lEls);
|
|
213
|
+
const r = build(limit, s.rBox, s.rEls);
|
|
214
|
+
return { kind: "node", bestCost: s.cost, count: elements.size, bounds, left: l, right: r };
|
|
215
|
+
}
|
|
216
|
+
// bad split — fall back to oversized leaf with overflow = size
|
|
217
|
+
return { kind: "leaf", overflow: elements.size, bounds, values: elements };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function add<K, V>(
|
|
221
|
+
limit: number,
|
|
222
|
+
key: K,
|
|
223
|
+
bounds: Box2d,
|
|
224
|
+
value: V,
|
|
225
|
+
node: BvhNode<K, V>,
|
|
226
|
+
): BvhNode<K, V> {
|
|
227
|
+
if (node.kind === "leaf") {
|
|
228
|
+
const existing = node.values.get(key);
|
|
229
|
+
if (existing !== undefined) {
|
|
230
|
+
const [oldB] = existing;
|
|
231
|
+
const newValues = new Map(node.values);
|
|
232
|
+
newValues.set(key, [bounds, value]);
|
|
233
|
+
let bb: Box2d;
|
|
234
|
+
if (boxContains(bounds, oldB)) {
|
|
235
|
+
bb = node.bounds;
|
|
236
|
+
} else {
|
|
237
|
+
bb = bounds;
|
|
238
|
+
for (const [, [b]] of newValues) bb = boxUnion(bb, b);
|
|
239
|
+
}
|
|
240
|
+
return { kind: "leaf", overflow: node.overflow, bounds: bb, values: newValues };
|
|
241
|
+
}
|
|
242
|
+
const newValues = new Map(node.values);
|
|
243
|
+
newValues.set(key, [bounds, value]);
|
|
244
|
+
const bb = boxUnion(node.bounds, bounds);
|
|
245
|
+
if (newValues.size >= 2 * node.overflow && newValues.size > limit) {
|
|
246
|
+
return build(limit, bb, newValues);
|
|
247
|
+
}
|
|
248
|
+
return { kind: "leaf", overflow: node.overflow, bounds: bb, values: newValues };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Inner: pick child by SAH.
|
|
252
|
+
const nb = boxUnion(node.bounds, bounds);
|
|
253
|
+
const lb = nodeBounds(node.left);
|
|
254
|
+
const rb = nodeBounds(node.right);
|
|
255
|
+
const lc = nodeCount(node.left);
|
|
256
|
+
const rc = nodeCount(node.right);
|
|
257
|
+
const invVol = boxArea(nb) > 0 ? 1.0 / boxArea(nb) : 0;
|
|
258
|
+
const lCost = cost(invVol, boxUnion(lb, bounds), 1 + lc, rb, rc);
|
|
259
|
+
const rCost = cost(invVol, lb, lc, boxUnion(rb, bounds), 1 + rc);
|
|
260
|
+
|
|
261
|
+
if (lCost < rCost) {
|
|
262
|
+
if (lCost > 2 * node.bestCost) {
|
|
263
|
+
const all = new Map<K, readonly [Box2d, V]>();
|
|
264
|
+
for (const [k, b, v] of iterAll(node)) all.set(k, [b, v]);
|
|
265
|
+
all.set(key, [bounds, value]);
|
|
266
|
+
return build(limit, nb, all);
|
|
267
|
+
}
|
|
268
|
+
const l = add(limit, key, bounds, value, node.left);
|
|
269
|
+
const lb2 = nodeBounds(l);
|
|
270
|
+
const lc2 = nodeCount(l);
|
|
271
|
+
const cc = cost(invVol, lb2, lc2, rb, rc);
|
|
272
|
+
const nb2 = boxUnion(lb2, rb);
|
|
273
|
+
return {
|
|
274
|
+
kind: "node",
|
|
275
|
+
bestCost: Math.min(node.bestCost, cc),
|
|
276
|
+
count: lc2 + rc,
|
|
277
|
+
bounds: nb2,
|
|
278
|
+
left: l,
|
|
279
|
+
right: node.right,
|
|
280
|
+
};
|
|
281
|
+
} else {
|
|
282
|
+
if (rCost > 2 * node.bestCost) {
|
|
283
|
+
const all = new Map<K, readonly [Box2d, V]>();
|
|
284
|
+
for (const [k, b, v] of iterAll(node)) all.set(k, [b, v]);
|
|
285
|
+
all.set(key, [bounds, value]);
|
|
286
|
+
return build(limit, nb, all);
|
|
287
|
+
}
|
|
288
|
+
const r = add(limit, key, bounds, value, node.right);
|
|
289
|
+
const rb2 = nodeBounds(r);
|
|
290
|
+
const rc2 = nodeCount(r);
|
|
291
|
+
const cc = cost(invVol, lb, lc, rb2, rc2);
|
|
292
|
+
const nb2 = boxUnion(lb, rb2);
|
|
293
|
+
return {
|
|
294
|
+
kind: "node",
|
|
295
|
+
bestCost: Math.min(node.bestCost, cc),
|
|
296
|
+
count: lc + rc2,
|
|
297
|
+
bounds: nb2,
|
|
298
|
+
left: node.left,
|
|
299
|
+
right: r,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function tryRemove<K, V>(
|
|
305
|
+
limit: number,
|
|
306
|
+
key: K,
|
|
307
|
+
bounds: Box2d,
|
|
308
|
+
node: BvhNode<K, V>,
|
|
309
|
+
): { value: V; rest: BvhNode<K, V> | undefined } | undefined {
|
|
310
|
+
if (node.kind === "leaf") {
|
|
311
|
+
if (!boxIntersects(node.bounds, bounds)) return undefined;
|
|
312
|
+
const found = node.values.get(key);
|
|
313
|
+
if (found === undefined) return undefined;
|
|
314
|
+
const [, v] = found;
|
|
315
|
+
const newValues = new Map(node.values);
|
|
316
|
+
newValues.delete(key);
|
|
317
|
+
if (newValues.size === 0) return { value: v, rest: undefined };
|
|
318
|
+
let bb: Box2d = Box2d.empty;
|
|
319
|
+
for (const [, [b]] of newValues) bb = boxUnion(bb, b);
|
|
320
|
+
return {
|
|
321
|
+
value: v,
|
|
322
|
+
rest: { kind: "leaf", overflow: Math.max(0, node.overflow - 1), bounds: bb, values: newValues },
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!boxIntersects(node.bounds, bounds)) return undefined;
|
|
327
|
+
const tl = tryRemove(limit, key, bounds, node.left);
|
|
328
|
+
if (tl !== undefined) {
|
|
329
|
+
if (tl.rest === undefined) return { value: tl.value, rest: node.right };
|
|
330
|
+
const lc = nodeCount(tl.rest);
|
|
331
|
+
const rc = nodeCount(node.right);
|
|
332
|
+
const lb = nodeBounds(tl.rest);
|
|
333
|
+
const rb = nodeBounds(node.right);
|
|
334
|
+
const o = boxUnion(lb, rb);
|
|
335
|
+
const cnt = lc + rc;
|
|
336
|
+
if (cnt <= limit) {
|
|
337
|
+
const merged = new Map<K, readonly [Box2d, V]>();
|
|
338
|
+
for (const [k, b, v] of iterAll(tl.rest)) merged.set(k, [b, v]);
|
|
339
|
+
for (const [k, b, v] of iterAll(node.right)) merged.set(k, [b, v]);
|
|
340
|
+
return {
|
|
341
|
+
value: tl.value,
|
|
342
|
+
rest: { kind: "leaf", overflow: 0, bounds: o, values: merged },
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const c = cost(boxArea(o) > 0 ? 1.0 / boxArea(o) : 0, lb, lc, rb, rc);
|
|
346
|
+
return {
|
|
347
|
+
value: tl.value,
|
|
348
|
+
rest: {
|
|
349
|
+
kind: "node",
|
|
350
|
+
bestCost: Math.min(c, node.bestCost),
|
|
351
|
+
count: cnt,
|
|
352
|
+
bounds: o,
|
|
353
|
+
left: tl.rest,
|
|
354
|
+
right: node.right,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const tr = tryRemove(limit, key, bounds, node.right);
|
|
359
|
+
if (tr !== undefined) {
|
|
360
|
+
if (tr.rest === undefined) return { value: tr.value, rest: node.left };
|
|
361
|
+
const lc = nodeCount(node.left);
|
|
362
|
+
const rc = nodeCount(tr.rest);
|
|
363
|
+
const lb = nodeBounds(node.left);
|
|
364
|
+
const rb = nodeBounds(tr.rest);
|
|
365
|
+
const o = boxUnion(lb, rb);
|
|
366
|
+
const cnt = lc + rc;
|
|
367
|
+
if (cnt <= limit) {
|
|
368
|
+
const merged = new Map<K, readonly [Box2d, V]>();
|
|
369
|
+
for (const [k, b, v] of iterAll(node.left)) merged.set(k, [b, v]);
|
|
370
|
+
for (const [k, b, v] of iterAll(tr.rest)) merged.set(k, [b, v]);
|
|
371
|
+
return {
|
|
372
|
+
value: tr.value,
|
|
373
|
+
rest: { kind: "leaf", overflow: 0, bounds: o, values: merged },
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const c = cost(boxArea(o) > 0 ? 1.0 / boxArea(o) : 0, lb, lc, rb, rc);
|
|
377
|
+
return {
|
|
378
|
+
value: tr.value,
|
|
379
|
+
rest: {
|
|
380
|
+
kind: "node",
|
|
381
|
+
bestCost: Math.min(c, node.bestCost),
|
|
382
|
+
count: cnt,
|
|
383
|
+
bounds: o,
|
|
384
|
+
left: node.left,
|
|
385
|
+
right: tr.rest,
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function getIntersecting<K, V>(query: Box2d, node: BvhNode<K, V>, out: Map<K, readonly [Box2d, V]>): void {
|
|
393
|
+
if (!boxIntersects(node.bounds, query, 1e-20)) return;
|
|
394
|
+
if (node.kind === "leaf") {
|
|
395
|
+
for (const [k, [b, v]] of node.values) {
|
|
396
|
+
if (boxIntersects(b, query)) out.set(k, [b, v]);
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
getIntersecting(query, node.left, out);
|
|
400
|
+
getIntersecting(query, node.right, out);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function getContaining<K, V>(query: Box2d, node: BvhNode<K, V>, out: Map<K, readonly [Box2d, V]>): void {
|
|
405
|
+
if (!boxContains(node.bounds, query)) return;
|
|
406
|
+
if (node.kind === "leaf") {
|
|
407
|
+
for (const [k, [b, v]] of node.values) {
|
|
408
|
+
if (boxContains(b, query)) out.set(k, [b, v]);
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
getContaining(query, node.left, out);
|
|
412
|
+
getContaining(query, node.right, out);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function getContained<K, V>(query: Box2d, node: BvhNode<K, V>, out: Map<K, readonly [Box2d, V]>): void {
|
|
417
|
+
if (boxContains(query, node.bounds)) {
|
|
418
|
+
for (const [k, b, v] of iterAll(node)) out.set(k, [b, v]);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (!boxIntersects(query, node.bounds)) return;
|
|
422
|
+
if (node.kind === "leaf") {
|
|
423
|
+
for (const [k, [b, v]] of node.values) {
|
|
424
|
+
if (boxContains(query, b)) out.set(k, [b, v]);
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
getContained(query, node.left, out);
|
|
428
|
+
getContained(query, node.right, out);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Immutable / persistent 2D bounding-volume hierarchy.
|
|
434
|
+
*
|
|
435
|
+
* Generic over K (entry key — must be usable as a Map key, i.e. either
|
|
436
|
+
* a primitive or a stable reference) and V (per-entry payload). Every
|
|
437
|
+
* mutation returns a new tree that shares unchanged subtrees with the
|
|
438
|
+
* original where possible.
|
|
439
|
+
*/
|
|
440
|
+
export class BvhTree2d<K, V> {
|
|
441
|
+
private constructor(
|
|
442
|
+
private readonly limit: number,
|
|
443
|
+
private readonly root: BvhNode<K, V> | undefined,
|
|
444
|
+
private readonly keyBounds: ReadonlyMap<K, Box2d>,
|
|
445
|
+
) {}
|
|
446
|
+
|
|
447
|
+
static empty<K, V>(splitLimit: number = SPLIT_LIMIT_DEFAULT): BvhTree2d<K, V> {
|
|
448
|
+
return new BvhTree2d<K, V>(splitLimit, undefined, new Map());
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Same as `empty` — kept as an alias for closer F# parity. */
|
|
452
|
+
static Empty<K, V>(splitLimit: number = SPLIT_LIMIT_DEFAULT): BvhTree2d<K, V> {
|
|
453
|
+
return BvhTree2d.empty(splitLimit);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
get count(): number {
|
|
457
|
+
return this.root === undefined ? 0 : nodeCount(this.root);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
get isEmpty(): boolean {
|
|
461
|
+
return this.root === undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
add(key: K, bounds: Box2d, value: V): BvhTree2d<K, V> {
|
|
465
|
+
if (!boxIsValid(bounds)) return this;
|
|
466
|
+
const newKB = new Map(this.keyBounds);
|
|
467
|
+
newKB.set(key, bounds);
|
|
468
|
+
let newRoot: BvhNode<K, V>;
|
|
469
|
+
if (this.root === undefined) {
|
|
470
|
+
const m = new Map<K, readonly [Box2d, V]>();
|
|
471
|
+
m.set(key, [bounds, value]);
|
|
472
|
+
newRoot = { kind: "leaf", overflow: 0, bounds, values: m };
|
|
473
|
+
} else {
|
|
474
|
+
newRoot = add(this.limit, key, bounds, value, this.root);
|
|
475
|
+
}
|
|
476
|
+
return new BvhTree2d(this.limit, newRoot, newKB);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
remove(key: K): BvhTree2d<K, V> {
|
|
480
|
+
const r = this.tryRemove(key);
|
|
481
|
+
return r === undefined ? this : r[1];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
tryRemove(key: K): [V, BvhTree2d<K, V>] | undefined {
|
|
485
|
+
const b = this.keyBounds.get(key);
|
|
486
|
+
if (b === undefined || this.root === undefined) return undefined;
|
|
487
|
+
const newKB = new Map(this.keyBounds);
|
|
488
|
+
newKB.delete(key);
|
|
489
|
+
const r = tryRemove(this.limit, key, b, this.root);
|
|
490
|
+
if (r === undefined) return undefined;
|
|
491
|
+
return [r.value, new BvhTree2d(this.limit, r.rest, newKB)];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
*getIntersecting(query: Box2d): Iterable<[K, V]> {
|
|
495
|
+
if (this.root === undefined) return;
|
|
496
|
+
const out = new Map<K, readonly [Box2d, V]>();
|
|
497
|
+
getIntersecting(query, this.root, out);
|
|
498
|
+
for (const [k, [, v]] of out) yield [k, v];
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
*getContaining(query: Box2d): Iterable<[K, V]> {
|
|
502
|
+
if (this.root === undefined) return;
|
|
503
|
+
const out = new Map<K, readonly [Box2d, V]>();
|
|
504
|
+
getContaining(query, this.root, out);
|
|
505
|
+
for (const [k, [, v]] of out) yield [k, v];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
*getContained(query: Box2d): Iterable<[K, V]> {
|
|
509
|
+
if (this.root === undefined) return;
|
|
510
|
+
const out = new Map<K, readonly [Box2d, V]>();
|
|
511
|
+
getContained(query, this.root, out);
|
|
512
|
+
for (const [k, [, v]] of out) yield [k, v];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Iterate every entry as `[K, Box2d, V]`. */
|
|
516
|
+
*toSeq(): Iterable<[K, Box2d, V]> {
|
|
517
|
+
if (this.root === undefined) return;
|
|
518
|
+
yield* iterAll(this.root);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
*keys(): Iterable<K> {
|
|
522
|
+
if (this.root === undefined) return;
|
|
523
|
+
for (const [k] of iterAll(this.root)) yield k;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Lookup the bounds we stored for `key`, or `undefined`. */
|
|
527
|
+
getBounds(key: K): Box2d | undefined {
|
|
528
|
+
return this.keyBounds.get(key);
|
|
529
|
+
}
|
|
530
|
+
}
|