@aardworx/wombat.rendering 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +529 -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 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +9 -1
- 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 +6 -5
- 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 +728 -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 +45 -1
- 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
- package/dist/runtime/heapShader.d.ts +0 -12
- package/dist/runtime/heapShader.d.ts.map +0 -1
- package/dist/runtime/heapShader.js +0 -157
- package/dist/runtime/heapShader.js.map +0 -1
- package/src/runtime/heapShader.ts +0 -185
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// TexturePacking — incremental immutable MaxRects-style 2D rectangle
|
|
2
|
+
// packer over `BvhTree2d`.
|
|
3
|
+
//
|
|
4
|
+
// Port of `Aardvark.Geometry.TexturePacking<'a>` from
|
|
5
|
+
// `aardvark.base/src/Aardvark.Geometry/TexturePacking.fs`. Algorithm:
|
|
6
|
+
// - Atlas is described by a set of free MaxRects (contained in a
|
|
7
|
+
// `BvhTree2d` keyed on the rect itself for O(log N) queries).
|
|
8
|
+
// - tryAdd picks the free rect with the smallest waste that fits the
|
|
9
|
+
// incoming size (with optional 90° rotation), splits the chosen
|
|
10
|
+
// rect into the placed sub-rect plus residual MaxRects.
|
|
11
|
+
// - Remove restores a sub-rect and merges with adjacent free rects
|
|
12
|
+
// using the F# `merge` routine, which incrementally maintains
|
|
13
|
+
// non-overlapping MaxRects covering the released region + neighbours.
|
|
14
|
+
//
|
|
15
|
+
// The wombat port keys the BVH on `Box2i` value-objects. We use a
|
|
16
|
+
// stable string key (`"minX,minY,maxX,maxY"`) so identical rectangles
|
|
17
|
+
// share a Map slot — Box2i instances themselves are reference types in
|
|
18
|
+
// JS and would not collide otherwise.
|
|
19
|
+
|
|
20
|
+
import { Box2d, Box2i, V2d, V2i } from "@aardworx/wombat.base";
|
|
21
|
+
import { BvhTree2d } from "./bvhTree2d.js";
|
|
22
|
+
|
|
23
|
+
// ─── Box2i helpers (mirroring the F# helpers in this module) ───────
|
|
24
|
+
|
|
25
|
+
const boxKey = (b: Box2i): string => {
|
|
26
|
+
const mn = b.min, mx = b.max;
|
|
27
|
+
return `${mn.x},${mn.y},${mx.x},${mx.y}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const sizeOf = (b: Box2i): V2i => {
|
|
31
|
+
const mn = b.min, mx = b.max;
|
|
32
|
+
return new V2i(mx.x - mn.x + 1, mx.y - mn.y + 1);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const isValid2i = (b: Box2i): boolean => {
|
|
36
|
+
const mn = b.min, mx = b.max;
|
|
37
|
+
return mn.x <= mx.x && mn.y <= mx.y;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const intersects2i = (a: Box2i, b: Box2i): boolean => {
|
|
41
|
+
const amin = a.min, amax = a.max, bmin = b.min, bmax = b.max;
|
|
42
|
+
return amin.x <= bmax.x && amax.x >= bmin.x && amin.y <= bmax.y && amax.y >= bmin.y;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const intersectsTouching2i = (a: Box2i, b: Box2i): boolean => {
|
|
46
|
+
// Mirror F# `intersects` inside `maxRects`: a 1-unit halo so adjacent
|
|
47
|
+
// rectangles count as overlapping (used to detect contact).
|
|
48
|
+
const amin = a.min, amax = a.max, bmin = b.min, bmax = b.max;
|
|
49
|
+
return amin.x <= bmax.x + 1 && amax.x >= bmin.x - 1 && amin.y <= bmax.y + 1 && amax.y >= bmin.y - 1;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const contains2i = (outer: Box2i, inner: Box2i): boolean => {
|
|
53
|
+
const omin = outer.min, omax = outer.max, imin = inner.min, imax = inner.max;
|
|
54
|
+
return omin.x <= imin.x && omin.y <= imin.y && omax.x >= imax.x && omax.y >= imax.y;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const equals2i = (a: Box2i, b: Box2i): boolean => {
|
|
58
|
+
const amin = a.min, amax = a.max, bmin = b.min, bmax = b.max;
|
|
59
|
+
return amin.x === bmin.x && amin.y === bmin.y && amax.x === bmax.x && amax.y === bmax.y;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const mkBox = (minX: number, minY: number, maxX: number, maxY: number): Box2i =>
|
|
63
|
+
new Box2i(minX, minY, maxX, maxY);
|
|
64
|
+
|
|
65
|
+
// `createBox`: half-pixel padded Box2d for BVH key. Exact mirror of
|
|
66
|
+
// F#'s `createBox`.
|
|
67
|
+
const createBox = (b: Box2i): Box2d => {
|
|
68
|
+
const mn = b.min, mx = b.max;
|
|
69
|
+
return Box2d.fromMinMax(
|
|
70
|
+
new V2d(mn.x - 0.5, mn.y - 0.5),
|
|
71
|
+
new V2d(mx.x + 0.5, mx.y + 0.5),
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Subtract `b` from `a` (the placed rect from a free rect) and produce
|
|
76
|
+
// up to four residual MaxRects strips. Mirrors F#'s `split`.
|
|
77
|
+
const splitRects = (a: Box2i, b: Box2i): Box2i[] => {
|
|
78
|
+
const cands: Box2i[] = [
|
|
79
|
+
mkBox(b.max.x + 1, a.min.y, a.max.x, a.max.y),
|
|
80
|
+
mkBox(a.min.x, a.min.y, b.min.x - 1, a.max.y),
|
|
81
|
+
mkBox(a.min.x, b.max.y + 1, a.max.x, a.max.y),
|
|
82
|
+
mkBox(a.min.x, a.min.y, a.max.x, b.min.y - 1),
|
|
83
|
+
];
|
|
84
|
+
return cands.filter(isValid2i);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// MaxRects union/cleanup. Direct port of F#'s `maxRects`.
|
|
88
|
+
const maxRectsCombine = (a: Box2i, b: Box2i): Box2i[] => {
|
|
89
|
+
if (intersectsTouching2i(a, b)) {
|
|
90
|
+
if (contains2i(a, b)) return [a];
|
|
91
|
+
if (contains2i(b, a)) return [b];
|
|
92
|
+
const ax0 = a.min.x, ax1 = a.max.x, ay0 = a.min.y, ay1 = a.max.y;
|
|
93
|
+
const bx0 = b.min.x, bx1 = b.max.x, by0 = b.min.y, by1 = b.max.y;
|
|
94
|
+
|
|
95
|
+
// RangeX of a contains RangeX of b: ax0<=bx0 && ax1>=bx1
|
|
96
|
+
const abx = ax0 <= bx0 && ax1 >= bx1;
|
|
97
|
+
const bax = bx0 <= ax0 && bx1 >= ax1;
|
|
98
|
+
const aby = ay0 <= by0 && ay1 >= by1;
|
|
99
|
+
const bay = by0 <= ay0 && by1 >= ay1;
|
|
100
|
+
|
|
101
|
+
if (abx && bax) {
|
|
102
|
+
// identical X — Y-union
|
|
103
|
+
return [mkBox(ax0, Math.min(ay0, by0), ax1, Math.max(ay1, by1))];
|
|
104
|
+
}
|
|
105
|
+
if (aby && bay) {
|
|
106
|
+
return [mkBox(Math.min(ax0, bx0), ay0, Math.max(ax1, bx1), ay1)];
|
|
107
|
+
}
|
|
108
|
+
if (abx) {
|
|
109
|
+
return [mkBox(bx0, Math.min(ay0, by0), bx1, Math.max(ay1, by1)), a];
|
|
110
|
+
}
|
|
111
|
+
if (bax) {
|
|
112
|
+
return [mkBox(ax0, Math.min(ay0, by0), ax1, Math.max(ay1, by1)), b];
|
|
113
|
+
}
|
|
114
|
+
if (aby) {
|
|
115
|
+
return [mkBox(Math.min(ax0, bx0), by0, Math.max(ax1, bx1), by1), a];
|
|
116
|
+
}
|
|
117
|
+
if (bay) {
|
|
118
|
+
return [mkBox(Math.min(ax0, bx0), ay0, Math.max(ax1, bx1), ay1), b];
|
|
119
|
+
}
|
|
120
|
+
// Cross case: produce the two orthogonal strips of each rect's
|
|
121
|
+
// overlap with the other, plus both originals; filter invalid.
|
|
122
|
+
const out: Box2i[] = [
|
|
123
|
+
mkBox(Math.min(ax0, bx0), Math.max(ay0, by0), Math.max(ax1, bx1), Math.min(ay1, by1)),
|
|
124
|
+
mkBox(Math.max(ax0, bx0), Math.min(ay0, by0), Math.min(ax1, bx1), Math.max(ay1, by1)),
|
|
125
|
+
a,
|
|
126
|
+
b,
|
|
127
|
+
];
|
|
128
|
+
return out.filter(isValid2i);
|
|
129
|
+
}
|
|
130
|
+
return [a, b];
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Merge a freshly-released rect `n` with already-known adjacent free
|
|
134
|
+
// rects, producing the new MaxRects covering the union. Direct port
|
|
135
|
+
// of F#'s `merge`.
|
|
136
|
+
const merge = (n: Box2i, boxes: Iterable<Box2i>): Box2i[] => {
|
|
137
|
+
const boxArr = [...boxes];
|
|
138
|
+
if (boxArr.length === 0) return [n];
|
|
139
|
+
|
|
140
|
+
type FreeT = BvhTree2d<string, Box2i>;
|
|
141
|
+
let free: FreeT = BvhTree2d.empty<string, Box2i>();
|
|
142
|
+
|
|
143
|
+
const addOne = (b: Box2i): void => {
|
|
144
|
+
const q = mkBox(b.min.x - 1, b.min.y - 1, b.max.x + 1, b.max.y + 1);
|
|
145
|
+
const touching: Box2i[] = [];
|
|
146
|
+
for (const [, val] of free.getIntersecting(createBox(q))) {
|
|
147
|
+
if (intersectsTouching2i(val, q)) touching.push(val);
|
|
148
|
+
}
|
|
149
|
+
if (touching.length === 0) {
|
|
150
|
+
free = free.add(boxKey(b), createBox(b), b);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
for (const o of touching) {
|
|
154
|
+
free = free.remove(boxKey(o));
|
|
155
|
+
for (const r of maxRectsCombine(o, b)) {
|
|
156
|
+
// skip if any existing free rect already contains r
|
|
157
|
+
let contained = false;
|
|
158
|
+
for (const [, val] of free.getContaining(createBox(r))) {
|
|
159
|
+
if (contains2i(val, r)) { contained = true; break; }
|
|
160
|
+
}
|
|
161
|
+
if (!contained) {
|
|
162
|
+
// remove any free rect r contains
|
|
163
|
+
const containedKeys: string[] = [];
|
|
164
|
+
for (const [k, val] of free.getContained(createBox(r))) {
|
|
165
|
+
if (contains2i(r, val)) containedKeys.push(k);
|
|
166
|
+
void val;
|
|
167
|
+
}
|
|
168
|
+
for (const k of containedKeys) free = free.remove(k);
|
|
169
|
+
free = free.add(boxKey(r), createBox(r), r);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
addOne(n);
|
|
176
|
+
for (const b of boxArr) addOne(b);
|
|
177
|
+
|
|
178
|
+
return [...free.toSeq()].map(([, , v]) => v);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Add a free rect into the tree iff no existing rect already contains
|
|
182
|
+
// it. Mirror of F#'s `addFree`.
|
|
183
|
+
const addFree = (b: Box2i, free: BvhTree2d<string, Box2i>): BvhTree2d<string, Box2i> => {
|
|
184
|
+
if (!isValid2i(b)) return free;
|
|
185
|
+
let containing = false;
|
|
186
|
+
for (const [, v] of free.getContaining(createBox(b))) {
|
|
187
|
+
if (contains2i(v, b)) { containing = true; break; }
|
|
188
|
+
}
|
|
189
|
+
if (containing) return free;
|
|
190
|
+
return free.add(boxKey(b), createBox(b), b);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Immutable 2D atlas. Generic over `K` (entry key — anything that's a
|
|
195
|
+
* valid `Map` key works; we recommend strings or numbers).
|
|
196
|
+
*/
|
|
197
|
+
export class TexturePacking<K> {
|
|
198
|
+
private constructor(
|
|
199
|
+
public readonly size: V2i,
|
|
200
|
+
public readonly allowRotate: boolean,
|
|
201
|
+
private readonly free: BvhTree2d<string, Box2i>,
|
|
202
|
+
/** Used rects keyed on the user's K. */
|
|
203
|
+
public readonly used: ReadonlyMap<K, Box2i>,
|
|
204
|
+
) {}
|
|
205
|
+
|
|
206
|
+
static empty<K>(size: V2i, allowRotate: boolean = true): TexturePacking<K> {
|
|
207
|
+
if (size.x <= 0 || size.y <= 0) {
|
|
208
|
+
return new TexturePacking<K>(new V2i(0, 0), allowRotate, BvhTree2d.empty<string, Box2i>(), new Map());
|
|
209
|
+
}
|
|
210
|
+
const bb = mkBox(0, 0, size.x - 1, size.y - 1);
|
|
211
|
+
const tree = BvhTree2d.empty<string, Box2i>().add(boxKey(bb), createBox(bb), bb);
|
|
212
|
+
return new TexturePacking<K>(size, allowRotate, tree, new Map());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
static create<K>(size: V2i, allowRotate: boolean = true): TexturePacking<K> {
|
|
216
|
+
return TexturePacking.empty<K>(size, allowRotate);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
get count(): number {
|
|
220
|
+
return this.used.size;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
get isEmpty(): boolean {
|
|
224
|
+
return this.used.size === 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** All free MaxRects. */
|
|
228
|
+
get freeRects(): Iterable<Box2i> {
|
|
229
|
+
return [...this.free.toSeq()].map(([, , v]) => v);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get occupancy(): number {
|
|
233
|
+
let area = 0;
|
|
234
|
+
for (const [, b] of this.used) {
|
|
235
|
+
const s = sizeOf(b);
|
|
236
|
+
area += s.x * s.y;
|
|
237
|
+
}
|
|
238
|
+
const total = this.size.x * this.size.y;
|
|
239
|
+
return total > 0 ? area / total : 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Try to insert one entry. Returns the new packing on success, or
|
|
244
|
+
* `null` if no free rect (with optional rotation) can accommodate
|
|
245
|
+
* `size`.
|
|
246
|
+
*
|
|
247
|
+
* If `id` is already present:
|
|
248
|
+
* - same size → returns `this` (no-op).
|
|
249
|
+
* - different size → removes then re-adds.
|
|
250
|
+
*/
|
|
251
|
+
tryAdd(id: K, size: V2i): TexturePacking<K> | null;
|
|
252
|
+
/** Atomic batch insert: returns null if any element would not fit. */
|
|
253
|
+
tryAdd(many: Iterable<readonly [K, V2i]>): TexturePacking<K> | null;
|
|
254
|
+
tryAdd(a: K | Iterable<readonly [K, V2i]>, b?: V2i): TexturePacking<K> | null {
|
|
255
|
+
if (b === undefined) {
|
|
256
|
+
let cur: TexturePacking<K> | null = this;
|
|
257
|
+
for (const [id, sz] of a as Iterable<readonly [K, V2i]>) {
|
|
258
|
+
if (cur === null) return null;
|
|
259
|
+
cur = cur.tryAddOne(id, sz);
|
|
260
|
+
}
|
|
261
|
+
return cur;
|
|
262
|
+
}
|
|
263
|
+
return this.tryAddOne(a as K, b);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private tryAddOne(id: K, size: V2i): TexturePacking<K> | null {
|
|
267
|
+
if (size.x <= 0 || size.y <= 0) return this;
|
|
268
|
+
const ex = this.used.get(id);
|
|
269
|
+
if (ex !== undefined) {
|
|
270
|
+
const s = sizeOf(ex);
|
|
271
|
+
if (s.x === size.x && s.y === size.y) return this;
|
|
272
|
+
const removed = this.remove(id);
|
|
273
|
+
return removed.tryAddOne(id, size);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Score each free rect; pick the smallest waste that fits.
|
|
277
|
+
let best: { rect: Box2i; upright: boolean; score: number } | undefined;
|
|
278
|
+
for (const [, , rect] of this.free.toSeq()) {
|
|
279
|
+
const sh = sizeOf(rect);
|
|
280
|
+
const f0 = sh.x >= size.x && sh.y >= size.y;
|
|
281
|
+
const f1 = this.allowRotate ? sh.x >= size.y && sh.y >= size.x : false;
|
|
282
|
+
const w0 = (sh.x - size.x) * (sh.y - size.y);
|
|
283
|
+
const w1 = (sh.x - size.y) * (sh.y - size.x);
|
|
284
|
+
let cand: { score: number; upright: boolean } | undefined;
|
|
285
|
+
if (f0 && f1) {
|
|
286
|
+
if (w0 < w1) cand = { score: w0, upright: true };
|
|
287
|
+
else cand = { score: w1, upright: false };
|
|
288
|
+
} else if (f0) cand = { score: w0, upright: true };
|
|
289
|
+
else if (f1) cand = { score: w1, upright: false };
|
|
290
|
+
if (cand !== undefined && (best === undefined || cand.score < best.score)) {
|
|
291
|
+
best = { rect, upright: cand.upright, score: cand.score };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (best === undefined) return null;
|
|
296
|
+
|
|
297
|
+
const placedSize = best.upright ? size : new V2i(size.y, size.x);
|
|
298
|
+
const fitting = best.rect;
|
|
299
|
+
|
|
300
|
+
// Two slabs left of `fitting` after placement: right strip + top
|
|
301
|
+
// strip. Mirror F# layout.
|
|
302
|
+
const r0 = mkBox(fitting.min.x + placedSize.x, fitting.min.y, fitting.max.x, fitting.max.y);
|
|
303
|
+
const r1 = mkBox(fitting.min.x, fitting.min.y + placedSize.y, fitting.max.x, fitting.max.y);
|
|
304
|
+
const rect = mkBox(
|
|
305
|
+
fitting.min.x,
|
|
306
|
+
fitting.min.y,
|
|
307
|
+
fitting.min.x + placedSize.x - 1,
|
|
308
|
+
fitting.min.y + placedSize.y - 1,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
let free = this.free.remove(boxKey(fitting));
|
|
312
|
+
free = addFree(r0, free);
|
|
313
|
+
free = addFree(r1, free);
|
|
314
|
+
|
|
315
|
+
// For every other free rect intersecting `rect`, replace with
|
|
316
|
+
// `splitRects(other, rect)`.
|
|
317
|
+
const intersecting: Array<[string, Box2i]> = [];
|
|
318
|
+
for (const [, , v] of free.toSeq()) {
|
|
319
|
+
if (intersects2i(v, rect)) intersecting.push([boxKey(v), v]);
|
|
320
|
+
}
|
|
321
|
+
for (const [k, other] of intersecting) {
|
|
322
|
+
free = free.remove(k);
|
|
323
|
+
for (const p of splitRects(other, rect)) free = addFree(p, free);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const newUsed = new Map(this.used);
|
|
327
|
+
newUsed.set(id, rect);
|
|
328
|
+
return new TexturePacking<K>(this.size, this.allowRotate, free, newUsed);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Remove `id` (no-op if absent). */
|
|
332
|
+
remove(id: K): TexturePacking<K> {
|
|
333
|
+
const rect = this.used.get(id);
|
|
334
|
+
if (rect === undefined) return this;
|
|
335
|
+
const newUsed = new Map(this.used);
|
|
336
|
+
newUsed.delete(id);
|
|
337
|
+
|
|
338
|
+
const qi = mkBox(rect.min.x - 1, rect.min.y - 1, rect.max.x + 1, rect.max.y + 1);
|
|
339
|
+
const adjacent: Box2i[] = [];
|
|
340
|
+
for (const [, , v] of [...this.free.getIntersecting(createBox(qi))].map(
|
|
341
|
+
([k, vv]) => [k, vv, vv] as const,
|
|
342
|
+
)) {
|
|
343
|
+
if (
|
|
344
|
+
v.min.x === qi.max.x ||
|
|
345
|
+
v.max.x === qi.min.x ||
|
|
346
|
+
v.min.y === qi.max.y ||
|
|
347
|
+
v.max.y === qi.min.y
|
|
348
|
+
) {
|
|
349
|
+
adjacent.push(v);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
let free = this.free;
|
|
353
|
+
for (const a of adjacent) free = free.remove(boxKey(a));
|
|
354
|
+
for (const m of merge(rect, adjacent)) free = addFree(m, free);
|
|
355
|
+
return new TexturePacking<K>(this.size, this.allowRotate, free, newUsed);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ─── Module-level helpers (matching the F# `module TexturePacking`) ─
|
|
360
|
+
|
|
361
|
+
export const empty = <K>(size: V2i): TexturePacking<K> => TexturePacking.empty<K>(size);
|
|
362
|
+
export const isEmpty = <K>(p: TexturePacking<K>): boolean => p.isEmpty;
|
|
363
|
+
export const count = <K>(p: TexturePacking<K>): number => p.count;
|
|
364
|
+
export const occupancy = <K>(p: TexturePacking<K>): number => p.occupancy;
|
|
365
|
+
export const tryAdd = <K>(id: K, size: V2i, p: TexturePacking<K>): TexturePacking<K> | null =>
|
|
366
|
+
p.tryAdd(id, size);
|
|
367
|
+
|
|
368
|
+
/** Sort-then-tryAdd helper. Sorting by descending max-extent matches the F# heuristic. */
|
|
369
|
+
export const tryAddMany = <K>(
|
|
370
|
+
elements: Iterable<readonly [K, V2i]>,
|
|
371
|
+
p: TexturePacking<K>,
|
|
372
|
+
): TexturePacking<K> | null => {
|
|
373
|
+
const arr = [...elements];
|
|
374
|
+
arr.sort((a, b) => Math.max(b[1].x, b[1].y) - Math.max(a[1].x, a[1].y));
|
|
375
|
+
return p.tryAdd(arr);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
export const tryOfArray = <K>(
|
|
379
|
+
size: V2i,
|
|
380
|
+
elements: ReadonlyArray<readonly [K, V2i]>,
|
|
381
|
+
): TexturePacking<K> | null => tryAddMany(elements, empty<K>(size));
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Build an optimal-square packing for the given elements, doubling
|
|
385
|
+
* the side until everything fits then binary-searching down. Mirrors
|
|
386
|
+
* F#'s `TexturePacking.square`.
|
|
387
|
+
*/
|
|
388
|
+
export const square = <K>(elements: ReadonlyArray<readonly [K, V2i]>): TexturePacking<K> => {
|
|
389
|
+
const arr = [...elements];
|
|
390
|
+
arr.sort((a, b) => Math.max(b[1].x, b[1].y) - Math.max(a[1].x, a[1].y));
|
|
391
|
+
if (arr.length === 0) return empty<K>(new V2i(0, 0));
|
|
392
|
+
if (arr.length === 1) {
|
|
393
|
+
const [id, sz] = arr[0]!;
|
|
394
|
+
const s = Math.max(sz.x, sz.y);
|
|
395
|
+
return empty<K>(new V2i(s, s)).tryAdd(id, sz)!;
|
|
396
|
+
}
|
|
397
|
+
let area = 0;
|
|
398
|
+
for (const [, sz] of arr) area += sz.x * sz.y;
|
|
399
|
+
let lo = Math.floor(Math.sqrt(area));
|
|
400
|
+
let hi = nextPow2(lo);
|
|
401
|
+
let best = tryOfArray(new V2i(hi, hi), arr);
|
|
402
|
+
while (best === null) {
|
|
403
|
+
lo = hi;
|
|
404
|
+
hi = hi * 2;
|
|
405
|
+
best = tryOfArray(new V2i(hi, hi), arr);
|
|
406
|
+
}
|
|
407
|
+
let bestPacking = best;
|
|
408
|
+
while (hi > lo + 1) {
|
|
409
|
+
const m = (hi + lo) >> 1;
|
|
410
|
+
const p = tryOfArray(new V2i(m, m), arr);
|
|
411
|
+
if (p !== null) {
|
|
412
|
+
bestPacking = p;
|
|
413
|
+
hi = m;
|
|
414
|
+
} else {
|
|
415
|
+
lo = m;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return bestPacking;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
function nextPow2(n: number): number {
|
|
422
|
+
if (n <= 1) return 1;
|
|
423
|
+
let p = 1;
|
|
424
|
+
while (p < n) p *= 2;
|
|
425
|
+
return p;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// silence unused-helper warning at export-time tooling
|
|
429
|
+
void equals2i;
|
package/src/window/loop.ts
CHANGED
|
@@ -39,6 +39,20 @@ export interface RunFrameOptions {
|
|
|
39
39
|
* behavior.
|
|
40
40
|
*/
|
|
41
41
|
readonly onAfterFrame?: () => void;
|
|
42
|
+
/**
|
|
43
|
+
* Frame pacer. When provided, runFrame awaits this promise after
|
|
44
|
+
* each frame's eval and BEFORE invoking `onAfterFrame` / scheduling
|
|
45
|
+
* the next rAF. Caps the JS encode rate to whatever the pacer
|
|
46
|
+
* resolves at — typically `() => device.queue.onSubmittedWorkDone()`
|
|
47
|
+
* to keep the GPU queue at depth 1 instead of letting submit-
|
|
48
|
+
* returns-immediately balloon the queue under load.
|
|
49
|
+
*
|
|
50
|
+
* Without a pacer, JS happily encodes 60 frames/s into a GPU that's
|
|
51
|
+
* actually running at 10 fps — the compositor then drops frames
|
|
52
|
+
* silently and the perceived rate has nothing to do with rAF rate.
|
|
53
|
+
* Always supply a pacer for continuous-redraw scenes.
|
|
54
|
+
*/
|
|
55
|
+
readonly pacer?: () => Promise<void>;
|
|
42
56
|
}
|
|
43
57
|
|
|
44
58
|
export type FrameCallback = (token: AdaptiveToken, info: FrameInfo) => void;
|
|
@@ -79,6 +93,17 @@ export function runFrame(
|
|
|
79
93
|
return frameNo;
|
|
80
94
|
});
|
|
81
95
|
|
|
96
|
+
const finalizeFrame = (): void => {
|
|
97
|
+
if (stopped) return;
|
|
98
|
+
if (opts.onAfterFrame !== undefined) {
|
|
99
|
+
transact(opts.onAfterFrame);
|
|
100
|
+
}
|
|
101
|
+
if (opts.maxFrames !== undefined && frameNo >= opts.maxFrames) {
|
|
102
|
+
stopped = true;
|
|
103
|
+
sub.dispose();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
82
107
|
const tick = (): void => {
|
|
83
108
|
if (stopped) return;
|
|
84
109
|
pending = false;
|
|
@@ -93,12 +118,13 @@ export function runFrame(
|
|
|
93
118
|
// Inside the eval the same mark would race the
|
|
94
119
|
// `outOfDate=false` reset and animation would stall after one
|
|
95
120
|
// frame.
|
|
96
|
-
if (opts.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
121
|
+
if (opts.pacer !== undefined) {
|
|
122
|
+
opts.pacer().then(finalizeFrame, (e) => {
|
|
123
|
+
stopped = true;
|
|
124
|
+
console.error("runFrame: pacer rejected", e);
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
finalizeFrame();
|
|
102
128
|
}
|
|
103
129
|
};
|
|
104
130
|
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { Effect } from "@aardworx/wombat.shader";
|
|
2
|
-
/**
|
|
3
|
-
* Compile a wombat.shader fragment `Effect` into a WGSL string
|
|
4
|
-
* usable as a `buildHeapScene` `fragmentShaders` value.
|
|
5
|
-
*
|
|
6
|
-
* The effect's fragment input field names must match the
|
|
7
|
-
* heap-scene `VsOut` fields: `worldPos`, `normal`, `color`,
|
|
8
|
-
* `lightLoc`. The fragment output must be a single `vec4<f32>`
|
|
9
|
-
* field (any name).
|
|
10
|
-
*/
|
|
11
|
-
export declare function compileHeapFragment(effect: Effect): string;
|
|
12
|
-
//# sourceMappingURL=heapShader.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"heapShader.d.ts","sourceRoot":"","sources":["../../src/runtime/heapShader.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAO1D"}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
// heapShader — bridge from wombat.shader effects to heap-scene
|
|
2
|
-
// fragment WGSL.
|
|
3
|
-
//
|
|
4
|
-
// `buildHeapScene` requires a fragment-stage WGSL string per group
|
|
5
|
-
// key (see runtime/heapScene.ts). Writing those strings by hand
|
|
6
|
-
// works but is the wrong long-term shape — users want to write the
|
|
7
|
-
// FS in the wombat.shader DSL and get the same effect-composition,
|
|
8
|
-
// type-checking, and DCE the per-RO path enjoys.
|
|
9
|
-
//
|
|
10
|
-
// `compileHeapFragment(effect)` runs wombat.shader's emit on a
|
|
11
|
-
// fragment Effect and rewrites the resulting WGSL to plug into the
|
|
12
|
-
// heap-scene's `VsOut` shape:
|
|
13
|
-
//
|
|
14
|
-
// ┌─ heap-scene's VsOut ┌─ DSL fragment input must use
|
|
15
|
-
// │ worldPos: vec3<f32> │ matching field names:
|
|
16
|
-
// │ normal: vec3<f32> │ worldPos, normal, color,
|
|
17
|
-
// │ color: vec4<f32> │ lightLoc
|
|
18
|
-
// │ lightLoc: vec3<f32> │
|
|
19
|
-
// └─ └─
|
|
20
|
-
//
|
|
21
|
-
// The DSL effect's fragment OUTPUT must be a single field; its name
|
|
22
|
-
// is irrelevant (the adapter rewraps to bare `@location(0) vec4<f32>`).
|
|
23
|
-
// `uniform.X` references inside the fragment body are redirected to
|
|
24
|
-
// `in.X` — the contract is that any uniform the FS reads must have
|
|
25
|
-
// been thread through the VS into the corresponding VsOut field.
|
|
26
|
-
//
|
|
27
|
-
// What this DOES NOT do: rewrite the vertex stage. The heap-scene's
|
|
28
|
-
// VS is fixed (storage-buffer uniform reads, vertex pulling from
|
|
29
|
-
// position/normal slabs); user effects only contribute the fragment.
|
|
30
|
-
/**
|
|
31
|
-
* Compile a wombat.shader fragment `Effect` into a WGSL string
|
|
32
|
-
* usable as a `buildHeapScene` `fragmentShaders` value.
|
|
33
|
-
*
|
|
34
|
-
* The effect's fragment input field names must match the
|
|
35
|
-
* heap-scene `VsOut` fields: `worldPos`, `normal`, `color`,
|
|
36
|
-
* `lightLoc`. The fragment output must be a single `vec4<f32>`
|
|
37
|
-
* field (any name).
|
|
38
|
-
*/
|
|
39
|
-
export function compileHeapFragment(effect) {
|
|
40
|
-
const compiled = effect.compile({ target: "wgsl" });
|
|
41
|
-
const fs = compiled.stages.find(s => s.stage === "fragment");
|
|
42
|
-
if (fs === undefined) {
|
|
43
|
-
throw new Error("compileHeapFragment: effect has no fragment stage");
|
|
44
|
-
}
|
|
45
|
-
return rewriteFragment(fs.source);
|
|
46
|
-
}
|
|
47
|
-
function rewriteFragment(wgsl) {
|
|
48
|
-
let s = wgsl;
|
|
49
|
-
// 1. Drop the auto-emitted UBO struct + binding. Heap-scene FS
|
|
50
|
-
// doesn't bind a UBO at all (uniforms ride on VsOut from VS).
|
|
51
|
-
s = s.replace(/struct\s+_UB_uniform\s*\{[\s\S]*?\};?\s*/g, "");
|
|
52
|
-
s = s.replace(/@group\(\d+\)\s*@binding\(\d+\)\s*var<uniform>\s+_w_uniform\s*:\s*_UB_uniform\s*;?\s*/g, "");
|
|
53
|
-
// 2. Redirect uniform reads to inter-stage. _w_uniform.LightLocation
|
|
54
|
-
// → in.lightLoc.
|
|
55
|
-
s = s.replace(/_w_uniform\.(\w+)/g, (_m, name) => "in." + uniformToVsOut(name));
|
|
56
|
-
// 3. Locate the @fragment function signature; capture the input
|
|
57
|
-
// struct name + output struct name from the signature itself.
|
|
58
|
-
// Two emit shapes seen in practice:
|
|
59
|
-
// - inline-marker: `wombat_fragment_<hash>` + `Wombat_fragment_<hash>{Input,Output}`
|
|
60
|
-
// - direct stage(): `<fnName>` + `<FnName>{Input,Output}`
|
|
61
|
-
// Detecting from the signature works for both.
|
|
62
|
-
const fnSigMatch = s.match(/@fragment\s+fn\s+(\w+)\s*\(\s*(\w+)\s*:\s*(\w+)\s*\)\s*->\s*(\w+)\s*\{/);
|
|
63
|
-
if (fnSigMatch === null) {
|
|
64
|
-
throw new Error("compileHeapFragment: emitted WGSL has no @fragment function header");
|
|
65
|
-
}
|
|
66
|
-
const fnHeader = fnSigMatch[0];
|
|
67
|
-
const paramName = fnSigMatch[2];
|
|
68
|
-
const inputStructName = fnSigMatch[3];
|
|
69
|
-
const outputStructName = fnSigMatch[4];
|
|
70
|
-
// 4. Drop the input struct.
|
|
71
|
-
const inputStructRe = new RegExp(`struct\\s+${escapeRegExp(inputStructName)}\\s*\\{[\\s\\S]*?\\};?`);
|
|
72
|
-
s = s.replace(inputStructRe, "");
|
|
73
|
-
// 5. Capture + drop the output struct, recording its (single) field name.
|
|
74
|
-
const outputStructRe = new RegExp(`struct\\s+${escapeRegExp(outputStructName)}\\s*\\{([\\s\\S]*?)\\};?`);
|
|
75
|
-
const outputMatch = s.match(outputStructRe);
|
|
76
|
-
if (outputMatch === null) {
|
|
77
|
-
throw new Error(`compileHeapFragment: cannot locate output struct '${outputStructName}'`);
|
|
78
|
-
}
|
|
79
|
-
const outputBody = outputMatch[1];
|
|
80
|
-
const fieldRe = /@location\(0\)\s+(\w+)\s*:\s*vec4<f32>\s*,?/g;
|
|
81
|
-
const fieldMatches = [...outputBody.matchAll(fieldRe)];
|
|
82
|
-
if (fieldMatches.length !== 1) {
|
|
83
|
-
throw new Error(`compileHeapFragment: fragment output must be a single @location(0) vec4<f32> field; got ${fieldMatches.length} field(s)`);
|
|
84
|
-
}
|
|
85
|
-
const outputFieldName = fieldMatches[0][1];
|
|
86
|
-
s = s.replace(outputMatch[0], "");
|
|
87
|
-
// 6. Rewrite the function header to the heap-scene shape. Any parameter
|
|
88
|
-
// name (in, input, …) is renormalised to `in` so the user shader's
|
|
89
|
-
// field references work uniformly downstream.
|
|
90
|
-
s = s.replace(fnHeader, "@fragment\nfn fs(in: VsOut) -> @location(0) vec4<f32> {");
|
|
91
|
-
if (paramName !== "in") {
|
|
92
|
-
// Rewrite `paramName.X` references to `in.X`.
|
|
93
|
-
const paramRe = new RegExp(`\\b${escapeRegExp(paramName)}\\.`, "g");
|
|
94
|
-
s = s.replace(paramRe, "in.");
|
|
95
|
-
}
|
|
96
|
-
// 7. Rewrite the body's `out: <Output>; ... out.<field> = expr; return out;` to
|
|
97
|
-
// return expr directly. wombat.shader's emit always uses this pattern; we
|
|
98
|
-
// transform it via two passes (capture the assignment, drop the var/return).
|
|
99
|
-
const fieldRef = `out\\.${escapeRegExp(outputFieldName)}`;
|
|
100
|
-
const assignRe = new RegExp(`var\\s+out\\s*:\\s*${escapeRegExp(outputStructName)}\\s*;([\\s\\S]*?)${fieldRef}\\s*=\\s*([\\s\\S]*?);([\\s\\S]*?)return\\s+out\\s*;`);
|
|
101
|
-
const assignMatch = s.match(assignRe);
|
|
102
|
-
if (assignMatch === null) {
|
|
103
|
-
// Fall back: if user wrote a more exotic body we surface a clear error.
|
|
104
|
-
throw new Error("compileHeapFragment: emitted body doesn't follow the var-out / out." + outputFieldName +
|
|
105
|
-
" = ... / return out pattern (got unsupported emit shape)");
|
|
106
|
-
}
|
|
107
|
-
const beforeAssign = assignMatch[1]; // statements between var-out decl and the field assignment
|
|
108
|
-
const expr = assignMatch[2];
|
|
109
|
-
const afterAssign = assignMatch[3]; // statements between the assignment and the return
|
|
110
|
-
s = s.replace(assignMatch[0], beforeAssign + afterAssign + `return ${expr};`);
|
|
111
|
-
// 8. Map references `in.<DslField>` to the corresponding heap VsOut
|
|
112
|
-
// field name (a 1:1 mapping when the user's input struct uses
|
|
113
|
-
// the canonical field names; identity otherwise).
|
|
114
|
-
s = s.replace(/in\.(\w+)/g, (_m, name) => "in." + dslFieldToVsOut(name));
|
|
115
|
-
// 9. Tidy: collapse 3+ blank lines to one.
|
|
116
|
-
s = s.replace(/\n{3,}/g, "\n\n").trimStart();
|
|
117
|
-
return s;
|
|
118
|
-
}
|
|
119
|
-
// Map a DSL uniform name to the heap-scene VsOut field that carries
|
|
120
|
-
// it. Today only `LightLocation` rides on VsOut; everything else
|
|
121
|
-
// would need to be added to the VS prelude in heapScene.ts.
|
|
122
|
-
function escapeRegExp(s) {
|
|
123
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
124
|
-
}
|
|
125
|
-
function uniformToVsOut(name) {
|
|
126
|
-
switch (name) {
|
|
127
|
-
case "LightLocation": return "lightLoc";
|
|
128
|
-
default:
|
|
129
|
-
throw new Error(`compileHeapFragment: uniform '${name}' is not carried on VsOut. Either thread it through the VS or use a fragment that doesn't read it.`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
// Map an inter-stage name from the DSL convention (PascalCase, e.g.
|
|
133
|
-
// 'Normals', 'WorldPositions') or our preferred direct names
|
|
134
|
-
// ('normal', 'worldPos') to the canonical VsOut field name.
|
|
135
|
-
function dslFieldToVsOut(name) {
|
|
136
|
-
switch (name) {
|
|
137
|
-
// canonical heap-scene names — pass through.
|
|
138
|
-
case "worldPos":
|
|
139
|
-
case "normal":
|
|
140
|
-
case "color":
|
|
141
|
-
case "lightLoc":
|
|
142
|
-
return name;
|
|
143
|
-
// legacy PascalCase from the wombat.dom convention — auto-translate
|
|
144
|
-
// so users can copy DSL effects from the per-RO path with minimal edits.
|
|
145
|
-
case "WorldPositions": return "worldPos";
|
|
146
|
-
case "WorldPosition": return "worldPos";
|
|
147
|
-
case "Normals": return "normal";
|
|
148
|
-
case "Normal": return "normal";
|
|
149
|
-
case "Colors": return "color";
|
|
150
|
-
case "Color": return "color";
|
|
151
|
-
case "LightLocation": return "lightLoc";
|
|
152
|
-
default:
|
|
153
|
-
throw new Error(`compileHeapFragment: input field '${name}' has no mapping to VsOut. ` +
|
|
154
|
-
`Use one of: worldPos, normal, color, lightLoc.`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
//# sourceMappingURL=heapShader.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"heapShader.js","sourceRoot":"","sources":["../../src/runtime/heapShader.ts"],"names":[],"mappings":"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"}
|