@driftengine/drft 4.4.2 → 4.6.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/README.md +1 -1
- package/dist/assemble.d.ts +7 -0
- package/dist/assemble.js +242 -0
- package/dist/drftAssembly.d.ts +62 -0
- package/dist/drftAssembly.js +267 -0
- package/dist/drftFormat.d.ts +58 -3
- package/dist/drftFormat.js +58 -4
- package/dist/drftLightVolume.d.ts +12 -0
- package/dist/drftLightVolume.js +172 -0
- package/dist/drftQuantised.js +4 -1
- package/dist/drftRead.d.ts +21 -1
- package/dist/drftRead.js +81 -4
- package/dist/drftRegions.d.ts +58 -0
- package/dist/drftRegions.js +313 -0
- package/dist/drftStream.d.ts +49 -1
- package/dist/drftStream.js +74 -5
- package/dist/drftWrite.d.ts +35 -1
- package/dist/drftWrite.js +103 -6
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/meshData.d.ts +19 -0
- package/dist/meshData.js +15 -0
- package/package.json +4 -4
- package/src/assemble.ts +268 -0
- package/src/drftAssembly.ts +322 -0
- package/src/drftFormat.ts +74 -4
- package/src/drftLightVolume.ts +179 -0
- package/src/drftQuantised.ts +8 -1
- package/src/drftRead.ts +107 -4
- package/src/drftRegions.ts +350 -0
- package/src/drftStream.ts +112 -7
- package/src/drftWrite.ts +147 -5
- package/src/index.ts +40 -0
- package/src/meshData.ts +38 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { DrftInstanceGroup } from './drftInstances.ts';
|
|
2
|
+
export interface DrftRegionLevel {
|
|
3
|
+
/** How far, in metres, this level's surface may stand from the real one. 0 for exact. */
|
|
4
|
+
readonly error: number;
|
|
5
|
+
/** The ordinals of the meshes this level draws. */
|
|
6
|
+
readonly meshes: readonly number[];
|
|
7
|
+
}
|
|
8
|
+
export interface DrftRegionCollision {
|
|
9
|
+
readonly positions: Float32Array;
|
|
10
|
+
readonly indices: Uint32Array;
|
|
11
|
+
}
|
|
12
|
+
export interface DrftRegion {
|
|
13
|
+
readonly id: number;
|
|
14
|
+
/** Everything the region holds, min xyz then max xyz. */
|
|
15
|
+
readonly bounds: ArrayLike<number>;
|
|
16
|
+
/** Finest first; at least one. */
|
|
17
|
+
readonly levels: readonly DrftRegionLevel[];
|
|
18
|
+
/** Its props, drawn at its finest level. */
|
|
19
|
+
readonly instances: readonly DrftInstanceGroup[];
|
|
20
|
+
/** Boxes that hide what stands behind them, six floats each. */
|
|
21
|
+
readonly occluders: Float32Array;
|
|
22
|
+
/** One triangle mesh the region collides as, or null. */
|
|
23
|
+
readonly collision: DrftRegionCollision | null;
|
|
24
|
+
}
|
|
25
|
+
/** The most levels a region may carry; the runtime keeps one value of a byte for "none". */
|
|
26
|
+
export declare const REGION_MAX_LEVELS = 254;
|
|
27
|
+
export declare function buildRegion(region: DrftRegion): Uint8Array;
|
|
28
|
+
/** One `REGN` payload, checked on its own against the file's mesh count. */
|
|
29
|
+
export declare function readRegion(buffer: ArrayBufferLike, offset: number, byteLength: number, meshCount: number): DrftRegion;
|
|
30
|
+
/**
|
|
31
|
+
* What regions must be to each other: an id once, a level mesh in one region's one level, and a
|
|
32
|
+
* group's prototype never also a level mesh. Kept across chunks by the writer, the whole-file reader
|
|
33
|
+
* and the stream alike, so a file one of them refuses the others refuse too.
|
|
34
|
+
*/
|
|
35
|
+
export declare class RegionLedger {
|
|
36
|
+
private readonly ids;
|
|
37
|
+
private readonly levelMeshes;
|
|
38
|
+
private readonly prototypes;
|
|
39
|
+
admit(region: DrftRegion): void;
|
|
40
|
+
/** Whether a mesh is any region's, as a level or a prop. */
|
|
41
|
+
names(mesh: number): boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Where each region's chunk goes among the meshes: `before[m]` is the regions written just ahead of
|
|
45
|
+
* mesh `m`, and `before[meshCount]` those after the last. A streaming reader then always has a
|
|
46
|
+
* region before the meshes it introduces.
|
|
47
|
+
*
|
|
48
|
+
* The meshes no region introduces come first, then each region's new meshes, in the order the
|
|
49
|
+
* regions are given and ascending. **A region introduces the kit pieces its assemblies copy, where
|
|
50
|
+
* no earlier region did** (`piecesOf` names them), so a piece travels with the first region that
|
|
51
|
+
* needs it rather than at the head of the file: a world's kit is most of its bytes, and a walker's
|
|
52
|
+
* first regions need a sliver of it. A piece ahead of the first mesh any region names is the
|
|
53
|
+
* file's own, as every piece was before a region could carry one, so either layout is valid. That
|
|
54
|
+
* a piece comes ahead of every assembly copying it is the kit's own rule (`checkCopies`). Anything
|
|
55
|
+
* else is refused by name rather than reordered, because the ordinals are what the regions and the
|
|
56
|
+
* materials already point at.
|
|
57
|
+
*/
|
|
58
|
+
export declare function planRegions(regions: readonly DrftRegion[], meshCount: number, piecesOf?: (mesh: number) => ArrayLike<number> | undefined): DrftRegion[][];
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `REGN`: one region of a streamed world — its geometry at every level of detail, its prop
|
|
3
|
+
* batches, its occluders and its collision — as one chunk, written just ahead of its meshes.
|
|
4
|
+
*
|
|
5
|
+
* **Required, because a reader that skipped it would draw every level of every region at once.**
|
|
6
|
+
* A region's levels are ordinary meshes, so an old reader would open a city as each district drawn
|
|
7
|
+
* three times over itself, and every prop it places as one copy at the origin — the `INST`
|
|
8
|
+
* argument. A refusal naming `REGN` is the honest answer. What that gives up is opening such a file
|
|
9
|
+
* at all in a reader before 1.21.
|
|
10
|
+
*
|
|
11
|
+
* **Levels are meshes named by ordinal, finest first, each with a geometric error**: how far, in
|
|
12
|
+
* metres, that level's surface may stand from the real one. The runtime turns an error into a
|
|
13
|
+
* switch distance against its own screen, which a file cannot know. A mesh belongs to at most one
|
|
14
|
+
* region's levels. An instance group's prototype may be shared by any number of regions — a lamp
|
|
15
|
+
* post is one mesh however many districts stand it — but is never also a level mesh.
|
|
16
|
+
*
|
|
17
|
+
* Layout, little-endian:
|
|
18
|
+
* - u32 id, then f32 × 6 bounds (min xyz, max xyz);
|
|
19
|
+
* - u32 level count, then per level: f32 error, u32 mesh count, that many u32 mesh ordinals;
|
|
20
|
+
* - u32 group count, then per group: u32 mesh ordinal, u32 copies, that many 16-float matrices;
|
|
21
|
+
* - u32 occluder count, then f32 × 6 a box (min xyz, max xyz);
|
|
22
|
+
* - u32 vertex count, u32 index count, f32 × 3 a vertex, u32 an index: the region's collision.
|
|
23
|
+
*/
|
|
24
|
+
import { DrftError, align } from './drftFormat.js';
|
|
25
|
+
/** The most levels a region may carry; the runtime keeps one value of a byte for "none". */
|
|
26
|
+
export const REGION_MAX_LEVELS = 254;
|
|
27
|
+
export function buildRegion(region) {
|
|
28
|
+
checkRegion(region, Number.POSITIVE_INFINITY);
|
|
29
|
+
let bytes = 4 + 24 + 4 + 4 + 4 + 8;
|
|
30
|
+
for (const level of region.levels)
|
|
31
|
+
bytes += 8 + level.meshes.length * 4;
|
|
32
|
+
for (const group of region.instances)
|
|
33
|
+
bytes += 8 + group.transforms.length * 4;
|
|
34
|
+
bytes += region.occluders.length * 4;
|
|
35
|
+
const vertices = region.collision?.positions.length ?? 0;
|
|
36
|
+
const indices = region.collision?.indices.length ?? 0;
|
|
37
|
+
bytes += vertices * 4 + indices * 4;
|
|
38
|
+
const out = new Uint8Array(align(bytes));
|
|
39
|
+
const view = new DataView(out.buffer);
|
|
40
|
+
let at = 0;
|
|
41
|
+
const u32 = (value) => {
|
|
42
|
+
view.setUint32(at, value, true);
|
|
43
|
+
at += 4;
|
|
44
|
+
};
|
|
45
|
+
const f32 = (value) => {
|
|
46
|
+
view.setFloat32(at, value, true);
|
|
47
|
+
at += 4;
|
|
48
|
+
};
|
|
49
|
+
u32(region.id);
|
|
50
|
+
for (let i = 0; i < 6; i++)
|
|
51
|
+
f32(region.bounds[i]);
|
|
52
|
+
u32(region.levels.length);
|
|
53
|
+
for (const level of region.levels) {
|
|
54
|
+
f32(level.error);
|
|
55
|
+
u32(level.meshes.length);
|
|
56
|
+
for (const mesh of level.meshes)
|
|
57
|
+
u32(mesh);
|
|
58
|
+
}
|
|
59
|
+
u32(region.instances.length);
|
|
60
|
+
for (const group of region.instances) {
|
|
61
|
+
u32(group.mesh);
|
|
62
|
+
u32(group.transforms.length / 16);
|
|
63
|
+
for (let i = 0; i < group.transforms.length; i++)
|
|
64
|
+
f32(group.transforms[i]);
|
|
65
|
+
}
|
|
66
|
+
u32(region.occluders.length / 6);
|
|
67
|
+
for (let i = 0; i < region.occluders.length; i++)
|
|
68
|
+
f32(region.occluders[i]);
|
|
69
|
+
u32(vertices / 3);
|
|
70
|
+
u32(indices);
|
|
71
|
+
for (let i = 0; i < vertices; i++)
|
|
72
|
+
f32(region.collision?.positions[i]);
|
|
73
|
+
for (let i = 0; i < indices; i++)
|
|
74
|
+
u32(region.collision?.indices[i]);
|
|
75
|
+
/* The one check the builder owns: it wrote exactly what it measured. */
|
|
76
|
+
if (at !== bytes)
|
|
77
|
+
throw new DrftError(`internal: REGN wrote ${at} of ${bytes} bytes`);
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/** One `REGN` payload, checked on its own against the file's mesh count. */
|
|
81
|
+
export function readRegion(buffer, offset, byteLength, meshCount) {
|
|
82
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
83
|
+
let at = 0;
|
|
84
|
+
const need = (bytes, what) => {
|
|
85
|
+
if (at + bytes > byteLength)
|
|
86
|
+
throw new DrftError(`REGN ends inside ${what}`);
|
|
87
|
+
};
|
|
88
|
+
const u32 = () => {
|
|
89
|
+
const value = view.getUint32(at, true);
|
|
90
|
+
at += 4;
|
|
91
|
+
return value;
|
|
92
|
+
};
|
|
93
|
+
const floats = (count) => {
|
|
94
|
+
const out = new Float32Array(count);
|
|
95
|
+
for (let i = 0; i < count; i++)
|
|
96
|
+
out[i] = view.getFloat32(at + i * 4, true);
|
|
97
|
+
at += count * 4;
|
|
98
|
+
return out;
|
|
99
|
+
};
|
|
100
|
+
need(32, 'its header');
|
|
101
|
+
const id = u32();
|
|
102
|
+
const bounds = floats(6);
|
|
103
|
+
const levelCount = u32();
|
|
104
|
+
const levels = [];
|
|
105
|
+
for (let l = 0; l < levelCount; l++) {
|
|
106
|
+
need(8, `region ${id}'s level ${l}`);
|
|
107
|
+
const error = view.getFloat32(at, true);
|
|
108
|
+
at += 4;
|
|
109
|
+
const count = u32();
|
|
110
|
+
need(count * 4, `region ${id}'s level ${l}`);
|
|
111
|
+
const meshes = [];
|
|
112
|
+
for (let m = 0; m < count; m++)
|
|
113
|
+
meshes.push(u32());
|
|
114
|
+
levels.push({ error, meshes });
|
|
115
|
+
}
|
|
116
|
+
need(4, `region ${id}'s groups`);
|
|
117
|
+
const groupCount = u32();
|
|
118
|
+
const instances = [];
|
|
119
|
+
for (let g = 0; g < groupCount; g++) {
|
|
120
|
+
need(8, `region ${id}'s group ${g}`);
|
|
121
|
+
const mesh = u32();
|
|
122
|
+
const copies = u32();
|
|
123
|
+
need(copies * 64, `region ${id}'s group ${g}`);
|
|
124
|
+
instances.push({ mesh, transforms: floats(copies * 16) });
|
|
125
|
+
}
|
|
126
|
+
need(4, `region ${id}'s occluders`);
|
|
127
|
+
const occluderCount = u32();
|
|
128
|
+
need(occluderCount * 24, `region ${id}'s occluders`);
|
|
129
|
+
const occluders = floats(occluderCount * 6);
|
|
130
|
+
need(8, `region ${id}'s collision`);
|
|
131
|
+
const vertices = u32();
|
|
132
|
+
const indexCount = u32();
|
|
133
|
+
need(vertices * 12 + indexCount * 4, `region ${id}'s collision`);
|
|
134
|
+
const positions = floats(vertices * 3);
|
|
135
|
+
const indices = new Uint32Array(indexCount);
|
|
136
|
+
for (let i = 0; i < indexCount; i++)
|
|
137
|
+
indices[i] = u32();
|
|
138
|
+
const region = {
|
|
139
|
+
id,
|
|
140
|
+
bounds,
|
|
141
|
+
levels,
|
|
142
|
+
instances,
|
|
143
|
+
occluders,
|
|
144
|
+
collision: vertices === 0 && indexCount === 0 ? null : { positions, indices },
|
|
145
|
+
};
|
|
146
|
+
checkRegion(region, meshCount);
|
|
147
|
+
return region;
|
|
148
|
+
}
|
|
149
|
+
/** What one region must be on its own, whoever is asking. */
|
|
150
|
+
function checkRegion(region, meshCount) {
|
|
151
|
+
const name = `region ${region.id}`;
|
|
152
|
+
const b = region.bounds;
|
|
153
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
154
|
+
if (!(b[axis] <= b[axis + 3])) {
|
|
155
|
+
throw new DrftError(`${name}'s bounds are inside out on axis ${axis}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (region.levels.length < 1 || region.levels.length > REGION_MAX_LEVELS) {
|
|
159
|
+
throw new DrftError(`${name} has ${region.levels.length} levels; 1 to ${REGION_MAX_LEVELS} are allowed`);
|
|
160
|
+
}
|
|
161
|
+
let previous = 0;
|
|
162
|
+
for (let l = 0; l < region.levels.length; l++) {
|
|
163
|
+
const level = region.levels[l];
|
|
164
|
+
if (!(level.error >= previous) || !Number.isFinite(level.error)) {
|
|
165
|
+
throw new DrftError(`${name}'s level ${l} has error ${level.error}, finer than the one before`);
|
|
166
|
+
}
|
|
167
|
+
previous = level.error;
|
|
168
|
+
for (const mesh of level.meshes) {
|
|
169
|
+
if (mesh >= meshCount) {
|
|
170
|
+
throw new DrftError(`${name} draws mesh ${mesh}, and the file carries ${meshCount}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const grouped = new Set();
|
|
175
|
+
for (const group of region.instances) {
|
|
176
|
+
if (group.mesh >= meshCount) {
|
|
177
|
+
throw new DrftError(`${name} places mesh ${group.mesh}, and the file carries ${meshCount}`);
|
|
178
|
+
}
|
|
179
|
+
if (group.transforms.length === 0 || group.transforms.length % 16 !== 0) {
|
|
180
|
+
throw new DrftError(`${name}'s group for mesh ${group.mesh} is not whole matrices`);
|
|
181
|
+
}
|
|
182
|
+
if (grouped.has(group.mesh))
|
|
183
|
+
throw new DrftError(`${name} groups mesh ${group.mesh} twice`);
|
|
184
|
+
grouped.add(group.mesh);
|
|
185
|
+
}
|
|
186
|
+
if (region.occluders.length % 6 !== 0) {
|
|
187
|
+
throw new DrftError(`${name}'s occluders are not whole boxes`);
|
|
188
|
+
}
|
|
189
|
+
const collision = region.collision;
|
|
190
|
+
if (collision !== null) {
|
|
191
|
+
const vertices = collision.positions.length / 3;
|
|
192
|
+
if (collision.positions.length % 3 !== 0 || collision.indices.length % 3 !== 0) {
|
|
193
|
+
throw new DrftError(`${name}'s collision is not whole vertices and triangles`);
|
|
194
|
+
}
|
|
195
|
+
for (let i = 0; i < collision.indices.length; i++) {
|
|
196
|
+
if (collision.indices[i] >= vertices) {
|
|
197
|
+
throw new DrftError(`${name}'s collision names vertex ${collision.indices[i]} of ${vertices}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* What regions must be to each other: an id once, a level mesh in one region's one level, and a
|
|
204
|
+
* group's prototype never also a level mesh. Kept across chunks by the writer, the whole-file reader
|
|
205
|
+
* and the stream alike, so a file one of them refuses the others refuse too.
|
|
206
|
+
*/
|
|
207
|
+
export class RegionLedger {
|
|
208
|
+
ids = new Set();
|
|
209
|
+
levelMeshes = new Set();
|
|
210
|
+
prototypes = new Set();
|
|
211
|
+
admit(region) {
|
|
212
|
+
if (this.ids.has(region.id))
|
|
213
|
+
throw new DrftError(`region ${region.id} appears twice`);
|
|
214
|
+
this.ids.add(region.id);
|
|
215
|
+
for (const level of region.levels) {
|
|
216
|
+
for (const mesh of level.meshes) {
|
|
217
|
+
if (this.levelMeshes.has(mesh) || this.prototypes.has(mesh)) {
|
|
218
|
+
throw new DrftError(`mesh ${mesh} is region ${region.id}'s and something else's; a level mesh belongs to one region's one level`);
|
|
219
|
+
}
|
|
220
|
+
this.levelMeshes.add(mesh);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const group of region.instances) {
|
|
224
|
+
if (this.levelMeshes.has(group.mesh)) {
|
|
225
|
+
throw new DrftError(`mesh ${group.mesh} is both a level mesh and a prop region ${region.id} places`);
|
|
226
|
+
}
|
|
227
|
+
this.prototypes.add(group.mesh);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** Whether a mesh is any region's, as a level or a prop. */
|
|
231
|
+
names(mesh) {
|
|
232
|
+
return this.levelMeshes.has(mesh) || this.prototypes.has(mesh);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Where each region's chunk goes among the meshes: `before[m]` is the regions written just ahead of
|
|
237
|
+
* mesh `m`, and `before[meshCount]` those after the last. A streaming reader then always has a
|
|
238
|
+
* region before the meshes it introduces.
|
|
239
|
+
*
|
|
240
|
+
* The meshes no region introduces come first, then each region's new meshes, in the order the
|
|
241
|
+
* regions are given and ascending. **A region introduces the kit pieces its assemblies copy, where
|
|
242
|
+
* no earlier region did** (`piecesOf` names them), so a piece travels with the first region that
|
|
243
|
+
* needs it rather than at the head of the file: a world's kit is most of its bytes, and a walker's
|
|
244
|
+
* first regions need a sliver of it. A piece ahead of the first mesh any region names is the
|
|
245
|
+
* file's own, as every piece was before a region could carry one, so either layout is valid. That
|
|
246
|
+
* a piece comes ahead of every assembly copying it is the kit's own rule (`checkCopies`). Anything
|
|
247
|
+
* else is refused by name rather than reordered, because the ordinals are what the regions and the
|
|
248
|
+
* materials already point at.
|
|
249
|
+
*/
|
|
250
|
+
export function planRegions(regions, meshCount, piecesOf = () => undefined) {
|
|
251
|
+
const ledger = new RegionLedger();
|
|
252
|
+
for (const region of regions) {
|
|
253
|
+
checkRegion(region, meshCount);
|
|
254
|
+
ledger.admit(region);
|
|
255
|
+
}
|
|
256
|
+
/* What each region introduces: its levels' meshes and its props, then the pieces its level
|
|
257
|
+
meshes copy that nobody introduced before it — those past the first mesh any region names;
|
|
258
|
+
a piece ahead of it is the file's own, written first as every piece once was. */
|
|
259
|
+
let firstNamed = meshCount;
|
|
260
|
+
for (let m = 0; m < meshCount; m++) {
|
|
261
|
+
if (ledger.names(m)) {
|
|
262
|
+
firstNamed = m;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const introduced = new Set();
|
|
267
|
+
const fresh = regions.map((region) => {
|
|
268
|
+
const out = [];
|
|
269
|
+
const take = (mesh) => {
|
|
270
|
+
if (introduced.has(mesh))
|
|
271
|
+
return;
|
|
272
|
+
introduced.add(mesh);
|
|
273
|
+
out.push(mesh);
|
|
274
|
+
};
|
|
275
|
+
for (const level of region.levels)
|
|
276
|
+
for (const mesh of level.meshes)
|
|
277
|
+
take(mesh);
|
|
278
|
+
for (const group of region.instances)
|
|
279
|
+
take(group.mesh);
|
|
280
|
+
for (const level of region.levels) {
|
|
281
|
+
for (const mesh of level.meshes) {
|
|
282
|
+
const pieces = piecesOf(mesh);
|
|
283
|
+
for (let i = 0; i < (pieces?.length ?? 0); i++) {
|
|
284
|
+
const piece = pieces?.[i];
|
|
285
|
+
if (piece > firstNamed && !ledger.names(piece))
|
|
286
|
+
take(piece);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return out.sort((a, b) => a - b);
|
|
291
|
+
});
|
|
292
|
+
let next = meshCount - introduced.size;
|
|
293
|
+
for (let m = 0; m < next; m++) {
|
|
294
|
+
if (introduced.has(m)) {
|
|
295
|
+
throw new DrftError(`mesh ${m} is a region's and comes before mesh ${next - 1}, which no region names; ` +
|
|
296
|
+
'a file with regions writes the meshes no region names first');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const before = [];
|
|
300
|
+
for (let m = 0; m <= meshCount; m++)
|
|
301
|
+
before.push([]);
|
|
302
|
+
regions.forEach((region, r) => {
|
|
303
|
+
before[next].push(region);
|
|
304
|
+
for (const mesh of fresh[r]) {
|
|
305
|
+
if (mesh !== next) {
|
|
306
|
+
throw new DrftError(`region ${region.id} introduces mesh ${mesh} where mesh ${next} is next; ` +
|
|
307
|
+
"write each region's new meshes in the order the regions are written");
|
|
308
|
+
}
|
|
309
|
+
next++;
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
return before;
|
|
313
|
+
}
|
package/dist/drftStream.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ import type { DrftMorph } from './drftSkin.ts';
|
|
|
8
8
|
import type { DrftInstanceGroup } from './drftInstances.ts';
|
|
9
9
|
import type { DrftSdfvEntry } from './sdfv.ts';
|
|
10
10
|
import type { DrftLight } from './drftLights.ts';
|
|
11
|
+
import type { PieceLookup } from './assemble.ts';
|
|
12
|
+
import type { DrftAssembly } from './drftAssembly.ts';
|
|
13
|
+
import type { DrftLightVolume } from './drftLightVolume.ts';
|
|
14
|
+
import type { DrftRegion } from './drftRegions.ts';
|
|
15
|
+
import { type NavPolyMesh } from './navm.ts';
|
|
16
|
+
import { type EntsScene } from './ents.ts';
|
|
11
17
|
/**
|
|
12
18
|
* What the file says is coming, known from the first few kilobytes.
|
|
13
19
|
*
|
|
@@ -60,8 +66,25 @@ export interface DrftStreamHandlers {
|
|
|
60
66
|
* stop drawing it in the same frame it starts drawing the model, or the two are both there.
|
|
61
67
|
*/
|
|
62
68
|
readonly onLod?: (mesh: MeshData, level: number) => void;
|
|
63
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* One mesh, in the order the file lays them out, with its ordinal in that order. A kit piece is
|
|
71
|
+
* not one — it goes to `onPiece` — and an assembly is one only where `onAssembly` is absent, in
|
|
72
|
+
* which case it arrives here expanded.
|
|
73
|
+
*/
|
|
64
74
|
readonly onMesh?: (mesh: MeshData, ordinal: number) => void;
|
|
75
|
+
/**
|
|
76
|
+
* The ordinals of the kit's pieces, once, when `KITS` lands — ahead of the geometry, so a
|
|
77
|
+
* consumer knows which meshes are drawn only as copies before any of them arrives.
|
|
78
|
+
*/
|
|
79
|
+
readonly onKit?: (pieces: readonly number[]) => void;
|
|
80
|
+
/** One kit piece, as it lands. The stream keeps it for the assemblies that copy it. */
|
|
81
|
+
readonly onPiece?: (mesh: MeshData, ordinal: number) => void;
|
|
82
|
+
/**
|
|
83
|
+
* One assembly as the file carried it, with a lookup for its pieces, which have all arrived. A
|
|
84
|
+
* consumer that pages its world keeps it this small and expands it (`expandAssembly`) when it is
|
|
85
|
+
* needed; without this handler the stream expands it and calls `onMesh`.
|
|
86
|
+
*/
|
|
87
|
+
readonly onAssembly?: (assembly: DrftAssembly, ordinal: number, piece: PieceLookup) => void;
|
|
65
88
|
/**
|
|
66
89
|
* One block of a Gaussian splat capture, with its ordinal.
|
|
67
90
|
*
|
|
@@ -84,6 +107,13 @@ export interface DrftStreamHandlers {
|
|
|
84
107
|
* puts it, so a consumer knows a mesh is drawn many times before that mesh arrives.
|
|
85
108
|
*/
|
|
86
109
|
readonly onInstances?: (groups: readonly DrftInstanceGroup[]) => void;
|
|
110
|
+
/**
|
|
111
|
+
* One region of a streamed world, when its `REGN` lands — ahead of the meshes it introduces, so
|
|
112
|
+
* a consumer knows a mesh is a region's level or prop before it arrives. See `drftRegions.ts`.
|
|
113
|
+
*/
|
|
114
|
+
readonly onRegion?: (region: DrftRegion) => void;
|
|
115
|
+
/** A world's summed lights, once, when `LVOL` lands. See `drftLightVolume.ts`. */
|
|
116
|
+
readonly onLightVolume?: (volume: DrftLightVolume) => void;
|
|
87
117
|
/** The lights the scene was authored with, once, when `LITE` lands. See `drftLights.ts`. */
|
|
88
118
|
readonly onLights?: (lights: readonly DrftLight[]) => void;
|
|
89
119
|
/**
|
|
@@ -103,6 +133,18 @@ export interface DrftStreamHandlers {
|
|
|
103
133
|
* the geometry so this is usually possible.
|
|
104
134
|
*/
|
|
105
135
|
readonly onClip?: (clip: AnimationClip, ordinal: number) => void;
|
|
136
|
+
/**
|
|
137
|
+
* The convex hulls the asset collides as, once, when `COLL` lands. See `drftColliders.ts`.
|
|
138
|
+
*
|
|
139
|
+
* These three — collision, the navigation mesh and the entities — were `readDrft`'s alone, so a
|
|
140
|
+
* scene that streamed its geometry had to fetch the file again, whole, to learn where it could
|
|
141
|
+
* walk and what stood in it.
|
|
142
|
+
*/
|
|
143
|
+
readonly onColliders?: (hulls: readonly Float32Array[]) => void;
|
|
144
|
+
/** The navigation mesh, once, when `NAVM` lands. See `navm.ts`. */
|
|
145
|
+
readonly onNavigation?: (navigation: NavPolyMesh) => void;
|
|
146
|
+
/** The serialised entities, once, when `ENTS` lands. See `ents.ts`. */
|
|
147
|
+
readonly onEntities?: (entities: EntsScene) => void;
|
|
106
148
|
readonly onMaterials?: (materials: readonly DrftMaterial[]) => void;
|
|
107
149
|
readonly onTexture?: (texture: DrftTexture, ordinal: number) => void;
|
|
108
150
|
}
|
|
@@ -137,10 +179,16 @@ export declare class DrftStream {
|
|
|
137
179
|
private preludeBytes;
|
|
138
180
|
private received;
|
|
139
181
|
private chunks;
|
|
182
|
+
/** What the regions so far claim, so a second claim on a mesh is refused as `readDrft` refuses it. */
|
|
183
|
+
private readonly regions;
|
|
140
184
|
/** How many table entries have been reported complete, in table order. */
|
|
141
185
|
private settled;
|
|
142
186
|
private meshOrdinal;
|
|
143
187
|
private textureOrdinal;
|
|
188
|
+
/** The kit's pieces once `KITS` lands, and each piece as it arrives. */
|
|
189
|
+
private kit;
|
|
190
|
+
private readonly pieces;
|
|
191
|
+
private readonly pieceAt;
|
|
144
192
|
private head;
|
|
145
193
|
private meshCount;
|
|
146
194
|
private materialCount;
|
package/dist/drftStream.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
/** Reading a `.drft` while it is still arriving, chunk by chunk. */
|
|
2
|
-
import { CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_LODM, CHUNK_SPLT, CHUNK_MATL, CHUNK_MESH, CHUNK_REQUIRED, CHUNK_TEXS, DRFT_MAGIC, DRFT_VERSION_MAJOR, DrftError, HEADER_BYTES, KNOWN_CHUNKS, fourCCName, CHUNK_ANIM, CHUNK_NODE, CHUNK_INST, CHUNK_LITE, CHUNK_SDFV, CHUNK_MSHQ, CHUNK_SKIN, CHUNK_MORP, } from './drftFormat.js';
|
|
2
|
+
import { CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_LODM, CHUNK_SPLT, CHUNK_MATL, CHUNK_MESH, CHUNK_REQUIRED, CHUNK_TEXS, DRFT_MAGIC, DRFT_VERSION_MAJOR, DrftError, HEADER_BYTES, KNOWN_CHUNKS, fourCCName, CHUNK_ANIM, CHUNK_NODE, CHUNK_INST, CHUNK_REGN, CHUNK_LVOL, CHUNK_LITE, CHUNK_SDFV, CHUNK_COLL, CHUNK_NAVM, CHUNK_ENTS, CHUNK_MSHQ, CHUNK_KITS, CHUNK_MSHC, isMeshChunk, CHUNK_SKIN, CHUNK_MORP, } from './drftFormat.js';
|
|
3
3
|
import { readHead, readMaterials, readMesh, readSplatBlock, readTexture } from './drftRead.js';
|
|
4
4
|
import { readClip, readMorph, readNodes, readSkin } from './drftSkin.js';
|
|
5
5
|
import { readInstances } from './drftInstances.js';
|
|
6
6
|
import { readLights } from './drftLights.js';
|
|
7
7
|
import { readSdfv } from './sdfv.js';
|
|
8
8
|
import { decodeQuantisedMesh } from './drftQuantised.js';
|
|
9
|
+
import { readColliders } from './drftColliders.js';
|
|
10
|
+
import { expandAssembly } from './assemble.js';
|
|
11
|
+
import { checkCopies, readAssembly, readKit } from './drftAssembly.js';
|
|
12
|
+
import { RegionLedger, readRegion } from './drftRegions.js';
|
|
13
|
+
import { readLightVolume } from './drftLightVolume.js';
|
|
14
|
+
import { readNavm } from './navm.js';
|
|
15
|
+
import { readEnts } from './ents.js';
|
|
9
16
|
/**
|
|
10
17
|
* An incremental `.drft` reader: feed it bytes, it reports each chunk as that chunk completes.
|
|
11
18
|
*
|
|
@@ -37,10 +44,21 @@ export class DrftStream {
|
|
|
37
44
|
preludeBytes = 0;
|
|
38
45
|
received = 0;
|
|
39
46
|
chunks = [];
|
|
47
|
+
/** What the regions so far claim, so a second claim on a mesh is refused as `readDrft` refuses it. */
|
|
48
|
+
regions = new RegionLedger();
|
|
40
49
|
/** How many table entries have been reported complete, in table order. */
|
|
41
50
|
settled = 0;
|
|
42
51
|
meshOrdinal = 0;
|
|
43
52
|
textureOrdinal = 0;
|
|
53
|
+
/** The kit's pieces once `KITS` lands, and each piece as it arrives. */
|
|
54
|
+
kit = null;
|
|
55
|
+
pieces = new Map();
|
|
56
|
+
pieceAt = (o) => {
|
|
57
|
+
const piece = this.pieces.get(o);
|
|
58
|
+
if (piece === undefined)
|
|
59
|
+
throw new DrftError(`kit piece ${o} has not arrived`);
|
|
60
|
+
return piece;
|
|
61
|
+
};
|
|
44
62
|
head = null;
|
|
45
63
|
meshCount = 0;
|
|
46
64
|
materialCount = 0;
|
|
@@ -200,7 +218,7 @@ export class DrftStream {
|
|
|
200
218
|
* counted in `meshCount`, which is the number of parts — an outline is not a part, and
|
|
201
219
|
* a readout saying "1 of 188 parts" would be counting the model twice.
|
|
202
220
|
*/
|
|
203
|
-
if (chunk.code
|
|
221
|
+
if (isMeshChunk(chunk.code) || chunk.code === CHUNK_LODM) {
|
|
204
222
|
meshBytes += chunk.byteLength;
|
|
205
223
|
}
|
|
206
224
|
if (chunk.code === CHUNK_TEXS)
|
|
@@ -221,7 +239,7 @@ export class DrftStream {
|
|
|
221
239
|
this.manifestSent = true;
|
|
222
240
|
this.handlers.onManifest?.({
|
|
223
241
|
totalBytes: this.buffer.byteLength,
|
|
224
|
-
meshCount: this.chunks.filter((c) => c.code
|
|
242
|
+
meshCount: this.chunks.filter((c) => isMeshChunk(c.code)).length,
|
|
225
243
|
textureCount: this.chunks.filter((c) => c.code === CHUNK_TEXS).length,
|
|
226
244
|
lodCount: this.chunks.filter((c) => c.code === CHUNK_LODM).length,
|
|
227
245
|
splatBlockCount: this.chunks.filter((c) => c.code === CHUNK_SPLT).length,
|
|
@@ -252,7 +270,40 @@ export class DrftStream {
|
|
|
252
270
|
? readMesh(this.buffer, chunk)
|
|
253
271
|
: decodeQuantisedMesh(this.buffer, chunk.offset, chunk.byteLength);
|
|
254
272
|
this.meshCount++;
|
|
255
|
-
|
|
273
|
+
const ordinal = this.meshOrdinal++;
|
|
274
|
+
if (this.kit?.has(ordinal) === true) {
|
|
275
|
+
this.pieces.set(ordinal, mesh);
|
|
276
|
+
this.handlers.onPiece?.(mesh, ordinal);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
this.handlers.onMesh?.(mesh, ordinal);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else if (chunk.code === CHUNK_MSHC) {
|
|
283
|
+
const assembly = readAssembly(this.buffer, chunk.offset, chunk.byteLength);
|
|
284
|
+
this.meshCount++;
|
|
285
|
+
const ordinal = this.meshOrdinal++;
|
|
286
|
+
if (this.kit?.has(ordinal) === true) {
|
|
287
|
+
throw new DrftError(`mesh ${ordinal} is a kit piece and an assembly`);
|
|
288
|
+
}
|
|
289
|
+
checkCopies(assembly, ordinal, this.kit ?? new Set(), (o) => this.pieces.get(o));
|
|
290
|
+
if (this.handlers.onAssembly !== undefined) {
|
|
291
|
+
this.handlers.onAssembly(assembly, ordinal, this.pieceAt);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
this.handlers.onMesh?.(expandAssembly(assembly, this.pieceAt), ordinal);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else if (chunk.code === CHUNK_KITS) {
|
|
298
|
+
const meshes = this.chunks.filter((c) => isMeshChunk(c.code)).length;
|
|
299
|
+
const kit = readKit(this.buffer, chunk.offset, chunk.byteLength, meshes);
|
|
300
|
+
/* A piece already handed over as a mesh has been drawn; the writer puts the kit first. */
|
|
301
|
+
const early = kit.find((o) => o < this.meshOrdinal);
|
|
302
|
+
if (early !== undefined) {
|
|
303
|
+
throw new DrftError(`KITS arrives after mesh ${early}, one of its pieces`);
|
|
304
|
+
}
|
|
305
|
+
this.kit = new Set(kit);
|
|
306
|
+
this.handlers.onKit?.(kit);
|
|
256
307
|
}
|
|
257
308
|
else if (chunk.code === CHUNK_SPLT) {
|
|
258
309
|
/* The ordinal the file gave it, so blocks append in the writer's order however they
|
|
@@ -273,9 +324,27 @@ export class DrftStream {
|
|
|
273
324
|
this.handlers.onNodes?.(readNodes(this.buffer, chunk.offset, chunk.byteLength));
|
|
274
325
|
}
|
|
275
326
|
else if (chunk.code === CHUNK_INST) {
|
|
276
|
-
const meshes = this.chunks.filter((c) => c.code
|
|
327
|
+
const meshes = this.chunks.filter((c) => isMeshChunk(c.code)).length;
|
|
277
328
|
this.handlers.onInstances?.(readInstances(this.buffer, chunk.offset, chunk.byteLength, meshes));
|
|
278
329
|
}
|
|
330
|
+
else if (chunk.code === CHUNK_REGN) {
|
|
331
|
+
const meshes = this.chunks.filter((c) => isMeshChunk(c.code)).length;
|
|
332
|
+
const region = readRegion(this.buffer, chunk.offset, chunk.byteLength, meshes);
|
|
333
|
+
this.regions.admit(region);
|
|
334
|
+
this.handlers.onRegion?.(region);
|
|
335
|
+
}
|
|
336
|
+
else if (chunk.code === CHUNK_LVOL) {
|
|
337
|
+
this.handlers.onLightVolume?.(readLightVolume(this.buffer, chunk.offset, chunk.byteLength));
|
|
338
|
+
}
|
|
339
|
+
else if (chunk.code === CHUNK_COLL) {
|
|
340
|
+
this.handlers.onColliders?.(readColliders(this.buffer, chunk.offset, chunk.byteLength));
|
|
341
|
+
}
|
|
342
|
+
else if (chunk.code === CHUNK_NAVM) {
|
|
343
|
+
this.handlers.onNavigation?.(readNavm(this.buffer, chunk.offset, chunk.byteLength));
|
|
344
|
+
}
|
|
345
|
+
else if (chunk.code === CHUNK_ENTS) {
|
|
346
|
+
this.handlers.onEntities?.(readEnts(this.buffer, chunk.offset, chunk.byteLength));
|
|
347
|
+
}
|
|
279
348
|
else if (chunk.code === CHUNK_LITE) {
|
|
280
349
|
this.handlers.onLights?.(readLights(this.buffer, chunk.offset, chunk.byteLength));
|
|
281
350
|
}
|
package/dist/drftWrite.d.ts
CHANGED
|
@@ -19,8 +19,11 @@ import { type DtexEntry } from './dtex.ts';
|
|
|
19
19
|
import { type EntsScene } from './ents.ts';
|
|
20
20
|
import { type NavPolyMesh } from './navm.ts';
|
|
21
21
|
import { type DrftGraph } from './ngrf.ts';
|
|
22
|
+
import type { DrftLightVolume } from './drftLightVolume.ts';
|
|
23
|
+
import type { DrftRegion } from './drftRegions.ts';
|
|
22
24
|
import type { DrftInstanceGroup } from './drftInstances.ts';
|
|
23
25
|
import type { DrftLight } from './drftLights.ts';
|
|
26
|
+
import type { DrftAssembly } from './drftAssembly.ts';
|
|
24
27
|
/** An image to embed, already compressed, with what its own header said about it. */
|
|
25
28
|
export interface DrftTextureSource {
|
|
26
29
|
/**
|
|
@@ -39,7 +42,18 @@ export interface DrftTextureSource {
|
|
|
39
42
|
/** What a caller hands over to be baked. */
|
|
40
43
|
export interface DrftSource {
|
|
41
44
|
readonly head?: Partial<DrftHead>;
|
|
42
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The meshes by ordinal. An assembly — painted copies of kit pieces — stands in a slot like any
|
|
47
|
+
* mesh and is written as one required `MSHC` chunk; see `drftAssembly.ts`.
|
|
48
|
+
*/
|
|
49
|
+
readonly meshes: readonly (MeshData | DrftAssembly)[];
|
|
50
|
+
/**
|
|
51
|
+
* The ordinals of the meshes that are kit pieces, ascending: drawn only as copies inside the
|
|
52
|
+
* assemblies, never on their own. One required `KITS` chunk, ahead of the geometry. A piece
|
|
53
|
+
* comes before every assembly that copies it, is never an assembly itself, and is neither placed
|
|
54
|
+
* by `INST` nor named by a region.
|
|
55
|
+
*/
|
|
56
|
+
readonly kit?: readonly number[];
|
|
43
57
|
/**
|
|
44
58
|
* One material per mesh, by ordinal, or absent for an asset that names none.
|
|
45
59
|
*
|
|
@@ -113,6 +127,19 @@ export interface DrftSource {
|
|
|
113
127
|
* a group names holds one copy; see `drftInstances.ts`.
|
|
114
128
|
*/
|
|
115
129
|
readonly instances?: readonly DrftInstanceGroup[];
|
|
130
|
+
/**
|
|
131
|
+
* The regions of a streamed world: one required `REGN` chunk each, written just ahead of the
|
|
132
|
+
* meshes it introduces. The meshes no region names come first, then each region's new meshes in
|
|
133
|
+
* region order — the writer refuses any other order rather than renumbering what the regions and
|
|
134
|
+
* the materials point at. See `drftRegions.ts`.
|
|
135
|
+
*/
|
|
136
|
+
readonly regions?: readonly DrftRegion[];
|
|
137
|
+
/**
|
|
138
|
+
* A world's fixed lights summed offline into one dense volume: one optional `LVOL` chunk, after
|
|
139
|
+
* the geometry, since the world draws lit by its exact lights until it lands. See
|
|
140
|
+
* `drftLightVolume.ts`.
|
|
141
|
+
*/
|
|
142
|
+
readonly lightVolume?: DrftLightVolume;
|
|
116
143
|
/** The lights the scene was authored with: one `LITE` chunk, optional. See `drftLights.ts`. */
|
|
117
144
|
readonly lights?: readonly DrftLight[];
|
|
118
145
|
/**
|
|
@@ -156,6 +183,13 @@ export interface DrftSource {
|
|
|
156
183
|
* carry geometry with no scene at all — which is every baked model this format has ever held.
|
|
157
184
|
*/
|
|
158
185
|
readonly entities?: EntsScene;
|
|
186
|
+
/**
|
|
187
|
+
* The textures ahead of the geometry rather than after it. A model wants its outline before its
|
|
188
|
+
* pictures, so a viewer sees a shape early; a world whose every region wears the same texture
|
|
189
|
+
* arrays wants the pictures before its first region, or that region waits for the file's end.
|
|
190
|
+
* What it costs a model that asks: its first part arrives later by the size of its pictures.
|
|
191
|
+
*/
|
|
192
|
+
readonly texturesFirst?: boolean;
|
|
159
193
|
}
|
|
160
194
|
/**
|
|
161
195
|
* Bake a `.drft` into one `ArrayBuffer`.
|